diff --git a/README.md b/README.md
index 25b3cfb..d9eb2ea 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,9 @@ we use our own CI/CD workflow at [`.forgejo/workflows/deploy.yaml`](.forgejo/wor
## theming
-currently the only way to theme the app is to edit css in the components directly, glhf
+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
relevant files:
diff --git a/config.ts b/config.ts
index 2b1b511..8d09cf6 100644
--- a/config.ts
+++ b/config.ts
@@ -9,27 +9,29 @@ export class Config {
static readonly PDS_URL: string = "https://pds.witchcraft.systems";
/**
- * The base URL of the frontend service for linking to replies
+ * The base URL of the frontend service for linking to replies/quotes/accounts etc.
* @default "https://deer.social"
*/
static readonly FRONTEND_URL: string = "https://deer.social";
/**
- * Maximum number of posts to show in the feed (across all users)
- * @default 100
+ * 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
*/
- static readonly MAX_POSTS: number = 100;
+ static readonly MAX_POSTS: number = 20;
/**
- * Footer text for the dashboard
- * @default "Astrally projected from witchcraft.systems"
+ * Footer text for the dashboard, you probably want to change this
*/
static readonly FOOTER_TEXT: string =
- "Astrally projected from witchcraft.systems ";
+ "Astrally projected from witchcraft.systems Source (github mirror )";
- /**
- * 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 0616852..724a5c0 100644
--- a/deno.lock
+++ b/deno.lock
@@ -8,6 +8,7 @@
"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"
@@ -415,6 +416,9 @@
"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": [
@@ -476,6 +480,7 @@
"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 9f84465..1db6461 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
"@atcute/bluesky": "^2.0.2",
"@atcute/client": "^3.0.1",
"@atcute/identity-resolver": "^0.1.2",
- "moment": "^2.30.1"
+ "moment": "^2.30.1",
+ "svelte-infinite-loading": "^1.4.0"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.3",
diff --git a/src/App.svelte b/src/App.svelte
index fa5a5c1..733320e 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -1,10 +1,36 @@
@@ -26,17 +52,14 @@
Error: {error.message}
{/await}
- {#await postsPromise}
- Loading...
- {:then postsData}
-
-
- {#each postsData as postObject}
-
- {/each}
-
-
- {/await}
+
+
+ {#each posts as postObject}
+
+ {/each}
+
+
+
diff --git a/src/lib/PostComponent.svelte b/src/lib/PostComponent.svelte
index c7c59ac..dc0b874 100644
--- a/src/lib/PostComponent.svelte
+++ b/src/lib/PostComponent.svelte
@@ -113,7 +113,7 @@
←
@@ -125,7 +125,7 @@
→
@@ -213,6 +213,10 @@
#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 0d36e8d..79edab0 100644
--- a/src/lib/pdsfetch.ts
+++ b/src/lib/pdsfetch.ts
@@ -18,11 +18,15 @@ import { Config } from "../../config";
// import { AppBskyActorDefs } from "@atcute/client/lexicons";
interface AccountMetadata {
- did: string;
+ did: At.Did;
displayName: string;
handle: string;
avatarCid: string | null;
+ currentCursor?: string;
}
+
+let accountsMetadata: AccountMetadata[] = [];
+
interface atUriObject {
repo: string;
collection: string;
@@ -45,7 +49,7 @@ class Post {
constructor(
record: ComAtprotoRepoListRecords.Record,
- account: AccountMetadata
+ account: AccountMetadata,
) {
this.postCid = record.cid;
this.recordName = processAtUri(record.uri).rkey;
@@ -68,7 +72,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":
@@ -82,7 +86,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;
@@ -118,8 +122,8 @@ const getDidsFromPDS = async (): Promise
=> {
return data.repos.map((repo: any) => repo.did) as At.Did[];
};
const getAccountMetadata = async (
- did: `did:${string}:${string}`
-): Promise => {
+ did: `did:${string}:${string}`,
+) => {
// gonna assume self exists in the app.bsky.actor.profile
try {
const { data } = await rpc.get("com.atproto.repo.getRecord", {
@@ -143,12 +147,7 @@ const getAccountMetadata = async (
return account;
} catch (e) {
console.error(`Error fetching metadata for ${did}:`, e);
- return {
- did: "error",
- displayName: "",
- avatarCid: null,
- handle: "error",
- };
+ return null;
}
};
@@ -157,33 +156,9 @@ 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.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,
- };
- }
+ return metadata.filter((account) => account !== null) as AccountMetadata[];
};
const identityResolve = async (did: At.Did) => {
@@ -196,7 +171,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 {
@@ -219,36 +194,146 @@ const blueskyHandleFromDid = async (did: At.Did) => {
}
};
-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
- );
- if (!user) {
- throw new Error(`User with DID ${userFetch.did} not found`);
+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;
}
- return new Post(record, user);
- })
- );
+ }
+ });
+ if (cutoffDate) {
+ return cutoffDate;
+ } else {
+ return new Date(now);
+ }
+};
- 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);
+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 = 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();
}
- 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 = 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) {
+ const now = Date.now();
+ records = records.filter((post) => {
+ const postDate = new Date(
+ (post.value as AppBskyFeedPost.Record).createdAt,
+ ).getTime();
+ return postDate <= now;
+ });
+ }
+
+ 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;
};
-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;
+ }
+};
+
+export { getAllMetadataFromPds, getNextPosts, Post };
export type { AccountMetadata };