A fork of pds-dash for selfhosted.social

idk i found the deno lint and fmt commands

+2 -3
.forgejo/workflows/deploy.yaml
··· 6 6 - main 7 7 - astra/ci 8 8 9 - 10 9 jobs: 11 10 deploy: 12 11 name: Deploy ··· 25 24 26 25 - name: Copy config file to root 27 26 run: cp overrides/config.ts ./config.ts 28 - 27 + 29 28 - name: Setup Node.js 30 29 uses: actions/setup-node@v3 31 30 with: 32 - node-version: '20' 31 + node-version: "20" 33 32 34 33 - name: Setup Deno 35 34 uses: https://github.com/denoland/setup-deno@v2
+25 -12
README.md
··· 1 1 # pds-dash 2 2 3 - A fork of [pds-dash](https://git.witchcraft.systems/scientific-witchery/pds-dash) for [selfhosted.social](https://selfhosted.social). The top part of the readme is about this fork. 4 - See after [Original Readme](#original-readme) to see the original readme for setup 3 + A fork of 4 + [pds-dash](https://git.witchcraft.systems/scientific-witchery/pds-dash) for 5 + [selfhosted.social](https://selfhosted.social). The top part of the readme is 6 + about this fork. See after [Original Readme](#original-readme) to see the 7 + original readme for setup 5 8 6 9 This fork is much the same but a few differences: 10 + 7 11 - [New theme](/themes/dark/theme.css) 8 - - Uses the CDN for loading images and videos instead of `com.atproto.sync.getBlob` 9 - - Caches a couple of things like did -> handle and PDS user profile lexicon inside localstorage. Not the best, but was simpler and has a expire on get. 12 + - Uses the CDN for loading images and videos instead of 13 + `com.atproto.sync.getBlob` 14 + - Caches a couple of things like did -> handle and PDS user profile lexicon 15 + inside localstorage. Not the best, but was simpler and has a expire on get. 10 16 - The text "Home to x accounts" only shows active accounts. 11 17 - I did add a sponsor button for my GitHub. 12 18 13 - An example of a caddy file you can use 19 + An example of a caddy file you can use 20 + 14 21 ```caddyfile 15 22 # Should be all the endpoints a PDS calls 16 23 @pds { ··· 31 38 try_files {path} /index.html 32 39 file_server 33 40 } 34 - 35 41 ``` 36 - 37 42 38 43 # Original Readme 39 44 ··· 47 52 48 53 ### installing 49 54 50 - clone the repo, copy `config.ts.example` to `config.ts` and edit it to your liking. 55 + clone the repo, copy `config.ts.example` to `config.ts` and edit it to your 56 + liking. 51 57 52 58 then, install dependencies using deno: 53 59 ··· 75 81 76 82 ## deploying 77 83 78 - we use our own CI/CD workflow at [`.forgejo/workflows/deploy.yaml`](.forgejo/workflows/deploy.yaml), but it boils down to building the project bundle and deploying it to a web server. it'll probably make more sense to host it on the same domain as your PDS, but it doesn't affect anything if you host it somewhere else. 84 + we use our own CI/CD workflow at 85 + [`.forgejo/workflows/deploy.yaml`](.forgejo/workflows/deploy.yaml), but it boils 86 + down to building the project bundle and deploying it to a web server. it'll 87 + probably make more sense to host it on the same domain as your PDS, but it 88 + doesn't affect anything if you host it somewhere else. 79 89 80 90 ## configuring 81 91 82 - [`config.ts`](config.ts) is the main configuration file, you can find more information in the file itself. 92 + [`config.ts`](config.ts) is the main configuration file, you can find more 93 + information in the file itself. 83 94 84 95 ## theming 85 96 86 - themes are located in the `themes/` directory, you can create your own theme by copying one of the existing themes and modifying it to your liking. 97 + themes are located in the `themes/` directory, you can create your own theme by 98 + copying one of the existing themes and modifying it to your liking. 87 99 88 - currently, the name of the theme is determined by the directory name, and the theme itself is defined in `theme.css` inside that directory. 100 + currently, the name of the theme is determined by the directory name, and the 101 + theme itself is defined in `theme.css` inside that directory. 89 102 90 103 you can switch themes by changing the `theme` property in `config.ts`. 91 104
+69
deno.lock
··· 4 4 "npm:@atcute/bluesky@^2.0.2": "2.0.2_@atcute+client@3.0.1", 5 5 "npm:@atcute/client@^3.0.1": "3.0.1", 6 6 "npm:@atcute/identity-resolver@~0.1.2": "0.1.2_@atcute+identity@0.1.3", 7 + "npm:@atproto/api@~0.16.9": "0.16.9", 7 8 "npm:@sveltejs/vite-plugin-svelte@^5.0.3": "5.0.3_svelte@5.28.1__acorn@8.14.1_vite@6.3.2__picomatch@4.0.2", 8 9 "npm:@tsconfig/svelte@^5.0.4": "5.0.4", 9 10 "npm:hls.js@^1.6.12": "1.6.12", ··· 52 53 "@badrap/valita" 53 54 ] 54 55 }, 56 + "@atproto/api@0.16.9": { 57 + "integrity": "sha512-hXbnBIDEIwXxxyduxxZsf0aP8Z+JKyfG7L47FZqAYOI6uNm8oBTLLrHQ2RmJZZeyMIMM17gvxNtPDoULKQfupw==", 58 + "dependencies": [ 59 + "@atproto/common-web", 60 + "@atproto/lexicon", 61 + "@atproto/syntax", 62 + "@atproto/xrpc", 63 + "await-lock", 64 + "multiformats", 65 + "tlds", 66 + "zod" 67 + ] 68 + }, 69 + "@atproto/common-web@0.4.3": { 70 + "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", 71 + "dependencies": [ 72 + "graphemer", 73 + "multiformats", 74 + "uint8arrays", 75 + "zod" 76 + ] 77 + }, 78 + "@atproto/lexicon@0.5.1": { 79 + "integrity": "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==", 80 + "dependencies": [ 81 + "@atproto/common-web", 82 + "@atproto/syntax", 83 + "iso-datestring-validator", 84 + "multiformats", 85 + "zod" 86 + ] 87 + }, 88 + "@atproto/syntax@0.4.1": { 89 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==" 90 + }, 91 + "@atproto/xrpc@0.7.5": { 92 + "integrity": "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA==", 93 + "dependencies": [ 94 + "@atproto/lexicon", 95 + "zod" 96 + ] 97 + }, 55 98 "@badrap/valita@0.4.4": { 56 99 "integrity": "sha512-GEhUCk9c4XbNxi+0YZHZsV4fYNd6HejfWuN4Ti4c02DauX+LyX5WY1Y3WfyZ8Pxxl0zqhs+MLtW98cMh86vv6g==" 57 100 }, ··· 345 388 "aria-query@5.3.2": { 346 389 "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" 347 390 }, 391 + "await-lock@2.2.2": { 392 + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==" 393 + }, 348 394 "axobject-query@4.1.0": { 349 395 "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" 350 396 }, ··· 421 467 "os": ["darwin"], 422 468 "scripts": true 423 469 }, 470 + "graphemer@1.4.0": { 471 + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" 472 + }, 424 473 "hls.js@1.6.12": { 425 474 "integrity": "sha512-Pz+7IzvkbAht/zXvwLzA/stUHNqztqKvlLbfpq6ZYU68+gZ+CZMlsbQBPUviRap+3IQ41E39ke7Ia+yvhsehEQ==" 426 475 }, ··· 430 479 "@types/estree" 431 480 ] 432 481 }, 482 + "iso-datestring-validator@2.2.2": { 483 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" 484 + }, 433 485 "kleur@4.1.5": { 434 486 "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" 435 487 }, ··· 450 502 }, 451 503 "ms@2.1.3": { 452 504 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 505 + }, 506 + "multiformats@9.9.0": { 507 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" 453 508 }, 454 509 "mutex-ts@1.2.1": { 455 510 "integrity": "sha512-OkcXgf0viuCgYdnm48kiNQ9PzC5OzISQ261svHr/Ybc2vBYC/5xfLXn44hQ+dYRX74v7MCSqV/LKPEbpYdDybw==" ··· 556 611 "picomatch" 557 612 ] 558 613 }, 614 + "tlds@1.260.0": { 615 + "integrity": "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==", 616 + "bin": true 617 + }, 559 618 "typescript@5.7.3": { 560 619 "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", 561 620 "bin": true 621 + }, 622 + "uint8arrays@3.0.0": { 623 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 624 + "dependencies": [ 625 + "multiformats" 626 + ] 562 627 }, 563 628 "vite@6.3.2_picomatch@4.0.2": { 564 629 "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", ··· 586 651 }, 587 652 "zimmerframe@1.1.2": { 588 653 "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==" 654 + }, 655 + "zod@3.25.76": { 656 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" 589 657 } 590 658 }, 591 659 "workspace": { ··· 594 662 "npm:@atcute/bluesky@^2.0.2", 595 663 "npm:@atcute/client@^3.0.1", 596 664 "npm:@atcute/identity-resolver@~0.1.2", 665 + "npm:@atproto/api@~0.16.9", 597 666 "npm:@sveltejs/vite-plugin-svelte@^5.0.3", 598 667 "npm:@tsconfig/svelte@^5.0.4", 599 668 "npm:hls.js@^1.6.12",
+4 -2
index.html
··· 4 4 <meta charset="UTF-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 6 <title>Selfhosted.social</title> 7 - <meta name="description" content="Landing page for selfhosted.social, a ATProto PDS"> 8 - 7 + <meta 8 + name="description" 9 + content="Landing page for selfhosted.social, an ATProto PDS" 10 + > 9 11 </head> 10 12 <body> 11 13 <div id="app"></div>
+1
package.json
··· 13 13 "@atcute/bluesky": "^2.0.2", 14 14 "@atcute/client": "^3.0.1", 15 15 "@atcute/identity-resolver": "^0.1.2", 16 + "@atproto/api": "^0.16.9", 16 17 "hls.js": "^1.6.12", 17 18 "moment": "^2.30.1", 18 19 "mutex-ts": "^1.2.1",
+2 -2
src/app.css
··· 1 - @import url('./themes/colors.css'); 1 + @import url("./themes/colors.css"); 2 2 body { 3 3 background-color: red; 4 - } 4 + }
+63 -6
src/lib/AccountComponent.svelte
··· 32 32 src="https://cdn.bsky.app/img/feed_thumbnail/plain/{account.did}/{account.avatarCid}@jpeg" 33 33 /> 34 34 </span> 35 - <div id="accountName"> 36 - {account.displayName || account.handle || account.did} 35 + <div class="name-block" style="margin-left: 12px;"> 36 + <div id="accountName" style="margin-left: 0;"> 37 + {account.displayName || account.handle || account.did} 38 + </div> 39 + {#if nowListeningTo} 40 + <div class="now-playing"><span>{nowListeningTo}</span></div> 41 + {/if} 37 42 </div> 38 43 {:else} 39 44 <span class="avatar-wrapper"> ··· 47 52 src="/unknown.png" 48 53 /> 49 54 </span> 50 - <div id="accountName" class="no-avatar"> 51 - {account.displayName || account.handle || account.did} 55 + <div class="name-block" style="margin-left: 12px;"> 56 + <div id="accountName" style="margin-left: 0;"> 57 + {account.displayName || account.handle || account.did} 58 + </div> 59 + {#if nowListeningTo} 60 + <div class="now-playing"><span>{nowListeningTo}</span></div> 61 + {/if} 52 62 </div> 53 63 {/if} 54 64 </div> 55 65 </a> 56 66 57 67 <style> 58 - .avatar-wrapper { position: relative; display: inline-block; } 59 - .avatar-badge { position: absolute; top: 34px; left: 40px; font-size: 24px; line-height: 1; } 68 + .avatar-wrapper { 69 + position: relative; 70 + display: inline-block; 71 + } 72 + 73 + .avatar-badge { 74 + position: absolute; 75 + top: 34px; 76 + left: 40px; 77 + font-size: 24px; 78 + line-height: 1; 79 + } 80 + 81 + .name-block { 82 + display: flex; 83 + flex-direction: column; 84 + min-width: 0; 85 + flex: 1; 86 + } 87 + 88 + #accountName { 89 + max-width: 100%; 90 + overflow: hidden; 91 + text-overflow: ellipsis; 92 + white-space: nowrap; 93 + } 94 + 95 + .now-playing { 96 + font-size: 0.8em; 97 + opacity: 0.8; 98 + margin-top: 2px; 99 + white-space: nowrap; 100 + overflow: hidden; 101 + } 102 + 103 + .now-playing > span { 104 + display: inline-block; 105 + padding-left: 100%; 106 + animation: account-now-playing-marquee 25s linear infinite; 107 + } 108 + 109 + @keyframes account-now-playing-marquee { 110 + from { 111 + transform: translateX(0); 112 + } 113 + to { 114 + transform: translateX(-100%); 115 + } 116 + } 60 117 </style>
+13 -1
src/lib/PostComponent.svelte
··· 136 136 .quotingUri.rkey}">quoting {post.quotingUri.repo}</a 137 137 > 138 138 {/if} 139 - <div id="postText">{post.text}</div> 139 + <div id="postText"> 140 + {#each post.richText.segments() as segment} 141 + {#if segment.mention} 142 + <a href="{Config.FRONTEND_URL}/profile/{segment.mention.did}" 143 + >{segment.text}</a 144 + > 145 + {:else if segment.link} 146 + <a style="text-decoration: underline" href="{segment.link.uri}">{segment.text}</a> 147 + {:else if segment.text} 148 + {segment.text} 149 + {/if} 150 + {/each} 151 + </div> 140 152 {#if post.imagesCid && post.imagesCid.length > 0} 141 153 <div id="carouselContainer"> 142 154 <img
+103 -95
src/lib/pdsfetch.ts
··· 1 1 import { simpleFetchHandler, XRPC } from "@atcute/client"; 2 2 import "@atcute/bluesky/lexicons"; 3 3 import type { 4 - AppBskyActorDefs, 5 4 AppBskyActorProfile, 5 + AppBskyEmbedImages, 6 6 AppBskyFeedPost, 7 7 At, 8 8 ComAtprotoRepoListRecords, ··· 12 12 PlcDidDocumentResolver, 13 13 WebDidDocumentResolver, 14 14 } from "@atcute/identity-resolver"; 15 - import { Config } from "../../config"; 16 - import { Mutex } from "mutex-ts" 15 + import { Config } from "../../config.ts"; 16 + import { Mutex } from "mutex-ts"; 17 17 import moment from "moment"; 18 - import type {DidDocument} from "@atcute/client/utils/did"; 18 + import { RichText } from "@atproto/api"; 19 + 19 20 // import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons"; 20 21 // import { AppBskyFeedPost } from "@atcute/client/lexicons"; 21 22 // import { AppBskyActorDefs } from "@atcute/client/lexicons"; ··· 50 51 imagesCid: string[] | null; 51 52 videosLinkCid: string | null; 52 53 gifLink: string | null; 54 + richText: RichText; 53 55 54 56 constructor( 55 57 record: ComAtprotoRepoListRecords.Record, 56 58 account: AccountMetadata, 59 + richText: RichText, 57 60 ) { 61 + this.richText = richText; 58 62 this.postCid = record.cid; 59 63 this.recordName = processAtUri(record.uri).rkey; 60 64 this.authorDid = account.did; ··· 77 81 switch (post.embed?.$type) { 78 82 case "app.bsky.embed.images": 79 83 this.imagesCid = post.embed.images.map( 80 - (imageRecord: any) => imageRecord.image.ref.$link, 84 + (imageRecord: AppBskyEmbedImages.Image) => 85 + imageRecord.image.ref.$link, 81 86 ); 82 87 break; 83 88 case "app.bsky.embed.video": ··· 119 124 }; 120 125 }; 121 126 122 - 123 127 const rpc = new XRPC({ 124 128 handler: simpleFetchHandler({ 125 129 service: Config.PDS_URL, 126 130 }), 127 131 }); 128 132 129 - const slingShot = new XRPC({ 130 - handler: simpleFetchHandler({ 131 - service: "https://slingshot.microcosm.blue", 132 - }), 133 - }); 134 - 135 133 const getDidsFromPDS = async (): Promise<At.Did[]> => { 136 134 const { data } = await rpc.get("com.atproto.sync.listRepos", { 137 135 params: { 138 - limit: 1000, 136 + limit: 1000, 139 137 }, 140 138 }); 141 - return data.repos.filter(x => x.active).map((repo: any) => repo.did).reverse() as At.Did[]; 139 + return data.repos.filter((x) => x.active).map((repo: Repo) => repo.did) 140 + .reverse() as At.Did[]; 142 141 }; 143 142 const getAccountMetadata = async ( 144 143 did: `did:${string}:${string}`, ··· 213 212 const localStorageKey = `did-handle:${did}`; 214 213 const cachedResult = cacheGet<string>(localStorageKey); 215 214 if (cachedResult) { 216 - return cachedResult; 215 + return cachedResult; 217 216 } 218 217 const doc = await identityResolve(did); 219 218 if (doc.alsoKnownAs) { ··· 267 266 return postDate >= cutoffDate; 268 267 }); 269 268 if (filtered.length > 0) { 270 - postAcc.account.currentCursor = processAtUri(filtered[filtered.length - 1].uri).rkey; 269 + postAcc.account.currentCursor = 270 + processAtUri(filtered[filtered.length - 1].uri).rkey; 271 271 } 272 272 return { 273 273 posts: filtered, ··· 318 318 account.currentCursor = postAcc.account.currentCursor; 319 319 } 320 320 return account; 321 - } 322 - ); 321 + }); 323 322 // throw the records in a big single array 324 323 let records = recordsCutoff.flatMap((postAcc) => postAcc.posts); 325 324 // sort the records by timestamp ··· 352 351 `Account with DID ${processAtUri(record.uri).repo} not found`, 353 352 ); 354 353 } 355 - return new Post(record, account); 354 + const post = record.value as AppBskyFeedPost.Record; 355 + const richText = new RichText({ text: post.text, facets: post.facets }); 356 + 357 + return new Post(record, account, richText); 356 358 }); 357 359 // release the mutex 358 360 release(); ··· 377 379 }; 378 380 379 381 type artists = { 380 - artistName: string; 381 - } 382 + artistName: string; 383 + }; 382 384 383 385 type dietTeal = { 384 - artists: artists[]; 385 - trackName: string; 386 - playedTime: number; 387 - } 386 + artists: artists[]; 387 + trackName: string; 388 + playedTime: number; 389 + }; 388 390 389 - const getTealNowListeningTo = async (did: At.Did) => { 390 - const { data } = await rpc.get("com.atproto.repo.listRecords", { 391 - params: { 392 - repo: did as At.Identifier, 393 - collection: "fm.teal.alpha.feed.play", 394 - limit: 1 395 - }, 396 - }); 397 - if (data.records.length > 0) { 398 - const record = data.records[0] as ComAtprotoRepoListRecords.Record; 399 - const value = record.value as dietTeal; 400 - const artists = value.artists.map((artist) => artist.artistName).join(", "); 401 - const timeStamp = moment(value.playedTime).isBefore(moment().subtract(1, "month")) 402 - ? moment(value.playedTime).format("MMM D, YYYY") 403 - : moment(value.playedTime).fromNow() 404 - return `Listening to ${value.trackName} by ${artists} ${timeStamp}`; 405 - } 406 - console.log(data); 407 - return null; 408 - } 391 + const getTealNowListeningTo = async (did: At.Did) => { 392 + const { data } = await rpc.get("com.atproto.repo.listRecords", { 393 + params: { 394 + repo: did as At.Identifier, 395 + collection: "fm.teal.alpha.feed.play", 396 + limit: 1, 397 + }, 398 + }); 399 + if (data.records.length > 0) { 400 + const record = data.records[0] as ComAtprotoRepoListRecords.Record; 401 + const value = record.value as dietTeal; 402 + const artists = value.artists.map((artist) => artist.artistName).join(", "); 403 + const timeStamp = 404 + moment(value.playedTime).isBefore(moment().subtract(1, "month")) 405 + ? moment(value.playedTime).format("MMM D, YYYY") 406 + : moment(value.playedTime).fromNow(); 407 + return `Listening to ${value.trackName} by ${artists} ${timeStamp}`; 408 + } 409 + console.log(data); 410 + return null; 411 + }; 409 412 410 413 type statusSphere = { 411 - status: string; 412 - } 414 + status: string; 415 + }; 413 416 414 417 const getStatusSphere = async (did: At.Did) => { 415 - const { data } = await rpc.get("com.atproto.repo.listRecords", { 416 - params: { 417 - repo: did as At.Identifier, 418 - collection: "xyz.statusphere.status", 419 - limit: 1 420 - }, 421 - }); 422 - if (data.records.length > 0) { 423 - const record = data.records[0].value as statusSphere; 424 - return record.status; 425 - } 426 - return null; 427 - } 418 + const { data } = await rpc.get("com.atproto.repo.listRecords", { 419 + params: { 420 + repo: did as At.Identifier, 421 + collection: "xyz.statusphere.status", 422 + limit: 1, 423 + }, 424 + }); 425 + if (data.records.length > 0) { 426 + const record = data.records[0].value as statusSphere; 427 + return record.status; 428 + } 429 + return null; 430 + }; 428 431 429 432 type CacheEntry<T> = { 430 - data: T; 431 - expire_timestamp: number; 432 - } 433 - 433 + data: T; 434 + expire_timestamp: number; 435 + }; 434 436 435 437 const cacheSet = <T>(key: string, value: T) => { 436 - try{ 437 - const day = 60 * 60 * 24 * 1000; 438 - const cacheData: CacheEntry<T> = { 439 - data: value, 440 - expire_timestamp: Date.now() + day 441 - } 442 - localStorage.setItem(key, JSON.stringify(cacheData)); 443 - } 444 - catch(e){ 445 - console.error("Error caching data:", e); 446 - //Going just clear the cache and assume it's full. 447 - localStorage.clear(); 448 - } 449 - } 438 + try { 439 + const day = 60 * 60 * 24 * 1000; 440 + const cacheData: CacheEntry<T> = { 441 + data: value, 442 + expire_timestamp: Date.now() + day, 443 + }; 444 + localStorage.setItem(key, JSON.stringify(cacheData)); 445 + } catch (e) { 446 + console.error("Error caching data:", e); 447 + //Going just clear the cache and assume it's full. 448 + localStorage.clear(); 449 + } 450 + }; 450 451 451 452 const cacheGet = <T>(key: string): T | null => { 452 - try{ 453 - const cachedData = localStorage.getItem(key); 454 - if (cachedData) { 455 - const parsedData = JSON.parse(cachedData) as CacheEntry<T>; 456 - if (parsedData.expire_timestamp > Date.now() ) { 457 - return parsedData.data; 458 - } else { 459 - localStorage.removeItem(key); 460 - } 461 - } 462 - //Return null if empty or expired 463 - return null; 464 - }catch(e){ 465 - console.error("Error fetching data from cache:", e); 466 - return null; 453 + try { 454 + const cachedData = localStorage.getItem(key); 455 + if (cachedData) { 456 + const parsedData = JSON.parse(cachedData) as CacheEntry<T>; 457 + if (parsedData.expire_timestamp > Date.now()) { 458 + return parsedData.data; 459 + } else { 460 + localStorage.removeItem(key); 461 + } 467 462 } 468 - } 463 + //Return null if empty or expired 464 + return null; 465 + } catch (e) { 466 + console.error("Error fetching data from cache:", e); 467 + return null; 468 + } 469 + }; 469 470 470 - export { getAllMetadataFromPds, getNextPosts, Post, blueskyHandleFromDid, getTealNowListeningTo, getStatusSphere }; 471 + export { 472 + blueskyHandleFromDid, 473 + getAllMetadataFromPds, 474 + getNextPosts, 475 + getStatusSphere, 476 + getTealNowListeningTo, 477 + Post, 478 + }; 471 479 export type { AccountMetadata };
+27 -8
themes/dark/theme.css
··· 43 43 --header-background-color: var(--color-base-200); 44 44 --content-background-color: var(--color-base-200); 45 45 --text-color: var(--color-base-content); 46 - --text-secondary-color: color-mix(in oklab, var(--color-base-content) 70%, var(--color-base-100)); 46 + --text-secondary-color: color-mix( 47 + in oklab, 48 + var(--color-base-content) 70%, 49 + var(--color-base-100) 50 + ); 47 51 --border-color: var(--color-base-300); 48 52 --link-color: var(--color-primary); 49 53 --link-hover-color: var(--color-primary-content); ··· 52 56 --indicator-active-color: var(--color-primary); 53 57 54 58 /* Subtle hover background for dark */ 55 - --button-hover: color-mix(in oklab, var(--color-base-200) 80%, var(--color-base-content)); 59 + --button-hover: color-mix( 60 + in oklab, 61 + var(--color-base-200) 80%, 62 + var(--color-base-content) 63 + ); 56 64 } 57 - 58 65 59 66 body { 60 67 margin: 0; ··· 63 70 min-width: 320px; 64 71 min-height: 100vh; 65 72 background-color: var(--background-color); 66 - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; 73 + font-family: 74 + "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, 75 + Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 67 76 font-size: 18px; 68 77 line-height: 1.5; 69 78 color: var(--text-color); ··· 113 122 114 123 #postContainer:hover { 115 124 transform: translateY(-2px); 116 - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 125 + box-shadow: 126 + 0 10px 15px -3px rgba(0, 0, 0, 0.1), 127 + 0 4px 6px -2px rgba(0, 0, 0, 0.05); 117 128 } 118 129 119 130 #postHeader { ··· 320 331 object-fit: cover; 321 332 border-radius: 50%; 322 333 border: 2px solid white; 323 - box-shadow: 0 1px 3px rgba(0,0,0,0.1); 334 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 324 335 } 325 336 326 337 /* App.Svelte Layout */ ··· 501 512 --header-background-color: var(--color-base-200); 502 513 --content-background-color: var(--color-base-200); 503 514 --text-color: var(--color-base-content); 504 - --text-secondary-color: color-mix(in oklab, var(--color-base-content) 70%, var(--color-base-100)); 515 + --text-secondary-color: color-mix( 516 + in oklab, 517 + var(--color-base-content) 70%, 518 + var(--color-base-100) 519 + ); 505 520 --border-color: var(--color-base-300); 506 521 --link-color: var(--color-primary); 507 522 --link-hover-color: var(--color-primary-content); ··· 510 525 --indicator-active-color: var(--color-primary); 511 526 512 527 /* Subtle hover background for dark */ 513 - --button-hover: color-mix(in oklab, var(--color-base-200) 80%, var(--color-base-content)); 528 + --button-hover: color-mix( 529 + in oklab, 530 + var(--color-base-200) 80%, 531 + var(--color-base-content) 532 + ); 514 533 }
+12 -9
themes/default/theme.css
··· 14 14 --border-color: #e2e8f0; 15 15 --indicator-inactive-color: #cbd5e1; 16 16 --indicator-active-color: #6366f1; 17 - 17 + 18 18 /* Modern shadows */ 19 19 --button-hover: #f3f4f6; 20 20 } 21 - 22 21 23 22 body { 24 23 margin: 0; ··· 27 26 min-width: 320px; 28 27 min-height: 100vh; 29 28 background-color: var(--background-color); 30 - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; 29 + font-family: 30 + "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, 31 + Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 31 32 font-size: 18px; 32 33 line-height: 1.5; 33 34 color: var(--text-color); ··· 77 78 78 79 #postContainer:hover { 79 80 transform: translateY(-2px); 80 - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 81 + box-shadow: 82 + 0 10px 15px -3px rgba(0, 0, 0, 0.1), 83 + 0 4px 6px -2px rgba(0, 0, 0, 0.05); 81 84 } 82 85 83 86 #postHeader { ··· 284 287 object-fit: cover; 285 288 border-radius: 50%; 286 289 border: 2px solid white; 287 - box-shadow: 0 1px 3px rgba(0,0,0,0.1); 290 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 288 291 } 289 292 290 293 /* App.Svelte Layout */ ··· 357 360 padding: 12px; 358 361 margin-top: 0; 359 362 } 360 - 363 + 361 364 #Account { 362 365 width: calc(100% - 32px); 363 366 padding: 16px; ··· 367 370 height: auto; 368 371 order: -1; 369 372 } 370 - 373 + 371 374 #Feed { 372 375 width: 100%; 373 376 margin: 0; 374 377 padding: 0; 375 378 overflow-y: visible; 376 379 } 377 - 380 + 378 381 #spacer { 379 382 height: 5vh; 380 383 } ··· 420 423 -ms-overflow-style: none; /* IE and Edge */ 421 424 -webkit-overflow-scrolling: touch; 422 425 -webkit-scrollbar: none; /* Safari */ 423 - } 426 + }
+1 -1
themes/express/theme.css
··· 251 251 } 252 252 253 253 .no-avatar { 254 - margin-left: 40px !important; 254 + margin-left: 40px !important; 255 255 } 256 256 257 257 /* App.Svelte */
+3 -5
themes/witchcraft/theme.css
··· 6 6 :root { 7 7 /* Color overrides, edit to whatever you want */ 8 8 --primary-h: 260; /* Hue */ 9 - 9 + 10 10 --link-color: hsl(calc(var(--primary-h) - 30), 75%, 60%); 11 11 --link-hover-color: hsl(calc(var(--primary-h) - 30), 75%, 50%); 12 12 --background-color: hsl(var(--primary-h), 75%, 10%); ··· 17 17 --indicator-inactive-color: #4a4a4a; 18 18 --indicator-active-color: var(--border-color); 19 19 } 20 - 21 20 22 21 a { 23 22 font-weight: 500; ··· 248 247 white-space: nowrap; 249 248 } 250 249 251 - 252 250 .no-avatar { 253 - margin-left: 70px !important; 251 + margin-left: 70px !important; 254 252 } 255 253 256 254 /* App.Svelte */ ··· 370 368 -ms-overflow-style: none; /* IE and Edge */ 371 369 -webkit-overflow-scrolling: touch; 372 370 -webkit-scrollbar: none; /* Safari */ 373 - } 371 + }
+30 -27
theming.ts
··· 1 - import { Plugin } from 'vite'; 2 - import { Config } from './config'; 3 - 1 + import { Plugin } from "vite"; 2 + import { Config } from "./config"; 4 3 5 4 // Replaces app.css with the contents of the file specified in the 6 5 // config file. 7 6 export const themePlugin = (): Plugin => { 8 - const themeFolder = Config.THEME; 9 - console.log(`Using theme folder: ${themeFolder}`); 10 - return { 11 - name: 'theme-generator', 12 - enforce: 'pre', // Ensure this plugin runs first 13 - transform(code, id) { 14 - if (id.endsWith('app.css')) { 15 - // Read the theme file and replace the contents of app.css with it 16 - // Needs full path to the file 17 - const themeCode = Deno.readTextFileSync(Deno.cwd() + '/themes/' + themeFolder + '/theme.css'); 18 - // Replace the contents of app.css with the theme code 7 + const themeFolder = Config.THEME; 8 + console.log(`Using theme folder: ${themeFolder}`); 9 + return { 10 + name: "theme-generator", 11 + enforce: "pre", // Ensure this plugin runs first 12 + transform(_code, id) { 13 + if (id.endsWith("app.css")) { 14 + // Read the theme file and replace the contents of app.css with it 15 + // Needs full path to the file 16 + //@ts-ignore Deno 17 + const themeCode = Deno.readTextFileSync( 18 + //@ts-ignore Deno 19 + Deno.cwd() + "/themes/" + themeFolder + "/theme.css", 20 + ); 21 + // Replace the contents of app.css with the theme code 19 22 20 - // and add a comment at the top 21 - const themeComment = `/* Generated from ${themeFolder} */\n`; 22 - const themeCodeWithComment = themeComment + themeCode; 23 - // Return the theme code as the new contents of app.css 24 - return { 25 - code: themeCodeWithComment, 26 - map: null, 27 - }; 28 - } 29 - return null; 30 - } 31 - }; 32 - }; 23 + // and add a comment at the top 24 + const themeComment = `/* Generated from ${themeFolder} */\n`; 25 + const themeCodeWithComment = themeComment + themeCode; 26 + // Return the theme code as the new contents of app.css 27 + return { 28 + code: themeCodeWithComment, 29 + map: null, 30 + }; 31 + } 32 + return null; 33 + }, 34 + }; 35 + };
+1 -1
vite.config.ts
··· 1 1 import { defineConfig } from "vite"; 2 2 import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 - import { themePlugin } from "./theming"; 3 + import { themePlugin } from "./theming.ts"; 4 4 5 5 // https://vite.dev/config/ 6 6 export default defineConfig({