https://domlink.deployments.hotsocket.fyi/

hyuuge refactor and more desktopping

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