Formatting, cleanup, misc style changes, more config options
All checks were successful
Deploy / Deploy (push) Successful in 28s

This commit is contained in:
Astra 2025-04-21 16:20:43 +09:00
parent 3af16a98e2
commit 2db2ca4a05
Signed by: astra
SSH key fingerprint: SHA256:jQDNS75/33T59Ey4yAzrUPP/5YQaXEetsW8hwUae+ag
9 changed files with 122 additions and 106 deletions

View file

@ -2,27 +2,34 @@
* Configuration module for the PDS Dashboard * Configuration module for the PDS Dashboard
*/ */
export class Config { export class Config {
/** /**
* The base URL of the PDS (Personal Data Server) * The base URL of the PDS (Personal Data Server)
* @default "https://pds.witchcraft.systems" * @default "https://pds.witchcraft.systems"
*/ */
static readonly PDS_URL: string = "https://pds.witchcraft.systems"; static readonly PDS_URL: string = "https://pds.witchcraft.systems";
/**
* 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 show in the feed (across all users)
* @default 100
*/
static readonly MAX_POSTS: number = 100;
/**
* Footer text for the dashboard
* @default "Astrally projected from witchcraft.systems"
*/
static readonly FOOTER_TEXT: string =
"Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a>";
/** /**
* The base URL of the frontend service for linking to replies * Whether to show the posts that are in the future
* @default "https://deer.social" * @default false
*/ */
static readonly FRONTEND_URL: string = "https://deer.social"; static readonly SHOW_FUTURE_POSTS: boolean = false;
}
/**
* Maximum number of posts to show in the feed (across all users)
* @default 100
*/
static readonly MAX_POSTS: number = 100;
/**
* Footer text for the dashboard
* @default "Astrally projected from witchcraft.systems"
*/
static readonly FOOTER_TEXT: string = "Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a>";
}

View file

@ -1,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

View file

@ -9,39 +9,38 @@
<main> <main>
<div id="Content"> <div id="Content">
{#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>
<div id="accountsList"> <div id="accountsList">
{#each accountsData as accountObject} {#each accountsData as accountObject}
<AccountComponent account={accountObject} /> <AccountComponent account={accountObject} />
{/each} {/each}
</div>
<p>{@html Config.FOOTER_TEXT}</p>
</div> </div>
<p>{@html Config.FOOTER_TEXT}</p> {:catch error}
</div> <p>Error: {error.message}</p>
{:catch error} {/await}
<p>Error: {error.message}</p>
{/await}
{#await postsPromise} {#await postsPromise}
<p>Loading...</p> <p>Loading...</p>
{:then postsData} {:then postsData}
<div id="Feed"> <div id="Feed">
<div id="spacer"></div> <div id="spacer"></div>
{#each postsData as postObject} {#each postsData as postObject}
<PostComponent post={postObject as Post} /> <PostComponent post={postObject as Post} />
{/each} {/each}
<div id="spacer"></div> <div id="spacer"></div>
</div> </div>
{/await} {/await}
</div> </div>
</main> </main>
<style> <style>
/* desktop style */ /* desktop style */
#Content { #Content {
@ -74,7 +73,7 @@
#Account { #Account {
width: 35%; width: 35%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
background-color: var(--content-background-color); background-color: var(--content-background-color);
height: 80vh; height: 80vh;

View file

@ -1,5 +1,5 @@
@font-face { @font-face {
font-family: 'ProggyClean'; font-family: "ProggyClean";
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf); src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
} }
@ -62,7 +62,7 @@ body {
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
background-color: var(--background-color); background-color: var(--background-color);
font-family: 'ProggyClean', monospace; font-family: "ProggyClean", monospace;
font-size: 24px; font-size: 24px;
color: var(--text-color); color: var(--text-color);
border-color: var(--border-color); border-color: var(--border-color);
@ -80,4 +80,4 @@ h1 {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
text-align: center; text-align: center;
} }

View file

@ -145,7 +145,6 @@
</div> </div>
<style> <style>
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
} }
@ -197,6 +196,7 @@
background-color: var(--content-background-color); background-color: var(--content-background-color);
color: var(--text-color); color: var(--text-color);
overflow-wrap: break-word; overflow-wrap: break-word;
white-space: pre-line;
} }
#replyingText { #replyingText {
font-size: 0.7em; font-size: 0.7em;

View file

@ -45,7 +45,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;
@ -67,8 +67,8 @@ class Post {
this.videosLinkCid = null; this.videosLinkCid = null;
switch (post.embed?.$type) { switch (post.embed?.$type) {
case "app.bsky.embed.images": case "app.bsky.embed.images":
this.imagesCid = post.embed.images.map((imageRecord: any) => this.imagesCid = post.embed.images.map(
imageRecord.image.ref.$link (imageRecord: any) => imageRecord.image.ref.$link
); );
break; break;
case "app.bsky.embed.video": case "app.bsky.embed.video":
@ -81,8 +81,8 @@ class Post {
this.quotingUri = processAtUri(post.embed.record.record.uri); this.quotingUri = processAtUri(post.embed.record.record.uri);
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((imageRecord) => this.imagesCid = post.embed.media.images.map(
imageRecord.image.ref.$link (imageRecord) => imageRecord.image.ref.$link
); );
break; break;
@ -111,36 +111,37 @@ const rpc = new XRPC({
}), }),
}); });
const getDidsFromPDS = async () : Promise<At.Did[]> => { const getDidsFromPDS = async (): Promise<At.Did[]> => {
const { data } = await rpc.get("com.atproto.sync.listRepos", { const { data } = await rpc.get("com.atproto.sync.listRepos", {
params: {}, params: {},
}); });
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 (did: `did:${string}:${string}`) : Promise<AccountMetadata> => { const getAccountMetadata = async (
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", {
params: { params: {
repo: did, repo: did,
collection: "app.bsky.actor.profile", collection: "app.bsky.actor.profile",
rkey: "self", rkey: "self",
}, },
}); });
const value = data.value as AppBskyActorProfile.Record; const value = data.value as AppBskyActorProfile.Record;
const handle = await blueskyHandleFromDid(did); const handle = await blueskyHandleFromDid(did);
const account: AccountMetadata = { const account: AccountMetadata = {
did: did, did: did,
handle: handle, handle: handle,
displayName: value.displayName || "", displayName: value.displayName || "",
avatarCid: null, avatarCid: null,
}; };
if (value.avatar) { if (value.avatar) {
account.avatarCid = value.avatar.ref["$link"]; account.avatarCid = value.avatar.ref["$link"];
} }
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 {
did: "error", did: "error",
@ -151,14 +152,14 @@ const getAccountMetadata = async (did: `did:${string}:${string}`) : Promise<Acco
} }
}; };
const getAllMetadataFromPds = async () : Promise<AccountMetadata[]> => { const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => {
const dids = await getDidsFromPDS(); const dids = await getDidsFromPDS();
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.did !== "error");
}; };
const fetchPosts = async (did: string) => { const fetchPosts = async (did: string) => {
@ -173,14 +174,14 @@ const fetchPosts = async (did: string) => {
return { return {
records: data.records as ComAtprotoRepoListRecords.Record[], records: data.records as ComAtprotoRepoListRecords.Record[],
did: did, did: did,
error: false error: false,
}; };
} catch (e) { } catch (e) {
console.error(`Error fetching posts for ${did}:`, e); console.error(`Error fetching posts for ${did}:`, e);
return { return {
records: [], records: [],
did: did, did: did,
error: true error: true,
}; };
} }
}; };
@ -195,7 +196,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 {
@ -221,15 +222,15 @@ const blueskyHandleFromDid = async (did: At.Did) => {
const fetchAllPosts = async () => { const fetchAllPosts = async () => {
const users: AccountMetadata[] = await getAllMetadataFromPds(); const users: AccountMetadata[] = await getAllMetadataFromPds();
const postRecords = await Promise.all( const postRecords = await Promise.all(
users.map(async (metadata: AccountMetadata) => users.map(
await fetchPosts(metadata.did) async (metadata: AccountMetadata) => await fetchPosts(metadata.did)
), )
); );
const validPostRecords = postRecords.filter(record => !record.error); const validPostRecords = postRecords.filter((record) => !record.error);
const posts: Post[] = validPostRecords.flatMap((userFetch) => const posts: Post[] = validPostRecords.flatMap((userFetch) =>
userFetch.records.map((record) => { userFetch.records.map((record) => {
const user = users.find((user: AccountMetadata) => const user = users.find(
user.did == userFetch.did (user: AccountMetadata) => user.did == userFetch.did
); );
if (!user) { if (!user) {
throw new Error(`User with DID ${userFetch.did} not found`); throw new Error(`User with DID ${userFetch.did} not found`);
@ -237,7 +238,16 @@ const fetchAllPosts = async () => {
return new Post(record, user); return new Post(record, user);
}) })
); );
posts.sort((a, b) => b.timestamp - a.timestamp); 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); return posts.slice(0, Config.MAX_POSTS);
}; };
export { fetchAllPosts, getAllMetadataFromPds, Post }; export { fetchAllPosts, getAllMetadataFromPds, Post };

View file

@ -1,9 +1,9 @@
import { mount } from 'svelte' import { mount } from "svelte";
import './app.css' import "./app.css";
import App from './App.svelte' import App from "./App.svelte";
const app = mount(App, { const app = mount(App, {
target: document.getElementById('app')!, target: document.getElementById("app")!,
}) });
export default app export default app;

View file

@ -1,7 +1,7 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
export default { export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors // for more information about preprocessors
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
} };

View file

@ -1,7 +1,7 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import { svelte } from '@sveltejs/vite-plugin-svelte' import { svelte } from "@sveltejs/vite-plugin-svelte";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [svelte()], plugins: [svelte()],
}) });