diff --git a/.forgejo/workflows/deploy.yaml b/.forgejo/workflows/deploy.yaml new file mode 100644 index 0000000..c6a05d0 --- /dev/null +++ b/.forgejo/workflows/deploy.yaml @@ -0,0 +1,58 @@ +name: Deploy + +on: + push: + branches: + - main + - astra/ci + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Setup Deno + uses: https://github.com/denoland/setup-deno@v2 + + - name: Install dependencies + run: deno install + + - name: Build project + run: deno task build + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + cat > ~/.ssh/config << EOF + Host deploy + HostName ${{ vars.SERVER_HOST }} + User ${{ vars.SERVER_USER }} + IdentityFile ~/.ssh/id_ed25519 + StrictHostKeyChecking accept-new + BatchMode yes + PasswordAuthentication no + PubkeyAuthentication yes + EOF + chmod 600 ~/.ssh/config + ssh-keyscan -H ${{ vars.SERVER_HOST }} >> ~/.ssh/known_hosts + echo "Deploying to ${{ vars.SERVER_HOST }} as ${{ vars.SERVER_USER }} to /var/www/pds/${{ github.ref_name }}" + + - name: Debug SSH Connection + run: ssh -v deploy echo "SSH Connection Successful" + + - name: Create folder if not exists + run: ssh deploy "mkdir -p /var/www/pds/${{ github.ref_name }}" + + - name: Deploy via SCP + run: scp -r ./dist/* deploy:/var/www/pds/${{ github.ref_name }} 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 af29271..8d09cf6 100644 --- a/config.ts +++ b/config.ts @@ -2,15 +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"; -} \ No newline at end of file + /** + * 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 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, 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 a8a0033..c6e7534 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,40 +1,71 @@
- {#await accountsPromise} -

Loading...

- {:then accountsData} -
-

ATProto PDS

-

Home to {accountsData.length} accounts

- {#each accountsData as accountObject} - - {/each} -
- {:catch error} -

Error: {error.message}

- {/await} + {#await accountsPromise} +

Loading...

+ {:then accountsData} +
+

ATProto PDS

+

Home to {accountsData.length} accounts

+
+ {#each accountsData as accountObject} + + {/each} +
+

{@html Config.FOOTER_TEXT}

+
+ {: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/app.css b/src/app.css index 2232051..50da734 100644 --- a/src/app.css +++ b/src/app.css @@ -1,15 +1,44 @@ @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; + padding: 0; + margin: 0; +} +::-webkit-scrollbar-thumb { + background: transparent; + border-radius: 0; +} +::-webkit-scrollbar-track { + background: transparent; + border-radius: 0; +} +::-webkit-scrollbar-corner { + background: transparent; + border-radius: 0; +} +::-webkit-scrollbar-button { + background: transparent; + border-radius: 0; } - * { - scrollbar-width: thin; + scrollbar-width: none; scrollbar-color: transparent transparent; -ms-overflow-style: none; /* IE and Edge */ -webkit-overflow-scrolling: touch; @@ -18,11 +47,12 @@ 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; } body { @@ -31,11 +61,15 @@ 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); + overflow-wrap: break-word; + word-wrap: normal; + word-break: break-word; + hyphens: none; } h1 { @@ -45,9 +79,10 @@ h1 { #app { max-width: 1400px; - margin: 0 auto; - padding: 2rem; + width: 100%; + margin: 0; + padding: 0; + margin-left: auto; + margin-right: auto; text-align: center; } - - diff --git a/src/assets/svelte.svg b/src/assets/svelte.svg deleted file mode 100644 index c5e0848..0000000 --- a/src/assets/svelte.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/lib/AccountComponent.svelte b/src/lib/AccountComponent.svelte index 4c987c8..880db3f 100644 --- a/src/lib/AccountComponent.svelte +++ b/src/lib/AccountComponent.svelte @@ -1,49 +1,49 @@ -
- {#if account.avatarCid} - avatar of {account.displayName} - {/if} -
- {account.displayName || account.handle || account.did} -
+
+ {#if account.avatarCid} + avatar of {account.displayName} + {/if} +
+ {account.displayName || account.handle || account.did}
+
diff --git a/src/lib/PostComponent.svelte b/src/lib/PostComponent.svelte index f077582..43ad667 100644 --- a/src/lib/PostComponent.svelte +++ b/src/lib/PostComponent.svelte @@ -1,7 +1,59 @@
@@ -14,110 +66,253 @@ /> {/if}
{#if post.replyingUri} replying to {post.replyingUri.repo} {/if} -

{post.text}

- {#if post.quotingUri} quoting {post.quotingUri.repo} {/if} - {#if post.imagesCid} -
- {#each post.imagesCid as imageLink} - Post Image - {/each} +
{post.text}
+ {#if post.imagesCid && post.imagesCid.length > 0} +
+ Post Image {currentImageIndex + 1} of {post.imagesCid.length} + + {#if post.imagesCid.length > 1} +
+ +
+ {#each post.imagesCid as _, i} +
+ {/each} +
+ +
+ {/if}
{/if} {#if post.videosLinkCid} + + {/if} + {#if post.gifLink} + Post GIF {/if}
diff --git a/src/lib/pdsfetch.ts b/src/lib/pdsfetch.ts index ed24c45..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; @@ -32,6 +37,7 @@ class Post { authorDid: string; authorAvatarCid: string | null; postCid: string; + recordName: string; authorHandle: string; displayName: string; text: string; @@ -41,12 +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 = processAtUri(record.uri).rkey; this.authorDid = account.did; this.authorAvatarCid = account.avatarCid; this.authorHandle = account.handle; @@ -63,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": @@ -79,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; @@ -90,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; } } } @@ -109,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: 5, - }, - }); - 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) => { @@ -215,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()], -}) +});