diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96f7eb1e..588ffa70 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,8 +21,21 @@ jobs: node-version: 22 cache: 'yarn' + - name: Cache electron binaries + uses: actions/cache@v4 + with: + path: ~/.cache/electron + key: ${{ runner.os }}-electron-${{ hashFiles('**/yarn.lock') }} + restore-keys: ${{ runner.os }}-electron- + - name: Install dependencies - run: yarn install --frozen-lockfile --ignore-engines + run: | + for i in 1 2 3; do + echo "yarn install attempt $i" + yarn install --frozen-lockfile --ignore-engines --network-timeout 100000 --network-concurrency 1 && break + sleep 5 + [ "$i" = "3" ] && exit 1 + done - name: Download IPFS run: node electron/download-ipfs && chmod +x bin/linux/ipfs @@ -75,8 +88,21 @@ jobs: # Use pip with --break-system-packages for CI environment pip3 install --break-system-packages setuptools || pip3 install --user setuptools || true + - name: Cache electron binaries + uses: actions/cache@v4 + with: + path: ~/Library/Caches/electron + key: ${{ runner.os }}-electron-${{ hashFiles('**/yarn.lock') }} + restore-keys: ${{ runner.os }}-electron- + - name: Install dependencies - run: yarn install --frozen-lockfile --ignore-engines + run: | + for i in 1 2 3; do + echo "yarn install attempt $i" + yarn install --frozen-lockfile --ignore-engines --network-timeout 100000 --network-concurrency 1 && break + sleep 5 + [ "$i" = "3" ] && exit 1 + done - name: Download IPFS run: node electron/download-ipfs && chmod +x bin/mac/ipfs @@ -135,8 +161,21 @@ jobs: # Use pip with --break-system-packages for CI environment pip3 install --break-system-packages setuptools || pip3 install --user setuptools || true + - name: Cache electron binaries + uses: actions/cache@v4 + with: + path: ~/Library/Caches/electron + key: ${{ runner.os }}-electron-${{ hashFiles('**/yarn.lock') }} + restore-keys: ${{ runner.os }}-electron- + - name: Install dependencies - run: yarn install --frozen-lockfile --ignore-engines + run: | + for i in 1 2 3; do + echo "yarn install attempt $i" + yarn install --frozen-lockfile --ignore-engines --network-timeout 100000 --network-concurrency 1 && break + sleep 5 + [ "$i" = "3" ] && exit 1 + done - name: Download IPFS run: node electron/download-ipfs && chmod +x bin/mac/ipfs @@ -195,8 +234,22 @@ jobs: node-version: 22 cache: 'yarn' + - name: Cache electron binaries + uses: actions/cache@v4 + with: + path: ~/AppData/Local/electron/Cache + key: ${{ runner.os }}-electron-${{ hashFiles('**/yarn.lock') }} + restore-keys: ${{ runner.os }}-electron- + - name: Install dependencies - run: yarn install --frozen-lockfile --ignore-engines --network-timeout 100000 --network-concurrency 1 + shell: bash + run: | + for i in 1 2 3; do + echo "yarn install attempt $i" + yarn install --frozen-lockfile --ignore-engines --network-timeout 100000 --network-concurrency 1 && break + sleep 5 + [ "$i" = "3" ] && exit 1 + done - name: Download IPFS run: node electron/download-ipfs @@ -235,51 +288,3 @@ jobs: ls -R dist exit 1 fi - - test-android: - name: Test Android - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Node.js v22 - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'yarn' - - - name: Setup Java 17 - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - - name: Install dependencies - run: yarn install --frozen-lockfile --ignore-engines - - - name: Build React App - run: yarn build - env: - CI: '' - - - name: Sync Capacitor - run: npx cap sync android - - - name: Build Android APK - run: | - cd android - chmod +x gradlew - ./gradlew assembleDebug - - - name: Verify APK exists - run: | - APK=$(find android/app/build/outputs/apk -name "*.apk" | head -n 1) - if [ -n "$APK" ]; then - echo "✓ APK built successfully: $APK" - ls -lh "$APK" - else - echo "✗ No APK found" - exit 1 - fi diff --git a/package.json b/package.json index 1851ff0f..84a9fff7 100644 --- a/package.json +++ b/package.json @@ -34,13 +34,13 @@ "lodash": "4.17.21", "memoizee": "0.4.15", "prettier": "3.0.3", - "react": "19.1.0", - "react-dom": "19.1.0", + "react": "19.1.2", + "react-dom": "19.1.2", "react-i18next": "13.2.2", "react-markdown": "8.0.6", "react-router-dom": "6.16.0", "react-router-hash-link": "2.4.3", - "react-virtuoso": "4.12.3", + "react-virtuoso": "4.17.0", "rehype-raw": "7.0.0", "rehype-sanitize": "5.0.1", "remark-gfm": "3.0.1", @@ -93,9 +93,9 @@ ] }, "devDependencies": { - "@capacitor/android": "7.2.0", - "@capacitor/cli": "7.2.0", - "@capacitor/core": "7.2.0", + "@capacitor/android": "7.4.4", + "@capacitor/cli": "7.4.4", + "@capacitor/core": "7.4.4", "@electron/rebuild": "4.0.0", "@react-scan/vite-plugin-react-scan": "0.1.8", "@types/memoizee": "0.4.9", @@ -112,9 +112,9 @@ "crypto-browserify": "3.12.1", "cz-conventional-changelog": "3.3.0", "decompress": "4.2.1", - "electron": "36.0.0", + "electron": "36.9.5", "electron-builder": "24.13.2", - "eslint": "8.56.0", + "eslint": "8.57.1", "eslint-config-react-app": "7.0.1", "eslint-plugin-react": "7.37.4", "eslint-plugin-react-compiler": "19.1.0-rc.2", @@ -126,11 +126,14 @@ "react-grab": "0.0.18", "react-scan": "0.4.3", "stream-browserify": "3.0.0", - "vite": "6.2.1", + "vite": "6.4.1", "vite-plugin-eslint": "1.8.1", "vite-plugin-node-polyfills": "0.23.0", "vite-plugin-pwa": "0.21.1", - "wait-on": "7.0.1" + "wait-on": "9.0.3" + }, + "resolutions": { + "js-yaml": "4.1.1" }, "main": "electron/main.js", "build": { diff --git a/public/assets/banners/banner-3.gif b/public/assets/banners/banner-3.gif index c1564f10..cceba530 100644 Binary files a/public/assets/banners/banner-3.gif and b/public/assets/banners/banner-3.gif differ diff --git a/public/assets/banners/banner-4.gif b/public/assets/banners/banner-4.gif deleted file mode 100644 index cceba530..00000000 Binary files a/public/assets/banners/banner-4.gif and /dev/null differ diff --git a/public/assets/banners/banner-5.png b/public/assets/banners/banner-4.png similarity index 100% rename from public/assets/banners/banner-5.png rename to public/assets/banners/banner-4.png diff --git a/public/translations/ar/default.json b/public/translations/ar/default.json index e56a340b..fd5d3ecd 100644 --- a/public/translations/ar/default.json +++ b/public/translations/ar/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "المنتديات التي تقوم بالإشراف عليها", "more_threads_last_year": "{{count}} مواضيع منذ {{currentTimeFilterName}} — [<1>عرض المزيد من المواضيع من السنة الماضية]", "moderator_of_this_board": "هذا المستخدم هو مشرف على هذا المنتدى", - "administrator_of_this_board": "هذا المستخدم هو مسؤول عن هذا المنتدى" + "administrator_of_this_board": "هذا المستخدم هو مسؤول عن هذا المنتدى", + "copy_content_id": "نسخ معرف المحتوى" } diff --git a/public/translations/bn/default.json b/public/translations/bn/default.json index fc37522d..4e230c99 100644 --- a/public/translations/bn/default.json +++ b/public/translations/bn/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "আপনি যে বোর্ডগুলি পরিচালনা করেন", "more_threads_last_year": "{{count}} থ্রেড {{currentTimeFilterName}} থেকে — [<1>গত বছরের আরও থ্রেড দেখান]", "moderator_of_this_board": "এই ব্যবহারকারী এই বোর্ডের একজন মডারেটর", - "administrator_of_this_board": "এই ব্যবহারকারী এই বোর্ডের একজন প্রশাসক" + "administrator_of_this_board": "এই ব্যবহারকারী এই বোর্ডের একজন প্রশাসক", + "copy_content_id": "কনটেন্ট আইডি কপি করুন" } diff --git a/public/translations/cs/default.json b/public/translations/cs/default.json index 90fbf6a1..e1f7e5d8 100644 --- a/public/translations/cs/default.json +++ b/public/translations/cs/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "Fóra, která moderujete", "more_threads_last_year": "{{count}} příspěvků od {{currentTimeFilterName}} — [<1>Zobrazit více příspěvků z minulého roku]", "moderator_of_this_board": "Tento uživatel je moderátorem tohoto fóra", - "administrator_of_this_board": "Tento uživatel je administrátorem tohoto fóra" + "administrator_of_this_board": "Tento uživatel je administrátorem tohoto fóra", + "copy_content_id": "kopírovat ID obsahu" } diff --git a/public/translations/da/default.json b/public/translations/da/default.json index f2fa04d2..46ceeb0e 100644 --- a/public/translations/da/default.json +++ b/public/translations/da/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "Boards du modererer", "more_threads_last_year": "{{count}} tråde siden {{currentTimeFilterName}} — [<1>Vis flere tråde fra sidste år]", "moderator_of_this_board": "Denne bruger er moderator for dette board", - "administrator_of_this_board": "Denne bruger er administrator for dette board" + "administrator_of_this_board": "Denne bruger er administrator for dette board", + "copy_content_id": "kopier indhold-ID" } diff --git a/public/translations/de/default.json b/public/translations/de/default.json index 093ccb45..854bd4c2 100644 --- a/public/translations/de/default.json +++ b/public/translations/de/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "Boards, die Sie moderieren", "more_threads_last_year": "{{count}} Threads seit {{currentTimeFilterName}} — [<1>Mehr Threads vom letzten Jahr anzeigen]", "moderator_of_this_board": "Dieser Benutzer ist Moderator dieses Boards", - "administrator_of_this_board": "Dieser Benutzer ist Administrator dieses Boards" + "administrator_of_this_board": "Dieser Benutzer ist Administrator dieses Boards", + "copy_content_id": "Inhalts-ID kopieren" } diff --git a/public/translations/el/default.json b/public/translations/el/default.json index 2891a27d..81f2d598 100644 --- a/public/translations/el/default.json +++ b/public/translations/el/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "Boards που διαχειρίζεστε", "more_threads_last_year": "{{count}} νήματα από {{currentTimeFilterName}} — [<1>Προβολή περισσότερων νημάτων από το περασμένο έτος]", "moderator_of_this_board": "Αυτός ο χρήστης είναι συντονιστής αυτού του πίνακα", - "administrator_of_this_board": "Αυτός ο χρήστης είναι διαχειριστής αυτού του πίνακα" + "administrator_of_this_board": "Αυτός ο χρήστης είναι διαχειριστής αυτού του πίνακα", + "copy_content_id": "αντιγραφή ID περιεχομένου" } diff --git a/public/translations/en/default.json b/public/translations/en/default.json index 8d89dea4..6e778db7 100644 --- a/public/translations/en/default.json +++ b/public/translations/en/default.json @@ -296,5 +296,6 @@ "boards_you_moderate": "boards you moderate", "more_threads_last_year": "{{count}} threads since {{currentTimeFilterName}} — [<1>Show More Threads from Last Year]", "moderator_of_this_board": "This user is a moderator of this board", - "administrator_of_this_board": "This user is an administrator of this board" + "administrator_of_this_board": "This user is an administrator of this board", + "copy_content_id": "Copy content ID" } diff --git a/public/translations/es/default.json b/public/translations/es/default.json index 40bed6fa..1ea2d451 100644 --- a/public/translations/es/default.json +++ b/public/translations/es/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "tableros que moderas", "more_threads_last_year": "{{count}} hilos desde {{currentTimeFilterName}} — [<1>Ver más hilos del año pasado]", "moderator_of_this_board": "Este usuario es moderador de este tablero", - "administrator_of_this_board": "Este usuario es administrador de este tablero" + "administrator_of_this_board": "Este usuario es administrador de este tablero", + "copy_content_id": "Copiar ID de contenido" } diff --git a/public/translations/fa/default.json b/public/translations/fa/default.json index bf1750f2..efe055d4 100644 --- a/public/translations/fa/default.json +++ b/public/translations/fa/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "تابلوهایی که مدیریت می‌کنید", "more_threads_last_year": "{{count}} پست از {{currentTimeFilterName}} — [<1>نمایش بیشتر پست‌های سال گذشته]", "moderator_of_this_board": "این کاربر مدیر این انجمن است", - "administrator_of_this_board": "این کاربر مدیرکل این انجمن است" + "administrator_of_this_board": "این کاربر مدیرکل این انجمن است", + "copy_content_id": "کپی شناسه محتوا" } diff --git a/public/translations/fi/default.json b/public/translations/fi/default.json index a7331be5..5812e9ac 100644 --- a/public/translations/fi/default.json +++ b/public/translations/fi/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "lautakunnat, joita valvot", "more_threads_last_year": "{{count}} ketjua sitten {{currentTimeFilterName}} — [<1>Näytä lisää viime vuoden ketjuja]", "moderator_of_this_board": "Tämä käyttäjä on tämän taulun moderaattori", - "administrator_of_this_board": "Tämä käyttäjä on tämän taulun järjestelmänvalvoja" + "administrator_of_this_board": "Tämä käyttäjä on tämän taulun järjestelmänvalvoja", + "copy_content_id": "kopioi sisältö-ID" } diff --git a/public/translations/fil/default.json b/public/translations/fil/default.json index 44caa9fc..cef59755 100644 --- a/public/translations/fil/default.json +++ b/public/translations/fil/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "mga board na iyong minomoderate", "more_threads_last_year": "{{count}} mga thread mula {{currentTimeFilterName}} — [<1>Ipakita ang higit pang mga thread mula noong nakaraang taon]", "moderator_of_this_board": "Ang user na ito ay moderator ng board na ito", - "administrator_of_this_board": "Ang user na ito ay administrator ng board na ito" + "administrator_of_this_board": "Ang user na ito ay administrator ng board na ito", + "copy_content_id": "kopyahin ang content ID" } diff --git a/public/translations/fr/default.json b/public/translations/fr/default.json index 2d40dbfa..bf427dff 100644 --- a/public/translations/fr/default.json +++ b/public/translations/fr/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "boards que vous modérez", "more_threads_last_year": "{{count}} fils depuis {{currentTimeFilterName}} — [<1>Afficher plus de fils de l'année dernière]", "moderator_of_this_board": "Cet utilisateur est modérateur de ce tableau", - "administrator_of_this_board": "Cet utilisateur est administrateur de ce tableau" + "administrator_of_this_board": "Cet utilisateur est administrateur de ce tableau", + "copy_content_id": "Copier l'ID du contenu" } diff --git a/public/translations/he/default.json b/public/translations/he/default.json index 3df2c535..71d414a1 100644 --- a/public/translations/he/default.json +++ b/public/translations/he/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "לוחות שאתה מפקח עליהם", "more_threads_last_year": "{{count}} חוטים מאז {{currentTimeFilterName}} — [<1>הצג עוד חוטים מהשנה שעברה]", "moderator_of_this_board": "משתמש זה הוא מנהל של הלוח הזה", - "administrator_of_this_board": "משתמש זה הוא מנהל מערכת של הלוח הזה" + "administrator_of_this_board": "משתמש זה הוא מנהל מערכת של הלוח הזה", + "copy_content_id": "העתק מזהה תוכן" } diff --git a/public/translations/hi/default.json b/public/translations/hi/default.json index 6e928097..a863332b 100644 --- a/public/translations/hi/default.json +++ b/public/translations/hi/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "बोर्ड जो आप मॉडरेट करते हैं", "more_threads_last_year": "{{count}} थ्रेड्स {{currentTimeFilterName}} से — [<1>पिछले साल के अधिक थ्रेड्स दिखाएं]", "moderator_of_this_board": "यह उपयोगकर्ता इस बोर्ड का मॉडरेटर है", - "administrator_of_this_board": "यह उपयोगकर्ता इस बोर्ड का प्रशासक है" + "administrator_of_this_board": "यह उपयोगकर्ता इस बोर्ड का प्रशासक है", + "copy_content_id": "कंटेंट ID कॉपी करें" } diff --git a/public/translations/hu/default.json b/public/translations/hu/default.json index cb312722..361d0ec3 100644 --- a/public/translations/hu/default.json +++ b/public/translations/hu/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "boardok, amelyeket moderálsz", "more_threads_last_year": "{{count}} szál {{currentTimeFilterName}} óta — [<1>Több szál megjelenítése a múlt évből]", "moderator_of_this_board": "Ez a felhasználó ennek a táblának a moderátora", - "administrator_of_this_board": "Ez a felhasználó ennek a táblának a rendszergazdája" + "administrator_of_this_board": "Ez a felhasználó ennek a táblának a rendszergazdája", + "copy_content_id": "tartalom ID másolása" } diff --git a/public/translations/id/default.json b/public/translations/id/default.json index 667a8b49..ded3510c 100644 --- a/public/translations/id/default.json +++ b/public/translations/id/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "board yang Anda moderasi", "more_threads_last_year": "{{count}} thread sejak {{currentTimeFilterName}} — [<1>Tampilkan lebih banyak thread dari tahun lalu]", "moderator_of_this_board": "Pengguna ini adalah moderator papan ini", - "administrator_of_this_board": "Pengguna ini adalah administrator papan ini" + "administrator_of_this_board": "Pengguna ini adalah administrator papan ini", + "copy_content_id": "salin ID konten" } diff --git a/public/translations/it/default.json b/public/translations/it/default.json index 56403309..20bbbd47 100644 --- a/public/translations/it/default.json +++ b/public/translations/it/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "board che moderi", "more_threads_last_year": "{{count}} thread da {{currentTimeFilterName}} — [<1>Mostra altri thread dell'anno scorso]", "moderator_of_this_board": "Questo utente è moderatore di questa bacheca", - "administrator_of_this_board": "Questo utente è amministratore di questa bacheca" + "administrator_of_this_board": "Questo utente è amministratore di questa bacheca", + "copy_content_id": "Copia ID contenuto" } diff --git a/public/translations/ja/default.json b/public/translations/ja/default.json index 7c2ce43b..68febef2 100644 --- a/public/translations/ja/default.json +++ b/public/translations/ja/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "あなたが管理するボード", "more_threads_last_year": "{{count}} スレッド {{currentTimeFilterName}} から — [<1>去年のスレッドをさらに表示]", "moderator_of_this_board": "このユーザーはこのボードのモデレーターです", - "administrator_of_this_board": "このユーザーはこのボードの管理者です" + "administrator_of_this_board": "このユーザーはこのボードの管理者です", + "copy_content_id": "コンテンツIDをコピー" } diff --git a/public/translations/ko/default.json b/public/translations/ko/default.json index defb7881..b0def60b 100644 --- a/public/translations/ko/default.json +++ b/public/translations/ko/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "당신이 관리하는 게시판", "more_threads_last_year": "{{count}} 스레드 {{currentTimeFilterName}} 이후 — [<1>작년의 스레드 더 보기]", "moderator_of_this_board": "이 사용자는 이 게시판의 관리자입니다", - "administrator_of_this_board": "이 사용자는 이 게시판의 관리자입니다" + "administrator_of_this_board": "이 사용자는 이 게시판의 관리자입니다", + "copy_content_id": "콘텐츠 ID 복사" } diff --git a/public/translations/mr/default.json b/public/translations/mr/default.json index d6cf0b71..34fda63c 100644 --- a/public/translations/mr/default.json +++ b/public/translations/mr/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "तुम्ही ज्यांचे नियंत्रण करता अशा बोर्ड्स", "more_threads_last_year": "{{count}} ध糀ं {{currentTimeFilterName}} पासून — [<1>गेल्या वर्षातील आणखी थ्रेड्स दाखवा]", "moderator_of_this_board": "हा वापरकर्ता या बोर्डचा मॉडरेटर आहे", - "administrator_of_this_board": "हा वापरकर्ता या बोर्डचा प्रशासक आहे" + "administrator_of_this_board": "हा वापरकर्ता या बोर्डचा प्रशासक आहे", + "copy_content_id": "कंटेंट ID कॉपी करा" } diff --git a/public/translations/nl/default.json b/public/translations/nl/default.json index 4589533a..1909770e 100644 --- a/public/translations/nl/default.json +++ b/public/translations/nl/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "boards die je beheert", "more_threads_last_year": "{{count}} threads sinds {{currentTimeFilterName}} — [<1>Meer threads van afgelopen jaar tonen]", "moderator_of_this_board": "Deze gebruiker is moderator van dit bord", - "administrator_of_this_board": "Deze gebruiker is beheerder van dit bord" + "administrator_of_this_board": "Deze gebruiker is beheerder van dit bord", + "copy_content_id": "kopieer inhoud-ID" } diff --git a/public/translations/no/default.json b/public/translations/no/default.json index 1328437f..ca400413 100644 --- a/public/translations/no/default.json +++ b/public/translations/no/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "boards du modererer", "more_threads_last_year": "{{count}} tråder siden {{currentTimeFilterName}} — [<1>Vis flere tråder fra forrige år]", "moderator_of_this_board": "Denne brukeren er moderator for dette tavlet", - "administrator_of_this_board": "Denne brukeren er administrator for dette tavlet" + "administrator_of_this_board": "Denne brukeren er administrator for dette tavlet", + "copy_content_id": "kopier innhold-ID" } diff --git a/public/translations/pl/default.json b/public/translations/pl/default.json index c6f46331..7fe844cf 100644 --- a/public/translations/pl/default.json +++ b/public/translations/pl/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "boards, które moderujesz", "more_threads_last_year": "{{count}} wątki od {{currentTimeFilterName}} — [<1>Pokaż więcej wątków z zeszłego roku]", "moderator_of_this_board": "Ten użytkownik jest moderatorem tego forum", - "administrator_of_this_board": "Ten użytkownik jest administratorem tego forum" + "administrator_of_this_board": "Ten użytkownik jest administratorem tego forum", + "copy_content_id": "skopiuj ID treści" } diff --git a/public/translations/pt/default.json b/public/translations/pt/default.json index 8bc42dba..a0ece426 100644 --- a/public/translations/pt/default.json +++ b/public/translations/pt/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "boards que você modera", "more_threads_last_year": "{{count}} tópicos desde {{currentTimeFilterName}} — [<1>Mostrar mais tópicos do ano passado]", "moderator_of_this_board": "Este usuário é moderador deste quadro", - "administrator_of_this_board": "Este usuário é administrador deste quadro" + "administrator_of_this_board": "Este usuário é administrador deste quadro", + "copy_content_id": "Copiar ID do conteúdo" } diff --git a/public/translations/ro/default.json b/public/translations/ro/default.json index 4b49f6ae..1d9f92e8 100644 --- a/public/translations/ro/default.json +++ b/public/translations/ro/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "boards pe care le moderezi", "more_threads_last_year": "{{count}} subiecte din {{currentTimeFilterName}} — [<1>Afișați mai multe subiecte de anul trecut]", "moderator_of_this_board": "Acest utilizator este moderator al acestui forum", - "administrator_of_this_board": "Acest utilizator este administrator al acestui forum" + "administrator_of_this_board": "Acest utilizator este administrator al acestui forum", + "copy_content_id": "copiați ID-ul conținutului" } diff --git a/public/translations/ru/default.json b/public/translations/ru/default.json index 35807a87..6c812038 100644 --- a/public/translations/ru/default.json +++ b/public/translations/ru/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "boards, которые вы модерируете", "more_threads_last_year": "{{count}} темы с {{currentTimeFilterName}} — [<1>Показать больше тем с прошлого года]", "moderator_of_this_board": "Этот пользователь является модератором этого форума", - "administrator_of_this_board": "Этот пользователь является администратором этого форума" + "administrator_of_this_board": "Этот пользователь является администратором этого форума", + "copy_content_id": "копировать ID контента" } diff --git a/public/translations/sq/default.json b/public/translations/sq/default.json index 68d5b71b..990ce141 100644 --- a/public/translations/sq/default.json +++ b/public/translations/sq/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "boards që i moderoni", "more_threads_last_year": "{{count}} tema nga {{currentTimeFilterName}} — [<1>Shfaq më shumë tema nga viti i kaluar]", "moderator_of_this_board": "Ky përdorues është moderator i këtij bordi", - "administrator_of_this_board": "Ky përdorues është administrator i këtij bordi" + "administrator_of_this_board": "Ky përdorues është administrator i këtij bordi", + "copy_content_id": "kopjo ID e përmbajtjes" } diff --git a/public/translations/sv/default.json b/public/translations/sv/default.json index 63f51e35..4108c506 100644 --- a/public/translations/sv/default.json +++ b/public/translations/sv/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "boards du modererar", "more_threads_last_year": "{{count}} trådar sedan {{currentTimeFilterName}} — [<1>Visa fler trådar från förra året]", "moderator_of_this_board": "Denna användare är moderator för detta forum", - "administrator_of_this_board": "Denna användare är administratör för detta forum" + "administrator_of_this_board": "Denna användare är administratör för detta forum", + "copy_content_id": "kopiera innehålls-ID" } diff --git a/public/translations/te/default.json b/public/translations/te/default.json index 703f6c34..c574e05c 100644 --- a/public/translations/te/default.json +++ b/public/translations/te/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "మీరు మోడరేట్ చేసే బోర్డులు", "more_threads_last_year": "{{count}} థ్రెడ్‌లు {{currentTimeFilterName}} నుండి — [<1>గత సంవత్సరం నుండి మరిన్ని థ్రెడ్‌లను చూపించండి]", "moderator_of_this_board": "ఈ వినియోగదారు ఈ బోర్డ్ యొక్క మోడరేటర్", - "administrator_of_this_board": "ఈ వినియోగదారు ఈ బోర్డ్ యొక్క నిర్వాహకుడు" + "administrator_of_this_board": "ఈ వినియోగదారు ఈ బోర్డ్ యొక్క నిర్వాహకుడు", + "copy_content_id": "కంటెంట్ ID కాపీ చేయండి" } diff --git a/public/translations/th/default.json b/public/translations/th/default.json index bf847efd..17799c5d 100644 --- a/public/translations/th/default.json +++ b/public/translations/th/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "บอร์ดที่คุณดูแล", "more_threads_last_year": "{{count}} กระทู้ตั้งแต่ {{currentTimeFilterName}} — [<1>แสดงกระทู้จากปีที่แล้วเพิ่มเติม]", "moderator_of_this_board": "ผู้ใช้รายนี้เป็นผู้ดูแลของบอร์ดนี้", - "administrator_of_this_board": "ผู้ใช้รายนี้เป็นผู้ดูแลระบบของบอร์ดนี้" + "administrator_of_this_board": "ผู้ใช้รายนี้เป็นผู้ดูแลระบบของบอร์ดนี้", + "copy_content_id": "คัดลอก ID เนื้อหา" } diff --git a/public/translations/tr/default.json b/public/translations/tr/default.json index 59ff603a..9d26f691 100644 --- a/public/translations/tr/default.json +++ b/public/translations/tr/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "modere ettiğiniz panolar", "more_threads_last_year": "{{count}} konu {{currentTimeFilterName}} itibariyle — [<1>Geçen yıldan daha fazla konu göster]", "moderator_of_this_board": "Bu kullanıcı bu panonun moderatörüdür", - "administrator_of_this_board": "Bu kullanıcı bu panonun yöneticisidir" + "administrator_of_this_board": "Bu kullanıcı bu panonun yöneticisidir", + "copy_content_id": "içerik ID'sini kopyala" } diff --git a/public/translations/uk/default.json b/public/translations/uk/default.json index 89de9c18..430203fa 100644 --- a/public/translations/uk/default.json +++ b/public/translations/uk/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "boards, які ви модеруватимете", "more_threads_last_year": "{{count}} тем з {{currentTimeFilterName}} — [<1>Показати більше тем з минулого року]", "moderator_of_this_board": "Цей користувач є модератором цього форуму", - "administrator_of_this_board": "Цей користувач є адміністратором цього форуму" + "administrator_of_this_board": "Цей користувач є адміністратором цього форуму", + "copy_content_id": "копіювати ID контенту" } diff --git a/public/translations/ur/default.json b/public/translations/ur/default.json index 8cf794e9..96b60bd3 100644 --- a/public/translations/ur/default.json +++ b/public/translations/ur/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "وہ بورڈز جن کی آپ نگرانی کرتے ہیں", "more_threads_last_year": "{{count}} تھریڈز {{currentTimeFilterName}} سے — [<1>گزشتہ سال کے مزید تھریڈز دکھائیں]", "moderator_of_this_board": "یہ صارف اس بورڈ کا ماڈریٹر ہے", - "administrator_of_this_board": "یہ صارف اس بورڈ کا منتظم ہے" + "administrator_of_this_board": "یہ صارف اس بورڈ کا منتظم ہے", + "copy_content_id": "مواد کی شناخت کاپی کریں" } diff --git a/public/translations/vi/default.json b/public/translations/vi/default.json index f80e4f77..696a1102 100644 --- a/public/translations/vi/default.json +++ b/public/translations/vi/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "boards bạn điều hành", "more_threads_last_year": "{{count}} chủ đề từ {{currentTimeFilterName}} — [<1>Hiển thị thêm các chủ đề từ năm ngoái]", "moderator_of_this_board": "Người dùng này là người điều hành của bảng này", - "administrator_of_this_board": "Người dùng này là quản trị viên của bảng này" + "administrator_of_this_board": "Người dùng này là quản trị viên của bảng này", + "copy_content_id": "sao chép ID nội dung" } diff --git a/public/translations/zh/default.json b/public/translations/zh/default.json index 717ef31f..6ce7a1ba 100644 --- a/public/translations/zh/default.json +++ b/public/translations/zh/default.json @@ -295,5 +295,6 @@ "boards_you_moderate": "您管理的板块", "more_threads_last_year": "{{count}} 主题自 {{currentTimeFilterName}} — [<1>显示更多去年的主题]", "moderator_of_this_board": "此用户是此版块的版主", - "administrator_of_this_board": "此用户是此版块的管理员" + "administrator_of_this_board": "此用户是此版块的管理员", + "copy_content_id": "复制内容ID" } diff --git a/src/app.tsx b/src/app.tsx index f65f2e08..cadb588f 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -9,10 +9,8 @@ import useSpecialThemeStore from './stores/use-special-theme-store'; import useIsMobile from './hooks/use-is-mobile'; import useTheme from './hooks/use-theme'; import { useDefaultSubplebbits } from './hooks/use-default-subplebbits'; -import { getSubplebbitAddress } from './lib/utils/route-utils'; +import { getSubplebbitAddress, isPostRoute, isPendingPostRoute } from './lib/utils/route-utils'; import styles from './app.module.css'; -import Board from './views/board'; -import Catalog from './views/catalog'; import FAQ from './views/faq'; import Home from './views/home'; import NotFound from './views/not-found'; @@ -22,6 +20,7 @@ import { DesktopBoardButtons, MobileBoardButtons } from './components/board-butt import BoardHeader from './components/board-header'; import ChallengeModal from './components/challenge-modal'; import CreateBoardModal from './components/create-board-modal'; +import FeedCacheContainer from './components/feed-cache-container'; import ReplyModal from './components/reply-modal'; import PostForm from './components/post-form'; import SubplebbitStats from './components/subplebbit-stats'; @@ -43,6 +42,9 @@ const BoardLayout = () => { const pendingPost = useAccountComment({ commentIndex: accountCommentIndex ? parseInt(accountCommentIndex) : undefined }); const { closeCreateBoardModal } = useCreateBoardModalStore(); + const isOnPostRoute = isPostRoute(location.pathname); + const isOnPendingPostRoute = isPendingPostRoute(location.pathname); + // Christmas theme const { isEnabled: isSpecialEnabled } = useSpecialThemeStore(); useEffect(() => { @@ -75,9 +77,8 @@ const BoardLayout = () => { {isMobile ? (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && ( <> - -
+ ) : (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && ( @@ -87,7 +88,8 @@ const BoardLayout = () => { )} - + + {(isOnPostRoute || isOnPendingPostRoute) && } ); }; @@ -142,36 +144,36 @@ const App = () => ( } /> } /> }> - } /> - } /> + + + + + + + + + + + + + + + + + + + + } /> - } /> - } /> - - } /> - } /> - } /> - } /> - - } /> - } /> - } /> - } /> - - } /> - } /> - - } /> - } /> - } /> - } /> - } /> } /> } /> } /> } /> } /> + + } /> + } /> } /> } /> diff --git a/src/components/catalog-row/catalog-row.tsx b/src/components/catalog-row/catalog-row.tsx index e3569adf..084a2616 100644 --- a/src/components/catalog-row/catalog-row.tsx +++ b/src/components/catalog-row/catalog-row.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { Link, useLocation, useParams } from 'react-router-dom'; @@ -24,6 +24,7 @@ import { ContentPreview } from '../../views/home/popular-threads-box'; import PostMenuDesktop from '../post-desktop/post-menu-desktop'; import styles from './catalog-row.module.css'; import _ from 'lodash'; +import { selectPostMenuProps } from '../../lib/utils/post-menu-props'; interface CatalogPostMediaProps { cid: string; @@ -142,6 +143,7 @@ const CatalogPost = ({ post }: { post: Comment }) => { const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); const defaultSubplebbits = useDefaultSubplebbits(); const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : ''; + const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]); const postLink = isInAllView && isDescription ? '/all/description' : `/${boardPath}/${isDescription ? 'description' : isRules ? 'rules' : `thread/${cid}`}`; @@ -287,7 +289,7 @@ const CatalogPost = ({ post }: { post: Comment }) => { )} - +
{(showOPComment || isTextOnlyThread) && (hasThumbnail ? postContent : {postContent})}
diff --git a/src/components/challenge-modal/challenge-modal.tsx b/src/components/challenge-modal/challenge-modal.tsx index d104fb61..1eb1b3b2 100644 --- a/src/components/challenge-modal/challenge-modal.tsx +++ b/src/components/challenge-modal/challenge-modal.tsx @@ -293,7 +293,9 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => { {showIframeConfirmation ? ( <>
-
board wants to open {readableUrl || 'an external site'}
+
+ {shortSubplebbitAddress || subplebbitAddress || 'unknown board'} wants to open {readableUrl || 'an external site'} +
diff --git a/src/components/feed-cache-container/feed-cache-container.module.css b/src/components/feed-cache-container/feed-cache-container.module.css new file mode 100644 index 00000000..79003f5c --- /dev/null +++ b/src/components/feed-cache-container/feed-cache-container.module.css @@ -0,0 +1,13 @@ +.visible { +} + +.hidden { + visibility: hidden; + position: absolute; + top: 0; + left: 0; + width: 100%; + pointer-events: none; + overflow: hidden; + height: 0; +} diff --git a/src/components/feed-cache-container/feed-cache-container.tsx b/src/components/feed-cache-container/feed-cache-container.tsx new file mode 100644 index 00000000..3547e1ec --- /dev/null +++ b/src/components/feed-cache-container/feed-cache-container.tsx @@ -0,0 +1,99 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; +import useFeedCacheStore, { CachedFeed } from '../../stores/use-feed-cache-store'; +import { getFeedCacheKey, getFeedType, isFeedRoute } from '../../lib/utils/route-utils'; +import Board from '../../views/board'; +import Catalog from '../../views/catalog'; +import styles from './feed-cache-container.module.css'; + +interface FeedContextFromKey { + viewType: 'all' | 'subs' | 'mod' | 'board'; + boardIdentifier?: string; + timeFilterName?: string; +} + +const parseFeedKey = (key: string): FeedContextFromKey => { + const segments = key.split('/').filter(Boolean); + + const filteredSegments = segments.filter((s) => s !== 'catalog'); + if (filteredSegments[0] === 'all') { + return { + viewType: 'all', + timeFilterName: filteredSegments[1], + }; + } + if (filteredSegments[0] === 'subs') { + return { + viewType: 'subs', + timeFilterName: filteredSegments[1], + }; + } + if (filteredSegments[0] === 'mod') { + return { + viewType: 'mod', + timeFilterName: filteredSegments[1], + }; + } + + return { + viewType: 'board', + boardIdentifier: filteredSegments[0], + timeFilterName: filteredSegments[1], + }; +}; + +interface CachedFeedWrapperProps { + feed: CachedFeed; + isVisible: boolean; +} + +const CachedFeedWrapper = ({ feed, isVisible }: CachedFeedWrapperProps) => { + const context = parseFeedKey(feed.key); + + return ( +
+ {feed.type === 'catalog' ? ( + + ) : ( + + )} +
+ ); +}; + +const FeedCacheContainer = () => { + const location = useLocation(); + const { cachedFeeds, accessFeed } = useFeedCacheStore(); + + const currentFeedKey = getFeedCacheKey(location.pathname); + const isOnFeedRoute = isFeedRoute(location.pathname); + const feedType = getFeedType(location.pathname); + + useEffect(() => { + if (isOnFeedRoute && currentFeedKey && feedType) { + accessFeed(currentFeedKey, feedType); + } + }, [currentFeedKey, isOnFeedRoute, feedType, accessFeed]); + + return ( + <> + {cachedFeeds.map((feed) => ( + + ))} + + ); +}; + +export default FeedCacheContainer; diff --git a/src/components/feed-cache-container/index.ts b/src/components/feed-cache-container/index.ts new file mode 100644 index 00000000..74e8a166 --- /dev/null +++ b/src/components/feed-cache-container/index.ts @@ -0,0 +1 @@ +export { default } from './feed-cache-container'; diff --git a/src/components/post-desktop/post-desktop.tsx b/src/components/post-desktop/post-desktop.tsx index fac937ec..bc30671b 100644 --- a/src/components/post-desktop/post-desktop.tsx +++ b/src/components/post-desktop/post-desktop.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { Link, useLocation, useParams } from 'react-router-dom'; import { Comment, useAuthorAvatar, useEditedComment } from '@plebbit/plebbit-react-hooks'; @@ -33,6 +33,7 @@ import { create } from 'zustand'; import _ from 'lodash'; import { shouldShowSnow } from '../../lib/snow'; import useReplyModalStore from '../../stores/use-reply-modal-store'; +import { selectPostMenuProps } from '../../lib/utils/post-menu-props'; interface ShowOmittedRepliesState { showOmittedReplies: Record; @@ -66,6 +67,7 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden }: PostProps) => { const { hideAvatars } = useAvatarVisibilityStore(); const defaultSubplebbits = useDefaultSubplebbits(); const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : undefined; + const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]); const params = useParams(); const location = useLocation(); @@ -133,9 +135,9 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden }: PostProps) => { {' '} + /> - )} + )}{' '}
{!(isDescription || isRules) && ( <> @@ -226,7 +228,7 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden }: PostProps) => { )} - {!(removed || deleted) && } + {!(removed || deleted) && } {cid && parentCid && replies && diff --git a/src/components/post-desktop/post-menu-desktop/post-menu-desktop.tsx b/src/components/post-desktop/post-menu-desktop/post-menu-desktop.tsx index 73ee6665..1c44cc95 100644 --- a/src/components/post-desktop/post-menu-desktop/post-menu-desktop.tsx +++ b/src/components/post-desktop/post-menu-desktop/post-menu-desktop.tsx @@ -1,31 +1,38 @@ -import { useState } from 'react'; +import { memo, useState } from 'react'; import { createPortal } from 'react-dom'; import { useLocation, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { autoUpdate, flip, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react'; -import { Comment, useBlock } from '@plebbit/plebbit-react-hooks'; +import { useBlock } from '@plebbit/plebbit-react-hooks'; import styles from './post-menu-desktop.module.css'; import { getCommentMediaInfo } from '../../../lib/utils/media-utils'; -import { copyShareLinkToClipboard, isValidURL } from '../../../lib/utils/url-utils'; +import { copyShareLinkToClipboard, isValidURL, type ShareLinkType } from '../../../lib/utils/url-utils'; +import { copyToClipboard } from '../../../lib/utils/clipboard-utils'; +import { getBoardPath } from '../../../lib/utils/route-utils'; +import { useDefaultSubplebbits } from '../../../hooks/use-default-subplebbits'; import { isAllView, isCatalogView, isPostPageView, isSubscriptionsView } from '../../../lib/utils/view-utils'; import useHide from '../../../hooks/use-hide'; import _ from 'lodash'; +import { PostMenuProps } from '../../../lib/utils/post-menu-props'; -interface PostMenuDesktopProps { - cid: string; - isDescription?: boolean; - isRules?: boolean; - subplebbitAddress: string; - onClose: () => void; -} +type CopyLinkButtonProps = + | { cid: string; subplebbitAddress: string; linkType: 'thread'; onClose: () => void } + | { subplebbitAddress: string; linkType: Exclude; onClose: () => void; cid?: undefined }; -const CopyLinkButton = ({ cid, subplebbitAddress, onClose }: PostMenuDesktopProps) => { +const CopyLinkButton = ({ cid, subplebbitAddress, linkType, onClose }: CopyLinkButtonProps) => { const { t } = useTranslation(); + const defaultSubplebbits = useDefaultSubplebbits(); + const boardIdentifier = getBoardPath(subplebbitAddress, defaultSubplebbits); return (
{ - copyShareLinkToClipboard(subplebbitAddress, cid); - onClose(); + onClick={async () => { + try { + await copyShareLinkToClipboard(boardIdentifier, linkType, cid); + } catch (error) { + console.error('Failed to copy share link', error); + } finally { + onClose(); + } }} >
{t('copy_link')}
@@ -33,6 +40,25 @@ const CopyLinkButton = ({ cid, subplebbitAddress, onClose }: PostMenuDesktopProp ); }; +const CopyContentIdButton = ({ cid, onClose }: { cid: string; onClose: () => void }) => { + const { t } = useTranslation(); + return ( +
{ + try { + await copyToClipboard(cid); + } catch (error) { + console.error('Failed to copy content id', error); + } finally { + onClose(); + } + }} + > +
{t('copy_content_id')}
+
+ ); +}; + const ImageSearchButton = ({ url, onClose }: { url: string; onClose: () => void }) => { const { t } = useTranslation(); const [isImageSearchMenuOpen, setIsImageSearchMenuOpen] = useState(false); @@ -88,14 +114,18 @@ const BlockBoardButton = ({ address }: { address: string }) => { ); }; -const PostMenuDesktop = ({ post }: { post: Comment }) => { +type PostMenuDesktopProps = { + postMenu: PostMenuProps; +}; + +const PostMenuDesktop = ({ postMenu }: PostMenuDesktopProps) => { const { t } = useTranslation(); - const { author, cid, isDescription, isRules, link, thumbnailUrl, linkWidth, linkHeight, postCid, subplebbitAddress } = post || {}; - const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight); + const { authorAddress, cid, isDescription, isRules, link, thumbnailUrl, linkWidth, linkHeight, postCid, subplebbitAddress } = postMenu || {}; + const commentMediaInfo = getCommentMediaInfo(link || '', thumbnailUrl || '', linkWidth ?? 0, linkHeight ?? 0); const { thumbnail, type, url } = commentMediaInfo || {}; const [menuBtnRotated, setMenuBtnRotated] = useState(false); - const { hidden, unhide, hide } = useHide({ cid }); + const { hidden, unhide, hide } = useHide({ cid: cid || '' }); const location = useLocation(); const params = useParams(); @@ -118,7 +148,7 @@ const PostMenuDesktop = ({ post }: { post: Comment }) => { const headingId = useId(); const handleMenuClick = () => { - if (cid) { + if (cid || isDescription || isRules) { setMenuBtnRotated((prev) => !prev); } }; @@ -142,7 +172,10 @@ const PostMenuDesktop = ({ post }: { post: Comment }) => { createPortal(
- {cid && subplebbitAddress && } + {cid && subplebbitAddress && } + {cid && } + {!cid && isDescription && subplebbitAddress && } + {!cid && isRules && subplebbitAddress && } {!(isInPostPageView && postCid === cid) && !isDescription && !isRules && (
{
)} {link && isValidURL(link) && (type === 'image' || type === 'gif' || thumbnail) && url && } - {!isDescription && !isRules && } - {(isInAllView || isInSubscriptionsView) && } + {!isDescription && !isRules && authorAddress && } + {!isDescription && !isRules && (isInAllView || isInSubscriptionsView) && subplebbitAddress && }
, document.body, @@ -165,4 +198,4 @@ const PostMenuDesktop = ({ post }: { post: Comment }) => { ); }; -export default PostMenuDesktop; +export default memo(PostMenuDesktop); diff --git a/src/components/post-form/post-form.module.css b/src/components/post-form/post-form.module.css index 649c43c1..6bf1341c 100644 --- a/src/components/post-form/post-form.module.css +++ b/src/components/post-form/post-form.module.css @@ -137,6 +137,15 @@ .postFormTable button { padding: 3px 6px; } + + .postFormMobile { + margin-bottom: 6px; + } + + .postFormTable { + padding-top: 20px; + padding-bottom: 3px; + } } @media (min-width: 640px) { diff --git a/src/components/post-mobile/post-menu-mobile/post-menu-mobile.tsx b/src/components/post-mobile/post-menu-mobile/post-menu-mobile.tsx index 86f175a0..27288e7b 100644 --- a/src/components/post-mobile/post-menu-mobile/post-menu-mobile.tsx +++ b/src/components/post-mobile/post-menu-mobile/post-menu-mobile.tsx @@ -1,36 +1,46 @@ -import { useState } from 'react'; +import { memo, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { Comment, useBlock } from '@plebbit/plebbit-react-hooks'; import { autoUpdate, flip, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react'; import styles from './post-menu-mobile.module.css'; import { getCommentMediaInfo } from '../../../lib/utils/media-utils'; -import { copyShareLinkToClipboard, isValidURL } from '../../../lib/utils/url-utils'; +import { copyShareLinkToClipboard, isValidURL, type ShareLinkType } from '../../../lib/utils/url-utils'; +import { copyToClipboard } from '../../../lib/utils/clipboard-utils'; +import { getBoardPath } from '../../../lib/utils/route-utils'; +import { useDefaultSubplebbits } from '../../../hooks/use-default-subplebbits'; import useEditCommentPrivileges from '../../../hooks/use-author-privileges'; import useHide from '../../../hooks/use-hide'; import EditMenu from '../../edit-menu/edit-menu'; import { isBoardView, isPostPageView } from '../../../lib/utils/view-utils'; import { useLocation, useParams } from 'react-router-dom'; +import { PostMenuProps } from '../../../lib/utils/post-menu-props'; -interface PostMenuMobileProps { - cid: string; - isDescription?: boolean; +type HideButtonProps = { + cid?: string; isReply?: boolean; - isRules?: boolean; postCid?: string; - subplebbitAddress?: string; onClose?: () => void; -} +}; -const CopyLinkButton = ({ cid, subplebbitAddress, onClose }: PostMenuMobileProps) => { +type CopyLinkButtonProps = + | { cid: string; subplebbitAddress: string; linkType: 'thread'; onClose: () => void } + | { subplebbitAddress: string; linkType: Exclude; onClose: () => void; cid?: undefined }; + +const CopyLinkButton = ({ cid, subplebbitAddress, linkType, onClose }: CopyLinkButtonProps) => { const { t } = useTranslation(); + const defaultSubplebbits = useDefaultSubplebbits(); + const boardIdentifier = getBoardPath(subplebbitAddress, defaultSubplebbits); return (
{ - if (subplebbitAddress) { - copyShareLinkToClipboard(subplebbitAddress, cid); + onClick={async () => { + try { + await copyShareLinkToClipboard(boardIdentifier, linkType, cid); + } catch (error) { + console.error('Failed to copy share link', error); + } finally { + onClose(); } - onClose && onClose(); }} >
{t('copy_link')}
@@ -38,6 +48,25 @@ const CopyLinkButton = ({ cid, subplebbitAddress, onClose }: PostMenuMobileProps ); }; +const CopyContentIdButton = ({ cid, onClose }: { cid: string; onClose: () => void }) => { + const { t } = useTranslation(); + return ( +
{ + try { + await copyToClipboard(cid); + } catch (error) { + console.error('Failed to copy content id', error); + } finally { + onClose(); + } + }} + > +
{t('copy_content_id')}
+
+ ); +}; + const ImageSearchButtons = ({ url, onClose }: { url: string; onClose: () => void }) => { const { t } = useTranslation(); return ( @@ -55,7 +84,7 @@ const ImageSearchButtons = ({ url, onClose }: { url: string; onClose: () => void ); }; -const HidePostButton = ({ cid, isReply, onClose, postCid }: PostMenuMobileProps) => { +const HidePostButton = ({ cid, isReply, onClose, postCid }: HideButtonProps) => { const { t } = useTranslation(); const { hide, hidden, unhide } = useHide({ cid }); const isInPostView = isPostPageView(useLocation().pathname, useParams()); @@ -96,9 +125,15 @@ const BlockBoardButton = ({ address }: { address: string }) => { ); }; -const PostMenuMobile = ({ post }: { post: Comment }) => { - const { author, cid, deleted, isDescription, isRules, link, linkHeight, linkWidth, parentCid, postCid, removed, subplebbitAddress, thumbnailUrl } = post || {}; - const { isAccountMod, isAccountCommentAuthor } = useEditCommentPrivileges({ commentAuthorAddress: author?.address, subplebbitAddress }); +type PostMenuMobileProps = { + postMenu: PostMenuProps; + editMenuPost: Comment; +}; + +const PostMenuMobile = ({ postMenu, editMenuPost }: PostMenuMobileProps) => { + const { authorAddress, cid, deleted, isDescription, isRules, link, linkHeight, linkWidth, parentCid, postCid, removed, subplebbitAddress, thumbnailUrl } = + postMenu || {}; + const { isAccountMod, isAccountCommentAuthor } = useEditCommentPrivileges({ commentAuthorAddress: authorAddress, subplebbitAddress }); const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight); const { thumbnail, type, url } = commentMediaInfo || {}; const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -116,7 +151,7 @@ const PostMenuMobile = ({ post }: { post: Comment }) => { const headingId = useId(); const handleMenuClick = () => { - if (cid) { + if (cid || isDescription || isRules) { setIsMenuOpen((prev) => !prev); } }; @@ -137,9 +172,12 @@ const PostMenuMobile = ({ post }: { post: Comment }) => { createPortal(
- {cid && subplebbitAddress && } - {cid && subplebbitAddress && } - {cid && subplebbitAddress && !isDescription && !isRules && } + {cid && subplebbitAddress && } + {cid && } + {!cid && isDescription && subplebbitAddress && } + {!cid && isRules && subplebbitAddress && } + {cid && subplebbitAddress && } + {cid && subplebbitAddress && !isDescription && !isRules && authorAddress && } {cid && subplebbitAddress && !isInBoardView && !isDescription && !isRules && } {link && isValidURL(link) && (type === 'image' || type === 'gif' || thumbnail) && url && }
@@ -150,11 +188,11 @@ const PostMenuMobile = ({ post }: { post: Comment }) => { )} {(isAccountMod || isAccountCommentAuthor) && cid && ( - + )} ); }; -export default PostMenuMobile; +export default memo(PostMenuMobile); diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index e158758a..87ab6f93 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link, useLocation, useParams } from 'react-router-dom'; import { Comment, useAuthorAvatar, useEditedComment } from '@plebbit/plebbit-react-hooks'; @@ -28,6 +28,7 @@ import Tooltip from '../tooltip'; import { PostProps } from '../../views/post/post'; import _ from 'lodash'; import useReplyModalStore from '../../stores/use-reply-modal-store'; +import { selectPostMenuProps } from '../../lib/utils/post-menu-props'; const PostInfoAndMedia = ({ post, postReplyCount = 0, roles }: PostProps) => { const { t } = useTranslation(); @@ -71,6 +72,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles }: PostProps) => { const hasThumbnail = getHasThumbnail(commentMediaInfo, link); const stateString = useStateString(post); + const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]); const handleUserAddressClick = useAuthorAddressClick(); const numberOfPostsByAuthor = document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length; @@ -98,7 +100,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles }: PostProps) => { return ( <>
- + diff --git a/src/components/subplebbit-description/subplebbit-description.tsx b/src/components/subplebbit-description/subplebbit-description.tsx index 041d86eb..046d25b0 100644 --- a/src/components/subplebbit-description/subplebbit-description.tsx +++ b/src/components/subplebbit-description/subplebbit-description.tsx @@ -20,10 +20,11 @@ const SubplebbitDescription = ({ avatarUrl, createdAt, description, replyCount, const location = useLocation(); const isInAllView = isAllView(location.pathname); const multisubMetadata = useMultisubMetadata(); + const descriptionSubplebbitAddress = isInAllView ? 'all' : subplebbitAddress; const post = { isDescription: true, - subplebbitAddress, + subplebbitAddress: descriptionSubplebbitAddress, timestamp: isInAllView ? multisubMetadata?.createdAt : createdAt, author: { displayName: _.capitalize(t('anonymous')) }, content: isInAllView ? multisubMetadata?.description : description, diff --git a/src/generated/asset-manifest.ts b/src/generated/asset-manifest.ts index f72d68c3..3271841f 100644 --- a/src/generated/asset-manifest.ts +++ b/src/generated/asset-manifest.ts @@ -2,12 +2,6 @@ // Run 'node scripts/generate-asset-manifest.js' to regenerate // This file is generated from public/assets/ directory -export const BANNERS = [ - 'assets/banners/banner-1.jpg', - 'assets/banners/banner-2.jpg', - 'assets/banners/banner-3.gif', - 'assets/banners/banner-4.gif', - 'assets/banners/banner-5.png', -] as const; +export const BANNERS = ['assets/banners/banner-1.jpg', 'assets/banners/banner-2.jpg', 'assets/banners/banner-3.gif', 'assets/banners/banner-4.png'] as const; export const NOT_FOUND_IMAGES = ['assets/not-found/not-found-1.png'] as const; diff --git a/src/hooks/use-time-filter.ts b/src/hooks/use-time-filter.ts index ccfda5b5..9c34c564 100644 --- a/src/hooks/use-time-filter.ts +++ b/src/hooks/use-time-filter.ts @@ -53,6 +53,34 @@ if (secondsSinceLastVisit > 30 * day) { export const timeFilterNames = [lastVisitTimeFilterName, '1h', '12h', '24h', '48h', '1w', '1m', '1y', 'all']; +export const timeFilterNameToSeconds = (timeFilterName: string | undefined): number | undefined => { + if (!timeFilterName || timeFilterName === 'all') return undefined; + + if (timeFilterName in timeFilterNamesToSeconds) { + return timeFilterNamesToSeconds[timeFilterName as keyof typeof timeFilterNamesToSeconds]; + } + + const match = timeFilterName.match(/^(\d+)([hdwmy])$/); + if (match) { + const [, value, unit] = match; + const numValue = parseInt(value, 10); + switch (unit) { + case 'h': + return numValue * 60 * 60; + case 'd': + return numValue * 24 * 60 * 60; + case 'w': + return numValue * 7 * 24 * 60 * 60; + case 'm': + return numValue * 30 * 24 * 60 * 60; + case 'y': + return numValue * 365 * 24 * 60 * 60; + } + } + + return undefined; +}; + function convertTimeStringToSeconds(timeString: string): number { const match = timeString.match(/^(\d+)([hdwmy])$/); if (!match) { diff --git a/src/lib/utils/post-menu-props.ts b/src/lib/utils/post-menu-props.ts new file mode 100644 index 00000000..c1bc50d1 --- /dev/null +++ b/src/lib/utils/post-menu-props.ts @@ -0,0 +1,33 @@ +import { Comment } from '@plebbit/plebbit-react-hooks'; + +export type PostMenuProps = { + cid?: string; + postCid?: string; + parentCid?: string; + subplebbitAddress?: string; + isDescription?: boolean; + isRules?: boolean; + authorAddress?: string; + link?: string; + linkWidth?: number; + linkHeight?: number; + thumbnailUrl?: string; + deleted?: boolean; + removed?: boolean; +}; + +export const selectPostMenuProps = (post?: Comment): PostMenuProps => ({ + cid: post?.cid, + postCid: post?.postCid, + parentCid: post?.parentCid, + subplebbitAddress: post?.subplebbitAddress, + isDescription: post?.isDescription, + isRules: post?.isRules, + authorAddress: post?.author?.address, + link: post?.link, + linkWidth: post?.linkWidth, + linkHeight: post?.linkHeight, + thumbnailUrl: post?.thumbnailUrl, + deleted: post?.deleted, + removed: post?.removed, +}); diff --git a/src/lib/utils/route-utils.ts b/src/lib/utils/route-utils.ts index 265154fa..bdef32f6 100644 --- a/src/lib/utils/route-utils.ts +++ b/src/lib/utils/route-utils.ts @@ -100,3 +100,85 @@ export const isDirectoryBoard = (identifier: string, subplebbits: MultisubSubple const directoryToAddress = getDirectoryToAddressMap(subplebbits); return directoryToAddress.has(identifier); }; + +export const isFeedRoute = (pathname: string): boolean => { + const normalizedPath = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname; + + if (normalizedPath.includes('/thread/')) return false; + if (normalizedPath.endsWith('/description')) return false; + if (normalizedPath.endsWith('/rules')) return false; + if (normalizedPath.startsWith('/pending/')) return false; + + const pathWithoutSettings = normalizedPath.replace(/\/settings$/, ''); + + if (pathWithoutSettings.startsWith('/all')) return true; + if (pathWithoutSettings.startsWith('/subs')) return true; + if (pathWithoutSettings.startsWith('/mod')) return true; + + const segments = pathWithoutSettings.split('/').filter(Boolean); + if (segments.length >= 1) { + if (segments.length === 1) return true; + if (segments.length === 2 && segments[1] === 'catalog') return true; + if (segments.length === 2 && /^(?:\d+(?:h|d|w|m|y)|all)$/.test(segments[1])) return true; + if (segments.length === 3 && segments[1] === 'catalog' && /^(?:\d+(?:h|d|w|m|y)|all)$/.test(segments[2])) return true; + } + + return false; +}; + +export const isPostRoute = (pathname: string): boolean => { + const normalizedPath = pathname.replace(/\/settings$/, ''); + + if (normalizedPath.includes('/thread/')) return true; + if (normalizedPath.endsWith('/description')) return true; + if (normalizedPath.endsWith('/rules')) return true; + + return false; +}; + +export const isPendingPostRoute = (pathname: string): boolean => { + const normalizedPath = pathname.replace(/\/settings$/, ''); + return normalizedPath.startsWith('/pending/'); +}; + +export const getFeedCacheKey = (pathname: string): string | null => { + let normalizedPath = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname; + normalizedPath = normalizedPath.replace(/\/settings$/, ''); + + if (normalizedPath.includes('/thread/')) { + const parts = normalizedPath.split('/thread/'); + return parts[0] || null; + } + + if (normalizedPath.endsWith('/description') || normalizedPath.endsWith('/rules')) { + return normalizedPath.replace(/\/(description|rules)$/, ''); + } + + if (normalizedPath.startsWith('/pending/')) { + return null; + } + + if (isFeedRoute(pathname)) { + return normalizedPath; + } + + return null; +}; + +export const getFeedType = (pathname: string): 'board' | 'catalog' | null => { + const normalizedPath = pathname.replace(/\/settings$/, ''); + + if (normalizedPath.includes('/catalog')) { + return 'catalog'; + } + + if (isFeedRoute(pathname)) { + return 'board'; + } + + if (isPostRoute(pathname)) { + return 'board'; + } + + return null; +}; diff --git a/src/lib/utils/url-utils.ts b/src/lib/utils/url-utils.ts index 2443ac45..a8f05747 100644 --- a/src/lib/utils/url-utils.ts +++ b/src/lib/utils/url-utils.ts @@ -17,10 +17,24 @@ export const isValidURL = (url: string) => { } }; -export const copyShareLinkToClipboard = async (subplebbitAddress: string, cid: string) => { - const shareLink = `https://pleb.bz/p/${subplebbitAddress}/c/${cid}?redirect=5chan.app`; +export type ShareLinkType = 'thread' | 'description' | 'rules'; + +// Copies a share link to clipboard for a board, thread, description, or rules page +export function copyShareLinkToClipboard(boardIdentifier: string, linkType: 'thread', cid: string): Promise; +export function copyShareLinkToClipboard(boardIdentifier: string, linkType: Exclude, cid?: undefined): Promise; +export async function copyShareLinkToClipboard(boardIdentifier: string, linkType: ShareLinkType, cid?: string): Promise { + if (linkType === 'thread') { + if (!cid) { + throw new Error('copyShareLinkToClipboard: thread links require a cid'); + } + const shareLink = `https://5chan.app/${boardIdentifier}/thread/${cid}`; + await copyToClipboard(shareLink); + return; + } + + const shareLink = `https://5chan.app/${boardIdentifier}/${linkType}`; await copyToClipboard(shareLink); -}; +} const CHAN_5_HOSTNAMES = ['pleb.bz', '5chan.app', '5chan.eth.limo', '5chan.eth.link', '5chan.eth.sucks', '5chan.netlify.app']; diff --git a/src/stores/use-feed-cache-store.ts b/src/stores/use-feed-cache-store.ts new file mode 100644 index 00000000..5fbbafc5 --- /dev/null +++ b/src/stores/use-feed-cache-store.ts @@ -0,0 +1,54 @@ +import { create } from 'zustand'; + +export interface CachedFeed { + key: string; + type: 'board' | 'catalog'; + lastAccessed: number; +} + +interface FeedCacheState { + cachedFeeds: CachedFeed[]; + maxCacheSize: number; + accessFeed: (key: string, type: 'board' | 'catalog') => void; + removeFeed: (key: string) => void; + isFeedCached: (key: string) => boolean; +} + +const useFeedCacheStore = create((set, get) => ({ + cachedFeeds: [], + maxCacheSize: 2, + + accessFeed: (key: string, type: 'board' | 'catalog') => { + const { cachedFeeds, maxCacheSize } = get(); + const now = Date.now(); + const existingIndex = cachedFeeds.findIndex((feed) => feed.key === key); + + if (existingIndex !== -1) { + const updatedFeeds = [...cachedFeeds]; + updatedFeeds[existingIndex] = { ...updatedFeeds[existingIndex], lastAccessed: now }; + set({ cachedFeeds: updatedFeeds }); + } else { + const newFeed: CachedFeed = { key, type, lastAccessed: now }; + let updatedFeeds = [...cachedFeeds, newFeed]; + + if (updatedFeeds.length > maxCacheSize) { + updatedFeeds.sort((a, b) => a.lastAccessed - b.lastAccessed); + updatedFeeds = updatedFeeds.slice(1); + } + + set({ cachedFeeds: updatedFeeds }); + } + }, + + removeFeed: (key: string) => { + const { cachedFeeds } = get(); + set({ cachedFeeds: cachedFeeds.filter((feed) => feed.key !== key) }); + }, + + isFeedCached: (key: string) => { + const { cachedFeeds } = get(); + return cachedFeeds.some((feed) => feed.key === key); + }, +})); + +export default useFeedCacheStore; diff --git a/src/views/board/board.tsx b/src/views/board/board.tsx index a9ef89dc..18a7d27b 100644 --- a/src/views/board/board.tsx +++ b/src/views/board/board.tsx @@ -1,19 +1,19 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { Link, useLocation, useParams } from 'react-router-dom'; +import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom'; import { Comment, useAccount, useAccountComments, useAccountSubplebbits, useBlock, useFeed, useSubplebbit } from '@plebbit/plebbit-react-hooks'; import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso'; import { Trans, useTranslation } from 'react-i18next'; import styles from './board.module.css'; import { shouldShowSnow } from '../../lib/snow'; import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils'; -import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils'; -import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; +import { useDefaultSubplebbitAddresses, useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; import { useResolvedSubplebbitAddress, useBoardPath } from '../../hooks/use-resolved-subplebbit-address'; import { useFeedStateString } from '../../hooks/use-state-string'; -import useTimeFilter from '../../hooks/use-time-filter'; +import useTimeFilter, { timeFilterNameToSeconds } from '../../hooks/use-time-filter'; import useInterfaceSettingsStore from '../../stores/use-interface-settings-store'; import useFeedResetStore from '../../stores/use-feed-reset-store'; import useSortingStore from '../../stores/use-sorting-store'; +import { getSubplebbitAddress } from '../../lib/utils/route-utils'; import ErrorDisplay from '../../components/error-display/error-display'; import LoadingEllipsis from '../../components/loading-ellipsis'; import SubplebbitDescription from '../../components/subplebbit-description'; @@ -33,21 +33,39 @@ const createThreadsWithoutImagesFilter = () => ({ key: 'threads-with-images-only', }); -const Board = () => { +export interface BoardProps { + feedCacheKey?: string; + viewType?: 'all' | 'subs' | 'mod' | 'board'; + boardIdentifier?: string; + timeFilterNameFromCache?: string; + isVisible?: boolean; +} + +const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, timeFilterNameFromCache, isVisible = true }: BoardProps) => { const { t } = useTranslation(); const location = useLocation(); - const subplebbitAddress = useResolvedSubplebbitAddress(); - const boardPath = useBoardPath(subplebbitAddress); + const params = useParams(); const { hideThreadsWithoutImages } = useInterfaceSettingsStore(); - const isInAllView = isAllView(location.pathname); + const isInAllView = viewType ? viewType === 'all' : false; + const isInSubscriptionsView = viewType ? viewType === 'subs' : false; + const isInModView = viewType ? viewType === 'mod' : false; + + const defaultSubplebbits = useDefaultSubplebbits(); + const resolvedAddressFromUrl = useResolvedSubplebbitAddress(); + const subplebbitAddress = useMemo(() => { + if (boardIdentifierProp) { + return getSubplebbitAddress(boardIdentifierProp, defaultSubplebbits); + } + return resolvedAddressFromUrl; + }, [boardIdentifierProp, defaultSubplebbits, resolvedAddressFromUrl]); + + const boardPath = useBoardPath(subplebbitAddress); const defaultSubplebbitAddresses = useDefaultSubplebbitAddresses(); const account = useAccount(); const subscriptions = account?.subscriptions; - const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); - const isInModView = isModView(location.pathname); const { accountSubplebbits } = useAccountSubplebbits(); const accountSubplebbitAddresses = Object.keys(accountSubplebbits); @@ -65,7 +83,9 @@ const Board = () => { }, [isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress, defaultSubplebbitAddresses, subscriptions, accountSubplebbitAddresses]); const { sortType } = useSortingStore(); - const { timeFilterSeconds, timeFilterName } = useTimeFilter(); + const { timeFilterSeconds: timeFilterSecondsFromHook, timeFilterName: timeFilterNameFromHook } = useTimeFilter(); + const timeFilterName = timeFilterNameFromCache || timeFilterNameFromHook; + const timeFilterSeconds = timeFilterNameFromCache ? timeFilterNameToSeconds(timeFilterNameFromCache) : timeFilterSecondsFromHook; const feedOptions = { subplebbitAddresses, @@ -82,8 +102,10 @@ const Board = () => { const setResetFunction = useFeedResetStore((state) => state.setResetFunction); useEffect(() => { - setResetFunction(reset); - }, [reset, setResetFunction, feed]); + if (isVisible) { + setResetFunction(reset); + } + }, [reset, setResetFunction, feed, isVisible]); // show account comments instantly in the feed once published (cid defined), instead of waiting for the feed to update const filteredComments = useMemo( @@ -175,7 +197,6 @@ const Board = () => { return () => clearTimeout(timer); }, []); - const params = useParams(); const currentTimeFilterName = params?.timeFilterName || timeFilterName; const Footer = () => { @@ -272,26 +293,42 @@ const Board = () => { ); }; - // save the last Virtuoso state to restore it when navigating back const virtuosoRef = useRef(null); + const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${sortType}-${timeFilterSeconds}` : `${location.pathname}-${sortType}-${timeFilterSeconds}`; + const navigationType = useNavigationType(); + + const hasBeenVisibleRef = useRef(false); useEffect(() => { + if (isVisible && !hasBeenVisibleRef.current) { + hasBeenVisibleRef.current = true; + if (navigationType !== 'POP') { + window.scrollTo({ top: 0, left: 0, behavior: 'auto' }); + } + } + }, [isVisible, navigationType]); + + useEffect(() => { + if (!isVisible) return; + + const currentKey = virtuosoStateKey; const setLastVirtuosoState = () => { virtuosoRef.current?.getState((snapshot: StateSnapshot) => { if (snapshot?.ranges?.length) { - lastVirtuosoStates[sortType + timeFilterSeconds] = snapshot; + lastVirtuosoStates[currentKey] = snapshot; } }); }; window.addEventListener('scroll', setLastVirtuosoState); return () => window.removeEventListener('scroll', setLastVirtuosoState); - }, [sortType, timeFilterSeconds]); + }, [virtuosoStateKey, isVisible]); - const lastVirtuosoState = lastVirtuosoStates?.[sortType + timeFilterSeconds]; + const lastVirtuosoState = navigationType === 'POP' ? lastVirtuosoStates?.[virtuosoStateKey] : undefined; useEffect(() => { + if (!isVisible) return; const boardTitle = title ? title : shortAddress || subplebbitAddress; document.title = boardTitle + ' - 5chan'; - }, [title, shortAddress, subplebbitAddress]); + }, [title, shortAddress, subplebbitAddress, isVisible]); const shouldShowErrorToUser = error?.message && feed.length === 0; diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx index f2448ecd..c218e6a5 100644 --- a/src/views/catalog/catalog.tsx +++ b/src/views/catalog/catalog.tsx @@ -1,21 +1,21 @@ import { useEffect, useMemo, useRef, useState, useCallback } from 'react'; -import { Link, useLocation, useParams } from 'react-router-dom'; +import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; import { Comment, useAccount, useFeed, useSubplebbit, useBlock, useAccountComments } from '@plebbit/plebbit-react-hooks'; import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso'; import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils'; -import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils'; import useCatalogFeedRows from '../../hooks/use-catalog-feed-rows'; import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; import { useResolvedSubplebbitAddress, useBoardPath } from '../../hooks/use-resolved-subplebbit-address'; import { useFeedStateString } from '../../hooks/use-state-string'; -import useTimeFilter from '../../hooks/use-time-filter'; +import useTimeFilter, { timeFilterNameToSeconds } from '../../hooks/use-time-filter'; import useWindowWidth from '../../hooks/use-window-width'; import useCatalogStyleStore from '../../stores/use-catalog-style-store'; import useFeedResetStore from '../../stores/use-feed-reset-store'; import useInterfaceSettingsStore from '../../stores/use-interface-settings-store'; import useSortingStore from '../../stores/use-sorting-store'; import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; +import { getSubplebbitAddress } from '../../lib/utils/route-utils'; import CatalogRow from '../../components/catalog-row'; import LoadingEllipsis from '../../components/loading-ellipsis'; import styles from './catalog.module.css'; @@ -123,20 +123,37 @@ const createCombinedFilter = ( }; }; -const Catalog = () => { +export interface CatalogProps { + feedCacheKey?: string; + viewType?: 'all' | 'subs' | 'mod' | 'board'; + boardIdentifier?: string; + timeFilterNameFromCache?: string; + isVisible?: boolean; +} + +const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, timeFilterNameFromCache, isVisible = true }: CatalogProps) => { const { t } = useTranslation(); const location = useLocation(); - const subplebbitAddress = useResolvedSubplebbitAddress(); - const boardPath = useBoardPath(subplebbitAddress); + const params = useParams(); + + const isInAllView = viewType ? viewType === 'all' : false; + const isInSubscriptionsView = viewType ? viewType === 'subs' : false; - const isInAllView = isAllView(location.pathname); const defaultSubplebbits = useDefaultSubplebbits(); + const resolvedAddressFromUrl = useResolvedSubplebbitAddress(); + const subplebbitAddress = useMemo(() => { + if (boardIdentifierProp) { + return getSubplebbitAddress(boardIdentifierProp, defaultSubplebbits); + } + return resolvedAddressFromUrl; + }, [boardIdentifierProp, defaultSubplebbits, resolvedAddressFromUrl]); + + const boardPath = useBoardPath(subplebbitAddress); const { hideAdultBoards } = useInterfaceSettingsStore(); const { showTextOnlyThreads, filterItems, searchText, clearMatchedFilters } = useCatalogFiltersStore(); const account = useAccount(); const subscriptions = account?.subscriptions; - const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); const subplebbitAddresses = useMemo(() => { const filteredDefaultSubplebbits = defaultSubplebbits @@ -166,8 +183,10 @@ const Catalog = () => { const columnCount = Math.floor(useWindowWidth() / columnWidth); const postsPerPage = columnCount <= 2 ? 10 : columnCount === 3 ? 15 : columnCount === 4 ? 20 : 25; - const { timeFilterSeconds, timeFilterName } = useTimeFilter(); + const { timeFilterSeconds: timeFilterSecondsFromHook, timeFilterName: timeFilterNameFromHook } = useTimeFilter(); const { sortType } = useSortingStore(); + const timeFilterName = timeFilterNameFromCache || timeFilterNameFromHook; + const timeFilterSeconds = timeFilterNameFromCache ? timeFilterNameToSeconds(timeFilterNameFromCache) : timeFilterSecondsFromHook; // Create a stable callback for filter matching const handleFilterMatch = useCallback((filterIndex: number, cid: string, subplebbitAddress: string) => { @@ -303,8 +322,10 @@ const Catalog = () => { const setResetFunction = useFeedResetStore((state) => state.setResetFunction); useEffect(() => { - setResetFunction(reset); - }, [reset, setResetFunction]); + if (isVisible) { + setResetFunction(reset); + } + }, [reset, setResetFunction, isVisible]); const subplebbit = useSubplebbit({ subplebbitAddress }); const { error, shortAddress, state, title } = subplebbit || {}; @@ -344,7 +365,6 @@ const Catalog = () => {
); - const params = useParams<{ sortType?: string; timeFilterName?: string }>(); const currentTimeFilterName = params?.timeFilterName || timeFilterName; const Footer = () => { @@ -450,27 +470,43 @@ const Catalog = () => { const rows = useCatalogFeedRows(columnCount, processedFeed, isFeedLoaded, subplebbit); - // save the last Virtuoso state to restore it when navigating back const virtuosoRef = useRef(null); + const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${sortType}-${timeFilterSeconds}` : `${location.pathname}-${sortType}-${timeFilterSeconds}-catalog`; + const navigationType = useNavigationType(); + + const hasBeenVisibleRef = useRef(false); useEffect(() => { + if (isVisible && !hasBeenVisibleRef.current) { + hasBeenVisibleRef.current = true; + if (navigationType !== 'POP') { + window.scrollTo({ top: 0, left: 0, behavior: 'auto' }); + } + } + }, [isVisible, navigationType]); + + useEffect(() => { + if (!isVisible) return; + + const currentKey = virtuosoStateKey; const setLastVirtuosoState = () => virtuosoRef.current?.getState((snapshot: StateSnapshot) => { if (snapshot?.ranges?.length) { - lastVirtuosoStates[sortType + timeFilterSeconds + 'catalog'] = snapshot; + lastVirtuosoStates[currentKey] = snapshot; } }); window.addEventListener('scroll', setLastVirtuosoState); return () => window.removeEventListener('scroll', setLastVirtuosoState); - }, [sortType, timeFilterSeconds]); + }, [virtuosoStateKey, isVisible]); - const lastVirtuosoState = lastVirtuosoStates?.[sortType + timeFilterSeconds + 'catalog']; + const lastVirtuosoState = navigationType === 'POP' ? lastVirtuosoStates?.[virtuosoStateKey] : undefined; useEffect(() => { + if (!isVisible) return; let documentTitle = title ? title : shortAddress; if (isInAllView) documentTitle = t('all'); else if (isInSubscriptionsView) documentTitle = t('subscriptions'); document.title = documentTitle + ` - ${t('catalog')} - 5chan`; - }, [title, shortAddress, isInAllView, isInSubscriptionsView, t]); + }, [title, shortAddress, isInAllView, isInSubscriptionsView, t, isVisible]); // Clear matched filters when component mounts or when subplebbit changes useEffect(() => { diff --git a/src/views/home/boards-list/boards-list.tsx b/src/views/home/boards-list/boards-list.tsx index ade581f7..c685fb7f 100644 --- a/src/views/home/boards-list/boards-list.tsx +++ b/src/views/home/boards-list/boards-list.tsx @@ -2,7 +2,6 @@ import { Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useDefaultSubplebbitsState, useDefaultSubplebbits, MultisubSubplebbit } from '../../../hooks/use-default-subplebbits'; import { getBoardPath } from '../../../lib/utils/route-utils'; -import LoadingEllipsis from '../../../components/loading-ellipsis'; import useDisclaimerModalStore from '../../../stores/use-disclaimer-modal-store'; import useDirectoryModalStore from '../../../stores/use-directory-modal-store'; import useBoardsFilterStore from '../../../stores/use-boards-filter-store'; @@ -31,7 +30,7 @@ const NSFWBadge = () => { const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { const { t } = useTranslation(); const navigate = useNavigate(); - const { loading, error } = useDefaultSubplebbitsState(); + const { error } = useDefaultSubplebbitsState(); const { showDisclaimerModal } = useDisclaimerModalStore(); const { openDirectoryModal } = useDirectoryModalStore(); const { useCatalogLinks, boardFilter } = useBoardsFilterStore(); @@ -56,37 +55,10 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { openDirectoryModal(); }; - if (loading) { - return ( -
-
-

{t('boards')}

- -
-
- -
-
- ); - } - - if (error) { - return ( -
-
-

{t('boards')}

- -
-
-
{error.message}
-
-
- ); - } - // Find active boards const bizAddress = findBoardAddress(multisub, '/biz/ - Business & Finance'); const polAddress = findBoardAddress(multisub, '/pol/ - Politically Incorrect'); + const errorMessage = error?.message; // Filtering logic: determine which categories to show const showAll = boardFilter === 'all'; @@ -110,6 +82,7 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {
+ {errorMessage &&
{errorMessage}
} {/* Column 1: Japanese Culture + Video Games */} {(showJapaneseCulture || showVideoGames) && (
diff --git a/src/views/home/home.module.css b/src/views/home/home.module.css index a4e2f51b..2f845310 100644 --- a/src/views/home/home.module.css +++ b/src/views/home/home.module.css @@ -190,6 +190,12 @@ text-align: center; } +.popularThreadsLoading { + text-align: left; + /* Reserve space only while loading: max 8 posts, ~2 rows of 4 cards on desktop */ + min-height: 500px; +} + .popularThread { vertical-align: top; display: inline-block; @@ -351,6 +357,11 @@ .boardsColumn a { font-size: 1.2em; } + + /* Mobile: popular threads cards stack more, so taller min-height needed */ + .popularThreadsLoading { + min-height: 1200px; + } } @media (min-width: 640px) { diff --git a/src/views/home/popular-threads-box/popular-threads-box.tsx b/src/views/home/popular-threads-box/popular-threads-box.tsx index 11b52aab..6ddb8178 100644 --- a/src/views/home/popular-threads-box/popular-threads-box.tsx +++ b/src/views/home/popular-threads-box/popular-threads-box.tsx @@ -78,6 +78,7 @@ const PopularThreadsBox = ({ multisub, subplebbits }: { multisub: MultisubSubple const filteredSubplebbits = useMemo(getFilteredSubplebbits, [subplebbits, showWorksafeContentOnly, showNsfwContentOnly, multisub]); const { popularPosts } = usePopularPosts(filteredSubplebbits); + const isLoading = popularPosts.length === 0; return (
@@ -85,12 +86,8 @@ const PopularThreadsBox = ({ multisub, subplebbits }: { multisub: MultisubSubple

{t('popular_threads')}

-
- {popularPosts.length === 0 ? ( - - ) : ( - popularPosts.map((post: any) => ) - )} +
+ {isLoading ? : popularPosts.map((post: any) => )}
); diff --git a/yarn.lock b/yarn.lock index b4e3e496..5292c23e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1199,20 +1199,20 @@ ipaddr.js "^2.2.0" punycode "^2.3.1" -"@capacitor/android@7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@capacitor/android/-/android-7.2.0.tgz#f30e313315ab92c500bd57d9f0c4716a46b42f40" - integrity sha512-zdhEy3jZPG5Toe/pGzKtDgIiBGywjaoEuQWnGVjBYPlSAEUtAhpZ2At7V0SCb26yluAuzrAUV0Ue+LQeEtHwFQ== +"@capacitor/android@7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@capacitor/android/-/android-7.4.4.tgz#a14a1e844bd5079982427e247fdd17555b5fbedd" + integrity sha512-y8knfV1JXNrd6XZZLZireGT+EBCN0lvOo+HZ/s7L8LkrPBu4nY5UZn0Wxz4yOezItEII9rqYJSHsS5fMJG9gdw== "@capacitor/app@7.0.1": version "7.0.1" resolved "https://registry.yarnpkg.com/@capacitor/app/-/app-7.0.1.tgz#0d0709fb4dde5046c24853f2d6b77a7ea411f748" integrity sha512-ArlVZAAla4MwQoKh26x2AaTDOBh5Vhp1VhMKR3RwqZSsZnazKTFGNrPbr9Ez5r1knnEDfApyjwp1uZnXK1WTYQ== -"@capacitor/cli@7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@capacitor/cli/-/cli-7.2.0.tgz#ea279f14e575b059edb7fe8dc64a8e3379088616" - integrity sha512-RNW9vtYYYSDmOdguYBSW0VpRnG/d6lGydlc9DLrJ7qbSPxFrotTz9IjkM48O+SruUma61DyuSqJttdbay2xSxg== +"@capacitor/cli@7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@capacitor/cli/-/cli-7.4.4.tgz#bfce7db8b15d6f44c9d58dc338a5e3d662fecfbb" + integrity sha512-J7ciBE7GlJ70sr2s8oz1+H4ZdNk4MGG41fsakUlDHWva5UWgFIZYMiEdDvGbYazAYTaxN3lVZpH9zil9FfZj+Q== dependencies: "@ionic/cli-framework-output" "^2.2.8" "@ionic/utils-subprocess" "^3.0.1" @@ -1232,10 +1232,10 @@ tslib "^2.8.1" xml2js "^0.6.2" -"@capacitor/core@7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@capacitor/core/-/core-7.2.0.tgz#fdd1c5d2fbfa257887ce6065f66151a51f0e6d1b" - integrity sha512-2zCnA6RJeZ9ec4470o8QMZEQTWpekw9FNoqm5TLc10jeCrhvHVI8MPgxdZVc3mOdFlyieYu4AS1fNxSqbS57Pw== +"@capacitor/core@7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@capacitor/core/-/core-7.4.4.tgz#b752dd7a12141e40d1c8ce2e37a79571d9f83518" + integrity sha512-xzjxpr+d2zwTpCaN0k+C6wKSZzWFAb9OVEUtmO72ihjr/NEDoLvsGl4WLfjWPcCO2zOy0b2X52tfRWjECFUjtw== dependencies: tslib "^2.1.0" @@ -1629,10 +1629,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" - integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== +"@eslint/js@8.57.1": + version "8.57.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== "@ethersproject/abi@5.6.4": version "5.6.4" @@ -2279,17 +2279,39 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== -"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" - integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== - -"@hapi/topo@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" - integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== +"@hapi/address@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@hapi/address/-/address-5.1.1.tgz#e9925fc1b65f5cc3fbea821f2b980e4652e84cb6" + integrity sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA== dependencies: - "@hapi/hoek" "^9.0.0" + "@hapi/hoek" "^11.0.2" + +"@hapi/formula@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-3.0.2.tgz#81b538060ee079481c906f599906d163c4badeaf" + integrity sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw== + +"@hapi/hoek@^11.0.2", "@hapi/hoek@^11.0.7": + version "11.0.7" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-11.0.7.tgz#56a920793e0a42d10e530da9a64cc0d3919c4002" + integrity sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ== + +"@hapi/pinpoint@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.1.tgz#32077e715655fc00ab8df74b6b416114287d6513" + integrity sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q== + +"@hapi/tlds@^1.1.1": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@hapi/tlds/-/tlds-1.1.4.tgz#df4a7b59082b54ba4f3b7b38f781e2ac3cbc359a" + integrity sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA== + +"@hapi/topo@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-6.0.2.tgz#f219c1c60da8430228af4c1f2e40c32a0d84bbb4" + integrity sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg== + dependencies: + "@hapi/hoek" "^11.0.2" "@helia/bitswap@^3.0.4", "@helia/bitswap@^3.0.7": version "3.0.7" @@ -2510,12 +2532,12 @@ race-signal "^2.0.0" uint8arrays "^5.1.0" -"@humanwhocodes/config-array@^0.11.13": - version "0.11.14" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" - integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== dependencies: - "@humanwhocodes/object-schema" "^2.0.2" + "@humanwhocodes/object-schema" "^2.0.3" debug "^4.3.1" minimatch "^3.0.5" @@ -2524,7 +2546,7 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^2.0.2": +"@humanwhocodes/object-schema@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== @@ -4319,115 +4341,115 @@ estree-walker "^2.0.2" picomatch "^4.0.2" -"@rollup/rollup-android-arm-eabi@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz#59e7478d310f7e6a7c72453978f562483828112f" - integrity sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA== +"@rollup/rollup-android-arm-eabi@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz#7e478b66180c5330429dd161bf84dad66b59c8eb" + integrity sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w== -"@rollup/rollup-android-arm64@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz#a825192a0b1b2f27a5c950c439e7e37a33c5d056" - integrity sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w== +"@rollup/rollup-android-arm64@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz#2b025510c53a5e3962d3edade91fba9368c9d71c" + integrity sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w== -"@rollup/rollup-darwin-arm64@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz#4ee37078bccd725ae3c5f30ef92efc8e1bf886f3" - integrity sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg== +"@rollup/rollup-darwin-arm64@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz#3577c38af68ccf34c03e84f476bfd526abca10a0" + integrity sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA== -"@rollup/rollup-darwin-x64@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz#43cc08bd05bf9f388f125e7210a544e62d368d90" - integrity sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw== +"@rollup/rollup-darwin-x64@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz#2bf5f2520a1f3b551723d274b9669ba5b75ed69c" + integrity sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ== -"@rollup/rollup-freebsd-arm64@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz#bc8e640e28abe52450baf3fc80d9b26d9bb6587d" - integrity sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ== +"@rollup/rollup-freebsd-arm64@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz#4bb9cc80252564c158efc0710153c71633f1927c" + integrity sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w== -"@rollup/rollup-freebsd-x64@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz#e981a22e057cc8c65bb523019d344d3a66b15bbc" - integrity sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw== +"@rollup/rollup-freebsd-x64@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz#2301289094d49415a380cf942219ae9d8b127440" + integrity sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q== -"@rollup/rollup-linux-arm-gnueabihf@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz#4036b68904f392a20f3499d63b33e055b67eb274" - integrity sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ== +"@rollup/rollup-linux-arm-gnueabihf@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz#1d03d776f2065e09fc141df7d143476e94acca88" + integrity sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw== -"@rollup/rollup-linux-arm-musleabihf@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz#d3b1b9589606e0ff916801c855b1ace9e733427a" - integrity sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q== +"@rollup/rollup-linux-arm-musleabihf@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz#8623de0e040b2fd52a541c602688228f51f96701" + integrity sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg== -"@rollup/rollup-linux-arm64-gnu@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz#cbf0943c477e3b96340136dd3448eaf144378cf2" - integrity sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg== +"@rollup/rollup-linux-arm64-gnu@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz#ce2d1999bc166277935dde0301cde3dd0417fb6e" + integrity sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w== -"@rollup/rollup-linux-arm64-musl@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz#837f5a428020d5dce1c3b4cc049876075402cf78" - integrity sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g== +"@rollup/rollup-linux-arm64-musl@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz#88c2523778444da952651a2219026416564a4899" + integrity sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A== -"@rollup/rollup-linux-loong64-gnu@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz#532c214ababb32ab4bc21b4054278b9a8979e516" - integrity sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ== +"@rollup/rollup-linux-loong64-gnu@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz#578ca2220a200ac4226c536c10c8cc6e4f276714" + integrity sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g== -"@rollup/rollup-linux-ppc64-gnu@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz#93900163b61b49cee666d10ee38257a8b1dd161a" - integrity sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g== +"@rollup/rollup-linux-ppc64-gnu@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz#aa338d3effd4168a20a5023834a74ba2c3081293" + integrity sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw== -"@rollup/rollup-linux-riscv64-gnu@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz#f0ffdcc7066ca04bc972370c74289f35c7a7dc42" - integrity sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg== +"@rollup/rollup-linux-riscv64-gnu@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz#16ba582f9f6cff58119aa242782209b1557a1508" + integrity sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g== -"@rollup/rollup-linux-riscv64-musl@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz#361695c39dbe96773509745d77a870a32a9f8e48" - integrity sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA== +"@rollup/rollup-linux-riscv64-musl@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz#e404a77ebd6378483888b8064c703adb011340ab" + integrity sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A== -"@rollup/rollup-linux-s390x-gnu@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz#09fc6cc2e266a2324e366486ae5d1bca48c43a6a" - integrity sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA== +"@rollup/rollup-linux-s390x-gnu@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz#92ad52d306227c56bec43d96ad2164495437ffe6" + integrity sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg== -"@rollup/rollup-linux-x64-gnu@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz#aa9d5b307c08f05d3454225bb0a2b4cc87eeb2e1" - integrity sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg== +"@rollup/rollup-linux-x64-gnu@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz#fd0dea3bb9aa07e7083579f25e1c2285a46cb9fa" + integrity sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w== -"@rollup/rollup-linux-x64-musl@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz#26949e5b4645502a61daba2f7a8416bd17cb5382" - integrity sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw== +"@rollup/rollup-linux-x64-musl@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz#37a3efb09f18d555f8afc490e1f0444885de8951" + integrity sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q== -"@rollup/rollup-openharmony-arm64@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz#ef493c072f9dac7e0edb6c72d63366846b6ffcd9" - integrity sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA== +"@rollup/rollup-openharmony-arm64@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz#c489bec9f4f8320d42c9b324cca220c90091c1f7" + integrity sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw== -"@rollup/rollup-win32-arm64-msvc@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz#56e1aaa6a630d2202ee7ec0adddd05cf384ffd44" - integrity sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ== +"@rollup/rollup-win32-arm64-msvc@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz#152832b5f79dc22d1606fac3db946283601b7080" + integrity sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw== -"@rollup/rollup-win32-ia32-msvc@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz#0a44bbf933a9651c7da2b8569fa448dec0de7480" - integrity sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw== +"@rollup/rollup-win32-ia32-msvc@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz#54d91b2bb3bf3e9f30d32b72065a4e52b3a172a5" + integrity sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA== -"@rollup/rollup-win32-x64-gnu@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz#730e12f0b60b234a7c02d5d3179ca3ec7972033d" - integrity sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ== +"@rollup/rollup-win32-x64-gnu@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz#df9df03e61a003873efec8decd2034e7f135c71e" + integrity sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg== -"@rollup/rollup-win32-x64-msvc@4.52.4": - version "4.52.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz#5b2dd648a960b8fa00d76f2cc4eea2f03daa80f4" - integrity sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w== +"@rollup/rollup-win32-x64-msvc@4.53.3": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz#38ae84f4c04226c1d56a3b17296ef1e0460ecdfe" + integrity sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ== "@rtsao/scc@^1.1.0": version "1.1.0" @@ -4483,23 +4505,6 @@ "@noble/hashes" "~1.8.0" "@scure/base" "~1.2.5" -"@sideway/address@^4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" - integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@sideway/formula@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" - integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== - -"@sideway/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" - integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== - "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -4737,6 +4742,11 @@ rpc-websockets "^9.0.2" superstruct "^2.0.2" +"@standard-schema/spec@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c" + integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA== + "@surma/rollup-plugin-off-main-thread@^2.2.3": version "2.2.3" resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" @@ -5917,13 +5927,14 @@ axe-core@^4.10.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.11.0.tgz#16f74d6482e343ff263d4f4503829e9ee91a86b6" integrity sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ== -axios@^0.27.2: - version "0.27.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" - integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== +axios@^1.13.2: + version "1.13.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.2.tgz#9ada120b7b5ab24509553ec3e40123521117f687" + integrity sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA== dependencies: - follow-redirects "^1.14.9" - form-data "^4.0.0" + follow-redirects "^1.15.6" + form-data "^4.0.4" + proxy-from-env "^1.1.0" axios@^1.7.2: version "1.12.2" @@ -7896,10 +7907,10 @@ electron-to-chromium@^1.5.227: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz#eacf61cef3f6345d0069ab427585c5a04d7084f0" integrity sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg== -electron@36.0.0: - version "36.0.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-36.0.0.tgz#ffbe7ca9edecaee53617f29a088be10bbed9a45f" - integrity sha512-MhBL5tgzqLsiw++YXxzXRvF5s90gelcEZP4Upz/aaRmmoscmCw/EtimxMSH6EPnKt+8KwBY9RVAdlcffFPYkyw== +electron@36.9.5: + version "36.9.5" + resolved "https://registry.yarnpkg.com/electron/-/electron-36.9.5.tgz#78e117e8cf97d53687f8d5a412e8bf9336229320" + integrity sha512-1UCss2IqxqujSzg/2jkRjuiT3G+EEXgd6UKB5kUekwQW1LJ6d4QCr8YItfC3Rr9VIGRDJ29eOERmnRNO1Eh+NA== dependencies: "@electron/get" "^2.0.0" "@types/node" "^22.7.7" @@ -8473,16 +8484,16 @@ eslint-visitor-keys@^4.2.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== -eslint@8.56.0: - version "8.56.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" - integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== +eslint@8.57.1: + version "8.57.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.56.0" - "@humanwhocodes/config-array" "^0.11.13" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" "@ungap/structured-clone" "^1.2.0" @@ -8839,7 +8850,7 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fdir@^6.5.0: +fdir@^6.4.4, fdir@^6.5.0: version "6.5.0" resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== @@ -8960,7 +8971,7 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== -follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.11, follow-redirects@^1.15.6: +follow-redirects@^1.0.0, follow-redirects@^1.15.11, follow-redirects@^1.15.6: version "1.15.11" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== @@ -11007,16 +11018,18 @@ jiti@^2.6.1: resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92" integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ== -joi@^17.7.0: - version "17.13.3" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.3.tgz#0f5cc1169c999b30d344366d384b12d92558bcec" - integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA== +joi@^18.0.1: + version "18.0.2" + resolved "https://registry.yarnpkg.com/joi/-/joi-18.0.2.tgz#30ced6aed00a7848cc11f92859515258301dc3a4" + integrity sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA== dependencies: - "@hapi/hoek" "^9.3.0" - "@hapi/topo" "^5.1.0" - "@sideway/address" "^4.1.5" - "@sideway/formula" "^3.0.1" - "@sideway/pinpoint" "^2.0.0" + "@hapi/address" "^5.1.1" + "@hapi/formula" "^3.0.2" + "@hapi/hoek" "^11.0.7" + "@hapi/pinpoint" "^2.0.1" + "@hapi/tlds" "^1.1.1" + "@hapi/topo" "^6.0.2" + "@standard-schema/spec" "^1.0.0" jose@4.11.0: version "4.11.0" @@ -11053,10 +11066,10 @@ js-sizeof@0.0.1: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== +js-yaml@4.1.1, js-yaml@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" @@ -12273,7 +12286,7 @@ minimist@1.2.7: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== -minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.7: +minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -13622,10 +13635,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@19.1.0: - version "19.1.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.0.tgz#133558deca37fa1d682708df8904b25186793623" - integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g== +react-dom@19.1.2: + version "19.1.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.2.tgz#18ce6b14f682f4871a69cc6b2568d42af2cd5a33" + integrity sha512-dEoydsCp50i7kS1xHOmPXq4zQYoGWedUsvqv9H6zdif2r7yLHygyfP9qou71TulRN0d6ng9EbRVsQhSqfUc19g== dependencies: scheduler "^0.26.0" @@ -13741,15 +13754,15 @@ react-scan@0.4.3: optionalDependencies: unplugin "2.1.0" -react-virtuoso@4.12.3: - version "4.12.3" - resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.12.3.tgz#beecf0582b31058c5a6ed3ec58fc43fd780e5844" - integrity sha512-6X1p/sU7hecmjDZMAwN+r3go9EVjofKhwkUbVlL8lXhBZecPv9XVCkZ/kBPYOr0Mv0Vl5+Ziwgexg9Kh7+NNXQ== +react-virtuoso@4.17.0: + version "4.17.0" + resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.17.0.tgz#e81f2da99792cfd9317e910b243d847ebeb09248" + integrity sha512-od3pi2v13v31uzn5zPXC2u3ouISFCVhjFVFch2VvS2Cx7pWA2F1aJa3XhNTN2F07M3lhfnMnsmGeH+7wZICr7w== -react@19.1.0: - version "19.1.0" - resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75" - integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== +react@19.1.2: + version "19.1.2" + resolved "https://registry.yarnpkg.com/react/-/react-19.1.2.tgz#fd75c4f684b52fe45101381535c58486f4407545" + integrity sha512-MdWVitvLbQULD+4DP8GYjZUrepGW7d+GQkNVqJEzNxE+e9WIa4egVFE/RDfVb1u9u/Jw7dNMmPB4IqxzbFYJ0w== read-binary-file-arch@^1.0.6: version "1.0.6" @@ -14118,35 +14131,35 @@ rollup@^2.43.1, rollup@^2.77.2: optionalDependencies: fsevents "~2.3.2" -rollup@^4.30.1: - version "4.52.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.52.4.tgz#71e64cce96a865fcbaa6bb62c6e82807f4e378a1" - integrity sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ== +rollup@^4.34.9: + version "4.53.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.53.3.tgz#dbc8cd8743b38710019fb8297e8d7a76e3faa406" + integrity sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA== dependencies: "@types/estree" "1.0.8" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.52.4" - "@rollup/rollup-android-arm64" "4.52.4" - "@rollup/rollup-darwin-arm64" "4.52.4" - "@rollup/rollup-darwin-x64" "4.52.4" - "@rollup/rollup-freebsd-arm64" "4.52.4" - "@rollup/rollup-freebsd-x64" "4.52.4" - "@rollup/rollup-linux-arm-gnueabihf" "4.52.4" - "@rollup/rollup-linux-arm-musleabihf" "4.52.4" - "@rollup/rollup-linux-arm64-gnu" "4.52.4" - "@rollup/rollup-linux-arm64-musl" "4.52.4" - "@rollup/rollup-linux-loong64-gnu" "4.52.4" - "@rollup/rollup-linux-ppc64-gnu" "4.52.4" - "@rollup/rollup-linux-riscv64-gnu" "4.52.4" - "@rollup/rollup-linux-riscv64-musl" "4.52.4" - "@rollup/rollup-linux-s390x-gnu" "4.52.4" - "@rollup/rollup-linux-x64-gnu" "4.52.4" - "@rollup/rollup-linux-x64-musl" "4.52.4" - "@rollup/rollup-openharmony-arm64" "4.52.4" - "@rollup/rollup-win32-arm64-msvc" "4.52.4" - "@rollup/rollup-win32-ia32-msvc" "4.52.4" - "@rollup/rollup-win32-x64-gnu" "4.52.4" - "@rollup/rollup-win32-x64-msvc" "4.52.4" + "@rollup/rollup-android-arm-eabi" "4.53.3" + "@rollup/rollup-android-arm64" "4.53.3" + "@rollup/rollup-darwin-arm64" "4.53.3" + "@rollup/rollup-darwin-x64" "4.53.3" + "@rollup/rollup-freebsd-arm64" "4.53.3" + "@rollup/rollup-freebsd-x64" "4.53.3" + "@rollup/rollup-linux-arm-gnueabihf" "4.53.3" + "@rollup/rollup-linux-arm-musleabihf" "4.53.3" + "@rollup/rollup-linux-arm64-gnu" "4.53.3" + "@rollup/rollup-linux-arm64-musl" "4.53.3" + "@rollup/rollup-linux-loong64-gnu" "4.53.3" + "@rollup/rollup-linux-ppc64-gnu" "4.53.3" + "@rollup/rollup-linux-riscv64-gnu" "4.53.3" + "@rollup/rollup-linux-riscv64-musl" "4.53.3" + "@rollup/rollup-linux-s390x-gnu" "4.53.3" + "@rollup/rollup-linux-x64-gnu" "4.53.3" + "@rollup/rollup-linux-x64-musl" "4.53.3" + "@rollup/rollup-openharmony-arm64" "4.53.3" + "@rollup/rollup-win32-arm64-msvc" "4.53.3" + "@rollup/rollup-win32-ia32-msvc" "4.53.3" + "@rollup/rollup-win32-x64-gnu" "4.53.3" + "@rollup/rollup-win32-x64-msvc" "4.53.3" fsevents "~2.3.2" rpc-websockets@10.0.0: @@ -14193,7 +14206,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^7.5.5, rxjs@^7.8.0: +rxjs@^7.5.5, rxjs@^7.8.0, rxjs@^7.8.2: version "7.8.2" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== @@ -15233,7 +15246,7 @@ tinycache@1.1.2: dependencies: js-sizeof "0.0.1" -tinyglobby@^0.2.10: +tinyglobby@^0.2.10, tinyglobby@^0.2.13: version "0.2.15" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== @@ -15998,14 +16011,17 @@ vite-plugin-pwa@0.21.1: workbox-build "^7.3.0" workbox-window "^7.3.0" -vite@6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/vite/-/vite-6.2.1.tgz#ae865d4bb93a11844be1bc647c8b2dd1856ea180" - integrity sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q== +vite@6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.4.1.tgz#afbe14518cdd6887e240a4b0221ab6d0ce733f96" + integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g== dependencies: esbuild "^0.25.0" + fdir "^6.4.4" + picomatch "^4.0.2" postcss "^8.5.3" - rollup "^4.30.1" + rollup "^4.34.9" + tinyglobby "^0.2.13" optionalDependencies: fsevents "~2.3.3" @@ -16019,16 +16035,16 @@ void-elements@3.1.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== -wait-on@7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-7.0.1.tgz#5cff9f8427e94f4deacbc2762e6b0a489b19eae9" - integrity sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog== +wait-on@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-9.0.3.tgz#3ea858db0b854039e6aff5f323885aaef25e32bf" + integrity sha512-13zBnyYvFDW1rBvWiJ6Av3ymAaq8EDQuvxZnPIw3g04UqGi4TyoIJABmfJ6zrvKo9yeFQExNkOk7idQbDJcuKA== dependencies: - axios "^0.27.2" - joi "^17.7.0" + axios "^1.13.2" + joi "^18.0.1" lodash "^4.17.21" - minimist "^1.2.7" - rxjs "^7.8.0" + minimist "^1.2.8" + rxjs "^7.8.2" wcwidth@^1.0.1: version "1.0.1"