diff --git a/README.md b/README.md index d9eb2ea..25b3cfb 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,7 @@ we use our own CI/CD workflow at [`.forgejo/workflows/deploy.yaml`](.forgejo/wor ## theming -the colors are designated in [`src/app.css`](src/app.css) as variables, go crazy with them - -the rest is done by editing the css files and style tags directly, good luck +currently the only way to theme the app is to edit css in the components directly, glhf relevant files: diff --git a/config.ts b/config.ts index 8d09cf6..2b1b511 100644 --- a/config.ts +++ b/config.ts @@ -9,29 +9,27 @@ export class Config { static readonly PDS_URL: string = "https://pds.witchcraft.systems"; /** - * The base URL of the frontend service for linking to replies/quotes/accounts etc. + * The base URL of the frontend service for linking to replies * @default "https://deer.social" */ static readonly FRONTEND_URL: string = "https://deer.social"; /** - * Maximum number of posts to fetch from the PDS per request - * Should be around 20 for about 10 users on the pds - * The more users you have, the lower the number should be - * since sorting is slow and is done on the frontend - * @default 20 + * Maximum number of posts to show in the feed (across all users) + * @default 100 */ - static readonly MAX_POSTS: number = 20; + static readonly MAX_POSTS: number = 100; /** - * Footer text for the dashboard, you probably want to change this + * Footer text for the dashboard + * @default "Astrally projected from witchcraft.systems" */ static readonly FOOTER_TEXT: string = - "Astrally projected from witchcraft.systems

Source (github mirror)"; + "Astrally projected from witchcraft.systems"; - /** - * Whether to show the posts that are in the future - * @default false - */ - static readonly SHOW_FUTURE_POSTS: boolean = false; + /** + * Whether to show the posts that are in the future + * @default false + */ + static readonly SHOW_FUTURE_POSTS: boolean = false; } diff --git a/deno.lock b/deno.lock index 724a5c0..0616852 100644 --- a/deno.lock +++ b/deno.lock @@ -8,7 +8,6 @@ "npm:@tsconfig/svelte@^5.0.4": "5.0.4", "npm:moment@^2.30.1": "2.30.1", "npm:svelte-check@^4.1.5": "4.1.6_svelte@5.28.1__acorn@8.14.1_typescript@5.7.3", - "npm:svelte-infinite-loading@^1.4.0": "1.4.0", "npm:svelte@^5.23.1": "5.28.1_acorn@8.14.1", "npm:typescript@~5.7.2": "5.7.3", "npm:vite@^6.3.1": "6.3.2_picomatch@4.0.2" @@ -416,9 +415,6 @@ "typescript" ] }, - "svelte-infinite-loading@1.4.0": { - "integrity": "sha512-Jo+f/yr/HmZQuIiiKKzAHVFXdAUWHW2RBbrcQTil8JVk1sCm/riy7KTJVzjBgQvHasrFQYKF84zvtc9/Y4lFYg==" - }, "svelte@5.28.1_acorn@8.14.1": { "integrity": "sha512-iOa9WmfNG95lSOSJdMhdjJ4Afok7IRAQYXpbnxhd5EINnXseG0GVa9j6WPght4eX78XfFez45Fi+uRglGKPV/Q==", "dependencies": [ @@ -480,7 +476,6 @@ "npm:@tsconfig/svelte@^5.0.4", "npm:moment@^2.30.1", "npm:svelte-check@^4.1.5", - "npm:svelte-infinite-loading@^1.4.0", "npm:svelte@^5.23.1", "npm:typescript@~5.7.2", "npm:vite@^6.3.1" diff --git a/package.json b/package.json index 1db6461..9f84465 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,7 @@ "@atcute/bluesky": "^2.0.2", "@atcute/client": "^3.0.1", "@atcute/identity-resolver": "^0.1.2", - "moment": "^2.30.1", - "svelte-infinite-loading": "^1.4.0" + "moment": "^2.30.1" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.3", diff --git a/src/App.svelte b/src/App.svelte index 733320e..fa5a5c1 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,36 +1,10 @@
@@ -52,14 +26,17 @@

Error: {error.message}

{/await} -
-
- {#each posts as postObject} - - {/each} - -
-
+ {#await postsPromise} +

Loading...

+ {:then postsData} +
+
+ {#each postsData as postObject} + + {/each} +
+
+ {/await}
diff --git a/src/lib/PostComponent.svelte b/src/lib/PostComponent.svelte index dc0b874..c7c59ac 100644 --- a/src/lib/PostComponent.svelte +++ b/src/lib/PostComponent.svelte @@ -113,7 +113,7 @@
@@ -125,7 +125,7 @@
@@ -213,10 +213,6 @@ #postText { margin: 0; padding: 0; - overflow-wrap: break-word; - word-wrap: normal; - word-break: break-word; - hyphens: none; } #headerText { margin-left: 10px; diff --git a/src/lib/pdsfetch.ts b/src/lib/pdsfetch.ts index 79edab0..0d36e8d 100644 --- a/src/lib/pdsfetch.ts +++ b/src/lib/pdsfetch.ts @@ -18,15 +18,11 @@ import { Config } from "../../config"; // import { AppBskyActorDefs } from "@atcute/client/lexicons"; interface AccountMetadata { - did: At.Did; + did: string; displayName: string; handle: string; avatarCid: string | null; - currentCursor?: string; } - -let accountsMetadata: AccountMetadata[] = []; - interface atUriObject { repo: string; collection: string; @@ -49,7 +45,7 @@ class Post { constructor( record: ComAtprotoRepoListRecords.Record, - account: AccountMetadata, + account: AccountMetadata ) { this.postCid = record.cid; this.recordName = processAtUri(record.uri).rkey; @@ -72,7 +68,7 @@ class Post { switch (post.embed?.$type) { case "app.bsky.embed.images": this.imagesCid = post.embed.images.map( - (imageRecord: any) => imageRecord.image.ref.$link, + (imageRecord: any) => imageRecord.image.ref.$link ); break; case "app.bsky.embed.video": @@ -86,7 +82,7 @@ class Post { switch (post.embed.media.$type) { case "app.bsky.embed.images": this.imagesCid = post.embed.media.images.map( - (imageRecord) => imageRecord.image.ref.$link, + (imageRecord) => imageRecord.image.ref.$link ); break; @@ -122,8 +118,8 @@ const getDidsFromPDS = async (): Promise => { return data.repos.map((repo: any) => repo.did) as At.Did[]; }; const getAccountMetadata = async ( - did: `did:${string}:${string}`, -) => { + did: `did:${string}:${string}` +): Promise => { // gonna assume self exists in the app.bsky.actor.profile try { const { data } = await rpc.get("com.atproto.repo.getRecord", { @@ -147,7 +143,12 @@ const getAccountMetadata = async ( return account; } catch (e) { console.error(`Error fetching metadata for ${did}:`, e); - return null; + return { + did: "error", + displayName: "", + avatarCid: null, + handle: "error", + }; } }; @@ -156,9 +157,33 @@ const getAllMetadataFromPds = async (): Promise => { const metadata = await Promise.all( dids.map(async (repo: `did:${string}:${string}`) => { return await getAccountMetadata(repo); - }), + }) ); - return metadata.filter((account) => account !== null) as AccountMetadata[]; + return metadata.filter((account) => account.did !== "error"); +}; + +const fetchPosts = async (did: string) => { + 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, + }, + }); + return { + records: data.records as ComAtprotoRepoListRecords.Record[], + did: did, + error: false, + }; + } catch (e) { + console.error(`Error fetching posts for ${did}:`, e); + return { + records: [], + did: did, + error: true, + }; + } }; const identityResolve = async (did: At.Did) => { @@ -171,7 +196,7 @@ const identityResolve = async (did: At.Did) => { if (did.startsWith("did:plc:") || did.startsWith("did:web:")) { const doc = await resolver.resolve( - did as `did:plc:${string}` | `did:web:${string}`, + did as `did:plc:${string}` | `did:web:${string}` ); return doc; } else { @@ -194,146 +219,36 @@ const blueskyHandleFromDid = async (did: At.Did) => { } }; -interface PostsAcc { - posts: ComAtprotoRepoListRecords.Record[]; - account: AccountMetadata; -} -const getCutoffDate = (postAccounts: PostsAcc[]) => { - const now = Date.now(); - let cutoffDate: Date | null = null; - 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) { - 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, +const fetchAllPosts = async () => { + const users: AccountMetadata[] = await getAllMetadataFromPds(); + const postRecords = await Promise.all( + users.map( + async (metadata: AccountMetadata) => await fetchPosts(metadata.did) + ) + ); + 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 ); - return postDate >= cutoffDate; - }); - if (filtered.length > 0) { - postAcc.account.currentCursor = processAtUri(filtered[filtered.length - 1].uri).rkey; - } - return { - posts: filtered, - account: postAcc.account, - }; - }); - return filteredPosts; -}; -// nightmare function. However it works so I am not touching it -const getNextPosts = async () => { - if (!accountsMetadata.length) { - accountsMetadata = await getAllMetadataFromPds(); - } - - 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, - }; + if (!user) { + throw new Error(`User with DID ${userFetch.did} not found`); } - }), + return new Post(record, user); + }) ); - 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 = accountsMetadata.map((account) => { - const postAcc = recordsCutoff.find( - (postAcc) => postAcc.account.did == account.did, - ); - if (postAcc) { - account.currentCursor = postAcc.account.currentCursor; - } - return 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) { + + 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(); - records = records.filter((post) => { - const postDate = new Date( - (post.value as AppBskyFeedPost.Record).createdAt, - ).getTime(); - return postDate <= now; - }); + const filteredPosts = posts.filter((post) => post.timestamp <= now); + return filteredPosts.slice(0, Config.MAX_POSTS); } - const newPosts = 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); - }); - return newPosts; + return posts.slice(0, Config.MAX_POSTS); }; - -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; - } -}; - -export { getAllMetadataFromPds, getNextPosts, Post }; +export { fetchAllPosts, getAllMetadataFromPds, Post }; export type { AccountMetadata };