Compare commits
13 commits
ari/CleanA
...
main
Author | SHA1 | Date | |
---|---|---|---|
614f2b4c30 | |||
72ba577950 | |||
3d38e0f68c | |||
5eca07724e | |||
cff9eed1a4 | |||
26aff55dc2 | |||
bd7572dbd8 | |||
2db2ca4a05 | |||
3af16a98e2 | |||
8cf19c6a77 | |||
8a2ad07d8b | |||
23cd738d71 | |||
b01b6b7f65 |
16 changed files with 411 additions and 181 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
# MIT License
|
||||
|
||||
Copyright (c) 2025 Witchcraft Systems
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
61
README.md
61
README.md
|
@ -1,3 +1,62 @@
|
|||
# pds-dash
|
||||
|
||||
Frontend with stats for your ATProto PDS
|
||||
a frontend dashboard with stats for your ATProto PDS.
|
||||
|
||||
## setup
|
||||
|
||||
### prerequisites
|
||||
|
||||
- [deno](https://deno.com/manual/getting_started/installation)
|
||||
|
||||
### installing
|
||||
|
||||
clone the repo, install dependencies using deno:
|
||||
|
||||
```sh
|
||||
deno install
|
||||
```
|
||||
|
||||
### development server
|
||||
|
||||
local develompent server with hot reloading:
|
||||
|
||||
```sh
|
||||
deno task dev
|
||||
```
|
||||
|
||||
### building
|
||||
|
||||
to build the optimized bundle run:
|
||||
|
||||
```sh
|
||||
deno task build
|
||||
```
|
||||
|
||||
the output will be in the `dist/` directory.
|
||||
|
||||
## deploying
|
||||
|
||||
we use our own CI/CD workflow at [`.forgejo/workflows/deploy.yaml`](.forgejo/workflows/deploy.yaml), but it boils down to building the project bundle and deploying it to a web server. it'll probably make more sense to host it on the same domain as your PDS, but it doesn't affect anything if you host it somewhere else.
|
||||
|
||||
## configuring
|
||||
|
||||
[`config.ts`](config.ts) is the main configuration file, you can find more information in the file itself.
|
||||
|
||||
## 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
|
||||
|
||||
relevant files:
|
||||
|
||||
- [`src/App.svelte`](src/App.svelte)
|
||||
- [`src/app.css`](src/app.css)
|
||||
- [`src/lib/AccountComponent.svelte`](src/lib/AccountComponent.svelte)
|
||||
- [`src/lib/PostComponent.svelte`](src/lib/PostComponent.svelte)
|
||||
|
||||
the favicon is located at [`public/favicon.ico`](public/favicon.ico)
|
||||
|
||||
## license
|
||||
|
||||
MIT
|
||||
|
|
23
config.ts
23
config.ts
|
@ -9,20 +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 fetch from the PDS per user
|
||||
* @default 10
|
||||
* 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_PER_USER: number = 22;
|
||||
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 <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a>";
|
||||
static readonly FOOTER_TEXT: string =
|
||||
"Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a><br><br><a href='https://git.witchcraft.systems/scientific-witchery/pds-dash' target='_blank'>Source</a> (<a href='https://github.com/witchcraft-systems/pds-dash/' target='_blank'>github mirror</a>)";
|
||||
|
||||
/**
|
||||
* Whether to show the posts that are in the future
|
||||
* @default false
|
||||
*/
|
||||
static readonly SHOW_FUTURE_POSTS: boolean = false;
|
||||
}
|
10
deno.lock
generated
10
deno.lock
generated
|
@ -6,7 +6,9 @@
|
|||
"npm:@atcute/identity-resolver@~0.1.2": "0.1.2_@atcute+identity@0.1.3",
|
||||
"npm:@sveltejs/vite-plugin-svelte@^5.0.3": "5.0.3_svelte@5.28.1__acorn@8.14.1_vite@6.3.2__picomatch@4.0.2",
|
||||
"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"
|
||||
|
@ -337,6 +339,9 @@
|
|||
"@jridgewell/sourcemap-codec"
|
||||
]
|
||||
},
|
||||
"moment@2.30.1": {
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="
|
||||
},
|
||||
"mri@1.2.0": {
|
||||
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="
|
||||
},
|
||||
|
@ -411,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": [
|
||||
|
@ -470,7 +478,9 @@
|
|||
"npm:@atcute/identity-resolver@~0.1.2",
|
||||
"npm:@sveltejs/vite-plugin-svelte@^5.0.3",
|
||||
"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"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
"dependencies": {
|
||||
"@atcute/bluesky": "^2.0.2",
|
||||
"@atcute/client": "^3.0.1",
|
||||
"@atcute/identity-resolver": "^0.1.2"
|
||||
"@atcute/identity-resolver": "^0.1.2",
|
||||
"moment": "^2.30.1",
|
||||
"svelte-infinite-loading": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1,10 +1,36 @@
|
|||
<script lang="ts">
|
||||
import PostComponent from "./lib/PostComponent.svelte";
|
||||
import AccountComponent from "./lib/AccountComponent.svelte";
|
||||
import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
|
||||
import InfiniteLoading from "svelte-infinite-loading";
|
||||
import { getNextPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
|
||||
import { Config } from "../config";
|
||||
const postsPromise = fetchAllPosts();
|
||||
const accountsPromise = getAllMetadataFromPds();
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let posts: Post[] = [];
|
||||
|
||||
onMount(() => {
|
||||
// Fetch initial posts
|
||||
getNextPosts().then((initialPosts) => {
|
||||
posts = initialPosts;
|
||||
});
|
||||
});
|
||||
// Infinite loading function
|
||||
const onInfinite = ({
|
||||
detail: { loaded, complete },
|
||||
}: {
|
||||
detail: { loaded: () => void; complete: () => void };
|
||||
}) => {
|
||||
getNextPosts().then((newPosts) => {
|
||||
console.log("Loading next posts...");
|
||||
if (newPosts.length > 0) {
|
||||
posts = [...posts, ...newPosts];
|
||||
loaded();
|
||||
} else {
|
||||
complete();
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<main>
|
||||
|
@ -26,17 +52,14 @@
|
|||
<p>Error: {error.message}</p>
|
||||
{/await}
|
||||
|
||||
{#await postsPromise}
|
||||
<p>Loading...</p>
|
||||
{:then postsData}
|
||||
<div id="Feed">
|
||||
<div id="spacer"></div>
|
||||
{#each postsData as postObject}
|
||||
{#each posts as postObject}
|
||||
<PostComponent post={postObject as Post} />
|
||||
{/each}
|
||||
<InfiniteLoading on:infinite={onInfinite} distance={3000} />
|
||||
<div id="spacer"></div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
@ -51,8 +74,8 @@
|
|||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #12082b;
|
||||
color: #ffffff;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
#Feed {
|
||||
width: 65%;
|
||||
|
@ -74,8 +97,8 @@
|
|||
width: 35%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #8054f0;
|
||||
background-color: #0d0620;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--content-background-color);
|
||||
height: 80vh;
|
||||
padding: 20px;
|
||||
margin-left: 20px;
|
||||
|
|
28
src/app.css
28
src/app.css
|
@ -1,8 +1,20 @@
|
|||
@font-face {
|
||||
font-family: 'ProggyClean';
|
||||
font-family: "ProggyClean";
|
||||
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
|
||||
}
|
||||
|
||||
:root {
|
||||
--link-color: #646cff;
|
||||
--link-hover-color: #535bf2;
|
||||
--background-color: #12082b;
|
||||
--header-background-color: #1f1145;
|
||||
--content-background-color: #0d0620;
|
||||
--text-color: white;
|
||||
--border-color: #8054f0;
|
||||
--indicator-inactive-color: #4a4a4a;
|
||||
--indicator-active-color: #8054f0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent;
|
||||
|
@ -35,11 +47,11 @@
|
|||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
color: var(--link-color);
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
color: var(--link-hover-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
@ -49,11 +61,11 @@ body {
|
|||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background-color: #12082b;
|
||||
font-family: 'ProggyClean', monospace;
|
||||
background-color: var(--background-color);
|
||||
font-family: "ProggyClean", monospace;
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
border-color: #8054f0;
|
||||
color: var(--text-color);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
@ -69,5 +81,3 @@ h1 {
|
|||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
Before Width: | Height: | Size: 1.9 KiB |
|
@ -24,10 +24,10 @@
|
|||
display: flex;
|
||||
text-align: start;
|
||||
align-items: center;
|
||||
background-color: #12082b;
|
||||
background-color: var(--background-color);
|
||||
padding: 0px;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #8054f0;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
#accountName {
|
||||
margin-left: 10px;
|
||||
|
@ -43,6 +43,6 @@
|
|||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 0px;
|
||||
border-right: #8054f0 1px solid;
|
||||
border-right: var(--border-color) 1px solid;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { Post } from "./pdsfetch";
|
||||
import { Config } from "../../config";
|
||||
import { onMount } from "svelte";
|
||||
import moment from "moment";
|
||||
|
||||
let { post }: { post: Post } = $props();
|
||||
|
||||
|
@ -76,7 +77,9 @@
|
|||
<a
|
||||
id="postLink"
|
||||
href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.recordName}"
|
||||
>{post.timenotstamp}</a
|
||||
>{moment(post.timenotstamp).isBefore(moment().subtract(1, "month"))
|
||||
? moment(post.timenotstamp).format("MMM D, YYYY")
|
||||
: moment(post.timenotstamp).fromNow()}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -110,7 +113,7 @@
|
|||
<div id="carouselControls">
|
||||
<button
|
||||
id="prevBtn"
|
||||
on:click={prevImage}
|
||||
onclick={prevImage}
|
||||
disabled={currentImageIndex === 0}>←</button
|
||||
>
|
||||
<div id="carouselIndicators">
|
||||
|
@ -122,7 +125,7 @@
|
|||
</div>
|
||||
<button
|
||||
id="nextBtn"
|
||||
on:click={nextImage}
|
||||
onclick={nextImage}
|
||||
disabled={currentImageIndex === post.imagesCid.length - 1}
|
||||
>→</button
|
||||
>
|
||||
|
@ -131,6 +134,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if post.videosLinkCid}
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video
|
||||
id="embedVideo"
|
||||
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}"
|
||||
|
@ -147,8 +151,8 @@
|
|||
#postContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #8054f0;
|
||||
background-color: black;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--background-color);
|
||||
margin-bottom: 15px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
@ -157,29 +161,29 @@
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
background-color: #1f1145;
|
||||
background-color: var(--header-background-color);
|
||||
padding: 0px 0px;
|
||||
height: fit-content;
|
||||
border-bottom: 1px solid #8054f0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-weight: bold;
|
||||
overflow-wrap: break-word;
|
||||
height: 60px;
|
||||
}
|
||||
#displayName {
|
||||
color: white;
|
||||
color: var(--text-color);
|
||||
font-size: 1.2em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
#handle {
|
||||
color: #8054f0;
|
||||
color: var(--border-color);
|
||||
font-size: 0.8em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#postLink {
|
||||
color: #8054f0;
|
||||
color: var(--border-color);
|
||||
font-size: 0.8em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
@ -189,9 +193,10 @@
|
|||
text-align: start;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
background-color: #0d0620;
|
||||
color: white;
|
||||
background-color: var(--content-background-color);
|
||||
color: var(--text-color);
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-line;
|
||||
}
|
||||
#replyingText {
|
||||
font-size: 0.7em;
|
||||
|
@ -208,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;
|
||||
|
@ -220,7 +229,7 @@
|
|||
height: 100%;
|
||||
margin: 0px;
|
||||
margin-left: 0px;
|
||||
border-right: #8054f0 1px solid;
|
||||
border-right: var(--border-color) 1px solid;
|
||||
}
|
||||
#carouselContainer {
|
||||
position: relative;
|
||||
|
@ -245,16 +254,16 @@
|
|||
.indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #4a4a4a;
|
||||
background-color: var(--indicator-inactive-color);
|
||||
}
|
||||
.indicator.active {
|
||||
background-color: #8054f0;
|
||||
background-color: var(--indicator-active-color);
|
||||
}
|
||||
#prevBtn,
|
||||
#nextBtn {
|
||||
background-color: rgba(31, 17, 69, 0.7);
|
||||
color: white;
|
||||
border: 1px solid #8054f0;
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--border-color);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -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;
|
||||
|
@ -48,7 +52,7 @@ class Post {
|
|||
account: AccountMetadata,
|
||||
) {
|
||||
this.postCid = record.cid;
|
||||
this.recordName = record.uri.split("/").slice(-1)[0];
|
||||
this.recordName = processAtUri(record.uri).rkey;
|
||||
this.authorDid = account.did;
|
||||
this.authorAvatarCid = account.avatarCid;
|
||||
this.authorHandle = account.handle;
|
||||
|
@ -67,8 +71,8 @@ class Post {
|
|||
this.videosLinkCid = null;
|
||||
switch (post.embed?.$type) {
|
||||
case "app.bsky.embed.images":
|
||||
this.imagesCid = post.embed.images.map((imageRecord: any) =>
|
||||
imageRecord.image.ref.$link
|
||||
this.imagesCid = post.embed.images.map(
|
||||
(imageRecord: any) => imageRecord.image.ref.$link,
|
||||
);
|
||||
break;
|
||||
case "app.bsky.embed.video":
|
||||
|
@ -81,8 +85,8 @@ class Post {
|
|||
this.quotingUri = processAtUri(post.embed.record.record.uri);
|
||||
switch (post.embed.media.$type) {
|
||||
case "app.bsky.embed.images":
|
||||
this.imagesCid = post.embed.media.images.map((imageRecord) =>
|
||||
imageRecord.image.ref.$link
|
||||
this.imagesCid = post.embed.media.images.map(
|
||||
(imageRecord) => imageRecord.image.ref.$link,
|
||||
);
|
||||
|
||||
break;
|
||||
|
@ -111,13 +115,15 @@ const rpc = new XRPC({
|
|||
}),
|
||||
});
|
||||
|
||||
const getDidsFromPDS = async () => {
|
||||
const getDidsFromPDS = async (): Promise<At.Did[]> => {
|
||||
const { data } = await rpc.get("com.atproto.sync.listRepos", {
|
||||
params: {},
|
||||
});
|
||||
return data.repos.map((repo: any) => (repo.did));
|
||||
return data.repos.map((repo: any) => repo.did) as At.Did[];
|
||||
};
|
||||
const getAccountMetadata = async (did: `did:${string}:${string}`) => {
|
||||
const getAccountMetadata = async (
|
||||
did: `did:${string}:${string}`,
|
||||
) => {
|
||||
// gonna assume self exists in the app.bsky.actor.profile
|
||||
try {
|
||||
const { data } = await rpc.get("com.atproto.repo.getRecord", {
|
||||
|
@ -139,49 +145,20 @@ const getAccountMetadata = async (did: `did:${string}:${string}`) => {
|
|||
account.avatarCid = value.avatar.ref["$link"];
|
||||
}
|
||||
return account;
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.error(`Error fetching metadata for ${did}:`, e);
|
||||
return {
|
||||
did: "error",
|
||||
displayName: "",
|
||||
avatarCid: null,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getAllMetadataFromPds = async () => {
|
||||
const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => {
|
||||
const dids = await getDidsFromPDS();
|
||||
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_PER_USER,
|
||||
},
|
||||
});
|
||||
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) => {
|
||||
|
@ -217,34 +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;
|
||||
}
|
||||
return new Post(record, user);
|
||||
})
|
||||
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,
|
||||
);
|
||||
posts.sort((a, b) => b.timestamp - a.timestamp);
|
||||
return posts;
|
||||
if (!cutoffDate) {
|
||||
cutoffDate = latestPost;
|
||||
} else {
|
||||
if (latestPost > cutoffDate) {
|
||||
cutoffDate = latestPost;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (cutoffDate) {
|
||||
return cutoffDate;
|
||||
} else {
|
||||
return new Date(now);
|
||||
}
|
||||
};
|
||||
|
||||
const testApiCall = async () => {
|
||||
const { data } = await rpc.get("com.atproto.sync.listRepos", {
|
||||
params: {},
|
||||
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;
|
||||
});
|
||||
console.log(data);
|
||||
if (filtered.length > 0) {
|
||||
postAcc.account.currentCursor = processAtUri(filtered[filtered.length - 1].uri).rkey;
|
||||
}
|
||||
return {
|
||||
posts: filtered,
|
||||
account: postAcc.account,
|
||||
};
|
||||
export { fetchAllPosts, getAllMetadataFromPds, Post };
|
||||
});
|
||||
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,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
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;
|
||||
};
|
||||
|
||||
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 };
|
||||
|
|
12
src/main.ts
12
src/main.ts
|
@ -1,9 +1,9 @@
|
|||
import { mount } from 'svelte'
|
||||
import './app.css'
|
||||
import App from './App.svelte'
|
||||
import { mount } from "svelte";
|
||||
import "./app.css";
|
||||
import App from "./App.svelte";
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById('app')!,
|
||||
})
|
||||
target: document.getElementById("app")!,
|
||||
});
|
||||
|
||||
export default app
|
||||
export default app;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
})
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue