diff --git a/deno.lock b/deno.lock
index 0121c80..89f0ed7 100644
--- a/deno.lock
+++ b/deno.lock
@@ -8,6 +8,7 @@
"npm:@atcute/client@^4.1.1": "4.1.1",
"npm:@atcute/identity-resolver@^1.2.1": "1.2.1_@atcute+identity@1.1.3",
"npm:@atcute/identity@^1.1.3": "1.1.3",
+ "npm:@atcute/jetstream@^1.1.2": "1.1.2",
"npm:@atcute/lexicons@^1.2.5": "1.2.5",
"npm:@atcute/oauth-browser-client@^2.0.3": "2.0.3_@atcute+identity@1.1.3",
"npm:@atcute/tid@^1.0.3": "1.0.3",
@@ -23,6 +24,7 @@
"npm:@tailwindcss/vite@^4.1.18": "4.1.18_vite@7.3.0__@types+node@25.0.3__picomatch@4.0.3_@types+node@25.0.3",
"npm:@types/node@^25.0.3": "25.0.3",
"npm:@wora/cache-persist@^2.2.1": "2.2.1",
+ "npm:async-cache-dedupe@^3.4.0": "3.4.0",
"npm:eslint-config-prettier@^10.1.8": "10.1.8_eslint@9.39.2",
"npm:eslint-plugin-svelte@^3.13.1": "3.13.1_eslint@9.39.2_svelte@5.46.1__acorn@8.15.0_postcss@8.5.6",
"npm:eslint@^9.39.2": "9.39.2",
@@ -94,6 +96,18 @@
"@badrap/valita"
]
},
+ "@atcute/jetstream@1.1.2": {
+ "integrity": "sha512-u6p/h2xppp7LE6W/9xErAJ6frfN60s8adZuCKtfAaaBBiiYbb1CfpzN8Uc+2qtJZNorqGvuuDb5572Jmh7yHBQ==",
+ "dependencies": [
+ "@atcute/lexicons",
+ "@badrap/valita",
+ "@mary-ext/event-iterator",
+ "@mary-ext/simple-event-emitter",
+ "partysocket",
+ "type-fest",
+ "yocto-queue@1.2.2"
+ ]
+ },
"@atcute/lexicons@1.2.5": {
"integrity": "sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q==",
"dependencies": [
@@ -405,6 +419,15 @@
"@jridgewell/sourcemap-codec"
]
},
+ "@mary-ext/event-iterator@1.0.0": {
+ "integrity": "sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ==",
+ "dependencies": [
+ "yocto-queue@1.2.2"
+ ]
+ },
+ "@mary-ext/simple-event-emitter@1.0.0": {
+ "integrity": "sha512-meA/zJZKIN1RVBNEYIbjufkUrW7/tRjHH60FjolpG1ixJKo76TB208qefQLNdOVDA7uIG0CGEDuhmMirtHKLAg=="
+ },
"@polka/url@1.0.0-next.29": {
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="
},
@@ -843,6 +866,13 @@
"aria-query@5.3.2": {
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="
},
+ "async-cache-dedupe@3.4.0": {
+ "integrity": "sha512-RkQr21CpltqMpbYpRaEAmF1BdUO5jnnS/scZkectmLiuWQ81w8u4lYraipbQf8zQ0yYvb3U0N1ozNAYmI4jQ3g==",
+ "dependencies": [
+ "mnemonist",
+ "safe-stable-stringify"
+ ]
+ },
"axobject-query@4.1.0": {
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="
},
@@ -1087,6 +1117,9 @@
"esutils@2.0.3": {
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
},
+ "event-target-polyfill@0.0.4": {
+ "integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="
+ },
"fast-deep-equal@3.1.3": {
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
@@ -1344,6 +1377,12 @@
"brace-expansion@2.0.2"
]
},
+ "mnemonist@0.40.3": {
+ "integrity": "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==",
+ "dependencies": [
+ "obliterator"
+ ]
+ },
"mri@1.2.0": {
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="
},
@@ -1364,6 +1403,9 @@
"natural-compare@1.4.0": {
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
},
+ "obliterator@2.0.5": {
+ "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw=="
+ },
"optionator@0.9.4": {
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dependencies": [
@@ -1378,7 +1420,7 @@
"p-limit@3.1.0": {
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dependencies": [
- "yocto-queue"
+ "yocto-queue@0.1.0"
]
},
"p-locate@5.0.0": {
@@ -1393,6 +1435,12 @@
"callsites"
]
},
+ "partysocket@1.1.10": {
+ "integrity": "sha512-ACfn0P6lQuj8/AqB4L5ZDFcIEbpnIteNNObrlxqV1Ge80GTGhjuJ2sNKwNQlFzhGi4kI7fP/C1Eqh8TR78HjDQ==",
+ "dependencies": [
+ "event-target-polyfill"
+ ]
+ },
"path-exists@4.0.0": {
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
},
@@ -1514,6 +1562,9 @@
"mri"
]
},
+ "safe-stable-stringify@2.5.0": {
+ "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="
+ },
"semver@7.7.3": {
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"bin": true
@@ -1658,6 +1709,9 @@
"prelude-ls"
]
},
+ "type-fest@4.41.0": {
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="
+ },
"typescript-eslint@8.50.1_eslint@9.39.2_typescript@5.9.3_@typescript-eslint+parser@8.50.1__eslint@9.39.2__typescript@5.9.3": {
"integrity": "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==",
"dependencies": [
@@ -1729,6 +1783,9 @@
"yocto-queue@0.1.0": {
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
},
+ "yocto-queue@1.2.2": {
+ "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="
+ },
"zimmerframe@1.1.4": {
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="
}
@@ -1743,6 +1800,7 @@
"npm:@atcute/client@^4.1.1",
"npm:@atcute/identity-resolver@^1.2.1",
"npm:@atcute/identity@^1.1.3",
+ "npm:@atcute/jetstream@^1.1.2",
"npm:@atcute/lexicons@^1.2.5",
"npm:@atcute/oauth-browser-client@^2.0.3",
"npm:@atcute/tid@^1.0.3",
@@ -1758,6 +1816,7 @@
"npm:@tailwindcss/vite@^4.1.18",
"npm:@types/node@^25.0.3",
"npm:@wora/cache-persist@^2.2.1",
+ "npm:async-cache-dedupe@^3.4.0",
"npm:eslint-config-prettier@^10.1.8",
"npm:eslint-plugin-svelte@^3.13.1",
"npm:eslint@^9.39.2",
diff --git a/package.json b/package.json
index dcade0d..5db45f0 100644
--- a/package.json
+++ b/package.json
@@ -21,12 +21,14 @@
"@atcute/client": "^4.1.1",
"@atcute/identity": "^1.1.3",
"@atcute/identity-resolver": "^1.2.1",
+ "@atcute/jetstream": "^1.1.2",
"@atcute/lexicons": "^1.2.5",
"@atcute/oauth-browser-client": "^2.0.3",
"@atcute/tid": "^1.0.3",
"@floating-ui/dom": "^1.7.4",
"@soffinal/websocket": "^0.2.1",
"@wora/cache-persist": "^2.2.1",
+ "async-cache-dedupe": "^3.4.0",
"hash-wasm": "^4.12.0",
"lru-cache": "^11.2.4",
"svelte-device-info": "^1.0.6",
diff --git a/src/components/AccountSelector.svelte b/src/components/AccountSelector.svelte
index c05b581..9863164 100644
--- a/src/components/AccountSelector.svelte
+++ b/src/components/AccountSelector.svelte
@@ -1,6 +1,6 @@
+
+
+
+
+
+ {#each ['recent', 'active'] as Sort[] as type (type)}
+
+ {/each}
+
+
+
+
+ {#if sortedFollowing.length === 0}
+
+ {:else}
+ {#each sortedFollowing as user (user.did)}
+ {@const lastPostAt = user.lastPostAt}
+ {@const relTime = getRelativeTime(lastPostAt)}
+ {@const color = generateColorForDid(user.did)}
+ {@const isHighlighted = highlightedDid === user.did}
+ {@const displayName = getClient(user.did as AtprotoDid)
+ .then((client) => client.getProfile())
+ .then((profile) => {
+ if (profile.ok) return profile.value.displayName;
+ return null;
+ })}
+ {@const handle = resolveDidDoc(user.did).then((doc) => {
+ if (doc.ok) return doc.value.handle;
+ return 'handle.invalid';
+ })}
+
+
(highlightedDid = user.did)}
+ onmouseleave={() => (highlightedDid = undefined)}
+ >
+
+
+
+ {#await Promise.all([displayName, handle]) then [displayName, handle]}
+ {displayName || handle}
+ @{handle}
+ {/await}
+
+
+
+ posted {relTime}
+ {relTime !== 'now' ? 'ago' : ''}
+
+ {#if user.postsInBurst > 0}
+
+ {user.postsInBurst} posts / 1h
+
+ {/if}
+
+
+
+ {/each}
+ {/if}
+
+
diff --git a/src/components/PostComposer.svelte b/src/components/PostComposer.svelte
index 56e8e6f..b51fdcb 100644
--- a/src/components/PostComposer.svelte
+++ b/src/components/PostComposer.svelte
@@ -36,7 +36,7 @@
});
// Parse rich text (mentions, links, tags)
- const rt = await parseToRichText(client, text);
+ const rt = await parseToRichText(text);
const record: AppBskyFeedPost.Main = {
$type: 'app.bsky.feed.post',
diff --git a/src/components/ProfilePicture.svelte b/src/components/ProfilePicture.svelte
index 86e2e02..c28834c 100644
--- a/src/components/ProfilePicture.svelte
+++ b/src/components/ProfilePicture.svelte
@@ -29,6 +29,7 @@
{#if isBlob(record.avatar)}
- import type { AtpClient } from '$lib/at/client';
import { parseToRichText } from '$lib/richtext';
import { settings } from '$lib/settings';
import type { BakedRichtext } from '@atcute/bluesky-richtext-builder';
@@ -8,13 +7,12 @@
interface Props {
text: string;
facets?: Facet[];
- client: AtpClient;
}
- const { text, facets, client }: Props = $props();
+ const { text, facets }: Props = $props();
const richtext: Promise = $derived(
- facets ? Promise.resolve({ text, facets }) : parseToRichText(client, text)
+ facets ? Promise.resolve({ text, facets }) : parseToRichText(text)
);
diff --git a/src/components/SettingsView.svelte b/src/components/SettingsView.svelte
index 31b8ee7..7d8906d 100644
--- a/src/components/SettingsView.svelte
+++ b/src/components/SettingsView.svelte
@@ -1,10 +1,10 @@
-{#snippet divider()}
-
-{/snippet}
-
{#snippet advancedTab()}
@@ -62,6 +56,7 @@
{@render _input('slingshot', 'slingshot url (for fetching records & resolving identity)')}
{@render _input('spacedust', 'spacedust url (for notifications)')}
{@render _input('constellation', 'constellation url (for backlinks)')}
+ {@render _input('jetstream', 'jetstream url (for real-time updates)')}
@@ -161,7 +156,9 @@
- {#each tabs as tab, idx (tab)}
+ {#each tabs as tab (tab)}
{@const isActive = activeTab === tab}