tangled
alpha
login
or
join now
hotsocket.fyi
/
domlink
0
fork
atom
https://domlink.deployments.hotsocket.fyi/
0
fork
atom
overview
issues
pulls
pipelines
hyuuge refactor and more desktopping
hotsocket.fyi
6 months ago
7bda9875
97ebf941
+270
-251
13 changed files
expand all
collapse all
unified
split
src
desktop.ts
extras.ts
issuesearch.ts
main.ts
pds.ts
support
atproto.ts
bluesky.ts
constellation.ts
slingshot.ts
tangled.ts
windowing_mod.ts
windows
bluesky
feed.ts
static
desktop.html
+16
-30
src/desktop.ts
···
1
1
-
// yeah screw it ill give the miskey thing a shot
2
2
-
3
3
-
import { Body, Column, Container, Node, Row, Button } from "./domlink.ts";
4
4
-
import { IssueSearch } from "./issuesearch.ts";
1
1
+
import { Body, Button, Column, Input, Row } from "./domlink.ts";
2
2
+
import { resolveMiniDoc } from "./support/slingshot.ts";
5
3
import { Window } from "./windowing_mod.ts";
6
6
-
7
7
-
function webFrame(url: string) {
8
8
-
const frame = document.createElement("iframe");
9
9
-
frame.src = url;
10
10
-
frame.style.width = "100%";
11
11
-
frame.style.height = "100%";
12
12
-
const controls = new Row().with(
13
13
-
"Web Frame Controls:",
14
14
-
new Button("reload", ()=>{frame.contentWindow?.location.reload();})
15
15
-
);
16
16
-
17
17
-
return new Column().with(
18
18
-
controls,
19
19
-
new Node(frame)
20
20
-
).style((s)=>{
21
21
-
s.width = "100%";
22
22
-
s.height = "100%";
23
23
-
});
24
24
-
}
4
4
+
import { Feed } from "./windows/bluesky/feed.ts";
25
5
6
6
+
const userInput = new Input();
7
7
+
const authorFeedWindowCreator = new Row().with(
8
8
+
userInput,
9
9
+
new Button("Get Author Feed", async () => {
10
10
+
const doc = await resolveMiniDoc(userInput.value);
11
11
+
Body.with(new Window("Posts by @"+doc.handle).with(
12
12
+
await new Feed().loadFeed(Feed.createAuthorGenerator(doc.did)),
13
13
+
));
14
14
+
})
15
15
+
);
26
16
const instantiator = new Column().with(
27
27
-
...[IssueSearch].map((x)=>{
28
28
-
return new Button(x.name, ()=>{
29
29
-
Body.with(new Window(x.name).with(new x()));
30
30
-
});
31
31
-
})
32
32
-
)
17
17
+
authorFeedWindowCreator
18
18
+
).style((x) => x.maxWidth = "100ch");
33
19
34
20
Body.with(
35
35
-
instantiator
21
21
+
instantiator,
36
22
);
+29
src/extras.ts
···
1
1
+
import { Row, Button, Column, Node } from "./domlink.ts";
2
2
+
3
3
+
// extra little handy things i guess.
4
4
+
export async function timeout<T>(time: number, promise: Promise<T>): Promise<T> {
5
5
+
const result = await Promise.race([new Promise<null>(r=>setTimeout(r, time, null)), promise]);
6
6
+
if (result == null) {
7
7
+
throw new Error("timeout");
8
8
+
}
9
9
+
return result;
10
10
+
}
11
11
+
12
12
+
function webFrame(url: string) {
13
13
+
const frame = document.createElement("iframe");
14
14
+
frame.src = url;
15
15
+
frame.style.width = "100%";
16
16
+
frame.style.height = "100%";
17
17
+
const controls = new Row().with(
18
18
+
"Web Frame Controls:",
19
19
+
new Button("reload", ()=>{frame.contentWindow?.location.reload();})
20
20
+
);
21
21
+
22
22
+
return new Column().with(
23
23
+
controls,
24
24
+
new Node(frame)
25
25
+
).style((s)=>{
26
26
+
s.width = "100%";
27
27
+
s.height = "100%";
28
28
+
});
29
29
+
}
+62
-99
src/issuesearch.ts
···
1
1
-
import { Body, Button, Column, Input, Label, Link, Row, Table, TableCell, TableRow } from "./domlink.ts";
2
2
-
import { AtURI, AtURIString } from "./support/atproto.ts";
3
3
-
import * as Constellation from "./support/constellation.ts"
1
1
+
import { Body, Button, Input, Label, Link, Node, Row, Table, TableCell, TableRow } from "./domlink.ts";
2
2
+
import { AtURI, AtURIString, GetRecord, ListRecords, XQuery } from "./support/atproto.ts";
3
3
+
import { Issue, Repo } from "./support/tangled.ts";
4
4
+
import * as Constellation from "./support/constellation.ts";
5
5
+
import { MiniDoc } from "./support/slingshot.ts";
4
6
Body.with(new Link("TypeScript source here!").to("https://tangled.sh/@hotsocket.fyi/domlink/blob/main/src/issuesearch.ts"));
5
5
-
type issueRecord = {
6
6
-
body: string;
7
7
-
createdAt: string;
8
8
-
issueId: number;
9
9
-
owner: string;
10
10
-
repo: string;
11
11
-
title: string;
12
12
-
};
13
13
-
type constellationRecordItem = {
14
14
-
did: string;
15
15
-
collection: string;
16
16
-
rkey: string;
17
17
-
};
18
18
-
type constellationResponse = {
19
19
-
total: number;
20
20
-
linking_records: [constellationRecordItem];
21
21
-
cursor: string;
22
22
-
};
23
23
-
type slingshotResponse<T> = {
24
24
-
cid: string;
25
25
-
uri: string;
26
26
-
value: T;
27
27
-
};
28
28
-
type miniDoc = {
29
29
-
did: string;
30
30
-
handle: string;
31
31
-
pds: string;
32
32
-
signing_key: string;
33
33
-
};
34
34
-
type repoRecord = {
35
35
-
knot: string;
36
36
-
name: string;
37
37
-
owner: string;
38
38
-
createdAt: string;
39
39
-
};
7
7
+
40
8
type recordListingItem<T> = {
41
9
uri: string;
42
10
cid: string;
43
11
value: T;
44
12
};
45
45
-
type recordListing<T> = {
46
46
-
cursor: string;
47
47
-
records: [recordListingItem<T>];
48
48
-
};
49
13
50
50
-
async function xcall(host: string, method: string, params: Record<string, string | number> | null = null): Promise<unknown> {
51
51
-
let url = `${host}/xrpc/${method}`;
52
52
-
if (params) {
53
53
-
let usp = new URLSearchParams();
54
54
-
for (let key in params) {
55
55
-
usp.append(key, params[key].toString());
56
56
-
}
57
57
-
url += "?" + usp.toString();
14
14
+
async function timeout<T>(time: number, promise: Promise<T>): Promise<T> {
15
15
+
const result = await Promise.race([new Promise<null>(r=>setTimeout(r, time, null)), promise]);
16
16
+
if (result == null) {
17
17
+
throw new Error("timeout");
58
18
}
59
59
-
return await (await fetch(url)).json();
19
19
+
return result;
60
20
}
61
21
62
62
-
type issueItem = ({handle: string;issue: issueRecord});
22
22
+
type issueItem = ({handle: string;issue: Issue});
63
23
let allIssues:issueItem[] = [];
64
64
-
const SLINGSHOT = "https://slingshot.microcosm.blue";
65
65
-
let currentRepoLink = ""; // essentially for issue links. ensures any extras are stripped off.
24
24
+
let currentRepoLink = ""; // used to build issue links
66
25
async function getIssues(repo: string) {
67
67
-
let repoUrl = new URL(repo);
68
68
-
let repoUrlSplat = repoUrl.pathname.split("/");
69
69
-
let repoOwnerHandle = repoUrlSplat[1].substring(1);
70
70
-
let repoName = repoUrlSplat[2];
71
71
-
currentRepoLink = `https://tangled.sh/@${repoOwnerHandle}/${repoName}`;
26
26
+
// pick out repo info
27
27
+
const repoSplat = repo.split("//");
28
28
+
let repoOwner: string;
29
29
+
let repoName: string;
30
30
+
if (repoSplat.length == 1) {
31
31
+
const repoHalves = repo.split("/");
32
32
+
if (repoHalves.length == 1) throw new Error("invalid repo string");
33
33
+
repoOwner = repoHalves[0];
34
34
+
repoName = repoHalves[1];
35
35
+
} else {
36
36
+
if (repoSplat[0] == "https:") {
37
37
+
const repoUrlSplat = new URL(repo).pathname.split("/");
38
38
+
// [ "", "@tangled.sh", "core" ]
39
39
+
if (repoUrlSplat.length < 2) throw new Error("invalid repo url");
40
40
+
repoOwner = repoUrlSplat[1];
41
41
+
repoName = repoUrlSplat[2];
42
42
+
} else {
43
43
+
throw new Error("unknown repo uri scheme");
44
44
+
}
45
45
+
}
46
46
+
if (repoOwner.startsWith("@")) repoOwner = repoOwner.substring(1);
47
47
+
currentRepoLink = `https://tangled.sh/@${repoOwner}/${repoName}`;
72
48
statusText.text = "resolving owner did";
73
73
-
let repoOwnerDoc = await xcall(SLINGSHOT, "com.bad-example.identity.resolveMiniDoc", {identifier: repoOwnerHandle}) as miniDoc;
49
49
+
const repoOwnerDoc = await XQuery<MiniDoc>("com.bad-example.identity.resolveMiniDoc", {identifier: repoOwner});
74
50
statusText.text = "finding repository";
75
75
-
let ownedRepos = await xcall(repoOwnerDoc.pds, "com.atproto.repo.listRecords", { repo: repoOwnerDoc.did, collection: "sh.tangled.repo" }) as recordListing<repoRecord>;
76
76
-
let repoRecord = ownedRepos.records.find(x=>x.value.name == repoName)!;
77
77
-
// let encodedRepoUri = encodeURIComponent(repoRecord.uri);
51
51
+
const ownedRepos = await ListRecords<Repo>(repoOwnerDoc.did, "sh.tangled.repo", repoOwnerDoc.pds);
52
52
+
const repoRecord = ownedRepos.records.find(x=>x.value.name == repoName)!;
78
53
statusText.text = `finding issues...`;
79
79
-
const allIssueRefs = await Constellation.links(AtURI.fromString(repoRecord.uri as AtURIString), "sh.tangled.repo.issue", ".repo");
80
80
-
// let allIssueRefs: constellationRecordItem[] = irsp.linking_records;
81
81
-
// let nextCursor = irsp.cursor;
82
82
-
// while (allIssueRefs.length < irsp.total && nextCursor != null) {
83
83
-
// statusText.text = `finding issues... (${allIssueRefs.length}/${irsp.total})`;
84
84
-
// let rsp = await (await fetch(`https://constellation.microcosm.blue/links?target=${encodedRepoUri}&collection=sh.tangled.repo.issue&path=.repo&cursor=${nextCursor}`, {headers:{Accept:"application/json"}})).json() as constellationResponse;
85
85
-
// nextCursor = rsp.cursor;
86
86
-
// allIssueRefs = allIssueRefs.concat(rsp.linking_records);
87
87
-
// if (nextCursor == null) break;
88
88
-
// }
54
54
+
const allIssueRefs = await timeout(10000, Constellation.links(AtURI.fromString(repoRecord.uri as AtURIString), "sh.tangled.repo.issue", ".repo"));
55
55
+
let issueinc = 0;
89
56
allIssues = (await Promise.all(allIssueRefs.map(async (issueRef) => {
90
90
-
let rsp = await Promise.race([(xcall(SLINGSHOT, "com.atproto.repo.getRecord", {
91
91
-
repo: issueRef.did,
92
92
-
collection: issueRef.collection,
93
93
-
rkey: issueRef.rkey
94
94
-
}) as slingshotResponse<issueRecord>), new Promise((resolve,_)=>{setTimeout(resolve, 2000, null);})]);
95
95
-
if (rsp == null) {
96
96
-
console.log(`getRecord timed out: at://${issueRef.did}/${issueRef.collection}/${issueRef.rkey}`);
57
57
+
let issue: Issue;
58
58
+
try {
59
59
+
issue = (await timeout(2000, GetRecord<Issue>(issueRef))).value;
60
60
+
} catch { return null; }
61
61
+
if (issue == null) {
62
62
+
console.log(`getRecord timed out: ${issueRef.toString()}`);
97
63
return null;
98
64
}
99
99
-
let issue = rsp.value;
100
65
let doc;
101
66
try {
102
102
-
doc = await xcall(SLINGSHOT, "com.bad-example.identity.resolveMiniDoc", { identifier: issue.owner }) as miniDoc;
103
103
-
} catch (error) {
104
104
-
return null;
105
105
-
}
106
106
-
statusText.text = `retrieved issue #${issue.issueId} (not in order!)`;
107
107
-
let handle = doc.handle;
67
67
+
doc = await XQuery<MiniDoc>("com.bad-example.identity.resolveMiniDoc", { identifier: issue.owner });
68
68
+
} catch { return null; }
69
69
+
statusText.text = `retrieved issue ${++issueinc}/${allIssueRefs.length} (${issue.issueId})`;
108
70
return {
109
109
-
handle: handle,
71
71
+
handle: doc.handle,
110
72
issue: issue
111
73
};
112
74
}))).filter(x=>x as issueItem).sort((a,b)=>{
···
116
78
}
117
79
return x;
118
80
}) as issueItem[];
119
119
-
statusText.text = `got ${allIssues.length} issues!`;
81
81
+
statusText.text = `got ${allIssues.length}/${allIssueRefs.length} issues!`;
120
82
}
121
83
function renderIssues(issues:issueItem[]) {
122
122
-
let view_issues_list = new Table().add(
84
84
+
const view_issues_list = new Table().add(
123
85
new TableRow().with(
124
86
new TableCell().add("Handle"),
125
87
new TableCell().add("Issue#"),
···
128
90
);
129
91
issues.forEach(issue =>{
130
92
if (issue) {
131
131
-
let view_issue_row = new TableRow();
93
93
+
const view_issue_row = new TableRow();
132
94
view_issue_row.with(
133
95
new TableCell().add(new Link(issue.handle).to(`https://tangled.sh/@${issue.handle}`)),
134
96
new TableCell().add(new Link(issue.issue.issueId.toString()).to(`${currentRepoLink}/issues/${issue.issue.issueId}`)),
···
141
103
}
142
104
143
105
144
144
-
let repoUrl = new Input();
145
145
-
let last: any;
146
146
-
let statusText = new Label("waiting");
147
147
-
let runButton = new Button("GO!",async ()=>{
106
106
+
const repoUrl = new Input();
107
107
+
let last: Node | null = null;
108
108
+
const statusText = new Label("waiting");
109
109
+
const runButton = new Button("GO!",async ()=>{
148
110
(runButton.wraps as HTMLButtonElement).disabled = true;
149
111
if (last) {
150
112
try {
151
113
Body.delete(last);
152
152
-
} catch {}
153
153
-
last = null;
114
114
+
} finally {
115
115
+
last = null;
116
116
+
}
154
117
}
155
118
allIssues = [];
156
119
searchRow.style(x=>x.display="none");
···
169
132
170
133
171
134
172
172
-
let searchInput = new Input();
135
135
+
const searchInput = new Input();
173
136
searchInput.watch((search)=>{
174
137
if (allIssues.length > 0) {
175
138
if (last) {
···
186
149
Body.add(last);
187
150
}
188
151
});
189
189
-
let searchRow = new Row().with(
152
152
+
const searchRow = new Row().with(
190
153
"search",
191
154
searchInput
192
155
);
-107
src/main.ts
···
1
1
-
// yeah this is mildly busted so dont get too excited
2
2
-
3
3
-
import { Client, ok, simpleFetchHandler, AtpSessionData, CredentialManager } from "@atcute/client";
4
4
-
import { Body, Button, Column, Container, Image, Input, Label, Modal, Row } from "./domlink.ts";
5
5
-
import { AppBskyFeedDefs, AppBskyFeedPost } from '@atcute/bluesky';
6
6
-
import { CompositeDidDocumentResolver, CompositeHandleResolver, DohJsonHandleResolver, PlcDidDocumentResolver, WebDidDocumentResolver, WellKnownHandleResolver } from "@atcute/identity-resolver";
7
7
-
8
8
-
const handleResolver = new CompositeHandleResolver({
9
9
-
strategy: 'race',
10
10
-
methods: {
11
11
-
dns: new DohJsonHandleResolver({ dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query' }),
12
12
-
http: new WellKnownHandleResolver(),
13
13
-
},
14
14
-
});
15
15
-
const docResolver = new CompositeDidDocumentResolver({
16
16
-
methods: {
17
17
-
plc: new PlcDidDocumentResolver(),
18
18
-
web: new WebDidDocumentResolver(),
19
19
-
},
20
20
-
});
21
21
-
const handler = simpleFetchHandler({service: "https://api.bsky.app"});
22
22
-
const client = new Client({handler});
23
23
-
let manager: CredentialManager | undefined;
24
24
-
const MY_DID = "did:plc:jlplwn5pi4dqrls7i6dx2me7";
25
25
-
26
26
-
class PostView extends Container {
27
27
-
constructor(post: AppBskyFeedDefs.PostView) {
28
28
-
super();
29
29
-
let record: AppBskyFeedPost.Main = post.record as AppBskyFeedPost.Main;
30
30
-
this.with(
31
31
-
new Row().with(
32
32
-
new Image(post.author.avatar ?? "/media/default-avatar.png").class("Avatar"),
33
33
-
new Column().with(
34
34
-
post.author.displayName ?? post.author.handle,
35
35
-
record.text,
36
36
-
new Row().with(
37
37
-
`🗨️ ${post.replyCount} `,
38
38
-
`🔁 ${post.repostCount} `,
39
39
-
`🔃 ${post.quoteCount} `,
40
40
-
`❤️ ${post.likeCount} `,
41
41
-
)
42
42
-
),
43
43
-
)
44
44
-
);
45
45
-
this.wraps.classList.add("PostView");
46
46
-
}
47
47
-
}
48
48
-
async function login() {
49
49
-
let mdl = new Modal();
50
50
-
let handle = new Input("text");
51
51
-
handle.placeholder = "Handle";
52
52
-
let pass = new Input("password");
53
53
-
pass.placeholder = "Password";
54
54
-
let col = new Column().with(
55
55
-
"log in here",
56
56
-
handle,pass,
57
57
-
new Row().with(
58
58
-
new Button("cancel",()=>{mdl.hide();}),
59
59
-
new Button("log in",()=>{
60
60
-
61
61
-
}),
62
62
-
)
63
63
-
);
64
64
-
mdl.add(col).show();
65
65
-
}
66
66
-
67
67
-
class AccountPanel extends Container {
68
68
-
constructor(client: Client) {
69
69
-
super();
70
70
-
this.wraps.classList.add("AccountPanel");
71
71
-
72
72
-
const sessionJson = localStorage.getItem('atp-session');
73
73
-
if (sessionJson) {
74
74
-
const session: AtpSessionData = JSON.parse(sessionJson);
75
75
-
this.with(
76
76
-
new Label(`Logged in as ${session.handle}`),
77
77
-
new Button("Log out", () => {
78
78
-
localStorage.removeItem('atp-session');
79
79
-
window.location.reload();
80
80
-
})
81
81
-
);
82
82
-
} else {
83
83
-
this.add(new Button("Log in", () => {
84
84
-
login();
85
85
-
}));
86
86
-
}
87
87
-
}
88
88
-
}
89
89
-
90
90
-
async function main() {
91
91
-
const postsColumn = new Column().class("PostsColumn");
92
92
-
const mainColumn = new Column().with(
93
93
-
//new AccountPanel(client),
94
94
-
//new Label("who up skeeting they deck"),
95
95
-
postsColumn
96
96
-
);
97
97
-
Body.add(mainColumn);
98
98
-
99
99
-
// Fetch and display the feed
100
100
-
let data = await ok(client.get("app.bsky.feed.getAuthorFeed", {
101
101
-
params: {actor: MY_DID}
102
102
-
}));
103
103
-
const postViews = data.feed.map(post => new PostView(post.post));
104
104
-
postsColumn.with(...postViews);
105
105
-
}
106
106
-
107
107
-
main();
+1
-1
src/pds.ts
···
1
1
-
import { Body, Button, Column, Input, Label, Row } from "./domlink.js";
1
1
+
import { Body, Button, Column, Input, Label, Row } from "./domlink.ts";
2
2
import { DidDocument } from "@atcute/identity";
3
3
4
4
let input_source_pds = [
+49
-2
src/support/atproto.ts
···
1
1
// skinny atproto types file for supporting constellation.ts
2
2
3
3
+
const DEFAULT_SERVICE = "https://api.bsky.app";
4
4
+
3
5
/** Type used to imply that a parameter will be run through {@link ValidateNSID} */
4
6
export type NSID = string;
5
7
const NSIDExpression = /^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z0-9]{0,62})?)$/;
···
7
9
return NSIDExpression.test(nsid) ? nsid : null;
8
10
}
9
11
10
10
-
export type AtURIString = `at://${string}/${string}/${string}`;
12
12
+
export type DID = `did:${"web"|"plc"}:${string}`;
11
13
export function ValidateDID(did: string): string | null {
12
14
const parts = did.split(":");
13
15
const isValid = parts.length == 3 && parts[0] == "did" && (parts[1] == "plc" || parts[1] == "web") && parts[2].length > 0;
14
16
return isValid ? did : null;
15
17
}
18
18
+
19
19
+
export type AtURIString = `at://${string}/${string}/${string}`;
16
20
export class AtURI {
17
21
readonly authority: string | null;
18
22
readonly collection: string | null;
···
39
43
* ```
40
44
*/
41
45
toString(): string | null {
42
42
-
let ret: (string|null)[] = ["at://"];
46
46
+
const ret: (string|null)[] = ["at://"];
43
47
// using `?? ""` to have a "bad" value to find
44
48
if (this.authority) {
45
49
ret.push(this.authority ?? "");
···
57
61
return ret.join("");
58
62
}
59
63
}
64
64
+
65
65
+
export type RecordResponse<T> = {
66
66
+
cid: string;
67
67
+
uri: string;
68
68
+
value: T;
69
69
+
}
70
70
+
71
71
+
// technically you can cast it to whatever you want but i feel like using a generic(?) makes it cleaner
72
72
+
/** Calls an XRPC "query" method (HTTP GET)
73
73
+
* @param service Defaults to {@link https://slingshot.microcosm.blue Slingshot}.
74
74
+
*/
75
75
+
export async function XQuery<T>(method: string, params: Record<string, string | number | null> | null = null, service: string = DEFAULT_SERVICE) {
76
76
+
let QueryURL = `${service}/xrpc/${method}`;
77
77
+
if (params) {
78
78
+
const usp = new URLSearchParams();
79
79
+
for (const key in params) {
80
80
+
if (params[key]) {
81
81
+
usp.append(key, params[key].toString());
82
82
+
}
83
83
+
}
84
84
+
QueryURL += "?" + usp.toString();
85
85
+
}
86
86
+
return (await (await fetch(QueryURL)).json()) as T;
87
87
+
}
88
88
+
/** Calls com.atproto.repo.getRecord with XQuery */
89
89
+
export async function GetRecord<T>(uri: AtURI, service: string = "https://slingshot.microcosm.blue") {
90
90
+
return await XQuery<RecordResponse<T>>("com.atproto.repo.getRecord", {
91
91
+
repo: uri.authority!,
92
92
+
collection: uri.collection!,
93
93
+
rkey: uri.rkey!
94
94
+
}, service);
95
95
+
}
96
96
+
type ListRecordsResponse<T> = {
97
97
+
cursor: string;
98
98
+
records: RecordResponse<T>[];
99
99
+
};
100
100
+
export async function ListRecords<T>(repo: string, collection: NSID, service: string, limit: number = 50) {
101
101
+
return await XQuery<ListRecordsResponse<T>>("com.atproto.repo.listRecords", {
102
102
+
repo: repo,
103
103
+
collection: collection,
104
104
+
limit: limit
105
105
+
}, service);
106
106
+
}
+6
src/support/bluesky.ts
···
1
1
+
import { AppBskyFeedDefs } from "@atcute/bluesky";
2
2
+
3
3
+
export type FeedResponse = {
4
4
+
feed: AppBskyFeedDefs.FeedViewPost[];
5
5
+
cursor: string;
6
6
+
};
+8
-9
src/support/constellation.ts
···
1
1
-
import { AtURI, NSID, ValidateNSID } from "./atproto.ts";
1
1
+
import { AtURI, DID, NSID, ValidateNSID } from "./atproto.ts";
2
2
3
3
-
// constellation api handling
4
3
const BASEURL = "https://constellation.microcosm.blue";
5
4
6
6
-
7
7
-
/** Essentially an at:// URI, fed back by constellation. */
5
5
+
/** Essentially an AtURI, fed back by Constellation. */
8
6
export type Reference = {
9
7
did: string;
10
8
collection: string;
···
12
10
}
13
11
14
12
/** Raw response type from /links */
15
15
-
export type LinksResponse = {
13
13
+
type LinksResponse = {
16
14
total: number;
17
15
linking_records: Reference[];
18
16
cursor: string;
19
17
};
18
18
+
19
19
+
type Target = AtURI | DID;
20
20
/**
21
21
* Retrieves an array of record references to records containing links to the specified target.
22
22
* @throws When the provided NSID is invalid.
23
23
*/
24
24
-
export async function links(target: AtURI, collection: NSID, path: string): Promise<Reference[]> {
24
24
+
export async function links(target: Target, collection: NSID, path: string): Promise<AtURI[]> {
25
25
if (ValidateNSID(collection) == null) {
26
26
throw new Error("invalid NSID for collection parameter");
27
27
}
28
28
const _target = encodeURIComponent(target.toString()!);
29
29
const _path = encodeURIComponent(path);
30
30
let cursor = "";
31
31
-
let records: Reference[] = [];
31
31
+
let records: AtURI[] = [];
32
32
while (cursor != null) {
33
33
const rsp = await fetch(`${BASEURL}/links?target=${_target.toString()}&collection=${collection}&path=${_path}${cursor ? `&cursor=${cursor}` : ""}`);
34
34
const data = await rsp.json() as LinksResponse;
35
35
-
console.log(data.total + " // " + data.cursor);
36
36
-
records = records.concat(data.linking_records);
35
35
+
records = records.concat(data.linking_records.map(x=>new AtURI(x.did, x.collection, x.rkey)));
37
36
cursor = data.cursor;
38
37
}
39
38
return records;
+21
src/support/slingshot.ts
···
1
1
+
import { AtURI, DID, RecordResponse, XQuery } from "./atproto.ts";
2
2
+
3
3
+
const SLINGSHOT = "https://slingshot.microcosm.blue";
4
4
+
5
5
+
export async function resolveHandle(handle: string) {
6
6
+
const res = await XQuery("com.atproto.identity.resolveHandle", {handle: handle}, SLINGSHOT) as {did: DID};
7
7
+
return res.did;
8
8
+
}
9
9
+
10
10
+
export type MiniDoc = {
11
11
+
did: string;
12
12
+
handle: string;
13
13
+
pds: string;
14
14
+
signing_key: string;
15
15
+
};
16
16
+
export async function resolveMiniDoc(identifier: string) {
17
17
+
return await XQuery<MiniDoc>("com.bad-example.identity.resolveMiniDoc", {identifier: identifier}, SLINGSHOT);
18
18
+
}
19
19
+
export async function getUriRecord<T>(at_uri: AtURI, cid: string) {
20
20
+
return await XQuery<RecordResponse<T>>("com.bad-example.repo.getRecord", {at_uri: at_uri.toString()!, cid: cid}, SLINGSHOT);
21
21
+
}
+14
src/support/tangled.ts
···
1
1
+
export type Issue = {
2
2
+
body: string;
3
3
+
createdAt: string;
4
4
+
issueId: number;
5
5
+
owner: string;
6
6
+
repo: string;
7
7
+
title: string;
8
8
+
};
9
9
+
export type Repo = {
10
10
+
knot: string;
11
11
+
name: string;
12
12
+
owner: string;
13
13
+
createdAt: string;
14
14
+
};
+11
-3
src/windowing_mod.ts
···
27
27
titleBar: TitleBar;
28
28
content = new Container().class("LWindowContent");
29
29
30
30
-
constructor(title: string = "New Window") {
30
30
+
constructor(title: string = "New Window", width: number = 400, height: number = 300) {
31
31
super();
32
32
this.class("LWindow");
33
33
this.titleBar = new TitleBar(title, ()=>{this.wraps.remove();});
···
35
35
this.titleBar.wraps.addEventListener("mousedown", this.titleGrabHandler.bind(this));
36
36
this.wraps.addEventListener("mousedown", ()=>{this.wraps.style.zIndex = `${Window.zIndexCounter++}`;});
37
37
this.add(this.content);
38
38
-
this.wraps.style.left = `0px`;
39
39
-
this.wraps.style.top = `0px`;
38
38
+
this.style((s)=>{
39
39
+
s.width = `${width}px`;
40
40
+
s.height = `${height}px`;
41
41
+
s.top = "0px";
42
42
+
s.left = "0px";
43
43
+
});
40
44
}
41
45
override with(...nodes: (Node | string)[]): this {
46
46
+
const w = this.wraps.style.width;
47
47
+
const h = this.wraps.style.height;
42
48
this.content.with(...nodes);
49
49
+
this.wraps.style.width = w;
50
50
+
this.wraps.style.height = h;
43
51
return this;
44
52
}
45
53
+52
src/windows/bluesky/feed.ts
···
1
1
+
// formerly src/main.ts. how far we've come.
2
2
+
3
3
+
import { Column, Container, Image, Row } from "../../domlink.ts";
4
4
+
import { AppBskyFeedDefs, AppBskyFeedPost } from '@atcute/bluesky';
5
5
+
import { XQuery } from "../../support/atproto.ts";
6
6
+
import { FeedResponse as RawFeedResponse } from "../../support/bluesky.ts";
7
7
+
8
8
+
class PostView extends Container {
9
9
+
constructor(fvp: AppBskyFeedDefs.FeedViewPost) {
10
10
+
super();
11
11
+
const post = fvp.post;
12
12
+
const record = post.record as AppBskyFeedPost.Main;
13
13
+
this.with(
14
14
+
new Row().with(
15
15
+
new Image(post.author.avatar ?? "/media/default-avatar.png").class("Avatar"),
16
16
+
new Column().with(
17
17
+
post.author.displayName ?? post.author.handle,
18
18
+
record.text,
19
19
+
new Row().with(
20
20
+
`🗨️ ${post.replyCount} `,
21
21
+
`🔁 ${post.repostCount} `,
22
22
+
`🔃 ${post.quoteCount} `,
23
23
+
`❤️ ${post.likeCount} `,
24
24
+
)
25
25
+
),
26
26
+
)
27
27
+
);
28
28
+
this.wraps.classList.add("PostView");
29
29
+
}
30
30
+
}
31
31
+
32
32
+
type FeedGenerator = (cursor: string | null) => Promise<RawFeedResponse>;
33
33
+
export class Feed extends Column {
34
34
+
generator: FeedGenerator | null = null;
35
35
+
cursor: string | null = null;
36
36
+
constructor() {
37
37
+
super();
38
38
+
this.class("Feed");
39
39
+
}
40
40
+
async loadFeed(generator: FeedGenerator) {
41
41
+
this.generator = generator;
42
42
+
this.children.forEach(x=>this.delete(x));
43
43
+
const data = await this.generator(this.cursor);
44
44
+
this.cursor = data.cursor;
45
45
+
const postViews = data.feed.map(post => new PostView(post));
46
46
+
this.with(...postViews);
47
47
+
return this;
48
48
+
}
49
49
+
static createAuthorGenerator(did: string): FeedGenerator {
50
50
+
return (cursor) => XQuery<RawFeedResponse>("app.bsky.feed.getAuthorFeed", {actor: did, cursor: cursor});
51
51
+
}
52
52
+
}
+1
static/desktop.html
···
5
5
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
<title>desktop</title>
7
7
<link rel="stylesheet" href="styles/domlink.css">
8
8
+
<link rel="stylesheet" href="mine.css">
8
9
</head>
9
10
<body>
10
11
<noscript>enable js or fukc off</noscript>