mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
@@ -11,6 +11,10 @@ on:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [x64, arm64]
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -20,24 +24,23 @@ jobs:
|
||||
with:
|
||||
# needed for git commit history changelog
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js v20
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install dependencies (with Node v20)
|
||||
run: yarn install --frozen-lockfile --ignore-engines
|
||||
# make sure the ipfs executable is executable
|
||||
- name: Download IPFS and set permissions (with Node v20)
|
||||
run: node electron/download-ipfs && sudo chmod +x bin/linux/ipfs
|
||||
|
||||
- name: Setup Node.js v22 for Electron build
|
||||
- name: Setup Node.js v22
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install dependencies (with Node v22)
|
||||
run: yarn install --frozen-lockfile --ignore-engines
|
||||
# make sure the ipfs executable is executable
|
||||
- name: Download IPFS and set permissions (with Node v22)
|
||||
run: node electron/download-ipfs && sudo chmod +x bin/linux/ipfs
|
||||
- name: Build React app (with Node v22)
|
||||
run: CI='' yarn build
|
||||
- name: Build Electron app for Linux (with Node v22)
|
||||
run: yarn electron:build:linux
|
||||
run: CI='' NODE_ENV=production yarn build
|
||||
- name: Build Electron app for Linux (arm64)
|
||||
if: ${{ matrix.arch == 'arm64' }}
|
||||
run: yarn electron:build:linux:arm64
|
||||
- name: Build Electron app for Linux (x64)
|
||||
if: ${{ matrix.arch == 'x64' }}
|
||||
run: yarn electron:build:linux:x64
|
||||
- name: List dist directory
|
||||
run: ls dist
|
||||
|
||||
@@ -46,14 +49,22 @@ jobs:
|
||||
run: node scripts/release-body > release-body.txt
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: 'dist/5chan*.AppImage,dist/5chan-html*.zip'
|
||||
artifacts: 'dist/5chan*.AppImage,dist/5chan*-arm64.AppImage,dist/5chan-html*.zip'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
replacesArtifacts: true
|
||||
bodyFile: "release-body.txt"
|
||||
omitBody: true
|
||||
allowUpdates: true
|
||||
|
||||
mac:
|
||||
runs-on: macos-15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-intel # Intel x64
|
||||
arch: x64
|
||||
- runner: macos-latest # Apple Silicon arm64
|
||||
arch: arm64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -62,10 +73,14 @@ jobs:
|
||||
with:
|
||||
# needed for git commit history changelog
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js v20
|
||||
- name: Setup Node.js v22
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
- name: Setup Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
# install missing dep for sqlite (use setup-python to avoid PEP 668 externally-managed-environment error)
|
||||
- uses: actions/setup-python@v5
|
||||
@@ -73,20 +88,22 @@ jobs:
|
||||
python-version: '3.12'
|
||||
- run: pip install setuptools
|
||||
|
||||
- name: Install dependencies (with Node v20)
|
||||
- name: Install dependencies (with Node v22)
|
||||
run: yarn install --frozen-lockfile --ignore-engines
|
||||
# make sure the ipfs executable is executable
|
||||
- name: Download IPFS and set permissions (with Node v20)
|
||||
- name: Download IPFS and set permissions (with Node v22)
|
||||
run: node electron/download-ipfs && sudo chmod +x bin/mac/ipfs
|
||||
|
||||
- name: Setup Node.js v22 for Electron build
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Build React app (with Node v22)
|
||||
run: CI='' yarn build
|
||||
run: CI='' NODE_ENV=production yarn build
|
||||
- name: Build Electron app for Mac (with Node v22)
|
||||
run: yarn electron:build:mac
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
|
||||
run: |
|
||||
if [ "${{ matrix.arch }}" = "arm64" ]; then
|
||||
yarn electron:build:mac:arm64
|
||||
else
|
||||
yarn electron:build:mac:x64
|
||||
fi
|
||||
- name: List dist directory
|
||||
run: ls dist
|
||||
|
||||
@@ -95,10 +112,10 @@ jobs:
|
||||
run: node scripts/release-body > release-body.txt
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: 'dist/5chan*.dmg'
|
||||
artifacts: 'dist/5chan*.dmg,dist/5chan*-arm64.dmg'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
replacesArtifacts: true
|
||||
bodyFile: "release-body.txt"
|
||||
omitBody: true
|
||||
allowUpdates: true
|
||||
|
||||
windows:
|
||||
@@ -111,15 +128,15 @@ jobs:
|
||||
with:
|
||||
# needed for git commit history changelog
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js v22 # electron-rebuild needs Node 22+
|
||||
- name: Setup Node.js v22
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install dependencies (with Node v22) # --network-timeout and --network-concurrency are yarn v1 flags.
|
||||
run: yarn install --frozen-lockfile --network-timeout 100000 --network-concurrency 1
|
||||
- name: Build React app (with Node v22)
|
||||
run: yarn build
|
||||
- name: Build Electron app for Windows (with Node v22)
|
||||
run: npx cross-env NODE_ENV=production yarn build
|
||||
- name: Build Electron app for Windows (x64)
|
||||
run: yarn electron:build:windows
|
||||
- name: List dist directory
|
||||
run: dir dist
|
||||
@@ -132,7 +149,7 @@ jobs:
|
||||
artifacts: 'dist/5chan*.exe'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
replacesArtifacts: true
|
||||
bodyFile: "release-body.txt"
|
||||
omitBody: true
|
||||
allowUpdates: true
|
||||
|
||||
android:
|
||||
@@ -153,14 +170,14 @@ jobs:
|
||||
gradle-version: 8.9
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
- run: sudo apt install -y apksigner zipalign
|
||||
|
||||
# install all dependencies (including devDependencies needed for React build)
|
||||
- name: Install dependencies (with Node v20)
|
||||
- name: Install dependencies (with Node v22)
|
||||
run: yarn install --frozen-lockfile --ignore-engines
|
||||
# build react app
|
||||
- run: CI='' yarn build
|
||||
- run: CI='' NODE_ENV=production yarn build
|
||||
# set android versionCode and versionName
|
||||
- run: sed -i "s/versionCode 1/versionCode $(git tag | wc -l)/" ./android/app/build.gradle
|
||||
- run: sed -i "s/versionName \"1.0\"/versionName \"$(node -e "console.log(require('./package.json').version)")\"/" ./android/app/build.gradle
|
||||
@@ -168,7 +185,7 @@ jobs:
|
||||
# build apk
|
||||
- run: npx cap sync android
|
||||
- run: cd android && gradle bundle
|
||||
- run: cd android && ./gradlew assembleRelease
|
||||
- run: cd android && ./gradlew assembleRelease --stacktrace
|
||||
# optimize apk
|
||||
- run: cd android/app/build/outputs/apk/release && zipalign 4 app-release-unsigned.apk app-release-unsigned-zip.apk
|
||||
# sign apk
|
||||
@@ -185,5 +202,31 @@ jobs:
|
||||
artifacts: 'dist/5chan*.apk'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
replacesArtifacts: true
|
||||
bodyFile: "release-body.txt"
|
||||
omitBody: true
|
||||
allowUpdates: true
|
||||
|
||||
finalize-release:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [linux, mac, windows, android]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js v22
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --ignore-engines
|
||||
- name: Generate final release body from GitHub assets
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||
run: node scripts/release-body > release-body.txt
|
||||
- name: Update release body only
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowUpdates: true
|
||||
bodyFile: "release-body.txt"
|
||||
|
||||
+110
-141
@@ -25,8 +25,8 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/electron
|
||||
key: ${{ runner.os }}-electron-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-electron-
|
||||
key: ${{ runner.os }}-electron-v2-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-electron-v2-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -37,37 +37,45 @@ jobs:
|
||||
[ "$i" = "3" ] && exit 1
|
||||
done
|
||||
|
||||
- name: Download IPFS
|
||||
run: node electron/download-ipfs && chmod +x bin/linux/ipfs
|
||||
|
||||
- name: Build React App
|
||||
run: yarn build
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
- name: Build Electron App (Linux)
|
||||
run: yarn electron:build:linux
|
||||
- name: Build Preload Script
|
||||
run: yarn build:preload
|
||||
|
||||
- name: Smoke Test
|
||||
- name: Verify build outputs
|
||||
run: |
|
||||
echo "Testing AppImage startup..."
|
||||
APPIMAGE=$(find dist -name "*.AppImage" | head -n 1)
|
||||
echo "Found AppImage: $APPIMAGE"
|
||||
chmod +x "$APPIMAGE"
|
||||
# Use --appimage-extract-and-run to avoid FUSE requirement in CI
|
||||
# Run with timeout and expect it to start (will be killed after timeout)
|
||||
timeout 10s "$APPIMAGE" --appimage-extract-and-run --no-sandbox &
|
||||
APP_PID=$!
|
||||
sleep 5
|
||||
# Check if process is still running (means it started successfully)
|
||||
if kill -0 $APP_PID 2>/dev/null; then
|
||||
echo "✓ App started successfully"
|
||||
kill $APP_PID 2>/dev/null || true
|
||||
exit 0
|
||||
else
|
||||
echo "✗ App failed to start"
|
||||
exit 1
|
||||
fi
|
||||
echo "=== Checking build/ directory ==="
|
||||
ls -la build/ || exit 1
|
||||
echo "=== Checking build/electron/preload.cjs ==="
|
||||
ls -la build/electron/preload.cjs || exit 1
|
||||
echo "=== Checking build/index.html ==="
|
||||
ls -la build/index.html || exit 1
|
||||
|
||||
- name: Verify forge config
|
||||
run: |
|
||||
echo "=== forge.config.js exists ==="
|
||||
ls -la forge.config.js
|
||||
echo "=== Validating forge config can be loaded ==="
|
||||
node -e "import('./forge.config.js').then(c => console.log('Config loaded, makers:', c.default.makers?.length || 0)).catch(e => { console.error(e); process.exit(1); })"
|
||||
|
||||
- name: Package Electron App
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
set -o pipefail
|
||||
yarn electron-forge package 2>&1 | tee forge-output.log
|
||||
echo "=== Checking out/ directory ==="
|
||||
ls -la out/
|
||||
|
||||
- name: Verify Executable
|
||||
run: |
|
||||
ls -la out/
|
||||
EXE=$(node scripts/find-forge-executable.js)
|
||||
echo "Found executable: $EXE"
|
||||
# Verify the file exists and is executable
|
||||
test -f "$EXE" && test -x "$EXE" && echo "✓ Executable is valid"
|
||||
|
||||
test-mac-intel:
|
||||
name: Test Mac (Intel)
|
||||
@@ -76,24 +84,22 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
- name: Setup Node.js v22
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'yarn'
|
||||
|
||||
|
||||
- name: Install setuptools for native modules
|
||||
run: |
|
||||
# Use pip with --break-system-packages for CI environment
|
||||
pip3 install --break-system-packages setuptools || pip3 install --user setuptools || true
|
||||
run: pip3 install --break-system-packages setuptools || pip3 install --user setuptools || true
|
||||
|
||||
- name: Cache electron binaries
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/Library/Caches/electron
|
||||
key: ${{ runner.os }}-electron-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-electron-
|
||||
key: ${{ runner.os }}-electron-v2-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-electron-v2-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -104,45 +110,33 @@ jobs:
|
||||
[ "$i" = "3" ] && exit 1
|
||||
done
|
||||
|
||||
- name: Download IPFS
|
||||
run: node electron/download-ipfs && chmod +x bin/mac/ipfs
|
||||
|
||||
- name: Build React App
|
||||
run: yarn build
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
- name: Build Electron App
|
||||
# Use --dir to build only the .app bundle without DMG
|
||||
# This avoids flaky hdiutil "Resource busy" errors in CI
|
||||
run: yarn build && yarn build:preload && yarn electron-builder build --publish never -m --dir
|
||||
- name: Build Preload Script
|
||||
run: yarn build:preload
|
||||
|
||||
- name: Smoke Test
|
||||
- name: Verify build outputs
|
||||
run: |
|
||||
if [ -d "dist/mac/5chan.app" ]; then
|
||||
echo "Testing dist/mac/5chan.app..."
|
||||
# Run the app in background - it will start IPFS which takes time
|
||||
# We just verify it launches without crashing
|
||||
./dist/mac/5chan.app/Contents/MacOS/5chan &
|
||||
APP_PID=$!
|
||||
sleep 10
|
||||
# Check if process is still running (means it started successfully)
|
||||
if kill -0 $APP_PID 2>/dev/null; then
|
||||
echo "✓ App started successfully"
|
||||
kill $APP_PID 2>/dev/null || true
|
||||
# Also kill any child processes (IPFS)
|
||||
pkill -P $APP_PID 2>/dev/null || true
|
||||
pkill -f ipfs 2>/dev/null || true
|
||||
exit 0
|
||||
else
|
||||
echo "✗ App failed to start or crashed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Could not find dist/mac/5chan.app to test"
|
||||
ls -R dist
|
||||
exit 1
|
||||
fi
|
||||
ls -la build/ || exit 1
|
||||
ls -la build/electron/preload.cjs || exit 1
|
||||
ls -la build/index.html || exit 1
|
||||
|
||||
- name: Package Electron App
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
set -o pipefail
|
||||
yarn electron-forge package 2>&1 | tee forge-output.log
|
||||
ls -la out/
|
||||
|
||||
- name: Verify Executable
|
||||
run: |
|
||||
ls -la out/
|
||||
EXE=$(node scripts/find-forge-executable.js)
|
||||
echo "Found executable: $EXE"
|
||||
test -f "$EXE" && test -x "$EXE" && echo "✓ Executable is valid"
|
||||
|
||||
test-mac-arm:
|
||||
name: Test Mac (Apple Silicon)
|
||||
@@ -151,7 +145,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
- name: Setup Node.js v22
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -159,16 +153,14 @@ jobs:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install setuptools for native modules
|
||||
run: |
|
||||
# Use pip with --break-system-packages for CI environment
|
||||
pip3 install --break-system-packages setuptools || pip3 install --user setuptools || true
|
||||
run: pip3 install --break-system-packages setuptools || pip3 install --user setuptools || true
|
||||
|
||||
- name: Cache electron binaries
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/Library/Caches/electron
|
||||
key: ${{ runner.os }}-electron-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-electron-
|
||||
key: ${{ runner.os }}-electron-v2-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-electron-v2-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -179,50 +171,33 @@ jobs:
|
||||
[ "$i" = "3" ] && exit 1
|
||||
done
|
||||
|
||||
- name: Download IPFS
|
||||
run: node electron/download-ipfs && chmod +x bin/mac/ipfs
|
||||
|
||||
- name: Build React App
|
||||
run: yarn build
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
- name: Build Electron App
|
||||
# On M1 runner, this should produce arm64 build
|
||||
# Use --dir to build only the .app bundle without DMG
|
||||
# This avoids flaky hdiutil "Resource busy" errors in CI
|
||||
run: yarn build && yarn build:preload && yarn electron-builder build --publish never -m --dir
|
||||
- name: Build Preload Script
|
||||
run: yarn build:preload
|
||||
|
||||
- name: Smoke Test
|
||||
- name: Verify build outputs
|
||||
run: |
|
||||
if [ -d "dist/mac-arm64/5chan.app" ]; then
|
||||
APP_PATH="dist/mac-arm64/5chan.app"
|
||||
elif [ -d "dist/mac/5chan.app" ]; then
|
||||
APP_PATH="dist/mac/5chan.app"
|
||||
else
|
||||
echo "Could not find 5chan.app to test"
|
||||
ls -R dist
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing $APP_PATH..."
|
||||
# Run the app in background - it will start IPFS which takes time
|
||||
# We just verify it launches without crashing
|
||||
"./$APP_PATH/Contents/MacOS/5chan" &
|
||||
APP_PID=$!
|
||||
sleep 10
|
||||
# Check if process is still running (means it started successfully)
|
||||
if kill -0 $APP_PID 2>/dev/null; then
|
||||
echo "✓ App started successfully"
|
||||
kill $APP_PID 2>/dev/null || true
|
||||
# Also kill any child processes (IPFS)
|
||||
pkill -P $APP_PID 2>/dev/null || true
|
||||
pkill -f ipfs 2>/dev/null || true
|
||||
exit 0
|
||||
else
|
||||
echo "✗ App failed to start or crashed"
|
||||
exit 1
|
||||
fi
|
||||
ls -la build/ || exit 1
|
||||
ls -la build/electron/preload.cjs || exit 1
|
||||
ls -la build/index.html || exit 1
|
||||
|
||||
- name: Package Electron App
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
set -o pipefail
|
||||
yarn electron-forge package 2>&1 | tee forge-output.log
|
||||
ls -la out/
|
||||
|
||||
- name: Verify Executable
|
||||
run: |
|
||||
ls -la out/
|
||||
EXE=$(node scripts/find-forge-executable.js)
|
||||
echo "Found executable: $EXE"
|
||||
test -f "$EXE" && test -x "$EXE" && echo "✓ Executable is valid"
|
||||
|
||||
test-windows:
|
||||
name: Test Windows
|
||||
@@ -231,7 +206,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
- name: Setup Node.js v22
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -242,8 +217,8 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/AppData/Local/electron/Cache
|
||||
key: ${{ runner.os }}-electron-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-electron-
|
||||
key: ${{ runner.os }}-electron-v2-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-electron-v2-
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
@@ -255,41 +230,35 @@ jobs:
|
||||
[ "$i" = "3" ] && exit 1
|
||||
done
|
||||
|
||||
- name: Download IPFS
|
||||
run: node electron/download-ipfs
|
||||
|
||||
- name: Build React App
|
||||
shell: bash
|
||||
run: yarn build
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
- name: Build Electron App
|
||||
run: yarn electron:build:windows
|
||||
timeout-minutes: 30
|
||||
- name: Build Preload Script
|
||||
shell: bash
|
||||
run: yarn build:preload
|
||||
|
||||
- name: Smoke Test
|
||||
- name: Verify build outputs
|
||||
shell: bash
|
||||
run: |
|
||||
# Try to find the unpacked executable first as it's easiest to run
|
||||
if [ -d "dist/win-unpacked" ]; then
|
||||
echo "Testing unpacked exe..."
|
||||
# Run with timeout - app starts IPFS so won't exit on its own
|
||||
timeout 15s ./dist/win-unpacked/5chan.exe &
|
||||
APP_PID=$!
|
||||
sleep 8
|
||||
# Check if process started successfully
|
||||
if kill -0 $APP_PID 2>/dev/null; then
|
||||
echo "✓ App started successfully"
|
||||
taskkill //F //PID $APP_PID 2>/dev/null || true
|
||||
# Kill any IPFS processes
|
||||
taskkill //F //IM ipfs.exe 2>/dev/null || true
|
||||
exit 0
|
||||
else
|
||||
echo "✗ App failed to start"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No unpacked directory found"
|
||||
ls -R dist
|
||||
exit 1
|
||||
fi
|
||||
ls -la build/ || exit 1
|
||||
ls -la build/electron/preload.cjs || exit 1
|
||||
ls -la build/index.html || exit 1
|
||||
|
||||
- name: Package Electron App
|
||||
shell: bash
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
set -o pipefail
|
||||
yarn electron-forge package 2>&1 | tee forge-output.log
|
||||
ls -la out/
|
||||
|
||||
- name: Verify Executable
|
||||
shell: bash
|
||||
run: |
|
||||
ls -la out/
|
||||
EXE=$(node scripts/find-forge-executable.js)
|
||||
echo "Found executable: $EXE"
|
||||
test -f "$EXE" && test -x "$EXE" && echo "✓ Executable is valid"
|
||||
|
||||
+7
-9
@@ -54,9 +54,6 @@ web_modules/
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Generated files
|
||||
src/generated/asset-manifest.ts
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
@@ -99,6 +96,11 @@ out
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Electron Forge output
|
||||
squashfs-root
|
||||
|
||||
# Vite PWA dev output
|
||||
dev-dist
|
||||
|
||||
# Gatsby files
|
||||
@@ -149,7 +151,7 @@ dev-dist
|
||||
/coverage
|
||||
|
||||
# production
|
||||
# (using dist, already ignored above)
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@@ -163,8 +165,4 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.vscode
|
||||
.cursor
|
||||
.playwright-mcp
|
||||
|
||||
.specstory
|
||||
.cursorindexingignore
|
||||
.cursor
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Project Overview
|
||||
|
||||
5chan is a serverless, adminless, decentralized 4chan alternative built on the Plebbit protocol.
|
||||
5chan is a serverless, adminless, decentralized 4chan alternative built on the Bitsocial protocol.
|
||||
|
||||
## Stack
|
||||
|
||||
@@ -31,8 +31,9 @@ yarn electron # Run Electron app
|
||||
## Code Style
|
||||
|
||||
- TypeScript strict mode
|
||||
- Prettier for formatting (runs on pre-commit)
|
||||
- **DRY principle**: Never repeat UI elements across views—extract them into reusable components in `src/components/`. Same applies to logic—extract into custom hooks in `src/hooks/`
|
||||
- **oxfmt** for formatting (runs on pre-commit via husky; recommend setting up AI hooks too)
|
||||
- **oxlint** for linting, **tsgo** for type-checking
|
||||
- **DRY principle**: Always follow the DRY principle when possible. Never repeat UI elements across views—extract them into reusable components in `src/components/`. Same applies to logic—extract into custom hooks in `src/hooks/`.
|
||||
|
||||
## React Patterns (Critical)
|
||||
|
||||
@@ -106,6 +107,59 @@ Discover and install skills from the open agent skills ecosystem.
|
||||
npx skills add https://github.com/vercel-labs/skills --skill find-skills
|
||||
```
|
||||
|
||||
## AI Agent Hooks (Recommended)
|
||||
|
||||
If you're using an AI coding assistant (Cursor, Claude Code, Codex, etc.), set up hooks to automatically enforce code quality. Most modern AI agents support lifecycle hooks.
|
||||
|
||||
### Recommended Hooks
|
||||
|
||||
Set up these hooks for this project:
|
||||
|
||||
| Hook | Command | Purpose |
|
||||
|------|---------|---------|
|
||||
| `afterFileEdit` | `npx oxfmt <file>` | Auto-format files after AI edits |
|
||||
| `stop` | `yarn build && yarn lint && yarn type-check && (yarn audit || true)` | Build, verify code, and check security when agent finishes. Note: `yarn audit` returns non-zero on vulnerabilities, so `|| true` makes it informational only |
|
||||
|
||||
### Why Use Hooks
|
||||
|
||||
- **Consistent formatting** — Every file follows the same style
|
||||
- **Catch build errors** — `yarn build` catches compilation errors that would break production
|
||||
- **Catch issues early** — Lint and type errors are caught before commit/CI
|
||||
- **Security awareness** — `yarn audit` flags known vulnerabilities in dependencies
|
||||
- **Less manual work** — No need to run `yarn build`, `yarn lint`, `yarn type-check`, `yarn audit` manually
|
||||
|
||||
### Example Hook Scripts
|
||||
|
||||
**Format hook** (runs after each file edit):
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Auto-format JS/TS files after AI edits
|
||||
# Hook receives JSON via stdin with file_path
|
||||
|
||||
input=$(cat)
|
||||
file_path=$(echo "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:.*"\([^"]*\)"/\1/')
|
||||
|
||||
case "$file_path" in
|
||||
*.js|*.ts|*.tsx|*.mjs) npx oxfmt "$file_path" 2>/dev/null ;;
|
||||
esac
|
||||
exit 0
|
||||
```
|
||||
|
||||
**Verify hook** (runs when agent finishes):
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Run build, lint, type-check, and security audit when agent finishes
|
||||
|
||||
cat > /dev/null # consume stdin
|
||||
echo "=== yarn build ===" && yarn build
|
||||
echo "=== yarn lint ===" && yarn lint
|
||||
echo "=== yarn type-check ===" && yarn type-check
|
||||
echo "=== yarn audit ===" && (yarn audit || true) # || true makes audit informational (non-fatal)
|
||||
exit 0
|
||||
```
|
||||
|
||||
Consult your AI tool's documentation for how to configure hooks (e.g., `hooks.json` for Cursor/Claude Code).
|
||||
|
||||
## Recommended MCP Servers
|
||||
|
||||
### GitHub MCP
|
||||
@@ -169,19 +223,64 @@ node scripts/update-translations.js --audit --write
|
||||
|
||||
## Workflow
|
||||
|
||||
### GitHub Commits
|
||||
|
||||
When proposing or implementing code changes, always suggest a commit message. Format:
|
||||
|
||||
- **Title**: Use [Conventional Commits](https://www.conventionalcommits.org/) style. Use `perf` for performance optimizations (not `fix`). Keep it short. **MUST be wrapped in backticks.**
|
||||
- **Description**: Optional. 2-3 informal sentences describing the solution (not the problem). Concise, technical, no bullet points. Use backticks for code references.
|
||||
|
||||
Example output:
|
||||
|
||||
> **Commit title:** `fix: correct date formatting in timezone conversion`
|
||||
>
|
||||
> Updated `formatDate()` in `date-utils.ts` to properly handle timezone offsets.
|
||||
|
||||
### GitHub Issues
|
||||
|
||||
When proposing or implementing code changes, always suggest a GitHub issue title and description. Format:
|
||||
When proposing or implementing code changes, always suggest a GitHub issue to track the problem. Format:
|
||||
|
||||
- **Title**: Use [Conventional Commits](https://www.conventionalcommits.org/) style (e.g., `fix: ...`, `feat: ...`, `perf: ...`, `refactor: ...`, `docs: ...`, `chore: ...`)
|
||||
- **Description**: 2-3 informal sentences describing the problem (not the solution). Write as if the issue hasn't been fixed yet. Use markdown.
|
||||
- **Title**: As short as possible. **MUST be wrapped in backticks.**
|
||||
- **Description**: 2-3 informal sentences describing the problem (not the solution). Write as if the issue hasn't been fixed yet. Use backticks for code references.
|
||||
|
||||
Use the `perf` type for performance optimizations (not `fix`).
|
||||
Example output:
|
||||
|
||||
> **GitHub issue:**
|
||||
> - **Title:** `Date formatting displays incorrect timezone`
|
||||
> - **Description:** Comment timestamps show incorrect timezones when users view posts from different regions. The `formatDate()` function doesn't account for user's local timezone settings.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
When stuck on a bug or issue, search the web for solutions. Developer communities often have recent fixes or workarounds that aren't in training data.
|
||||
|
||||
## Dependency Management
|
||||
|
||||
### Pin All Package Versions (No Carets)
|
||||
|
||||
When adding or updating npm packages, **always use exact version numbers**—never use carets (`^`) or tildes (`~`).
|
||||
|
||||
```bash
|
||||
# ✅ Correct
|
||||
yarn add lodash@4.17.21
|
||||
|
||||
# ❌ Wrong (will add caret by default)
|
||||
yarn add lodash
|
||||
```
|
||||
|
||||
**Why pin versions:**
|
||||
|
||||
- **Supply chain security**: A compromised package could push a malicious minor/patch update. With carets, running `yarn upgrade` or regenerating `yarn.lock` would auto-install it.
|
||||
- **Reproducibility**: Guarantees identical dependencies across all environments.
|
||||
- **Defense in depth**: While `yarn.lock` pins versions in practice, explicit pinning in `package.json` protects against lockfile regeneration and makes the intended version auditable.
|
||||
|
||||
**When upgrading packages:**
|
||||
|
||||
1. Specify the exact version: `yarn add package@1.2.3`
|
||||
2. Review the changelog for breaking changes or security notes
|
||||
3. Test the upgrade before committing
|
||||
|
||||
**Note:** This applies to both `dependencies` and `devDependencies`. There are no exceptions—the convenience of auto-updates doesn't justify the security risk.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Never commit secrets or API keys
|
||||
@@ -189,4 +288,3 @@ When stuck on a bug or issue, search the web for solutions. Developer communitie
|
||||
- Keep components focused—split large components
|
||||
- Add comments for complex/unclear code (especially custom functions in this FOSS project with many contributors). Skip comments for obvious code
|
||||
- Test on mobile viewport (this is a responsive app)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Currently, directory assignments are temporarily handpicked by developers throug
|
||||
|
||||
### How It Works
|
||||
|
||||
- **Current System**: Developers manually curate directory assignments by reviewing pull requests to the [5chan-multisub.json](https://github.com/bitsocialhq/lists/blob/master/5chan-multisub.json) file.
|
||||
- **Current System**: Developers manually curate directory assignments by reviewing pull requests to the [5chan-directories.json](https://github.com/bitsocialhq/lists/blob/master/5chan-directories.json) file.
|
||||
|
||||
- **Future System**: Directory board assignments will be determined through gasless voting using pubsub. Community members will vote on which board should be assigned to each directory, and the highest-voted board will automatically become the directory board. This creates a competitive marketplace where board quality and community engagement determine directory placement.
|
||||
|
||||
@@ -72,7 +72,7 @@ To have your board appear in a directory on the 5chan homepage:
|
||||
- Relevant to the directory category
|
||||
- **99% uptime** (since a board acts like its own server—it's a P2P node)
|
||||
|
||||
2. Open a pull request on GitHub by editing the [5chan-multisub.json](https://github.com/bitsocialhq/lists/blob/master/5chan-multisub.json) file
|
||||
2. Open a pull request on GitHub by editing the [5chan-directories.json](https://github.com/bitsocialhq/lists/blob/master/5chan-directories.json) file
|
||||
|
||||
3. Add your board's entry with:
|
||||
- Title: in the format `/directoryCode/ - Title`, e.g. "/biz/ - Business & Finance";
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const packageJson = require('../package.json');
|
||||
const rootPath = path.resolve(__dirname, '..');
|
||||
const distFolderPath = path.resolve(rootPath, 'dist');
|
||||
|
||||
function addPortableToPortableExecutableFileName() {
|
||||
const files = fs.readdirSync(distFolderPath);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.exe') && !file.match('Setup')) {
|
||||
const filePath = path.resolve(distFolderPath, file);
|
||||
const renamedFilePath = path.resolve(distFolderPath, file.replace('5chan', '5chan Portable'));
|
||||
fs.moveSync(filePath, renamedFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createHtmlArchive() {
|
||||
if (process.platform !== 'linux') {
|
||||
return;
|
||||
}
|
||||
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`);
|
||||
// Vite outputs to 'dist', not 'build' (CRA default)
|
||||
const inputFolder = path.resolve(rootPath, 'dist');
|
||||
try {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async function afterAllArtifactBuild(buildResult) {
|
||||
addPortableToPortableExecutableFileName();
|
||||
createHtmlArchive();
|
||||
};
|
||||
+40
-47
@@ -17,11 +17,28 @@ const ipfsClientLinuxPath = path.join(ipfsClientsPath, 'linux');
|
||||
// const ipfsClientMacUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-darwin-amd64`
|
||||
// const ipfsClientLinuxUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-linux-amd64`
|
||||
|
||||
// official kubo download links https://docs.ipfs.tech/install/command-line/#install-official-binary-distributions
|
||||
const ipfsClientVersion = '0.32.1';
|
||||
const ipfsClientWindowsUrl = `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_windows-amd64.zip`;
|
||||
const ipfsClientMacUrl = `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_darwin-amd64.tar.gz`;
|
||||
const ipfsClientLinuxUrl = `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_linux-amd64.tar.gz`;
|
||||
// NOTE: Keep this version in sync with the kubo version in package.json to avoid repo version mismatches
|
||||
const ipfsClientVersion = '0.39.0';
|
||||
|
||||
// Resolve desired build arch: allow overriding via env (so cross-arch builds pick correct binary)
|
||||
const resolveBuildArch = () => {
|
||||
const envArch = process.env.BUILD_ARCH;
|
||||
if (envArch === 'arm64' || envArch === 'x64') return envArch;
|
||||
// fallback to host arch
|
||||
if (process.arch === 'arm64') return 'arm64';
|
||||
return 'x64';
|
||||
};
|
||||
|
||||
const toKuboArch = (arch) => (arch === 'arm64' ? 'arm64' : 'amd64');
|
||||
|
||||
const getKuboUrl = (platform) => {
|
||||
const arch = toKuboArch(resolveBuildArch());
|
||||
if (platform === 'win32') return `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_windows-${arch}.zip`;
|
||||
if (platform === 'darwin') return `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_darwin-${arch}.tar.gz`;
|
||||
if (platform === 'linux') return `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_linux-${arch}.tar.gz`;
|
||||
// default to linux
|
||||
return `https://dist.ipfs.io/kubo/v${ipfsClientVersion}/kubo_v${ipfsClientVersion}_linux-${arch}.tar.gz`;
|
||||
};
|
||||
|
||||
const downloadWithProgress = (url) =>
|
||||
new Promise((resolve, reject) => {
|
||||
@@ -79,25 +96,6 @@ const downloadWithRetry = async (url, retries = 3) => {
|
||||
}
|
||||
};
|
||||
|
||||
// plebbit kubo downloads dont need to be extracted
|
||||
const download = async (url, destinationPath) => {
|
||||
let binName = 'ipfs';
|
||||
if (destinationPath.endsWith('win')) {
|
||||
binName += '.exe';
|
||||
}
|
||||
const binPath = path.join(destinationPath, binName);
|
||||
// already downloaded, don't download again
|
||||
if (fs.pathExistsSync(binPath)) {
|
||||
return;
|
||||
}
|
||||
const split = url.split('/');
|
||||
const fileName = split[split.length - 1];
|
||||
const downloadPath = path.join(destinationPath, fileName);
|
||||
const file = await downloadWithRetry(url);
|
||||
fs.ensureDirSync(destinationPath);
|
||||
await fs.writeFile(binPath, file);
|
||||
};
|
||||
|
||||
// official kubo downloads need to be extracted
|
||||
const downloadAndExtract = async (url, destinationPath) => {
|
||||
let binName = 'ipfs';
|
||||
@@ -111,14 +109,14 @@ const downloadAndExtract = async (url, destinationPath) => {
|
||||
console.log(`Downloading IPFS client from ${url} to ${destinationPath}`);
|
||||
const split = url.split('/');
|
||||
const fileName = split[split.length - 1];
|
||||
const downloadPath = path.join(destinationPath, fileName);
|
||||
const archivePath = path.join(destinationPath, fileName);
|
||||
const file = await downloadWithRetry(url);
|
||||
fs.ensureDirSync(destinationPath);
|
||||
await fs.writeFile(downloadPath, file);
|
||||
console.log(`Downloaded archive to ${downloadPath}`);
|
||||
console.log(`Extracting ${downloadPath} to ${destinationPath}`);
|
||||
await fs.writeFile(archivePath, file);
|
||||
console.log(`Downloaded archive to ${archivePath}`);
|
||||
console.log(`Extracting ${archivePath} to ${destinationPath}`);
|
||||
try {
|
||||
await decompress(downloadPath, destinationPath);
|
||||
await decompress(archivePath, destinationPath);
|
||||
console.log('Decompression complete');
|
||||
} catch (err) {
|
||||
console.error('Error during decompression:', err);
|
||||
@@ -130,31 +128,26 @@ const downloadAndExtract = async (url, destinationPath) => {
|
||||
fs.moveSync(extractedBinPath, binPath);
|
||||
console.log('Binary moved');
|
||||
console.log('Cleaning up temporary files');
|
||||
fs.removeSync(downloadPath);
|
||||
fs.removeSync(archivePath);
|
||||
console.log('Cleanup complete');
|
||||
};
|
||||
|
||||
export const downloadIpfsClients = async () => {
|
||||
const platform = process.platform;
|
||||
console.log(`Starting IPFS client download for platform: ${platform}`);
|
||||
switch (platform) {
|
||||
case 'win32':
|
||||
await downloadAndExtract(ipfsClientWindowsUrl, ipfsClientWindowsPath);
|
||||
break;
|
||||
case 'darwin':
|
||||
await downloadAndExtract(ipfsClientMacUrl, ipfsClientMacPath);
|
||||
break;
|
||||
case 'linux':
|
||||
await downloadAndExtract(ipfsClientLinuxUrl, ipfsClientLinuxPath);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown platform: ${platform}, downloading all IPFS clients`);
|
||||
await downloadAndExtract(ipfsClientWindowsUrl, ipfsClientWindowsPath);
|
||||
await downloadAndExtract(ipfsClientMacUrl, ipfsClientMacPath);
|
||||
await downloadAndExtract(ipfsClientLinuxUrl, ipfsClientLinuxPath);
|
||||
console.log(`Starting IPFS client download for platform: ${platform}, targetArch: ${resolveBuildArch()}`);
|
||||
const url = getKuboUrl(platform);
|
||||
if (platform === 'win32') {
|
||||
await downloadAndExtract(url, ipfsClientWindowsPath);
|
||||
} else if (platform === 'darwin') {
|
||||
await downloadAndExtract(url, ipfsClientMacPath);
|
||||
} else if (platform === 'linux') {
|
||||
await downloadAndExtract(url, ipfsClientLinuxPath);
|
||||
} else {
|
||||
console.warn(`Unknown platform: ${platform}, defaulting to linux path`);
|
||||
await downloadAndExtract(url, ipfsClientLinuxPath);
|
||||
}
|
||||
};
|
||||
|
||||
export default async (context) => {
|
||||
export default async (_context) => {
|
||||
await downloadIpfsClients();
|
||||
};
|
||||
|
||||
+12
-15
@@ -11,7 +11,7 @@ fi
|
||||
node electron/download-ipfs || { echo "Error: failed script 'node electron/download-ipfs'" ; exit 1; }
|
||||
|
||||
dockerfile='
|
||||
FROM electronuserland/builder:16
|
||||
FROM node:22
|
||||
|
||||
# install node_modules
|
||||
WORKDIR /usr/src/5chan
|
||||
@@ -19,44 +19,41 @@ COPY ./package.json .
|
||||
COPY ./yarn.lock .
|
||||
RUN yarn
|
||||
|
||||
# build native dependencies like sqlite3
|
||||
RUN electron-builder install-app-deps
|
||||
|
||||
# copy source files
|
||||
# copy source files and configs
|
||||
COPY ./bin ./bin
|
||||
COPY ./electron ./electron
|
||||
COPY ./src ./src
|
||||
COPY ./public ./public
|
||||
|
||||
# required or yarn build fails
|
||||
COPY ./.eslintrc.json ./.eslintrc.json
|
||||
COPY ./.prettierrc ./.prettierrc
|
||||
COPY ./forge.config.js ./forge.config.js
|
||||
COPY ./vite.config.js ./vite.config.js
|
||||
COPY ./tsconfig.json ./tsconfig.json
|
||||
COPY ./index.html ./index.html
|
||||
|
||||
# react build
|
||||
RUN yarn build
|
||||
'
|
||||
|
||||
# build electron-builder docker image
|
||||
# build electron-forge docker image
|
||||
# temporary .dockerignore to save build time
|
||||
echo $'node_modules\ndist' > .dockerignore
|
||||
echo "$dockerfile" | sudo docker build \
|
||||
. \
|
||||
--tag 5chan-electron-builder \
|
||||
--tag 5chan-electron-forge \
|
||||
--file -
|
||||
rm .dockerignore
|
||||
|
||||
# build linux binary
|
||||
sudo docker run \
|
||||
--name 5chan-electron-builder \
|
||||
--name 5chan-electron-forge \
|
||||
--volume "$root_path"/dist:/usr/src/5chan/dist \
|
||||
--rm \
|
||||
5chan-electron-builder \
|
||||
5chan-electron-forge \
|
||||
yarn electron:build:linux
|
||||
|
||||
# build windows binary
|
||||
sudo docker run \
|
||||
--name 5chan-electron-builder \
|
||||
--name 5chan-electron-forge \
|
||||
--volume "$root_path"/dist:/usr/src/5chan/dist \
|
||||
--rm \
|
||||
5chan-electron-builder \
|
||||
5chan-electron-forge \
|
||||
yarn electron:build:windows
|
||||
|
||||
+12
-3
@@ -94,7 +94,7 @@ const createMainWindow = () => {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
devTools: true, // TODO: change to isDev when no bugs left
|
||||
preload: path.join(dirname, '../dist/electron/preload.cjs'),
|
||||
preload: path.join(dirname, '../build/electron/preload.cjs'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -140,7 +140,7 @@ const createMainWindow = () => {
|
||||
callback({ responseHeaders: details.responseHeaders });
|
||||
});
|
||||
|
||||
const startURL = isDev ? 'http://localhost:3000' : `file://${path.join(dirname, '../dist/index.html')}`;
|
||||
const startURL = isDev ? 'http://localhost:3000' : `file://${path.join(dirname, '../build/index.html')}`;
|
||||
|
||||
mainWindow.loadURL(startURL);
|
||||
|
||||
@@ -244,7 +244,7 @@ const createMainWindow = () => {
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
// tray
|
||||
const trayIconPath = path.join(dirname, '..', isDev ? 'public' : 'dist', 'electron-tray-icon.png');
|
||||
const trayIconPath = path.join(dirname, '..', isDev ? 'public' : 'build', 'electron-tray-icon.png');
|
||||
const tray = new Tray(trayIconPath);
|
||||
tray.setToolTip('5chan');
|
||||
const trayMenu = Menu.buildFromTemplate([
|
||||
@@ -318,6 +318,15 @@ const createMainWindow = () => {
|
||||
};
|
||||
|
||||
app.whenReady().then(() => {
|
||||
// Set app name and dock icon for development mode on macOS
|
||||
if (process.platform === 'darwin') {
|
||||
app.setName('seedit');
|
||||
if (app.dock) {
|
||||
const iconPath = path.join(dirname, '..', isDev ? 'public' : 'build', 'icon.png');
|
||||
app.dock.setIcon(iconPath);
|
||||
}
|
||||
}
|
||||
|
||||
createMainWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
|
||||
+85
-36
@@ -6,43 +6,92 @@ import ps from 'node:process';
|
||||
import proxyServer from './proxy-server.js';
|
||||
import tcpPortUsed from 'tcp-port-used';
|
||||
import EnvPaths from 'env-paths';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { fileURLToPath, pathToFileURL } from 'url';
|
||||
const dirname = path.join(path.dirname(fileURLToPath(import.meta.url)));
|
||||
const envPaths = EnvPaths('plebbit', { suffix: false });
|
||||
|
||||
// Get platform-specific binary name
|
||||
const getIpfsBinaryName = () => (process.platform === 'win32' ? 'ipfs.exe' : 'ipfs');
|
||||
|
||||
// Get platform subdirectory name for bin/ folder
|
||||
const getPlatformDir = () => {
|
||||
if (process.platform === 'win32') return 'win';
|
||||
if (process.platform === 'darwin') return 'mac';
|
||||
return 'linux';
|
||||
};
|
||||
|
||||
// Resolve kubo binary path
|
||||
const getKuboPath = async () => {
|
||||
if (isDev) {
|
||||
// In dev, use kubo from node_modules
|
||||
const { path: getKuboBinaryPath } = await import('kubo');
|
||||
return getKuboBinaryPath();
|
||||
} else {
|
||||
// In production, the binary is downloaded to bin/<platform>/ipfs by generateAssets hook
|
||||
// With asar: false, files are at resources/app/ instead of resources/app.asar.unpacked
|
||||
const appPath = process.resourcesPath;
|
||||
const binaryName = getIpfsBinaryName();
|
||||
const platformDir = getPlatformDir();
|
||||
|
||||
// Try the bin/ directory first (where generateAssets downloads binaries)
|
||||
const binDirPath = path.join(appPath, 'app', 'bin', platformDir, binaryName);
|
||||
if (fs.existsSync(binDirPath)) {
|
||||
return binDirPath;
|
||||
}
|
||||
|
||||
// Fallback: try app.asar.unpacked for ASAR builds (if we ever re-enable ASAR)
|
||||
const unpackedPath = path.join(appPath, 'app.asar.unpacked');
|
||||
const kuboModulePath = path.join(unpackedPath, 'node_modules', 'kubo');
|
||||
|
||||
// Try to import kubo from unpacked location
|
||||
try {
|
||||
const kuboUrl = pathToFileURL(path.resolve(kuboModulePath)).href;
|
||||
const kuboModule = await import(kuboUrl);
|
||||
const { path: getKuboBinaryPath } = kuboModule;
|
||||
return getKuboBinaryPath();
|
||||
} catch (err) {
|
||||
// Fallback: try to find the binary directly in kubo module
|
||||
const kuboBinPath = path.join(kuboModulePath, 'kubo', binaryName);
|
||||
if (fs.existsSync(kuboBinPath)) {
|
||||
return kuboBinPath;
|
||||
}
|
||||
|
||||
// Last resort: check in resources/app/node_modules/kubo for non-ASAR builds
|
||||
const appModulePath = path.join(appPath, 'app', 'node_modules', 'kubo');
|
||||
const appKuboBinPath = path.join(appModulePath, 'kubo', binaryName);
|
||||
if (fs.existsSync(appKuboBinPath)) {
|
||||
return appKuboBinPath;
|
||||
}
|
||||
|
||||
throw new Error(`Could not find kubo binary. Checked: ${binDirPath}, ${kuboBinPath}, ${appKuboBinPath}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// use this custom function instead of spawnSync for better logging
|
||||
// also spawnSync might have been causing crash on start on windows
|
||||
const spawnAsync = (...args) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const spawedProcess = spawn(...args);
|
||||
spawedProcess.on('exit', (exitCode, signal) => {
|
||||
const spawnedProcess = spawn(...args);
|
||||
spawnedProcess.on('exit', (exitCode, signal) => {
|
||||
if (exitCode === 0) resolve();
|
||||
else reject(Error(`spawnAsync process '${spawedProcess.pid}' exited with code '${exitCode}' signal '${signal}'`));
|
||||
else reject(Error(`spawnAsync process '${spawnedProcess.pid}' exited with code '${exitCode}' signal '${signal}'`));
|
||||
});
|
||||
spawedProcess.stderr.on('data', (data) => console.error(data.toString()));
|
||||
spawedProcess.stdin.on('data', (data) => console.log(data.toString()));
|
||||
spawedProcess.stdout.on('data', (data) => console.log(data.toString()));
|
||||
spawedProcess.on('error', (data) => console.error(data.toString()));
|
||||
// Always surface errors from short-lived commands
|
||||
spawnedProcess.stderr.on('data', (data) => console.error(data.toString()));
|
||||
// Short-lived command stdout can be useful in dev, but is noisy in prod
|
||||
if (isDev) {
|
||||
spawnedProcess.stdout.on('data', (data) => console.log(data.toString()));
|
||||
} else {
|
||||
// Drain to avoid backpressure without logging
|
||||
spawnedProcess.stdout.on('data', () => {});
|
||||
}
|
||||
spawnedProcess.on('error', (data) => console.error(data.toString?.() || String(data)));
|
||||
});
|
||||
|
||||
const startIpfs = async () => {
|
||||
const ipfsFileName = process.platform == 'win32' ? 'ipfs.exe' : 'ipfs';
|
||||
let ipfsPath = path.join(process.resourcesPath, 'bin', ipfsFileName);
|
||||
let ipfsDataPath = path.join(envPaths.data, 'ipfs');
|
||||
|
||||
// test launching the ipfs binary in dev mode
|
||||
// they must be downloaded first using `yarn electron:build`
|
||||
if (isDev) {
|
||||
let binFolderName = 'win';
|
||||
if (process.platform === 'linux') {
|
||||
binFolderName = 'linux';
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
binFolderName = 'mac';
|
||||
}
|
||||
ipfsPath = path.join(dirname, '..', 'bin', binFolderName, ipfsFileName);
|
||||
ipfsDataPath = path.join(dirname, '..', '.plebbit', 'ipfs');
|
||||
}
|
||||
const ipfsPath = await getKuboPath();
|
||||
const ipfsDataPath = isDev ? path.join(dirname, '..', '.plebbit', 'ipfs') : path.join(envPaths.data, 'ipfs');
|
||||
|
||||
if (!fs.existsSync(ipfsPath)) {
|
||||
throw Error(`ipfs binary '${ipfsPath}' doesn't exist`);
|
||||
@@ -51,16 +100,17 @@ const startIpfs = async () => {
|
||||
console.log({ ipfsPath, ipfsDataPath });
|
||||
|
||||
fs.ensureDirSync(ipfsDataPath);
|
||||
const env = { IPFS_PATH: ipfsDataPath };
|
||||
// Reduce IPFS daemon log verbosity in production to avoid UI lag from excessive logging
|
||||
const env = { ...process.env, IPFS_PATH: ipfsDataPath, ...(isDev ? {} : { GOLOG_LOG_LEVEL: 'error' }) };
|
||||
// init ipfs client on first launch
|
||||
try {
|
||||
await spawnAsync(ipfsPath, ['init'], { env, hideWindows: true });
|
||||
} catch (e) {}
|
||||
} catch {}
|
||||
|
||||
// make sure repo is migrated
|
||||
try {
|
||||
await spawnAsync(ipfsPath, ['repo', 'migrate'], { env, hideWindows: true });
|
||||
} catch (e) {}
|
||||
} catch {}
|
||||
|
||||
// dont use 8080 port because it's too common
|
||||
await spawnAsync(ipfsPath, ['config', '--json', 'Addresses.Gateway', '"/ip4/127.0.0.1/tcp/6473"'], {
|
||||
@@ -83,17 +133,16 @@ const startIpfs = async () => {
|
||||
let lastError;
|
||||
ipfsProcess.stderr.on('data', (data) => {
|
||||
lastError = data.toString();
|
||||
console.error(data.toString());
|
||||
if (isDev) console.error(lastError);
|
||||
});
|
||||
ipfsProcess.stdin.on('data', (data) => console.log(data.toString()));
|
||||
ipfsProcess.stdout.on('data', (data) => {
|
||||
data = data.toString();
|
||||
console.log(data);
|
||||
if (data.includes('Daemon is ready')) {
|
||||
ipfsProcess.stdout.on('data', (chunk) => {
|
||||
const text = chunk.toString();
|
||||
if (isDev) console.log(text);
|
||||
if (text.includes('Daemon is ready')) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
ipfsProcess.on('error', (data) => console.error(data.toString()));
|
||||
ipfsProcess.on('error', (err) => console.error(err?.toString?.() || String(err)));
|
||||
ipfsProcess.on('exit', () => {
|
||||
console.error(`ipfs process with pid ${ipfsProcess.pid} exited`);
|
||||
reject(Error(lastError));
|
||||
@@ -135,7 +184,7 @@ const startIpfsAutoRestart = async () => {
|
||||
try {
|
||||
// try to run exported onError callback, can be undefined
|
||||
DefaultExport.onError(e)?.catch?.(console.log);
|
||||
} catch (e) {}
|
||||
} catch {}
|
||||
}
|
||||
pendingStart = false;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ export default defineConfig({
|
||||
fileName: () => 'preload.cjs',
|
||||
formats: ['cjs'],
|
||||
},
|
||||
outDir: resolve(__dirname, '../dist/electron'),
|
||||
outDir: resolve(__dirname, '../build/electron'),
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
external: ['electron'],
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { downloadIpfsClients } from './electron/before-pack.js';
|
||||
|
||||
const config = {
|
||||
packagerConfig: {
|
||||
name: '5chan',
|
||||
executableName: '5chan',
|
||||
appBundleId: '5chan.desktop',
|
||||
icon: './public/icon', // electron-forge adds the correct extension per platform
|
||||
|
||||
// NOTE: asar is disabled because of a bug where electron-packager silently fails
|
||||
// during asar creation with 5chan's large node_modules. The app works fine without it.
|
||||
// TODO: investigate and fix the asar creation issue
|
||||
asar: false,
|
||||
|
||||
// Exclude unnecessary files from the package
|
||||
ignore: [
|
||||
/^\/src$/,
|
||||
/^\/public$/,
|
||||
/^\/android$/,
|
||||
/^\/\.github$/,
|
||||
/^\/scripts$/,
|
||||
/^\/\.git/,
|
||||
/^\/\.plebbit$/,
|
||||
/^\/out$/,
|
||||
/^\/dist$/,
|
||||
/^\/squashfs-root$/,
|
||||
/\.map$/,
|
||||
/\.md$/,
|
||||
/\.ts$/,
|
||||
/tsconfig\.json$/,
|
||||
/\.oxfmtrc/,
|
||||
/oxlintrc/,
|
||||
/vite\.config/,
|
||||
/forge\.config/,
|
||||
/capacitor\.config/,
|
||||
/\.env$/,
|
||||
/\.DS_Store$/,
|
||||
/yarn\.lock$/,
|
||||
// Exclude build-time scripts from the package
|
||||
/electron\/before-pack\.js/,
|
||||
// kubo npm package creates symlinks that break build - exclude its bin dir
|
||||
// (we download our own kubo binary in generateAssets hook)
|
||||
/node_modules\/kubo\/bin/,
|
||||
// Exclude .bin directories anywhere in node_modules (contain escaping symlinks)
|
||||
/node_modules\/.*\/\.bin/,
|
||||
/node_modules\/\.bin/,
|
||||
/node_modules\/\.cache/,
|
||||
],
|
||||
},
|
||||
|
||||
rebuildConfig: {
|
||||
force: true,
|
||||
},
|
||||
|
||||
hooks: {
|
||||
// Download IPFS/Kubo binaries before packaging
|
||||
generateAssets: async () => {
|
||||
console.log('Downloading IPFS clients...');
|
||||
await downloadIpfsClients();
|
||||
console.log('IPFS clients downloaded.');
|
||||
},
|
||||
},
|
||||
|
||||
makers: [
|
||||
// macOS
|
||||
{
|
||||
name: '@electron-forge/maker-dmg',
|
||||
platforms: ['darwin'],
|
||||
config: {
|
||||
name: '5chan',
|
||||
icon: './public/icon.png',
|
||||
format: 'UDZO',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-zip',
|
||||
platforms: ['darwin'],
|
||||
},
|
||||
// Windows
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
platforms: ['win32'],
|
||||
config: {
|
||||
name: '5chan',
|
||||
setupIcon: './public/favicon.ico',
|
||||
},
|
||||
},
|
||||
// Linux
|
||||
{
|
||||
name: '@reforged/maker-appimage',
|
||||
platforms: ['linux'],
|
||||
config: {
|
||||
options: {
|
||||
name: '5chan',
|
||||
icon: './public/icon.png',
|
||||
categories: ['Network'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
+8
-1
@@ -18,7 +18,14 @@
|
||||
</script>
|
||||
<script>
|
||||
// Redirect non-hash URLs to hash URLs on initial load
|
||||
if (window.location.pathname && window.location.pathname !== '/' && window.location.pathname !== '/index.html' && !window.location.hash) {
|
||||
// Skip redirect for file:// protocol (Electron) or if pathname ends with index.html
|
||||
if (
|
||||
window.location.protocol !== 'file:' &&
|
||||
window.location.pathname &&
|
||||
window.location.pathname !== '/' &&
|
||||
!window.location.pathname.endsWith('/index.html') &&
|
||||
!window.location.hash
|
||||
) {
|
||||
window.location.replace('/#' + window.location.pathname + window.location.search);
|
||||
}
|
||||
</script>
|
||||
|
||||
+27
-50
@@ -11,7 +11,7 @@
|
||||
"@capacitor/status-bar": "7.0.1",
|
||||
"@capawesome/capacitor-android-edge-to-edge-support": "7.2.2",
|
||||
"@floating-ui/react": "0.26.1",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#60b9d18ebc647f76cdda8f36e2143192d7378658",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#df3cb6901f63054870dc127cd7565c98617c2a4f",
|
||||
"@react-spring/web": "9.7.5",
|
||||
"@types/node": "20.8.2",
|
||||
"@types/react": "18.2.25",
|
||||
@@ -28,7 +28,7 @@
|
||||
"i18next-browser-languagedetector": "7.1.0",
|
||||
"i18next-http-backend": "2.2.2",
|
||||
"json-stringify-pretty-compact": "4.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"lodash": "4.17.23",
|
||||
"memoizee": "0.4.15",
|
||||
"react": "19.1.2",
|
||||
"react-dom": "19.1.2",
|
||||
@@ -43,7 +43,8 @@
|
||||
"remark-supersub": "1.0.0",
|
||||
"tcp-port-used": "1.0.2",
|
||||
"typescript": "5.1.6",
|
||||
"zustand": "4.4.3"
|
||||
"zustand": "4.4.3",
|
||||
"kubo": "0.39.0"
|
||||
},
|
||||
"scripts": {
|
||||
"generate:assets": "node scripts/generate-asset-manifest.js",
|
||||
@@ -56,15 +57,20 @@
|
||||
"test": "vitest",
|
||||
"preview": "vite preview",
|
||||
"analyze-bundle": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true vite build && npx source-map-explorer 'dist/assets/*.js'",
|
||||
"electron": "yarn build:preload && cross-env ELECTRON_IS_DEV=1 yarn electron:before && cross-env ELECTRON_IS_DEV=1 electron .",
|
||||
"electron:no-delete-data": "yarn electron:before:download-ipfs && electron .",
|
||||
"electron": "yarn build:preload && yarn electron:before && electron .",
|
||||
"electron:no-delete-data": "yarn build:preload && electron .",
|
||||
"electron:start": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && yarn electron\"",
|
||||
"electron:start:no-delete-data": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && yarn electron:no-delete-data\"",
|
||||
"electron:build:linux": "yarn build && yarn build:preload && electron-builder build --publish never -l",
|
||||
"electron:build:windows": "yarn build && yarn build:preload && electron-builder build --publish never -w",
|
||||
"electron:build:mac": "yarn build && yarn build:preload && electron-builder build --publish never -m",
|
||||
"electron:before": "yarn electron-rebuild && yarn electron:before:download-ipfs && yarn electron:before:delete-data",
|
||||
"electron:before:download-ipfs": "node electron/download-ipfs",
|
||||
"electron:package": "yarn build && yarn build:preload && electron-forge package",
|
||||
"electron:build": "yarn build && yarn build:preload && electron-forge make",
|
||||
"electron:build:linux": "yarn build && yarn build:preload && electron-forge make --platform=linux",
|
||||
"electron:build:mac": "yarn build && yarn build:preload && electron-forge make --platform=darwin",
|
||||
"electron:build:windows": "yarn build && yarn build:preload && electron-forge make --platform=win32",
|
||||
"electron:build:linux:x64": "yarn build && yarn build:preload && electron-forge make --platform=linux --arch=x64",
|
||||
"electron:build:linux:arm64": "yarn build && yarn build:preload && electron-forge make --platform=linux --arch=arm64",
|
||||
"electron:build:mac:x64": "yarn build && yarn build:preload && electron-forge make --platform=darwin --arch=x64",
|
||||
"electron:build:mac:arm64": "yarn build && yarn build:preload && electron-forge make --platform=darwin --arch=arm64",
|
||||
"electron:before": "yarn electron:before:delete-data",
|
||||
"electron:before:delete-data": "rimraf .plebbit",
|
||||
"android:build:icons": "cordova-res android --skip-config --copy --resources /tmp/plebbit-react-android-icons --icon-source ./android/icons/icon.png --splash-source ./android/icons/splash.png --icon-foreground-source ./android/icons/icon-foreground.png --icon-background-source '#ffffee'",
|
||||
"lint": "oxlint src/**/*.{js,ts,tsx}",
|
||||
@@ -93,7 +99,6 @@
|
||||
"@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",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20251226.1",
|
||||
@@ -109,11 +114,17 @@
|
||||
"cz-conventional-changelog": "3.3.0",
|
||||
"decompress": "4.2.1",
|
||||
"electron": "36.9.5",
|
||||
"electron-builder": "24.13.2",
|
||||
"@electron-forge/cli": "7.6.0",
|
||||
"@electron-forge/maker-dmg": "7.6.0",
|
||||
"@electron-forge/maker-squirrel": "7.6.0",
|
||||
"@electron-forge/maker-zip": "7.6.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.6.0",
|
||||
"@reforged/maker-appimage": "5.1.1",
|
||||
"husky": "4.3.8",
|
||||
"isomorphic-fetch": "3.0.0",
|
||||
"jsdom": "27.3.0",
|
||||
"lint-staged": "12.3.8",
|
||||
"node-fetch": "3.3.2",
|
||||
"oxfmt": "0.20.0",
|
||||
"oxlint": "1.35.0",
|
||||
"progress": "2.0.3",
|
||||
@@ -143,46 +154,12 @@
|
||||
"preact": "10.27.3",
|
||||
"@remix-run/router": "1.23.2",
|
||||
"undici": "^7.18.2",
|
||||
"diff": "^8.0.3"
|
||||
"diff": "^8.0.3",
|
||||
"lodash": "4.17.23",
|
||||
"seroval": "1.4.1",
|
||||
"tar": "7.5.4"
|
||||
},
|
||||
"main": "electron/main.js",
|
||||
"build": {
|
||||
"appId": "5chan.desktop",
|
||||
"productName": "5chan",
|
||||
"beforePack": "electron/before-pack.js",
|
||||
"afterAllArtifactBuild": "electron/after-all-artifact-build.cjs",
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "bin/${os}",
|
||||
"to": "bin",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"electron/**/*",
|
||||
"package.json",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
"extends": null,
|
||||
"mac": {
|
||||
"target": "dmg",
|
||||
"category": "public.app-category.social-networking",
|
||||
"type": "distribution"
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"portable",
|
||||
"nsis"
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"target": "AppImage",
|
||||
"category": "Network"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{js,ts,tsx}": [
|
||||
"oxfmt --write"
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Find the packaged Electron executable built by Electron Forge.
|
||||
* This script locates the executable in the out/ directory structure.
|
||||
*/
|
||||
|
||||
import { readdirSync, statSync, existsSync, readFileSync } from 'fs';
|
||||
import { isAbsolute, join, resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = resolve(__filename, '..');
|
||||
|
||||
const platform = process.platform;
|
||||
const repoRoot = resolve(__dirname, '..');
|
||||
const packageJson = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf-8'));
|
||||
const appName = (packageJson.build?.productName || packageJson.name || '5chan').toLowerCase();
|
||||
|
||||
const resolveOutDir = (dir) => (isAbsolute(dir) ? dir : join(repoRoot, dir));
|
||||
|
||||
const envOutDir = process.env.ELECTRON_FORGE_OUT_DIR;
|
||||
|
||||
const candidateRoots = [
|
||||
envOutDir ? resolveOutDir(envOutDir) : null,
|
||||
join(repoRoot, 'out'),
|
||||
join(repoRoot, 'out', 'make'),
|
||||
join(repoRoot, '..', 'out'),
|
||||
join(repoRoot, '..', '..', 'out'),
|
||||
join(repoRoot, 'electron', 'out'),
|
||||
].filter(Boolean);
|
||||
|
||||
// Skip directories that contain helper binaries/app code, not the main executable
|
||||
// 'resources' contains the app bundle with IPFS binaries in bin/ - don't recurse there
|
||||
const skipDirs = new Set(['node_modules', '.git', 'bin', 'app', 'resources']);
|
||||
|
||||
function findExecutable(dir, platform) {
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
let fallback = null; // Track a valid candidate that doesn't match appName
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (skipDirs.has(entry.name)) continue;
|
||||
|
||||
if (platform === 'darwin' && entry.name.endsWith('.app')) {
|
||||
const appPath = fullPath;
|
||||
const exePath = join(appPath, 'Contents', 'MacOS', entry.name.replace('.app', ''));
|
||||
if (existsSync(exePath)) {
|
||||
const lowerAppName = entry.name.toLowerCase();
|
||||
if (lowerAppName.includes(appName)) {
|
||||
return exePath; // Exact match, return immediately
|
||||
}
|
||||
fallback = fallback || exePath; // Store as fallback, continue searching
|
||||
}
|
||||
}
|
||||
|
||||
const result = findExecutable(fullPath, platform);
|
||||
if (result) {
|
||||
const resultName = result.split('/').pop().toLowerCase();
|
||||
if (resultName.includes(appName)) {
|
||||
return result; // Exact match from recursion
|
||||
}
|
||||
fallback = fallback || result; // Store as fallback
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
// Check if it's an executable
|
||||
if (platform === 'win32') {
|
||||
const lowerName = entry.name.toLowerCase();
|
||||
if (lowerName.endsWith('.exe') && !lowerName.includes('electron') && !lowerName.includes('crashpad')) {
|
||||
if (lowerName.includes(appName)) {
|
||||
return fullPath; // Exact match, return immediately
|
||||
}
|
||||
fallback = fallback || fullPath; // Store as fallback, continue searching
|
||||
}
|
||||
} else if (platform === 'darwin') {
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isFile() && stat.mode & parseInt('111', 8)) {
|
||||
const lowerName = entry.name.toLowerCase();
|
||||
if (!lowerName.includes('helper') && !lowerName.includes('crashpad')) {
|
||||
if (lowerName.includes(appName)) {
|
||||
return fullPath; // Exact match, return immediately
|
||||
}
|
||||
fallback = fallback || fullPath; // Store as fallback, continue searching
|
||||
}
|
||||
}
|
||||
} else if (platform === 'linux') {
|
||||
// Linux executables (AppImage or unpacked)
|
||||
if (entry.name.endsWith('.AppImage')) {
|
||||
const lowerName = entry.name.toLowerCase();
|
||||
if (lowerName.includes(appName)) {
|
||||
return fullPath; // Exact match, return immediately
|
||||
}
|
||||
fallback = fallback || fullPath; // Store as fallback, continue searching
|
||||
}
|
||||
// Check for executable files (not .so libraries)
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isFile() && stat.mode & parseInt('111', 8) && !entry.name.includes('.so')) {
|
||||
// Skip helper binaries
|
||||
const lowerName = entry.name.toLowerCase();
|
||||
if (!lowerName.includes('chrome') && !lowerName.includes('crashpad')) {
|
||||
if (lowerName.includes(appName)) {
|
||||
return fullPath; // Exact match, return immediately
|
||||
}
|
||||
fallback = fallback || fullPath; // Store as fallback, continue searching
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fallback; // Return fallback candidate if no exact appName match found
|
||||
}
|
||||
|
||||
let executable = null;
|
||||
const checkedDirs = [];
|
||||
|
||||
for (const root of candidateRoots) {
|
||||
if (!existsSync(root)) {
|
||||
checkedDirs.push(`${root} (missing)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = findExecutable(root, platform);
|
||||
checkedDirs.push(root);
|
||||
if (result) {
|
||||
executable = result;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!executable) {
|
||||
console.error('Error: Could not find packaged executable.');
|
||||
console.error('Platform:', platform);
|
||||
console.error('Checked directories:', checkedDirs.join(', '));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(executable);
|
||||
+106
-22
@@ -1,34 +1,118 @@
|
||||
import {execSync} from 'child_process'
|
||||
import path from 'path'
|
||||
import {fileURLToPath} from 'url'
|
||||
import {readFileSync} from 'fs'
|
||||
import { execSync } from 'child_process';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readFileSync, readdirSync } from 'fs';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const dirname = path.join(path.dirname(fileURLToPath(import.meta.url)))
|
||||
const conventionalChangelog = path.join(dirname, '..', 'node_modules', '.bin', 'conventional-changelog')
|
||||
const packageJsonPath = path.join(dirname, '..', 'package.json')
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
||||
const version = packageJson.version
|
||||
const dirname = path.join(path.dirname(fileURLToPath(import.meta.url)));
|
||||
const conventionalChangelog = path.join(dirname, '..', 'node_modules', '.bin', 'conventional-changelog');
|
||||
const version = JSON.parse(readFileSync(path.join(dirname, '..', 'package.json'), 'utf8')).version;
|
||||
|
||||
// sometimes release-count 1 is empty
|
||||
let releaseChangelog =
|
||||
execSync(`${conventionalChangelog} --preset angular --release-count 1`).toString() ||
|
||||
execSync(`${conventionalChangelog} --preset angular --release-count 2`).toString()
|
||||
// changelog (use last non-empty)
|
||||
let releaseChangelog =
|
||||
execSync(`${conventionalChangelog} --preset angular --release-count 1`).toString() ||
|
||||
execSync(`${conventionalChangelog} --preset angular --release-count 2`).toString();
|
||||
releaseChangelog = releaseChangelog.trim().replace(/\n\n+/g, '\n\n');
|
||||
|
||||
// format
|
||||
releaseChangelog = releaseChangelog.trim().replace(/\n\n+/g, '\n\n')
|
||||
// discover artifacts
|
||||
const distDir = path.join(dirname, '..', 'dist');
|
||||
let files = [];
|
||||
try {
|
||||
files = readdirSync(distDir);
|
||||
} catch {}
|
||||
|
||||
// In CI finalization, dist is empty. Prefer GitHub release assets when available.
|
||||
const getReleaseAssetNames = async () => {
|
||||
try {
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
const repo = process.env.GITHUB_REPOSITORY; // owner/repo
|
||||
const tag = process.env.GITHUB_REF_NAME || `v${version}`;
|
||||
if (!token || !repo || !tag) return [];
|
||||
const res = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${tag}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': '5chan-release-notes',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
Accept: 'application/vnd.github+json',
|
||||
},
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return (data.assets || []).map((a) => a.name).filter(Boolean);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const remoteFiles = await getReleaseAssetNames();
|
||||
if (remoteFiles.length) {
|
||||
files = remoteFiles;
|
||||
}
|
||||
|
||||
const linkTo = (file) => `https://github.com/plebbit/5chan/releases/download/v${version}/${file}`;
|
||||
const has = (s, sub) => s.toLowerCase().includes(sub);
|
||||
const isArm = (s) => has(s, 'arm64') || has(s, 'aarch64');
|
||||
const isX64 = (s) => has(s, 'x64') || has(s, 'x86_64') || !isArm(s);
|
||||
|
||||
// buckets
|
||||
const androidApk = files.find((f) => f.endsWith('.apk'));
|
||||
const htmlZip = files.find((f) => f.includes('5chan-html') && f.endsWith('.zip'));
|
||||
|
||||
const linux = files.filter((f) => f.endsWith('.AppImage'));
|
||||
const mac = files.filter((f) => f.endsWith('.dmg'));
|
||||
const win = files.filter((f) => f.toLowerCase().endsWith('.exe'));
|
||||
|
||||
const linuxArm = linux.find(isArm);
|
||||
const linuxX64 = linux.find(isX64);
|
||||
const macArm = mac.find(isArm);
|
||||
const macX64 = mac.find(isX64);
|
||||
|
||||
const winSetupX64 = win.find((f) => has(f, 'setup') && isX64(f));
|
||||
const winPortableX64 = win.find((f) => has(f, 'portable') && isX64(f));
|
||||
|
||||
// small section builder without push()
|
||||
const section = (title, lines) => {
|
||||
const body = lines.filter(Boolean).join('\n');
|
||||
return body ? `### ${title}\n${body}` : '';
|
||||
};
|
||||
|
||||
const macSection = section('macOS', [
|
||||
macArm && `- Apple Silicon (arm64): [Download DMG](${linkTo(macArm)})`,
|
||||
macX64 && `- Intel (x64): [Download DMG](${linkTo(macX64)})`,
|
||||
mac.length > 0 &&
|
||||
`- If macOS shows "5chan.app is damaged and can't be opened. You should move it to the Trash.", run this in Terminal to fix it: \`xattr -dr com.apple.quarantine "/Applications/5chan.app"\` then open the app again.`,
|
||||
]);
|
||||
|
||||
const winSection = section('Windows', [
|
||||
winSetupX64 && `- Installer (x64): [Download EXE](${linkTo(winSetupX64)})`,
|
||||
winPortableX64 && `- Portable (x64): [Download EXE](${linkTo(winPortableX64)})`,
|
||||
win.length > 0 &&
|
||||
`- If Windows shows "Windows protected your PC" (SmartScreen), click "More info" then "Run anyway". To permanently allow, right-click the .exe → Properties → check "Unblock", or run in PowerShell: \`Unblock-File -Path .\\path\\to\\file.exe\`.`,
|
||||
]);
|
||||
|
||||
const linuxSection = section('Linux', [
|
||||
linuxArm && `- AppImage (arm64): [Download](${linkTo(linuxArm)})`,
|
||||
linuxX64 && `- AppImage (x64): [Download](${linkTo(linuxX64)})`,
|
||||
]);
|
||||
|
||||
const androidSection = section('Android', [androidApk && `- APK: [Download](${linkTo(androidApk)})`]);
|
||||
|
||||
const htmlSection = section('Static HTML build', [htmlZip && `- 5chan-html (zip): [Download](${linkTo(htmlZip)})`]);
|
||||
|
||||
const downloads = [macSection, winSection, linuxSection, androidSection, htmlSection].filter(Boolean).join('\n\n');
|
||||
|
||||
const releaseBody = `This version rebrands the app to 5chan, introducing a set of new features and several performance improvements.
|
||||
|
||||
- Web app: https://5chan.app
|
||||
- Decentralized web app: https://5chan.eth (only works on [Brave Browser](https://brave.com/) or via [IPFS Companion](https://docs.ipfs.tech/install/ipfs-companion/#prerequisites))
|
||||
- Decentralized web app via IPFS/IPNS gateways (works on any browser): [5chan.eth.limo](https://5chan.eth.limo), [5chan.eth.link](https://5chan.eth.link), [dweb.link/ipfs.io](https://dweb.link/ipns/5chan.eth)
|
||||
- Decentralized web app via IPFS node you run: https://5chan.eth (works on [Brave Browser](https://brave.com/) or use [IPFS Companion](https://docs.ipfs.tech/install/ipfs-companion/#prerequisites))
|
||||
|
||||
## Downloads
|
||||
|
||||
- Android app: [Download APK](https://github.com/plebbit/5chan/releases/download/v${version}/5chan-${version}.apk)
|
||||
- Linux app: [Download AppImage](https://github.com/plebbit/5chan/releases/download/v${version}/5chan-${version}.AppImage)
|
||||
- macOS app: [Download DMG](https://github.com/plebbit/5chan/releases/download/v${version}/5chan-${version}.dmg)
|
||||
- Windows app: [Download EXE](https://github.com/plebbit/5chan/releases/download/v${version}/5chan.Setup.${version}.exe)
|
||||
${downloads}
|
||||
|
||||
${releaseChangelog}`
|
||||
## Changes
|
||||
|
||||
console.log(releaseBody)
|
||||
${releaseChangelog}`;
|
||||
|
||||
console.log(releaseBody);
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@ import useCreateBoardModalStore from './stores/use-create-board-modal-store';
|
||||
import useSpecialThemeStore from './stores/use-special-theme-store';
|
||||
import useIsMobile from './hooks/use-is-mobile';
|
||||
import useTheme from './hooks/use-theme';
|
||||
import { useDefaultSubplebbits } from './hooks/use-default-subplebbits';
|
||||
import { useDirectories } from './hooks/use-directories';
|
||||
import { getSubplebbitAddress, isPostRoute, isPendingPostRoute, isModQueueRoute } from './lib/utils/route-utils';
|
||||
import styles from './app.module.css';
|
||||
import FAQ from './views/faq';
|
||||
@@ -44,8 +44,8 @@ const BoardLayout = () => {
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
|
||||
const isInModView = isModView(location.pathname);
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const subplebbitAddress = boardIdentifier ? getSubplebbitAddress(boardIdentifier, defaultSubplebbits) : undefined;
|
||||
const directories = useDirectories();
|
||||
const subplebbitAddress = boardIdentifier ? getSubplebbitAddress(boardIdentifier, directories) : undefined;
|
||||
const pendingPost = useAccountComment({ commentIndex: accountCommentIndex ? parseInt(accountCommentIndex) : undefined });
|
||||
const { closeCreateBoardModal } = useCreateBoardModalStore();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useAccountComment, useSubscribe } from '@plebbit/plebbit-react-hooks';
|
||||
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
|
||||
import { isAllView, isCatalogView, isModView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||
import { useDirectories } from '../../hooks/use-directories';
|
||||
import { getBoardPath, isDirectoryBoard } from '../../lib/utils/route-utils';
|
||||
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
||||
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
|
||||
@@ -35,7 +35,7 @@ interface BoardButtonsProps {
|
||||
const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
const createCatalogLink = () => {
|
||||
if (isInAllView) {
|
||||
@@ -50,9 +50,9 @@ const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModVie
|
||||
}
|
||||
let boardPath = '';
|
||||
if (address) {
|
||||
boardPath = getBoardPath(address, defaultSubplebbits);
|
||||
} else if (Array.isArray(defaultSubplebbits) && defaultSubplebbits.length > 0 && defaultSubplebbits[0]?.address) {
|
||||
boardPath = getBoardPath(defaultSubplebbits[0].address, defaultSubplebbits);
|
||||
boardPath = getBoardPath(address, directories);
|
||||
} else if (Array.isArray(directories) && directories.length > 0 && directories[0]?.address) {
|
||||
boardPath = getBoardPath(directories[0].address, directories);
|
||||
}
|
||||
return `/${boardPath}/catalog`;
|
||||
};
|
||||
@@ -78,7 +78,7 @@ const SubscribeButton = ({ address }: BoardButtonsProps) => {
|
||||
const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView, isInModQueueView }: BoardButtonsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
const createReturnLink = () => {
|
||||
if (isInAllView) {
|
||||
@@ -99,9 +99,9 @@ const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView
|
||||
}
|
||||
let boardPath = '';
|
||||
if (address) {
|
||||
boardPath = getBoardPath(address, defaultSubplebbits);
|
||||
} else if (Array.isArray(defaultSubplebbits) && defaultSubplebbits.length > 0 && defaultSubplebbits[0]?.address) {
|
||||
boardPath = getBoardPath(defaultSubplebbits[0].address, defaultSubplebbits);
|
||||
boardPath = getBoardPath(address, directories);
|
||||
} else if (Array.isArray(directories) && directories.length > 0 && directories[0]?.address) {
|
||||
boardPath = getBoardPath(directories[0].address, directories);
|
||||
}
|
||||
return `/${boardPath}`;
|
||||
};
|
||||
@@ -116,13 +116,13 @@ const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView
|
||||
const VoteButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
// Get the boardIdentifier from params (try boardIdentifier first, then subplebbitAddress for backward compatibility)
|
||||
const boardIdentifier = params.boardIdentifier || params.subplebbitAddress;
|
||||
|
||||
// Only render the vote button if we're on a directory board route
|
||||
if (!boardIdentifier || !isDirectoryBoard(boardIdentifier, defaultSubplebbits)) {
|
||||
if (!boardIdentifier || !isDirectoryBoard(boardIdentifier, directories)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -418,9 +418,9 @@ export const MobileBoardButtons = () => {
|
||||
const { filteredCount, searchText } = useCatalogFiltersStore();
|
||||
|
||||
// Check if we should show the vote button (only for directory boards)
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
const boardIdentifier = params.boardIdentifier || params.subplebbitAddress;
|
||||
const showVoteButton = boardIdentifier && isDirectoryBoard(boardIdentifier, defaultSubplebbits);
|
||||
const showVoteButton = boardIdentifier && isDirectoryBoard(boardIdentifier, directories);
|
||||
|
||||
return (
|
||||
<div className={`${styles.mobileBoardButtons} ${!isInCatalogView ? styles.addMargin : ''}`}>
|
||||
@@ -539,9 +539,9 @@ export const DesktopBoardButtons = () => {
|
||||
const { filteredCount, searchText } = useCatalogFiltersStore();
|
||||
|
||||
// Check if we should show the vote button (only for directory boards)
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
const boardIdentifier = params.boardIdentifier || params.subplebbitAddress;
|
||||
const showVoteButton = boardIdentifier && isDirectoryBoard(boardIdentifier, defaultSubplebbits);
|
||||
const showVoteButton = boardIdentifier && isDirectoryBoard(boardIdentifier, directories);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -655,9 +655,9 @@ const SearchOPsBar = () => {
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
|
||||
const isInModView = isModView(location.pathname);
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
const resolvedAddress = useResolvedSubplebbitAddress();
|
||||
const boardPath = resolvedAddress ? getBoardPath(resolvedAddress, defaultSubplebbits) : params?.boardIdentifier || params?.subplebbitAddress;
|
||||
const boardPath = resolvedAddress ? getBoardPath(resolvedAddress, directories) : params?.boardIdentifier || params?.subplebbitAddress;
|
||||
|
||||
const handleSearch = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
|
||||
@@ -8,7 +8,7 @@ import Plebbit from '@plebbit/plebbit-js';
|
||||
import { useStableSubplebbit } from '../../hooks/use-stable-subplebbit';
|
||||
import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils';
|
||||
import styles from './board-header.module.css';
|
||||
import { useMultisubMetadata, useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||
import { useDirectoriesMetadata, useDirectories } from '../../hooks/use-directories';
|
||||
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
||||
@@ -59,11 +59,11 @@ const BoardHeader = () => {
|
||||
const stableSubplebbit = useStableSubplebbit(subplebbitAddress);
|
||||
const { address, shortAddress } = stableSubplebbit || {};
|
||||
|
||||
const multisubMetadata = useMultisubMetadata();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directoriesMetadata = useDirectoriesMetadata();
|
||||
const directories = useDirectories();
|
||||
|
||||
// Find matching subplebbit from default list to get its title
|
||||
const defaultSubplebbit = subplebbitAddress ? defaultSubplebbits.find((s) => s.address === subplebbitAddress) : null;
|
||||
const defaultSubplebbit = subplebbitAddress ? directories.find((s) => s.address === subplebbitAddress) : null;
|
||||
|
||||
// Use accounts store with selector to only subscribe to subscriptions count
|
||||
const subscriptionsCount = useAccountsStore((state) => {
|
||||
@@ -74,7 +74,7 @@ const BoardHeader = () => {
|
||||
const subscriptionsSubtitle = t('subscriptions_subtitle', { count: subscriptionsCount });
|
||||
|
||||
const title = isInAllView
|
||||
? multisubMetadata?.title || '/all/ - 5chan Directories'
|
||||
? directoriesMetadata?.title || '/all/ - 5chan Directories'
|
||||
: isInSubscriptionsView
|
||||
? '/subs/ - Subscriptions'
|
||||
: isInModView
|
||||
|
||||
@@ -9,7 +9,7 @@ import { shouldShowSnow } from '../../lib/snow';
|
||||
import { getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import { getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||
import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||
import { useDirectories } from '../../hooks/use-directories';
|
||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
|
||||
import useCatalogStyleStore from '../../stores/use-catalog-style-store';
|
||||
@@ -125,8 +125,8 @@ const CatalogPost = memo(
|
||||
const params = useParams();
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : '';
|
||||
const directories = useDirectories();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : '';
|
||||
const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]);
|
||||
|
||||
const postLink = boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`;
|
||||
|
||||
@@ -42,8 +42,8 @@ const CreateBoardModal = () => {
|
||||
<h3>Submitting to a Directory</h3>
|
||||
<p>
|
||||
Open a PR editing{' '}
|
||||
<a href='https://github.com/plebbit/lists/blob/master/5chan-multisub.json' target='_blank' rel='noopener noreferrer'>
|
||||
5chan-multisub.json
|
||||
<a href='https://github.com/plebbit/lists/blob/master/5chan-directories.json' target='_blank' rel='noopener noreferrer'>
|
||||
5chan-directories.json
|
||||
</a>{' '}
|
||||
with your board's title, address, and NSFW status. Devs will review and merge if approved. 99% uptime is required.
|
||||
</p>
|
||||
|
||||
@@ -50,8 +50,8 @@ const DirectoryModal = () => {
|
||||
<h3>Directory Submission</h3>
|
||||
<p>
|
||||
To submit your board, open a PR editing{' '}
|
||||
<a href='https://github.com/plebbit/lists/blob/master/5chan-multisub.json' target='_blank' rel='noopener noreferrer'>
|
||||
5chan-multisub.json
|
||||
<a href='https://github.com/plebbit/lists/blob/master/5chan-directories.json' target='_blank' rel='noopener noreferrer'>
|
||||
5chan-directories.json
|
||||
</a>{' '}
|
||||
with your board's title, address, and NSFW status. Requirements: 99% uptime (they're P2P nodes), active, well-moderated, and relevant to the
|
||||
category. Devs will review and approve/reject based on these criteria.
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-util
|
||||
import { isValidURL } from '../../lib/utils/url-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 { useDirectories } from '../../hooks/use-directories';
|
||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||
import useAvatarVisibilityStore from '../../stores/use-avatar-visibility-store';
|
||||
import useAuthorAddressClick from '../../hooks/use-author-address-click';
|
||||
@@ -85,8 +85,8 @@ const PostInfo = ({
|
||||
const { showOmittedReplies } = useShowOmittedReplies();
|
||||
const { imageUrl: avatarImageUrl } = useAuthorAvatar({ author });
|
||||
const { hideAvatars } = useAvatarVisibilityStore();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : undefined;
|
||||
const directories = useDirectories();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : undefined;
|
||||
const postMenuProps = useMemo(() => selectPostMenuProps(post), [post]);
|
||||
|
||||
const params = useParams();
|
||||
@@ -453,7 +453,7 @@ const PostMedia = ({
|
||||
const { url } = commentMediaInfo || {};
|
||||
let type = commentMediaInfo?.type;
|
||||
const gifFrameUrl = useFetchGifFirstFrame(url);
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
if (type === 'gif' && gifFrameUrl !== null) {
|
||||
type = 'animated gif';
|
||||
@@ -465,7 +465,7 @@ const PostMedia = ({
|
||||
const [showThumbnail, setShowThumbnail] = useState(true);
|
||||
|
||||
const mediaDimensions = getMediaDimensions(commentMediaInfo);
|
||||
const boardPath = getBoardPath(subplebbitAddress, defaultSubplebbits);
|
||||
const boardPath = getBoardPath(subplebbitAddress, directories);
|
||||
|
||||
return (
|
||||
<div className={styles.file}>
|
||||
@@ -528,8 +528,8 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => {
|
||||
}
|
||||
|
||||
const { author, cid, deleted, link, linkHeight, linkWidth, postCid, reason, removed, spoiler, subplebbitAddress, thumbnailUrl, parentCid } = post || {};
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : undefined;
|
||||
const directories = useDirectories();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : undefined;
|
||||
|
||||
const location = useLocation();
|
||||
const route = boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`;
|
||||
@@ -591,8 +591,8 @@ const PostDesktop = ({
|
||||
const isInPostPageView = isPostPageView(location.pathname, params);
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : undefined;
|
||||
const directories = useDirectories();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : undefined;
|
||||
|
||||
const { hidden, unhide, hide } = useHide({ cid });
|
||||
const isHidden = hidden && !isInPostPageView;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getCommentMediaInfo } from '../../../lib/utils/media-utils';
|
||||
import { copyShareLinkToClipboard, isValidURL, type ShareLinkType } from '../../../lib/utils/url-utils';
|
||||
import { copyToClipboard } from '../../../lib/utils/clipboard-utils';
|
||||
import { getBoardPath } from '../../../lib/utils/route-utils';
|
||||
import { useDefaultSubplebbits } from '../../../hooks/use-default-subplebbits';
|
||||
import { useDirectories } from '../../../hooks/use-directories';
|
||||
import { isAllView, isCatalogView, isPostPageView, isSubscriptionsView } from '../../../lib/utils/view-utils';
|
||||
import useHide from '../../../hooks/use-hide';
|
||||
import _ from 'lodash';
|
||||
@@ -21,8 +21,8 @@ type CopyLinkButtonProps =
|
||||
|
||||
const CopyLinkButton = ({ cid, subplebbitAddress, linkType, onClose }: CopyLinkButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const boardIdentifier = getBoardPath(subplebbitAddress, defaultSubplebbits);
|
||||
const directories = useDirectories();
|
||||
const boardIdentifier = getBoardPath(subplebbitAddress, directories);
|
||||
return (
|
||||
<div
|
||||
onClick={async () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ 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, isModQueueView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||
import { useDirectories } from '../../hooks/use-directories';
|
||||
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
||||
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
|
||||
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
||||
@@ -71,7 +71,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const isInModView = isModView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
|
||||
const subscriptions = account?.subscriptions || [];
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
const { accountSubplebbits } = useAccountSubplebbits();
|
||||
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
|
||||
@@ -338,7 +338,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
<select onChange={(e) => setPublishPostOptions({ subplebbitAddress: e.target.value })} value={subplebbitAddress}>
|
||||
<option value=''>{t('choose_one')}</option>
|
||||
{isInAllView &&
|
||||
defaultSubplebbits
|
||||
directories
|
||||
.filter((subplebbit) => subplebbit.title && subplebbit.address)
|
||||
.map((subplebbit) => (
|
||||
<option key={subplebbit.address} value={subplebbit.address}>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getCommentMediaInfo } from '../../../lib/utils/media-utils';
|
||||
import { copyShareLinkToClipboard, isValidURL, type ShareLinkType } from '../../../lib/utils/url-utils';
|
||||
import { copyToClipboard } from '../../../lib/utils/clipboard-utils';
|
||||
import { getBoardPath } from '../../../lib/utils/route-utils';
|
||||
import { useDefaultSubplebbits } from '../../../hooks/use-default-subplebbits';
|
||||
import { useDirectories } from '../../../hooks/use-directories';
|
||||
import useEditCommentPrivileges from '../../../hooks/use-author-privileges';
|
||||
import useHide from '../../../hooks/use-hide';
|
||||
import EditMenu from '../../edit-menu/edit-menu';
|
||||
@@ -29,8 +29,8 @@ type CopyLinkButtonProps =
|
||||
|
||||
const CopyLinkButton = ({ cid, subplebbitAddress, linkType, onClose }: CopyLinkButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const boardIdentifier = getBoardPath(subplebbitAddress, defaultSubplebbits);
|
||||
const directories = useDirectories();
|
||||
const boardIdentifier = getBoardPath(subplebbitAddress, directories);
|
||||
return (
|
||||
<div
|
||||
onClick={async () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getTextColorForBackground, hashStringToColor } from '../../lib/utils/po
|
||||
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-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 { useDirectories } from '../../hooks/use-directories';
|
||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||
import useAvatarVisibilityStore from '../../stores/use-avatar-visibility-store';
|
||||
import useAuthorAddressClick from '../../hooks/use-author-address-click';
|
||||
@@ -40,10 +40,10 @@ const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
|
||||
const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: PostProps) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
const { author, cid, deleted, link, linkHeight, linkWidth, locked, parentCid, pinned, postCid, reason, removed, state, subplebbitAddress, timestamp, thumbnailUrl } =
|
||||
post || {};
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : undefined;
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : undefined;
|
||||
const isReply = parentCid;
|
||||
const title = post?.title?.trim();
|
||||
const { address, shortAddress } = author || {};
|
||||
@@ -397,8 +397,8 @@ const Reply = ({ postReplyCount, reply, roles, threadNumber }: PostProps) => {
|
||||
post = editedComment;
|
||||
}
|
||||
const { author, cid, deleted, postCid, reason, removed, subplebbitAddress } = post || {};
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : undefined;
|
||||
const directories = useDirectories();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : undefined;
|
||||
const location = useLocation();
|
||||
const route = boardPath ? `/${boardPath}/thread/${cid}` : `/thread/${cid}`;
|
||||
const isRouteLinkToReply = cid ? location.pathname.startsWith(route) : false;
|
||||
@@ -442,8 +442,8 @@ const PostMobile = ({
|
||||
const navigationType = useNavigationType();
|
||||
const isInPendingPostView = isPendingPostView(location.pathname, params);
|
||||
const isInPostView = isPostPageView(location.pathname, params);
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : undefined;
|
||||
const directories = useDirectories();
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : undefined;
|
||||
const linksCount = useCountLinksInReplies(post);
|
||||
const { replies, hasMore, loadMore } = useReplies({ comment: post, accountComments: { newerThan: Infinity } });
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Comment, useAccount } from '@plebbit/plebbit-react-hooks';
|
||||
import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react';
|
||||
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||
import { useDirectories } from '../../hooks/use-directories';
|
||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import styles from '../../views/post/post.module.css';
|
||||
@@ -59,7 +59,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
||||
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
||||
const placementRef = useRef<Placement>('right');
|
||||
const availableWidthRef = useRef<number>(0);
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
const { refs, floatingStyles, update } = useFloating({
|
||||
placement: placementRef.current,
|
||||
@@ -102,7 +102,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
||||
const handleClick = (e: React.MouseEvent, cid: string | undefined, subplebbitAddress: string | undefined) => {
|
||||
e.preventDefault();
|
||||
if (cid && subplebbitAddress) {
|
||||
const boardPath = getBoardPath(subplebbitAddress, defaultSubplebbits);
|
||||
const boardPath = getBoardPath(subplebbitAddress, directories);
|
||||
navigate(`/${boardPath}/thread/${cid}`);
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(`[data-cid="${cid}"]`);
|
||||
@@ -130,7 +130,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
||||
setOutOfViewCid(null);
|
||||
};
|
||||
|
||||
const backlinkBoardPath = backlinkReply?.subplebbitAddress ? getBoardPath(backlinkReply.subplebbitAddress, defaultSubplebbits) : undefined;
|
||||
const backlinkBoardPath = backlinkReply?.subplebbitAddress ? getBoardPath(backlinkReply.subplebbitAddress, directories) : undefined;
|
||||
const backlinkRoute = backlinkReply?.cid ? (backlinkBoardPath ? `/${backlinkBoardPath}/thread/${backlinkReply.cid}` : `/thread/${backlinkReply.cid}`) : '#';
|
||||
|
||||
const replyBacklink = (
|
||||
@@ -159,7 +159,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
||||
|
||||
const account = useAccount();
|
||||
|
||||
const quotelinkBoardPath = quotelinkReply?.subplebbitAddress ? getBoardPath(quotelinkReply.subplebbitAddress, defaultSubplebbits) : undefined;
|
||||
const quotelinkBoardPath = quotelinkReply?.subplebbitAddress ? getBoardPath(quotelinkReply.subplebbitAddress, directories) : undefined;
|
||||
const quotelinkRoute = quotelinkReply?.cid ? (quotelinkBoardPath ? `/${quotelinkBoardPath}/thread/${quotelinkReply.cid}` : `/thread/${quotelinkReply.cid}`) : '#';
|
||||
|
||||
const replyQuotelink = (
|
||||
@@ -193,7 +193,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
||||
const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => {
|
||||
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
||||
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
const { refs, floatingStyles, update } = useFloating({
|
||||
placement: 'bottom',
|
||||
@@ -215,7 +215,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
||||
const handleClick = (e: React.MouseEvent, cid: string | undefined, subplebbitAddress: string | undefined) => {
|
||||
e.preventDefault();
|
||||
if (cid && subplebbitAddress) {
|
||||
const boardPath = getBoardPath(subplebbitAddress, defaultSubplebbits);
|
||||
const boardPath = getBoardPath(subplebbitAddress, directories);
|
||||
navigate(`/${boardPath}/thread/${cid}`);
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(`[data-cid="${cid}"]`);
|
||||
@@ -255,7 +255,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
||||
</span>
|
||||
{backlinkReply?.number &&
|
||||
(() => {
|
||||
const backlinkBoardPath = backlinkReply?.subplebbitAddress ? getBoardPath(backlinkReply.subplebbitAddress, defaultSubplebbits) : undefined;
|
||||
const backlinkBoardPath = backlinkReply?.subplebbitAddress ? getBoardPath(backlinkReply.subplebbitAddress, directories) : undefined;
|
||||
const backlinkRoute = backlinkReply?.cid ? (backlinkBoardPath ? `/${backlinkBoardPath}/thread/${backlinkReply.cid}` : `/thread/${backlinkReply.cid}`) : '#';
|
||||
return (
|
||||
<Link to={backlinkRoute} className={styles.backlinkHash} onClick={(e) => handleClick(e, backlinkReply?.cid, backlinkReply?.subplebbitAddress)}>
|
||||
@@ -290,7 +290,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
||||
</span>
|
||||
{quotelinkReply?.number &&
|
||||
(() => {
|
||||
const quotelinkBoardPath = quotelinkReply?.subplebbitAddress ? getBoardPath(quotelinkReply.subplebbitAddress, defaultSubplebbits) : undefined;
|
||||
const quotelinkBoardPath = quotelinkReply?.subplebbitAddress ? getBoardPath(quotelinkReply.subplebbitAddress, directories) : undefined;
|
||||
const quotelinkRoute = quotelinkReply?.cid
|
||||
? quotelinkBoardPath
|
||||
? `/${quotelinkBoardPath}/thread/${quotelinkReply.cid}`
|
||||
|
||||
@@ -5,7 +5,7 @@ import Plebbit from '@plebbit/plebbit-js';
|
||||
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||
import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts';
|
||||
import { isAllView, isCatalogView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { useDefaultSubplebbits, MultisubSubplebbit } from '../../hooks/use-default-subplebbits';
|
||||
import { useDirectories, DirectoryCommunity } from '../../hooks/use-directories';
|
||||
import { useBoardPath, useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
||||
import { getBoardPath, extractDirectoryFromTitle } from '../../lib/utils/route-utils';
|
||||
import { TimeFilter } from '../board-buttons';
|
||||
@@ -77,8 +77,8 @@ const SearchBar = ({ setShowSearchBar }: { setShowSearchBar: (show: boolean) =>
|
||||
};
|
||||
|
||||
// Helper function to find board address by directory code
|
||||
const findBoardAddressByCode = (code: string, defaultSubplebbits: MultisubSubplebbit[]): string | null => {
|
||||
const entry = defaultSubplebbits.find((subplebbit) => {
|
||||
const findBoardAddressByCode = (code: string, directories: DirectoryCommunity[]): string | null => {
|
||||
const entry = directories.find((subplebbit) => {
|
||||
if (!subplebbit.title) return false;
|
||||
const directory = extractDirectoryFromTitle(subplebbit.title);
|
||||
return directory === code;
|
||||
@@ -97,7 +97,7 @@ const TopBarDesktop = () => {
|
||||
const { openTopbarEditModal } = useTopbarEditModalStore();
|
||||
const { openDirectoryModal } = useDirectoryModalStore();
|
||||
const { visibleDirectories, visibleSubscriptions } = useTopbarVisibilityStore();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
// Memoize allBoardCodes since it's derived from a constant
|
||||
const allBoardCodes = useMemo(() => getAllBoardCodes(), []);
|
||||
@@ -150,7 +150,7 @@ const TopBarDesktop = () => {
|
||||
|
||||
// Render a board code link or placeholder
|
||||
const renderBoardCode = (code: string, isLastInGroup: boolean) => {
|
||||
const address = findBoardAddressByCode(code, defaultSubplebbits);
|
||||
const address = findBoardAddressByCode(code, directories);
|
||||
const isPlaceholder = !address;
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
@@ -186,7 +186,7 @@ const TopBarDesktop = () => {
|
||||
|
||||
// Render a subscription link
|
||||
const renderSubscription = (address: string, index: number, total: number) => {
|
||||
const boardPath = getBoardPath(address, defaultSubplebbits);
|
||||
const boardPath = getBoardPath(address, directories);
|
||||
const displayText = address.endsWith('.eth') || address.endsWith('.sol') ? address : Plebbit.getShortAddress({ address });
|
||||
|
||||
return (
|
||||
@@ -249,12 +249,12 @@ const TopBarDesktop = () => {
|
||||
const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
const displaySubplebbitAddress = subplebbitAddress && subplebbitAddress.length > 30 ? subplebbitAddress.slice(0, 30).concat('...') : subplebbitAddress;
|
||||
const [showSearchBar, setShowSearchBar] = useState(false);
|
||||
|
||||
// Filter to only show directory boards (those with titles)
|
||||
const directoryBoards = useMemo(() => defaultSubplebbits.filter((sub) => sub.title && extractDirectoryFromTitle(sub.title)), [defaultSubplebbits]);
|
||||
const directoryBoards = useMemo(() => directories.filter((sub) => sub.title && extractDirectoryFromTitle(sub.title)), [directories]);
|
||||
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Each 5chan directory is assigned to a board (subplebbit) handpicked by the team, until DAO curation is implemented.\n\nhttps://github.com/plebbit/temporary-default-subplebbits",
|
||||
"createdAt": 1762179811,
|
||||
"updatedAt": 1762179811,
|
||||
"subplebbits": [
|
||||
"communities": [
|
||||
{
|
||||
"title": "/pol/ - Politically Incorrect",
|
||||
"address": "politically-incorrect.eth",
|
||||
@@ -0,0 +1,34 @@
|
||||
// Auto-generated file - do not edit manually
|
||||
// Run 'node scripts/generate-asset-manifest.js' to regenerate
|
||||
// This file is generated from public/assets/ directory
|
||||
|
||||
export const BANNERS = ['assets/banners/banner-1.jpg', 'assets/banners/banner-2.jpg', 'assets/banners/banner-3.gif', 'assets/banners/banner-4.png'] as const;
|
||||
|
||||
export const NOT_FOUND_IMAGES = ['assets/not-found/not-found-1.png'] as const;
|
||||
|
||||
// Theme button images (cross, plus, minus, help, button-fade variants)
|
||||
export const THEME_BUTTON_IMAGES = [
|
||||
'assets/buttons/button-fade-blue.png',
|
||||
'assets/buttons/button-fade-dark.png',
|
||||
'assets/buttons/button-fade.png',
|
||||
'assets/buttons/cross-blue.png',
|
||||
'assets/buttons/cross-dark.png',
|
||||
'assets/buttons/cross-photon.png',
|
||||
'assets/buttons/cross-red.png',
|
||||
'assets/buttons/help-blue.png',
|
||||
'assets/buttons/help-dark.png',
|
||||
'assets/buttons/help-photon.png',
|
||||
'assets/buttons/help-red.png',
|
||||
'assets/buttons/icon-close-red.png',
|
||||
'assets/buttons/minus-blue.png',
|
||||
'assets/buttons/minus-dark.png',
|
||||
'assets/buttons/minus-photon.png',
|
||||
'assets/buttons/minus-red.png',
|
||||
'assets/buttons/plus-blue.png',
|
||||
'assets/buttons/plus-dark.png',
|
||||
'assets/buttons/plus-photon.png',
|
||||
'assets/buttons/plus-red.png',
|
||||
] as const;
|
||||
|
||||
// Theme background pattern images
|
||||
export const THEME_BACKGROUND_IMAGES = ['assets/background-fade.png', 'assets/background-fade-blue.png'] as const;
|
||||
@@ -1,7 +1,7 @@
|
||||
import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts';
|
||||
import { MultisubSubplebbit } from './use-default-subplebbits';
|
||||
import { DirectoryCommunity } from './use-directories';
|
||||
|
||||
export const useAccountSubplebbitsWithMetadata = (): MultisubSubplebbit[] => {
|
||||
export const useAccountSubplebbitsWithMetadata = (): DirectoryCommunity[] => {
|
||||
const subplebbitsWithMetadata = useAccountsStore(
|
||||
(state) => {
|
||||
const activeAccountId = state.activeAccountId;
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import defaultSubplebbitsData from '../data/default-subplebbits.json';
|
||||
import directoriesData from '../data/5chan-directories.json';
|
||||
|
||||
export interface MultisubMetadata {
|
||||
export interface DirectoriesMetadata {
|
||||
title: string;
|
||||
description: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface MultisubSubplebbit {
|
||||
export interface DirectoryCommunity {
|
||||
title?: string;
|
||||
address: string;
|
||||
nsfw?: boolean;
|
||||
}
|
||||
|
||||
export interface MultisubData {
|
||||
export interface DirectoriesData {
|
||||
title: string;
|
||||
description: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
subplebbits: MultisubSubplebbit[];
|
||||
communities: DirectoryCommunity[];
|
||||
}
|
||||
|
||||
export interface DefaultSubplebbitsState {
|
||||
subplebbits: MultisubSubplebbit[];
|
||||
export interface DirectoriesState {
|
||||
communities: DirectoryCommunity[];
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
const GITHUB_URL = 'https://raw.githubusercontent.com/plebbit/lists/master/5chan-multisub.json';
|
||||
const LOCALSTORAGE_KEY = '5chan-subplebbits-cache';
|
||||
const LOCALSTORAGE_TIMESTAMP_KEY = '5chan-subplebbits-cache-timestamp';
|
||||
const GITHUB_URL = 'https://raw.githubusercontent.com/plebbit/lists/master/5chan-directories.json';
|
||||
const LOCALSTORAGE_KEY = '5chan-directories-cache';
|
||||
const LOCALSTORAGE_TIMESTAMP_KEY = '5chan-directories-cache-timestamp';
|
||||
const CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
let cacheSubplebbits: MultisubSubplebbit[] | null = null;
|
||||
let cacheMetadata: MultisubMetadata | null = null;
|
||||
let cacheCommunities: DirectoryCommunity[] | null = null;
|
||||
let cacheMetadata: DirectoriesMetadata | null = null;
|
||||
|
||||
const getFromLocalStorage = (): MultisubData | null => {
|
||||
const getFromLocalStorage = (): DirectoriesData | null => {
|
||||
try {
|
||||
const cached = localStorage.getItem(LOCALSTORAGE_KEY);
|
||||
const timestamp = localStorage.getItem(LOCALSTORAGE_TIMESTAMP_KEY);
|
||||
@@ -52,7 +52,7 @@ const getFromLocalStorage = (): MultisubData | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const saveToLocalStorage = (data: MultisubData) => {
|
||||
const saveToLocalStorage = (data: DirectoriesData) => {
|
||||
try {
|
||||
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(data));
|
||||
localStorage.setItem(LOCALSTORAGE_TIMESTAMP_KEY, Date.now().toString());
|
||||
@@ -61,7 +61,7 @@ const saveToLocalStorage = (data: MultisubData) => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMultisubData = async (): Promise<MultisubData> => {
|
||||
const fetchDirectoriesData = async (): Promise<DirectoriesData> => {
|
||||
try {
|
||||
const response = await fetch(GITHUB_URL);
|
||||
if (!response.ok) {
|
||||
@@ -72,25 +72,25 @@ const fetchMultisubData = async (): Promise<MultisubData> => {
|
||||
saveToLocalStorage(data);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch subplebbits from GitHub, using vendored fallback:', e);
|
||||
console.warn('Failed to fetch directories from GitHub, using vendored fallback:', e);
|
||||
// Fall back to vendored file
|
||||
return defaultSubplebbitsData as MultisubData;
|
||||
return directoriesData as DirectoriesData;
|
||||
}
|
||||
};
|
||||
|
||||
export const useDefaultSubplebbits = () => {
|
||||
export const useDirectories = () => {
|
||||
// Use vendored data as initial state to prevent theme flash on first load
|
||||
// This ensures NSFW status is known synchronously before first render
|
||||
const [state, setState] = useState<DefaultSubplebbitsState>({
|
||||
subplebbits: (defaultSubplebbitsData as MultisubData).subplebbits,
|
||||
const [state, setState] = useState<DirectoriesState>({
|
||||
communities: (directoriesData as DirectoriesData).communities,
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (cacheSubplebbits) {
|
||||
if (cacheCommunities) {
|
||||
setState({
|
||||
subplebbits: cacheSubplebbits,
|
||||
communities: cacheCommunities,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
@@ -104,21 +104,21 @@ export const useDefaultSubplebbits = () => {
|
||||
// Check localStorage first
|
||||
const cachedData = getFromLocalStorage();
|
||||
if (cachedData) {
|
||||
cacheSubplebbits = cachedData.subplebbits;
|
||||
cacheCommunities = cachedData.communities;
|
||||
if (isMounted) {
|
||||
setState({
|
||||
subplebbits: cachedData.subplebbits,
|
||||
communities: cachedData.communities,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
// Still try to fetch fresh data in background (don't await)
|
||||
fetchMultisubData()
|
||||
fetchDirectoriesData()
|
||||
.then((data) => {
|
||||
if (isMounted) {
|
||||
cacheSubplebbits = data.subplebbits;
|
||||
cacheCommunities = data.communities;
|
||||
setState({
|
||||
subplebbits: data.subplebbits,
|
||||
communities: data.communities,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
@@ -131,23 +131,23 @@ export const useDefaultSubplebbits = () => {
|
||||
}
|
||||
|
||||
// No cache, fetch fresh data
|
||||
const multisub = await fetchMultisubData();
|
||||
const directories = await fetchDirectoriesData();
|
||||
if (isMounted) {
|
||||
cacheSubplebbits = multisub.subplebbits;
|
||||
cacheCommunities = directories.communities;
|
||||
setState({
|
||||
subplebbits: multisub.subplebbits,
|
||||
communities: directories.communities,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load subplebbits:', e);
|
||||
console.warn('Failed to load directories:', e);
|
||||
// Fallback to vendored data
|
||||
const fallbackData = defaultSubplebbitsData as MultisubData;
|
||||
const fallbackData = directoriesData as DirectoriesData;
|
||||
if (isMounted) {
|
||||
cacheSubplebbits = fallbackData.subplebbits;
|
||||
cacheCommunities = fallbackData.communities;
|
||||
setState({
|
||||
subplebbits: fallbackData.subplebbits,
|
||||
communities: fallbackData.communities,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
@@ -160,24 +160,24 @@ export const useDefaultSubplebbits = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Always prefer cacheSubplebbits (module-level, stable reference) when available
|
||||
// Only use state.subplebbits during initial load before cache is populated
|
||||
// Always prefer cacheCommunities (module-level, stable reference) when available
|
||||
// Only use state.communities during initial load before cache is populated
|
||||
// This ensures a stable reference for memoization in consuming hooks
|
||||
return cacheSubplebbits || state.subplebbits;
|
||||
return cacheCommunities || state.communities;
|
||||
};
|
||||
|
||||
export const useDefaultSubplebbitsState = () => {
|
||||
export const useDirectoriesState = () => {
|
||||
// Use vendored data as fallback to prevent theme flash on first load
|
||||
const [state, setState] = useState<DefaultSubplebbitsState>({
|
||||
subplebbits: cacheSubplebbits || (defaultSubplebbitsData as MultisubData).subplebbits,
|
||||
loading: !cacheSubplebbits,
|
||||
const [state, setState] = useState<DirectoriesState>({
|
||||
communities: cacheCommunities || (directoriesData as DirectoriesData).communities,
|
||||
loading: !cacheCommunities,
|
||||
error: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (cacheSubplebbits) {
|
||||
if (cacheCommunities) {
|
||||
setState({
|
||||
subplebbits: cacheSubplebbits,
|
||||
communities: cacheCommunities,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
@@ -191,21 +191,21 @@ export const useDefaultSubplebbitsState = () => {
|
||||
// Check localStorage first
|
||||
const cachedData = getFromLocalStorage();
|
||||
if (cachedData) {
|
||||
cacheSubplebbits = cachedData.subplebbits;
|
||||
cacheCommunities = cachedData.communities;
|
||||
if (isMounted) {
|
||||
setState({
|
||||
subplebbits: cachedData.subplebbits,
|
||||
communities: cachedData.communities,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
// Still try to fetch fresh data in background (don't await)
|
||||
fetchMultisubData()
|
||||
fetchDirectoriesData()
|
||||
.then((data) => {
|
||||
if (isMounted) {
|
||||
cacheSubplebbits = data.subplebbits;
|
||||
cacheCommunities = data.communities;
|
||||
setState({
|
||||
subplebbits: data.subplebbits,
|
||||
communities: data.communities,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
@@ -218,23 +218,23 @@ export const useDefaultSubplebbitsState = () => {
|
||||
}
|
||||
|
||||
// No cache, fetch fresh data
|
||||
const multisub = await fetchMultisubData();
|
||||
const directories = await fetchDirectoriesData();
|
||||
if (isMounted) {
|
||||
cacheSubplebbits = multisub.subplebbits;
|
||||
cacheCommunities = directories.communities;
|
||||
setState({
|
||||
subplebbits: multisub.subplebbits,
|
||||
communities: directories.communities,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load subplebbits:', e);
|
||||
console.warn('Failed to load directories:', e);
|
||||
// Fallback to vendored data
|
||||
const fallbackData = defaultSubplebbitsData as MultisubData;
|
||||
const fallbackData = directoriesData as DirectoriesData;
|
||||
if (isMounted) {
|
||||
cacheSubplebbits = fallbackData.subplebbits;
|
||||
cacheCommunities = fallbackData.communities;
|
||||
setState({
|
||||
subplebbits: fallbackData.subplebbits,
|
||||
communities: fallbackData.communities,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
@@ -250,13 +250,13 @@ export const useDefaultSubplebbitsState = () => {
|
||||
return state;
|
||||
};
|
||||
|
||||
export const useDefaultSubplebbitAddresses = () => {
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
return useMemo(() => defaultSubplebbits.map((subplebbit) => subplebbit.address), [defaultSubplebbits]);
|
||||
export const useDirectoryAddresses = () => {
|
||||
const directories = useDirectories();
|
||||
return useMemo(() => directories.map((community) => community.address), [directories]);
|
||||
};
|
||||
|
||||
export const useMultisubMetadata = () => {
|
||||
const [metadata, setMetadata] = useState<MultisubMetadata | null>(null);
|
||||
export const useDirectoriesMetadata = () => {
|
||||
const [metadata, setMetadata] = useState<DirectoriesMetadata | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (cacheMetadata) {
|
||||
@@ -270,7 +270,7 @@ export const useMultisubMetadata = () => {
|
||||
// Check localStorage first
|
||||
const cachedData = getFromLocalStorage();
|
||||
if (cachedData) {
|
||||
const metadata: MultisubMetadata = {
|
||||
const metadata: DirectoriesMetadata = {
|
||||
title: cachedData.title,
|
||||
description: cachedData.description,
|
||||
createdAt: cachedData.createdAt,
|
||||
@@ -281,10 +281,10 @@ export const useMultisubMetadata = () => {
|
||||
setMetadata(metadata);
|
||||
}
|
||||
// Still try to fetch fresh data in background (don't await)
|
||||
fetchMultisubData()
|
||||
fetchDirectoriesData()
|
||||
.then((data) => {
|
||||
if (isMounted) {
|
||||
const freshMetadata: MultisubMetadata = {
|
||||
const freshMetadata: DirectoriesMetadata = {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
createdAt: data.createdAt,
|
||||
@@ -301,13 +301,13 @@ export const useMultisubMetadata = () => {
|
||||
}
|
||||
|
||||
// No cache, fetch fresh data
|
||||
const multisub = await fetchMultisubData();
|
||||
const directories = await fetchDirectoriesData();
|
||||
if (isMounted) {
|
||||
const metadata: MultisubMetadata = {
|
||||
title: multisub.title,
|
||||
description: multisub.description,
|
||||
createdAt: multisub.createdAt,
|
||||
updatedAt: multisub.updatedAt,
|
||||
const metadata: DirectoriesMetadata = {
|
||||
title: directories.title,
|
||||
description: directories.description,
|
||||
createdAt: directories.createdAt,
|
||||
updatedAt: directories.updatedAt,
|
||||
};
|
||||
cacheMetadata = metadata;
|
||||
setMetadata(metadata);
|
||||
@@ -315,9 +315,9 @@ export const useMultisubMetadata = () => {
|
||||
} catch (e) {
|
||||
console.warn('Failed to load metadata, using vendored fallback:', e);
|
||||
// Fallback to vendored data
|
||||
const fallbackData = defaultSubplebbitsData as MultisubData;
|
||||
const fallbackData = directoriesData as DirectoriesData;
|
||||
if (isMounted) {
|
||||
const metadata: MultisubMetadata = {
|
||||
const metadata: DirectoriesMetadata = {
|
||||
title: fallbackData.title,
|
||||
description: fallbackData.description,
|
||||
createdAt: fallbackData.createdAt,
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useDefaultSubplebbits } from './use-default-subplebbits';
|
||||
import useAllFeedFilterStore from '../stores/use-all-feed-filter-store';
|
||||
|
||||
export const useFilteredDefaultSubplebbitAddresses = () => {
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const { filter } = useAllFeedFilterStore();
|
||||
|
||||
return useMemo(() => {
|
||||
if (filter === 'all') {
|
||||
return defaultSubplebbits.map((subplebbit) => subplebbit.address);
|
||||
}
|
||||
if (filter === 'nsfw') {
|
||||
return defaultSubplebbits.filter((subplebbit) => subplebbit.nsfw === true).map((subplebbit) => subplebbit.address);
|
||||
}
|
||||
// filter === 'sfw'
|
||||
return defaultSubplebbits.filter((subplebbit) => subplebbit.nsfw !== true).map((subplebbit) => subplebbit.address);
|
||||
}, [defaultSubplebbits, filter]);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useDirectories } from './use-directories';
|
||||
import useAllFeedFilterStore from '../stores/use-all-feed-filter-store';
|
||||
|
||||
export const useFilteredDirectoryAddresses = () => {
|
||||
const directories = useDirectories();
|
||||
const { filter } = useAllFeedFilterStore();
|
||||
|
||||
return useMemo(() => {
|
||||
if (filter === 'all') {
|
||||
return directories.map((community) => community.address);
|
||||
}
|
||||
if (filter === 'nsfw') {
|
||||
return directories.filter((community) => community.nsfw === true).map((community) => community.address);
|
||||
}
|
||||
// filter === 'sfw'
|
||||
return directories.filter((community) => community.nsfw !== true).map((community) => community.address);
|
||||
}, [directories, filter]);
|
||||
};
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import useThemeStore from '../stores/use-theme-store';
|
||||
import { useDefaultSubplebbits } from './use-default-subplebbits';
|
||||
import { useDirectories } from './use-directories';
|
||||
import { isAllView, isHomeView, isNotFoundView, isPendingPostView, isSubscriptionsView, isModView } from '../lib/utils/view-utils';
|
||||
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||
|
||||
const useInitialTheme = (pendingPostSubplebbitAddress?: string) => {
|
||||
const location = useLocation();
|
||||
const { subplebbitAddress: paramsSubplebbitAddress, accountCommentIndex } = useParams<{ subplebbitAddress: string; accountCommentIndex?: string }>();
|
||||
const { communityAddress: paramsSubplebbitAddress, accountCommentIndex } = useParams<{ communityAddress: string; accountCommentIndex?: string }>();
|
||||
const commentIndex = accountCommentIndex ? parseInt(accountCommentIndex) : undefined;
|
||||
const pendingPost = useAccountComment({ commentIndex });
|
||||
// Subscribe to the actual themes data, not just functions
|
||||
const themes = useThemeStore((state) => state.themes);
|
||||
const subplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
const params = useParams();
|
||||
const isInHomeView = isHomeView(location.pathname);
|
||||
const isInNotFoundView = isNotFoundView(location.pathname, params);
|
||||
@@ -25,10 +25,10 @@ const useInitialTheme = (pendingPostSubplebbitAddress?: string) => {
|
||||
let theme = 'yotsuba';
|
||||
|
||||
if (isInPendingPostView) {
|
||||
const subplebbitAddress = pendingPostSubplebbitAddress || pendingPost?.subplebbitAddress;
|
||||
if (subplebbitAddress) {
|
||||
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
|
||||
if (subplebbit?.nsfw) {
|
||||
const communityAddress = pendingPostSubplebbitAddress || pendingPost?.communityAddress;
|
||||
if (communityAddress) {
|
||||
const community = directories.find((s) => s.address === communityAddress);
|
||||
if (community?.nsfw) {
|
||||
theme = themes.nsfw || 'yotsuba';
|
||||
} else {
|
||||
theme = themes.sfw || 'yotsuba-b';
|
||||
@@ -41,8 +41,8 @@ const useInitialTheme = (pendingPostSubplebbitAddress?: string) => {
|
||||
} else if (isInHomeView || isInNotFoundView) {
|
||||
theme = 'yotsuba';
|
||||
} else if (paramsSubplebbitAddress) {
|
||||
const subplebbit = subplebbits.find((s) => s.address === paramsSubplebbitAddress);
|
||||
if (subplebbit?.nsfw) {
|
||||
const community = directories.find((s) => s.address === paramsSubplebbitAddress);
|
||||
if (community?.nsfw) {
|
||||
theme = themes.nsfw || 'yotsuba';
|
||||
} else {
|
||||
theme = themes.sfw || 'yotsuba-b';
|
||||
@@ -59,7 +59,7 @@ const useInitialTheme = (pendingPostSubplebbitAddress?: string) => {
|
||||
isInNotFoundView,
|
||||
paramsSubplebbitAddress,
|
||||
themes,
|
||||
subplebbits,
|
||||
directories,
|
||||
pendingPostSubplebbitAddress,
|
||||
pendingPost,
|
||||
]);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useDefaultSubplebbits } from './use-default-subplebbits';
|
||||
import { useDirectories } from './use-directories';
|
||||
import { getSubplebbitAddress, getBoardPath } from '../lib/utils/route-utils';
|
||||
|
||||
/**
|
||||
* Hook to resolve boardIdentifier from URL params to subplebbitAddress
|
||||
* Handles both directory codes (e.g., "biz") and full addresses (e.g., "someboard.eth")
|
||||
*
|
||||
* Performance: Uses useMemo to avoid recalculating when params/defaultSubplebbits haven't changed.
|
||||
* The defaultSubplebbits reference is stable (from cache) after initial load, so memoization works effectively.
|
||||
* Performance: Uses useMemo to avoid recalculating when params/directories haven't changed.
|
||||
* The directories reference is stable (from cache) after initial load, so memoization works effectively.
|
||||
*/
|
||||
export const useResolvedSubplebbitAddress = (): string | undefined => {
|
||||
const params = useParams();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
// Try boardIdentifier first (new format), then subplebbitAddress (old format for backward compatibility)
|
||||
const boardIdentifier = params.boardIdentifier || params.subplebbitAddress;
|
||||
@@ -24,18 +24,18 @@ export const useResolvedSubplebbitAddress = (): string | undefined => {
|
||||
|
||||
// Resolve directory code to address if needed
|
||||
// getSubplebbitAddress uses internal caching, so this is efficient
|
||||
return getSubplebbitAddress(boardIdentifier, defaultSubplebbits);
|
||||
}, [boardIdentifier, defaultSubplebbits]);
|
||||
return getSubplebbitAddress(boardIdentifier, directories);
|
||||
}, [boardIdentifier, directories]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get the board path (directory code or address) for use in links
|
||||
*
|
||||
* Performance: Uses useMemo to avoid recalculating when subplebbitAddress/defaultSubplebbits haven't changed.
|
||||
* The defaultSubplebbits reference is stable (from cache) after initial load, so memoization works effectively.
|
||||
* Performance: Uses useMemo to avoid recalculating when subplebbitAddress/directories haven't changed.
|
||||
* The directories reference is stable (from cache) after initial load, so memoization works effectively.
|
||||
*/
|
||||
export const useBoardPath = (subplebbitAddress: string | undefined): string | undefined => {
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
return useMemo(() => {
|
||||
if (!subplebbitAddress) {
|
||||
@@ -43,6 +43,6 @@ export const useBoardPath = (subplebbitAddress: string | undefined): string | un
|
||||
}
|
||||
|
||||
// getBoardPath uses internal caching, so this is efficient
|
||||
return getBoardPath(subplebbitAddress, defaultSubplebbits);
|
||||
}, [subplebbitAddress, defaultSubplebbits]);
|
||||
return getBoardPath(subplebbitAddress, directories);
|
||||
}, [subplebbitAddress, directories]);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useCallback, useMemo } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { isAllView, isSubscriptionsView, isModView } from '../lib/utils/view-utils';
|
||||
import useThemeStore from '../stores/use-theme-store';
|
||||
import { useDefaultSubplebbits } from './use-default-subplebbits';
|
||||
import { useDirectories } from './use-directories';
|
||||
import { useResolvedSubplebbitAddress } from './use-resolved-subplebbit-address';
|
||||
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||
import useSpecialThemeStore from '../stores/use-special-theme-store';
|
||||
@@ -29,7 +29,7 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
const setThemeStore = useThemeStore((state) => state.setTheme);
|
||||
// Subscribe to the actual themes data, not just the getter function
|
||||
const themes = useThemeStore((state) => state.themes);
|
||||
const subplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
@@ -64,7 +64,7 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
if (isInAllView || isInSubscriptionsView || isInModView) {
|
||||
storedTheme = themes.nsfw;
|
||||
} else if (subplebbitAddress) {
|
||||
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
|
||||
const subplebbit = directories.find((s) => s.address === subplebbitAddress);
|
||||
if (subplebbit?.nsfw) {
|
||||
storedTheme = themes.nsfw;
|
||||
} else {
|
||||
@@ -73,7 +73,7 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
}
|
||||
|
||||
return storedTheme || 'yotsuba';
|
||||
}, [location.pathname, isEnabled, isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress, subplebbits, themes]);
|
||||
}, [location.pathname, isEnabled, isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress, directories, themes]);
|
||||
|
||||
// Update DOM class when theme changes
|
||||
useEffect(() => {
|
||||
@@ -85,7 +85,7 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
if (isInAllView || isInSubscriptionsView || isInModView) {
|
||||
await setThemeStore('nsfw', newTheme);
|
||||
} else if (subplebbitAddress) {
|
||||
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
|
||||
const subplebbit = directories.find((s) => s.address === subplebbitAddress);
|
||||
if (subplebbit?.nsfw) {
|
||||
await setThemeStore('nsfw', newTheme);
|
||||
} else {
|
||||
@@ -93,7 +93,7 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
}
|
||||
}
|
||||
},
|
||||
[isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress, subplebbits, setThemeStore],
|
||||
[isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress, directories, setThemeStore],
|
||||
);
|
||||
|
||||
return [currentTheme, setSubplebbitTheme];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MultisubSubplebbit } from '../../hooks/use-default-subplebbits';
|
||||
import { DirectoryCommunity } from '../../hooks/use-directories';
|
||||
|
||||
/**
|
||||
* Extract directory short code from title (e.g., "/biz/ - Business & Finance" -> "biz")
|
||||
@@ -9,79 +9,79 @@ export const extractDirectoryFromTitle = (title: string): string | null => {
|
||||
};
|
||||
|
||||
// Cache for directory-to-address map
|
||||
let cachedSubplebbitsForDirectory: MultisubSubplebbit[] | null = null;
|
||||
let cachedCommunitiesForDirectory: DirectoryCommunity[] | null = null;
|
||||
let cachedDirectoryToAddressMap: Map<string, string> | null = null;
|
||||
|
||||
// Cache for address-to-directory map
|
||||
let cachedSubplebbitsForAddress: MultisubSubplebbit[] | null = null;
|
||||
let cachedCommunitiesForAddress: DirectoryCommunity[] | null = null;
|
||||
let cachedAddressToDirectoryMap: Map<string, string> | null = null;
|
||||
|
||||
/**
|
||||
* Create a map from directory codes to subplebbit addresses
|
||||
* Uses caching to avoid recreating the map when subplebbits array hasn't changed
|
||||
* Create a map from directory codes to community addresses
|
||||
* Uses caching to avoid recreating the map when communities array hasn't changed
|
||||
*/
|
||||
export const getDirectoryToAddressMap = (subplebbits: MultisubSubplebbit[]): Map<string, string> => {
|
||||
export const getDirectoryToAddressMap = (communities: DirectoryCommunity[]): Map<string, string> => {
|
||||
// Check if we can use cached map (same array reference)
|
||||
if (cachedDirectoryToAddressMap && cachedSubplebbitsForDirectory === subplebbits) {
|
||||
if (cachedDirectoryToAddressMap && cachedCommunitiesForDirectory === communities) {
|
||||
return cachedDirectoryToAddressMap;
|
||||
}
|
||||
|
||||
const map = new Map<string, string>();
|
||||
for (const subplebbit of subplebbits) {
|
||||
if (subplebbit.title) {
|
||||
const directory = extractDirectoryFromTitle(subplebbit.title);
|
||||
if (directory && subplebbit.address) {
|
||||
map.set(directory, subplebbit.address);
|
||||
for (const community of communities) {
|
||||
if (community.title) {
|
||||
const directory = extractDirectoryFromTitle(community.title);
|
||||
if (directory && community.address) {
|
||||
map.set(directory, community.address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the map and array reference
|
||||
cachedDirectoryToAddressMap = map;
|
||||
cachedSubplebbitsForDirectory = subplebbits;
|
||||
cachedCommunitiesForDirectory = communities;
|
||||
return map;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a map from subplebbit addresses to directory codes
|
||||
* Uses caching to avoid recreating the map when subplebbits array hasn't changed
|
||||
* Create a map from community addresses to directory codes
|
||||
* Uses caching to avoid recreating the map when communities array hasn't changed
|
||||
*/
|
||||
export const getAddressToDirectoryMap = (subplebbits: MultisubSubplebbit[]): Map<string, string> => {
|
||||
export const getAddressToDirectoryMap = (communities: DirectoryCommunity[]): Map<string, string> => {
|
||||
// Check if we can use cached map (same array reference)
|
||||
if (cachedAddressToDirectoryMap && cachedSubplebbitsForAddress === subplebbits) {
|
||||
if (cachedAddressToDirectoryMap && cachedCommunitiesForAddress === communities) {
|
||||
return cachedAddressToDirectoryMap;
|
||||
}
|
||||
|
||||
const map = new Map<string, string>();
|
||||
for (const subplebbit of subplebbits) {
|
||||
if (subplebbit.title && subplebbit.address) {
|
||||
const directory = extractDirectoryFromTitle(subplebbit.title);
|
||||
for (const community of communities) {
|
||||
if (community.title && community.address) {
|
||||
const directory = extractDirectoryFromTitle(community.title);
|
||||
if (directory) {
|
||||
map.set(subplebbit.address, directory);
|
||||
map.set(community.address, directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the map and array reference
|
||||
cachedAddressToDirectoryMap = map;
|
||||
cachedSubplebbitsForAddress = subplebbits;
|
||||
cachedCommunitiesForAddress = communities;
|
||||
return map;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert subplebbit address to URL path (directory code if available, otherwise full address)
|
||||
* Convert community address to URL path (directory code if available, otherwise full address)
|
||||
*/
|
||||
export const getBoardPath = (subplebbitAddress: string, subplebbits: MultisubSubplebbit[]): string => {
|
||||
const addressToDirectory = getAddressToDirectoryMap(subplebbits);
|
||||
const directory = addressToDirectory.get(subplebbitAddress);
|
||||
return directory || subplebbitAddress;
|
||||
export const getBoardPath = (communityAddress: string, communities: DirectoryCommunity[]): string => {
|
||||
const addressToDirectory = getAddressToDirectoryMap(communities);
|
||||
const directory = addressToDirectory.get(communityAddress);
|
||||
return directory || communityAddress;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert URL path (directory code or address) to subplebbit address
|
||||
* Convert URL path (directory code or address) to community address
|
||||
*/
|
||||
export const getSubplebbitAddress = (boardIdentifier: string, subplebbits: MultisubSubplebbit[]): string => {
|
||||
const directoryToAddress = getDirectoryToAddressMap(subplebbits);
|
||||
export const getSubplebbitAddress = (boardIdentifier: string, communities: DirectoryCommunity[]): string => {
|
||||
const directoryToAddress = getDirectoryToAddressMap(communities);
|
||||
|
||||
// Check if it's a directory code
|
||||
const address = directoryToAddress.get(boardIdentifier);
|
||||
@@ -96,8 +96,8 @@ export const getSubplebbitAddress = (boardIdentifier: string, subplebbits: Multi
|
||||
/**
|
||||
* Check if an identifier is a directory short code
|
||||
*/
|
||||
export const isDirectoryBoard = (identifier: string, subplebbits: MultisubSubplebbit[]): boolean => {
|
||||
const directoryToAddress = getDirectoryToAddressMap(subplebbits);
|
||||
export const isDirectoryBoard = (identifier: string, communities: DirectoryCommunity[]): boolean => {
|
||||
const directoryToAddress = getDirectoryToAddressMap(communities);
|
||||
return directoryToAddress.has(identifier);
|
||||
};
|
||||
|
||||
|
||||
+11
-32
@@ -7,8 +7,8 @@ import { Trans, useTranslation } from 'react-i18next';
|
||||
import styles from './board.module.css';
|
||||
import { shouldShowSnow } from '../../lib/snow';
|
||||
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import { useDefaultSubplebbitAddresses, useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||
import { useFilteredDefaultSubplebbitAddresses } from '../../hooks/use-filtered-default-subplebbit-addresses';
|
||||
import { useDirectoryAddresses, useDirectories } from '../../hooks/use-directories';
|
||||
import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses';
|
||||
import { useResolvedSubplebbitAddress, useBoardPath } from '../../hooks/use-resolved-subplebbit-address';
|
||||
import { useFeedStateString } from '../../hooks/use-state-string';
|
||||
import useTimeFilter, { timeFilterNameToSeconds } from '../../hooks/use-time-filter';
|
||||
@@ -194,18 +194,18 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
const isInSubscriptionsView = viewType ? viewType === 'subs' : false;
|
||||
const isInModView = viewType ? viewType === 'mod' : false;
|
||||
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
const resolvedAddressFromUrl = useResolvedSubplebbitAddress();
|
||||
const subplebbitAddress = useMemo(() => {
|
||||
if (boardIdentifierProp) {
|
||||
return getSubplebbitAddress(boardIdentifierProp, defaultSubplebbits);
|
||||
return getSubplebbitAddress(boardIdentifierProp, directories);
|
||||
}
|
||||
return resolvedAddressFromUrl;
|
||||
}, [boardIdentifierProp, defaultSubplebbits, resolvedAddressFromUrl]);
|
||||
}, [boardIdentifierProp, directories, resolvedAddressFromUrl]);
|
||||
|
||||
const boardPath = useBoardPath(subplebbitAddress);
|
||||
const defaultSubplebbitAddresses = useDefaultSubplebbitAddresses();
|
||||
const filteredDefaultSubplebbitAddresses = useFilteredDefaultSubplebbitAddresses();
|
||||
const directoryAddresses = useDirectoryAddresses();
|
||||
const filteredDirectoryAddresses = useFilteredDirectoryAddresses();
|
||||
|
||||
const account = useAccount();
|
||||
const subscriptions = account?.subscriptions;
|
||||
@@ -215,7 +215,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
|
||||
const subplebbitAddresses = useMemo(() => {
|
||||
if (isInAllView) {
|
||||
return filteredDefaultSubplebbitAddresses;
|
||||
return filteredDirectoryAddresses;
|
||||
}
|
||||
if (isInSubscriptionsView) {
|
||||
return subscriptions || [];
|
||||
@@ -224,16 +224,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
return accountSubplebbitAddresses;
|
||||
}
|
||||
return [subplebbitAddress];
|
||||
}, [
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
subplebbitAddress,
|
||||
defaultSubplebbitAddresses,
|
||||
filteredDefaultSubplebbitAddresses,
|
||||
subscriptions,
|
||||
accountSubplebbitAddresses,
|
||||
]);
|
||||
}, [isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress, directoryAddresses, filteredDirectoryAddresses, subscriptions, accountSubplebbitAddresses]);
|
||||
|
||||
const { sortType } = useSortingStore();
|
||||
const { timeFilterSeconds: timeFilterSecondsFromHook, timeFilterName: timeFilterNameFromHook } = useTimeFilter();
|
||||
@@ -442,7 +433,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
const boardIdentifier = params.boardIdentifier || boardIdentifierProp;
|
||||
const isDirectory = boardIdentifier ? isDirectoryBoard(boardIdentifier, defaultSubplebbits) : false;
|
||||
const isDirectory = boardIdentifier ? isDirectoryBoard(boardIdentifier, directories) : false;
|
||||
|
||||
let boardTitle: string;
|
||||
if (isInAllView) {
|
||||
@@ -457,19 +448,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
boardTitle = title ? title : shortAddress || subplebbitAddress || '';
|
||||
}
|
||||
document.title = boardTitle + ' - 5chan';
|
||||
}, [
|
||||
title,
|
||||
shortAddress,
|
||||
subplebbitAddress,
|
||||
isVisible,
|
||||
params.boardIdentifier,
|
||||
boardIdentifierProp,
|
||||
defaultSubplebbits,
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
t,
|
||||
]);
|
||||
}, [title, shortAddress, subplebbitAddress, isVisible, params.boardIdentifier, boardIdentifierProp, directories, isInAllView, isInSubscriptionsView, isInModView, t]);
|
||||
|
||||
const shouldShowErrorToUser = subplebbitError?.message && feed.length === 0;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Comment, useAccount, useFeed, useSubplebbit, useBlock, useAccountCommen
|
||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import useCatalogFeedRows from '../../hooks/use-catalog-feed-rows';
|
||||
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||
import { useFilteredDefaultSubplebbitAddresses } from '../../hooks/use-filtered-default-subplebbit-addresses';
|
||||
import { useDirectories } from '../../hooks/use-directories';
|
||||
import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses';
|
||||
import { useResolvedSubplebbitAddress, useBoardPath } from '../../hooks/use-resolved-subplebbit-address';
|
||||
import { useFeedStateString } from '../../hooks/use-state-string';
|
||||
import useTimeFilter, { timeFilterNameToSeconds } from '../../hooks/use-time-filter';
|
||||
@@ -302,32 +302,32 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
const isInAllView = viewType ? viewType === 'all' : false;
|
||||
const isInSubscriptionsView = viewType ? viewType === 'subs' : false;
|
||||
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
const resolvedAddressFromUrl = useResolvedSubplebbitAddress();
|
||||
const subplebbitAddress = useMemo(() => {
|
||||
if (boardIdentifierProp) {
|
||||
return getSubplebbitAddress(boardIdentifierProp, defaultSubplebbits);
|
||||
return getSubplebbitAddress(boardIdentifierProp, directories);
|
||||
}
|
||||
return resolvedAddressFromUrl;
|
||||
}, [boardIdentifierProp, defaultSubplebbits, resolvedAddressFromUrl]);
|
||||
}, [boardIdentifierProp, directories, resolvedAddressFromUrl]);
|
||||
|
||||
const boardPath = useBoardPath(subplebbitAddress);
|
||||
const { showTextOnlyThreads, filterItems, searchText, clearMatchedFilters } = useCatalogFiltersStore();
|
||||
|
||||
const account = useAccount();
|
||||
const subscriptions = account?.subscriptions;
|
||||
const filteredDefaultSubplebbitAddresses = useFilteredDefaultSubplebbitAddresses();
|
||||
const filteredDirectoryAddresses = useFilteredDirectoryAddresses();
|
||||
|
||||
const subplebbitAddresses = useMemo(() => {
|
||||
if (isInAllView) {
|
||||
return filteredDefaultSubplebbitAddresses;
|
||||
return filteredDirectoryAddresses;
|
||||
}
|
||||
if (isInSubscriptionsView) {
|
||||
return (subscriptions || []).filter(Boolean); // Filter out any undefined/null values
|
||||
}
|
||||
// Only include subplebbitAddress if it's defined
|
||||
return subplebbitAddress ? [subplebbitAddress] : [];
|
||||
}, [isInAllView, isInSubscriptionsView, subplebbitAddress, filteredDefaultSubplebbitAddresses, subscriptions]);
|
||||
}, [isInAllView, isInSubscriptionsView, subplebbitAddress, filteredDirectoryAddresses, subscriptions]);
|
||||
|
||||
const { imageSize } = useCatalogStyleStore();
|
||||
const columnWidth = imageSize === 'Large' ? 270 : 180;
|
||||
@@ -601,7 +601,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
const boardIdentifier = params.boardIdentifier || boardIdentifierProp;
|
||||
const isDirectory = boardIdentifier ? isDirectoryBoard(boardIdentifier, defaultSubplebbits) : false;
|
||||
const isDirectory = boardIdentifier ? isDirectoryBoard(boardIdentifier, directories) : false;
|
||||
|
||||
let documentTitle: string;
|
||||
if (isInAllView) {
|
||||
@@ -614,7 +614,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
documentTitle = title ? title : shortAddress || subplebbitAddress || '';
|
||||
}
|
||||
document.title = documentTitle + ` - ${t('catalog')} - 5chan`;
|
||||
}, [title, shortAddress, subplebbitAddress, isInAllView, isInSubscriptionsView, t, isVisible, params.boardIdentifier, boardIdentifierProp, defaultSubplebbits]);
|
||||
}, [title, shortAddress, subplebbitAddress, isInAllView, isInSubscriptionsView, t, isVisible, params.boardIdentifier, boardIdentifierProp, directories]);
|
||||
|
||||
// Clear matched filters when component mounts or when subplebbit changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDefaultSubplebbitsState, useDefaultSubplebbits, MultisubSubplebbit } from '../../../hooks/use-default-subplebbits';
|
||||
import { useDirectoriesState, useDirectories, DirectoryCommunity } from '../../../hooks/use-directories';
|
||||
import { getBoardPath } from '../../../lib/utils/route-utils';
|
||||
import useDisclaimerModalStore from '../../../stores/use-disclaimer-modal-store';
|
||||
import useDirectoryModalStore from '../../../stores/use-directory-modal-store';
|
||||
@@ -9,7 +9,7 @@ import BoardsFilterModal from './boards-filter-modal';
|
||||
import styles from '../home.module.css';
|
||||
|
||||
// Helper function to find board address by matching title pattern
|
||||
const findBoardAddress = (multisub: MultisubSubplebbit[], titlePattern: string): string | null => {
|
||||
const findBoardAddress = (multisub: DirectoryCommunity[], titlePattern: string): string | null => {
|
||||
const entry = multisub.find((ms) => ms.title === titlePattern);
|
||||
return entry?.address || null;
|
||||
};
|
||||
@@ -27,25 +27,25 @@ const NSFWBadge = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {
|
||||
const BoardsList = ({ multisub }: { multisub: DirectoryCommunity[] }) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { error } = useDefaultSubplebbitsState();
|
||||
const { error } = useDirectoriesState();
|
||||
const { showDisclaimerModal } = useDisclaimerModalStore();
|
||||
const { openDirectoryModal } = useDirectoryModalStore();
|
||||
const { useCatalogLinks, boardFilter } = useBoardsFilterStore();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>, address: string) => {
|
||||
e.preventDefault();
|
||||
const boardPath = getBoardPath(address, defaultSubplebbits);
|
||||
const boardPath = getBoardPath(address, directories);
|
||||
showDisclaimerModal(address, navigate, boardPath);
|
||||
};
|
||||
|
||||
// Helper to generate link URL with optional catalog suffix
|
||||
const getBoardLink = (address: string | null): string => {
|
||||
if (!address) return '#';
|
||||
const boardPath = getBoardPath(address, defaultSubplebbits);
|
||||
const boardPath = getBoardPath(address, directories);
|
||||
return `/${boardPath}${useCatalogLinks ? '/catalog' : ''}`;
|
||||
};
|
||||
|
||||
|
||||
+14
-14
@@ -3,7 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useSubplebbits } from '@plebbit/plebbit-react-hooks';
|
||||
import styles from './home.module.css';
|
||||
import { useDefaultSubplebbits, useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
|
||||
import { useDirectories, useDirectoryAddresses } from '../../hooks/use-directories';
|
||||
import { SubplebbitStatsCollector, useSubplebbitsStatsStore } from '../../hooks/use-subplebbits-stats';
|
||||
import PopularThreadsBox from './popular-threads-box';
|
||||
import BoardsList from './boards-list';
|
||||
@@ -14,19 +14,19 @@ import DirectoryModal from '../../components/directory-modal';
|
||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||
import _ from 'lodash';
|
||||
|
||||
// https://github.com/plebbit/lists/blob/master/5chan-multisub.json
|
||||
// https://github.com/plebbit/lists/blob/master/5chan-directories.json
|
||||
|
||||
const SearchBar = () => {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
const handleSearchSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
const searchInput = searchInputRef.current?.value;
|
||||
if (searchInput) {
|
||||
const boardPath = getBoardPath(searchInput, defaultSubplebbits);
|
||||
const boardPath = getBoardPath(searchInput, directories);
|
||||
navigate(`/${boardPath}`);
|
||||
}
|
||||
};
|
||||
@@ -73,7 +73,7 @@ const InfoBox = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Stats = ({ subplebbitAddresses }: { subplebbitAddresses: string[] }) => {
|
||||
const Stats = ({ directoryAddresses }: { directoryAddresses: string[] }) => {
|
||||
const { t } = useTranslation();
|
||||
const subplebbitsStats = useSubplebbitsStatsStore((state) => state.subplebbitsStats);
|
||||
|
||||
@@ -82,7 +82,7 @@ const Stats = ({ subplebbitAddresses }: { subplebbitAddresses: string[] }) => {
|
||||
let currentUsers = 0;
|
||||
let boardsTracked = 0;
|
||||
|
||||
subplebbitAddresses.forEach((address) => {
|
||||
directoryAddresses.forEach((address) => {
|
||||
const stat = subplebbitsStats[address];
|
||||
if (stat) {
|
||||
totalPosts += stat.allPostCount || 0;
|
||||
@@ -92,12 +92,12 @@ const Stats = ({ subplebbitAddresses }: { subplebbitAddresses: string[] }) => {
|
||||
});
|
||||
|
||||
return { totalPosts, currentUsers, boardsTracked };
|
||||
}, [subplebbitsStats, subplebbitAddresses]);
|
||||
}, [subplebbitsStats, directoryAddresses]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Render collectors to fetch stats for each subplebbit */}
|
||||
{subplebbitAddresses.map((address) => (
|
||||
{directoryAddresses.map((address) => (
|
||||
<SubplebbitStatsCollector key={address} subplebbitAddress={address} />
|
||||
))}
|
||||
<div className={styles.box}>
|
||||
@@ -192,9 +192,9 @@ export const HomeLogo = () => {
|
||||
};
|
||||
|
||||
const Home = () => {
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const subplebbitAddresses = useDefaultSubplebbitAddresses();
|
||||
const { subplebbits } = useSubplebbits({ subplebbitAddresses });
|
||||
const directories = useDirectories();
|
||||
const directoryAddresses = useDirectoryAddresses();
|
||||
const { subplebbits } = useSubplebbits({ subplebbitAddresses: directoryAddresses });
|
||||
const { closeDirectoryModal } = useDirectoryModalStore();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -216,9 +216,9 @@ const Home = () => {
|
||||
<HomeLogo />
|
||||
<SearchBar />
|
||||
<InfoBox />
|
||||
<BoardsList multisub={defaultSubplebbits} />
|
||||
<PopularThreadsBox multisub={defaultSubplebbits} subplebbits={subplebbits} />
|
||||
<Stats subplebbitAddresses={subplebbitAddresses} />
|
||||
<BoardsList multisub={directories} />
|
||||
<PopularThreadsBox directories={directories} subplebbits={subplebbits} />
|
||||
<Stats directoryAddresses={directoryAddresses} />
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -9,13 +9,13 @@ import { getCommentMediaInfo } from '../../../lib/utils/media-utils';
|
||||
import { CatalogPostMedia } from '../../../components/catalog-row';
|
||||
import LoadingEllipsis from '../../../components/loading-ellipsis';
|
||||
import BoxModal from '../box-modal';
|
||||
import { MultisubSubplebbit, useDefaultSubplebbits } from '../../../hooks/use-default-subplebbits';
|
||||
import { DirectoryCommunity } from '../../../hooks/use-directories';
|
||||
import { getBoardPath } from '../../../lib/utils/route-utils';
|
||||
import { removeMarkdown } from '../../../lib/utils/post-utils';
|
||||
|
||||
interface PopularThreadProps {
|
||||
post: Comment;
|
||||
multisub: MultisubSubplebbit[];
|
||||
directories: DirectoryCommunity[];
|
||||
}
|
||||
|
||||
export const ContentPreview = ({ content, maxLength = 99 }: { content: string; maxLength?: number }) => {
|
||||
@@ -27,15 +27,14 @@ export const ContentPreview = ({ content, maxLength = 99 }: { content: string; m
|
||||
|
||||
// Memoize to prevent rerenders when parent rerenders due to updatingState
|
||||
const PopularThreadCard = memo(
|
||||
({ post, multisub }: PopularThreadProps) => {
|
||||
({ post, directories }: PopularThreadProps) => {
|
||||
const { cid, content, link, linkHeight, linkWidth, subplebbitAddress, thumbnailUrl, title } = post || {};
|
||||
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
|
||||
// Find the matching MultisubSubplebbit entry and get its title
|
||||
const multisubEntry = multisub.find((ms) => ms?.address === subplebbitAddress);
|
||||
const boardTitle = multisubEntry?.title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, defaultSubplebbits) : '';
|
||||
// Find the matching DirectoryCommunity entry and get its title
|
||||
const directoriesEntry = directories.find((ms) => ms?.address === subplebbitAddress);
|
||||
const boardTitle = directoriesEntry?.title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : '';
|
||||
|
||||
return (
|
||||
<div className={styles.popularThread} key={cid}>
|
||||
@@ -57,37 +56,37 @@ const PopularThreadCard = memo(
|
||||
</div>
|
||||
);
|
||||
},
|
||||
// Custom equality: rerender if post.cid or multisub entry title changes
|
||||
// Custom equality: rerender if post.cid or directories entry title changes
|
||||
(prevProps, nextProps) => {
|
||||
if (prevProps.post?.cid !== nextProps.post?.cid) return false;
|
||||
// Compare the relevant multisub entry for this post's subplebbitAddress
|
||||
const prevEntry = prevProps.multisub.find((ms) => ms?.address === prevProps.post?.subplebbitAddress);
|
||||
const nextEntry = nextProps.multisub.find((ms) => ms?.address === nextProps.post?.subplebbitAddress);
|
||||
// Compare the relevant directories entry for this post's subplebbitAddress
|
||||
const prevEntry = prevProps.directories.find((ms) => ms?.address === prevProps.post?.subplebbitAddress);
|
||||
const nextEntry = nextProps.directories.find((ms) => ms?.address === nextProps.post?.subplebbitAddress);
|
||||
return prevEntry?.title === nextEntry?.title;
|
||||
},
|
||||
);
|
||||
|
||||
const PopularThreadsBox = ({ multisub, subplebbits }: { multisub: MultisubSubplebbit[]; subplebbits: any }) => {
|
||||
const PopularThreadsBox = ({ directories, subplebbits }: { directories: DirectoryCommunity[]; subplebbits: any }) => {
|
||||
const { t } = useTranslation();
|
||||
const { showWorksafeContentOnly, showNsfwContentOnly } = usePopularThreadsOptionsStore();
|
||||
|
||||
const getFilteredSubplebbits = () => {
|
||||
if (showWorksafeContentOnly) {
|
||||
return subplebbits.filter((sub: Subplebbit) => {
|
||||
const multisubEntry = multisub.find((ms) => ms?.address === sub?.address);
|
||||
return multisubEntry ? !multisubEntry.nsfw : true;
|
||||
const directoriesEntry = directories.find((ms) => ms?.address === sub?.address);
|
||||
return directoriesEntry ? !directoriesEntry.nsfw : true;
|
||||
});
|
||||
}
|
||||
if (showNsfwContentOnly) {
|
||||
return subplebbits.filter((sub: Subplebbit) => {
|
||||
const multisubEntry = multisub.find((ms) => ms?.address === sub?.address);
|
||||
return multisubEntry ? multisubEntry.nsfw : false;
|
||||
const directoriesEntry = directories.find((ms) => ms?.address === sub?.address);
|
||||
return directoriesEntry ? directoriesEntry.nsfw : false;
|
||||
});
|
||||
}
|
||||
return subplebbits;
|
||||
};
|
||||
|
||||
const filteredSubplebbits = useMemo(getFilteredSubplebbits, [subplebbits, showWorksafeContentOnly, showNsfwContentOnly, multisub]);
|
||||
const filteredSubplebbits = useMemo(getFilteredSubplebbits, [subplebbits, showWorksafeContentOnly, showNsfwContentOnly, directories]);
|
||||
const { popularPosts } = usePopularPosts(filteredSubplebbits);
|
||||
const isLoading = popularPosts.length === 0;
|
||||
|
||||
@@ -98,7 +97,11 @@ const PopularThreadsBox = ({ multisub, subplebbits }: { multisub: MultisubSubple
|
||||
<BoxModal />
|
||||
</div>
|
||||
<div className={`${styles.boxContent} ${styles.popularThreads} ${isLoading ? styles.popularThreadsLoading : ''}`}>
|
||||
{isLoading ? <LoadingEllipsis string={t('loading')} /> : popularPosts.map((post: any) => <PopularThreadCard key={post.cid} post={post} multisub={multisub} />)}
|
||||
{isLoading ? (
|
||||
<LoadingEllipsis string={t('loading')} />
|
||||
) : (
|
||||
popularPosts.map((post: any) => <PopularThreadCard key={post.cid} post={post} directories={directories} />)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useParams, Link } from 'react-router-dom';
|
||||
import { useFeed, Comment, usePublishCommentModeration, useEditedComment, useSubplebbit, useComment } from '@plebbit/plebbit-react-hooks';
|
||||
import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import styles from './mod-queue.module.css';
|
||||
import useModQueueStore from '../../stores/use-mod-queue-store';
|
||||
import { useAccountSubplebbitsWithMetadata } from '../../hooks/use-account-subplebbits-with-metadata';
|
||||
@@ -12,7 +11,7 @@ import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import ErrorDisplay from '../../components/error-display/error-display';
|
||||
import { useFeedStateString } from '../../hooks/use-state-string';
|
||||
import { getSubplebbitAddress, getBoardPath } from '../../lib/utils/route-utils';
|
||||
import { useDefaultSubplebbits, MultisubSubplebbit } from '../../hooks/use-default-subplebbits';
|
||||
import { useDirectories, DirectoryCommunity } from '../../hooks/use-directories';
|
||||
import { useBoardPath } from '../../hooks/use-resolved-subplebbit-address';
|
||||
import { getHasThumbnail, getCommentMediaInfo } from '../../lib/utils/media-utils';
|
||||
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||
@@ -430,10 +429,10 @@ const ModQueueFeedPost = ({ comment }: { comment: Comment }) => {
|
||||
};
|
||||
|
||||
interface ModQueueBoardFilterProps {
|
||||
subplebbits: MultisubSubplebbit[];
|
||||
communities: DirectoryCommunity[];
|
||||
}
|
||||
|
||||
const ModQueueBoardFilter = ({ subplebbits }: ModQueueBoardFilterProps) => {
|
||||
const ModQueueBoardFilter = ({ communities }: ModQueueBoardFilterProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { selectedBoardFilter, setSelectedBoardFilter } = useModQueueStore();
|
||||
|
||||
@@ -442,12 +441,12 @@ const ModQueueBoardFilter = ({ subplebbits }: ModQueueBoardFilterProps) => {
|
||||
setSelectedBoardFilter(value);
|
||||
};
|
||||
|
||||
if (!subplebbits || subplebbits.length === 0) {
|
||||
if (!communities || communities.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Default to first board if none selected
|
||||
const firstBoardAddress = subplebbits.find((sub) => sub.address)?.address;
|
||||
const firstBoardAddress = communities.find((sub) => sub.address)?.address;
|
||||
const currentFilter = selectedBoardFilter || firstBoardAddress || '';
|
||||
|
||||
// Auto-select first board if none is selected
|
||||
@@ -461,12 +460,12 @@ const ModQueueBoardFilter = ({ subplebbits }: ModQueueBoardFilterProps) => {
|
||||
<div className={styles.filterContainer}>
|
||||
<label>{t('filter_by_board')}:</label>
|
||||
<select value={currentFilter} onChange={handleChange}>
|
||||
{subplebbits.map((sub) => {
|
||||
{communities.map((sub) => {
|
||||
const address = sub.address;
|
||||
if (!address) return null;
|
||||
return (
|
||||
<option key={address} value={address}>
|
||||
/{getBoardPath(address, subplebbits)}/
|
||||
/{getBoardPath(address, communities)}/
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
@@ -594,7 +593,7 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
|
||||
(state) => {
|
||||
const activeAccountId = state.activeAccountId;
|
||||
const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined;
|
||||
const accountSubplebbits = activeAccount?.subplebbits || {};
|
||||
const accountSubplebbits = activeAccount?.communities || {};
|
||||
return Object.keys(accountSubplebbits);
|
||||
},
|
||||
(prev, next) => {
|
||||
@@ -603,14 +602,14 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
|
||||
},
|
||||
);
|
||||
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
const resolvedAddress = useMemo(() => {
|
||||
if (boardIdentifier) {
|
||||
return getSubplebbitAddress(boardIdentifier, defaultSubplebbits);
|
||||
return getSubplebbitAddress(boardIdentifier, directories);
|
||||
}
|
||||
return undefined;
|
||||
}, [boardIdentifier, defaultSubplebbits]);
|
||||
}, [boardIdentifier, directories]);
|
||||
|
||||
const subplebbitAddresses = useMemo(() => {
|
||||
if (resolvedAddress) {
|
||||
@@ -652,7 +651,7 @@ export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueV
|
||||
(state) => {
|
||||
const activeAccountId = state.activeAccountId;
|
||||
const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined;
|
||||
const accountSubplebbits = activeAccount?.subplebbits || {};
|
||||
const accountSubplebbits = activeAccount?.communities || {};
|
||||
return Object.keys(accountSubplebbits);
|
||||
},
|
||||
(prev, next) => {
|
||||
@@ -661,18 +660,18 @@ export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueV
|
||||
},
|
||||
);
|
||||
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
const boardIdentifier = propBoardIdentifier || params.boardIdentifier;
|
||||
|
||||
const resolvedAddress = useMemo(() => {
|
||||
if (boardIdentifier) {
|
||||
return getSubplebbitAddress(boardIdentifier, defaultSubplebbits);
|
||||
return getSubplebbitAddress(boardIdentifier, directories);
|
||||
}
|
||||
return undefined;
|
||||
}, [boardIdentifier, defaultSubplebbits]);
|
||||
}, [boardIdentifier, directories]);
|
||||
|
||||
const subplebbitsWithMetadata = useAccountSubplebbitsWithMetadata();
|
||||
const communitiesWithMetadata = useAccountSubplebbitsWithMetadata();
|
||||
|
||||
const subplebbitAddresses = useMemo(() => {
|
||||
if (resolvedAddress) {
|
||||
@@ -740,7 +739,7 @@ export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueV
|
||||
{!resolvedAddress && (
|
||||
<div className={styles.controls}>
|
||||
<div className={styles.controlsLeft}>
|
||||
<ModQueueBoardFilter subplebbits={subplebbitsWithMetadata} />
|
||||
<ModQueueBoardFilter communities={communitiesWithMetadata} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
||||
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||
import { useDirectories } from '../../hooks/use-directories';
|
||||
import { getSubplebbitAddress } from '../../lib/utils/route-utils';
|
||||
import { HomeLogo } from '../home';
|
||||
import styles from './not-found.module.css';
|
||||
@@ -18,8 +18,8 @@ const NotFound = () => {
|
||||
// Extract boardIdentifier from pathname (could be directory code or address)
|
||||
const pathParts = location.pathname.split('/').filter(Boolean);
|
||||
const boardIdentifier = pathParts[0] && pathParts[0] !== 'not-found' && pathParts[0] !== 'faq' ? pathParts[0] : '';
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const subplebbitAddress = boardIdentifier ? getSubplebbitAddress(boardIdentifier, defaultSubplebbits) : '';
|
||||
const directories = useDirectories();
|
||||
const subplebbitAddress = boardIdentifier ? getSubplebbitAddress(boardIdentifier, directories) : '';
|
||||
// Only subscribe to address and shortAddress to avoid rerenders from updatingState changes
|
||||
const address = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.address);
|
||||
const shortAddress = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.shortAddress);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useAccountComment, useAccountComments } from '@plebbit/plebbit-react-hooks';
|
||||
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||
import { useDirectories } from '../../hooks/use-directories';
|
||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||
import { Post } from '../post';
|
||||
|
||||
@@ -11,7 +11,7 @@ const PendingPost = () => {
|
||||
const commentIndex = accountCommentIndex ? parseInt(accountCommentIndex) : undefined;
|
||||
const post = useAccountComment({ commentIndex });
|
||||
const navigate = useNavigate();
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
useEffect(() => window.scrollTo(0, 0), []);
|
||||
|
||||
@@ -31,10 +31,10 @@ const PendingPost = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (post?.cid && post?.subplebbitAddress) {
|
||||
const boardPath = getBoardPath(post.subplebbitAddress, defaultSubplebbits);
|
||||
const boardPath = getBoardPath(post.subplebbitAddress, directories);
|
||||
navigate(`/${boardPath}/thread/${post.cid}`, { replace: true });
|
||||
}
|
||||
}, [post, navigate, defaultSubplebbits]);
|
||||
}, [post, navigate, directories]);
|
||||
|
||||
return <Post post={post} />;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { isAllView } from '../../lib/utils/view-utils';
|
||||
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
||||
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||
import { useDirectories } from '../../hooks/use-directories';
|
||||
import { isDirectoryBoard } from '../../lib/utils/route-utils';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import ErrorDisplay from '../../components/error-display/error-display';
|
||||
@@ -113,7 +113,7 @@ const PostPage = () => {
|
||||
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const { error: subplebbitError, shortAddress, title } = subplebbit || {};
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
|
||||
// if the comment is a reply, return the post comment instead, then the reply will be highlighted in the thread
|
||||
const postComment = useComment({ commentCid: comment?.postCid });
|
||||
@@ -133,7 +133,7 @@ const PostPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const boardIdentifier = params.boardIdentifier;
|
||||
const isDirectory = boardIdentifier ? isDirectoryBoard(boardIdentifier, defaultSubplebbits) : false;
|
||||
const isDirectory = boardIdentifier ? isDirectoryBoard(boardIdentifier, directories) : false;
|
||||
|
||||
let boardTitle: string;
|
||||
if (isInAllView) {
|
||||
@@ -147,7 +147,7 @@ const PostPage = () => {
|
||||
const postTitle = post?.title?.slice(0, 30) || post?.content?.slice(0, 30);
|
||||
const postTitlePart = postTitle ? ` - ${postTitle.trim()}...` : '';
|
||||
document.title = `${boardTitle}${postTitlePart} - 5chan`;
|
||||
}, [title, shortAddress, subplebbitAddress, post?.title, post?.content, isInAllView, t, params.boardIdentifier, defaultSubplebbits]);
|
||||
}, [title, shortAddress, subplebbitAddress, post?.title, post?.content, isInAllView, t, params.boardIdentifier, directories]);
|
||||
|
||||
const shouldShowCommentError = comment?.error?.message && !comment?.cid;
|
||||
const shouldShowPostError = post?.error && post?.replyCount > 0 && post?.replies?.length === 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState, FormEvent } from 'react';
|
||||
import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { Footer, HomeLogo } from '../home';
|
||||
import { useDefaultSubplebbits, MultisubSubplebbit } from '../../hooks/use-default-subplebbits';
|
||||
import { useDirectories, DirectoryCommunity } from '../../hooks/use-directories';
|
||||
import styles from './rules.module.css';
|
||||
|
||||
const getBoardShortCode = (title?: string): string => {
|
||||
@@ -16,7 +16,7 @@ const getBoardName = (title?: string): string => {
|
||||
return match ? match[1] : title;
|
||||
};
|
||||
|
||||
const BoardRulesDisplay = ({ subplebbitAddress, defaultSubplebbits }: { subplebbitAddress: string; defaultSubplebbits: MultisubSubplebbit[] }) => {
|
||||
const BoardRulesDisplay = ({ subplebbitAddress, directories }: { subplebbitAddress: string; directories: DirectoryCommunity[] }) => {
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const { rules, state, title, shortAddress } = subplebbit || {};
|
||||
|
||||
@@ -42,7 +42,7 @@ const BoardRulesDisplay = ({ subplebbitAddress, defaultSubplebbits }: { subplebb
|
||||
|
||||
const isLoaded = state === 'succeeded';
|
||||
|
||||
const defaultSub = defaultSubplebbits.find((sub) => sub.address === subplebbitAddress);
|
||||
const defaultSub = directories.find((sub) => sub.address === subplebbitAddress);
|
||||
let displayTitle: string;
|
||||
if (defaultSub?.title) {
|
||||
const shortCode = getBoardShortCode(defaultSub.title);
|
||||
@@ -87,11 +87,11 @@ const BoardRulesDisplay = ({ subplebbitAddress, defaultSubplebbits }: { subplebb
|
||||
};
|
||||
|
||||
const BoardSelector = ({
|
||||
defaultSubplebbits,
|
||||
directories,
|
||||
selectedAddress,
|
||||
onSelect,
|
||||
}: {
|
||||
defaultSubplebbits: MultisubSubplebbit[];
|
||||
directories: DirectoryCommunity[];
|
||||
selectedAddress: string;
|
||||
onSelect: (address: string) => void;
|
||||
}) => {
|
||||
@@ -121,7 +121,7 @@ const BoardSelector = ({
|
||||
<div className={styles.selectorRow}>
|
||||
<select value={selectedAddress} onChange={handleSelectChange} className={styles.boardSelect}>
|
||||
<option value=''>Select board...</option>
|
||||
{defaultSubplebbits.map((sub) => {
|
||||
{directories.map((sub) => {
|
||||
const shortCode = getBoardShortCode(sub.title);
|
||||
const boardName = getBoardName(sub.title);
|
||||
return (
|
||||
@@ -151,7 +151,7 @@ const BoardSelector = ({
|
||||
};
|
||||
|
||||
const Rules = () => {
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const directories = useDirectories();
|
||||
const [selectedAddress, setSelectedAddress] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -175,8 +175,8 @@ const Rules = () => {
|
||||
Please read and respect the rules of whatever board you decide to post to.
|
||||
</div>
|
||||
</div>
|
||||
<BoardSelector defaultSubplebbits={defaultSubplebbits} selectedAddress={selectedAddress} onSelect={setSelectedAddress} />
|
||||
{selectedAddress && <BoardRulesDisplay subplebbitAddress={selectedAddress} defaultSubplebbits={defaultSubplebbits} />}
|
||||
<BoardSelector directories={directories} selectedAddress={selectedAddress} onSelect={setSelectedAddress} />
|
||||
{selectedAddress && <BoardRulesDisplay subplebbitAddress={selectedAddress} directories={directories} />}
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{ "source": "/(.*)", "destination": "/index.html" }
|
||||
]
|
||||
],
|
||||
"buildCommand": "yarn build",
|
||||
"outputDirectory": "build",
|
||||
"framework": "vite"
|
||||
}
|
||||
|
||||
|
||||
|
||||
+57
-62
@@ -13,17 +13,21 @@ export default defineConfig({
|
||||
react({
|
||||
babel: {
|
||||
plugins: [
|
||||
['babel-plugin-react-compiler', {
|
||||
verbose: true
|
||||
}]
|
||||
]
|
||||
}
|
||||
[
|
||||
'babel-plugin-react-compiler',
|
||||
{
|
||||
verbose: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}),
|
||||
// Only include React Scan in development mode - never in production builds
|
||||
(isDevelopment || (!isProduction && process.env.NODE_ENV !== 'production')) && reactScan({
|
||||
showToolbar: true,
|
||||
playSound: true,
|
||||
}),
|
||||
(isDevelopment || (!isProduction && process.env.NODE_ENV !== 'production')) &&
|
||||
reactScan({
|
||||
showToolbar: true,
|
||||
playSound: true,
|
||||
}),
|
||||
nodePolyfills({
|
||||
globals: {
|
||||
Buffer: true,
|
||||
@@ -57,20 +61,20 @@ export default defineConfig({
|
||||
{
|
||||
src: '/android-chrome-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: '/android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png'
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: '/android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
src: '/android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable',
|
||||
},
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
clientsClaim: true,
|
||||
@@ -85,8 +89,8 @@ export default defineConfig({
|
||||
urlPattern: ({ url }) => url.pathname === '/' || url.pathname === '/index.html',
|
||||
handler: 'StaleWhileRevalidate',
|
||||
options: {
|
||||
cacheName: 'html-cache'
|
||||
}
|
||||
cacheName: 'html-cache',
|
||||
},
|
||||
},
|
||||
// PNG caching
|
||||
{
|
||||
@@ -95,9 +99,9 @@ export default defineConfig({
|
||||
options: {
|
||||
cacheName: 'images',
|
||||
expiration: {
|
||||
maxEntries: 50
|
||||
}
|
||||
}
|
||||
maxEntries: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Add additional asset caching
|
||||
{
|
||||
@@ -107,9 +111,9 @@ export default defineConfig({
|
||||
cacheName: 'assets-cache',
|
||||
expiration: {
|
||||
maxEntries: 100,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days
|
||||
}
|
||||
}
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days
|
||||
},
|
||||
},
|
||||
},
|
||||
// Google Fonts caching
|
||||
{
|
||||
@@ -119,12 +123,12 @@ export default defineConfig({
|
||||
cacheName: 'google-fonts-cache',
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365 // 365 days
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365, // 365 days
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200]
|
||||
}
|
||||
}
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
|
||||
@@ -133,27 +137,27 @@ export default defineConfig({
|
||||
cacheName: 'google-fonts-webfonts',
|
||||
expiration: {
|
||||
maxEntries: 30,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365 // 365 days
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365, // 365 days
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
'node-fetch': 'isomorphic-fetch',
|
||||
'assert': 'assert',
|
||||
'stream': 'stream-browserify',
|
||||
'crypto': 'crypto-browserify',
|
||||
'buffer': 'buffer',
|
||||
assert: 'assert',
|
||||
stream: 'stream-browserify',
|
||||
crypto: 'crypto-browserify',
|
||||
buffer: 'buffer',
|
||||
'util/': 'util',
|
||||
'util': 'util',
|
||||
util: 'util',
|
||||
},
|
||||
},
|
||||
server: {
|
||||
@@ -163,11 +167,12 @@ export default defineConfig({
|
||||
usePolling: true,
|
||||
},
|
||||
hmr: {
|
||||
overlay: false
|
||||
}
|
||||
overlay: false,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
// Use 'build' to match what electron/main.js expects (../build/index.html)
|
||||
outDir: 'build',
|
||||
emptyOutDir: true,
|
||||
sourcemap: process.env.GENERATE_SOURCEMAP === 'true',
|
||||
target: process.env.ELECTRON ? 'electron-renderer' : 'esnext',
|
||||
@@ -177,27 +182,17 @@ export default defineConfig({
|
||||
if (/[\\/]node_modules[\\/](react|react-dom|react-router-dom|react-i18next|i18next|i18next-browser-languagedetector|i18next-http-backend)[\\/]/.test(id)) {
|
||||
return 'vendor';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: process.env.PUBLIC_URL || '/',
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'ethers',
|
||||
'assert',
|
||||
'buffer',
|
||||
'process',
|
||||
'util',
|
||||
'stream-browserify',
|
||||
'isomorphic-fetch',
|
||||
'workbox-core',
|
||||
'workbox-precaching'
|
||||
],
|
||||
include: ['ethers', 'assert', 'buffer', 'process', 'util', 'stream-browserify', 'isomorphic-fetch', 'workbox-core', 'workbox-precaching'],
|
||||
},
|
||||
define: {
|
||||
'process.env.VITE_COMMIT_REF': JSON.stringify(process.env.COMMIT_REF),
|
||||
'global': 'globalThis',
|
||||
'__dirname': '""',
|
||||
}
|
||||
global: 'globalThis',
|
||||
__dirname: '""',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user