+2
-3
.forgejo/workflows/deploy.yaml
+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
+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
+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
-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
+1
package.json
+2
-2
src/app.css
+2
-2
src/app.css
+63
-6
src/lib/AccountComponent.svelte
+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
+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
+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
+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
+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
+1
-1
themes/express/theme.css
+3
-5
themes/witchcraft/theme.css
+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
+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
+
};