Merge pull request #888 from bitsocialhq/master

Development
This commit is contained in:
Tom
2026-02-10 21:47:09 +08:00
committed by GitHub
66 changed files with 965 additions and 417 deletions
+36 -10
View File
@@ -32,7 +32,7 @@ jobs:
run: yarn install --frozen-lockfile --ignore-engines
# make sure the ipfs executable is executable
- name: Download IPFS and set permissions (with Node v22)
run: node electron/download-ipfs && sudo chmod +x bin/linux/ipfs
run: BUILD_ARCH=${{ matrix.arch }} node electron/download-ipfs && sudo chmod +x bin/linux/ipfs
- name: Build React app (with Node v22)
run: CI='' NODE_ENV=production yarn build
- name: Build Electron app for Linux (arm64)
@@ -41,15 +41,26 @@ jobs:
- name: Build Electron app for Linux (x64)
if: ${{ matrix.arch == 'x64' }}
run: yarn electron:build:linux:x64
- name: List out directory
run: ls -laR out/make
- name: Create static HTML release archive
run: |
VERSION=$(node -e "console.log(require('./package.json').version)")
HTML_ARCHIVE_DIR="5chan-html-$VERSION"
mkdir -p dist
rm -rf "$HTML_ARCHIVE_DIR"
cp -R build "$HTML_ARCHIVE_DIR"
zip -r "dist/${HTML_ARCHIVE_DIR}.zip" "$HTML_ARCHIVE_DIR"
rm -rf "$HTML_ARCHIVE_DIR"
- name: List dist directory
run: ls dist
run: ls -la dist
# publish version release
- name: Generate release body
run: node scripts/release-body > release-body.txt
- uses: ncipollo/release-action@v1
with:
artifacts: 'dist/5chan*.AppImage,dist/5chan*-arm64.AppImage,dist/5chan-html*.zip'
artifacts: 'out/make/AppImage/**/*.AppImage,dist/5chan-html*.zip'
token: ${{ secrets.GITHUB_TOKEN }}
replacesArtifacts: true
omitBody: true
@@ -89,7 +100,12 @@ jobs:
- run: pip install setuptools
- name: Install dependencies (with Node v22)
run: yarn install --frozen-lockfile --ignore-engines
run: |
for i in 1 2 3; do
yarn install --frozen-lockfile --ignore-engines && break
echo "Retry $i..."
sleep 10
done
# make sure the ipfs executable is executable
- name: Download IPFS and set permissions (with Node v22)
run: node electron/download-ipfs && sudo chmod +x bin/mac/ipfs
@@ -104,15 +120,23 @@ jobs:
else
yarn electron:build:mac:x64
fi
- name: List dist directory
run: ls dist
- name: List out directory
run: ls -laR out/make
# Rename DMG to include architecture (ZIPs already include arch from Forge)
- name: Rename DMG with architecture suffix
run: |
for f in out/make/*.dmg; do
[ -f "$f" ] || continue
base=$(basename "$f" .dmg)
mv "$f" "out/make/${base}-${{ matrix.arch }}.dmg"
done
# publish version release
- name: Generate release body
run: node scripts/release-body > release-body.txt
- uses: ncipollo/release-action@v1
with:
artifacts: 'dist/5chan*.dmg,dist/5chan*-arm64.dmg'
artifacts: 'out/make/*.dmg,out/make/zip/**/*.zip'
token: ${{ secrets.GITHUB_TOKEN }}
replacesArtifacts: true
omitBody: true
@@ -138,15 +162,15 @@ jobs:
run: npx cross-env NODE_ENV=production yarn build
- name: Build Electron app for Windows (x64)
run: yarn electron:build:windows
- name: List dist directory
run: dir dist
- name: List out directory
run: dir out\make\squirrel.windows\x64
# publish version release
- name: Generate release body
run: node scripts/release-body > release-body.txt
- uses: ncipollo/release-action@v1
with:
artifacts: 'dist/5chan*.exe'
artifacts: 'out/make/squirrel.windows/x64/*.exe'
token: ${{ secrets.GITHUB_TOKEN }}
replacesArtifacts: true
omitBody: true
@@ -208,6 +232,8 @@ jobs:
finalize-release:
runs-on: ubuntu-22.04
needs: [linux, mac, windows, android]
permissions:
contents: write
steps:
- uses: actions/checkout@v2
with:
+25 -1
View File
@@ -118,15 +118,17 @@ Set up these hooks for this project:
| Hook | Command | Purpose |
|------|---------|---------|
| `afterFileEdit` | `npx oxfmt <file>` | Auto-format files after AI edits |
| `afterFileEdit` | `.cursor/hooks/yarn-install.sh` | Run `yarn install` when `package.json` changes to keep `yarn.lock` in sync |
| `stop` | `yarn build && yarn lint && yarn type-check && (yarn audit || true)` | Build, verify code, and check security when agent finishes. Note: `yarn audit` returns non-zero on vulnerabilities, so `|| true` makes it informational only |
### Why Use Hooks
- **Consistent formatting** — Every file follows the same style
- **Keep lockfile in sync** — `yarn install` runs automatically when `package.json` changes, preventing stale `yarn.lock` files
- **Catch build errors** — `yarn build` catches compilation errors that would break production
- **Catch issues early** — Lint and type errors are caught before commit/CI
- **Security awareness** — `yarn audit` flags known vulnerabilities in dependencies
- **Less manual work** — No need to run `yarn build`, `yarn lint`, `yarn type-check`, `yarn audit` manually
- **Less manual work** — No need to run `yarn install`, `yarn build`, `yarn lint`, `yarn type-check`, `yarn audit` manually
### Example Hook Scripts
@@ -158,6 +160,28 @@ echo "=== yarn audit ===" && (yarn audit || true) # || true makes audit informa
exit 0
```
**Yarn install hook** (runs when `package.json` is edited):
```bash
#!/bin/bash
# Run yarn install when package.json is changed
# Hook receives JSON via stdin with file_path
input=$(cat)
file_path=$(echo "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:.*"\([^"]*\)"/\1/')
if [ -z "$file_path" ]; then
exit 0
fi
if [ "$file_path" = "package.json" ]; then
cd "$(dirname "$0")/../.." || exit 0
echo "package.json changed - running yarn install to update yarn.lock..."
yarn install
fi
exit 0
```
Consult your AI tool's documentation for how to configure hooks (e.g., `hooks.json` for Cursor/Claude Code).
## Recommended MCP Servers
+43
View File
@@ -1,3 +1,46 @@
## [0.6.4](https://github.com/plebbit/plebchan/compare/v0.6.3...v0.6.4) (2026-02-10)
### Bug Fixes
* **components:** remove redundant instant scroll from quote links ([1bd1d34](https://github.com/plebbit/plebchan/commit/1bd1d34feebe39cdb319cefc4c67cf681e886032))
* include manual post-number quotes in reply backlinks ([702816f](https://github.com/plebbit/plebchan/commit/702816f99b0e31d4d9b59e11831a519caf1c9e97))
* **post:** improve user ID display with domain detection and length limits ([9d17213](https://github.com/plebbit/plebchan/commit/9d17213e6606fe42908131cf26cb64e1ed4061b1))
* **post:** prevent quote hover highlight on op cards ([b8546a8](https://github.com/plebbit/plebchan/commit/b8546a86c55d25746b0a119e0519d77e36ae25e7))
* **release:** remove duplicate architecture suffixes from artifact names ([b30eb15](https://github.com/plebbit/plebchan/commit/b30eb150d05707daee48b0599053c9fee15c1aac))
* **release:** restore 5chan html zip artifact in tag releases ([cdd6be9](https://github.com/plebbit/plebchan/commit/cdd6be915ed98ef696b8efe04c2472654c457253))
* **reply modal:** restore multiline quote insertion ([db70a91](https://github.com/plebbit/plebchan/commit/db70a9148b6a9d34d93ef8246cd7c062a285e29f))
* shorten rendered user ID display from 12 to 8 characters ([24d329a](https://github.com/plebbit/plebchan/commit/24d329a3294cd49045410257472421d79359e990))
* show OP badge for number-based quote links ([223ec4c](https://github.com/plebbit/plebchan/commit/223ec4c4943fba789a9e65b38fcbea08764ebfde))
* update subscriptions subtitle to present tense ([8636005](https://github.com/plebbit/plebchan/commit/8636005f12eca2273aa8194e1293c80e9eff1888))
### Features
* add copy user ID menu item and rename copy link to copy direct link ([4a96f88](https://github.com/plebbit/plebchan/commit/4a96f88efd2b21578cfc823b822a7286956bd9a1))
* **post:** add backlinks for quotedCids ([c7c8c46](https://github.com/plebbit/plebchan/commit/c7c8c46557f09decee5aadaf0e65a087bd7c725b))
* **posts:** support pseudonymityMode per-reply hiding ([c1b0d13](https://github.com/plebbit/plebchan/commit/c1b0d13bb7f26d94513e74b67e79c50cca997783))
* **release:** extract one-liner release description ([3be7839](https://github.com/plebbit/plebchan/commit/3be78393d1af103ae6323d8e1615f742d4211a27))
* render >>{number} as interactive quote links with hover preview ([c4b6c77](https://github.com/plebbit/plebchan/commit/c4b6c77d4cb49357065fa21b51fbf3209895c58a))
* **reply-modal:** insert quoted post numbers at textarea caret ([8a192b4](https://github.com/plebbit/plebchan/commit/8a192b41b465995ce3421503f6811281c1aca3cc))
## [0.6.3](https://github.com/plebbit/plebchan/compare/v0.6.2...v0.6.3) (2026-01-30)
### Bug Fixes
* **electron-forge:** add app icon configuration for all platforms ([a2d0869](https://github.com/plebbit/plebchan/commit/a2d086990484ea4cf823282d997c89719f5c7bc9))
* **electron:** fix production build crashes ([388a120](https://github.com/plebbit/plebchan/commit/388a120c8ea387a48fa24152caac5c55a4c33734))
* **find-forge-executable:** make appName preference effective ([16012b4](https://github.com/plebbit/plebchan/commit/16012b42395523b8770411c65d82767da41fdb81))
* **forge.config.js:** remove malformed iconUrl from Squirrel config ([c4e2f75](https://github.com/plebbit/plebchan/commit/c4e2f753e38a0d69198e7aba83c0b4968671e180))
* **package.json:** remove unneeded var ([0c6e03d](https://github.com/plebbit/plebchan/commit/0c6e03d039d0303b3d7e896ff5e9ad169527db80))
* **release:** resolve build failures for v0.6.3 ([01bda3b](https://github.com/plebbit/plebchan/commit/01bda3bffb3ba423a4fda447668dc3682263e945))
* resolve PR 877 issues - route params, artifact paths, and build configs ([50ee10c](https://github.com/plebbit/plebchan/commit/50ee10cbc9055d9e2aa0e67df66b325b8f355b96))
## [0.6.2](https://github.com/plebbit/plebchan/compare/v0.6.1...v0.6.2) (2026-01-26)
+5 -5
View File
@@ -3,13 +3,13 @@ import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'fivechan.android',
appName: '5chan',
webDir: 'dist',
webDir: 'build',
plugins: {
CapacitorHttp: {
enabled: true,
},
FileUploader: {
enabled: true
enabled: true,
},
StatusBar: {
style: 'Dark',
@@ -21,8 +21,8 @@ const config: CapacitorConfig = {
},
},
server: {
androidScheme: 'https'
}
androidScheme: 'https',
},
};
export default config;
export default config;
+1 -1
View File
@@ -319,7 +319,7 @@ const createMainWindow = () => {
app.whenReady().then(() => {
// Set app name and dock icon for development mode on macOS
if (process.platform === 'darwin') {
if (process.platform === 'darwin' && isDev) {
app.setName('seedit');
if (app.dock) {
const iconPath = path.join(dirname, '..', isDev ? 'public' : 'build', 'icon.png');
+2 -2
View File
@@ -69,7 +69,7 @@ const config = {
config: {
name: '5chan',
icon: './public/icon.png',
format: 'UDZO',
format: 'ULFO',
},
},
{
@@ -82,7 +82,7 @@ const config = {
platforms: ['win32'],
config: {
name: '5chan',
setupIcon: './public/favicon.ico',
setupIcon: './public/windows-icon.ico',
},
},
// Linux
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "5chan",
"version": "0.6.2",
"version": "0.6.4",
"description": "A bitsocial client with a 4chan UI",
"type": "module",
"author": "Bitsocial Labs",
@@ -114,11 +114,11 @@
"cz-conventional-changelog": "3.3.0",
"decompress": "4.2.1",
"electron": "36.9.5",
"@electron-forge/cli": "7.6.0",
"@electron-forge/maker-dmg": "7.6.0",
"@electron-forge/maker-squirrel": "7.6.0",
"@electron-forge/maker-zip": "7.6.0",
"@electron-forge/plugin-auto-unpack-natives": "7.6.0",
"@electron-forge/cli": "7.8.0",
"@electron-forge/maker-dmg": "7.8.0",
"@electron-forge/maker-squirrel": "7.8.0",
"@electron-forge/maker-zip": "7.8.0",
"@electron-forge/plugin-auto-unpack-natives": "7.8.0",
"@reforged/maker-appimage": "5.1.1",
"husky": "4.3.8",
"isomorphic-fetch": "3.0.0",
@@ -157,7 +157,7 @@
"diff": "^8.0.3",
"lodash": "4.17.23",
"seroval": "1.4.1",
"tar": "7.5.4"
"tar": "6.2.1"
},
"main": "electron/main.js",
"lint-staged": {
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "واجهة",
"invalid_url": "رابط غير صالح",
"image_search": "البحث عن صورة",
"copy_link": "نسخ الرابط",
"hide_post": "إخفاء المنشور",
"hide_thread": "إخفاء الموضوع",
"unhide_post": "إظهار المنشور",
@@ -247,7 +246,7 @@
"delete_avatar": "حذف الصورة الرمزية",
"updates": "التحديثات",
"downloading_board": "جارٍ تنزيل المنتدى",
"subscriptions_subtitle": "لقد اشتركت في {{count}} لوحة",
"subscriptions_subtitle": "أنت مشترك في {{count}} لوحة",
"all_boards": "جميع اللوحات",
"nsfw_boards_only": "لوحات NSFW فقط",
"worksafe_boards_only": "لوحات آمنة للعمل فقط",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "في انتظار موافقة المشرف، غير مرئي للمستخدمين",
"copy_user_id": "نسخ معرف المستخدم",
"copy_direct_link": "نسخ الرابط المباشر"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "ইন্টারফেস",
"invalid_url": "অবৈধ ইউআরএল",
"image_search": "চিত্র অনুসন্ধান",
"copy_link": "লিংক অনুলিপি করুন",
"hide_post": "পোস্ট লুকাও",
"hide_thread": "থ্রেড লুকাও",
"unhide_post": "পোস্ট প্রদর্শন করুন",
@@ -247,7 +246,7 @@
"delete_avatar": "অবতার মুছুন",
"updates": "আপডেট",
"downloading_board": "বোর্ড ডাউনলোড করা হচ্ছে",
"subscriptions_subtitle": "আপনি {{count}}টি board-এ সাবস্ক্রাইব করছেন",
"subscriptions_subtitle": "আপনি {{count}}টি board-এ সাবস্ক্রাইব করছেন",
"all_boards": "সব বোর্ড",
"nsfw_boards_only": "NSFW বোর্ড শুধুমাত্র",
"worksafe_boards_only": "কাজের জন্য নিরাপদ বোর্ড শুধুমাত্র",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "মডারেটর অনুমোদনের জন্য অপেক্ষা করছে, ব্যবহারকারীদের কাছে দৃশ্যমান নয়",
"copy_user_id": "ইউজার আইডি কপি করুন",
"copy_direct_link": "সরাসরি লিংক অনুলিপি করুন"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "rozhraní",
"invalid_url": "Neplatná adresa URL",
"image_search": "hledání obrázku",
"copy_link": "kopírovat odkaz",
"hide_post": "skrýt příspěvek",
"hide_thread": "skrýt vlákno",
"unhide_post": "zobrazit příspěvek",
@@ -247,7 +246,7 @@
"delete_avatar": "Smazat avatar",
"updates": "Aktualizace",
"downloading_board": "stahování fóra",
"subscriptions_subtitle": "Přihlásili jste se k {{count}} boardu",
"subscriptions_subtitle": "Jste přihlášeni k {{count}} boards",
"all_boards": "Všechny desky",
"nsfw_boards_only": "Pouze NSFW desky",
"worksafe_boards_only": "Pouze bezpečné desky",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Čekání na schválení moderatorem, není viditelné uživatelům",
"copy_user_id": "kopírovat ID uživatele",
"copy_direct_link": "kopírovat přímý odkaz"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "interface",
"invalid_url": "Ugyldig webadresse",
"image_search": "billedsøgning",
"copy_link": "kopier link",
"hide_post": "skjul post",
"hide_thread": "skjul tråd",
"unhide_post": "vis indlæg",
@@ -247,7 +246,7 @@
"delete_avatar": "Slet avatar",
"updates": "Opdateringer",
"downloading_board": "downloader forum",
"subscriptions_subtitle": "Du har abonneret på {{count}} board",
"subscriptions_subtitle": "Du er abonneret på {{count}} boards",
"all_boards": "Alle tavler",
"nsfw_boards_only": "Kun NSFW-tavler",
"worksafe_boards_only": "Kun arbejdssikre tavler",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Afventer moderatorgodkendelse, ikke synlig for brugere",
"copy_user_id": "kopier bruger-ID",
"copy_direct_link": "kopier direkte link"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "Schnittstelle",
"invalid_url": "Ungültige URL",
"image_search": "Bildersuche",
"copy_link": "Link kopieren",
"hide_post": "Beitrag ausblenden",
"hide_thread": "Thread ausblenden",
"unhide_post": "Beitrag anzeigen",
@@ -247,7 +246,7 @@
"delete_avatar": "Avatar löschen",
"updates": "Aktualisierungen",
"downloading_board": "Forum wird heruntergeladen",
"subscriptions_subtitle": "Sie haben {{count}} Board abonniert",
"subscriptions_subtitle": "Sie sind {{count}} Board abonniert",
"all_boards": "Alle Boards",
"nsfw_boards_only": "Nur NSFW-Boards",
"worksafe_boards_only": "Nur arbeitsplatzsichere Boards",
@@ -276,5 +275,7 @@
"type": "Typ",
"yes": "Ja",
"no": "Nein",
"pending_mod_approval": "Ausstehende Moderatorgenehmigung, für Benutzer nicht sichtbar"
"pending_mod_approval": "Ausstehende Moderatorgenehmigung, für Benutzer nicht sichtbar",
"copy_user_id": "Benutzer-ID kopieren",
"copy_direct_link": "Direktlink kopieren"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "διεπαφή",
"invalid_url": "Μη έγκυρη διεύθυνση URL",
"image_search": "αναζήτηση εικόνων",
"copy_link": "αντιγραφή συνδέσμου",
"hide_post": "απόκρυψη ανάρτησης",
"hide_thread": "απόκρυψη νήματος",
"unhide_post": "εμφάνιση ανάρτησης",
@@ -247,7 +246,7 @@
"delete_avatar": "Διαγραφή avatar",
"updates": "Ενημερώσεις",
"downloading_board": "λήψη φόρουμ",
"subscriptions_subtitle": "Έχετε εγγραφεί σε {{count}} board",
"subscriptions_subtitle": "Είστε εγγεγραμμένοι σε {{count}} boards",
"all_boards": "Όλες οι σανίδες",
"nsfw_boards_only": "Μόνο σανίδες NSFW",
"worksafe_boards_only": "Μόνο ασφαλείς σανίδες",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Σε αναμονή έγκρισης διαχειριστή, δεν είναι ορατό στους χρήστες",
"copy_user_id": "αντιγραφή ID χρήστη",
"copy_direct_link": "αντιγραφή άμεσου συνδέσμου"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "interface",
"invalid_url": "Invalid URL",
"image_search": "image search",
"copy_link": "Copy link",
"hide_post": "Hide post",
"hide_thread": "Hide thread",
"unhide_post": "Unhide post",
@@ -249,7 +248,7 @@
"moderator_of_this_board": "This user is a moderator of this board",
"administrator_of_this_board": "This user is an administrator of this board",
"copy_content_id": "Copy content ID",
"subscriptions_subtitle": "You have subscribed to {{count}} boards",
"subscriptions_subtitle": "You are subscribed to {{count}} boards",
"bitsocial_account": "Bitsocial Account",
"p2p_options": "P2P options",
"purge": "purge",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Pending mod approval, not visible to users",
"copy_user_id": "Copy user ID",
"copy_direct_link": "Copy direct link"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "interfaz",
"invalid_url": "URL no válida",
"image_search": "búsqueda de imágenes",
"copy_link": "Copiar enlace",
"hide_post": "Ocultar publicación",
"hide_thread": "Ocultar hilo",
"unhide_post": "Mostrar publicación",
@@ -247,7 +246,7 @@
"delete_avatar": "Eliminar avatar",
"updates": "Actualizaciones",
"downloading_board": "descargando foro",
"subscriptions_subtitle": "Te has suscrito a {{count}} board",
"subscriptions_subtitle": "Estás suscrito a {{count}} boards",
"all_boards": "Todas las tablas",
"nsfw_boards_only": "Solo tablas NSFW",
"worksafe_boards_only": "Solo tablas seguras para el trabajo",
@@ -276,5 +275,7 @@
"type": "Tipo",
"yes": "Sí",
"no": "No",
"pending_mod_approval": "Pendiente de aprobación del moderador, no visible para los usuarios"
"pending_mod_approval": "Pendiente de aprobación del moderador, no visible para los usuarios",
"copy_user_id": "Copiar ID de usuario",
"copy_direct_link": "Copiar enlace directo"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "رابط",
"invalid_url": "آدرس اینترنتی نامعتبر است",
"image_search": "جستجوی تصویر",
"copy_link": "رونوشت لینک",
"hide_post": "پنهان کردن پست",
"hide_thread": "پنهان کردن موضوع",
"unhide_post": "نمایش پست",
@@ -247,7 +246,7 @@
"delete_avatar": "حذف آواتار",
"updates": "به‌روزرسانی‌ها",
"downloading_board": "در حال دانلود تالار",
"subscriptions_subtitle": "شما به {{count}} board مشترک شده‌اید",
"subscriptions_subtitle": "شما به {{count}} board مشترک هستید",
"all_boards": "همه تابلوها",
"nsfw_boards_only": "فقط تابلوهای NSFW",
"worksafe_boards_only": "فقط تابلوهای ایمن برای کار",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "در انتظار تایید مدیر، برای کاربران قابل مشاهده نیست",
"copy_user_id": "کپی شناسه کاربر",
"copy_direct_link": "رونوشت لینک مستقیم"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "liitäntä",
"invalid_url": "Virheellinen URL-osoite",
"image_search": "kuvahaku",
"copy_link": "kopioi linkki",
"hide_post": "piilota viesti",
"hide_thread": "piilota keskustelu",
"unhide_post": "näytä viesti",
@@ -247,7 +246,7 @@
"delete_avatar": "Poista avatar",
"updates": "Päivitykset",
"downloading_board": "ladataan foorumia",
"subscriptions_subtitle": "Olet tilannut {{count}} boardia",
"subscriptions_subtitle": "Olet tilannut {{count}} boards",
"all_boards": "Kaikki taulut",
"nsfw_boards_only": "Vain NSFW-taulut",
"worksafe_boards_only": "Vain työturvalliset taulut",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Odottaa moderaattorin hyväksyntää, ei näkyvissä käyttäjille",
"copy_user_id": "kopioi käyttäjätunnus",
"copy_direct_link": "kopioi suora linkki"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "interface",
"invalid_url": "Hindi wastong URL",
"image_search": "paghahanap ng larawan",
"copy_link": "kopyahin ang link",
"hide_post": "itago ang post",
"hide_thread": "itago ang thread",
"unhide_post": "ipakita ang post",
@@ -247,7 +246,7 @@
"delete_avatar": "Tanggalin ang avatar",
"updates": "Mga Update",
"downloading_board": "nagda-download ng board",
"subscriptions_subtitle": "Nag-subscribe ka sa {{count}} na board",
"subscriptions_subtitle": "Naka-subscribe ka sa {{count}} na boards",
"all_boards": "Lahat ng boards",
"nsfw_boards_only": "NSFW boards lang",
"worksafe_boards_only": "Worksafe boards lang",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Naghihintay ng pag-apruba ng moderator, hindi nakikita ng mga gumagamit",
"copy_user_id": "kopyahin ang user ID",
"copy_direct_link": "kopyahin ang direct link"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "interface",
"invalid_url": "URL non valide",
"image_search": "recherche d'images",
"copy_link": "Copier le lien",
"hide_post": "Masquer la publication",
"hide_thread": "Masquer le fil",
"unhide_post": "Afficher la publication",
@@ -247,7 +246,7 @@
"delete_avatar": "Supprimer l'avatar",
"updates": "Mises à jour",
"downloading_board": "téléchargement du forum",
"subscriptions_subtitle": "Vous vous êtes abonné à {{count}} board",
"subscriptions_subtitle": "Vous êtes abonné à {{count}} boards",
"all_boards": "Tous les tableaux",
"nsfw_boards_only": "Tableaux NSFW uniquement",
"worksafe_boards_only": "Tableaux sûrs uniquement",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Oui",
"no": "Non",
"pending_mod_approval": "En attente d'approbation du modérateur, non visible pour les utilisateurs"
"pending_mod_approval": "En attente d'approbation du modérateur, non visible pour les utilisateurs",
"copy_user_id": "Copier l'ID utilisateur",
"copy_direct_link": "Copier le lien direct"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "ממשק",
"invalid_url": "כתובת אינטרנט לא תקינה",
"image_search": "חיפוש תמונות",
"copy_link": "העתק קישור",
"hide_post": "הסתר פוסט",
"hide_thread": "הסתר ת’רד",
"unhide_post": "הצג פוסט",
@@ -247,7 +246,7 @@
"delete_avatar": "מחק אווטר",
"updates": "עדכונים",
"downloading_board": "מוריד פורום",
"subscriptions_subtitle": "נרשמת ל-{{count}} board",
"subscriptions_subtitle": "נרשמת ל-{{count}} boards",
"all_boards": "כל הלוחות",
"nsfw_boards_only": "לוחות NSFW בלבד",
"worksafe_boards_only": "לוחות בטוחים לעבודה בלבד",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "ממתין לאישור מנהל, לא ניתן לראות למשתמשים",
"copy_user_id": "העתק מזהה משתמש",
"copy_direct_link": "העתק קישור ישיר"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "इंटरफेस",
"invalid_url": "अमान्य URL",
"image_search": "चित्र खोज",
"copy_link": "लिंक कॉपी करें",
"hide_post": "पोस्ट छुपाएं",
"hide_thread": "धागा छुपाएं",
"unhide_post": "पोस्ट प्रदर्शित करें",
@@ -247,7 +246,7 @@
"delete_avatar": "अवतार हटाएं",
"updates": "अपडेट",
"downloading_board": "फोरम डाउनलोड हो रहा है",
"subscriptions_subtitle": "आपने {{count}} board की सदस्यता ल है",
"subscriptions_subtitle": "आप {{count}} boards की सदस्यता ले रहे है",
"all_boards": "सभी बोर्ड",
"nsfw_boards_only": "केवल NSFW बोर्ड",
"worksafe_boards_only": "केवल कार्य-सुरक्षित बोर्ड",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "मॉडरेटर की मंजूरी के लिए प्रतीक्षा में, उपयोगकर्ताओं को दिखाई नहीं देता",
"copy_user_id": "यूज़र ID कॉपी करें",
"copy_direct_link": "डायरेक्ट लिंक कॉपी करें"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "felület",
"invalid_url": "Érvénytelen URL-cím",
"image_search": "képkeresés",
"copy_link": "link másolása",
"hide_post": "bejegyzés elrejtése",
"hide_thread": "szál elrejtése",
"unhide_post": "bejegyzés megjelenítése",
@@ -247,7 +246,7 @@
"delete_avatar": "Avatar törlése",
"updates": "Frissítések",
"downloading_board": "fórum letöltése",
"subscriptions_subtitle": "Feliratkoztál {{count}} boardra",
"subscriptions_subtitle": "Fel vagy iratkozva {{count}} boardra",
"all_boards": "Minden tábla",
"nsfw_boards_only": "Csak NSFW táblák",
"worksafe_boards_only": "Csak munkavédelmi táblák",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Moderátor jóváhagyása alatt áll, a felhasználók számára nem látható",
"copy_user_id": "felhasználói ID másolása",
"copy_direct_link": "közvetlen link másolása"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "antarmuka",
"invalid_url": "URL tidak valid",
"image_search": "pencarian gambar",
"copy_link": "salin tautan",
"hide_post": "sembunyikan pos",
"hide_thread": "sembunyikan utas",
"unhide_post": "tampilkan pos",
@@ -247,7 +246,7 @@
"delete_avatar": "Hapus avatar",
"updates": "Pembaruan",
"downloading_board": "mengunduh forum",
"subscriptions_subtitle": "Anda telah berlangganan {{count}} board",
"subscriptions_subtitle": "Anda berlangganan {{count}} boards",
"all_boards": "Semua papan",
"nsfw_boards_only": "Hanya papan NSFW",
"worksafe_boards_only": "Hanya papan aman untuk kerja",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Menunggu persetujuan moderator, tidak terlihat oleh pengguna",
"copy_user_id": "salin ID pengguna",
"copy_direct_link": "salin tautan langsung"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "interfaccia",
"invalid_url": "URL non valido",
"image_search": "ricerca immagini",
"copy_link": "Copia link",
"hide_post": "Nascondi post",
"hide_thread": "Nascondi thread",
"unhide_post": "Mostra post",
@@ -247,7 +246,7 @@
"delete_avatar": "Elimina avatar",
"updates": "Aggiornamenti",
"downloading_board": "scaricando board",
"subscriptions_subtitle": "Ti sei iscritto a {{count}} board",
"subscriptions_subtitle": "Sei iscritto a {{count}} board",
"all_boards": "Tutte le bacheche",
"nsfw_boards_only": "Solo bacheche NSFW",
"worksafe_boards_only": "Solo bacheche sicure per il lavoro",
@@ -276,5 +275,7 @@
"type": "Tipo",
"yes": "Sì",
"no": "No",
"pending_mod_approval": "In attesa di approvazione del moderatore, non visibile agli utenti"
"pending_mod_approval": "In attesa di approvazione del moderatore, non visibile agli utenti",
"copy_user_id": "Copia ID utente",
"copy_direct_link": "Copia link diretto"
}
+3 -2
View File
@@ -99,7 +99,6 @@
"interface": "インターフェース",
"invalid_url": "無効なURL",
"image_search": "画像検索",
"copy_link": "リンクをコピー",
"hide_post": "投稿を非表示",
"hide_thread": "スレッドを非表示",
"unhide_post": "投稿を表示",
@@ -276,5 +275,7 @@
"type": "タイプ",
"yes": "はい",
"no": "いいえ",
"pending_mod_approval": "モデレーターの承認待ち、ユーザーには表示されません"
"pending_mod_approval": "モデレーターの承認待ち、ユーザーには表示されません",
"copy_user_id": "ユーザーIDをコピー",
"copy_direct_link": "直接リンクをコピー"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "인터페이스",
"invalid_url": "잘못된 URL",
"image_search": "이미지 검색",
"copy_link": "링크 복사",
"hide_post": "게시물 숨기기",
"hide_thread": "스레드 숨기기",
"unhide_post": "게시물 숨기기 해제",
@@ -247,7 +246,7 @@
"delete_avatar": "아바타 삭제",
"updates": "업데이트",
"downloading_board": "게시판 다운로드 중",
"subscriptions_subtitle": "{{count}}개의 board를 구독했습니다",
"subscriptions_subtitle": "{{count}}개의 board를 구독 중입니다",
"all_boards": "모든 게시판",
"nsfw_boards_only": "NSFW 게시판만",
"worksafe_boards_only": "직장 안전 게시판만",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "중재자 승인 대기 중, 사용자에게 표시되지 않음",
"copy_user_id": "사용자 ID 복사",
"copy_direct_link": "직접 링크 복사"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "इंटरफेस",
"invalid_url": "अवैध URL",
"image_search": "प्रतिमा शोध",
"copy_link": "लिंक कॉपी करा",
"hide_post": "पोस्ट लपवा",
"hide_thread": "थ्रेड लपवा",
"unhide_post": "पोस्ट दाखवा",
@@ -247,7 +246,7 @@
"delete_avatar": "अवतार हटवा",
"updates": "अपडेट्स",
"downloading_board": "फोरम डाउनलोड करत आहे",
"subscriptions_subtitle": "तुम्ही {{count}} board ला सदस्यता घेतली आहे",
"subscriptions_subtitle": "तुम्ही {{count}} boards ला सदस्यता घेतली आहे",
"all_boards": "सर्व बोर्ड",
"nsfw_boards_only": "फक्त NSFW बोर्ड",
"worksafe_boards_only": "फक्त कामासाठी सुरक्षित बोर्ड",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "संचालकाच्या मंजुरीची प्रतीक्षा, वापरकर्त्यांना दिसत नाही",
"copy_user_id": "यूजर ID कॉपी करा",
"copy_direct_link": "डायरेक्ट लिंक कॉपी करा"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "interface",
"invalid_url": "Ongeldige URL",
"image_search": "afbeeldingen zoeken",
"copy_link": "link kopiëren",
"hide_post": "bericht verbergen",
"hide_thread": "thread verbergen",
"unhide_post": "bericht weergeven",
@@ -247,7 +246,7 @@
"delete_avatar": "Avatar verwijderen",
"updates": "Updates",
"downloading_board": "board downloaden",
"subscriptions_subtitle": "Je bent geabonneerd op {{count}} board",
"subscriptions_subtitle": "Je bent geabonneerd op {{count}} boards",
"all_boards": "Alle borden",
"nsfw_boards_only": "Alleen NSFW-borden",
"worksafe_boards_only": "Alleen werkveilige borden",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "In afwachting van moderatorgoedkeuring, niet zichtbaar voor gebruikers",
"copy_user_id": "gebruikers-ID kopiëren",
"copy_direct_link": "directe link kopiëren"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "grensesnitt",
"invalid_url": "Ugyldig URL",
"image_search": "bilde søk",
"copy_link": "kopier lenke",
"hide_post": "skjul innlegg",
"hide_thread": "skjul tråd",
"unhide_post": "vis innlegg",
@@ -247,7 +246,7 @@
"delete_avatar": "Slett avatar",
"updates": "Oppdateringer",
"downloading_board": "laster ned forum",
"subscriptions_subtitle": "Du har abonnert på {{count}} board",
"subscriptions_subtitle": "Du er abonnent på {{count}} boards",
"all_boards": "Alle tavler",
"nsfw_boards_only": "Kun NSFW-tavler",
"worksafe_boards_only": "Kun arbeidssikre tavler",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Venter på moderatorgodkjenning, ikke synlig for brukere",
"copy_user_id": "kopier bruker-ID",
"copy_direct_link": "kopier direkte lenke"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "interfejs",
"invalid_url": "Nieprawidłowy adres URL",
"image_search": "wyszukiwanie obrazów",
"copy_link": "skopiuj link",
"hide_post": "ukryj post",
"hide_thread": "ukryj wątek",
"unhide_post": "pokaż post",
@@ -247,7 +246,7 @@
"delete_avatar": "Usuń avatar",
"updates": "Aktualizacje",
"downloading_board": "pobieranie forum",
"subscriptions_subtitle": "Subskrybujesz {{count}} board",
"subscriptions_subtitle": "Subskrybujesz {{count}} boards",
"all_boards": "Wszystkie tablice",
"nsfw_boards_only": "Tylko tablice NSFW",
"worksafe_boards_only": "Tylko bezpieczne tablice",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Oczekiwanie na zatwierdzenie moderatora, niewidoczne dla użytkowników",
"copy_user_id": "skopiuj ID użytkownika",
"copy_direct_link": "skopiuj bezpośredni link"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "interface",
"invalid_url": "URL inválida",
"image_search": "pesquisa de imagens",
"copy_link": "Copiar link",
"hide_post": "Ocultar postagem",
"hide_thread": "Ocultar tópico",
"unhide_post": "Mostrar postagem",
@@ -247,7 +246,7 @@
"delete_avatar": "Excluir avatar",
"updates": "Atualizações",
"downloading_board": "baixando fórum",
"subscriptions_subtitle": "Você se inscreveu em {{count}} board",
"subscriptions_subtitle": "Você está inscrito em {{count}} boards",
"all_boards": "Todos os quadros",
"nsfw_boards_only": "Apenas quadros NSFW",
"worksafe_boards_only": "Apenas quadros seguros para o trabalho",
@@ -276,5 +275,7 @@
"type": "Tipo",
"yes": "Sim",
"no": "Não",
"pending_mod_approval": "Aguardando aprovação do moderador, não visível para os usuários"
"pending_mod_approval": "Aguardando aprovação do moderador, não visível para os usuários",
"copy_user_id": "Copiar ID do usuário",
"copy_direct_link": "Copiar link direto"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "interfață",
"invalid_url": "URL invalid",
"image_search": "căutare de imagini",
"copy_link": "copiați linkul",
"hide_post": "ascundeți postarea",
"hide_thread": "ascundeți firul",
"unhide_post": "afișează postarea",
@@ -247,7 +246,7 @@
"delete_avatar": "Șterge avatar",
"updates": "Actualizări",
"downloading_board": "se descarcă forumul",
"subscriptions_subtitle": "Te-ai abonat la {{count}} board",
"subscriptions_subtitle": "Ești abonat la {{count}} boards",
"all_boards": "Toate panourile",
"nsfw_boards_only": "Doar panouri NSFW",
"worksafe_boards_only": "Doar panouri sigure pentru muncă",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "În așteptarea aprobării moderatorului, nu este vizibil pentru utilizatori",
"copy_user_id": "copiați ID-ul utilizatorului",
"copy_direct_link": "copiați linkul direct"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "интерфейс",
"invalid_url": "Недействительный URL",
"image_search": "поиск изображений",
"copy_link": "копировать ссылку",
"hide_post": "скрыть пост",
"hide_thread": "скрыть ветку",
"unhide_post": "показать пост",
@@ -247,7 +246,7 @@
"delete_avatar": "Удалить аватар",
"updates": "Обновления",
"downloading_board": "загрузка форума",
"subscriptions_subtitle": "Вы подписались на {{count}} board",
"subscriptions_subtitle": "Вы подписаны на {{count}} boards",
"all_boards": "Все доски",
"nsfw_boards_only": "Только доски NSFW",
"worksafe_boards_only": "Только безопасные для работы доски",
@@ -276,5 +275,7 @@
"type": "Тип",
"yes": "Да",
"no": "Нет",
"pending_mod_approval": "Ожидает одобрения модератора, не видно пользователям"
"pending_mod_approval": "Ожидает одобрения модератора, не видно пользователям",
"copy_user_id": "копировать ID пользователя",
"copy_direct_link": "копировать прямую ссылку"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "ndërfaqe",
"invalid_url": "URL jo e vlefshëm",
"image_search": "kërkim imazhesh",
"copy_link": "kopjo linkun",
"hide_post": "fsheh postën",
"hide_thread": "fsheh fijen",
"unhide_post": "shfaq postën",
@@ -247,7 +246,7 @@
"delete_avatar": "Fshi avatarin",
"updates": "Përditësime",
"downloading_board": "duke shkarkuar forum",
"subscriptions_subtitle": "Jeni abonuar në {{count}} board",
"subscriptions_subtitle": "Jeni abonuar në {{count}} boards",
"all_boards": "Të gjitha tabelat",
"nsfw_boards_only": "Vetëm tabelat NSFW",
"worksafe_boards_only": "Vetëm tabelat e sigurta për punë",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Duke pritur miratimin e moderatorit, jo i dukshëm për përdoruesit",
"copy_user_id": "kopjo ID e përdoruesit",
"copy_direct_link": "kopjo lidhjen e drejtpërdrejtë"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "gränssnitt",
"invalid_url": "Ogiltig URL",
"image_search": "bildsökning",
"copy_link": "kopiera länk",
"hide_post": "dölj inlägg",
"hide_thread": "dölj tråd",
"unhide_post": "visa inlägg",
@@ -247,7 +246,7 @@
"delete_avatar": "Ta bort avatar",
"updates": "Uppdateringar",
"downloading_board": "laddar ner forum",
"subscriptions_subtitle": "Du har prenumererat på {{count}} board",
"subscriptions_subtitle": "Du är prenumerant på {{count}} boards",
"all_boards": "Alla tavlor",
"nsfw_boards_only": "Endast NSFW-tavlor",
"worksafe_boards_only": "Endast arbetsplats-säkra tavlor",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Väntar på moderatorgodkännande, inte synligt för användare",
"copy_user_id": "kopiera användar-ID",
"copy_direct_link": "kopiera direktlänk"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "అంతరాఫ్సు",
"invalid_url": "చెల్లని URL",
"image_search": "చిత్రాల శోధన",
"copy_link": "లింక్ ను కాపీ చేయండి",
"hide_post": "పోస్ట్ దాచు",
"hide_thread": "థ్రెడ్ దాచు",
"unhide_post": "పోస్ట్ చూపించు",
@@ -247,7 +246,7 @@
"delete_avatar": "అవతార్ను తొలగించు",
"updates": "నవీకరణలు",
"downloading_board": "ఫోరం డౌన్‌లోడ్ చేయడం",
"subscriptions_subtitle": "మీరు {{count}} boardకు సభ్యత్వం పొందారు",
"subscriptions_subtitle": "మీరు {{count}} boardsకు సభ్యత్వం పొందారు",
"all_boards": "అన్ని బోర్డులు",
"nsfw_boards_only": "NSFW బోర్డులు మాత్రమే",
"worksafe_boards_only": "పని కోసం సురక్షితమైన బోర్డులు మాత్రమే",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "మాడరేటర్ ఆమోదం కోసం చేతనిస్తోంది, వినియోగదారులకు కనిపించవు",
"copy_user_id": "యూజర్ ID కాపీ చేయండి",
"copy_direct_link": "డైరెక్ట్ లింక్ కాపీ చేయండి"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "อินเทอร์เฟซ",
"invalid_url": "URL ไม่ถูกต้อง",
"image_search": "ค้นหารูปภาพ",
"copy_link": "คัดลอกลิงก์",
"hide_post": "ซ่อนโพสต์",
"hide_thread": "ซ่อนเธรด",
"unhide_post": "แสดงโพสต์",
@@ -247,7 +246,7 @@
"delete_avatar": "ลบอวตาร",
"updates": "อัปเดต",
"downloading_board": "กำลังดาวน์โหลดกระดาน",
"subscriptions_subtitle": "คุณได้สมัครสมาชิก {{count}} board แล้ว",
"subscriptions_subtitle": "คุณกำลังสมัครสมาชิก {{count}} boards",
"all_boards": "บอร์ดทั้งหมด",
"nsfw_boards_only": "บอร์ด NSFW เท่านั้น",
"worksafe_boards_only": "บอร์ดที่ปลอดภัยสำหรับงานเท่านั้น",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "รอการอนุมัติจากผู้ดูแล ไม่มองเห็นสำหรับผู้ใช้",
"copy_user_id": "คัดลอก ID ผู้ใช้",
"copy_direct_link": "คัดลอกลิงก์โดยตรง"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "arayüz",
"invalid_url": "Geçersiz URL",
"image_search": "resim arama",
"copy_link": "bağlantıyı kopyala",
"hide_post": "gönderiyi gizle",
"hide_thread": "konuyu gizle",
"unhide_post": "gönderiyi göster",
@@ -247,7 +246,7 @@
"delete_avatar": "Avatarı sil",
"updates": "Güncellemeler",
"downloading_board": "forum indiriliyor",
"subscriptions_subtitle": "{{count}} board'a abone oldunuz",
"subscriptions_subtitle": "{{count}} board'a abonesiniz",
"all_boards": "Tüm panolar",
"nsfw_boards_only": "Sadece NSFW panoları",
"worksafe_boards_only": "Sadece iş için güvenli panolar",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Moderatör onayı beklemede, kullanıcılara görünmüyor",
"copy_user_id": "kullanıcı ID'sini kopyala",
"copy_direct_link": "doğrudan bağlantıyı kopyala"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "інтерфейс",
"invalid_url": "Недійсний URL",
"image_search": "пошук зображень",
"copy_link": "копіювати посилання",
"hide_post": "сховати допис",
"hide_thread": "сховати нитку",
"unhide_post": "показати допис",
@@ -247,7 +246,7 @@
"delete_avatar": "Видалити аватар",
"updates": "Оновлення",
"downloading_board": "завантаження форуму",
"subscriptions_subtitle": "Ви підписались на {{count}} board",
"subscriptions_subtitle": "Ви підписані на {{count}} boards",
"all_boards": "Всі дошки",
"nsfw_boards_only": "Тільки дошки NSFW",
"worksafe_boards_only": "Тільки безпечні для роботи дошки",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Очікує затвердження модератора, не видно користувачам",
"copy_user_id": "копіювати ID користувача",
"copy_direct_link": "копіювати пряме посилання"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "انٹرفیس",
"invalid_url": "غیر درست یو آر ایل",
"image_search": "تصویر کی تلاش",
"copy_link": "لنک کاپی کریں",
"hide_post": "پوسٹ چھپائیں",
"hide_thread": "دھاگہ چھپائیں",
"unhide_post": "پوسٹ دکھائیں",
@@ -247,7 +246,7 @@
"delete_avatar": "اوتار حذف کریں",
"updates": "اپڈیٹس",
"downloading_board": "فورم ڈاؤن لوڈ ہو رہا ہے",
"subscriptions_subtitle": "آپ نے {{count}} board کی سبسکرپشن لی ہے",
"subscriptions_subtitle": "آپ {{count}} boards کی سبسکرپشن لے رہے ہیں",
"all_boards": "تمام بورڈز",
"nsfw_boards_only": "صرف NSFW بورڈز",
"worksafe_boards_only": "صرف کام کے لیے محفوظ بورڈز",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "ماڈریٹر کی منظوری کا انتظار، صارفین کے لیے نظر نہیں آتا",
"copy_user_id": "صارف کی شناخت کاپی کریں",
"copy_direct_link": "براہ راست لنک کاپی کریں"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "giao diện",
"invalid_url": "URL không hợp lệ",
"image_search": "tìm kiếm hình ảnh",
"copy_link": "sao chép liên kết",
"hide_post": "Ẩn bài viết",
"hide_thread": "Ẩn chủ đề",
"unhide_post": "Hiện bài viết",
@@ -247,7 +246,7 @@
"delete_avatar": "Xóa avatar",
"updates": "Cập nhật",
"downloading_board": "đang tải diễn đàn",
"subscriptions_subtitle": "Bạn đã đăng ký {{count}} board",
"subscriptions_subtitle": "Bạn đang đăng ký {{count}} boards",
"all_boards": "Tất cả bảng",
"nsfw_boards_only": "Chỉ bảng NSFW",
"worksafe_boards_only": "Chỉ bảng an toàn cho công việc",
@@ -276,5 +275,7 @@
"type": "Type",
"yes": "Yes",
"no": "No",
"pending_mod_approval": "Pending mod approval, not visible to users"
"pending_mod_approval": "Chờ duyệt của người điều hành, không hiển thị cho người dùng",
"copy_user_id": "sao chép ID người dùng",
"copy_direct_link": "sao chép liên kết trực tiếp"
}
+4 -3
View File
@@ -99,7 +99,6 @@
"interface": "界面",
"invalid_url": "无效的网址",
"image_search": "图像搜索",
"copy_link": "复制链接",
"hide_post": "隐藏帖子",
"hide_thread": "隐藏主题",
"unhide_post": "显示帖子",
@@ -247,7 +246,7 @@
"delete_avatar": "删除头像",
"updates": "更新",
"downloading_board": "正在下载论坛",
"subscriptions_subtitle": "您已订阅 {{count}} 个 board",
"subscriptions_subtitle": "您已订阅 {{count}} 个 boards",
"all_boards": "所有板块",
"nsfw_boards_only": "仅NSFW板块",
"worksafe_boards_only": "仅工作安全板块",
@@ -276,5 +275,7 @@
"type": "类型",
"yes": "是",
"no": "否",
"pending_mod_approval": "等待版主批准,用户不可见"
"pending_mod_approval": "等待版主批准,用户不可见",
"copy_user_id": "复制用户ID",
"copy_direct_link": "复制直接链接"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

+8
View File
@@ -1,6 +1,7 @@
import { readdirSync, writeFileSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -56,6 +57,13 @@ mkdirSync(generatedDir, { recursive: true });
const outputPath = join(generatedDir, 'asset-manifest.ts');
writeFileSync(outputPath, output, 'utf8');
// Format with oxfmt so output matches committed (formatted) version
try {
execSync(`npx oxfmt ${outputPath}`, { stdio: 'ignore' });
} catch {
// oxfmt not available (e.g. CI without devDependencies), skip formatting
}
console.log(
`✅ Generated asset manifest with ${banners.length} banners, ${notFoundImages.length} not-found images, ${buttonImages.length} button images, and ${themeBackgrounds.length} background images`,
);
+5 -1
View File
@@ -101,7 +101,11 @@ const htmlSection = section('Static HTML build', [htmlZip && `- 5chan-html (zip)
const downloads = [macSection, winSection, linuxSection, androidSection, htmlSection].filter(Boolean).join('\n\n');
const releaseBody = `This version rebrands the app to 5chan, introducing a set of new features and several performance improvements.
// One-liner summary of what changed in this release. Update before each release.
const oneLinerDescription =
'This version adds multiple-replying support, 4chan-like user anonymity with or without user IDs depending on the board, and several bug fixes.';
const releaseBody = `${oneLinerDescription}
- Web app: https://5chan.app
- Decentralized web app via IPFS/IPNS gateways (works on any browser): [5chan.eth.limo](https://5chan.eth.limo), [5chan.eth.link](https://5chan.eth.link), [dweb.link/ipfs.io](https://dweb.link/ipns/5chan.eth)
+4 -4
View File
@@ -170,16 +170,16 @@ const App = () => (
<Route path='/mod/catalog/:timeFilterName?' element={null} />
<Route path='/mod/catalog/:timeFilterName?/settings' element={null} />
<Route path='/mod/queue' element={<ModQueueView />} />
<Route path='/mod/queue/settings' element={<ModQueueView />} />
<Route path='/mod/modqueue' element={<ModQueueView />} />
<Route path='/mod/modqueue/settings' element={<ModQueueView />} />
<Route path='/:boardIdentifier' element={null} />
<Route path='/:boardIdentifier/settings' element={null} />
<Route path='/:boardIdentifier/catalog' element={null} />
<Route path='/:boardIdentifier/catalog/settings' element={null} />
<Route path='/:boardIdentifier/queue' element={<ModQueueView />} />
<Route path='/:boardIdentifier/queue/settings' element={<ModQueueView />} />
<Route path='/:boardIdentifier/modqueue' element={<ModQueueView />} />
<Route path='/:boardIdentifier/modqueue/settings' element={<ModQueueView />} />
<Route path='/:boardIdentifier/thread/:commentCid' element={<Post />} />
<Route path='/:boardIdentifier/thread/:commentCid/settings' element={<Post />} />
@@ -1,8 +1,9 @@
import { Fragment, useState } from 'react';
import { Fragment, useMemo, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next';
import { Comment, useComment } from '@plebbit/plebbit-react-hooks';
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
import usePostNumberStore from '../../stores/use-post-number-store';
import Plebbit from '@plebbit/plebbit-js';
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { isPostPageView } from '../../lib/utils/view-utils';
@@ -15,6 +16,15 @@ import Tooltip from '../../components/tooltip';
import styles from '../../views/post/post.module.css';
import _ from 'lodash';
const QuotedCidLink = ({ cid, postCid }: { cid: string; postCid: string }) => {
const commentFromStore = useSubplebbitsPagesStore((state) => state.comments[cid]);
const commentFromHook = useComment({ commentCid: cid, onlyIfCached: true });
// Prefer hook version to ensure 'number' property is populated for deeper nested replies in Virtuoso
const quotedComment = commentFromHook?.number !== undefined ? commentFromHook : commentFromStore;
const isOP = cid === postCid;
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotedComment} isOP={isOP} />;
};
const CommentContent = ({ comment: post }: { comment: Comment }) => {
const { t } = useTranslation();
const params = useParams();
@@ -23,7 +33,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
const [showOriginal, setShowOriginal] = useState(false);
const isMobile = useIsMobile();
const { cid, content, deleted, edit, original, parentCid, postCid, pendingApproval, reason, removed, state, subplebbitAddress } = post || {};
const { cid, content, deleted, edit, original, parentCid, postCid, pendingApproval, quotedCids, reason, removed, state, subplebbitAddress } = post || {};
const banned = !!post?.author?.subplebbit?.banExpiresAt;
const [showFullComment, setShowFullComment] = useState(false);
@@ -43,13 +53,35 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
const isReply = !!parentCid;
const isReplyingToReply = isReply && parentCid !== postCid;
const contentNumbers = useMemo(() => {
if (!content) return new Set<number>();
const matches = content.matchAll(/(?<![>/\w])>>(\d+)(?![\d/])/g);
return new Set([...matches].map((m) => parseInt(m[1], 10)));
}, [content]);
const cidToNumber = usePostNumberStore((state) => state.cidToNumber);
const filteredQuotedCids = useMemo(() => {
if (!quotedCids?.length) return [];
return quotedCids.filter((cid: string) => {
const num = cidToNumber[cid];
return num === undefined || !contentNumbers.has(num);
});
}, [quotedCids, cidToNumber, contentNumbers]);
const shouldShowReplyingToReply = isReplyingToReply && (parentCid ? !contentNumbers.has(cidToNumber[parentCid] ?? -1) : true);
const stateString = useStateString(post);
const loadingString = <div className={styles.stateString}>{stateString !== 'Failed' ? <LoadingEllipsis string={stateString || t('loading')} /> : stateString}</div>;
return (
<blockquote className={`${styles.postMessage} ${!isReply && isMobile && styles.clampLines}`}>
{isReply && state !== 'failed' && isReplyingToReply && !(deleted || removed) && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />}
{isReply &&
state !== 'failed' &&
!(deleted || removed) &&
(filteredQuotedCids.length > 0
? filteredQuotedCids.map((cid: string) => <QuotedCidLink key={cid} cid={cid} postCid={postCid} />)
: shouldShowReplyingToReply && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />)}
{removed ? (
reason ? (
<>
@@ -72,7 +104,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
)
) : (
<>
{!showOriginal && <Markdown content={displayContent} />}
{!showOriginal && <Markdown content={displayContent} postCid={postCid} />}
{pendingApproval && (
<>
<br />
@@ -88,7 +120,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
)}
{edit && original?.content !== content && (
<span className={styles.editedInfo}>
{showOriginal && <Markdown content={original?.content} />}
{showOriginal && <Markdown content={original?.content} postCid={postCid} />}
<br />
<br />
<Trans
@@ -8,6 +8,16 @@
text-decoration: var(--post-content-link-text-decoration-hover);
}
.markdown .inlineQuoteLink a {
color: var(--post-quotelink-text-color);
text-decoration: var(--post-quotelink-text-decoration);
}
.markdown .inlineQuoteLink a:hover {
color: var(--post-quotelink-text-color-hover);
text-decoration: var(--post-quotelink-text-decoration-hover);
}
.hrWrapper {
padding: 0.5em 0;
}
+35 -4
View File
@@ -14,6 +14,10 @@ import styles from './markdown.module.css';
import { Link, useLocation, useParams } from 'react-router-dom';
import { canEmbed } from '../embed';
import { is5chanLink, transform5chanLinkToInternal, preprocess5chanPatterns } from '../../lib/utils/url-utils';
import usePostNumberStore from '../../stores/use-post-number-store';
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
import { useComment } from '@plebbit/plebbit-react-hooks';
import ReplyQuotePreview from '../reply-quote-preview';
interface ContentLinkEmbedProps {
children: any;
@@ -159,13 +163,40 @@ const spoilerTransform = () => (tree: any) => {
interface MarkdownProps {
content: string;
title?: string;
postCid?: string;
}
const renderAnchorLink = (children: React.ReactNode, href: string) => {
const NUMBER_QUOTE_HREF_REGEX = /^#q-(\d+)$/;
const NumberQuoteLink = ({ number, threadPostCid }: { number: number; threadPostCid?: string }) => {
const cid = usePostNumberStore((state) => state.numberToCid[number]);
const commentFromStore = useSubplebbitsPagesStore((state) => (cid ? state.comments[cid] : undefined));
const commentFromHook = useComment({ commentCid: cid, onlyIfCached: true });
const comment = commentFromHook?.number !== undefined ? commentFromHook : commentFromStore;
const isOP = Boolean(threadPostCid && cid === threadPostCid);
if (!comment) {
return <span>{`>>${number}`}</span>;
}
return <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={comment} isOP={isOP} />;
};
const renderAnchorLink = (children: React.ReactNode, href: string, threadPostCid?: string) => {
if (!href) {
return <span>{children}</span>;
}
const numberQuoteMatch = href.match(NUMBER_QUOTE_HREF_REGEX);
if (numberQuoteMatch) {
const number = parseInt(numberQuoteMatch[1], 10);
return (
<span className={styles.inlineQuoteLink}>
<NumberQuoteLink number={number} threadPostCid={threadPostCid} />
</span>
);
}
// Check if this is a valid 5chan link that should be handled internally
if (is5chanLink(href)) {
const internalPath = transform5chanLinkToInternal(href);
@@ -215,7 +246,7 @@ const renderAnchorLink = (children: React.ReactNode, href: string) => {
);
};
const Markdown = ({ content, title }: MarkdownProps) => {
const Markdown = ({ content, title, postCid }: MarkdownProps) => {
const remarkPlugins: any[] = [[supersub]];
if (content && content.length <= MAX_LENGTH_FOR_GFM) {
@@ -285,10 +316,10 @@ const Markdown = ({ content, title }: MarkdownProps) => {
console.debug('Invalid URL:', href);
}
return renderAnchorLink(children, href);
return renderAnchorLink(children, href, postCid);
}
return renderAnchorLink(children, href || '');
return renderAnchorLink(children, href || '', postCid);
},
} as ExtendedComponents
}
+95 -28
View File
@@ -8,8 +8,9 @@ import styles from '../../views/post/post.module.css';
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/post-utils';
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { isValidURL } from '../../lib/utils/url-utils';
import { isValidURL, QUOTE_NUMBER_REGEX } from '../../lib/utils/url-utils';
import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { formatUserIDForDisplay } from '../../lib/utils/string-utils';
import useModQueueStore from '../../stores/use-mod-queue-store';
import { useDirectories } from '../../hooks/use-directories';
import { getBoardPath } from '../../lib/utils/route-utils';
@@ -21,6 +22,7 @@ import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
import useHide from '../../hooks/use-hide';
import useStateString from '../../hooks/use-state-string';
import useScrollToReply from '../../hooks/use-scroll-to-reply';
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
import CommentContent from '../comment-content';
import CommentMedia from '../comment-media';
import EditMenu from '../edit-menu/edit-menu';
@@ -36,6 +38,7 @@ import { shouldShowSnow } from '../../lib/snow';
import useReplyModalStore from '../../stores/use-reply-modal-store';
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
import useChallengesStore from '../../stores/use-challenges-store';
import usePostNumberStore from '../../stores/use-post-number-store';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
import { usePublishCommentModeration } from '@plebbit/plebbit-react-hooks';
@@ -72,6 +75,7 @@ const PostInfo = ({
isPublishing,
onApprove,
onReject,
quotedByMap,
}: PostProps) => {
const { t } = useTranslation();
const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, subplebbitAddress, timestamp } = post || {};
@@ -195,13 +199,16 @@ const PostInfo = ({
const alertThresholdSeconds = getAlertThresholdSeconds();
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
const userID = address && Plebbit.getShortAddress({ address }); // should not be shortened to less than 12 characters, because users can create unlimited addresses/IDs before authenticating or passing challenges, so if the ID is short enough they can spoof it to troll users with the same ID
const userID = address && Plebbit.getShortAddress({ address }); // shortened to 8 chars for display; users can verify the full user ID via "Copy user ID" in the post menu to guard against spoofing
const userIDBackgroundColor = hashStringToColor(userID);
const userIDTextColor = getTextColorForBackground(userIDBackgroundColor);
const handleUserAddressClick = useAuthorAddressClick();
const numberOfPostsByAuthor = document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length;
const pseudonymityMode = useSubplebbitField(subplebbitAddress, (sub) => sub?.features?.pseudonymityMode);
const showUserID = pseudonymityMode !== 'per-reply';
const { hidden } = useHide(post);
const { openReplyModal } = useReplyModalStore();
@@ -265,28 +272,32 @@ const PostInfo = ({
<img src={avatarImageUrl} alt='' />
</span>
) : null}
(ID:{' '}
{deleted ? (
t('deleted')
) : removed ? (
t('removed')
) : (
<Tooltip
children={
<span
title={t('highlight_posts')}
className={styles.userAddress}
onClick={() => handleUserAddressClick(userID, postCid)}
style={{ backgroundColor: userIDBackgroundColor, color: userIDTextColor }}
>
{userID}
</span>
}
content={`${numberOfPostsByAuthor === 1 ? t('1_post_by_this_id') : t('x_posts_by_this_id', { number: numberOfPostsByAuthor })}`}
showTooltip={isInPostPageView || showOmittedReplies[postCid] || (postReplyCount < 6 && !pinned)}
/>
{showUserID && (
<>
(ID:{' '}
{deleted ? (
t('deleted')
) : removed ? (
t('removed')
) : (
<Tooltip
children={
<span
title={t('highlight_posts')}
className={styles.userAddress}
onClick={() => handleUserAddressClick(userID, postCid)}
style={{ backgroundColor: userIDBackgroundColor, color: userIDTextColor }}
>
{formatUserIDForDisplay(userID)}
</span>
}
content={`${numberOfPostsByAuthor === 1 ? t('1_post_by_this_id') : t('x_posts_by_this_id', { number: numberOfPostsByAuthor })}`}
showTooltip={isInPostPageView || showOmittedReplies[postCid] || (postReplyCount < 6 && !pinned)}
/>
)}
){' '}
</>
)}
){' '}
</span>
<span className={styles.dateTime}>
{isInModQueueView && isOverThreshold ? (
@@ -417,6 +428,16 @@ const PostInfo = ({
reply?.cid &&
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
)}
{cid &&
parentCid &&
quotedByMap
?.get(cid)
?.map(
(reply: Comment, index: number) =>
reply?.parentCid !== cid &&
reply?.cid &&
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
)}
</span>
</div>
);
@@ -519,7 +540,7 @@ const PostMedia = ({
);
};
const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => {
const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: PostProps) => {
let post = reply;
// handle pending mod or author edit
const { editedComment } = useEditedComment({ comment: reply });
@@ -547,7 +568,7 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => {
<div className={styles.replyDesktop}>
<div className={styles.sideArrows}>{'>>'}</div>
<div className={`${styles.reply} ${isRouteLinkToReply && styles.highlight}`} data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid}>
<PostInfo post={post} postReplyCount={postReplyCount} roles={roles} isHidden={hidden} threadNumber={threadNumber} />
<PostInfo post={post} postReplyCount={postReplyCount} roles={roles} isHidden={hidden} threadNumber={threadNumber} quotedByMap={quotedByMap} />
{link && !hidden && !(deleted || removed) && isValidURL(link) && (
<PostMedia
commentMediaInfo={commentMediaInfo}
@@ -598,6 +619,21 @@ const PostDesktop = ({
const isHidden = hidden && !isInPostPageView;
const { replies, hasMore, loadMore } = useReplies({ comment: post, flat: true, accountComments: { newerThan: Infinity } });
const registerComments = usePostNumberStore((s) => s.registerComments);
const numberToCid = usePostNumberStore((s) => s.numberToCid);
const prevCidsRef = useRef<string>('');
useEffect(() => {
const all = post ? [post, ...(replies || [])] : replies || [];
if (!all.length) return;
const cidsKey = all
.map((c) => c?.cid)
.filter(Boolean)
.sort()
.join(',');
if (cidsKey === prevCidsRef.current) return;
prevCidsRef.current = cidsKey;
registerComments(all);
}, [post, replies, registerComments]);
const visiblelinksCount = useCountLinksInReplies(post, 5);
const totalLinksCount = useCountLinksInReplies(post);
const replyCount = replies?.length;
@@ -614,6 +650,37 @@ const PostDesktop = ({
// Filter out deleted replies with no children for both virtuoso and non-virtuoso rendering
const filteredReplies = useMemo(() => (replies || []).filter((reply) => !(reply.deleted && (reply.replyCount === 0 || !reply.replyCount))), [replies]);
// Compute quotedByMap: map each quoted CID to array of replies that quote it
const quotedByMap = useMemo(() => {
const map = new Map<string, Comment[]>();
for (const reply of filteredReplies) {
const cidSet = new Set<string>();
if (reply.quotedCids?.length) {
for (const quotedCid of reply.quotedCids) {
cidSet.add(quotedCid);
}
}
if (reply.content) {
for (const match of reply.content.matchAll(QUOTE_NUMBER_REGEX)) {
const postNumber = parseInt(match[1], 10);
const quotedCid = numberToCid[postNumber];
if (quotedCid) {
cidSet.add(quotedCid);
}
}
}
for (const quotedCid of cidSet) {
const arr = map.get(quotedCid);
if (arr) arr.push(reply);
else map.set(quotedCid, [reply]);
}
}
return map;
}, [filteredReplies, numberToCid]);
// Virtuoso scroll position management for infinite replies
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
const virtuosoStateKey = `replies-desktop-${cid}`;
@@ -739,7 +806,7 @@ const PostDesktop = ({
data={filteredReplies}
itemContent={(index, reply) => (
<div className={styles.replyContainer}>
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} />
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} quotedByMap={quotedByMap} />
</div>
)}
useWindowScroll={true}
@@ -758,7 +825,7 @@ const PostDesktop = ({
!hasMore &&
filteredReplies.map((reply, index) => (
<div key={index} className={styles.replyContainer}>
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} />
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} quotedByMap={quotedByMap} />
</div>
))}
{/* Non-virtualized rendering for board view (last 5 replies or show omitted) */}
@@ -770,7 +837,7 @@ const PostDesktop = ({
showReplies &&
(showOmittedReplies[cid] ? filteredReplies : filteredReplies.slice(-5)).map((reply, index) => (
<div key={index} className={styles.replyContainer}>
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} />
<Reply reply={reply} roles={roles} postReplyCount={replyCount} threadNumber={post?.number} quotedByMap={quotedByMap} />
</div>
))}
</div>
@@ -39,7 +39,7 @@ const CopyLinkButton = ({ cid, subplebbitAddress, linkType, onClose }: CopyLinkB
}
}}
>
<div className={styles.postMenuItem}>{t('copy_link')}</div>
<div className={styles.postMenuItem}>{t('copy_direct_link')}</div>
</div>
);
};
@@ -63,6 +63,25 @@ const CopyContentIdButton = ({ cid, onClose }: { cid: string; onClose: () => voi
);
};
const CopyUserIdButton = ({ address, onClose }: { address: string; onClose: () => void }) => {
const { t } = useTranslation();
return (
<div
onClick={async () => {
try {
await copyToClipboard(address);
} catch (error) {
console.error('Failed to copy user id', error);
} finally {
onClose();
}
}}
>
<div className={styles.postMenuItem}>{t('copy_user_id')}</div>
</div>
);
};
const ImageSearchButton = ({ url, onClose }: { url: string; onClose: () => void }) => {
const { t } = useTranslation();
const [isImageSearchMenuOpen, setIsImageSearchMenuOpen] = useState(false);
@@ -178,6 +197,7 @@ const PostMenuDesktop = ({ postMenu }: PostMenuDesktopProps) => {
<div className={styles.postMenu} ref={refs.setFloating} style={floatingStyles} aria-labelledby={headingId} {...getFloatingProps()}>
{cid && subplebbitAddress && <CopyLinkButton cid={cid} subplebbitAddress={subplebbitAddress} linkType='thread' onClose={handleClose} />}
{cid && <CopyContentIdButton cid={cid} onClose={handleClose} />}
{authorAddress && <CopyUserIdButton address={authorAddress} onClose={handleClose} />}
{!(isInPostPageView && postCid === cid) && (
<div
className={styles.postMenuItem}
@@ -47,7 +47,7 @@ const CopyLinkButton = ({ cid, subplebbitAddress, linkType, onClose }: CopyLinkB
}
}}
>
<div className={styles.postMenuItem}>{t('copy_link')}</div>
<div className={styles.postMenuItem}>{t('copy_direct_link')}</div>
</div>
);
};
@@ -71,6 +71,25 @@ const CopyContentIdButton = ({ cid, onClose }: { cid: string; onClose: () => voi
);
};
const CopyUserIdButton = ({ address, onClose }: { address: string; onClose: () => void }) => {
const { t } = useTranslation();
return (
<div
onClick={async () => {
try {
await copyToClipboard(address);
} catch (error) {
console.error('Failed to copy user id', error);
} finally {
onClose();
}
}}
>
<div className={styles.postMenuItem}>{t('copy_user_id')}</div>
</div>
);
};
const ImageSearchButtons = ({ url, onClose }: { url: string; onClose: () => void }) => {
const { t } = useTranslation();
return (
@@ -180,6 +199,7 @@ const PostMenuMobile = ({ postMenu, editMenuPost }: PostMenuMobileProps) => {
<div className={styles.postMenu} ref={refs.setFloating} style={floatingStyles} aria-labelledby={headingId} {...getFloatingProps()}>
{cid && subplebbitAddress && <CopyLinkButton cid={cid} subplebbitAddress={subplebbitAddress} linkType='thread' onClose={handleClose} />}
{cid && <CopyContentIdButton cid={cid} onClose={handleClose} />}
{authorAddress && <CopyUserIdButton address={authorAddress} onClose={handleClose} />}
{cid && subplebbitAddress && <HidePostButton cid={cid} isReply={!!parentCid} postCid={postCid} onClose={handleClose} />}
{cid && subplebbitAddress && authorAddress && <BlockUserButton address={authorAddress} />}
{cid && subplebbitAddress && !isInBoardView && <BlockBoardButton address={subplebbitAddress} />}
+94 -29
View File
@@ -9,7 +9,9 @@ import { shouldShowSnow } from '../../lib/snow';
import { getHasThumbnail } from '../../lib/utils/media-utils';
import { getTextColorForBackground, hashStringToColor } from '../../lib/utils/post-utils';
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { QUOTE_NUMBER_REGEX } from '../../lib/utils/url-utils';
import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { formatUserIDForDisplay } from '../../lib/utils/string-utils';
import useModQueueStore from '../../stores/use-mod-queue-store';
import { useDirectories } from '../../hooks/use-directories';
import { getBoardPath } from '../../lib/utils/route-utils';
@@ -20,6 +22,7 @@ import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
import useHide from '../../hooks/use-hide';
import useStateString from '../../hooks/use-state-string';
import useScrollToReply from '../../hooks/use-scroll-to-reply';
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
import CommentContent from '../comment-content';
import CommentMedia from '../comment-media';
import LoadingEllipsis from '../loading-ellipsis';
@@ -31,6 +34,7 @@ import _ from 'lodash';
import useReplyModalStore from '../../stores/use-reply-modal-store';
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
import useChallengesStore from '../../stores/use-challenges-store';
import usePostNumberStore from '../../stores/use-post-number-store';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
const { addChallenge } = useChallengesStore.getState();
@@ -169,7 +173,10 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
const handleUserAddressClick = useAuthorAddressClick();
const numberOfPostsByAuthor = document.querySelectorAll(`[data-author-address="${shortAddress}"][data-post-cid="${postCid}"]`).length;
const userID = address && Plebbit.getShortAddress({ address }); // should not be shortened to less than 12 characters, because users can create unlimited addresses/IDs before authenticating or passing challenges, so if the ID is short enough they can spoof it to troll users with the same ID
const pseudonymityMode = useSubplebbitField(subplebbitAddress, (sub) => sub?.features?.pseudonymityMode);
const showUserID = pseudonymityMode !== 'per-reply';
const userID = address && Plebbit.getShortAddress({ address }); // shortened to 8 chars for display; users can verify the full user ID via "Copy user ID" in the post menu to guard against spoofing
const userIDBackgroundColor = hashStringToColor(userID);
const userIDTextColor = getTextColorForBackground(userIDBackgroundColor);
@@ -231,28 +238,32 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
<img src={avatarImageUrl} alt='' />
</span>
) : null}
(ID: {''}
{removed ? (
_.lowerCase(t('removed'))
) : deleted ? (
_.lowerCase(t('deleted'))
) : (
<Tooltip
children={
<span
title={t('highlight_posts')}
className={styles.userAddress}
onClick={() => handleUserAddressClick(userID, postCid)}
style={{ backgroundColor: userIDBackgroundColor, color: userIDTextColor }}
>
{userID}
</span>
}
content={`${numberOfPostsByAuthor === 1 ? t('1_post_by_this_id') : t('x_posts_by_this_id', { number: numberOfPostsByAuthor })}`}
showTooltip={isInPostPageView || postReplyCount < 6}
/>
{showUserID && (
<>
(ID: {''}
{removed ? (
_.lowerCase(t('removed'))
) : deleted ? (
_.lowerCase(t('deleted'))
) : (
<Tooltip
children={
<span
title={t('highlight_posts')}
className={styles.userAddress}
onClick={() => handleUserAddressClick(userID, postCid)}
style={{ backgroundColor: userIDBackgroundColor, color: userIDTextColor }}
>
{formatUserIDForDisplay(userID)}
</span>
}
content={`${numberOfPostsByAuthor === 1 ? t('1_post_by_this_id') : t('x_posts_by_this_id', { number: numberOfPostsByAuthor })}`}
showTooltip={isInPostPageView || postReplyCount < 6}
/>
)}
){' '}
</>
)}
){' '}
{pinned && (
<span className={styles.stickyIconWrapper}>
<img src='assets/icons/sticky.gif' alt='' className={styles.stickyIcon} title={t('sticky')} />
@@ -369,14 +380,14 @@ const PostMediaContent = ({ post, link }: { post: any; link: string }) => {
);
};
const ReplyBacklinks = ({ post }: PostProps) => {
const ReplyBacklinks = ({ post, quotedByMap }: PostProps) => {
const { cid, parentCid } = post || {};
const { replies } = useReplies({ comment: post, flat: true, accountComments: { newerThan: Infinity } });
return (
cid &&
parentCid &&
replies.length > 0 && (
(replies.length > 0 || quotedByMap?.get(cid)?.length) && (
<div className={styles.mobileReplyBacklinks}>
{replies.map(
(reply: Comment, index: number) =>
@@ -384,12 +395,20 @@ const ReplyBacklinks = ({ post }: PostProps) => {
reply?.cid &&
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
)}
{quotedByMap
?.get(cid)
?.map(
(reply: Comment, index: number) =>
reply?.parentCid !== cid &&
reply?.cid &&
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${index}`} isBacklinkReply={true} backlinkReply={reply} />,
)}
</div>
)
);
};
const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => {
const Reply = ({ postReplyCount, reply, roles, threadNumber, quotedByMap }: PostProps) => {
let post = reply;
// handle pending mod or author edit
const { editedComment } = useEditedComment({ comment: reply });
@@ -415,7 +434,7 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => {
>
<PostInfoAndMedia post={post} postReplyCount={postReplyCount} roles={roles} threadNumber={threadNumber} />
{!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && <CommentContent comment={post} />}
<ReplyBacklinks post={reply} />
<ReplyBacklinks post={reply} quotedByMap={quotedByMap} />
</div>
</div>
</div>
@@ -446,6 +465,21 @@ const PostMobile = ({
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : undefined;
const linksCount = useCountLinksInReplies(post);
const { replies, hasMore, loadMore } = useReplies({ comment: post, accountComments: { newerThan: Infinity } });
const registerComments = usePostNumberStore((s) => s.registerComments);
const numberToCid = usePostNumberStore((s) => s.numberToCid);
const prevCidsRef = useRef<string>('');
useEffect(() => {
const all = post ? [post, ...(replies || [])] : replies || [];
if (!all.length) return;
const cidsKey = all
.map((c) => c?.cid)
.filter(Boolean)
.sort()
.join(',');
if (cidsKey === prevCidsRef.current) return;
prevCidsRef.current = cidsKey;
registerComments(all);
}, [post, replies, registerComments]);
const isInPostPageView = isPostPageView(location.pathname, params);
const { hidden, unhide } = useHide({ cid });
@@ -455,6 +489,37 @@ const PostMobile = ({
// Filter out deleted replies with no children for both virtuoso and non-virtuoso rendering
const filteredReplies = useMemo(() => (replies || []).filter((reply) => !(reply.deleted && (reply.replyCount === 0 || !reply.replyCount))), [replies]);
// Compute quotedByMap: map each quoted CID to array of replies that quote it
const quotedByMap = useMemo(() => {
const map = new Map<string, Comment[]>();
for (const reply of filteredReplies) {
const cidSet = new Set<string>();
if (reply.quotedCids?.length) {
for (const quotedCid of reply.quotedCids) {
cidSet.add(quotedCid);
}
}
if (reply.content) {
for (const match of reply.content.matchAll(QUOTE_NUMBER_REGEX)) {
const postNumber = parseInt(match[1], 10);
const quotedCid = numberToCid[postNumber];
if (quotedCid) {
cidSet.add(quotedCid);
}
}
}
for (const quotedCid of cidSet) {
const arr = map.get(quotedCid);
if (arr) arr.push(reply);
else map.set(quotedCid, [reply]);
}
}
return map;
}, [filteredReplies, numberToCid]);
// Virtuoso scroll position management for infinite replies
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
const virtuosoStateKey = `replies-mobile-${cid}`;
@@ -570,7 +635,7 @@ const PostMobile = ({
data={filteredReplies}
itemContent={(index, reply) => (
<div className={styles.replyContainer}>
<Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} />
<Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} quotedByMap={quotedByMap} />
</div>
)}
useWindowScroll={true}
@@ -589,7 +654,7 @@ const PostMobile = ({
!hasMore &&
filteredReplies.map((reply, index) => (
<div key={index} className={styles.replyContainer}>
<Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} />
<Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} quotedByMap={quotedByMap} />
</div>
))}
{/* Non-virtualized rendering for board view (last 5 replies) */}
@@ -600,7 +665,7 @@ const PostMobile = ({
showReplies &&
filteredReplies.slice(-5).map((reply, index) => (
<div key={index} className={styles.replyContainer}>
<Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} />
<Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} quotedByMap={quotedByMap} />
</div>
))}
</div>
+75 -1
View File
@@ -8,6 +8,7 @@ import { getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { isValidURL } from '../../lib/utils/url-utils';
import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
import useSelectedTextStore from '../../stores/use-selected-text-store';
import useReplyModalStore from '../../stores/use-reply-modal-store';
import usePublishReply from '../../hooks/use-publish-reply';
import useIsMobile from '../../hooks/use-is-mobile';
import styles from './reply-modal.module.css';
@@ -43,7 +44,13 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
const [url, setUrl] = useState('');
const textRef = useRef<HTMLTextAreaElement | null>(null);
const urlRef = useRef<HTMLInputElement>(null);
const lastSelectionStartRef = useRef(0);
const lastSelectionEndRef = useRef(0);
const lastProcessedQuoteInsertRequestIdRef = useRef(0);
const { selectedText } = useSelectedTextStore();
const quoteInsertRequestId = useReplyModalStore((state) => state.quoteInsertRequestId);
const quoteInsertNumber = useReplyModalStore((state) => state.quoteInsertNumber);
const quoteInsertSelectedText = useReplyModalStore((state) => state.quoteInsertSelectedText);
const [error, setError] = useState<string | null>(null);
const [lengthError, setLengthError] = useState<string | null>(null);
@@ -186,6 +193,9 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
if (showReplyModal && textRef.current) {
textRef.current.spellcheck = false;
textRef.current.value = contentPrefix + (selectedText || '');
const len = textRef.current.value.length;
lastSelectionStartRef.current = len;
lastSelectionEndRef.current = len;
setTimeout(() => {
if (textRef.current) {
@@ -200,6 +210,8 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
if (!value.startsWith(contentPrefix)) {
e.target.value = contentPrefix + value.slice(contentPrefix.length);
}
lastSelectionStartRef.current = e.target.selectionStart ?? e.target.value.length;
lastSelectionEndRef.current = e.target.selectionEnd ?? lastSelectionStartRef.current;
};
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
@@ -211,6 +223,52 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
}
};
useEffect(() => {
const canInsertQuote = showReplyModal && quoteInsertRequestId !== 0 && !!textRef.current;
const textarea = textRef.current;
if (!canInsertQuote || !textarea) {
return;
}
// Guard: skip if we already processed this exact request id.
// setPublishReplyOptions identity changes after each call (store update -> new content -> new useCallback),
// which re-triggers this effect. Without this guard, that creates an infinite update loop.
if (quoteInsertRequestId === lastProcessedQuoteInsertRequestIdRef.current) {
return;
}
lastProcessedQuoteInsertRequestIdRef.current = quoteInsertRequestId;
const quote = `>>${quoteInsertNumber ?? '?'}`;
const selectedQuote = quoteInsertSelectedText?.trimEnd() || '';
const isFocused = document.activeElement === textarea;
const rawStart = isFocused ? (textarea.selectionStart ?? textarea.value.length) : lastSelectionStartRef.current;
const selectionEnd = isFocused ? (textarea.selectionEnd ?? rawStart) : lastSelectionEndRef.current;
const minStart = contentPrefix.length;
const start = Math.max(rawStart, minStart);
const end = Math.max(selectionEnd, minStart);
const before = textarea.value.slice(0, start);
const after = textarea.value.slice(end);
const needsLeadingNewline = before.length > minStart && !before.endsWith('\n');
let insertion = `${needsLeadingNewline ? '\n' : ''}${quote}\n`;
if (selectedQuote) {
insertion += `${selectedQuote}\n`;
}
const nextValue = `${before}${insertion}${after}`;
textarea.value = nextValue;
const nextCursor = before.length + insertion.length;
textarea.focus();
textarea.setSelectionRange(nextCursor, nextCursor);
lastSelectionStartRef.current = nextCursor;
lastSelectionEndRef.current = nextCursor;
const contentWithoutPrefix = nextValue.slice(contentPrefix.length);
const formattedContent = formatMarkdown(contentWithoutPrefix);
setPublishReplyOptions({ content: formattedContent });
checkContentLength(formattedContent, t);
}, [showReplyModal, quoteInsertRequestId, quoteInsertNumber, quoteInsertSelectedText, contentPrefix, setPublishReplyOptions, checkContentLength, t]);
// on android, auto upload file to image hosting sites with open api
const [isUploading, setIsUploading] = useState(false);
const [uploadedFileName, setUploadedFileName] = useState<string | null>(null);
@@ -294,7 +352,23 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
/>
</div>
<div className={styles.content}>
<textarea cols={48} rows={4} wrap='soft' ref={textRef} spellCheck={true} onInput={handleContentInput} onChange={handleContentChange} />
<textarea
cols={48}
rows={4}
wrap='soft'
ref={textRef}
spellCheck={true}
onInput={handleContentInput}
onChange={handleContentChange}
onSelect={(e) => {
lastSelectionStartRef.current = e.currentTarget.selectionStart ?? e.currentTarget.value.length;
lastSelectionEndRef.current = e.currentTarget.selectionEnd ?? lastSelectionStartRef.current;
}}
onBlur={(e) => {
lastSelectionStartRef.current = e.currentTarget.selectionStart ?? e.currentTarget.value.length;
lastSelectionEndRef.current = e.currentTarget.selectionEnd ?? lastSelectionStartRef.current;
}}
/>
</div>
<div className={styles.footer}>
{url && !isAndroid && (
@@ -14,10 +14,12 @@ interface ReplyQuotePreviewProps {
backlinkReply?: Comment;
isQuotelinkReply?: boolean;
quotelinkReply?: Comment;
isOP?: boolean;
}
const handleQuoteHover = (cid: string, onElementOutOfView: () => void) => {
const targetElements = document.querySelectorAll(`[data-cid="${cid}"]`);
const isOpElement = (element: HTMLElement) => element.getAttribute('data-post-cid') === cid;
const isInViewport = (element: HTMLElement) => {
const bounding = element.getBoundingClientRect();
@@ -34,6 +36,13 @@ const handleQuoteHover = (cid: string, onElementOutOfView: () => void) => {
targetElements.forEach((element) => {
const htmlElement = element as HTMLElement;
if (isInViewport(htmlElement)) {
// Never apply quote-hover highlight styles to OP cards.
if (isOpElement(htmlElement)) {
htmlElement.classList.remove('highlight', 'double-highlight');
anyInView = true;
return;
}
const hasHighlight = Array.from(htmlElement.classList).some((className) => className.includes('highlight') && !className.includes('double-highlight'));
if (hasHighlight) {
htmlElement.classList.remove('highlight');
@@ -54,7 +63,7 @@ const handleQuoteHover = (cid: string, onElementOutOfView: () => void) => {
}
};
const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => {
const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP }: ReplyQuotePreviewProps) => {
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
const placementRef = useRef<Placement>('right');
@@ -104,10 +113,6 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
if (cid && subplebbitAddress) {
const boardPath = getBoardPath(subplebbitAddress, directories);
navigate(`/${boardPath}/thread/${cid}`);
setTimeout(() => {
const element = document.querySelector(`[data-cid="${cid}"]`);
element?.scrollIntoView();
}, 100);
}
};
@@ -173,6 +178,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
onClick={(e) => handleClick(e, quotelinkReply?.cid, quotelinkReply?.subplebbitAddress)}
>
{`>>${quotelinkReply?.number ?? '?'}`}
{isOP && ' (OP)'}
{quotelinkReply?.author?.address === account?.author?.address && ' (You)'}
</Link>
<br />
@@ -190,7 +196,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
};
const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => {
const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP }: ReplyQuotePreviewProps) => {
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
const directories = useDirectories();
@@ -217,10 +223,6 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
if (cid && subplebbitAddress) {
const boardPath = getBoardPath(subplebbitAddress, directories);
navigate(`/${boardPath}/thread/${cid}`);
setTimeout(() => {
const element = document.querySelector(`[data-cid="${cid}"]`);
element?.scrollIntoView();
}, 100);
}
};
@@ -286,6 +288,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
onMouseLeave={() => handleMouseLeave(quotelinkReply?.cid)}
>
{`>>${quotelinkReply?.number ?? '?'}`}
{isOP && ' (OP)'}
{quotelinkReply?.author?.address === account?.author?.address && ' (You)'}
</span>
{quotelinkReply?.number &&
@@ -317,13 +320,19 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
};
const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => {
const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP }: ReplyQuotePreviewProps) => {
const isMobile = useIsMobile();
return isMobile ? (
<MobileQuotePreview backlinkReply={backlinkReply} quotelinkReply={quotelinkReply} isBacklinkReply={isBacklinkReply} isQuotelinkReply={isQuotelinkReply} />
<MobileQuotePreview backlinkReply={backlinkReply} quotelinkReply={quotelinkReply} isBacklinkReply={isBacklinkReply} isQuotelinkReply={isQuotelinkReply} isOP={isOP} />
) : (
<DesktopQuotePreview backlinkReply={backlinkReply} quotelinkReply={quotelinkReply} isBacklinkReply={isBacklinkReply} isQuotelinkReply={isQuotelinkReply} />
<DesktopQuotePreview
backlinkReply={backlinkReply}
quotelinkReply={quotelinkReply}
isBacklinkReply={isBacklinkReply}
isQuotelinkReply={isQuotelinkReply}
isOP={isOP}
/>
);
};
+7 -4
View File
@@ -3,11 +3,12 @@ import { useLocation, useParams } from 'react-router-dom';
import useThemeStore from '../stores/use-theme-store';
import { useDirectories } from './use-directories';
import { isAllView, isHomeView, isNotFoundView, isPendingPostView, isSubscriptionsView, isModView } from '../lib/utils/view-utils';
import { getSubplebbitAddress } from '../lib/utils/route-utils';
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
const useInitialTheme = (pendingPostSubplebbitAddress?: string) => {
const location = useLocation();
const { communityAddress: paramsSubplebbitAddress, accountCommentIndex } = useParams<{ communityAddress: string; accountCommentIndex?: string }>();
const { boardIdentifier, accountCommentIndex } = useParams<{ boardIdentifier: string; accountCommentIndex?: string }>();
const commentIndex = accountCommentIndex ? parseInt(accountCommentIndex) : undefined;
const pendingPost = useAccountComment({ commentIndex });
// Subscribe to the actual themes data, not just functions
@@ -21,13 +22,15 @@ const useInitialTheme = (pendingPostSubplebbitAddress?: string) => {
const isInModView = isModView(location.pathname);
const isInPendingPostView = isPendingPostView(location.pathname, params);
const paramsSubplebbitAddress = boardIdentifier ? getSubplebbitAddress(boardIdentifier, directories) : undefined;
const initialTheme = useMemo(() => {
let theme = 'yotsuba';
if (isInPendingPostView) {
const communityAddress = pendingPostSubplebbitAddress || pendingPost?.communityAddress;
if (communityAddress) {
const community = directories.find((s) => s.address === communityAddress);
const subplebbitAddress = pendingPostSubplebbitAddress || pendingPost?.subplebbitAddress;
if (subplebbitAddress) {
const community = directories.find((s) => s.address === subplebbitAddress);
if (community?.nsfw) {
theme = themes.nsfw || 'yotsuba';
} else {
+2
View File
@@ -12,6 +12,7 @@ import {
isNotFoundView,
ParamsType,
} from './view-utils';
import { formatUserIDForDisplay } from './string-utils';
export {
isAllView,
@@ -25,5 +26,6 @@ export {
isSettingsView,
isSubscriptionsView,
isNotFoundView,
formatUserIDForDisplay,
};
export type { ParamsType };
+3 -3
View File
@@ -106,7 +106,7 @@ export const isFeedRoute = (pathname: string): boolean => {
if (normalizedPath.includes('/thread/')) return false;
if (normalizedPath.startsWith('/pending/')) return false;
if (normalizedPath.includes('/queue')) return false;
if (normalizedPath.includes('/modqueue')) return false;
const pathWithoutSettings = normalizedPath.replace(/\/settings$/, '');
@@ -140,7 +140,7 @@ export const isPendingPostRoute = (pathname: string): boolean => {
export const isModQueueRoute = (pathname: string): boolean => {
const normalizedPath = pathname.replace(/\/settings$/, '');
return normalizedPath.includes('/queue');
return normalizedPath.includes('/modqueue');
};
export const getFeedCacheKey = (pathname: string): string | null => {
@@ -156,7 +156,7 @@ export const getFeedCacheKey = (pathname: string): string | null => {
return null;
}
if (normalizedPath.includes('/queue')) {
if (normalizedPath.includes('/modqueue')) {
return null;
}
+22
View File
@@ -0,0 +1,22 @@
/**
* Format a user ID for display.
* - If ID contains a dot (domain), show full ID but truncate with "..." if exceeds maxLength
* - Otherwise, shorten to 8 characters
* @param userID - The user ID to format
* @param maxDomainLength - Maximum length for domain IDs before truncating with "..."
* @returns Formatted user ID
*/
export function formatUserIDForDisplay(userID: string | undefined, maxDomainLength: number = 40): string {
if (!userID) return '';
// If ID contains a dot, it's a domain - show fully but truncate if too long
if (userID.includes('.')) {
if (userID.length > maxDomainLength) {
return `${userID.slice(0, maxDomainLength - 3)}...`;
}
return userID;
}
// Otherwise, shorten to 8 characters
return userID.slice(0, 8);
}
+10 -1
View File
@@ -1,5 +1,7 @@
import { copyToClipboard } from './clipboard-utils';
export const QUOTE_NUMBER_REGEX = /(?<![>/\w])>>(\d+)/g;
export const getHostname = (url: string) => {
try {
return new URL(url).hostname.replace(/^www\./, '');
@@ -180,14 +182,21 @@ export const isValidCrossboardPattern = (pattern: string): boolean => {
return isValidDomain(pathPart) || isValidIPNSKey(pathPart);
};
// Transform >>{number} post number patterns to markdown links with special anchor
const preprocessPostNumberPatterns = (content: string): string => {
// Match >> followed by digits, avoid overlap with greentext (>>>), cross-board (>>>/), URLs, CID-like patterns
return content.replace(QUOTE_NUMBER_REGEX, (_, num) => `[>>${num}](#q-${num})`);
};
// Preprocess content to convert plain text 5chan cross-board patterns to markdown links
export const preprocess5chanPatterns = (content: string): string => {
const withPostNumbers = preprocessPostNumberPatterns(content);
// Pattern to match ">>>/something" or ">>>/something/cid"
// Negative lookbehind prevents matching patterns that are already part of URLs
// Matches: >>>/directory/, >>>/directory/cid (46 chars), >>>/address, >>>/address/cid (46 chars)
const pattern = /(?<!https?:\/\/[^\s]*)>>>\/([a-zA-Z0-9]{1,10}\/(?:[a-zA-Z0-9]{46})?|[a-zA-Z0-9\-.]+(?:\/[a-zA-Z0-9]{46})?)[.,:;!?]*/g;
return content.replace(pattern, (match, capturedPath) => {
return withPostNumbers.replace(pattern, (match, capturedPath) => {
// Remove any trailing punctuation from the captured path
const cleanPath = capturedPath.replace(/[.,:;!?]+$/, '');
const fullPattern = `>>>/${cleanPath}`;
+1 -1
View File
@@ -60,7 +60,7 @@ export const isModView = (pathname: string): boolean => {
};
export const isModQueueView = (pathname: string): boolean => {
return pathname.includes('/queue');
return pathname.includes('/modqueue');
};
export const isPendingPostView = (pathname: string, params: ParamsType): boolean => {
+34
View File
@@ -0,0 +1,34 @@
import { create } from 'zustand';
import type { Comment } from '@plebbit/plebbit-react-hooks';
interface PostNumberState {
numberToCid: Record<number, string>;
cidToNumber: Record<string, number>;
registerComments: (comments: Comment[]) => void;
}
const usePostNumberStore = create<PostNumberState>((set) => ({
numberToCid: {},
cidToNumber: {},
registerComments: (comments: Comment[]) => {
if (!comments?.length) return;
set((state) => {
const nextNumberToCid = { ...state.numberToCid };
const nextCidToNumber = { ...state.cidToNumber };
for (const c of comments) {
const num = c?.number;
const cid = c?.cid;
if (typeof num === 'number' && cid) {
nextNumberToCid[num] = cid;
nextCidToNumber[cid] = num;
}
}
return { numberToCid: nextNumberToCid, cidToNumber: nextCidToNumber };
});
},
}));
export default usePostNumberStore;
+34 -7
View File
@@ -9,10 +9,28 @@ interface ReplyModalState {
threadCid: string | null;
subplebbitAddress: string | null;
scrollY: number;
quoteInsertRequestId: number;
quoteInsertNumber: number | null;
quoteInsertSelectedText: string | null;
closeModal: () => void;
openReplyModal: (parentCid: string, parentNumber: number | undefined, postCid: string, threadNumber: number | undefined, subplebbitAddress: string) => void;
}
const getQuotedSelection = () => {
const text = document.getSelection()?.toString();
if (!text) return '';
// Keep each selected line as 5chan greentext and normalize newlines.
const normalizedText = text.replace(/\r\n/g, '\n').replace(/\n+$/g, '');
if (!normalizedText) return '';
return normalizedText
.split('\n')
.map((line) => `>${line}`)
.join('\n');
};
const useReplyModalStore = create<ReplyModalState>((set, get) => ({
showReplyModal: false,
activeCid: null,
@@ -21,6 +39,9 @@ const useReplyModalStore = create<ReplyModalState>((set, get) => ({
threadCid: null,
subplebbitAddress: null,
scrollY: 0,
quoteInsertRequestId: 0,
quoteInsertNumber: null,
quoteInsertSelectedText: null,
closeModal: () => {
// Reset selected text if you're using that store
@@ -30,20 +51,26 @@ const useReplyModalStore = create<ReplyModalState>((set, get) => ({
activeCid: null,
parentNumber: null,
threadNumber: null,
quoteInsertNumber: null,
quoteInsertSelectedText: null,
});
},
openReplyModal: (parentCid, parentNumber, postCid, threadNumber, subplebbitAddress) => {
// Don't update if already open with different parent
if (get().activeCid && get().activeCid !== parentCid) {
window.alert('Multiple quotes are not possible on 5chan for the time being, because of a protocol limitation. Please reply to one post at a time.');
const quotedSelection = getQuotedSelection();
// If the reply modal is already open, insert this quote in the current textarea at caret.
if (get().showReplyModal) {
set((state) => ({
quoteInsertRequestId: state.quoteInsertRequestId + 1,
quoteInsertNumber: parentNumber ?? null,
quoteInsertSelectedText: quotedSelection || null,
}));
return;
}
// Get selected text
const text = document.getSelection()?.toString();
if (text) {
useSelectedTextStore.getState().setSelectedText(`>${text}\n`);
if (quotedSelection) {
useSelectedTextStore.getState().setSelectedText(`${quotedSelection}\n`);
}
// Handle mobile scrollY
+5 -5
View File
@@ -547,7 +547,7 @@ const ModQueueButtonContent = ({ feed, alertThresholdSeconds, boardIdentifier, i
}, [statusMap]);
const totalCount = normalCount + urgentCount;
const to = boardIdentifier ? `/${boardIdentifier}/queue` : '/mod/queue';
const to = boardIdentifier ? `/${boardIdentifier}/modqueue` : '/mod/modqueue';
const buttonContent = (
<button className='button'>
@@ -593,7 +593,7 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
(state) => {
const activeAccountId = state.activeAccountId;
const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined;
const accountSubplebbits = activeAccount?.communities || {};
const accountSubplebbits = activeAccount?.subplebbits || {};
return Object.keys(accountSubplebbits);
},
(prev, next) => {
@@ -651,7 +651,7 @@ export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueV
(state) => {
const activeAccountId = state.activeAccountId;
const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined;
const accountSubplebbits = activeAccount?.communities || {};
const accountSubplebbits = activeAccount?.subplebbits || {};
return Object.keys(accountSubplebbits);
},
(prev, next) => {
@@ -678,7 +678,7 @@ export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueV
return [resolvedAddress];
}
// Always require a board filter when viewing /mod/queue (no boardIdentifier)
// Always require a board filter when viewing /mod/modqueue (no boardIdentifier)
if (selectedBoardFilter) {
return [selectedBoardFilter];
}
@@ -708,7 +708,7 @@ export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueV
setResetFunction(reset);
}, [reset, setResetFunction]);
// Auto-select first board if viewing /mod/queue without a boardIdentifier and no filter is set
// Auto-select first board if viewing /mod/modqueue without a boardIdentifier and no filter is set
useEffect(() => {
if (!resolvedAddress && !selectedBoardFilter && accountSubplebbitAddresses.length > 0) {
const { setSelectedBoardFilter } = useModQueueStore.getState();
+1
View File
@@ -31,6 +31,7 @@ export interface PostProps {
isPublishing?: boolean;
onApprove?: () => void;
onReject?: () => void;
quotedByMap?: Map<string, Comment[]>;
}
export const Post = ({
+161 -179
View File
@@ -1227,27 +1227,29 @@
resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz#333fedabc3fd1a8e5d0100013731cf19e6a8c5d3"
integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==
"@electron-forge/cli@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/cli/-/cli-7.6.0.tgz#4c88ac4525a333b20ab6c4083a8b79fbc072782c"
integrity sha512-5G7rBbvTb4HJDiCuhncBzNaRj1e1dEmrk6jExpziqv4Y8p9b+nxfdOjsjWu0hvAl4k2V65Rnm1uEkAA7MmlZOQ==
"@electron-forge/cli@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/cli/-/cli-7.8.0.tgz#871e6f5e491cf7c41b11847f3176369176c2e073"
integrity sha512-XZ+Hg7pxeE9pgrahqcpMlND+VH0l0UTZLyO5wkI+YfanNyBQksB2mw24XeEtCA6x8F2IaEYdIGgijmPF6qpjzA==
dependencies:
"@electron-forge/core" "7.6.0"
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/core" "7.8.0"
"@electron-forge/core-utils" "7.8.0"
"@electron-forge/shared-types" "7.8.0"
"@electron/get" "^3.0.0"
chalk "^4.0.0"
commander "^4.1.1"
commander "^11.1.0"
debug "^4.3.1"
fs-extra "^10.0.0"
listr2 "^7.0.2"
log-symbols "^4.0.0"
semver "^7.2.1"
"@electron-forge/core-utils@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/core-utils/-/core-utils-7.6.0.tgz#cca5fbd64d111fde13eae1440eac475dae9051a6"
integrity sha512-7XVKHPI87p558kVen280yB1UC2cVGHvrMfnPFv4zm3TQHEVaKWKW+5y+UZsKUnGAukNlahHWuHF/1S8dRCJNEg==
"@electron-forge/core-utils@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/core-utils/-/core-utils-7.8.0.tgz#e38682c892ac260d9f60ac11c3f9f7c150e56fde"
integrity sha512-ZioRzqkXVOGuwkfvXN/FPZxcssJ9AkOZx6RvxomQn90F77G2KfEbw4ZwAxVTQ+jWNUzydTic5qavWle++Y5IeA==
dependencies:
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/shared-types" "7.8.0"
"@electron/rebuild" "^3.7.0"
"@malept/cross-spawn-promise" "^2.0.0"
chalk "^4.0.0"
@@ -1256,24 +1258,23 @@
fs-extra "^10.0.0"
log-symbols "^4.0.0"
semver "^7.2.1"
yarn-or-npm "^3.0.1"
"@electron-forge/core@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/core/-/core-7.6.0.tgz#2c662b844ec412c19d58820dfad6e2680ec26bd8"
integrity sha512-DgkjpoK+SPExNTLZL1v81zl0RswQWvMXkMnMqZYf0/S/KHKTXWsoE9KTzr8fDGpiG3nUJXWMqHyny9zLoUdKXQ==
"@electron-forge/core@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/core/-/core-7.8.0.tgz#45991916312c6ee76cb627653c565182a3dd189b"
integrity sha512-7byf660ECZND+irOhGxvpmRXjk1bMrsTWh5J2AZMEvaXI8tub9OrZY9VSbi5fcDt0lpHPKmgVk7NRf/ZjJ+beQ==
dependencies:
"@electron-forge/core-utils" "7.6.0"
"@electron-forge/maker-base" "7.6.0"
"@electron-forge/plugin-base" "7.6.0"
"@electron-forge/publisher-base" "7.6.0"
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/template-base" "7.6.0"
"@electron-forge/template-vite" "7.6.0"
"@electron-forge/template-vite-typescript" "7.6.0"
"@electron-forge/template-webpack" "7.6.0"
"@electron-forge/template-webpack-typescript" "7.6.0"
"@electron-forge/tracer" "7.6.0"
"@electron-forge/core-utils" "7.8.0"
"@electron-forge/maker-base" "7.8.0"
"@electron-forge/plugin-base" "7.8.0"
"@electron-forge/publisher-base" "7.8.0"
"@electron-forge/shared-types" "7.8.0"
"@electron-forge/template-base" "7.8.0"
"@electron-forge/template-vite" "7.8.0"
"@electron-forge/template-vite-typescript" "7.8.0"
"@electron-forge/template-webpack" "7.8.0"
"@electron-forge/template-webpack-typescript" "7.8.0"
"@electron-forge/tracer" "7.8.0"
"@electron/get" "^3.0.0"
"@electron/packager" "^18.3.5"
"@electron/rebuild" "^3.7.0"
@@ -1284,27 +1285,25 @@
filenamify "^4.1.0"
find-up "^5.0.0"
fs-extra "^10.0.0"
global-dirs "^3.0.0"
got "^11.8.5"
interpret "^3.1.1"
listr2 "^7.0.2"
lodash "^4.17.20"
log-symbols "^4.0.0"
node-fetch "^2.6.7"
progress "^2.0.3"
rechoir "^0.8.0"
resolve-package "^1.0.1"
semver "^7.2.1"
source-map-support "^0.5.13"
sudo-prompt "^9.1.1"
username "^5.1.0"
yarn-or-npm "^3.0.1"
"@electron-forge/maker-base@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/maker-base/-/maker-base-7.6.0.tgz#b95564390a444b44d037b53e3b796e083e935d2f"
integrity sha512-GrVYhiA/g0NXrI13LcXrT+JKLlq8kkYyO6w0jQORqDFeRSLRoLhrru5w0msg0wINGugBe+/NwyAyFZ2KaQ6o4g==
"@electron-forge/maker-base@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/maker-base/-/maker-base-7.8.0.tgz#4790a4cd477bd5952aba783e9ff433d15a56217c"
integrity sha512-yGRvz70w+NnKO7PhzNFRgYM+x6kxYFgpbChJIQBs3WChd9bGjL+MZLrwYqmxOFLpWNwRAJ6PEi4E/8U5GgV6AQ==
dependencies:
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/shared-types" "7.8.0"
fs-extra "^10.0.0"
which "^2.0.2"
@@ -1317,60 +1316,60 @@
fs-extra "^10.0.0"
which "^2.0.2"
"@electron-forge/maker-dmg@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/maker-dmg/-/maker-dmg-7.6.0.tgz#facd0f5a4aef75d036a7709f841da9150a13c80b"
integrity sha512-Wa4XG9r4RldF5uy7vef5/BR8o/P+BmltZIbZulnFwhOYEu4Quj+SDF1bet7y3tM2i1am/sEocFcBHXC7CRk8xg==
"@electron-forge/maker-dmg@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/maker-dmg/-/maker-dmg-7.8.0.tgz#1286ebffd5c7a8f4ac91d042b0578c8b6ccb6750"
integrity sha512-ml6GpHvUyhOapIF1ALEM4zCqXiAf2+t+3FqKnjNtiVbH5fnV2CW//SWWozrvAGTrYGi/6V4s9TL/rIek0BHOPA==
dependencies:
"@electron-forge/maker-base" "7.6.0"
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/maker-base" "7.8.0"
"@electron-forge/shared-types" "7.8.0"
fs-extra "^10.0.0"
optionalDependencies:
electron-installer-dmg "^5.0.1"
"@electron-forge/maker-squirrel@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/maker-squirrel/-/maker-squirrel-7.6.0.tgz#98475a3c400db6dcdaf876f25bc4ef893de8ab41"
integrity sha512-8tqsJBRAe37YZSKv1fPc1tijQljkSlUQCaeun37ZOM/viurSeydt5nu2M+UDmJHAfD/PRZMjnYvCCWH+08wGVg==
"@electron-forge/maker-squirrel@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/maker-squirrel/-/maker-squirrel-7.8.0.tgz#56cccbaed4d47b3a405b22c6fa760f57455b7632"
integrity sha512-On8WIyjNtNlWf8NJRRVToighGCCU+wcxytFM0F8Zx/pLszgc01bt7wIarOiAIzuIT9Z8vshAYA0iG1U099jfeA==
dependencies:
"@electron-forge/maker-base" "7.6.0"
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/maker-base" "7.8.0"
"@electron-forge/shared-types" "7.8.0"
fs-extra "^10.0.0"
optionalDependencies:
electron-winstaller "^5.3.0"
"@electron-forge/maker-zip@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/maker-zip/-/maker-zip-7.6.0.tgz#4d590b1d2ea3553374e2a634ab1e517cd4279d14"
integrity sha512-sDPQoEs6CnkxsydvnfZByBGf+RREky2xqiusWCvaPnUoLRpq96SFaBb1BRCS6tQKQHKkaEUXEC5pBdrYGLHPVg==
"@electron-forge/maker-zip@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/maker-zip/-/maker-zip-7.8.0.tgz#8d594f8a4cb3e66f0775e766e2f3c30eadf035f8"
integrity sha512-7MLD7GkZdlGecC9GvgBu0sWYt48p3smYvr+YCwlpdH1CTeLmWhvCqeH33a2AB0XI5CY8U8jnkG2jgdTkzr/EQw==
dependencies:
"@electron-forge/maker-base" "7.6.0"
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/maker-base" "7.8.0"
"@electron-forge/shared-types" "7.8.0"
cross-zip "^4.0.0"
fs-extra "^10.0.0"
got "^11.8.5"
"@electron-forge/plugin-auto-unpack-natives@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/plugin-auto-unpack-natives/-/plugin-auto-unpack-natives-7.6.0.tgz#222ea2d265a00f96a3a65f37f547c90e28c0d740"
integrity sha512-rSWRLJinRIxtlkLke0uJzOLksRnXszu3hZrzlgOWChDuMFM298yb6gxWAjYh94VoNxXrUHl9Cd4ia/5+wgPwwg==
"@electron-forge/plugin-auto-unpack-natives@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/plugin-auto-unpack-natives/-/plugin-auto-unpack-natives-7.8.0.tgz#5167410e0e74d3bd8949c6acbe79249b7cffd1ce"
integrity sha512-JGal5ltZmbTQ5rNq67OgGC4MJ2zjjFW0fqykHy8X9J8cgaH7SRdKkT4yYZ8jH01IAF1J57FD2zIob1MvcBqjcg==
dependencies:
"@electron-forge/plugin-base" "7.6.0"
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/plugin-base" "7.8.0"
"@electron-forge/shared-types" "7.8.0"
"@electron-forge/plugin-base@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/plugin-base/-/plugin-base-7.6.0.tgz#3925651b6e33a989209f7272fc54724f1364dd43"
integrity sha512-9llu4algWZJAJFVVZtd/Xa71c0QVxRmoMrpHX2SB+XJ+ZlFVdXrlnhn2hc/CnM0by9cBElyAL3cx3533OKS7lA==
"@electron-forge/plugin-base@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/plugin-base/-/plugin-base-7.8.0.tgz#bb617781c28b99d2b65d6f2e66679b245d388e78"
integrity sha512-rDeeChRWIp5rQVo3Uc1q0ncUvA+kWWURW7tMuQjPvy2qVSgX+jIf5krk+T1Dp06+D4YZzEIrkibRaamAaIcR1w==
dependencies:
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/shared-types" "7.8.0"
"@electron-forge/publisher-base@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/publisher-base/-/publisher-base-7.6.0.tgz#869748f2988994371e4ef842077970dcec9f5367"
integrity sha512-IL9bbIb/4J4I1bfW53RAmE/Al835XJsOwFXTLUnxnaGtbWg5jz7eiyw9Vl8XvvfHN1Dpoa9f94to8keU2MXgDg==
"@electron-forge/publisher-base@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/publisher-base/-/publisher-base-7.8.0.tgz#5b86f7dec28aecf19448a54b7f65371e18bcc9c7"
integrity sha512-wrZyptJ0Uqvlh2wYzDZfIu2HgCQ+kdGiBlcucmLY4W+GUqf043O8cbYso3D9NXQxOow55QC/1saCQkgLphprPA==
dependencies:
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/shared-types" "7.8.0"
"@electron-forge/shared-types@7.11.1":
version "7.11.1"
@@ -1382,61 +1381,62 @@
"@electron/rebuild" "^3.7.0"
listr2 "^7.0.2"
"@electron-forge/shared-types@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/shared-types/-/shared-types-7.6.0.tgz#5e073e2a9949e2af1b9119a75a2a0a7fec690efc"
integrity sha512-qpJRaPo/tx/+t3iFdUWnK4Tk/elo+Izk3yS+BhzfaF0XOK8wS+NNYW4vycK6eVMxN3Yu7/924MQFtPlCKlWHvA==
"@electron-forge/shared-types@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/shared-types/-/shared-types-7.8.0.tgz#dbc9b20ecd547d0758d7abc90587983c7f2fd3e8"
integrity sha512-Ul+7HPvAZiAirqpZm0vc9YvlkAE+2bcrI10p3t50mEtuxn5VO/mB72NXiEKfWzHm8F31JySIe9bUV6s1MHQcCw==
dependencies:
"@electron-forge/tracer" "7.6.0"
"@electron-forge/tracer" "7.8.0"
"@electron/packager" "^18.3.5"
"@electron/rebuild" "^3.7.0"
listr2 "^7.0.2"
"@electron-forge/template-base@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-base/-/template-base-7.6.0.tgz#b057590b4e8a1d8bce460ed8520973f95d3699dd"
integrity sha512-lhvab8a/knuGnpzep8BMOEkgnkHGr11QELGBzslEnA6rwZi9DDyEgmMCk6VWOVQNHMeuEqh5XlgjVqJmjW6nIQ==
"@electron-forge/template-base@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-base/-/template-base-7.8.0.tgz#9721d6c42120db094c83436404a458a2cb6e5412"
integrity sha512-hc8NwoDqEEmZFH/p0p3MK/7xygMmI+cm8Gavoj2Mr2xS7VUUu4r3b5PwIGKvkLfPG34uwsiVwtid2t1rWGF4UA==
dependencies:
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/core-utils" "7.8.0"
"@electron-forge/shared-types" "7.8.0"
"@malept/cross-spawn-promise" "^2.0.0"
debug "^4.3.1"
fs-extra "^10.0.0"
username "^5.1.0"
"@electron-forge/template-vite-typescript@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.6.0.tgz#f00ccb5cba2de02ce2ebde8f65058d708a2ef79f"
integrity sha512-i2Bt5Hehoq2CNNrUQjl8DQX7VatBMQ6mv+CCa+m+EV92nUYxXsoFva62/5ITpc3gFAGd1upw/S7dTbHV6GOwsA==
"@electron-forge/template-vite-typescript@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.8.0.tgz#106558f6e3af263629e730fd2a60e9eaffefb7cf"
integrity sha512-kW3CaVxKHUYuVfY+rT3iepeZ69frBRGh3YZOngLY2buCvGIqNEx+VCgrFBRDDbOKGmwQtwO1E9wp2rtC8q6Ztg==
dependencies:
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/template-base" "7.6.0"
"@electron-forge/shared-types" "7.8.0"
"@electron-forge/template-base" "7.8.0"
fs-extra "^10.0.0"
"@electron-forge/template-vite@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-vite/-/template-vite-7.6.0.tgz#a1bed7d52c93883efb0108ceb9d41f76e1a0daf3"
integrity sha512-C0V0dGDO1hLXnAM9lGnZU0esNOTbxwcgILWJXv0mYErBkmputAIi3HM1Is3h3JdSijXgVbRWcIQxFxJlOCpB/A==
"@electron-forge/template-vite@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-vite/-/template-vite-7.8.0.tgz#8906aa82cb4c0efc3501da0141a9dd382715dd86"
integrity sha512-bf/jd8WzD0gU7Jet+WSi0Lm0SQmseb08WY27ZfJYEs2EVNMiwDfPicgQnOaqP++2yTrXhj1OY/rolZCP9CUyVw==
dependencies:
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/template-base" "7.6.0"
"@electron-forge/shared-types" "7.8.0"
"@electron-forge/template-base" "7.8.0"
fs-extra "^10.0.0"
"@electron-forge/template-webpack-typescript@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.6.0.tgz#2a38e50181d94fb1257350180eaac3f47d115280"
integrity sha512-fDj4DkGxJJjGL8lpowFnkX7PvV9koLHKJuyusK8p8ayVMGoHpHrIcVCrV06tKYOvhFrL/ahW+CKKvjlxF8niEg==
"@electron-forge/template-webpack-typescript@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.8.0.tgz#26731829d8fa18196ae65bf3db46fb6659b1eac7"
integrity sha512-Pl8l+gv3HzqCfFIMLxlEsoAkNd0VEWeZZ675SYyqs0/kBQUifn0bKNhVE4gUZwKGgQCcG1Gvb23KdVGD3H3XmA==
dependencies:
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/template-base" "7.6.0"
"@electron-forge/shared-types" "7.8.0"
"@electron-forge/template-base" "7.8.0"
fs-extra "^10.0.0"
"@electron-forge/template-webpack@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-webpack/-/template-webpack-7.6.0.tgz#b8f6f108dd0fedb9b459db5bb7180cb839656775"
integrity sha512-+HEf0ryUfLpHvl27TXSdP2Ob69+ktNtr5EnmroZGGIxhSAtEs4HloPtDF9PSfBzm38pZhQBZn78kY9LbITTGjg==
"@electron-forge/template-webpack@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-webpack/-/template-webpack-7.8.0.tgz#b85b2827193142c6c836ceec8a1776471fdd28c4"
integrity sha512-AdLGC6NVgrd7Q0SaaeiwJKmSBjN6C2EHxZgLMy1yxNSpazU9m3DtYQilDjXqmCWfxkeNzdke0NaeDvLgdJSw5A==
dependencies:
"@electron-forge/shared-types" "7.6.0"
"@electron-forge/template-base" "7.6.0"
"@electron-forge/shared-types" "7.8.0"
"@electron-forge/template-base" "7.8.0"
fs-extra "^10.0.0"
"@electron-forge/tracer@7.11.1":
@@ -1446,10 +1446,10 @@
dependencies:
chrome-trace-event "^1.0.3"
"@electron-forge/tracer@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@electron-forge/tracer/-/tracer-7.6.0.tgz#f31fc5bbe75dec70bb79add4d4c72adb18f742b3"
integrity sha512-Rn76RHqNhLyZDnu+xY/X73+bv+Q09XKaZBL/WvlYBbvrrHe26NOHJ3IHXxkWRokSWd4B7lOGLGKm3j1Il8dVbQ==
"@electron-forge/tracer@7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@electron-forge/tracer/-/tracer-7.8.0.tgz#19a9a8164efd2b7a9fa144d424d5e714f115cf77"
integrity sha512-t4fIATZEX6/7PJNfyh6tLzKEsNMpO01Nz/rgHWBxeRvjCw5UNul9OOxoM7b43vfFAO9Jv++34oI3VJ09LeVQ2Q==
dependencies:
chrome-trace-event "^1.0.3"
@@ -2911,13 +2911,6 @@
wrap-ansi "^8.1.0"
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
"@isaacs/fs-minipass@^4.0.0":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32"
integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==
dependencies:
minipass "^7.0.4"
"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5":
version "0.3.13"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
@@ -6457,10 +6450,10 @@ chownr@^1.1.1:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chownr@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4"
integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
chrome-trace-event@^1.0.3:
version "1.0.4"
@@ -6620,6 +6613,11 @@ comma-separated-tokens@^2.0.0:
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
commander@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906"
integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==
commander@^12.0.0, commander@^12.1.0:
version "12.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
@@ -6635,11 +6633,6 @@ commander@^2.20.0, commander@^2.20.3, commander@^2.8.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
commander@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
@@ -6986,7 +6979,7 @@ cross-fetch@3.1.6:
dependencies:
node-fetch "^2.6.11"
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
cross-spawn@^6.0.0:
version "6.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57"
integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==
@@ -8292,7 +8285,7 @@ find-up@^3.0.0:
dependencies:
locate-path "^3.0.0"
find-up@^4.0.0, find-up@^4.1.0:
find-up@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
@@ -8460,6 +8453,13 @@ fs-extra@^8.1.0:
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
dependencies:
minipass "^3.0.0"
fs-minipass@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.3.tgz#79a85981c4dc120065e96f62086bf6f9dc26cc54"
@@ -8583,13 +8583,6 @@ get-caller-file@^2.0.5:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-installed-path@^2.0.3:
version "2.1.1"
resolved "https://registry.yarnpkg.com/get-installed-path/-/get-installed-path-2.1.1.tgz#a1f33dc6b8af542c9331084e8edbe37fe2634152"
integrity sha512-Qkn9eq6tW5/q9BDVdMpB8tOHljX9OSP0jRC5TRNVA4qRc839t4g8KQaR8t0Uv0EFVL0MlyG7m/ofjEgAROtYsA==
dependencies:
global-modules "1.0.0"
get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
@@ -8747,7 +8740,14 @@ global-directory@^4.0.1:
dependencies:
ini "4.1.1"
global-modules@1.0.0, global-modules@^1.0.0:
global-dirs@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485"
integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==
dependencies:
ini "2.0.0"
global-modules@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==
@@ -9321,6 +9321,11 @@ inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
ini@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1"
@@ -11748,12 +11753,17 @@ minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3, minipass@^3.
dependencies:
yallist "^4.0.0"
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.3, minipass@^7.0.4, minipass@^7.1.2:
minipass@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.3, minipass@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
minizlib@^2.0.0, minizlib@^2.1.2:
minizlib@^2.0.0, minizlib@^2.1.1, minizlib@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
@@ -11761,13 +11771,6 @@ minizlib@^2.0.0, minizlib@^2.1.2:
minipass "^3.0.0"
yallist "^4.0.0"
minizlib@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.1.0.tgz#6ad76c3a8f10227c9b51d1c9ac8e30b27f5a251c"
integrity sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==
dependencies:
minipass "^7.1.2"
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
@@ -11780,6 +11783,11 @@ mkdirp@^0.5.1:
dependencies:
minimist "^1.2.6"
mkdirp@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
modify-filename@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/modify-filename/-/modify-filename-1.1.0.tgz#9a2dec83806fbb2d975f22beec859ca26b393aa1"
@@ -12812,13 +12820,6 @@ pkg-conf@^3.1.0:
find-up "^3.0.0"
load-json-file "^5.2.0"
pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
dependencies:
find-up "^4.0.0"
pkg-dir@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760"
@@ -13588,13 +13589,6 @@ resolve-from@^5.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
resolve-package@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/resolve-package/-/resolve-package-1.0.1.tgz#686f70b188bd7d675f5bbc4282ccda060abb9d27"
integrity sha512-rzB7NnQpOkPHBWFPP3prUMqOP6yg3HkRGgcvR+lDyvyHoY3fZLFLYDkPXh78SPVBAE6VTCk/V+j8we4djg6o4g==
dependencies:
get-installed-path "^2.0.3"
resolve-pkg-maps@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f"
@@ -14736,16 +14730,17 @@ tar-stream@^2.1.4:
inherits "^2.0.3"
readable-stream "^3.1.1"
tar@7.5.4, tar@^6.0.5, tar@^6.1.11, tar@^6.1.2, tar@^6.2.1, tar@^7.4.3:
version "7.5.4"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.4.tgz#18b53b44f939a7e03ed874f1fafe17d29e306c81"
integrity sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==
tar@6.2.1, tar@^6.0.5, tar@^6.1.11, tar@^6.1.2, tar@^6.2.1, tar@^7.4.3:
version "6.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
dependencies:
"@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0"
minipass "^7.1.2"
minizlib "^3.1.0"
yallist "^5.0.0"
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^5.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
tcp-port-used@1.0.2:
version "1.0.2"
@@ -16179,11 +16174,6 @@ yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yallist@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533"
integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==
yaml@^1.10.0, yaml@^1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
@@ -16212,14 +16202,6 @@ yargs@^17.0.1, yargs@^17.7.1:
y18n "^5.0.5"
yargs-parser "^21.1.1"
yarn-or-npm@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/yarn-or-npm/-/yarn-or-npm-3.0.1.tgz#6336eea4dff7e23e226acc98c1a8ada17a1b8666"
integrity sha512-fTiQP6WbDAh5QZAVdbMQkecZoahnbOjClTQhzv74WX5h2Uaidj1isf9FDes11TKtsZ0/ZVfZsqZ+O3x6aLERHQ==
dependencies:
cross-spawn "^6.0.5"
pkg-dir "^4.2.0"
yauzl@^2.10.0, yauzl@^2.4.2:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"