+1
-1
config.ts
+1
-1
config.ts
+6
-2
src/App.svelte
+6
-2
src/App.svelte
···
1
1
<script lang="ts">
2
2
import PostComponent from "./lib/PostComponent.svelte";
3
3
import AccountComponent from "./lib/AccountComponent.svelte";
4
-
import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
4
+
import { getNextPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
5
5
import { Config } from "../config";
6
-
const postsPromise = fetchAllPosts();
6
+
const postsPromise = getNextPosts();
7
7
const accountsPromise = getAllMetadataFromPds();
8
8
</script>
9
9
···
12
12
{#await accountsPromise}
13
13
<p>Loading...</p>
14
14
{:then accountsData}
15
+
15
16
<div id="Account">
16
17
<h1 id="Header">ATProto PDS</h1>
17
18
<p>Home to {accountsData.length} accounts</p>
···
29
30
{#await postsPromise}
30
31
<p>Loading...</p>
31
32
{:then postsData}
33
+
<button on:click={getNextPosts}>
34
+
Load more posts
35
+
</button>
32
36
<div id="Feed">
33
37
<div id="spacer"></div>
34
38
{#each postsData as postObject}
+178
-40
src/lib/pdsfetch.ts
+178
-40
src/lib/pdsfetch.ts
···
18
18
// import { AppBskyActorDefs } from "@atcute/client/lexicons";
19
19
20
20
interface AccountMetadata {
21
-
did: string;
21
+
did: At.Did;
22
22
displayName: string;
23
23
handle: string;
24
24
avatarCid: string | null;
25
+
currentCursor?: string;
25
26
}
27
+
28
+
let accountsMetadata: AccountMetadata[] = [];
29
+
// a chronologically sorted list of posts for all users, that will be shown by svelte
30
+
// getNextPosts will populate this list with additional posts as needed
31
+
let posts: Post[] = [];
26
32
interface atUriObject {
27
33
repo: string;
28
34
collection: string;
···
45
51
46
52
constructor(
47
53
record: ComAtprotoRepoListRecords.Record,
48
-
account: AccountMetadata
54
+
account: AccountMetadata,
49
55
) {
50
56
this.postCid = record.cid;
51
57
this.recordName = processAtUri(record.uri).rkey;
···
68
74
switch (post.embed?.$type) {
69
75
case "app.bsky.embed.images":
70
76
this.imagesCid = post.embed.images.map(
71
-
(imageRecord: any) => imageRecord.image.ref.$link
77
+
(imageRecord: any) => imageRecord.image.ref.$link,
72
78
);
73
79
break;
74
80
case "app.bsky.embed.video":
···
82
88
switch (post.embed.media.$type) {
83
89
case "app.bsky.embed.images":
84
90
this.imagesCid = post.embed.media.images.map(
85
-
(imageRecord) => imageRecord.image.ref.$link
91
+
(imageRecord) => imageRecord.image.ref.$link,
86
92
);
87
93
88
94
break;
···
118
124
return data.repos.map((repo: any) => repo.did) as At.Did[];
119
125
};
120
126
const getAccountMetadata = async (
121
-
did: `did:${string}:${string}`
122
-
): Promise<AccountMetadata> => {
127
+
did: `did:${string}:${string}`,
128
+
) => {
123
129
// gonna assume self exists in the app.bsky.actor.profile
124
130
try {
125
131
const { data } = await rpc.get("com.atproto.repo.getRecord", {
···
143
149
return account;
144
150
} catch (e) {
145
151
console.error(`Error fetching metadata for ${did}:`, e);
146
-
return {
147
-
did: "error",
148
-
displayName: "",
149
-
avatarCid: null,
150
-
handle: "error",
151
-
};
152
+
return null;
152
153
}
153
154
};
154
155
···
157
158
const metadata = await Promise.all(
158
159
dids.map(async (repo: `did:${string}:${string}`) => {
159
160
return await getAccountMetadata(repo);
160
-
})
161
+
}),
161
162
);
162
-
return metadata.filter((account) => account.did !== "error");
163
+
return metadata.filter((account) => account !== null) as AccountMetadata[];
163
164
};
164
165
166
+
// OLD
165
167
const fetchPosts = async (did: string) => {
166
168
try {
167
169
const { data } = await rpc.get("com.atproto.repo.listRecords", {
···
196
198
197
199
if (did.startsWith("did:plc:") || did.startsWith("did:web:")) {
198
200
const doc = await resolver.resolve(
199
-
did as `did:plc:${string}` | `did:web:${string}`
201
+
did as `did:plc:${string}` | `did:web:${string}`,
200
202
);
201
203
return doc;
202
204
} else {
···
219
221
}
220
222
};
221
223
222
-
const fetchAllPosts = async () => {
223
-
const users: AccountMetadata[] = await getAllMetadataFromPds();
224
-
const postRecords = await Promise.all(
225
-
users.map(
226
-
async (metadata: AccountMetadata) => await fetchPosts(metadata.did)
227
-
)
228
-
);
229
-
const validPostRecords = postRecords.filter((record) => !record.error);
230
-
const posts: Post[] = validPostRecords.flatMap((userFetch) =>
231
-
userFetch.records.map((record) => {
232
-
const user = users.find(
233
-
(user: AccountMetadata) => user.did == userFetch.did
224
+
interface PostsAcc {
225
+
posts: ComAtprotoRepoListRecords.Record[];
226
+
account: AccountMetadata;
227
+
}
228
+
const getCutoffDate = (postAccounts: PostsAcc[]) => {
229
+
const now = Date.now();
230
+
let cutoffDate: Date | null = null;
231
+
postAccounts.forEach((postAcc) => {
232
+
const latestPost = new Date(
233
+
(postAcc.posts[postAcc.posts.length - 1].value as AppBskyFeedPost.Record)
234
+
.createdAt,
235
+
);
236
+
if (!cutoffDate) {
237
+
cutoffDate = latestPost;
238
+
} else {
239
+
if (latestPost > cutoffDate) {
240
+
cutoffDate = latestPost;
241
+
}
242
+
}
243
+
});
244
+
if (cutoffDate) {
245
+
console.log("Cutoff date:", cutoffDate);
246
+
return cutoffDate;
247
+
} else {
248
+
return new Date(now);
249
+
}
250
+
};
251
+
252
+
const filterPostsByDate = (posts: PostsAcc[], cutoffDate: Date) => {
253
+
// filter posts for each account that are older than the cutoff date and save the cursor of the last post included
254
+
const filteredPosts: PostsAcc[] = posts.map((postAcc) => {
255
+
const filtered = postAcc.posts.filter((post) => {
256
+
const postDate = new Date(
257
+
(post.value as AppBskyFeedPost.Record).createdAt,
234
258
);
235
-
if (!user) {
236
-
throw new Error(`User with DID ${userFetch.did} not found`);
259
+
return postDate >= cutoffDate;
260
+
});
261
+
if (filtered.length > 0) {
262
+
postAcc.account.currentCursor = filtered[filtered.length - 1].cid;
263
+
}
264
+
return {
265
+
posts: filtered,
266
+
account: postAcc.account,
267
+
};
268
+
});
269
+
return filteredPosts;
270
+
};
271
+
const getNextPosts = async () => {
272
+
if (!accountsMetadata.length) {
273
+
accountsMetadata = await getAllMetadataFromPds();
274
+
}
275
+
276
+
const postsAcc: PostsAcc[] = await Promise.all(
277
+
accountsMetadata.map(async (account) => {
278
+
const posts = await fetchPostsForUser(
279
+
account.did,
280
+
account.currentCursor || null,
281
+
);
282
+
if (posts) {
283
+
return {
284
+
posts: posts,
285
+
account: account,
286
+
};
287
+
} else {
288
+
return {
289
+
posts: [],
290
+
account: account,
291
+
};
237
292
}
238
-
return new Post(record, user);
239
-
})
293
+
}),
240
294
);
241
-
242
-
posts.sort((a, b) => b.timestamp - a.timestamp);
243
-
244
-
if(!Config.SHOW_FUTURE_POSTS) {
245
-
// Filter out posts that are in the future
295
+
const recordsFiltered = postsAcc.filter((postAcc) =>
296
+
postAcc.posts.length > 0
297
+
);
298
+
const cutoffDate = getCutoffDate(recordsFiltered);
299
+
const recordsCutoff = filterPostsByDate(recordsFiltered, cutoffDate);
300
+
// update the accountMetadata with the new cursor
301
+
accountsMetadata = recordsCutoff.map((postAcc) => postAcc.account);
302
+
// throw the records in a big single array
303
+
let records = recordsCutoff.flatMap((postAcc) => postAcc.posts);
304
+
// sort the records by timestamp
305
+
records = records.sort((a, b) => {
306
+
const aDate = new Date(
307
+
(a.value as AppBskyFeedPost.Record).createdAt,
308
+
).getTime();
309
+
const bDate = new Date(
310
+
(b.value as AppBskyFeedPost.Record).createdAt,
311
+
).getTime();
312
+
return bDate - aDate;
313
+
}
314
+
);
315
+
// filter out posts that are in the future
316
+
if (!Config.SHOW_FUTURE_POSTS) {
246
317
const now = Date.now();
247
-
const filteredPosts = posts.filter((post) => post.timestamp <= now);
248
-
return filteredPosts.slice(0, Config.MAX_POSTS);
318
+
records = records.filter((post) => {
319
+
const postDate = new Date(
320
+
(post.value as AppBskyFeedPost.Record).createdAt,
321
+
).getTime();
322
+
return postDate <= now;
323
+
});
249
324
}
325
+
// append the new posts to the existing posts
326
+
posts = posts.concat(
327
+
records.map((record) => {
328
+
const account = accountsMetadata.find(
329
+
(account) => account.did == processAtUri(record.uri).repo,
330
+
);
331
+
if (!account) {
332
+
throw new Error(`Account with DID ${processAtUri(record.uri).repo} not found`);
333
+
}
334
+
return new Post(record, account);
335
+
}),
336
+
);
337
+
console.log("Fetched posts:", posts);
338
+
return posts;
339
+
};
250
340
251
-
return posts.slice(0, Config.MAX_POSTS);
341
+
const fetchPostsForUser = async (did: At.Did, cursor: string | null) => {
342
+
try {
343
+
const { data } = await rpc.get("com.atproto.repo.listRecords", {
344
+
params: {
345
+
repo: did as At.Identifier,
346
+
collection: "app.bsky.feed.post",
347
+
limit: Config.MAX_POSTS,
348
+
cursor: cursor || undefined,
349
+
},
350
+
});
351
+
return data.records as ComAtprotoRepoListRecords.Record[];
352
+
} catch (e) {
353
+
console.error(`Error fetching posts for ${did}:`, e);
354
+
return null;
355
+
}
252
356
};
253
-
export { fetchAllPosts, getAllMetadataFromPds, Post };
357
+
358
+
// const fetchAllPosts = async () => {
359
+
// const users: AccountMetadata[] = await getAllMetadataFromPds();
360
+
// const postRecords = await Promise.all(
361
+
// users.map(
362
+
// async (metadata: AccountMetadata) => await fetchPosts(metadata.did),
363
+
// ),
364
+
// );
365
+
// // Filter out any records that have an error
366
+
// const validPostRecords = postRecords.filter((record) => !record.error);
367
+
368
+
// const posts: Post[] = validPostRecords.flatMap((userFetch) =>
369
+
// userFetch.records.map((record) => {
370
+
// const user = users.find(
371
+
// (user: AccountMetadata) => user.did == userFetch.did,
372
+
// );
373
+
// if (!user) {
374
+
// throw new Error(`User with DID ${userFetch.did} not found`);
375
+
// }
376
+
// return new Post(record, user);
377
+
// })
378
+
// );
379
+
380
+
// posts.sort((a, b) => b.timestamp - a.timestamp);
381
+
382
+
// if (!Config.SHOW_FUTURE_POSTS) {
383
+
// // Filter out posts that are in the future
384
+
// const now = Date.now();
385
+
// const filteredPosts = posts.filter((post) => post.timestamp <= now);
386
+
// return filteredPosts.slice(0, Config.MAX_POSTS);
387
+
// }
388
+
389
+
// return posts.slice(0, Config.MAX_POSTS);
390
+
// };
391
+
export { getAllMetadataFromPds, getNextPosts, Post, posts };
254
392
export type { AccountMetadata };