+3
-1
src/components/TimelineView.svelte
+3
-1
src/components/TimelineView.svelte
···
63
63
loaderState.status = 'LOADING';
64
64
65
65
try {
66
-
await fetchTimeline(client, did, 7, showReplies);
66
+
await fetchTimeline(client, did, 7, showReplies, {
67
+
downwards: userDid === did ? 'sameAuthor' : 'none'
68
+
});
67
69
// only fetch interactions if logged in (because if not who is the interactor)
68
70
if (client.user && userDid) {
69
71
if (!fetchingInteractions) {
+32
-25
src/lib/at/fetch.ts
+32
-25
src/lib/at/fetch.ts
···
59
59
}
60
60
};
61
61
62
+
export type HydrateOptions = {
63
+
downwards: 'sameAuthor' | 'none';
64
+
};
65
+
62
66
export const hydratePosts = async (
63
67
client: AtpClient,
64
68
repo: Did,
65
69
data: PostWithBacklinks[],
66
-
cacheFn: (did: Did, rkey: RecordKey) => Ok<PostWithUri> | undefined
70
+
cacheFn: (did: Did, rkey: RecordKey) => Ok<PostWithUri> | undefined,
71
+
options?: Partial<HydrateOptions>
67
72
): Promise<Result<Map<ResourceUri, PostWithUri>, string>> => {
68
73
let posts: Map<ResourceUri, PostWithUri> = new Map();
69
74
try {
···
115
120
};
116
121
await Promise.all(posts.values().map(fetchUpwardsChain));
117
122
118
-
try {
119
-
const fetchDownwardsChain = async (post: PostWithUri) => {
120
-
const { repo: postRepo } = expect(parseCanonicalResourceUri(post.uri));
121
-
if (repo === postRepo) return;
123
+
if (options?.downwards !== 'none') {
124
+
try {
125
+
const fetchDownwardsChain = async (post: PostWithUri) => {
126
+
const { repo: postRepo } = expect(parseCanonicalResourceUri(post.uri));
127
+
if (repo === postRepo) return;
122
128
123
-
// get chains that are the same author until we exhaust them
124
-
const backlinks = await client.getBacklinks(post.uri, replySource);
125
-
if (!backlinks.ok) return;
129
+
// get chains that are the same author until we exhaust them
130
+
const backlinks = await client.getBacklinks(post.uri, replySource);
131
+
if (!backlinks.ok) return;
126
132
127
-
const promises = [];
128
-
for (const reply of backlinks.value.records) {
129
-
if (reply.did !== postRepo) continue;
130
-
// if we already have this reply, then we already fetched this chain / are fetching it
131
-
if (posts.has(toCanonicalUri(reply))) continue;
132
-
const record =
133
-
cacheFn(reply.did, reply.rkey) ??
134
-
(await client.getRecord(AppBskyFeedPost.mainSchema, reply.did, reply.rkey));
135
-
if (!record.ok) break; // TODO: this doesnt handle deleted posts in between
136
-
posts.set(record.value.uri, record.value);
137
-
promises.push(fetchDownwardsChain(record.value));
138
-
}
133
+
const promises = [];
134
+
for (const reply of backlinks.value.records) {
135
+
if (reply.did !== postRepo) continue;
136
+
// if we already have this reply, then we already fetched this chain / are fetching it
137
+
if (posts.has(toCanonicalUri(reply))) continue;
138
+
const record =
139
+
cacheFn(reply.did, reply.rkey) ??
140
+
(await client.getRecord(AppBskyFeedPost.mainSchema, reply.did, reply.rkey));
141
+
if (!record.ok) break; // TODO: this doesnt handle deleted posts in between
142
+
posts.set(record.value.uri, record.value);
143
+
promises.push(fetchDownwardsChain(record.value));
144
+
}
139
145
140
-
await Promise.all(promises);
141
-
};
142
-
await Promise.all(posts.values().map(fetchDownwardsChain));
143
-
} catch (error) {
144
-
return err(`cant fetch post reply chain: ${error}`);
146
+
await Promise.all(promises);
147
+
};
148
+
await Promise.all(posts.values().map(fetchDownwardsChain));
149
+
} catch (error) {
150
+
return err(`cant fetch post reply chain: ${error}`);
151
+
}
145
152
}
146
153
147
154
return ok(posts);
+33
-14
src/lib/state.svelte.ts
+33
-14
src/lib/state.svelte.ts
···
7
7
} from './at/client.svelte';
8
8
import { SvelteMap, SvelteDate, SvelteSet } from 'svelte/reactivity';
9
9
import type { Did, Handle, Nsid, RecordKey, ResourceUri } from '@atcute/lexicons';
10
-
import { fetchPosts, hydratePosts, type PostWithUri } from './at/fetch';
10
+
import { fetchPosts, hydratePosts, type HydrateOptions, type PostWithUri } from './at/fetch';
11
11
import { parseCanonicalResourceUri, type AtprotoDid } from '@atcute/lexicons/syntax';
12
12
import {
13
13
AppBskyActorProfile,
···
406
406
};
407
407
408
408
export const allPosts = new SvelteMap<Did, SvelteMap<ResourceUri, PostWithUri>>();
409
+
export type DeletedPostInfo = { reply?: PostWithUri['record']['reply'] };
410
+
export const deletedPosts = new SvelteMap<ResourceUri, DeletedPostInfo>();
409
411
// did -> post uris that are replies to that did
410
412
export const replyIndex = new SvelteMap<Did, SvelteSet<ResourceUri>>();
411
413
···
448
450
}
449
451
};
450
452
453
+
export const deletePost = (uri: ResourceUri) => {
454
+
const did = extractDidFromUri(uri)!;
455
+
const post = allPosts.get(did)?.get(uri);
456
+
if (!post) return;
457
+
allPosts.get(did)?.delete(uri);
458
+
// remove reply from index
459
+
const subjectDid = extractDidFromUri(post.record.reply?.parent.uri ?? '');
460
+
if (subjectDid) replyIndex.get(subjectDid)?.delete(uri);
461
+
deletedPosts.set(uri, { reply: post.record.reply });
462
+
};
463
+
451
464
export const timelines = new SvelteMap<Did, SvelteSet<ResourceUri>>();
452
465
export const postCursors = new SvelteMap<Did, { value?: string; end: boolean }>();
453
466
···
479
492
client: AtpClient,
480
493
subject: Did,
481
494
limit: number = 6,
482
-
withBacklinks: boolean = true
495
+
withBacklinks: boolean = true,
496
+
hydrateOptions?: Partial<HydrateOptions>
483
497
) => {
484
498
const cursor = postCursors.get(subject);
485
499
if (cursor && cursor.end) return;
···
490
504
// if the cursor is undefined, we've reached the end of the timeline
491
505
const newCursor = { value: accPosts.value.cursor, end: !accPosts.value.cursor };
492
506
postCursors.set(subject, newCursor);
493
-
const hydrated = await hydratePosts(client, subject, accPosts.value.posts, hydrateCacheFn);
507
+
const hydrated = await hydratePosts(
508
+
client,
509
+
subject,
510
+
accPosts.value.posts,
511
+
hydrateCacheFn,
512
+
hydrateOptions
513
+
);
494
514
if (!hydrated.ok) throw `cant hydrate posts ${subject}: ${hydrated.error}`;
495
515
496
516
addPosts(hydrated.value.values());
···
543
563
const uri: ResourceUri = toCanonicalUri({ did, ...commit });
544
564
if (commit.collection === 'app.bsky.feed.post') {
545
565
if (commit.operation === 'create') {
566
+
const record = commit.record as AppBskyFeedPost.Main;
546
567
const posts = [
547
568
{
548
-
record: commit.record as AppBskyFeedPost.Main,
569
+
record,
549
570
uri,
550
571
cid: commit.cid
551
572
}
552
573
];
553
-
await setRecordCache(uri, commit.record);
574
+
await setRecordCache(uri, record);
554
575
const client = clients.get(did) ?? viewClient;
555
576
const hydrated = await hydratePosts(client, did, posts, hydrateCacheFn);
556
577
if (!hydrated.ok) {
···
559
580
}
560
581
addPosts(hydrated.value.values());
561
582
addTimeline(did, hydrated.value.keys());
583
+
if (record.reply) {
584
+
const parentDid = extractDidFromUri(record.reply.parent.uri)!;
585
+
addTimeline(parentDid, [uri]);
586
+
// const rootDid = extractDidFromUri(record.reply.root.uri)!;
587
+
// addTimeline(rootDid, [uri]);
588
+
}
562
589
} else if (commit.operation === 'delete') {
563
-
const post = allPosts.get(did)?.get(uri);
564
-
if (post) {
565
-
allPosts.get(did)?.delete(uri);
566
-
// remove from timeline
567
-
timelines.get(did)?.delete(uri);
568
-
// remove reply from index
569
-
const subjectDid = extractDidFromUri(post.record.reply?.parent.uri ?? '');
570
-
if (subjectDid) replyIndex.get(subjectDid)?.delete(uri);
571
-
}
590
+
deletePost(uri);
572
591
}
573
592
}
574
593
};