diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..581839a
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
index 6194e14..d9eb2ea 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,62 @@
# pds-dash
-Frontend with stats for your ATProto PDS
\ No newline at end of file
+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
diff --git a/config.ts b/config.ts
index b8284a6..8d09cf6 100644
--- a/config.ts
+++ b/config.ts
@@ -2,27 +2,36 @@
* Configuration module for the PDS Dashboard
*/
export class Config {
- /**
- * The base URL of the PDS (Personal Data Server)
- * @default "https://pds.witchcraft.systems"
- */
- static readonly PDS_URL: string = "https://pds.witchcraft.systems";
+ /**
+ * The base URL of the PDS (Personal Data Server)
+ * @default "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";
+ /**
+ * 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
- */
- static readonly MAX_POSTS_PER_USER: number = 22;
+ /**
+ * 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 = 20;
- /**
- * Footer text for the dashboard
- * @default "Astrally projected from witchcraft.systems"
- */
- static readonly FOOTER_TEXT: string = "Astrally projected from witchcraft.systems ";
-}
\ No newline at end of file
+ /**
+ * Footer text for the dashboard, you probably want to change this
+ */
+ static readonly FOOTER_TEXT: string =
+ "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;
+}
diff --git a/deno.lock b/deno.lock
index df8c920..90a8393 100644
--- a/deno.lock
+++ b/deno.lock
@@ -1,12 +1,15 @@
{
- "version": "4",
+ "version": "5",
"specifiers": {
"npm:@atcute/bluesky@^2.0.2": "2.0.2_@atcute+client@3.0.1",
"npm:@atcute/client@^3.0.1": "3.0.1",
"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:mutex-ts@^1.2.1": "1.2.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"
@@ -52,79 +55,129 @@
"integrity": "sha512-GEhUCk9c4XbNxi+0YZHZsV4fYNd6HejfWuN4Ti4c02DauX+LyX5WY1Y3WfyZ8Pxxl0zqhs+MLtW98cMh86vv6g=="
},
"@esbuild/aix-ppc64@0.25.2": {
- "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="
+ "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
+ "os": ["aix"],
+ "cpu": ["ppc64"]
},
"@esbuild/android-arm64@0.25.2": {
- "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w=="
+ "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
+ "os": ["android"],
+ "cpu": ["arm64"]
},
"@esbuild/android-arm@0.25.2": {
- "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA=="
+ "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
+ "os": ["android"],
+ "cpu": ["arm"]
},
"@esbuild/android-x64@0.25.2": {
- "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg=="
+ "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
+ "os": ["android"],
+ "cpu": ["x64"]
},
"@esbuild/darwin-arm64@0.25.2": {
- "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA=="
+ "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
+ "os": ["darwin"],
+ "cpu": ["arm64"]
},
"@esbuild/darwin-x64@0.25.2": {
- "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA=="
+ "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
+ "os": ["darwin"],
+ "cpu": ["x64"]
},
"@esbuild/freebsd-arm64@0.25.2": {
- "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w=="
+ "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
+ "os": ["freebsd"],
+ "cpu": ["arm64"]
},
"@esbuild/freebsd-x64@0.25.2": {
- "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ=="
+ "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
+ "os": ["freebsd"],
+ "cpu": ["x64"]
},
"@esbuild/linux-arm64@0.25.2": {
- "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g=="
+ "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
+ "os": ["linux"],
+ "cpu": ["arm64"]
},
"@esbuild/linux-arm@0.25.2": {
- "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g=="
+ "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
+ "os": ["linux"],
+ "cpu": ["arm"]
},
"@esbuild/linux-ia32@0.25.2": {
- "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ=="
+ "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
+ "os": ["linux"],
+ "cpu": ["ia32"]
},
"@esbuild/linux-loong64@0.25.2": {
- "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w=="
+ "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
+ "os": ["linux"],
+ "cpu": ["loong64"]
},
"@esbuild/linux-mips64el@0.25.2": {
- "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q=="
+ "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
+ "os": ["linux"],
+ "cpu": ["mips64el"]
},
"@esbuild/linux-ppc64@0.25.2": {
- "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g=="
+ "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
+ "os": ["linux"],
+ "cpu": ["ppc64"]
},
"@esbuild/linux-riscv64@0.25.2": {
- "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw=="
+ "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
+ "os": ["linux"],
+ "cpu": ["riscv64"]
},
"@esbuild/linux-s390x@0.25.2": {
- "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q=="
+ "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
+ "os": ["linux"],
+ "cpu": ["s390x"]
},
"@esbuild/linux-x64@0.25.2": {
- "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg=="
+ "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
+ "os": ["linux"],
+ "cpu": ["x64"]
},
"@esbuild/netbsd-arm64@0.25.2": {
- "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw=="
+ "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
+ "os": ["netbsd"],
+ "cpu": ["arm64"]
},
"@esbuild/netbsd-x64@0.25.2": {
- "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg=="
+ "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
+ "os": ["netbsd"],
+ "cpu": ["x64"]
},
"@esbuild/openbsd-arm64@0.25.2": {
- "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg=="
+ "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
+ "os": ["openbsd"],
+ "cpu": ["arm64"]
},
"@esbuild/openbsd-x64@0.25.2": {
- "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw=="
+ "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
+ "os": ["openbsd"],
+ "cpu": ["x64"]
},
"@esbuild/sunos-x64@0.25.2": {
- "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA=="
+ "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
+ "os": ["sunos"],
+ "cpu": ["x64"]
},
"@esbuild/win32-arm64@0.25.2": {
- "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q=="
+ "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
+ "os": ["win32"],
+ "cpu": ["arm64"]
},
"@esbuild/win32-ia32@0.25.2": {
- "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg=="
+ "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
+ "os": ["win32"],
+ "cpu": ["ia32"]
},
"@esbuild/win32-x64@0.25.2": {
- "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="
+ "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
+ "os": ["win32"],
+ "cpu": ["x64"]
},
"@jridgewell/gen-mapping@0.3.8": {
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
@@ -151,64 +204,104 @@
]
},
"@rollup/rollup-android-arm-eabi@4.40.0": {
- "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="
+ "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==",
+ "os": ["android"],
+ "cpu": ["arm"]
},
"@rollup/rollup-android-arm64@4.40.0": {
- "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="
+ "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==",
+ "os": ["android"],
+ "cpu": ["arm64"]
},
"@rollup/rollup-darwin-arm64@4.40.0": {
- "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ=="
+ "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==",
+ "os": ["darwin"],
+ "cpu": ["arm64"]
},
"@rollup/rollup-darwin-x64@4.40.0": {
- "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="
+ "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==",
+ "os": ["darwin"],
+ "cpu": ["x64"]
},
"@rollup/rollup-freebsd-arm64@4.40.0": {
- "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg=="
+ "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==",
+ "os": ["freebsd"],
+ "cpu": ["arm64"]
},
"@rollup/rollup-freebsd-x64@4.40.0": {
- "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="
+ "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==",
+ "os": ["freebsd"],
+ "cpu": ["x64"]
},
"@rollup/rollup-linux-arm-gnueabihf@4.40.0": {
- "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA=="
+ "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==",
+ "os": ["linux"],
+ "cpu": ["arm"]
},
"@rollup/rollup-linux-arm-musleabihf@4.40.0": {
- "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg=="
+ "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==",
+ "os": ["linux"],
+ "cpu": ["arm"]
},
"@rollup/rollup-linux-arm64-gnu@4.40.0": {
- "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="
+ "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==",
+ "os": ["linux"],
+ "cpu": ["arm64"]
},
"@rollup/rollup-linux-arm64-musl@4.40.0": {
- "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ=="
+ "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==",
+ "os": ["linux"],
+ "cpu": ["arm64"]
},
"@rollup/rollup-linux-loongarch64-gnu@4.40.0": {
- "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="
+ "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==",
+ "os": ["linux"],
+ "cpu": ["loong64"]
},
"@rollup/rollup-linux-powerpc64le-gnu@4.40.0": {
- "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw=="
+ "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==",
+ "os": ["linux"],
+ "cpu": ["ppc64"]
},
"@rollup/rollup-linux-riscv64-gnu@4.40.0": {
- "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA=="
+ "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==",
+ "os": ["linux"],
+ "cpu": ["riscv64"]
},
"@rollup/rollup-linux-riscv64-musl@4.40.0": {
- "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ=="
+ "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==",
+ "os": ["linux"],
+ "cpu": ["riscv64"]
},
"@rollup/rollup-linux-s390x-gnu@4.40.0": {
- "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="
+ "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==",
+ "os": ["linux"],
+ "cpu": ["s390x"]
},
"@rollup/rollup-linux-x64-gnu@4.40.0": {
- "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="
+ "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==",
+ "os": ["linux"],
+ "cpu": ["x64"]
},
"@rollup/rollup-linux-x64-musl@4.40.0": {
- "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="
+ "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==",
+ "os": ["linux"],
+ "cpu": ["x64"]
},
"@rollup/rollup-win32-arm64-msvc@4.40.0": {
- "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ=="
+ "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==",
+ "os": ["win32"],
+ "cpu": ["arm64"]
},
"@rollup/rollup-win32-ia32-msvc@4.40.0": {
- "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="
+ "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==",
+ "os": ["win32"],
+ "cpu": ["ia32"]
},
"@rollup/rollup-win32-x64-msvc@4.40.0": {
- "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="
+ "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==",
+ "os": ["win32"],
+ "cpu": ["x64"]
},
"@sveltejs/acorn-typescript@1.0.5_acorn@8.14.1": {
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
@@ -245,7 +338,8 @@
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
},
"acorn@8.14.1": {
- "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="
+ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+ "bin": true
},
"aria-query@5.3.2": {
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="
@@ -273,7 +367,7 @@
},
"esbuild@0.25.2": {
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
- "dependencies": [
+ "optionalDependencies": [
"@esbuild/aix-ppc64",
"@esbuild/android-arm",
"@esbuild/android-arm64",
@@ -299,7 +393,9 @@
"@esbuild/win32-arm64",
"@esbuild/win32-ia32",
"@esbuild/win32-x64"
- ]
+ ],
+ "scripts": true,
+ "bin": true
},
"esm-env@1.2.2": {
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
@@ -314,10 +410,15 @@
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dependencies": [
"picomatch"
+ ],
+ "optionalPeers": [
+ "picomatch"
]
},
"fsevents@2.3.3": {
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "os": ["darwin"],
+ "scripts": true
},
"is-reference@3.0.3": {
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
@@ -337,14 +438,21 @@
"@jridgewell/sourcemap-codec"
]
},
+ "moment@2.30.1": {
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="
+ },
"mri@1.2.0": {
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="
},
"ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
+ "mutex-ts@1.2.1": {
+ "integrity": "sha512-OkcXgf0viuCgYdnm48kiNQ9PzC5OzISQ261svHr/Ybc2vBYC/5xfLXn44hQ+dYRX74v7MCSqV/LKPEbpYdDybw=="
+ },
"nanoid@3.3.11": {
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "bin": true
},
"picocolors@1.1.1": {
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
@@ -366,6 +474,9 @@
"rollup@4.40.0": {
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
"dependencies": [
+ "@types/estree"
+ ],
+ "optionalDependencies": [
"@rollup/rollup-android-arm-eabi",
"@rollup/rollup-android-arm64",
"@rollup/rollup-darwin-arm64",
@@ -386,9 +497,9 @@
"@rollup/rollup-win32-arm64-msvc",
"@rollup/rollup-win32-ia32-msvc",
"@rollup/rollup-win32-x64-msvc",
- "@types/estree",
"fsevents"
- ]
+ ],
+ "bin": true
},
"sade@1.8.1": {
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
@@ -409,7 +520,11 @@
"sade",
"svelte",
"typescript"
- ]
+ ],
+ "bin": true
+ },
+ "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==",
@@ -438,24 +553,31 @@
]
},
"typescript@5.7.3": {
- "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="
+ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
+ "bin": true
},
"vite@6.3.2_picomatch@4.0.2": {
"integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==",
"dependencies": [
"esbuild",
"fdir",
- "fsevents",
"picomatch",
"postcss",
"rollup",
"tinyglobby"
- ]
+ ],
+ "optionalDependencies": [
+ "fsevents"
+ ],
+ "bin": true
},
"vitefu@1.0.6_vite@6.3.2__picomatch@4.0.2": {
"integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==",
"dependencies": [
"vite"
+ ],
+ "optionalPeers": [
+ "vite"
]
},
"zimmerframe@1.1.2": {
@@ -470,7 +592,10 @@
"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:mutex-ts@^1.2.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/index.html b/index.html
index f71d006..adcfab3 100644
--- a/index.html
+++ b/index.html
@@ -1,4 +1,4 @@
-
+
diff --git a/package.json b/package.json
index 59269d2..3b293c0 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,10 @@
"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",
+ "mutex-ts": "^1.2.1",
+ "svelte-infinite-loading": "^1.4.0"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.3",
diff --git a/public/vite.svg b/public/vite.svg
deleted file mode 100644
index e7b8dfb..0000000
--- a/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/App.svelte b/src/App.svelte
index 6a635b1..c6e7534 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -1,48 +1,71 @@
- {#await accountsPromise}
-
Loading...
- {:then accountsData}
-
-
-
Home to {accountsData.length} accounts
-
- {#each accountsData as accountObject}
-
- {/each}
+ {#await accountsPromise}
+
Loading...
+ {:then accountsData}
+
+
+
Home to {accountsData.length} accounts
+
+ {#each accountsData as accountObject}
+
+ {/each}
+
+
{@html Config.FOOTER_TEXT}
-
{@html Config.FOOTER_TEXT}
-
- {:catch error}
-
Error: {error.message}
- {/await}
+ {:catch error}
+
Error: {error.message}
+ {/await}
- {#await postsPromise}
-
Loading...
- {:then postsData}
- {#each postsData as postObject}
+ {#each posts as postObject}
{/each}
+
- {/await}
diff --git a/src/lib/PostComponent.svelte b/src/lib/PostComponent.svelte
index 894540d..43ad667 100644
--- a/src/lib/PostComponent.svelte
+++ b/src/lib/PostComponent.svelte
@@ -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 @@
{post.timenotstamp} {moment(post.timenotstamp).isBefore(moment().subtract(1, "month"))
+ ? moment(post.timenotstamp).format("MMM D, YYYY")
+ : moment(post.timenotstamp).fromNow()}
@@ -110,7 +113,7 @@
←
@@ -122,7 +125,7 @@
→
@@ -131,12 +134,20 @@
{/if}
{#if post.videosLinkCid}
+
{/if}
+ {#if post.gifLink}
+
+ {/if}
@@ -147,8 +158,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 +168,38 @@
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;
+ display: block;
+ color: var(--text-color);
font-size: 1.2em;
padding: 0;
margin: 0;
+ overflow-wrap:normal;
+ word-wrap: break-word;
+ word-break: break-word;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ width: 100%;
}
#handle {
- color: #8054f0;
+ display: block;
+ 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 +209,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,19 +229,31 @@
#postText {
margin: 0;
padding: 0;
+ overflow-wrap: break-word;
+ word-wrap: normal;
+ word-break: break-word;
+ hyphens: none;
}
#headerText {
margin-left: 10px;
font-size: 0.9em;
text-align: start;
- overflow-wrap: break-word;
+ word-break: break-word;
+ max-width: 80%;
+ max-height: 95%;
overflow: hidden;
+ align-self: flex-start;
+ margin-top: auto;
+ margin-bottom: auto;
}
#avatar {
- height: 100%;
+ height: 60px;
+ width: 60px;
margin: 0px;
margin-left: 0px;
- border-right: #8054f0 1px solid;
+ overflow: hidden;
+ object-fit: cover;
+ border-right: var(--border-color) 1px solid;
}
#carouselContainer {
position: relative;
@@ -245,16 +278,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;
diff --git a/src/lib/pdsfetch.ts b/src/lib/pdsfetch.ts
index d48e1b9..20120fb 100644
--- a/src/lib/pdsfetch.ts
+++ b/src/lib/pdsfetch.ts
@@ -13,16 +13,21 @@ import {
WebDidDocumentResolver,
} from "@atcute/identity-resolver";
import { Config } from "../../config";
+import { Mutex } from "mutex-ts"
// import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons";
// import { AppBskyFeedPost } from "@atcute/client/lexicons";
// 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;
@@ -42,13 +47,14 @@ class Post {
replyingUri: atUriObject | null;
imagesCid: string[] | null;
videosLinkCid: string | null;
+ gifLink: string | null;
constructor(
record: ComAtprotoRepoListRecords.Record,
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;
@@ -65,10 +71,11 @@ class Post {
this.quotingUri = null;
this.imagesCid = null;
this.videosLinkCid = null;
+ this.gifLink = 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 +88,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;
@@ -92,6 +99,11 @@ class Post {
break;
}
break;
+ case "app.bsky.embed.external": // assuming that external embeds are gifs for now
+ if (post.embed.external.uri.includes(".gif")) {
+ this.gifLink = post.embed.external.uri;
+ }
+ break;
}
}
}
@@ -111,77 +123,50 @@ const rpc = new XRPC({
}),
});
-const getDidsFromPDS = async () => {
+const getDidsFromPDS = async (): Promise => {
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", {
- params: {
- repo: did,
- collection: "app.bsky.actor.profile",
- rkey: "self",
- },
- });
- const value = data.value as AppBskyActorProfile.Record;
- const handle = await blueskyHandleFromDid(did);
- const account: AccountMetadata = {
- did: did,
- handle: handle,
- displayName: value.displayName || "",
- avatarCid: null,
- };
- if (value.avatar) {
- account.avatarCid = value.avatar.ref["$link"];
- }
- return account;
- }
- catch (e) {
- console.error(`Error fetching metadata for ${did}:`, e);
- return {
- did: "error",
- displayName: "",
+ const { data } = await rpc.get("com.atproto.repo.getRecord", {
+ params: {
+ repo: did,
+ collection: "app.bsky.actor.profile",
+ rkey: "self",
+ },
+ });
+ const value = data.value as AppBskyActorProfile.Record;
+ const handle = await blueskyHandleFromDid(did);
+ const account: AccountMetadata = {
+ did: did,
+ handle: handle,
+ displayName: value.displayName || "",
avatarCid: null,
};
+ if (value.avatar) {
+ account.avatarCid = value.avatar.ref["$link"];
+ }
+ return account;
+ } catch (e) {
+ console.error(`Error fetching metadata for ${did}:`, e);
+ return null;
}
};
-const getAllMetadataFromPds = async () => {
+const getAllMetadataFromPds = async (): Promise => {
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 +202,151 @@ 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);
- })
- );
- posts.sort((a, b) => b.timestamp - a.timestamp);
- return posts;
+ }
+ });
+ 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;
+ });
+ if (filtered.length > 0) {
+ postAcc.account.currentCursor = processAtUri(filtered[filtered.length - 1].uri).rkey;
+ }
+ return {
+ posts: filtered,
+ account: postAcc.account,
+ };
});
- console.log(data);
+ return filteredPosts;
};
-export { fetchAllPosts, getAllMetadataFromPds, Post };
+
+const postsMutex = new Mutex();
+// nightmare function. However it works so I am not touching it
+const getNextPosts = async () => {
+ const release = await postsMutex.obtain();
+ 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);
+ });
+ // release the mutex
+ release();
+ 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 };
diff --git a/src/main.ts b/src/main.ts
index 664a057..d47b930 100644
--- a/src/main.ts
+++ b/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;
diff --git a/svelte.config.js b/svelte.config.js
index b0683fd..de2ddd6 100644
--- a/svelte.config.js
+++ b/svelte.config.js
@@ -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(),
-}
+};
diff --git a/vite.config.ts b/vite.config.ts
index d32eba1..20d2272 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -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()],
-})
+});