- {#if thread.branchParentPost}
- {@render replyPost(thread.branchParentPost)}
- {/if}
- {#each thread.posts as post, idx (post.data.uri)}
- {@const mini =
- !expandedThreads.has(thread.rootUri) &&
- thread.posts.length > 4 &&
- idx > 0 &&
- idx < thread.posts.length - 2}
- {#if !mini}
-
- {
- postComposerState.focus = 'focused';
- postComposerState.quoting = post;
- }}
- onReply={(post) => {
- postComposerState.focus = 'focused';
- postComposerState.replying = post;
- }}
- {...post}
- />
-
- {:else if mini}
- {#if idx === 1}
- {@render replyPost(post, !reverseChronological)}
-
- {:else if idx === thread.posts.length - 3}
- {@render replyPost(post)}
- {/if}
- {/if}
- {/each}
-
- {#if i < threads.length - 1}
- 0)}
+ canLoad={!!(client && userDid)}
+ onLoadMore={loadMore}
+ {isComplete}
+/>
diff --git a/src/components/FollowingView.svelte b/src/components/FollowingView.svelte
index 3e17626..b95cb28 100644
--- a/src/components/FollowingView.svelte
+++ b/src/components/FollowingView.svelte
@@ -49,25 +49,12 @@
const interactionScores =
followingSort === 'conversational'
- ? calculateInteractionScores(
- selectedDid,
- followsMap,
- allPosts,
- allBacklinks,
- replyIndex,
- staticNow
- )
+ ? calculateInteractionScores(selectedDid, allPosts, allBacklinks, replyIndex, staticNow)
: null;
- const userStatsList = followsMap.values().map((f) => ({
- did: f.subject,
- data: calculateFollowedUserStats(
- followingSort,
- f.subject,
- allPosts,
- interactionScores,
- staticNow
- )
+ const userStatsList = followsMap.keys().map((did) => ({
+ did,
+ data: calculateFollowedUserStats(followingSort, did, allPosts, interactionScores, staticNow)
}));
const following = userStatsList.filter((u) => u.data !== null);
diff --git a/src/components/GenericTimelineView.svelte b/src/components/GenericTimelineView.svelte
new file mode 100644
index 0000000..07e772a
--- /dev/null
+++ b/src/components/GenericTimelineView.svelte
@@ -0,0 +1,193 @@
+
+
+
+
+{#snippet replyPost(post: ThreadPost, reverse: boolean = reverseChronological)}
+
+ {reverse ? '↱' : '↳'}
+
+
+{/snippet}
+
+{#snippet threadsView()}
+ {#each visibleThreads as thread, i (thread.rootUri)}
+
+ {#if thread.branchParentPost}
+ {@render replyPost(thread.branchParentPost)}
+ {/if}
+ {#each thread.posts as post, idx (post.data.uri)}
+ {@const mini =
+ !expandedThreads.has(thread.rootUri) &&
+ thread.posts.length > 4 &&
+ idx > 0 &&
+ idx < thread.posts.length - 2}
+ {#if !mini}
+
+ {
+ postComposerState.focus = 'focused';
+ postComposerState.quoting = post;
+ }}
+ onReply={(post) => {
+ postComposerState.focus = 'focused';
+ postComposerState.replying = post;
+ }}
+ {...post}
+ />
+
+ {:else if mini}
+ {#if idx === 1}
+ {@render replyPost(post, !reverseChronological)}
+
+ {:else if idx === thread.posts.length - 3}
+ {@render replyPost(post)}
+ {/if}
+ {/if}
+ {/each}
+
+ {#if i < visibleThreads.length - 1}
+
+ {/if}
+ {/each}
+{/snippet}
+
+
+ 0 && boundaryTime !== null && threads[0].newestTime > boundaryTime}
+ onclick={showNewPosts}
+ />
+ {#if isLoggedIn}
+
+ {@render threadsView()}
+ {#snippet noData()}
+
+ {/snippet}
+ {#snippet loading()}
+
+ {/snippet}
+ {#snippet error()}
+
+ {/snippet}
+
+ {:else}
+
+ {/if}
+
diff --git a/src/components/LoadNewPosts.svelte b/src/components/LoadNewPosts.svelte
new file mode 100644
index 0000000..1b057ab
--- /dev/null
+++ b/src/components/LoadNewPosts.svelte
@@ -0,0 +1,22 @@
+
+
+{#if visible}
+
+
+
+{/if}
diff --git a/src/components/PostComposer.svelte b/src/components/PostComposer.svelte
index 3496d25..6055e22 100644
--- a/src/components/PostComposer.svelte
+++ b/src/components/PostComposer.svelte
@@ -322,8 +322,6 @@
};
const doPost = () => {
- if (_state.text.length === 0 || _state.text.length > 300) return;
-
postError = '';
posting = true;
post(_state.text)
diff --git a/src/components/ProfileActions.svelte b/src/components/ProfileActions.svelte
index 9e2a00b..d07cd3b 100644
--- a/src/components/ProfileActions.svelte
+++ b/src/components/ProfileActions.svelte
@@ -8,7 +8,6 @@
createBlock,
deleteBlock,
follows,
- setAccountPreferences,
updateAccountPreferences
} from '$lib/state.svelte';
import { generateColorForDid } from '$lib/accounts';
@@ -33,11 +32,7 @@
let actionsPos = $state({ x: 0, y: 0 });
const followsMap = $derived(userDid ? follows.get(userDid) : undefined);
- const follow = $derived(
- followsMap
- ? Array.from(followsMap.entries()).find(([, follow]) => follow.subject === targetDid)
- : undefined
- );
+ const follow = $derived(followsMap ? followsMap.get(targetDid) : undefined);
const currentPrefs = $derived(userDid ? accountPreferences.get(userDid) : null);
const mutes = $derived(currentPrefs?.mutes ?? []);
@@ -55,8 +50,8 @@
if (!userDid || !client.user) return;
if (follow) {
- const [uri] = follow;
- followsMap?.delete(uri);
+ const { uri } = follow;
+ followsMap?.delete(targetDid);
// extract rkey from uri
const parsedUri = parseCanonicalResourceUri(uri);
@@ -85,8 +80,8 @@
rkey
});
- if (!followsMap) follows.set(userDid, new SvelteMap([[uri, record]]));
- else followsMap.set(uri, record);
+ if (!followsMap) follows.set(userDid, new SvelteMap([[targetDid, { uri, record }]]));
+ else followsMap.set(targetDid, { uri, record });
await client.user.atcute.post('com.atproto.repo.createRecord', {
input: {
diff --git a/src/components/ReplyTimelineView.svelte b/src/components/ReplyTimelineView.svelte
index b597379..c0f9e36 100644
--- a/src/components/ReplyTimelineView.svelte
+++ b/src/components/ReplyTimelineView.svelte
@@ -1,11 +1,7 @@
-{#snippet replyPost(post: ThreadPost, reverse: boolean = reverseChronological)}
-
- {reverse ? '↱' : '↳'}
-
-
-{/snippet}
-
-{#snippet threadsView()}
- {#each threads as thread, i (thread.rootUri)}
-
- {#if thread.branchParentPost}
- {@render replyPost(thread.branchParentPost)}
- {/if}
- {#each thread.posts as post, idx (post.data.uri)}
- {@const mini =
- !expandedThreads.has(thread.rootUri) &&
- thread.posts.length > 4 &&
- idx > 0 &&
- idx < thread.posts.length - 2}
- {#if !mini}
-
- {
- postComposerState.focus = 'focused';
- postComposerState.quoting = post;
- }}
- onReply={(post) => {
- postComposerState.focus = 'focused';
- postComposerState.replying = post;
- }}
- {...post}
- />
-
- {:else if mini}
- {#if idx === 1}
- {@render replyPost(post, !reverseChronological)}
-
- {:else if idx === thread.posts.length - 3}
- {@render replyPost(post)}
- {/if}
- {/if}
- {/each}
-
- {#if i < threads.length - 1}
-
- {/if}
- {/each}
-{/snippet}
-
-
- {#if did || $accounts.length > 0}
-
- {@render threadsView()}
- {#snippet noData()}
-
- {/snippet}
- {#snippet loading()}
-
- {/snippet}
- {#snippet error()}
-
- {/snippet}
-
- {:else}
-
- {/if}
-
+ 0)}
+ canLoad={!!(client && userDid && did)}
+ onLoadMore={loadMore}
+ isComplete={did ? postCursors.get(did)?.end : false}
+/>
diff --git a/src/lib/following.ts b/src/lib/following.ts
index 6b82474..96d9fe4 100644
--- a/src/lib/following.ts
+++ b/src/lib/following.ts
@@ -136,7 +136,6 @@ const getPostRate = (did: Did, posts: Map, now: number
export const calculateInteractionScores = (
user: Did,
- followsMap: Map,
allPosts: Map>,
allBacklinks: Map>>>,
replyIndex: Map>,
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 0f7b29b..59ace5b 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -31,6 +31,7 @@ export const replySource: BacklinksSource = 'app.bsky.feed.post:reply.parent.uri
export const replyRootSource: BacklinksSource = 'app.bsky.feed.post:reply.root.uri';
export const blockSource: BacklinksSource = 'app.bsky.graph.block:subject';
+// returns nanos
export const timestampFromCursor = (cursor: string | undefined) => {
if (!cursor) return undefined;
try {
diff --git a/src/lib/state.svelte.ts b/src/lib/state.svelte.ts
index a25f223..22a22c9 100644
--- a/src/lib/state.svelte.ts
+++ b/src/lib/state.svelte.ts
@@ -348,19 +348,22 @@ export const updateAccountPreferences = async (
await syncAccountPreferences(did);
};
-export const follows = new SvelteMap>();
+type FollowWithUri = {
+ uri: ResourceUri;
+ record: AppBskyGraphFollow.Main;
+};
+export const follows = new SvelteMap>();
export const addFollows = (
did: Did,
- followMap: Iterable<[ResourceUri, AppBskyGraphFollow.Main]>
+ followList: Iterable
) => {
let map = follows.get(did)!;
if (!map) {
- map = new SvelteMap(followMap);
+ map = new SvelteMap();
follows.set(did, map);
- return;
}
- for (const [uri, record] of followMap) map.set(uri, record);
+ for (const follow of followList) map.set(follow.record.subject, follow);
};
export const fetchFollows = async (
@@ -374,7 +377,10 @@ export const fetchFollows = async (
}
addFollows(
account.did,
- res.value.records.map((follow) => [follow.uri, follow.value as AppBskyGraphFollow.Main])
+ res.value.records.map((follow) => ({
+ uri: follow.uri,
+ record: follow.value as AppBskyGraphFollow.Main
+ }))
);
return res.value.records.values().map((follow) => follow.value as AppBskyGraphFollow.Main);
};
@@ -461,10 +467,7 @@ export const fetchFollowingTimeline = async (client: AtpClient, targetDid?: Did,
return;
}
- const followsMap = follows.get(userDid);
- const subjects = new Set();
- if (followsMap)
- for (const follow of followsMap.values()) subjects.add(follow.subject);
+ const subjects = new Set(follows.get(userDid)?.keys());
subjects.add(userDid);
// 2. Find the "newest" cursor(s)
@@ -1000,34 +1003,25 @@ export const handleJetstreamEvent = async (event: JetstreamEvent) => {
addPosts(hydrated.value.values());
addTimeline(did, hydrated.value.keys());
+ if (record.reply) {
+ const parentDid = extractDidFromUri(record.reply.parent.uri)!;
+ addTimeline(parentDid, [uri]);
+ }
+
// Broadcast to following feeds of local accounts
for (const account of get(accounts)) {
// does this account follow the author?
let isFollowing = account.did === did;
if (!isFollowing) {
const accountFollows = follows.get(account.did);
- if (accountFollows) {
- for (const follow of accountFollows.values()) {
- if (follow.subject === did) {
- isFollowing = true;
- break;
- }
- }
- }
+ if (accountFollows?.has(did)) isFollowing = true;
}
if (isFollowing) {
const feed = followingFeed.get(account.did);
- if (feed) {
- for (const uri of hydrated.value.keys()) feed.add(uri);
- }
+ if (feed) for (const uri of hydrated.value.keys()) feed.add(uri);
}
}
-
- if (record.reply) {
- const parentDid = extractDidFromUri(record.reply.parent.uri)!;
- addTimeline(parentDid, [uri]);
- }
} else if (commit.operation === 'delete') {
deletePost(uri);
}
diff --git a/src/lib/thread.ts b/src/lib/thread.ts
index fa7a2bb..396b8b5 100644
--- a/src/lib/thread.ts
+++ b/src/lib/thread.ts
@@ -5,6 +5,7 @@ import type { Account } from './accounts';
import { expect } from './result';
import type { PostWithUri } from './at/fetch';
import { isBlockedBy } from './state.svelte';
+import { timestampFromCursor } from '$lib';
export type ThreadPost = {
data: PostWithUri;
@@ -42,6 +43,7 @@ export const buildThreads = (
const rootUri = (data.record.reply?.root.uri as ResourceUri) || uri;
const parentUri = (data.record.reply?.parent.uri as ResourceUri) || null;
+ const cursorTime = timestampFromCursor(parsedUri.rkey);
const post: ThreadPost = {
data,
account,
@@ -49,7 +51,7 @@ export const buildThreads = (
rkey: parsedUri.rkey,
parentUri,
depth: 0,
- newestTime: new Date(data.record.createdAt).getTime(),
+ newestTime: cursorTime ? cursorTime / 1000 : new Date(data.record.createdAt).getTime(),
isBlocked: isBlockedBy(parsedUri.repo, account),
isMuted: mutes.includes(parsedUri.repo),
};
@@ -169,12 +171,15 @@ export const hasNonOwnPost = (posts: ThreadPost[], accounts: Account[]) =>
export type FilterOptions = {
viewOwnPosts: boolean;
+ filterReplies?: boolean;
filterRootsToDids?: Set;
};
export const filterThreads = (threads: Thread[], accounts: Account[], opts: FilterOptions) =>
threads.filter((thread) => {
if (thread.posts.length === 0) return false;
+ if (opts.filterReplies && thread.posts[0].data.record.reply) return false;
+
if (!opts.viewOwnPosts) if (hasNonOwnPost(thread.posts, accounts)) return false;
if (opts.filterRootsToDids) {
diff --git a/src/routes/[...catchall]/+page.svelte b/src/routes/[...catchall]/+page.svelte
index 5016df9..f9eb676 100644
--- a/src/routes/[...catchall]/+page.svelte
+++ b/src/routes/[...catchall]/+page.svelte
@@ -35,6 +35,7 @@
import type { Sort } from '$lib/following';
import { SvelteMap } from 'svelte/reactivity';
import FeedSelector from '$components/FeedSelector.svelte';
+ import { extractDidFromUri } from '$lib';
const { data: loadData }: PageProps = $props();
@@ -166,9 +167,7 @@
$effect(() => {
const wantedDids: Did[] = ['did:web:guestbook.gaze.systems'];
- const followDids = follows
- .values()
- .flatMap((followMap) => followMap.values().map((follow) => follow.subject));
+ const followDids = follows.values().flatMap((followMap) => followMap.keys());
const accountDids = $accounts.values().map((account) => account.did);
wantedDids.push(...followDids, ...accountDids);
// console.log('updating jetstream options:', wantedDids);
@@ -311,6 +310,10 @@
onPostSent={(post) => {
addPosts([post]);
addTimeline(selectedDid!, [post.uri]);
+ if (post.record.reply) {
+ const parentDid = extractDidFromUri(post.record.reply.parent.uri)!;
+ addTimeline(parentDid, [post.uri]);
+ }
}}
bind:_state={postComposerState}
/>
--
2.51.2