Merge pull request #868 from bitsocialhq/master

Development
This commit is contained in:
Tom
2026-01-26 14:55:40 +08:00
committed by GitHub
55 changed files with 996 additions and 304 deletions
-93
View File
@@ -1,93 +0,0 @@
# send notifications to telegram group on commits, releases, issues
name: notifications
on:
pull_request:
branches:
- master
# push:
# branches:
# - master
release:
types: [published]
issues:
types: [opened]
jobs:
# push:
# if: ${{ github.event_name == 'push' }}
# runs-on: ubuntu-latest
# strategy:
# matrix:
# commit: ${{ github.event.commits }}
# steps:
# # - name: debug
# # env:
# # DEBUG: ${{ toJSON(matrix.commit) }}
# # run: echo "$DEBUG"
# # write message to file
# - run: echo "<code>${{ matrix.commit.message }}</code>" >> message.txt
# - run: echo "" >> message.txt
# - run: echo "by <i>${{ matrix.commit.author.username }}</i>" >> message.txt
# - run: echo "${{ matrix.commit.url }}" >> message.txt
# - run: date -d "${{ matrix.commit.timestamp }}" +"%d/%m/%y at %H:%M" >> message.txt
# # send message, @plebbit telegram chat id is -1001665335693
# - name: "telegram notification"
# uses: appleboy/telegram-action@master
# with:
# to: -1001665335693
# token: ${{ secrets.TELEGRAM_TOKEN }}
# # commit links don't show anything useful in preview
# disable_web_page_preview: true
# format: html
# message_file: message.txt
release:
if: ${{ github.event_name == 'release' }}
runs-on: ubuntu-latest
steps:
# - name: debug
# env:
# DEBUG: ${{ toJSON(github) }}
# run: echo "$DEBUG"
# write message to file
- run: echo "<b>${{ github.event.repository.name }} ${{ github.event.release.name }}</b>" >> message.txt
- run: echo "" >> message.txt
- run: echo "${{ github.event.release.body }}" >> message.txt
- run: echo "${{ github.event.release.html_url }}" >> message.txt
# send message, @plebbit telegram chat id is -1001665335693
- name: "telegram notification"
uses: appleboy/telegram-action@master
with:
to: -1001665335693
token: ${{ secrets.TELEGRAM_TOKEN }}
format: html
message_file: message.txt
issue:
if: ${{ github.event_name == 'issues' }}
runs-on: ubuntu-latest
steps:
# - name: debug
# env:
# DEBUG: ${{ toJSON(github) }}
# run: echo "$DEBUG"
# write message to file
- run: echo "<code>${{ github.event.issue.title }}</code>" >> message.txt
- run: echo "" >> message.txt
- run: echo "by <i>${{ github.event.issue.user.login }}</i>" >> message.txt
- run: echo "${{ github.event.issue.html_url }}" >> message.txt
# send message, @plebbit telegram chat id is -1001665335693
- name: "telegram notification"
uses: appleboy/telegram-action@master
with:
to: -1001665335693
token: ${{ secrets.TELEGRAM_TOKEN }}
format: html
message_file: message.txt
+2 -3
View File
@@ -53,7 +53,7 @@ jobs:
allowUpdates: true
mac:
runs-on: macOS-13
runs-on: macos-15
permissions:
contents: write
steps:
@@ -164,8 +164,7 @@ jobs:
- run: sed -i "s/versionName \"1.0\"/versionName \"$(node -e "console.log(require('./package.json').version)")\"/" ./android/app/build.gradle
- run: cat ./android/app/build.gradle
# build apk
- run: npx cap update
- run: npx cap copy
- run: npx cap sync android
- run: cd android && gradle bundle
- run: cd android && ./gradlew assembleRelease
# optimize apk
+1 -1
View File
@@ -146,7 +146,7 @@ jobs:
test-mac-arm:
name: Test Mac (Apple Silicon)
runs-on: macOS-14
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
+43
View File
@@ -105,6 +105,49 @@ Each MCP server injects its tool definitions into the context window, consuming
If you notice many MCP tools in your context, or if the user reports degraded responses, warn them that they may have too many MCP servers enabled and suggest disabling unused ones to free up context space.
## Translations
This project uses i18next with translation files in `public/translations/{lang}/default.json`.
### Adding/Updating Translations
Use `scripts/update-translations.js` to update translations across all languages. **Do not manually edit each language file.**
**Workflow:**
1. Create a temporary dictionary file (e.g., `translations-temp.json`) with translations for each language:
```json
{
"en": "English text",
"es": "Spanish text",
"fr": "French text",
"de": "German text",
...
}
```
2. Run the script with the `--map` flag:
```bash
node scripts/update-translations.js --key my_new_key --map translations-temp.json --include-en --write
```
3. Delete the temporary dictionary file after the script completes.
**Other useful commands:**
```bash
# Copy a key's value from English to all languages (dry run first)
node scripts/update-translations.js --key some_key --from en --dry
node scripts/update-translations.js --key some_key --from en --write
# Delete a key from all languages
node scripts/update-translations.js --key obsolete_key --delete --write
# Audit for unused translation keys
node scripts/update-translations.js --audit --dry
node scripts/update-translations.js --audit --write
```
## Boundaries
- Never commit secrets or API keys
+6 -1
View File
@@ -1,10 +1,11 @@
# [0.6.0](https://github.com/plebbit/plebchan/compare/v0.5.3...v0.6.0) (2026-01-22)
## [0.6.1](https://github.com/plebbit/plebchan/compare/v0.6.0...v0.6.1) (2026-01-25)
### Bug Fixes
* align asset-manifest generator output with prettier config ([4fd9a9c](https://github.com/plebbit/plebchan/commit/4fd9a9c1a98838dd9bbd05240905564fb6b6c85e))
* **android:** capacitor config was pointing to build instead of dist (legacy from CRA) ([436988e](https://github.com/plebbit/plebchan/commit/436988ee26f83c3c32c82a1a436006facec302de))
* **android:** release build fails due to tar v7 incompatibility with Capacitor CLI ([eda8b5e](https://github.com/plebbit/plebchan/commit/eda8b5ed86d5376c3fac92ca90901d8c83d1edf2))
* **board buttons:** guard against undefined address ([2a521ad](https://github.com/plebbit/plebchan/commit/2a521ad8f82160b83f2d400c5a75f41bcb61f6d1))
* **board header:** limit clickable area of subscriptions counter to text only ([4f49ec6](https://github.com/plebbit/plebchan/commit/4f49ec666626b7711d9166fdb9933b83b2bec186))
* **boards-list:** hide Multiboards section when filtering for worksafe boards ([594be88](https://github.com/plebbit/plebchan/commit/594be882101cf849e172b90c0944b98a3629bad2))
@@ -17,6 +18,7 @@
* **ci:** macOS test builds fail with hdiutil "Resource busy" error ([46f0f49](https://github.com/plebbit/plebchan/commit/46f0f494e99a5fe113f0e27f46e97dcc0be6a2eb))
* **ci:** remove redundant electron-rebuild from build scripts ([96a3068](https://github.com/plebbit/plebchan/commit/96a306892868993f000af9c1f8928acbd928e3b3))
* **ci:** update deprecated intel mac runner ([2d23f44](https://github.com/plebbit/plebchan/commit/2d23f44bd80df5db75677b829c3484878d7f8d7c))
* **ci:** update macos runner image ([5f85a0a](https://github.com/plebbit/plebchan/commit/5f85a0ac1548b18566425ec76569647fe8ae4805))
* **comment-content:** ensure parent comment number is populated for nested replies ([c2bbe2d](https://github.com/plebbit/plebchan/commit/c2bbe2dd0b95d7e4b1fb1e3b988df0039d5e1684))
* **comment-content:** use correct banExpiresAt path for ban visibility ([476681c](https://github.com/plebbit/plebchan/commit/476681c689edba6fe0dbc052c9908649188cca60))
* correct horizontal centering of create board modal ([e863ddb](https://github.com/plebbit/plebchan/commit/e863ddb8b901191e7b5bb4f9527b0af994895a42))
@@ -35,6 +37,7 @@
* **hooks:** fix usePopularPosts loading state and change detection ([a7614db](https://github.com/plebbit/plebchan/commit/a7614db743c3eff052b48a1401f427b38dbd03d2))
* **hooks:** remove postCid requirement from isAccountCommentAuthor check ([6175744](https://github.com/plebbit/plebchan/commit/6175744e7b4f9f56e562265751ac3bf5bcad6f2b))
* **hooks:** use structural equality for roles comparison in useStableSubplebbit ([647139c](https://github.com/plebbit/plebchan/commit/647139c0ec4a23f21fb4d7830285cc5123f9202a))
* HTML zip archive is empty in releases ([2f0fae8](https://github.com/plebbit/plebchan/commit/2f0fae846a49d8d5e949bf58e33381e2f08ebf81))
* limit vite-plugin-eslint to src/ files only ([f65635b](https://github.com/plebbit/plebchan/commit/f65635b549a7c73f31b52c7f5656fc20b6f02a1c))
* **lint:** use correct oxlint rule names and enable react plugin in config ([9fd41f2](https://github.com/plebbit/plebchan/commit/9fd41f2f9f8099fd23d2341371d6168b204c97fe))
* **markdown:** embedded media in comment.content shouldn't be expandable and should be already expanded ([fcd8838](https://github.com/plebbit/plebchan/commit/fcd8838045a8ba66852b2f69904f939d3a8c6153))
@@ -50,6 +53,7 @@
* **mod-queue:** alert animation and counter only for pending items ([a23f5bc](https://github.com/plebbit/plebchan/commit/a23f5bc2388f3ceae6d9bdbe9b7f24247c094d4b))
* **mod-queue:** button counter includes already approved/rejected items ([02fc6a3](https://github.com/plebbit/plebchan/commit/02fc6a3171517e06012138ad9f3ac6aa620d69dc))
* **mod-queue:** close CSS breakpoint gap and remove duplicate time-ago rendering ([ab98bd0](https://github.com/plebbit/plebchan/commit/ab98bd0a87f92af5ba70b88a360fd1132e0668bb))
* **mod-queue:** improve alert threshold input validation and add i18n for view selector ([d866372](https://github.com/plebbit/plebchan/commit/d86637200bc74f41e095023bfe58a5fb4e58ec35))
* **mod-queue:** style action buttons to match board buttons and make status indicators bold/colored ([fbdfadc](https://github.com/plebbit/plebchan/commit/fbdfadc65cd0ad24eff28552570ef91fa89fcddf))
* **mod-queue:** use link as excerpt when post has no title/content and prevent empty strings ([410fd54](https://github.com/plebbit/plebchan/commit/410fd541b33655d2668f85c25d52a1392703e3d5))
* **p2p-options:** correct Solana chainId from 1 to 101 ([52fea38](https://github.com/plebbit/plebchan/commit/52fea3851e0fe3bccadd8cae8b02e2204c45e397))
@@ -123,6 +127,7 @@
* implement post numbers ([7684541](https://github.com/plebbit/plebchan/commit/76845412e8b34c11fb70bf993a0aeaeda54a3ee4))
* **mod-queue:** add minutes support to alert threshold ([ab353b5](https://github.com/plebbit/plebchan/commit/ab353b59f62e94edd76e97e5c18cab1f3cfb2115))
* **mod-queue:** add number column, improve time display, and use edited comments ([a33b2e5](https://github.com/plebbit/plebchan/commit/a33b2e5a9aa04d3ae5daf418b51b13ce6d23d579))
* **mod-queue:** add view mode selection and integrate with post components ([14e8c92](https://github.com/plebbit/plebchan/commit/14e8c92fa0fd2cc6eb9d5bad7646020207389373))
* **modals:** use icon-close-red.png for close buttons on home view ([21d9c0c](https://github.com/plebbit/plebchan/commit/21d9c0c6163dcd3947d26bcf3bdc5e5d0aae52dc))
* **moderation:** add purge action to edit menu ([1abc841](https://github.com/plebbit/plebchan/commit/1abc8410cd09e8d1b79df6809a9fea0241a3ae4c))
* **p2p options:** add http routers field ([3d67606](https://github.com/plebbit/plebchan/commit/3d676062af3ed54942a5f92b0958ce3501616c8b))
+6 -3
View File
@@ -23,10 +23,13 @@ function createHtmlArchive() {
const zipBinPath = path.resolve(rootPath, 'node_modules', '7zip-bin', 'linux', 'x64', '7za');
const fivechanHtmlFolderName = `5chan-html-${packageJson.version}`;
const outputFile = path.resolve(distFolderPath, `${fivechanHtmlFolderName}.zip`);
const inputFolder = path.resolve(rootPath, 'build');
// Vite outputs to 'dist', not 'build' (CRA default)
const inputFolder = path.resolve(rootPath, 'dist');
try {
execSync(`${zipBinPath} a ${outputFile} ${inputFolder}`);
execSync(`${zipBinPath} rn -r ${outputFile} build ${fivechanHtmlFolderName}`);
// Exclude Electron builder artifacts from HTML archive
const excludes = '-xr!*.AppImage -xr!*.exe -xr!*.dmg -xr!*.blockmap -xr!*.yml -xr!*.yaml -xr!win-unpacked -xr!mac -xr!mac-arm64 -xr!linux-unpacked -xr!builder-*';
execSync(`${zipBinPath} a ${outputFile} ${inputFolder} ${excludes}`);
execSync(`${zipBinPath} rn -r ${outputFile} dist ${fivechanHtmlFolderName}`);
} catch (e) {
console.error('electron build createHtmlArchive error:', e);
}
+4 -5
View File
@@ -1,6 +1,6 @@
{
"name": "5chan",
"version": "0.6.0",
"version": "0.6.1",
"description": "A bitsocial client with a 4chan UI",
"type": "module",
"author": "Bitsocial Labs",
@@ -90,9 +90,9 @@
]
},
"devDependencies": {
"@capacitor/android": "7.4.4",
"@capacitor/cli": "7.4.4",
"@capacitor/core": "7.4.4",
"@capacitor/android": "7.4.5",
"@capacitor/cli": "7.4.5",
"@capacitor/core": "7.4.5",
"@electron/rebuild": "4.0.0",
"@react-scan/vite-plugin-react-scan": "0.1.8",
"@types/memoizee": "0.4.9",
@@ -142,7 +142,6 @@
"cacache": "19.0.1",
"preact": "10.27.3",
"@remix-run/router": "1.23.2",
"tar": "^7.5.3",
"undici": "^7.18.2",
"diff": "^8.0.3"
},
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "تم الإرسال",
"hours": "ساعات",
"minutes": "دقائق",
"alert_threshold": "عتبة التنبيه"
"alert_threshold": "عتبة التنبيه",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "জমা দেওয়া হয়েছে",
"hours": "ঘণ্টা",
"minutes": "মিনিট",
"alert_threshold": "সতর্কতা থ্রেশহোল্ড"
"alert_threshold": "সতর্কতা থ্রেশহোল্ড",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Odesláno",
"hours": "hodin",
"minutes": "minut",
"alert_threshold": "Práh upozornění"
"alert_threshold": "Práh upozornění",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Indsendt",
"hours": "timer",
"minutes": "minutter",
"alert_threshold": "Alarmtærskel"
"alert_threshold": "Alarmtærskel",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Eingereicht",
"hours": "Stunden",
"minutes": "Minuten",
"alert_threshold": "Alarmschwellenwert"
"alert_threshold": "Alarmschwellenwert",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Υποβλήθηκε",
"hours": "ώρες",
"minutes": "λεπτά",
"alert_threshold": "Όριο ειδοποίησης"
"alert_threshold": "Όριο ειδοποίησης",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+3
View File
@@ -263,6 +263,9 @@
"alert_threshold": "Alert threshold",
"hours": "hours",
"minutes": "minutes",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed",
"no_content": "No content",
"excerpt": "Excerpt",
"publishing": "Publishing",
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Enviado",
"hours": "horas",
"minutes": "minutos",
"alert_threshold": "Umbral de alerta"
"alert_threshold": "Umbral de alerta",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "ارسال شده",
"hours": "ساعت",
"minutes": "دقیقه",
"alert_threshold": "آستانه هشدار"
"alert_threshold": "آستانه هشدار",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Lähetetty",
"hours": "tuntia",
"minutes": "minuuttia",
"alert_threshold": "Hälytyskynnys"
"alert_threshold": "Hälytyskynnys",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Isinumite",
"hours": "oras",
"minutes": "minuto",
"alert_threshold": "Alert threshold"
"alert_threshold": "Alert threshold",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Soumis",
"hours": "heures",
"minutes": "minutes",
"alert_threshold": "Seuil d'alerte"
"alert_threshold": "Seuil d'alerte",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "נשלח",
"hours": "שעות",
"minutes": "דקות",
"alert_threshold": "סף התרעה"
"alert_threshold": "סף התרעה",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "सबमिट किया गया",
"hours": "घंटे",
"minutes": "मिनट",
"alert_threshold": "सतर्कता सीमा"
"alert_threshold": "सतर्कता सीमा",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Beküldve",
"hours": "óra",
"minutes": "perc",
"alert_threshold": "Riasztási küszöb"
"alert_threshold": "Riasztási küszöb",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Dikirim",
"hours": "jam",
"minutes": "menit",
"alert_threshold": "Ambang peringatan"
"alert_threshold": "Ambang peringatan",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Inviato",
"hours": "ore",
"minutes": "minuti",
"alert_threshold": "Soglia di allerta"
"alert_threshold": "Soglia di allerta",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "送信済み",
"hours": "時間",
"minutes": "分",
"alert_threshold": "アラート閾値"
"alert_threshold": "アラート閾値",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "제출됨",
"hours": "시간",
"minutes": "분",
"alert_threshold": "알림 임계값"
"alert_threshold": "알림 임계값",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "सबमिट केले",
"hours": "तास",
"minutes": "मिनिटे",
"alert_threshold": "सतर्कता मर्यादा"
"alert_threshold": "सतर्कता मर्यादा",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Ingediend",
"hours": "uur",
"minutes": "minuten",
"alert_threshold": "Drempelwaarde voor waarschuwing"
"alert_threshold": "Drempelwaarde voor waarschuwing",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Sendt inn",
"hours": "timer",
"minutes": "minutter",
"alert_threshold": "Terskel for varsel"
"alert_threshold": "Terskel for varsel",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Przesłane",
"hours": "godzin",
"minutes": "minut",
"alert_threshold": "Próg alertu"
"alert_threshold": "Próg alertu",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Enviado",
"hours": "horas",
"minutes": "minutos",
"alert_threshold": "Limite de alerta"
"alert_threshold": "Limite de alerta",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Trimis",
"hours": "ore",
"minutes": "minute",
"alert_threshold": "Prag de alertă"
"alert_threshold": "Prag de alertă",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Отправлено",
"hours": "часов",
"minutes": "минут",
"alert_threshold": "Порог предупреждения"
"alert_threshold": "Порог предупреждения",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Dërguar",
"hours": "orë",
"minutes": "minuta",
"alert_threshold": "Pragu i alarmit"
"alert_threshold": "Pragu i alarmit",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Inskickat",
"hours": "timmar",
"minutes": "minuter",
"alert_threshold": "Tröskelvärde för varning"
"alert_threshold": "Tröskelvärde för varning",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "సమర్పించబడింది",
"hours": "గంటలు",
"minutes": "నిమిషాలు",
"alert_threshold": "హెచ్చరిక థ్రెషోల్డ్"
"alert_threshold": "హెచ్చరిక థ్రెషోల్డ్",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "ส่งแล้ว",
"hours": "ชั่วโมง",
"minutes": "นาที",
"alert_threshold": "เกณฑ์การแจ้งเตือน"
"alert_threshold": "เกณฑ์การแจ้งเตือน",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Gönderildi",
"hours": "saat",
"minutes": "dakika",
"alert_threshold": "Uyarı eşiği"
"alert_threshold": "Uyarı eşiği",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Відправлено",
"hours": "годин",
"minutes": "хвилин",
"alert_threshold": "Поріг попередження"
"alert_threshold": "Поріг попередження",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "جمع کرایا گیا",
"hours": "گھنٹے",
"minutes": "منٹ",
"alert_threshold": "انتباہ کی حد"
"alert_threshold": "انتباہ کی حد",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "Đã gửi",
"hours": "giờ",
"minutes": "phút",
"alert_threshold": "Ngưỡng cảnh báo"
"alert_threshold": "Ngưỡng cảnh báo",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+4 -1
View File
@@ -269,5 +269,8 @@
"submitted": "已提交",
"hours": "小时",
"minutes": "分钟",
"alert_threshold": "警报阈值"
"alert_threshold": "警报阈值",
"modQueue.viewLabel": "View",
"modQueue.compact": "Compact",
"modQueue.feed": "Feed"
}
+2 -3
View File
@@ -83,14 +83,13 @@ const BoardLayout = () => {
<DisclaimerModal />
<BoardHeader />
{isMobile
? (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress) &&
!isOnModQueueRoute && (
? (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress || isOnModQueueRoute) && (
<>
<PostForm key={key} />
<MobileBoardButtons />
</>
)
: (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
: (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress || isOnModQueueRoute) && (
<>
<PostForm key={key} />
{!(isInAllView || isInSubscriptionsView || isInModView) && !isOnModQueueRoute && <SubplebbitStats />}
@@ -66,6 +66,30 @@
align-items: center;
}
.modQueueControls {
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 8px;
font-size: 12px;
}
.modQueueControls label {
display: inline-flex;
align-items: center;
gap: 6px;
}
.alertThresholdInput {
width: 50px;
}
.mobileBoardButtons .modQueueControls {
display: flex;
justify-content: center;
margin: 6px 0;
}
.desktopBoardButtons .rightSideButtons select {
margin-right: 5px;
}
@@ -11,6 +11,7 @@ import useCatalogStyleStore from '../../stores/use-catalog-style-store';
import useFeedResetStore from '../../stores/use-feed-reset-store';
import useSortingStore from '../../stores/use-sorting-store';
import useAllFeedFilterStore from '../../stores/use-all-feed-filter-store';
import useModQueueStore from '../../stores/use-mod-queue-store';
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
import useIsMobile from '../../hooks/use-is-mobile';
import useTimeFilter from '../../hooks/use-time-filter';
@@ -229,6 +230,97 @@ const ImageSizeOptions = () => {
);
};
const MAX_ALERT_THRESHOLD = 10000; // Maximum threshold value in minutes (~166 hours)
const ModQueueAlertThreshold = () => {
const { t } = useTranslation();
const { alertThresholdValue, alertThresholdUnit, setAlertThreshold } = useModQueueStore();
const handleThresholdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value.trim();
// Handle empty input - allow it temporarily for better UX
if (inputValue === '') {
return;
}
// Parse safely
const parsedValue = parseInt(inputValue, 10);
// Default to 1 if invalid or NaN
if (isNaN(parsedValue) || parsedValue < 1) {
setAlertThreshold(1, alertThresholdUnit);
return;
}
// Convert to minutes for clamping, then convert back to current unit
const valueInMinutes = alertThresholdUnit === 'hours' ? parsedValue * 60 : parsedValue;
const clampedMinutes = Math.min(valueInMinutes, MAX_ALERT_THRESHOLD);
const finalValue = alertThresholdUnit === 'hours' ? Math.round(clampedMinutes / 60) : clampedMinutes;
setAlertThreshold(finalValue, alertThresholdUnit);
};
const handleThresholdBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const inputValue = e.target.value.trim();
// If empty or invalid, restore to current value or default to 1
if (inputValue === '' || isNaN(parseInt(inputValue, 10))) {
const safeValue = alertThresholdValue >= 1 ? alertThresholdValue : 1;
setAlertThreshold(safeValue, alertThresholdUnit);
}
};
return (
<div className={styles.modQueueControls}>
<label>
{t('alert_threshold')}:
<input
type='number'
min='1'
step='1'
value={alertThresholdValue}
onChange={handleThresholdChange}
onBlur={handleThresholdBlur}
className={styles.alertThresholdInput}
/>
<select
value={alertThresholdUnit}
onChange={(e) => {
const newUnit = e.target.value as 'hours' | 'minutes';
const newValue =
alertThresholdUnit === 'hours' && newUnit === 'minutes'
? alertThresholdValue * 60
: alertThresholdUnit === 'minutes' && newUnit === 'hours'
? Math.round(alertThresholdValue / 60)
: alertThresholdValue;
setAlertThreshold(Math.max(1, newValue), newUnit);
}}
>
<option value='minutes'>{t('minutes')}</option>
<option value='hours'>{t('hours')}</option>
</select>
</label>
</div>
);
};
const ModQueueViewSelector = () => {
const { t } = useTranslation();
const { viewMode, setViewMode } = useModQueueStore();
return (
<div className={styles.modQueueControls}>
<label>
{t('modQueue.viewLabel')}:
<select value={viewMode} onChange={(e) => setViewMode(e.target.value as 'compact' | 'feed')}>
<option value='compact'>{t('modQueue.compact')}</option>
<option value='feed'>{t('modQueue.feed')}</option>
</select>
</label>
</div>
);
};
const ShowOPCommentOption = () => {
const { t } = useTranslation();
const { showOPComment, setShowOPComment } = useCatalogStyleStore();
@@ -353,6 +445,8 @@ export const MobileBoardButtons = () => {
isInModQueueView={isInModQueueView}
/>
<RefreshButton />
<ModQueueAlertThreshold />
<ModQueueViewSelector />
</>
) : (
<>
@@ -476,6 +570,10 @@ export const DesktopBoardButtons = () => {
/>
] [
<RefreshButton />]
<span className={styles.rightSideButtons}>
<ModQueueAlertThreshold />
<ModQueueViewSelector />
</span>
</>
) : (
<>
+96 -8
View File
@@ -9,7 +9,8 @@ import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDim
import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/post-utils';
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { isValidURL } from '../../lib/utils/url-utils';
import { isAllView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import useModQueueStore from '../../stores/use-mod-queue-store';
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
import { getBoardPath } from '../../lib/utils/route-utils';
import useAvatarVisibilityStore from '../../stores/use-avatar-visibility-store';
@@ -54,7 +55,19 @@ const useShowOmittedReplies = create<ShowOmittedRepliesState>((set) => ({
})),
}));
const PostInfo = ({ post, postReplyCount = 0, roles, isHidden, threadNumber }: PostProps) => {
const PostInfo = ({
post,
postReplyCount = 0,
roles,
isHidden,
threadNumber,
isModQueue,
modQueueStatus,
modQueueError,
isPublishing,
onApprove,
onReject,
}: PostProps) => {
const { t } = useTranslation();
const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, state, subplebbitAddress, timestamp } = post || {};
const title = post?.title?.trim();
@@ -74,6 +87,17 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden, threadNumber }: P
const params = useParams();
const location = useLocation();
const isInPostPageView = isPostPageView(location.pathname, params);
const isInModQueueView = isModQueueView(location.pathname);
const { getAlertThresholdSeconds } = useModQueueStore();
// Check if post is awaiting approval and over threshold (for mod queue view)
const approved = post?.approved;
const alreadyApproved = approved === true;
const alreadyRejected = removed === true;
const isAwaitingApproval = isInModQueueView && !alreadyApproved && !alreadyRejected;
const timeWaiting = timestamp ? Date.now() / 1000 - timestamp : 0;
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 userIDBackgroundColor = hashStringToColor(userID);
@@ -169,7 +193,14 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden, threadNumber }: P
){' '}
</span>
<span className={styles.dateTime}>
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} />{' '}
{isInModQueueView && isOverThreshold ? (
<>
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} /> (
<span className={styles.alert}>{getFormattedTimeAgo(timestamp)}</span>)
</>
) : (
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} />
)}{' '}
</span>
<span className={styles.postNum}>
{cid ? (
@@ -204,7 +235,7 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden, threadNumber }: P
<img src='assets/icons/closed.gif' alt='' className={styles.closedIcon} title={t('closed')} />
</span>
)}
{!isInPostPageView && !isReply && !isHidden && (
{!isInPostPageView && !isReply && !isHidden && !isModQueue && (
<span className={styles.replyButton}>
[
<Link to={boardPath ? `/${boardPath}/thread/${postCid}` : `/thread/${postCid}`} onClick={(e) => !cid && e.preventDefault()}>
@@ -213,8 +244,41 @@ const PostInfo = ({ post, postReplyCount = 0, roles, isHidden, threadNumber }: P
]
</span>
)}
{isModQueue && (
<span className={styles.modQueueActions}>
{modQueueStatus === 'approved' ? (
<span className={styles.modQueueStatusApproved}>{t('approved')}</span>
) : modQueueStatus === 'rejected' ? (
<span className={styles.modQueueStatusRejected}>{t('rejected')}</span>
) : modQueueStatus === 'failed' ? (
<span className={styles.modQueueStatusRejected}>
{t('failed')}
{modQueueError ? `: ${modQueueError}` : ''}
</span>
) : isPublishing ? (
<LoadingEllipsis string={t('publishing')} />
) : (
<>
<span className={styles.modQueueButtonWrapper}>
[
<button className={styles.modQueueActionButton} onClick={onApprove} disabled={isPublishing}>
{t('approve')}
</button>
]
</span>
<span className={styles.modQueueButtonWrapper}>
[
<button className={styles.modQueueActionButton} onClick={onReject} disabled={isPublishing}>
{t('reject')}
</button>
]
</span>
</>
)}
</span>
)}
</span>
{!(removed || deleted) && <PostMenuDesktop postMenu={postMenuProps} />}
{!(removed || deleted) && !isModQueue && <PostMenuDesktop postMenu={postMenuProps} />}
{cid &&
parentCid &&
replies &&
@@ -376,7 +440,19 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => {
);
};
const PostDesktop = ({ post, roles, showAllReplies, showReplies = true, targetReplyCid }: PostProps) => {
const PostDesktop = ({
post,
roles,
showAllReplies,
showReplies = true,
targetReplyCid,
isModQueue,
modQueueStatus,
modQueueError,
isPublishing,
onApprove,
onReject,
}: PostProps) => {
const { t } = useTranslation();
const { author, cid, content, deleted, link, linkHeight, linkWidth, pinned, postCid, removed, spoiler, state, subplebbitAddress, thumbnailUrl, parentCid } = post || {};
const params = useParams();
@@ -450,7 +526,7 @@ const PostDesktop = ({ post, roles, showAllReplies, showReplies = true, targetRe
return (
<div className={styles.postDesktop}>
{showReplies ? (
{showReplies || isModQueue ? (
<div className={styles.hrWrapper}>
<hr />
</div>
@@ -485,7 +561,19 @@ const PostDesktop = ({ post, roles, showAllReplies, showReplies = true, targetRe
isInSubscriptionsView={isInSubscriptionsView}
/>
)}
<PostInfo isHidden={hidden} post={post} postReplyCount={replyCount} roles={roles} threadNumber={post?.number} />
<PostInfo
isHidden={hidden}
post={post}
postReplyCount={replyCount}
roles={roles}
threadNumber={post?.number}
isModQueue={isModQueue}
modQueueStatus={modQueueStatus}
modQueueError={modQueueError}
isPublishing={isPublishing}
onApprove={onApprove}
onReject={onReject}
/>
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
{!isHidden && <CommentContent comment={post} />}
</div>
@@ -18,6 +18,18 @@
margin-top: -3px;
}
.modQueueTitle {
font-size: 1.5em;
font-weight: bold;
padding: 10px 0;
}
@media (min-width: 640px) {
.modQueueTitle {
font-size: 1em;
}
}
.closed {
font-size: x-large;
text-align: center;
+8 -3
View File
@@ -8,7 +8,7 @@ import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/s
import { getHasThumbnail, getLinkMediaInfo } from '../../lib/utils/media-utils';
import { formatMarkdown } from '../../lib/utils/post-utils';
import { isValidURL } from '../../lib/utils/url-utils';
import { isAllView, isCatalogView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { isAllView, isCatalogView, isModQueueView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
@@ -373,6 +373,7 @@ const PostForm = () => {
const isInPostView = isPostPageView(location.pathname, params);
const isInAllView = isAllView(location.pathname);
const isInModView = isModView(location.pathname);
const isInModQueueView = isModQueueView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const isInCatalogView = isCatalogView(location.pathname, params);
@@ -398,7 +399,9 @@ const PostForm = () => {
<>
<div className={styles.postFormDesktop}>
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && <OfflineAlert subplebbitAddress={subplebbitAddress} />}
{isThreadClosed ? (
{isInModQueueView ? (
<div className={styles.modQueueTitle}>{t('moderation_queue')}</div>
) : isThreadClosed ? (
<div className={styles.closed}>
{t('thread_closed')}
<br />
@@ -418,7 +421,9 @@ const PostForm = () => {
</div>
<div className={styles.postFormMobile}>
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && <OfflineAlert subplebbitAddress={subplebbitAddress} />}
{isThreadClosed ? (
{isInModQueueView ? (
<div className={styles.modQueueTitle}>{t('moderation_queue')}</div>
) : isThreadClosed ? (
<div className={styles.closed}>
{t('thread_closed')}
<br />
+66 -9
View File
@@ -9,7 +9,8 @@ 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 { isAllView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { isAllView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import useModQueueStore from '../../stores/use-mod-queue-store';
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
import { getBoardPath } from '../../lib/utils/route-utils';
import useAvatarVisibilityStore from '../../stores/use-avatar-visibility-store';
@@ -52,10 +53,21 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
const isInAllView = isAllView(location.pathname);
const isInPostPageView = isPostPageView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const isInModQueueView = isModQueueView(location.pathname);
const { getAlertThresholdSeconds } = useModQueueStore();
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
// Check if post is awaiting approval and over threshold (for mod queue view)
const approved = post?.approved;
const alreadyApproved = approved === true;
const alreadyRejected = removed === true;
const isAwaitingApproval = isInModQueueView && !alreadyApproved && !alreadyRejected;
const timeWaiting = timestamp ? Date.now() / 1000 - timestamp : 0;
const alertThresholdSeconds = getAlertThresholdSeconds();
const isOverThreshold = isAwaitingApproval && timeWaiting > alertThresholdSeconds;
const stateString = useStateString(post);
const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]);
@@ -176,7 +188,14 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos
<Link to={`/${boardPath}`}>Board: {boardPath}</Link>
</div>
)}
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} />{' '}
{isInModQueueView && isOverThreshold ? (
<>
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} /> (
<span className={styles.alert}>{getFormattedTimeAgo(timestamp)}</span>)
</>
) : (
<Tooltip children={<span>{getFormattedDate(timestamp)}</span>} content={getFormattedTimeAgo(timestamp)} />
)}{' '}
{cid ? (
<span className={styles.postNumLink}>
<Link
@@ -283,7 +302,19 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => {
);
};
const PostMobile = ({ post, roles, showAllReplies, showReplies = true, targetReplyCid }: PostProps) => {
const PostMobile = ({
post,
roles,
showAllReplies,
showReplies = true,
targetReplyCid,
isModQueue,
modQueueStatus,
modQueueError,
isPublishing,
onApprove,
onReject,
}: PostProps) => {
const { t } = useTranslation();
const { author, cid, pinned, postCid, replyCount, state, subplebbitAddress } = post || {};
const params = useParams();
@@ -356,12 +387,12 @@ const PostMobile = ({ post, roles, showAllReplies, showReplies = true, targetRep
</>
) : (
<div className={styles.postMobile}>
{showReplies && (
{(showReplies || isModQueue) && (
<div className={styles.hrWrapper}>
<hr />
</div>
)}
<div className={showReplies ? styles.thread : styles.quotePreview}>
<div className={showReplies || isModQueue ? styles.thread : styles.quotePreview}>
<div className={styles.postContainer}>
<div
className={`${styles.postOp} ${shouldShowSnow() ? styles.xmasHatWrapper : ''}`}
@@ -373,15 +404,41 @@ const PostMobile = ({ post, roles, showAllReplies, showReplies = true, targetRep
<PostInfoAndMedia post={post} postReplyCount={replyCount} roles={roles} threadNumber={post?.number} />
<CommentContent comment={post} />
</div>
{!isInPostView && !isInPendingPostView && showReplies && (
{!isInPostView && !isInPendingPostView && (showReplies || isModQueue) && (
<div className={styles.postLink}>
<span className={styles.info}>
{replyCount > 0 && `${replyCount} Replies`}
{linksCount > 0 && ` / ${linksCount} Links`}
</span>
<Link to={boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`} className='button'>
{t('view_thread')}
</Link>
{isModQueue ? (
<div className={styles.modQueueActions}>
{modQueueStatus === 'approved' ? (
<span className={styles.modQueueStatusApproved}>{t('approved')}</span>
) : modQueueStatus === 'rejected' ? (
<span className={styles.modQueueStatusRejected}>{t('rejected')}</span>
) : modQueueStatus === 'failed' ? (
<span className={styles.modQueueStatusRejected}>
{t('failed')}
{modQueueError ? `: ${modQueueError}` : ''}
</span>
) : isPublishing ? (
<LoadingEllipsis string={t('publishing')} />
) : (
<>
<button className={`button ${styles.approveButton}`} onClick={onApprove} disabled={isPublishing}>
{t('approve')}
</button>
<button className={`button ${styles.rejectButton}`} onClick={onReject} disabled={isPublishing}>
{t('reject')}
</button>
</>
)}
</div>
) : (
<Link to={boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`} className='button'>
{t('view_thread')}
</Link>
)}
</div>
)}
</div>
+10 -2
View File
@@ -2,13 +2,16 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type AlertThresholdUnit = 'hours' | 'minutes';
export type ModQueueViewMode = 'compact' | 'feed';
interface ModQueueState {
alertThresholdValue: number;
alertThresholdUnit: AlertThresholdUnit;
selectedBoardFilter: string | null;
viewMode: ModQueueViewMode;
setAlertThreshold: (value: number, unit: AlertThresholdUnit) => void;
setSelectedBoardFilter: (boardAddress: string | null) => void;
setViewMode: (viewMode: ModQueueViewMode) => void;
// Helper to get threshold in seconds for calculations
getAlertThresholdSeconds: () => number;
}
@@ -19,10 +22,11 @@ interface OldPersistedState {
alertThresholdValue?: number;
alertThresholdUnit?: AlertThresholdUnit;
selectedBoardFilter?: string | null;
viewMode?: ModQueueViewMode;
}
// Type for persisted data (without methods)
type PersistedModQueueData = Pick<ModQueueState, 'alertThresholdValue' | 'alertThresholdUnit' | 'selectedBoardFilter'>;
type PersistedModQueueData = Pick<ModQueueState, 'alertThresholdValue' | 'alertThresholdUnit' | 'selectedBoardFilter' | 'viewMode'>;
const useModQueueStore = create<ModQueueState>()(
persist(
@@ -30,8 +34,10 @@ const useModQueueStore = create<ModQueueState>()(
alertThresholdValue: 6,
alertThresholdUnit: 'hours' as AlertThresholdUnit,
selectedBoardFilter: null,
viewMode: 'compact',
setAlertThreshold: (value, unit) => set({ alertThresholdValue: value, alertThresholdUnit: unit }),
setSelectedBoardFilter: (boardAddress) => set({ selectedBoardFilter: boardAddress }),
setViewMode: (viewMode) => set({ viewMode }),
getAlertThresholdSeconds: () => {
const { alertThresholdValue, alertThresholdUnit } = get();
return alertThresholdUnit === 'hours' ? alertThresholdValue * 3600 : alertThresholdValue * 60;
@@ -39,7 +45,7 @@ const useModQueueStore = create<ModQueueState>()(
}),
{
name: 'mod-queue-storage',
version: 1,
version: 2,
// Migrate old alertThresholdHours format to new alertThresholdValue/alertThresholdUnit format
migrate: (persistedState, version): ModQueueState => {
const state = persistedState as OldPersistedState;
@@ -48,6 +54,7 @@ const useModQueueStore = create<ModQueueState>()(
alertThresholdValue: state.alertThresholdHours,
alertThresholdUnit: 'hours' as AlertThresholdUnit,
selectedBoardFilter: state.selectedBoardFilter ?? null,
viewMode: state.viewMode ?? 'compact',
};
// Zustand will merge this with the store definition (which includes methods)
return migrated as ModQueueState;
@@ -57,6 +64,7 @@ const useModQueueStore = create<ModQueueState>()(
alertThresholdValue: state.alertThresholdValue ?? 6,
alertThresholdUnit: state.alertThresholdUnit ?? 'hours',
selectedBoardFilter: state.selectedBoardFilter ?? null,
viewMode: state.viewMode ?? 'compact',
};
return current as ModQueueState;
},
+46
View File
@@ -233,6 +233,52 @@
display: inline;
}
.mobileCard {
border: var(--mod-queue-table-border);
background: var(--post-mobile-background-color, transparent);
padding: 8px;
margin: 6px 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.cardHeader {
display: flex;
justify-content: space-between;
font-size: 9pt;
font-weight: bold;
}
.cardNumber {
color: var(--body-font-color);
}
.cardTime {
white-space: nowrap;
}
.cardContent {
font-size: 9pt;
color: var(--body-font-color);
}
.cardContent a {
text-decoration: var(--post-link-text-decoration);
color: var(--post-link-text-color);
}
.cardContent a:hover {
text-decoration: var(--post-link-text-decoration-hover);
color: var(--post-link-text-color-hover);
}
.cardActions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* Desktop: buttons styled as text links (same as board buttons) */
@media (min-width: 640px) {
.actions {
+275 -117
View File
@@ -1,4 +1,4 @@
import React, { useMemo, useState, useEffect } from 'react';
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, Link } from 'react-router-dom';
import { useFeed, Comment, usePublishCommentModeration, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
@@ -21,6 +21,7 @@ import useChallengesStore from '../../stores/use-challenges-store';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
import Tooltip from '../../components/tooltip';
import useIsMobile from '../../hooks/use-is-mobile';
import { Post } from '../post/post';
const { addChallenge } = useChallengesStore.getState();
@@ -55,30 +56,22 @@ interface ModQueueRowProps {
// Track which action was initiated to show appropriate completion message
type ModerationAction = 'approve' | 'reject' | null;
const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
interface ModQueueActionState {
status: 'approved' | 'rejected' | 'failed' | null;
errorMessage?: string;
isPublishing: boolean;
handleApprove: () => Promise<void>;
handleReject: () => Promise<void>;
}
const useModQueueActions = (comment: Comment): ModQueueActionState => {
const { t } = useTranslation();
const { getAlertThresholdSeconds } = useModQueueStore();
const { cid, subplebbitAddress, approved, removed } = comment || {};
const [initiatedAction, setInitiatedAction] = useState<ModerationAction>(null);
const isMobile = useIsMobile();
const { editedComment } = useEditedComment({ comment });
const displayComment = editedComment || comment;
const { content, title, timestamp, subplebbitAddress, cid, shortCid, threadCid, link, thumbnailUrl, linkWidth, linkHeight, removed, approved, number } = displayComment;
// Check if already moderated (from previous session or API update)
// Note: `approved` and `removed` are direct fields on the comment from CommentUpdate,
// not nested under commentModeration (which is the options object for publishing moderation actions)
const alreadyApproved = approved === true;
const alreadyRejected = removed === true;
const timeWaiting = Date.now() / 1000 - timestamp;
const alertThresholdSeconds = getAlertThresholdSeconds();
const isOverThreshold = timeWaiting > alertThresholdSeconds;
// Only show alert animation for comments awaiting approval (not approved or rejected)
const isAwaitingApproval = !alreadyApproved && !alreadyRejected;
const {
publishCommentModeration: approve,
state: approveState,
@@ -117,8 +110,7 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
},
});
const handleApprove = async () => {
// Double confirmation for approve action
const handleApprove = useCallback(async () => {
const confirm = window.confirm(t('double_confirm'));
if (!confirm) {
return;
@@ -130,10 +122,9 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
} catch (e) {
console.error(e);
}
};
}, [approve, t]);
const handleReject = async () => {
// Double confirmation for reject action
const handleReject = useCallback(async () => {
const confirm = window.confirm(t('double_confirm'));
if (!confirm) {
return;
@@ -145,9 +136,8 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
} catch (e) {
console.error(e);
}
};
}, [reject, t]);
// Determine the current moderation state based on which action was initiated
const isApproving = initiatedAction === 'approve' && approveState !== 'initializing' && approveState !== 'succeeded' && approveState !== 'failed';
const isRejecting = initiatedAction === 'reject' && rejectState !== 'initializing' && rejectState !== 'succeeded' && rejectState !== 'failed';
const isPublishing = isApproving || isRejecting;
@@ -158,6 +148,37 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
const approveFailed = initiatedAction === 'approve' && approveState === 'failed';
const rejectFailed = initiatedAction === 'reject' && rejectState === 'failed';
const status = alreadyApproved || approveSucceeded ? 'approved' : alreadyRejected || rejectSucceeded ? 'rejected' : approveFailed || rejectFailed ? 'failed' : null;
const errorMessage = approveFailed ? approveError?.message : rejectFailed ? rejectError?.message : undefined;
return { status, errorMessage, isPublishing, handleApprove, handleReject };
};
const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
const { t } = useTranslation();
const { getAlertThresholdSeconds } = useModQueueStore();
const isMobile = useIsMobile();
const { editedComment } = useEditedComment({ comment });
const displayComment = editedComment || comment;
const { content, title, timestamp, subplebbitAddress, cid, threadCid, link, thumbnailUrl, linkWidth, linkHeight, removed, approved, number } = displayComment;
// Check if already moderated (from previous session or API update)
// Note: `approved` and `removed` are direct fields on the comment from CommentUpdate,
// not nested under commentModeration (which is the options object for publishing moderation actions)
const alreadyApproved = approved === true;
const alreadyRejected = removed === true;
const timeWaiting = Date.now() / 1000 - timestamp;
const alertThresholdSeconds = getAlertThresholdSeconds();
const isOverThreshold = timeWaiting > alertThresholdSeconds;
// Only show alert animation for comments awaiting approval (not approved or rejected)
const isAwaitingApproval = !alreadyApproved && !alreadyRejected;
const { status, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(displayComment);
const boardPath = useBoardPath(subplebbitAddress);
const hasTitle = title && title.trim().length > 0;
const hasContent = content && content.trim().length > 0;
@@ -176,23 +197,17 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
// Render the status or action buttons
const renderActions = () => {
// Check existing moderation state first (from API/previous sessions)
if (alreadyApproved || approveSucceeded) {
if (status === 'approved') {
return <span className={`${styles.button} ${styles.approve}`}>{t('approved')}</span>;
}
if (alreadyRejected || rejectSucceeded) {
if (status === 'rejected') {
return <span className={`${styles.button} ${styles.reject}`}>{t('rejected')}</span>;
}
if (approveFailed) {
if (status === 'failed') {
return (
<span className={`${styles.button} ${styles.reject}`}>
{t('failed')}: {approveError?.message}
</span>
);
}
if (rejectFailed) {
return (
<span className={`${styles.button} ${styles.reject}`}>
{t('failed')}: {rejectError?.message}
{t('failed')}
{errorMessage ? `: ${errorMessage}` : ''}
</span>
);
}
@@ -258,6 +273,137 @@ const ModQueueRow = ({ comment, isOdd = false }: ModQueueRowProps) => {
);
};
interface ModQueueCardProps {
comment: Comment;
}
const ModQueueCard = ({ comment }: ModQueueCardProps) => {
const { t } = useTranslation();
const { getAlertThresholdSeconds } = useModQueueStore();
const { editedComment } = useEditedComment({ comment });
const displayComment = editedComment || comment;
const { content, title, timestamp, subplebbitAddress, cid, threadCid, link, thumbnailUrl, linkWidth, linkHeight, removed, approved, number } = displayComment;
const alreadyApproved = approved === true;
const alreadyRejected = removed === true;
const timeWaiting = Date.now() / 1000 - timestamp;
const alertThresholdSeconds = getAlertThresholdSeconds();
const isOverThreshold = timeWaiting > alertThresholdSeconds;
const isAwaitingApproval = !alreadyApproved && !alreadyRejected;
const { status, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(displayComment);
const boardPath = useBoardPath(subplebbitAddress);
const hasTitle = title && title.trim().length > 0;
const hasContent = content && content.trim().length > 0;
const hasLink = link && link.length > 0;
const rawExcerpt =
(hasTitle ? title : null) ||
(hasContent ? content : null) ||
(hasLink ? link : null) ||
(getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link) ? t('image') : null) ||
t('no_content');
const excerpt = rawExcerpt.length > 140 ? rawExcerpt.slice(0, 137) + '...' : rawExcerpt;
const threadTargetCid = threadCid || cid;
const postUrl = boardPath && threadTargetCid ? `/${boardPath}/thread/${threadTargetCid}` : undefined;
const renderActions = () => {
if (status === 'approved') {
return (
<div className={styles.cardActions}>
<span className={`${styles.button} ${styles.approve}`}>{t('approved')}</span>
</div>
);
}
if (status === 'rejected') {
return (
<div className={styles.cardActions}>
<span className={`${styles.button} ${styles.reject}`}>{t('rejected')}</span>
</div>
);
}
if (status === 'failed') {
return (
<div className={styles.cardActions}>
<span className={`${styles.button} ${styles.reject}`}>
{t('failed')}
{errorMessage ? `: ${errorMessage}` : ''}
</span>
</div>
);
}
if (isPublishing) {
return (
<div className={styles.cardActions}>
<LoadingEllipsis string={t('publishing')} />
</div>
);
}
return (
<div className={styles.cardActions}>
<button className={styles.button} onClick={handleApprove} disabled={isPublishing}>
{t('approve')}
</button>
<button className={styles.button} onClick={handleReject} disabled={isPublishing}>
{t('reject')}
</button>
</div>
);
};
return (
<div className={styles.mobileCard}>
<div className={styles.cardHeader}>
<span className={styles.cardNumber}>No. {number ?? 'N/A'}</span>
<span className={styles.cardTime}>
{isAwaitingApproval && isOverThreshold ? (
<>
{getFormattedDate(timestamp)} (<span className={styles.alert}>{getFormattedTimeAgo(timestamp)}</span>)
</>
) : (
getFormattedDate(timestamp)
)}
</span>
</div>
<div className={styles.cardContent}>
{t('excerpt')}:{' '}
{postUrl ? (
<Link to={postUrl} title={excerpt}>
{excerpt}
</Link>
) : (
<span title={excerpt}>{excerpt}</span>
)}
</div>
{renderActions()}
</div>
);
};
const ModQueueFeedPost = ({ comment }: { comment: Comment }) => {
const { editedComment } = useEditedComment({ comment });
const displayComment = editedComment || comment;
const { status, errorMessage, isPublishing, handleApprove, handleReject } = useModQueueActions(displayComment);
return (
<Post
post={displayComment}
showAllReplies={false}
showReplies={false}
isModQueue={true}
modQueueStatus={status}
modQueueError={errorMessage}
isPublishing={isPublishing}
onApprove={handleApprove}
onReject={handleReject}
/>
);
};
interface ModQueueBoardFilterProps {
subplebbits: MultisubSubplebbit[];
}
@@ -474,7 +620,8 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProps) => {
const { t } = useTranslation();
const params = useParams();
const { selectedBoardFilter, alertThresholdValue, alertThresholdUnit, setAlertThreshold } = useModQueueStore();
const { selectedBoardFilter, viewMode } = useModQueueStore();
const isMobile = useIsMobile();
const accountSubplebbitAddresses = useAccountsStore(
(state) => {
@@ -563,97 +710,108 @@ export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueV
[hasMore, subplebbitAddresses, subplebbitError, feed.length],
);
const alertThresholdControl = (
<div className={styles.alertThresholdSetting}>
<label>
{t('alert_threshold')}:
<input
type='number'
min='1'
step={alertThresholdUnit === 'minutes' ? '1' : '1'}
value={alertThresholdValue}
onChange={(e) => setAlertThreshold(Number(e.target.value), alertThresholdUnit)}
className={styles.alertThresholdInput}
/>
<select
value={alertThresholdUnit}
onChange={(e) => {
const newUnit = e.target.value as 'hours' | 'minutes';
const newValue =
alertThresholdUnit === 'hours' && newUnit === 'minutes'
? alertThresholdValue * 60
: alertThresholdUnit === 'minutes' && newUnit === 'hours'
? Math.round(alertThresholdValue / 60)
: alertThresholdValue;
setAlertThreshold(Math.max(1, newValue), newUnit);
}}
>
<option value='minutes'>{t('minutes')}</option>
<option value='hours'>{t('hours')}</option>
</select>
</label>
</div>
);
return (
<div className={styles.container}>
{!resolvedAddress && (
<div className={styles.header}>
<div className={styles.title}>{t('moderation_queue')}</div>
<div className={styles.controls}>
<div className={styles.controlsLeft}>
<ModQueueBoardFilter subplebbits={subplebbitsWithMetadata} />
</div>
</div>
)}
<div className={styles.controls}>
{!resolvedAddress ? (
<>
<div className={styles.controlsLeft}>
<ModQueueBoardFilter subplebbits={subplebbitsWithMetadata} />
</div>
<div className={styles.controlsRight}>{alertThresholdControl}</div>
</>
) : (
<>
<div className={styles.controlsLeft}>
<div className={styles.title}>{t('moderation_queue')}</div>
</div>
<div className={styles.controlsRight}>{alertThresholdControl}</div>
</>
)}
</div>
{feed.length === 0 && !hasMore ? (
<div className={styles.empty}>{t('queue_is_empty')}</div>
) : (
<>
<div className={styles.tableHeader}>
<div className={styles.numberHeader}>No.</div>
<div className={styles.excerptHeader}>{t('excerpt')}</div>
<div className={styles.timeHeader}>{t('submitted')}</div>
<div className={styles.actionsHeader}>{t('actions')}</div>
</div>
{/* Use Virtuoso for infinite scroll only when there's more content to paginate */}
{hasMore ? (
<Virtuoso
useWindowScroll
data={feed}
totalCount={feed.length}
endReached={loadMore}
increaseViewportBy={{ bottom: 1200, top: 1200 }}
itemContent={(index, comment) => <ModQueueRow key={comment.cid} comment={comment} isOdd={index % 2 === 0} />}
components={footerComponents}
/>
) : (
{viewMode === 'compact' && !isMobile && (
<>
{feed.map((comment, index) => (
<ModQueueRow key={comment.cid} comment={comment} isOdd={index % 2 === 0} />
))}
{subplebbitError?.message && feed.length === 0 && (
<div className={styles.error}>
<ErrorDisplay error={subplebbitError} />
</div>
<div className={styles.tableHeader}>
<div className={styles.numberHeader}>No.</div>
<div className={styles.excerptHeader}>{t('excerpt')}</div>
<div className={styles.timeHeader}>{t('submitted')}</div>
<div className={styles.actionsHeader}>{t('actions')}</div>
</div>
{hasMore ? (
<Virtuoso
useWindowScroll
data={feed}
totalCount={feed.length}
endReached={loadMore}
increaseViewportBy={{ bottom: 1200, top: 1200 }}
itemContent={(index, comment) => <ModQueueRow key={comment.cid} comment={comment} isOdd={index % 2 === 0} />}
components={footerComponents}
/>
) : (
<>
{feed.map((comment, index) => (
<ModQueueRow key={comment.cid} comment={comment} isOdd={index % 2 === 0} />
))}
{subplebbitError?.message && feed.length === 0 && (
<div className={styles.error}>
<ErrorDisplay error={subplebbitError} />
</div>
)}
<ModQueueFooter hasMore={hasMore} subplebbitAddresses={subplebbitAddresses} />
</>
)}
</>
)}
{viewMode === 'compact' && isMobile && (
<>
{hasMore ? (
<Virtuoso
useWindowScroll
data={feed}
totalCount={feed.length}
endReached={loadMore}
increaseViewportBy={{ bottom: 1200, top: 1200 }}
itemContent={(_index, comment) => <ModQueueCard key={comment.cid} comment={comment} />}
components={footerComponents}
/>
) : (
<>
{feed.map((comment) => (
<ModQueueCard key={comment.cid} comment={comment} />
))}
{subplebbitError?.message && feed.length === 0 && (
<div className={styles.error}>
<ErrorDisplay error={subplebbitError} />
</div>
)}
<ModQueueFooter hasMore={hasMore} subplebbitAddresses={subplebbitAddresses} />
</>
)}
</>
)}
{viewMode === 'feed' && (
<>
{hasMore ? (
<Virtuoso
useWindowScroll
data={feed}
totalCount={feed.length}
endReached={loadMore}
increaseViewportBy={{ bottom: 1200, top: 1200 }}
itemContent={(_index, comment) => <ModQueueFeedPost key={comment.cid} comment={comment} />}
components={footerComponents}
/>
) : (
<>
{feed.map((comment) => (
<ModQueueFeedPost key={comment.cid} comment={comment} />
))}
{subplebbitError?.message && feed.length === 0 && (
<div className={styles.error}>
<ErrorDisplay error={subplebbitError} />
</div>
)}
<ModQueueFooter hasMore={hasMore} subplebbitAddresses={subplebbitAddresses} />
</>
)}
<ModQueueFooter hasMore={hasMore} subplebbitAddresses={subplebbitAddresses} />
</>
)}
</>
+61
View File
@@ -189,6 +189,55 @@
color: var(--button-desktop-text-color-hover);
}
.modQueueActions {
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 8px;
}
.modQueueButtonWrapper {
display: inline-flex;
}
.modQueueActionButton {
all: unset;
cursor: pointer;
text-transform: capitalize;
color: var(--button-desktop-text-color);
text-decoration: var(--button-text-decoration);
font-family: inherit;
}
.modQueueActionButton:hover {
color: var(--button-desktop-text-color-hover);
}
.modQueueActionButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.modQueueStatusApproved {
color: green;
font-weight: 700;
}
.modQueueStatusRejected {
color: red;
font-weight: 700;
}
.approveButton {
background-color: #3fa96b;
color: #fff;
}
.rejectButton {
background-color: #d9534f;
color: #fff;
}
.postDesktop .spacer {
width: 100%;
padding: 5px;
@@ -668,4 +717,16 @@
color: red;
font-weight: 700;
text-transform: uppercase;
}
.alert {
color: var(--mod-queue-alert-color, red);
font-weight: bold;
animation: blink 2s infinite;
}
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
+44 -3
View File
@@ -25,9 +25,26 @@ export interface PostProps {
showReplies?: boolean;
targetReplyCid?: string;
threadNumber?: number;
isModQueue?: boolean;
modQueueStatus?: 'approved' | 'rejected' | 'failed' | null;
modQueueError?: string;
isPublishing?: boolean;
onApprove?: () => void;
onReject?: () => void;
}
export const Post = ({ post, showAllReplies = false, showReplies = true, targetReplyCid }: PostProps) => {
export const Post = ({
post,
showAllReplies = false,
showReplies = true,
targetReplyCid,
isModQueue,
modQueueStatus,
modQueueError,
isPublishing,
onApprove,
onReject,
}: PostProps) => {
// Only subscribe to roles field to avoid rerenders from updatingState changes
const roles = useSubplebbitField(post?.subplebbitAddress, (subplebbit) => subplebbit?.roles);
const isMobile = useIsMobile();
@@ -44,9 +61,33 @@ export const Post = ({ post, showAllReplies = false, showReplies = true, targetR
<div className={styles.thread}>
<div className={styles.postContainer}>
{isMobile ? (
<PostMobile post={comment} roles={roles} showAllReplies={showAllReplies} showReplies={showReplies} targetReplyCid={targetReplyCid} />
<PostMobile
post={comment}
roles={roles}
showAllReplies={showAllReplies}
showReplies={showReplies}
targetReplyCid={targetReplyCid}
isModQueue={isModQueue}
modQueueStatus={modQueueStatus}
modQueueError={modQueueError}
isPublishing={isPublishing}
onApprove={onApprove}
onReject={onReject}
/>
) : (
<PostDesktop post={comment} roles={roles} showAllReplies={showAllReplies} showReplies={showReplies} targetReplyCid={targetReplyCid} />
<PostDesktop
post={comment}
roles={roles}
showAllReplies={showAllReplies}
showReplies={showReplies}
targetReplyCid={targetReplyCid}
isModQueue={isModQueue}
modQueueStatus={modQueueStatus}
modQueueError={modQueueError}
isPublishing={isPublishing}
onApprove={onApprove}
onReject={onReject}
/>
)}
</div>
</div>
+53 -19
View File
@@ -1037,20 +1037,20 @@
ipaddr.js "^2.2.0"
punycode "^2.3.1"
"@capacitor/android@7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@capacitor/android/-/android-7.4.4.tgz#a14a1e844bd5079982427e247fdd17555b5fbedd"
integrity sha512-y8knfV1JXNrd6XZZLZireGT+EBCN0lvOo+HZ/s7L8LkrPBu4nY5UZn0Wxz4yOezItEII9rqYJSHsS5fMJG9gdw==
"@capacitor/android@7.4.5":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@capacitor/android/-/android-7.4.5.tgz#3832b457e0a3a7079a645bf5be3cc23d406c2971"
integrity sha512-G76C+PGIFkG+hxj6JLlY003txRaGZwsXBPo4b4xiZMZaD6DG++sDCoumYl/5xL40dL2b8P5tb3CKf/khWyL2Gw==
"@capacitor/app@7.0.1":
version "7.0.1"
resolved "https://registry.yarnpkg.com/@capacitor/app/-/app-7.0.1.tgz#0d0709fb4dde5046c24853f2d6b77a7ea411f748"
integrity sha512-ArlVZAAla4MwQoKh26x2AaTDOBh5Vhp1VhMKR3RwqZSsZnazKTFGNrPbr9Ez5r1knnEDfApyjwp1uZnXK1WTYQ==
"@capacitor/cli@7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@capacitor/cli/-/cli-7.4.4.tgz#bfce7db8b15d6f44c9d58dc338a5e3d662fecfbb"
integrity sha512-J7ciBE7GlJ70sr2s8oz1+H4ZdNk4MGG41fsakUlDHWva5UWgFIZYMiEdDvGbYazAYTaxN3lVZpH9zil9FfZj+Q==
"@capacitor/cli@7.4.5":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@capacitor/cli/-/cli-7.4.5.tgz#056d8acc0d27ec3f8f79a5588e6cd6539db2895d"
integrity sha512-ApKxpzcWTqmyTD+WxBvL7UJt+x0xr9Vqa+S4Iqbl/R2kC0tf1ay22wE9pOrdWXbTqFbNhBuUuRvTvZ6Edp7QzQ==
dependencies:
"@ionic/cli-framework-output" "^2.2.8"
"@ionic/utils-subprocess" "^3.0.1"
@@ -1060,7 +1060,7 @@
env-paths "^2.2.0"
fs-extra "^11.2.0"
kleur "^4.1.5"
native-run "^2.0.1"
native-run "^2.0.3"
open "^8.4.0"
plist "^3.1.0"
prompts "^2.4.2"
@@ -1070,10 +1070,10 @@
tslib "^2.8.1"
xml2js "^0.6.2"
"@capacitor/core@7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@capacitor/core/-/core-7.4.4.tgz#b752dd7a12141e40d1c8ce2e37a79571d9f83518"
integrity sha512-xzjxpr+d2zwTpCaN0k+C6wKSZzWFAb9OVEUtmO72ihjr/NEDoLvsGl4WLfjWPcCO2zOy0b2X52tfRWjECFUjtw==
"@capacitor/core@7.4.5":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@capacitor/core/-/core-7.4.5.tgz#334d77356fa473e95d51cf6375e9b36471faf2d6"
integrity sha512-JnUywi0WRbqm4QYoplNsMQ7KAVuyrQFha0dOH9YDVCyrVHUvPcvrnK+9j5elUMgy+pOh3ve+pk6BVoySr91c+A==
dependencies:
tslib "^2.1.0"
@@ -6263,6 +6263,11 @@ chownr@^1.1.1:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
chownr@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4"
@@ -8179,6 +8184,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"
@@ -11271,12 +11283,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:
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.0.4, 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==
@@ -11296,6 +11313,11 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
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"
@@ -11424,10 +11446,10 @@ napi-build-utils@^2.0.0:
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e"
integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==
native-run@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/native-run/-/native-run-2.0.1.tgz#a9b213c32824b007cbdd0279e0edd3c24bcc2f7a"
integrity sha512-XfG1FBZLM50J10xH9361whJRC9SHZ0Bub4iNRhhI61C8Jv0e1ud19muex6sNKB51ibQNUJNuYn25MuYET/rE6w==
native-run@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/native-run/-/native-run-2.0.3.tgz#b1ceb39c8d24abf4916c07ae9aad5a9fbcd31a02"
integrity sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==
dependencies:
"@ionic/utils-fs" "^3.1.7"
"@ionic/utils-terminal" "^2.3.4"
@@ -13958,7 +13980,19 @@ tar-stream@^2.1.4:
inherits "^2.0.3"
readable-stream "^3.1.1"
tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2, tar@^6.2.1, tar@^7.4.3, tar@^7.5.3:
tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2, tar@^6.2.1:
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:
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"
tar@^7.4.3:
version "7.5.3"
resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.3.tgz#e1a41236e32446f75e63b720222112c4ffe5b3a1"
integrity sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==