ari/DynamicPageLoads #2

Merged
astra merged 6 commits from ari/DynamicPageLoads into main 2025-04-22 03:14:38 +00:00
3 changed files with 187 additions and 45 deletions
Showing only changes of commit 6ca0a971f6 - Show all commits

View file

@ -18,7 +18,7 @@ export class Config {
* Maximum number of posts to show in the feed (across all users) * Maximum number of posts to show in the feed (across all users)
* @default 100 * @default 100
*/ */
static readonly MAX_POSTS: number = 100; static readonly MAX_POSTS: number = 5;
astra marked this conversation as resolved Outdated

The documentation for the variable needs updating i think

The documentation for the variable needs updating i think

Updated!

Updated!
/** /**
* Footer text for the dashboard * Footer text for the dashboard

View file

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import PostComponent from "./lib/PostComponent.svelte"; import PostComponent from "./lib/PostComponent.svelte";
import AccountComponent from "./lib/AccountComponent.svelte"; import AccountComponent from "./lib/AccountComponent.svelte";
import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch"; import { getNextPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
import { Config } from "../config"; import { Config } from "../config";
const postsPromise = fetchAllPosts(); const postsPromise = getNextPosts();
const accountsPromise = getAllMetadataFromPds(); const accountsPromise = getAllMetadataFromPds();
</script> </script>
@ -12,6 +12,7 @@
{#await accountsPromise} {#await accountsPromise}
<p>Loading...</p> <p>Loading...</p>
{:then accountsData} {:then accountsData}
<div id="Account"> <div id="Account">
<h1 id="Header">ATProto PDS</h1> <h1 id="Header">ATProto PDS</h1>
<p>Home to {accountsData.length} accounts</p> <p>Home to {accountsData.length} accounts</p>
@ -29,6 +30,9 @@
{#await postsPromise} {#await postsPromise}
<p>Loading...</p> <p>Loading...</p>
{:then postsData} {:then postsData}
<button on:click={getNextPosts}>
Load more posts
</button>
<div id="Feed"> <div id="Feed">
<div id="spacer"></div> <div id="spacer"></div>
{#each postsData as postObject} {#each postsData as postObject}

View file

@ -18,11 +18,17 @@ import { Config } from "../../config";
// import { AppBskyActorDefs } from "@atcute/client/lexicons"; // import { AppBskyActorDefs } from "@atcute/client/lexicons";
interface AccountMetadata { interface AccountMetadata {
did: string; did: At.Did;
displayName: string; displayName: string;
handle: string; handle: string;
avatarCid: string | null; avatarCid: string | null;
currentCursor?: string;
} }
let accountsMetadata: AccountMetadata[] = [];
// a chronologically sorted list of posts for all users, that will be shown by svelte
// getNextPosts will populate this list with additional posts as needed
let posts: Post[] = [];
interface atUriObject { interface atUriObject {
repo: string; repo: string;
collection: string; collection: string;
@ -45,7 +51,7 @@ class Post {
constructor( constructor(
record: ComAtprotoRepoListRecords.Record, record: ComAtprotoRepoListRecords.Record,
account: AccountMetadata account: AccountMetadata,
) { ) {
this.postCid = record.cid; this.postCid = record.cid;
this.recordName = processAtUri(record.uri).rkey; this.recordName = processAtUri(record.uri).rkey;
@ -68,7 +74,7 @@ class Post {
switch (post.embed?.$type) { switch (post.embed?.$type) {
case "app.bsky.embed.images": case "app.bsky.embed.images":
this.imagesCid = post.embed.images.map( this.imagesCid = post.embed.images.map(
(imageRecord: any) => imageRecord.image.ref.$link (imageRecord: any) => imageRecord.image.ref.$link,
); );
break; break;
case "app.bsky.embed.video": case "app.bsky.embed.video":
@ -82,7 +88,7 @@ class Post {
switch (post.embed.media.$type) { switch (post.embed.media.$type) {
case "app.bsky.embed.images": case "app.bsky.embed.images":
this.imagesCid = post.embed.media.images.map( this.imagesCid = post.embed.media.images.map(
(imageRecord) => imageRecord.image.ref.$link (imageRecord) => imageRecord.image.ref.$link,
); );
break; break;
@ -118,8 +124,8 @@ const getDidsFromPDS = async (): Promise<At.Did[]> => {
return data.repos.map((repo: any) => repo.did) as At.Did[]; return data.repos.map((repo: any) => repo.did) as At.Did[];
}; };
const getAccountMetadata = async ( const getAccountMetadata = async (
did: `did:${string}:${string}` did: `did:${string}:${string}`,
): Promise<AccountMetadata> => { ) => {
// gonna assume self exists in the app.bsky.actor.profile // gonna assume self exists in the app.bsky.actor.profile
try { try {
const { data } = await rpc.get("com.atproto.repo.getRecord", { const { data } = await rpc.get("com.atproto.repo.getRecord", {
@ -143,12 +149,7 @@ const getAccountMetadata = async (
return account; return account;
} catch (e) { } catch (e) {
console.error(`Error fetching metadata for ${did}:`, e); console.error(`Error fetching metadata for ${did}:`, e);
return { return null;
did: "error",
displayName: "",
avatarCid: null,
handle: "error",
};
} }
}; };
@ -157,11 +158,12 @@ const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => {
const metadata = await Promise.all( const metadata = await Promise.all(
dids.map(async (repo: `did:${string}:${string}`) => { dids.map(async (repo: `did:${string}:${string}`) => {
return await getAccountMetadata(repo); return await getAccountMetadata(repo);
}) }),
); );
return metadata.filter((account) => account.did !== "error"); return metadata.filter((account) => account !== null) as AccountMetadata[];
}; };
// OLD
const fetchPosts = async (did: string) => { const fetchPosts = async (did: string) => {
try { try {
const { data } = await rpc.get("com.atproto.repo.listRecords", { const { data } = await rpc.get("com.atproto.repo.listRecords", {
@ -196,7 +198,7 @@ const identityResolve = async (did: At.Did) => {
if (did.startsWith("did:plc:") || did.startsWith("did:web:")) { if (did.startsWith("did:plc:") || did.startsWith("did:web:")) {
const doc = await resolver.resolve( const doc = await resolver.resolve(
did as `did:plc:${string}` | `did:web:${string}` did as `did:plc:${string}` | `did:web:${string}`,
); );
return doc; return doc;
} else { } else {
@ -219,36 +221,172 @@ const blueskyHandleFromDid = async (did: At.Did) => {
} }
}; };
const fetchAllPosts = async () => { interface PostsAcc {
const users: AccountMetadata[] = await getAllMetadataFromPds(); posts: ComAtprotoRepoListRecords.Record[];
const postRecords = await Promise.all( account: AccountMetadata;
users.map( }
async (metadata: AccountMetadata) => await fetchPosts(metadata.did) const getCutoffDate = (postAccounts: PostsAcc[]) => {
)
);
const validPostRecords = postRecords.filter((record) => !record.error);
const posts: Post[] = validPostRecords.flatMap((userFetch) =>
userFetch.records.map((record) => {
const user = users.find(
(user: AccountMetadata) => user.did == userFetch.did
);
if (!user) {
throw new Error(`User with DID ${userFetch.did} not found`);
}
return new Post(record, user);
})
);
posts.sort((a, b) => b.timestamp - a.timestamp);
if(!Config.SHOW_FUTURE_POSTS) {
// Filter out posts that are in the future
const now = Date.now(); const now = Date.now();
const filteredPosts = posts.filter((post) => post.timestamp <= now); let cutoffDate: Date | null = null;
return filteredPosts.slice(0, Config.MAX_POSTS); postAccounts.forEach((postAcc) => {
const latestPost = new Date(
(postAcc.posts[postAcc.posts.length - 1].value as AppBskyFeedPost.Record)
.createdAt,
);
if (!cutoffDate) {
cutoffDate = latestPost;
} else {
if (latestPost > cutoffDate) {
cutoffDate = latestPost;
}
}
});
if (cutoffDate) {
console.log("Cutoff date:", cutoffDate);
return cutoffDate;
} else {
return new Date(now);
}
};
const filterPostsByDate = (posts: PostsAcc[], cutoffDate: Date) => {
// filter posts for each account that are older than the cutoff date and save the cursor of the last post included
const filteredPosts: PostsAcc[] = posts.map((postAcc) => {
const filtered = postAcc.posts.filter((post) => {
const postDate = new Date(
(post.value as AppBskyFeedPost.Record).createdAt,
);
return postDate >= cutoffDate;
});
if (filtered.length > 0) {
postAcc.account.currentCursor = filtered[filtered.length - 1].cid;
}
return {
posts: filtered,
account: postAcc.account,
};
});
return filteredPosts;
};
const getNextPosts = async () => {
if (!accountsMetadata.length) {
accountsMetadata = await getAllMetadataFromPds();
} }
return posts.slice(0, Config.MAX_POSTS); const postsAcc: PostsAcc[] = await Promise.all(
accountsMetadata.map(async (account) => {
const posts = await fetchPostsForUser(
account.did,
account.currentCursor || null,
);
if (posts) {
return {
posts: posts,
account: account,
};
} else {
return {
posts: [],
account: account,
};
}
}),
);
const recordsFiltered = postsAcc.filter((postAcc) =>
postAcc.posts.length > 0
);
const cutoffDate = getCutoffDate(recordsFiltered);
const recordsCutoff = filterPostsByDate(recordsFiltered, cutoffDate);
// update the accountMetadata with the new cursor
accountsMetadata = recordsCutoff.map((postAcc) => postAcc.account);
// throw the records in a big single array
let records = recordsCutoff.flatMap((postAcc) => postAcc.posts);
// sort the records by timestamp
records = records.sort((a, b) => {
const aDate = new Date(
(a.value as AppBskyFeedPost.Record).createdAt,
).getTime();
const bDate = new Date(
(b.value as AppBskyFeedPost.Record).createdAt,
).getTime();
return bDate - aDate;
}
);
// filter out posts that are in the future
if (!Config.SHOW_FUTURE_POSTS) {
const now = Date.now();
records = records.filter((post) => {
const postDate = new Date(
(post.value as AppBskyFeedPost.Record).createdAt,
).getTime();
return postDate <= now;
});
}
// append the new posts to the existing posts
posts = posts.concat(
records.map((record) => {
const account = accountsMetadata.find(
(account) => account.did == processAtUri(record.uri).repo,
);
if (!account) {
throw new Error(`Account with DID ${processAtUri(record.uri).repo} not found`);
}
return new Post(record, account);
}),
);
console.log("Fetched posts:", posts);
return posts;
}; };
export { fetchAllPosts, getAllMetadataFromPds, Post };
const fetchPostsForUser = async (did: At.Did, cursor: string | null) => {
try {
const { data } = await rpc.get("com.atproto.repo.listRecords", {
params: {
repo: did as At.Identifier,
collection: "app.bsky.feed.post",
limit: Config.MAX_POSTS,
cursor: cursor || undefined,
},
});
return data.records as ComAtprotoRepoListRecords.Record[];
} catch (e) {
console.error(`Error fetching posts for ${did}:`, e);
return null;
}
};
// const fetchAllPosts = async () => {
// const users: AccountMetadata[] = await getAllMetadataFromPds();
// const postRecords = await Promise.all(
// users.map(
// async (metadata: AccountMetadata) => await fetchPosts(metadata.did),
// ),
// );
// // Filter out any records that have an error
// const validPostRecords = postRecords.filter((record) => !record.error);
// const posts: Post[] = validPostRecords.flatMap((userFetch) =>
// userFetch.records.map((record) => {
// const user = users.find(
// (user: AccountMetadata) => user.did == userFetch.did,
// );
// if (!user) {
// throw new Error(`User with DID ${userFetch.did} not found`);
// }
// return new Post(record, user);
// })
// );
// posts.sort((a, b) => b.timestamp - a.timestamp);
// if (!Config.SHOW_FUTURE_POSTS) {
// // Filter out posts that are in the future
// const now = Date.now();
// const filteredPosts = posts.filter((post) => post.timestamp <= now);
// return filteredPosts.slice(0, Config.MAX_POSTS);
// }
// return posts.slice(0, Config.MAX_POSTS);
// };
export { getAllMetadataFromPds, getNextPosts, Post, posts };
export type { AccountMetadata }; export type { AccountMetadata };