mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "تم الإرسال",
|
||||
"hours": "ساعات",
|
||||
"minutes": "دقائق",
|
||||
"alert_threshold": "عتبة التنبيه"
|
||||
"alert_threshold": "عتبة التنبيه",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "জমা দেওয়া হয়েছে",
|
||||
"hours": "ঘণ্টা",
|
||||
"minutes": "মিনিট",
|
||||
"alert_threshold": "সতর্কতা থ্রেশহোল্ড"
|
||||
"alert_threshold": "সতর্কতা থ্রেশহোল্ড",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "Υποβλήθηκε",
|
||||
"hours": "ώρες",
|
||||
"minutes": "λεπτά",
|
||||
"alert_threshold": "Όριο ειδοποίησης"
|
||||
"alert_threshold": "Όριο ειδοποίησης",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "ارسال شده",
|
||||
"hours": "ساعت",
|
||||
"minutes": "دقیقه",
|
||||
"alert_threshold": "آستانه هشدار"
|
||||
"alert_threshold": "آستانه هشدار",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "נשלח",
|
||||
"hours": "שעות",
|
||||
"minutes": "דקות",
|
||||
"alert_threshold": "סף התרעה"
|
||||
"alert_threshold": "סף התרעה",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "सबमिट किया गया",
|
||||
"hours": "घंटे",
|
||||
"minutes": "मिनट",
|
||||
"alert_threshold": "सतर्कता सीमा"
|
||||
"alert_threshold": "सतर्कता सीमा",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "送信済み",
|
||||
"hours": "時間",
|
||||
"minutes": "分",
|
||||
"alert_threshold": "アラート閾値"
|
||||
"alert_threshold": "アラート閾値",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "제출됨",
|
||||
"hours": "시간",
|
||||
"minutes": "분",
|
||||
"alert_threshold": "알림 임계값"
|
||||
"alert_threshold": "알림 임계값",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "सबमिट केले",
|
||||
"hours": "तास",
|
||||
"minutes": "मिनिटे",
|
||||
"alert_threshold": "सतर्कता मर्यादा"
|
||||
"alert_threshold": "सतर्कता मर्यादा",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "Отправлено",
|
||||
"hours": "часов",
|
||||
"minutes": "минут",
|
||||
"alert_threshold": "Порог предупреждения"
|
||||
"alert_threshold": "Порог предупреждения",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "సమర్పించబడింది",
|
||||
"hours": "గంటలు",
|
||||
"minutes": "నిమిషాలు",
|
||||
"alert_threshold": "హెచ్చరిక థ్రెషోల్డ్"
|
||||
"alert_threshold": "హెచ్చరిక థ్రెషోల్డ్",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "ส่งแล้ว",
|
||||
"hours": "ชั่วโมง",
|
||||
"minutes": "นาที",
|
||||
"alert_threshold": "เกณฑ์การแจ้งเตือน"
|
||||
"alert_threshold": "เกณฑ์การแจ้งเตือน",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "Відправлено",
|
||||
"hours": "годин",
|
||||
"minutes": "хвилин",
|
||||
"alert_threshold": "Поріг попередження"
|
||||
"alert_threshold": "Поріг попередження",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "جمع کرایا گیا",
|
||||
"hours": "گھنٹے",
|
||||
"minutes": "منٹ",
|
||||
"alert_threshold": "انتباہ کی حد"
|
||||
"alert_threshold": "انتباہ کی حد",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -269,5 +269,8 @@
|
||||
"submitted": "已提交",
|
||||
"hours": "小时",
|
||||
"minutes": "分钟",
|
||||
"alert_threshold": "警报阈值"
|
||||
"alert_threshold": "警报阈值",
|
||||
"modQueue.viewLabel": "View",
|
||||
"modQueue.compact": "Compact",
|
||||
"modQueue.feed": "Feed"
|
||||
}
|
||||
|
||||
+2
-3
@@ -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>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -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,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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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
@@ -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} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user