mirror of
https://github.com/plebbit/seedit.git
synced 2026-02-07 20:41:04 -05:00
93
.github/workflows/notifications.yml
vendored
93
.github/workflows/notifications.yml
vendored
@@ -1,93 +0,0 @@
|
||||
# send notifications to telegram group on commits, releases, issues
|
||||
name: notifications
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
# push:
|
||||
# branches:
|
||||
# - master
|
||||
release:
|
||||
types: [published]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
# push:
|
||||
# if: ${{ github.event_name == 'push' }}
|
||||
# runs-on: ubuntu-latest
|
||||
# strategy:
|
||||
# matrix:
|
||||
# commit: ${{ github.event.commits }}
|
||||
# steps:
|
||||
# # - name: debug
|
||||
# # env:
|
||||
# # DEBUG: ${{ toJSON(matrix.commit) }}
|
||||
# # run: echo "$DEBUG"
|
||||
|
||||
# # write message to file
|
||||
# - run: echo "<code>${{ matrix.commit.message }}</code>" >> message.txt
|
||||
# - run: echo "" >> message.txt
|
||||
# - run: echo "by <i>${{ matrix.commit.author.username }}</i>" >> message.txt
|
||||
# - run: echo "${{ matrix.commit.url }}" >> message.txt
|
||||
# - run: date -d "${{ matrix.commit.timestamp }}" +"%d/%m/%y at %H:%M" >> message.txt
|
||||
|
||||
# # send message, @plebbit telegram chat id is -1001665335693
|
||||
# - name: "telegram notification"
|
||||
# uses: appleboy/telegram-action@master
|
||||
# with:
|
||||
# to: -1001665335693
|
||||
# token: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
# # commit links don't show anything useful in preview
|
||||
# disable_web_page_preview: true
|
||||
# format: html
|
||||
# message_file: message.txt
|
||||
|
||||
release:
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# - name: debug
|
||||
# env:
|
||||
# DEBUG: ${{ toJSON(github) }}
|
||||
# run: echo "$DEBUG"
|
||||
|
||||
# write message to file
|
||||
- run: echo "<b>${{ github.event.repository.name }} ${{ github.event.release.name }}</b>" >> message.txt
|
||||
- run: echo "" >> message.txt
|
||||
- run: echo "${{ github.event.release.body }}" >> message.txt
|
||||
- run: echo "${{ github.event.release.html_url }}" >> message.txt
|
||||
|
||||
# send message, @plebbit telegram chat id is -1001665335693
|
||||
- name: "telegram notification"
|
||||
uses: appleboy/telegram-action@master
|
||||
with:
|
||||
to: -1001665335693
|
||||
token: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
format: html
|
||||
message_file: message.txt
|
||||
|
||||
issue:
|
||||
if: ${{ github.event_name == 'issues' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# - name: debug
|
||||
# env:
|
||||
# DEBUG: ${{ toJSON(github) }}
|
||||
# run: echo "$DEBUG"
|
||||
|
||||
# write message to file
|
||||
- run: echo "<code>${{ github.event.issue.title }}</code>" >> message.txt
|
||||
- run: echo "" >> message.txt
|
||||
- run: echo "by <i>${{ github.event.issue.user.login }}</i>" >> message.txt
|
||||
- run: echo "${{ github.event.issue.html_url }}" >> message.txt
|
||||
|
||||
# send message, @plebbit telegram chat id is -1001665335693
|
||||
- name: "telegram notification"
|
||||
uses: appleboy/telegram-action@master
|
||||
with:
|
||||
to: -1001665335693
|
||||
token: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
format: html
|
||||
message_file: message.txt
|
||||
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -60,9 +60,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macOS-13 # Intel x64
|
||||
- runner: macos-15-intel # Intel x64
|
||||
arch: x64
|
||||
- runner: macOS-14 # Apple Silicon arm64
|
||||
- runner: macos-latest # Apple Silicon arm64
|
||||
arch: arm64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
@@ -82,6 +82,12 @@ jobs:
|
||||
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
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: pip install setuptools
|
||||
|
||||
- name: Install dependencies (with Node v22)
|
||||
run: yarn install --frozen-lockfile --ignore-engines
|
||||
# make sure the ipfs executable is executable
|
||||
@@ -177,8 +183,7 @@ jobs:
|
||||
- run: sed -i "s/versionName \"1.0\"/versionName \"$(node -e "console.log(require('./package.json').version)")\"/" ./android/app/build.gradle
|
||||
- run: cat ./android/app/build.gradle
|
||||
# build apk
|
||||
- run: npx cap update
|
||||
- run: npx cap copy
|
||||
- run: npx cap sync android
|
||||
- run: cd android && gradle bundle
|
||||
- run: cd android && ./gradlew assembleRelease --stacktrace
|
||||
# optimize apk
|
||||
|
||||
253
.github/workflows/test.yml
vendored
253
.github/workflows/test.yml
vendored
@@ -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,54 +110,42 @@ 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/seedit.app" ]; then
|
||||
echo "Testing dist/mac/seedit.app..."
|
||||
# Run the app in background - it will start IPFS which takes time
|
||||
# We just verify it launches without crashing
|
||||
./dist/mac/seedit.app/Contents/MacOS/seedit &
|
||||
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/seedit.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)
|
||||
runs-on: macOS-14
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- 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/seedit.app" ]; then
|
||||
APP_PATH="dist/mac-arm64/seedit.app"
|
||||
elif [ -d "dist/mac/seedit.app" ]; then
|
||||
APP_PATH="dist/mac/seedit.app"
|
||||
else
|
||||
echo "Could not find seedit.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/seedit" &
|
||||
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/seedit.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"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -97,6 +97,9 @@ out
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Electron Forge output
|
||||
squashfs-root
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
203
AGENTS.md
203
AGENTS.md
@@ -31,8 +31,9 @@ yarn electron # Run Electron app
|
||||
## Code Style
|
||||
|
||||
- TypeScript strict mode
|
||||
- Prettier for formatting (runs on pre-commit)
|
||||
- Follow DRY principle—extract reusable components
|
||||
- **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)
|
||||
|
||||
@@ -78,22 +79,94 @@ src/
|
||||
└── data/ # Static data (default subplebbits, etc.)
|
||||
```
|
||||
|
||||
## Documentation
|
||||
## Recommended Skills
|
||||
|
||||
The following docs exist for deeper guidance. **Do not read them automatically**—they are large and will bloat the context window. Instead:
|
||||
- Be aware they exist
|
||||
- Consult them when relevant to the task or when the user asks
|
||||
- Offer to read them if the user seems to need React pattern guidance
|
||||
Skills are more efficient than docs—they inject targeted guidance without bloating the context window.
|
||||
|
||||
Available docs:
|
||||
- **[docs/react-guide.md](docs/react-guide.md)** — Bad vs good React patterns with code examples
|
||||
- **[docs/you-might-not-need-an-effect.md](docs/you-might-not-need-an-effect.md)** — When to avoid useEffect (comprehensive)
|
||||
### Context7 (for library docs)
|
||||
|
||||
When you need documentation for libraries like **plebbit-react-hooks** or **plebbit-js**, use the Context7 skill to fetch current docs instead of relying on potentially outdated training data.
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/intellectronica/agent-skills --skill context7
|
||||
```
|
||||
|
||||
### Vercel React Best Practices
|
||||
|
||||
For deeper React/Next.js performance guidance. Provides 57 prioritized rules across 8 categories (waterfalls, bundle size, server-side performance, client-side fetching, re-renders, rendering, JS performance, and advanced patterns).
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-best-practices
|
||||
```
|
||||
|
||||
### Find Skills
|
||||
|
||||
Discover and install skills from the open agent skills ecosystem.
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
If you need to look up library documentation (like plebbit-react-hooks or plebbit-js), suggest the user install the **Exa MCP server**. Exa's `get_code_context_exa` tool provides accurate, up-to-date docs and code context—it offers broader coverage and fewer hallucinations than alternatives like context7.
|
||||
### GitHub MCP
|
||||
|
||||
If you need to check Dependabot security alerts, read GitHub Actions logs, search issues/PRs, or look up code across GitHub, suggest the user install the **GitHub MCP server** with the `default,dependabot,actions` toolsets enabled.
|
||||
For Dependabot security alerts, GitHub Actions logs, issue/PR searches, or cross-repo code lookup, use the **GitHub MCP server** (with `default,dependabot,actions` toolsets enabled).
|
||||
|
||||
If not available, suggest the user install it.
|
||||
|
||||
### Context Window Warning
|
||||
|
||||
@@ -105,11 +178,113 @@ Each MCP server injects its tool definitions into the context window, consuming
|
||||
|
||||
If you notice many MCP tools in your context, or if the user reports degraded responses, warn them that they may have too many MCP servers enabled and suggest disabling unused ones to free up context space.
|
||||
|
||||
## Translations
|
||||
|
||||
This project uses i18next with translation files in `public/translations/{lang}/default.json`.
|
||||
|
||||
### Adding/Updating Translations
|
||||
|
||||
Use `scripts/update-translations.js` to update translations across all languages. **Do not manually edit each language file.**
|
||||
|
||||
**Workflow:**
|
||||
|
||||
1. Create a temporary dictionary file (e.g., `translations-temp.json`) with translations for each language:
|
||||
```json
|
||||
{
|
||||
"en": "English text",
|
||||
"es": "Spanish text",
|
||||
"fr": "French text",
|
||||
"de": "German text",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
2. Run the script with the `--map` flag:
|
||||
```bash
|
||||
node scripts/update-translations.js --key my_new_key --map translations-temp.json --include-en --write
|
||||
```
|
||||
|
||||
3. Delete the temporary dictionary file after the script completes.
|
||||
|
||||
**Other useful commands:**
|
||||
|
||||
```bash
|
||||
# Copy a key's value from English to all languages (dry run first)
|
||||
node scripts/update-translations.js --key some_key --from en --dry
|
||||
node scripts/update-translations.js --key some_key --from en --write
|
||||
|
||||
# Delete a key from all languages
|
||||
node scripts/update-translations.js --key obsolete_key --delete --write
|
||||
|
||||
# Audit for unused translation keys
|
||||
node scripts/update-translations.js --audit --dry
|
||||
node scripts/update-translations.js --audit --write
|
||||
```
|
||||
|
||||
## 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 to track the problem. Format:
|
||||
|
||||
- **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.
|
||||
|
||||
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
|
||||
- Use yarn, not npm
|
||||
- Keep components focused—split large components
|
||||
- Add comments for complex logic, skip obvious code
|
||||
- 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)
|
||||
|
||||
|
||||
25
README.md
25
README.md
@@ -1,31 +1,36 @@
|
||||
[](https://github.com/bitsocialhq/seedit/actions/workflows/test.yml)
|
||||
[](https://github.com/bitsocialhq/seedit/releases/latest)
|
||||
[](https://github.com/bitsocialhq/seedit/blob/master/LICENSE)
|
||||
[](http://commitizen.github.io/cz-cli/)
|
||||
|
||||
<img src="https://github.com/plebeius-eth/assets/blob/main/seedit-logo.png" width="302" height="111">
|
||||
|
||||
_Telegram group for this repo https://t.me/seeditreact_
|
||||
|
||||
# Seedit
|
||||
|
||||
Seedit is a serverless, adminless, decentralized reddit alternative. Seedit is a client (interface) for the Plebbit protocol, which is a decentralized social network where anyone can create and fully own unstoppable communities. Learn more: https://plebbit.com
|
||||
Seedit is a serverless, adminless, decentralized and open-source (old)reddit alternative built on the [Bitsocial protocol](https://bitsocial.net). Like reddit, anyone can create a seedit community, It features a similar homepage structure as reddit, but with a crucial difference: **anyone can create and own communities, and multiple communities can compete for each default community slot**.
|
||||
|
||||
- Seedit web version: https://seedit.app — or, using Brave/IPFS Companion: https://seedit.eth
|
||||
|
||||
### Downloads
|
||||
- Seedit desktop version (full p2p plebbit node, seeds automatically): available for Mac/Windows/Linux, [download link in the release page](https://github.com/plebbit/seedit/releases/latest)
|
||||
- Seedit mobile version: available for Android, [download link in the release page](https://github.com/plebbit/seedit/releases/latest)
|
||||
- Seedit desktop version (full p2p bitsocial node, seeds automatically): available for Mac/Windows/Linux, [download link in the release page](https://github.com/bitsocialhq/seedit/releases/latest)
|
||||
- Seedit mobile version: available for Android, [download link in the release page](https://github.com/bitsocialhq/seedit/releases/latest)
|
||||
|
||||
<br />
|
||||
|
||||
<img src="https://github.com/plebeius-eth/assets/blob/main/seedit-screenshot.jpg" width="849">
|
||||
|
||||
## How to create a community
|
||||
In the plebbit protocol, a seedit community is called a _subplebbit_. To run a subplebbit, you can choose between two options:
|
||||
To run a community, you can choose between two options:
|
||||
|
||||
1. If you prefer to use a **GUI**, download the desktop version of the Seedit client, available for Windows, MacOS and Linux: [latest release](https://github.com/plebbit/seedit/releases/latest). Create a subplebbit using using the familiar old.reddit-like UI, and modify its settings to your liking. The app runs an IPFS node, meaning you have to keep it running to have your board online.
|
||||
2. If you prefer to use a **command line interface**, install plebbit-cli, available for Windows, MacOS and Linux: [latest release](https://github.com/plebbit/plebbit-cli/releases/latest). Follow the instructions in the readme of the repo. When running the daemon for the first time, it will output WebUI links you can use to manage your subplebbit with the ease of the GUI.
|
||||
1. If you prefer to use a **GUI**, download the desktop version of the Seedit client, available for Windows, MacOS and Linux: [latest release](https://github.com/bitsocialhq/seedit/releases/latest). Create a community using using the familiar old.reddit-like UI, and modify its settings to your liking. The app runs an IPFS node, meaning you have to keep it running to have your board online.
|
||||
2. If you prefer to use a **command line interface**, install bitsocial-cli, available for Windows, MacOS and Linux: [latest release](https://github.com/bitsocialhq/bitsocial-cli/releases/latest). Follow the instructions in the readme of the repo. When running the daemon for the first time, it will output WebUI links you can use to manage your community with the ease of the GUI.
|
||||
|
||||
Peers can connect to your subplebbit using any plebbit client, such as Plebchan or Seedit. They only need the subplebbit's address, which is not stored in any central database, as plebbit is a pure peer-to-peer protocol.
|
||||
Peers can connect to your bitsocial community using any bitsocial client, such as Seedit or [5chan](https://github.com/bitsocialhq/5chan). They only need the community address, which is not stored in any central database, as bitsocial is a pure peer-to-peer protocol.
|
||||
|
||||
### How to add a community to the default list (p/all)
|
||||
The default list of communities, used on p/all on Seedit, is plebbit's [temporary default subplebbits](https://github.com/plebbit/lists) list. You can open a pull request in that repo to add your subplebbit to the list, or contact devs via telegram [@plebbit](https://t.me/plebbit). In the future, this process will be automated by submitting proposals to a plebbit DAO, using the [plebbit token](https://etherscan.io/token/0xea81dab2e0ecbc6b5c4172de4c22b6ef6e55bd8f).
|
||||
### How to add a community to the default list (s/all)
|
||||
The default list of communities, used on s/all on Seedit, is bitsocial's [default-multisub.json list](https://github.com/bitsocialhq/lists/blob/master/default-multisub.json). You can open a pull request in that repo to add your community to the list.
|
||||
|
||||
## To run locally
|
||||
|
||||
@@ -44,4 +49,4 @@ The default list of communities, used on p/all on Seedit, is plebbit's [temporar
|
||||
|
||||
### Build:
|
||||
|
||||
The linux/windows/mac/android build scripts are in https://github.com/plebbit/seedit/blob/master/.github/workflows/release.yml
|
||||
The linux/windows/mac/android build scripts are in https://github.com/bitsocialhq/seedit/blob/master/.github/workflows/release.yml
|
||||
|
||||
Binary file not shown.
@@ -1,813 +0,0 @@
|
||||
# React guide
|
||||
|
||||
A lot of AI-generated React code is heavily biased toward using useState and useEffect, often in verbose or naive ways. That’s because these hooks are:
|
||||
|
||||
1. Explicit, making them easy for LLMs to pattern-match.
|
||||
2. Safe defaults, requiring less architectural decision-making.
|
||||
3. Well-documented, so the model’s training data is oversaturated with examples.
|
||||
|
||||
But modern, well-architected React apps rarely rely on raw useState/useEffect beyond simple UI state or lifecycle quirks. Here’s a breakdown of what serious devs use instead—and why this is the real moat for human engineers.
|
||||
|
||||
Alternatives to useState and useEffect
|
||||
|
||||
1. State Machines / Statecharts (e.g. Zustand)
|
||||
- Why: Replaces ad-hoc local state management with deterministic, declarative logic.
|
||||
- Advantage: Less bugs, easier to test, clearer state transitions.
|
||||
- AI gap: LLMs struggle to model state transitions declaratively. LLM Agent must overcome this.
|
||||
|
||||
2. React Query / SWR for Data Fetching
|
||||
- Why: Avoids useEffect-based data fetching and loading/error state juggling.
|
||||
- Advantage: Caching, stale-while-revalidate, retries, pagination—all handled out of the box.
|
||||
- AI gap: AI often uses useEffect + fetch and forgets cleanup, race conditions, or caching.
|
||||
|
||||
3. Global State with Zustand, Jotai, Recoil, or Redux Toolkit
|
||||
- Why: Local useState doesn’t scale for shared or persistent state.
|
||||
- Advantage: Better performance, devtools support, persistence.
|
||||
- AI gap: Defaults to prop-drilling or bloated context APIs.
|
||||
|
||||
4. Custom Hooks and Composition
|
||||
- Why: Reuse logic cleanly without bloating components or copy-pasting useEffect.
|
||||
- Advantage: Separation of concerns, encapsulation.
|
||||
- AI gap: Often fails to factor logic out, keeping everything in one component.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
AI can mimic patterns, but it can’t:
|
||||
- Architect a system for long-term maintainability.
|
||||
- Predict runtime performance tradeoffs.
|
||||
- Refactor spaghetti effects into custom hooks or state machines.
|
||||
- Decide when state should be colocated vs globalized.
|
||||
- Avoid footguns like stale closures, unnecessary re-renders, or race conditions.
|
||||
|
||||
In short: AI is great at writing code, but bad at engineering software.
|
||||
|
||||
Takeaway
|
||||
- Use fewer useEffect and useState hooks.
|
||||
- Learn to design systems, not just code components.
|
||||
- Reach for declarative, composable patterns.
|
||||
- Understand caching, reactivity, and scalability beyond local state.
|
||||
|
||||
Here’s a side-by-side breakdown of common AI-generated React patterns (bad) vs what experienced engineers write (good) across 4 key areas: state management, data fetching, side effects, and logic reuse.
|
||||
|
||||
## 1. State Management
|
||||
|
||||
❌ Bad (AI-style useState)
|
||||
|
||||
```typescript
|
||||
function TodoApp() {
|
||||
const [todos, setTodos] = useState([]);
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
const addTodo = () => {
|
||||
setTodos([...todos, { text: input }]);
|
||||
setInput('');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input value={input} onChange={(e) => setInput(e.target.value)} />
|
||||
<button onClick={addTodo}>Add</button>
|
||||
<ul>
|
||||
{todos.map((todo, i) => <li key={i}>{todo.text}</li>)}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Zustand store, state extracted)
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand';
|
||||
|
||||
const useTodoStore = create((set) => ({
|
||||
todos: [],
|
||||
input: '',
|
||||
addTodo: () => set((state) => ({
|
||||
todos: [...state.todos, { text: state.input }],
|
||||
input: '',
|
||||
})),
|
||||
setInput: (val) => set({ input: val }),
|
||||
}));
|
||||
|
||||
function TodoApp() {
|
||||
const { todos, input, setInput, addTodo } = useTodoStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
<input value={input} onChange={(e) => setInput(e.target.value)} />
|
||||
<button onClick={addTodo}>Add</button>
|
||||
<ul>
|
||||
{todos.map((todo, i) => <li key={i}>{todo.text}</li>)}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 2. Data Fetching
|
||||
|
||||
❌ Bad (AI-style useEffect)
|
||||
|
||||
```typescript
|
||||
function UserList() {
|
||||
const [users, setUsers] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/users')
|
||||
.then(res => res.json())
|
||||
.then(setUsers);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{users.map(u => <li key={u.id}>{u.name}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (React Query)
|
||||
|
||||
```typescript
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
function UserList() {
|
||||
const { data: users = [], isLoading } = useQuery({
|
||||
queryKey: ['users'],
|
||||
queryFn: () => fetch('/api/users').then(res => res.json()),
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{users.map(u => <li key={u.id}>{u.name}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Side Effects
|
||||
|
||||
❌ Bad (naive effect with race condition)
|
||||
|
||||
```typescript
|
||||
function Search({ term }) {
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/search?q=${term}`)
|
||||
.then(res => res.json())
|
||||
.then(setResults);
|
||||
}, [term]);
|
||||
|
||||
return <div>{results.length} results</div>;
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (abort previous fetch with AbortController)
|
||||
|
||||
```typescript
|
||||
function Search({ term }) {
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch(`/api/search?q=${term}`, { signal: controller.signal })
|
||||
.then(res => res.json())
|
||||
.then(setResults)
|
||||
.catch((e) => {
|
||||
if (e.name !== 'AbortError') console.error(e);
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [term]);
|
||||
|
||||
return <div>{results.length} results</div>;
|
||||
}
|
||||
```
|
||||
|
||||
Better yet? Don’t use useEffect at all. Use React Query with term as a key.
|
||||
|
||||
## 4. Logic Reuse
|
||||
|
||||
❌ Bad (repeated logic inline)
|
||||
|
||||
```typescript
|
||||
function ComponentA() {
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setCount(c => c + 1), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
function ComponentB() {
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setCount(c => c + 1), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (custom hook)
|
||||
|
||||
```typescript
|
||||
function useCounter(intervalMs = 1000) {
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setCount(c => c + 1), intervalMs);
|
||||
return () => clearInterval(id);
|
||||
}, [intervalMs]);
|
||||
return count;
|
||||
}
|
||||
|
||||
function ComponentA() {
|
||||
const count = useCounter();
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
function ComponentB() {
|
||||
const count = useCounter();
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 5. State Machines with Zustand
|
||||
|
||||
❌ Bad (AI-style complex useState with boolean flags)
|
||||
|
||||
```typescript
|
||||
function OrderProcess() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [cart, setCart] = useState([]);
|
||||
const [paymentDetails, setPaymentDetails] = useState(null);
|
||||
const [orderPlaced, setOrderPlaced] = useState(false);
|
||||
|
||||
const addToCart = (item) => {
|
||||
setCart([...cart, item]);
|
||||
};
|
||||
|
||||
const submitPayment = async (details) => {
|
||||
setIsLoading(true);
|
||||
setIsError(false);
|
||||
|
||||
try {
|
||||
await processPayment(details);
|
||||
setPaymentDetails(details);
|
||||
setIsSuccess(true);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsError(true);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const placeOrder = async () => {
|
||||
setIsLoading(true);
|
||||
setIsError(false);
|
||||
|
||||
try {
|
||||
await submitOrder(cart, paymentDetails);
|
||||
setOrderPlaced(true);
|
||||
setIsSuccess(true);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsError(true);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Complex conditional rendering based on multiple state variables */}
|
||||
{isLoading && <LoadingSpinner />}
|
||||
{isError && <ErrorMessage />}
|
||||
{!isLoading && !isError && !orderPlaced && (
|
||||
<>
|
||||
<CartItems items={cart} onAddItem={addToCart} />
|
||||
<PaymentForm onSubmit={submitPayment} />
|
||||
{paymentDetails && <OrderButton onClick={placeOrder} />}
|
||||
</>
|
||||
)}
|
||||
{orderPlaced && <OrderConfirmation />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Zustand state machine)
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand';
|
||||
|
||||
// Create a Zustand store with state machine pattern
|
||||
const useOrderStore = create((set, get) => ({
|
||||
// State machine's current state
|
||||
state: 'browsing',
|
||||
|
||||
// Context/data
|
||||
cart: [],
|
||||
paymentDetails: null,
|
||||
error: null,
|
||||
|
||||
// Actions that transition between states
|
||||
addToCart: (item) => set((state) => ({
|
||||
cart: [...state.cart, item]
|
||||
})),
|
||||
|
||||
goToCheckout: () => set({ state: 'paymentEntry' }),
|
||||
|
||||
goBack: () => {
|
||||
const { state: currentState } = get();
|
||||
if (currentState === 'paymentEntry') {
|
||||
set({ state: 'browsing' });
|
||||
} else if (currentState === 'confirmOrder') {
|
||||
set({ state: 'paymentEntry' });
|
||||
}
|
||||
},
|
||||
|
||||
submitPayment: async (details) => {
|
||||
set({ state: 'processingPayment', error: null });
|
||||
|
||||
try {
|
||||
const result = await processPayment(details);
|
||||
set({
|
||||
state: 'confirmOrder',
|
||||
paymentDetails: result
|
||||
});
|
||||
} catch (error) {
|
||||
set({
|
||||
state: 'paymentEntry',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
placeOrder: async () => {
|
||||
const { cart, paymentDetails } = get();
|
||||
set({ state: 'processingOrder', error: null });
|
||||
|
||||
try {
|
||||
await submitOrder(cart, paymentDetails);
|
||||
set({ state: 'orderComplete' });
|
||||
} catch (error) {
|
||||
set({
|
||||
state: 'confirmOrder',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
function OrderProcess() {
|
||||
const {
|
||||
state,
|
||||
cart,
|
||||
paymentDetails,
|
||||
error,
|
||||
addToCart,
|
||||
goToCheckout,
|
||||
goBack,
|
||||
submitPayment,
|
||||
placeOrder
|
||||
} = useOrderStore();
|
||||
|
||||
// Render UI based on current state
|
||||
return (
|
||||
<div>
|
||||
{error && <ErrorMessage message={error} />}
|
||||
|
||||
{state === 'browsing' && (
|
||||
<CartItems
|
||||
items={cart}
|
||||
onAddItem={addToCart}
|
||||
onCheckout={goToCheckout}
|
||||
/>
|
||||
)}
|
||||
|
||||
{state === 'paymentEntry' && (
|
||||
<PaymentForm
|
||||
onSubmit={submitPayment}
|
||||
onBack={goBack}
|
||||
/>
|
||||
)}
|
||||
|
||||
{state === 'processingPayment' && (
|
||||
<LoadingSpinner message="Processing payment..." />
|
||||
)}
|
||||
|
||||
{state === 'confirmOrder' && (
|
||||
<OrderSummary
|
||||
cart={cart}
|
||||
paymentDetails={paymentDetails}
|
||||
onConfirm={placeOrder}
|
||||
onBack={goBack}
|
||||
/>
|
||||
)}
|
||||
|
||||
{state === 'processingOrder' && (
|
||||
<LoadingSpinner message="Placing your order..." />
|
||||
)}
|
||||
|
||||
{state === 'orderComplete' && (
|
||||
<OrderConfirmation orderDetails={{ cart, paymentDetails }} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Key Benefits of the Zustand State Machine Approach:
|
||||
|
||||
1. **Explicit states**: The system can only be in one well-defined state at a time
|
||||
2. **Centralized logic**: All state transitions are defined in one place
|
||||
3. **Predictable transitions**: State changes follow clear patterns
|
||||
4. **Simplified UI logic**: Components just render based on the current state
|
||||
5. **Async handling**: Asynchronous operations are contained within the state transitions
|
||||
6. **Shared state**: Any component can access the store without prop drilling
|
||||
7. **Improved debugging**: The current state is always clear and predictable
|
||||
8. **No impossible states**: Unlike boolean flags that could create invalid combinations
|
||||
9. **Simpler API**: Compared to XState, Zustand offers a more approachable API for many developers
|
||||
10. **Smaller bundle size**: Zustand is significantly smaller than XState
|
||||
|
||||
This approach gives most of the benefits of formal state machines while maintaining the simplicity and familiarity of React state management. It's a pragmatic middle ground that's often easier to adopt in existing projects.
|
||||
|
||||
## 6. Concurrent UI Techniques
|
||||
|
||||
❌ Bad (Blocking UI updates without concurrency):
|
||||
```jsx
|
||||
// Bad example: Synchronously fetching data in useEffect can cause UI jank.
|
||||
function SearchResults({ query }) {
|
||||
const [results, setResults] = React.useState([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetch(`/api/search?q=${query}`)
|
||||
.then(res => res.json())
|
||||
.then(data => setResults(data));
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{results.map(item => <p key={item.id}>{item.title}</p>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Using useTransition for non-blocking updates):
|
||||
```jsx
|
||||
// Good example: Leveraging useTransition to mark state updates as low-priority.
|
||||
function SearchResults({ query }) {
|
||||
const [results, setResults] = React.useState([]);
|
||||
const [isPending, startTransition] = React.useTransition();
|
||||
|
||||
React.useEffect(() => {
|
||||
startTransition(() => {
|
||||
fetch(`/api/search?q=${query}`)
|
||||
.then(res => res.json())
|
||||
.then(data => setResults(data));
|
||||
});
|
||||
}, [query, startTransition]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isPending && <div>Loading...</div>}
|
||||
{results.map(item => <p key={item.id}>{item.title}</p>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Routing and Navigation (Not applicable with Next.js apps)
|
||||
|
||||
❌ Bad (Manual routing without a dedicated library):
|
||||
```jsx
|
||||
// Bad example: Handling routing manually using state and window history.
|
||||
function App() {
|
||||
const [route, setRoute] = React.useState(window.location.pathname);
|
||||
|
||||
React.useEffect(() => {
|
||||
const onPopState = () => setRoute(window.location.pathname);
|
||||
window.addEventListener('popstate', onPopState);
|
||||
return () => window.removeEventListener('popstate', onPopState);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{route === '/' && <Home />}
|
||||
{route === '/about' && <About />}
|
||||
<button onClick={() => {
|
||||
window.history.pushState({}, '', '/about');
|
||||
setRoute('/about');
|
||||
}}>
|
||||
About
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Using React Router v6 with lazy-loaded routes):
|
||||
```jsx
|
||||
// Good example: Utilizing React Router to manage routes and lazy load components.
|
||||
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
|
||||
const Home = lazy(() => import('./Home'));
|
||||
const About = lazy(() => import('./About'));
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<nav>
|
||||
<Link to="/">Home</Link>
|
||||
<Link to="/about">About</Link>
|
||||
</nav>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Error Handling
|
||||
|
||||
❌ Bad (No error boundary; crashes on errors):
|
||||
```jsx
|
||||
// Bad example: A component that throws errors without an error boundary.
|
||||
function MyComponent({ data }) {
|
||||
if (!data) {
|
||||
throw new Error("Data not found");
|
||||
}
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return <MyComponent data={null} />;
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Wrapping components with an Error Boundary):
|
||||
```jsx
|
||||
// Good example: Using an ErrorBoundary component to catch and handle errors gracefully.
|
||||
import React from 'react';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
console.error("Error caught in boundary:", error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return <h1>Something went wrong.</h1>;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function MyComponent({ data }) {
|
||||
if (!data) {
|
||||
throw new Error("Data not found");
|
||||
}
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<MyComponent data={null} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Form Management
|
||||
|
||||
❌ Bad (Manual state handling with no validation):
|
||||
```jsx
|
||||
// Bad example: Uncontrolled form without validation; error handling is omitted.
|
||||
function ContactForm() {
|
||||
const [formData, setFormData] = React.useState({ name: '', email: '' });
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
console.log(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="Name"
|
||||
/>
|
||||
<input
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
placeholder="Email"
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Using React Hook Form with Zod for schema validation):
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
// 1. Define your schema with Zod
|
||||
const contactSchema = z.object({
|
||||
name: z.string().nonempty('Name is required'),
|
||||
email: z
|
||||
.string()
|
||||
.nonempty('Email is required')
|
||||
.email('Invalid email'),
|
||||
});
|
||||
|
||||
// 2. Infer the TypeScript type of your form data
|
||||
type ContactFormData = z.infer<typeof contactSchema>;
|
||||
|
||||
export function ContactForm() {
|
||||
// 3. Hook up RHF with the zodResolver
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<ContactFormData>({
|
||||
resolver: zodResolver(contactSchema),
|
||||
});
|
||||
|
||||
const onSubmit = (data: ContactFormData) => {
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<input
|
||||
{...register('name')}
|
||||
placeholder="Name"
|
||||
aria-invalid={!!errors.name}
|
||||
/>
|
||||
{errors.name && <p role="alert">{errors.name.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
{...register('email')}
|
||||
placeholder="Email"
|
||||
aria-invalid={!!errors.email}
|
||||
/>
|
||||
{errors.email && <p role="alert">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 10. Code Splitting and Lazy Loading
|
||||
|
||||
❌ Bad (Eagerly importing heavy components, increasing the initial bundle size):
|
||||
```jsx
|
||||
// Bad example: Directly importing a large component causing a heavier bundle.
|
||||
import HeavyComponent from './HeavyComponent';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<HeavyComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Lazy loading heavy components with React.lazy and Suspense):
|
||||
```jsx
|
||||
// Good example: Dynamically importing components to reduce initial load time.
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
|
||||
const HeavyComponent = lazy(() => import('./HeavyComponent'));
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading Component...</div>}>
|
||||
<HeavyComponent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 11. Testing Strategies
|
||||
|
||||
❌ Bad (Testing implementation details, resulting in brittle tests):
|
||||
```jsx
|
||||
// Bad example: Relying on internal DOM structure which may change.
|
||||
import { render } from '@testing-library/react';
|
||||
import MyComponent from './MyComponent';
|
||||
|
||||
test('MyComponent renders data', () => {
|
||||
const { container } = render(<MyComponent />);
|
||||
expect(container.querySelector('.data')).toBeDefined();
|
||||
});
|
||||
```
|
||||
|
||||
✅ Good (Focusing on user-centric behaviors using React Testing Library):
|
||||
```jsx
|
||||
// Good example: Testing component behavior by simulating user interactions.
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MyComponent from './MyComponent';
|
||||
|
||||
test('MyComponent displays loaded data after user action', async () => {
|
||||
render(<MyComponent />);
|
||||
|
||||
// Check for an initial loading state
|
||||
expect(screen.getByText(/Loading/i)).toBeInTheDocument();
|
||||
|
||||
// Simulate user action that triggers data loading
|
||||
const button = screen.getByRole('button', { name: /load data/i });
|
||||
userEvent.click(button);
|
||||
|
||||
// Wait and assert that data is displayed
|
||||
expect(await screen.findByText(/Data loaded/i)).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
## 12. Accessibility & Theming
|
||||
|
||||
❌ Bad (Non-semantic elements and missing ARIA labels):
|
||||
```jsx
|
||||
// Bad example: Using a div as a clickable element without semantics or accessibility.
|
||||
function ThemedButton() {
|
||||
return <div onClick={() => console.log('Clicked!')}>Click me</div>;
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Semantic button with ARIA attributes and theming via CSS variables):
|
||||
```jsx
|
||||
// Good example: Accessible button component that utilizes theming.
|
||||
function ThemedButton() {
|
||||
return (
|
||||
<button
|
||||
aria-label="Click me"
|
||||
onClick={() => console.log('Clicked!')}
|
||||
style={{ padding: '8px 16px', backgroundColor: 'var(--primary-color)', color: '#fff' }}
|
||||
>
|
||||
Click me
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* CSS (in a separate file or styled-components):
|
||||
:root {
|
||||
--primary-color: #007bff;
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
# Summary Table
|
||||
|
||||
| Concern | Bad AI Pattern | Solid Engineering Practice |
|
||||
|---------|---------------|----------------------------|
|
||||
| **State Management** | `useState` everywhere, prop‑drilling | Central store (Zustand / Redux Toolkit) or colocated custom hooks |
|
||||
| **Data Fetching** | `useEffect` + `fetch`, manual loading/error juggling | React Query / SWR with caching, retries, stale‑while‑revalidate |
|
||||
| **Side Effects** | Effects without cleanup → race conditions, leaks | AbortController or query library handles cancellation & retries |
|
||||
| **Logic Reuse** | Copy‑pasted hooks and state in every component | Extract cross‑cutting logic into custom hooks |
|
||||
| **Workflow / State Machine** | Boolean‑flag soup, impossible states | Deterministic statechart in a single store (Zustand / XState) |
|
||||
| **Concurrent UI Updates** | Blocking synchronous work inside effects | `useTransition` / `startTransition` for low‑priority updates |
|
||||
| **Routing & Navigation** | DIY history manipulation, inline route state | React Router v6 (or Next.js built‑ins) with lazy‑loaded routes |
|
||||
| **Error Handling** | No error boundary → full‑app crashes | Top‑level `ErrorBoundary` plus logging in `componentDidCatch` |
|
||||
| **Form Management** | Manual state, zero validation or feedback | React Hook Form + Yup/Zod schema for declarative validation |
|
||||
| **Code Splitting & Lazy Loading** | Eager imports ballooning bundle size | `React.lazy` + `Suspense` for on‑demand loading |
|
||||
| **Testing Strategy** | Brittle tests keyed to DOM structure | User‑centric tests with React Testing Library & jest‑dom |
|
||||
| **Accessibility & Theming** | Non‑semantic elements, missing ARIA, inline colors | Semantic HTML, ARIA labels, CSS variables / theme provider |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,68 @@ 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) =>
|
||||
@@ -32,23 +90,8 @@ const spawnAsync = (...args) =>
|
||||
});
|
||||
|
||||
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`);
|
||||
|
||||
98
forge.config.js
Normal file
98
forge.config.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { downloadIpfsClients } from './electron/before-pack.js';
|
||||
|
||||
const config = {
|
||||
packagerConfig: {
|
||||
name: 'seedit',
|
||||
executableName: 'seedit',
|
||||
appBundleId: 'seedit.desktop',
|
||||
|
||||
// NOTE: asar is disabled because of a bug where electron-packager silently fails
|
||||
// during asar creation with seedit'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: 'seedit',
|
||||
format: 'UDZO',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-zip',
|
||||
platforms: ['darwin'],
|
||||
},
|
||||
// Windows
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
platforms: ['win32'],
|
||||
config: {
|
||||
name: 'seedit',
|
||||
},
|
||||
},
|
||||
// Linux
|
||||
{
|
||||
name: '@reforged/maker-appimage',
|
||||
platforms: ['linux'],
|
||||
config: {
|
||||
options: {
|
||||
categories: ['Network'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
95
package.json
95
package.json
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Seedit",
|
||||
"name": "seedit",
|
||||
"version": "0.5.10",
|
||||
"description": "A bitsocial client with an old.reddit UI",
|
||||
"author": "Bitsocial Labs",
|
||||
@@ -8,16 +8,17 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@capacitor/app": "7.0.1",
|
||||
"@capacitor/filesystem": "^7.1.4",
|
||||
"@capacitor/filesystem": "7.1.4",
|
||||
"@capacitor/local-notifications": "7.0.1",
|
||||
"@capacitor/share": "^7.0.2",
|
||||
"@capacitor/share": "7.0.2",
|
||||
"@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#5524168ef478bc06518168d4c99a829a85c92f69",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#df3cb6901f63054870dc127cd7565c98617c2a4f",
|
||||
"@types/node": "20.8.2",
|
||||
"@types/react": "18.2.25",
|
||||
"@types/react-dom": "18.2.10",
|
||||
"@types/react": "19.1.2",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@vercel/analytics": "1.6.1",
|
||||
"ace-builds": "1.41.0",
|
||||
"cross-env": "7.0.3",
|
||||
"electron-context-menu": "3.3.0",
|
||||
@@ -40,7 +41,7 @@
|
||||
"react-dom": "19.1.2",
|
||||
"react-dropzone": "14.3.8",
|
||||
"react-i18next": "13.2.2",
|
||||
"react-markdown": "8.0.6",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router-dom": "6.30.2",
|
||||
"react-router-hash-link": "2.4.3",
|
||||
"react-virtuoso": "4.7.8",
|
||||
@@ -50,7 +51,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": {
|
||||
"start": "vite",
|
||||
@@ -61,20 +63,15 @@
|
||||
"preview": "vite preview",
|
||||
"analyze-bundle": "cross-env NODE_ENV=production PUBLIC_URL=./ GENERATE_SOURCEMAP=true vite build && npx source-map-explorer 'build/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:no-delete-data": "yarn electron:before:delete-data && 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-rebuild && electron-builder build --publish never -l",
|
||||
"electron:build:windows": "yarn build && yarn build:preload && yarn electron-rebuild && electron-builder build --publish never -w",
|
||||
"electron:build:mac": "yarn build && yarn build:preload && yarn electron-rebuild && electron-builder build --publish never -m",
|
||||
"electron:build:mac:arm64": "cross-env SEEDIT_BUILD_ARCH=arm64 yarn build && yarn build:preload && yarn electron-rebuild && electron-builder build --publish never -m --arm64",
|
||||
"electron:build:mac:x64": "cross-env SEEDIT_BUILD_ARCH=x64 yarn build && yarn build:preload && yarn electron-rebuild && electron-builder build --publish never -m --x64",
|
||||
"electron:build:windows:arm64": "cross-env SEEDIT_BUILD_ARCH=arm64 yarn build && yarn build:preload && yarn electron-rebuild && electron-builder build --publish never -w --arm64",
|
||||
"electron:build:windows:x64": "cross-env SEEDIT_BUILD_ARCH=x64 yarn build && yarn build:preload && yarn electron-rebuild && electron-builder build --publish never -w --x64",
|
||||
"electron:build:linux:arm64": "cross-env SEEDIT_BUILD_ARCH=arm64 yarn build && yarn build:preload && electron-rebuild && electron-builder build --publish never -l --arm64",
|
||||
"electron:build:linux:x64": "cross-env SEEDIT_BUILD_ARCH=x64 yarn build && yarn build:preload && electron-rebuild && electron-builder build --publish never -l --x64",
|
||||
"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:before": "yarn electron-rebuild && 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'",
|
||||
"prettier": "oxfmt src/**/*.{js,ts,tsx} electron/**/*.{js,mjs}",
|
||||
@@ -98,9 +95,14 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "7.2.0",
|
||||
"@capacitor/cli": "7.2.0",
|
||||
"@capacitor/core": "7.2.0",
|
||||
"@capacitor/android": "7.4.5",
|
||||
"@capacitor/cli": "7.4.5",
|
||||
"@capacitor/core": "7.4.5",
|
||||
"@electron-forge/cli": "7.6.0",
|
||||
"@electron-forge/maker-dmg": "7.6.0",
|
||||
"@electron-forge/maker-squirrel": "7.6.0",
|
||||
"@electron-forge/maker-zip": "7.6.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.6.0",
|
||||
"@electron/rebuild": "4.0.1",
|
||||
"@react-scan/vite-plugin-react-scan": "0.1.8",
|
||||
"@types/memoizee": "0.4.9",
|
||||
@@ -118,7 +120,6 @@
|
||||
"cz-conventional-changelog": "3.3.0",
|
||||
"decompress": "4.2.1",
|
||||
"electron": "36.4.0",
|
||||
"electron-builder": "26.0.12",
|
||||
"husky": "4.3.8",
|
||||
"isomorphic-fetch": "3.0.0",
|
||||
"lint-staged": "12.3.8",
|
||||
@@ -130,14 +131,17 @@
|
||||
"vite": "npm:rolldown-vite@7.3.1",
|
||||
"vite-plugin-node-polyfills": "0.24.0",
|
||||
"vite-plugin-pwa": "0.21.1",
|
||||
"wait-on": "7.0.1"
|
||||
"wait-on": "7.0.1",
|
||||
"@reforged/maker-appimage": "5.1.1",
|
||||
"glob": "10.5.0",
|
||||
"progress": "2.0.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"js-yaml": "4.1.1",
|
||||
"baseline-browser-mapping": "^2.9.11",
|
||||
"baseline-browser-mapping": "2.9.11",
|
||||
"vite": "npm:rolldown-vite@7.3.1",
|
||||
"qs": "6.14.1",
|
||||
"mdast-util-to-hast": "12.3.0",
|
||||
"mdast-util-to-hast": "13.2.1",
|
||||
"node-forge": "1.3.3",
|
||||
"glob": "10.5.0",
|
||||
"tmp": "0.2.5",
|
||||
@@ -151,43 +155,6 @@
|
||||
"@peculiar/asn1-schema": "2.5.0"
|
||||
},
|
||||
"main": "electron/main.js",
|
||||
"build": {
|
||||
"appId": "seedit.desktop",
|
||||
"productName": "seedit",
|
||||
"beforePack": "electron/before-pack.js",
|
||||
"afterAllArtifactBuild": "electron/after-all-artifact-build.cjs",
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "bin/${os}",
|
||||
"to": "bin",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"build/**/*",
|
||||
"electron/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"extends": null,
|
||||
"mac": {
|
||||
"target": "dmg",
|
||||
"category": "public.app-category.social-networking",
|
||||
"type": "distribution",
|
||||
"identity": null
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"portable",
|
||||
"nsis"
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"target": "AppImage",
|
||||
"category": "Network"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"{src,electron}/**/*.{js,ts,tsx,mjs}": [
|
||||
"oxfmt"
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "أنت تشاهد هذه المجموعة بالفعل"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "আপনি ইতিমধ্যে এই সম্প্রদায়টি দেখছেন"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Tuto komunitu již prohlížíte"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Du ser allerede denne fællesskab"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Sie sehen diese Community bereits"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Εμφανίζετε ήδη αυτήν την κοινότητα"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"tags": "Tags",
|
||||
"moderator_of": "moderator of",
|
||||
"not_subscriber_nor_moderator": "You are not a subscriber nor a moderator of any community.",
|
||||
"more_posts_last_year": "{{count}} posts last {{currentTimeFilterName}}: <1>show more posts from last year</1>"
|
||||
"more_posts_last_year": "{{count}} posts last {{currentTimeFilterName}}: <1>show more posts from last year</1>",
|
||||
"already_in_community": "You're already viewing this community"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Ya estás viendo esta comunidad"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "شما در حال مشاهده این انجمن هستید"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Katsot jo tätä yhteisöä"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Nakikita mo na ang komunidad na ito"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Vous consultez déjà cette communauté"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "אתה כבר צופה בקהילה זו"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "आप पहले से ही इस समुदाय को देख रहे हैं"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Már ezt a közösséget nézed"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Anda sudah melihat komunitas ini"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Stai già visualizzando questa comunità"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "このコミュニティは既に表示中です"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "이미 이 커뮤니티를 보고 있습니다"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "तुम्ही आधीच हे समुदाय पाहत आहात"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Je bekijkt deze community al"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Du ser allerede på dette fellesskapet"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Już przeglądasz tę społeczność"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Você já está visualizando esta comunidade"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Vizualizezi deja această comunitate"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Вы уже просматриваете это сообщество"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Ju po shikoni tashmë këtë komunitet"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Du visar redan denna gemenskap"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "మీరు ఇప్పటికే ఈ కమ్యూనిటీని చూస్తున్నారు"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "คุณกำลังดูชุมชนนี้อยู่แล้ว"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Bu topluluğu zaten görüntülüyorsunuz"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Ви вже переглядаєте це співтовариство"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "آپ پہلے سے ہی اس کمیونٹی کو دیکھ رہے ہیں"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "Bạn đã đang xem cộng đồng này"
|
||||
}
|
||||
|
||||
@@ -386,5 +386,6 @@
|
||||
"no_global_rules": "...no global rules.",
|
||||
"for_reddits_downfall": "...for Reddit's downfall.",
|
||||
"evil_corp_cant_stop_us": "...Evil Corp™ can't stop us.",
|
||||
"no_gods_no_global_admins": "...no gods, no global admins."
|
||||
"no_gods_no_global_admins": "...no gods, no global admins.",
|
||||
"already_in_community": "您已经在查看此社区"
|
||||
}
|
||||
|
||||
124
scripts/find-forge-executable.js
Normal file
124
scripts/find-forge-executable.js
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/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 || 'seedit').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 });
|
||||
|
||||
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)) {
|
||||
return exePath;
|
||||
}
|
||||
}
|
||||
|
||||
const result = findExecutable(fullPath, platform);
|
||||
if (result) return result;
|
||||
} 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;
|
||||
}
|
||||
return fullPath;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
} else if (platform === 'linux') {
|
||||
// Linux executables (AppImage or unpacked)
|
||||
if (entry.name.endsWith('.AppImage')) {
|
||||
return fullPath;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
143
scripts/verify-executable.js
Normal file
143
scripts/verify-executable.js
Normal file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Verify that a packaged Electron executable starts correctly.
|
||||
* This script launches the executable and checks that the RPC port (9138) becomes available.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import { createServer } from 'net';
|
||||
|
||||
const executablePath = process.argv[2];
|
||||
const rpcPort = 9138;
|
||||
const timeout = 30000; // 30 seconds
|
||||
|
||||
if (!executablePath) {
|
||||
console.error('Usage: node scripts/verify-executable.js <executable-path>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(executablePath)) {
|
||||
console.error(`Error: Executable not found: ${executablePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Starting executable: ${executablePath}`);
|
||||
console.log(`Checking for RPC port ${rpcPort}...`);
|
||||
|
||||
// Start the executable
|
||||
const appProcess = spawn(executablePath, [], {
|
||||
detached: false,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
let appExited = false;
|
||||
let portAvailable = false;
|
||||
let timeoutId;
|
||||
|
||||
// Check if port is in use (app is running)
|
||||
function checkPort() {
|
||||
return new Promise((resolve) => {
|
||||
const server = createServer();
|
||||
server.listen(rpcPort, '127.0.0.1', () => {
|
||||
server.close();
|
||||
resolve(false); // Port is available (not in use yet)
|
||||
});
|
||||
server.on('error', (err) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
resolve(true); // Port is in use (app is running - success!)
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Monitor app process
|
||||
appProcess.on('exit', (code, signal) => {
|
||||
appExited = true;
|
||||
if (code !== null && code !== 0) {
|
||||
console.error(`App exited with code ${code}`);
|
||||
cleanup();
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
appProcess.stderr.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
// Filter out common harmless errors
|
||||
if (!text.includes('Gtk') && !text.includes('libnotify')) {
|
||||
console.error('stderr:', text);
|
||||
}
|
||||
});
|
||||
|
||||
// Poll for port availability
|
||||
async function pollPort() {
|
||||
const maxAttempts = 30; // 30 attempts over 30 seconds
|
||||
let attempts = 0;
|
||||
let pollTimeoutId;
|
||||
|
||||
const tick = async () => {
|
||||
attempts++;
|
||||
const inUse = await checkPort();
|
||||
|
||||
if (inUse) {
|
||||
portAvailable = true;
|
||||
console.log(`✓ RPC port ${rpcPort} is in use (app is running)`);
|
||||
clearTimeout(pollTimeoutId);
|
||||
clearTimeout(timeoutId);
|
||||
cleanup();
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (attempts >= maxAttempts || appExited) {
|
||||
clearTimeout(pollTimeoutId);
|
||||
clearTimeout(timeoutId);
|
||||
if (!portAvailable) {
|
||||
console.error(`✗ RPC port ${rpcPort} did not become available within ${timeout / 1000} seconds`);
|
||||
cleanup();
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
pollTimeoutId = setTimeout(tick, 1000);
|
||||
};
|
||||
|
||||
pollTimeoutId = setTimeout(tick, 1000);
|
||||
}
|
||||
|
||||
// Set overall timeout
|
||||
timeoutId = setTimeout(() => {
|
||||
if (!portAvailable) {
|
||||
console.error(`✗ Timeout: RPC port ${rpcPort} did not become available within ${timeout / 1000} seconds`);
|
||||
cleanup();
|
||||
process.exit(1);
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
function cleanup() {
|
||||
try {
|
||||
if (appProcess && !appProcess.killed) {
|
||||
appProcess.kill();
|
||||
// Also try to kill any child processes
|
||||
if (appProcess.pid) {
|
||||
try {
|
||||
process.kill(appProcess.pid, 'SIGTERM');
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
// Handle process termination
|
||||
process.on('SIGINT', cleanup);
|
||||
process.on('SIGTERM', cleanup);
|
||||
|
||||
// Start polling
|
||||
pollPort();
|
||||
@@ -1,30 +1,88 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { copyToClipboard } from '../../lib/utils/clipboard-utils';
|
||||
import styles from './error-display.module.css';
|
||||
|
||||
const ErrorDisplay = ({ error }: { error: any }) => {
|
||||
type ErrorDetails = {
|
||||
message?: string;
|
||||
stack?: string;
|
||||
details?: unknown;
|
||||
};
|
||||
|
||||
const getErrorDetails = (error: unknown): ErrorDetails | null => {
|
||||
if (!error) return null;
|
||||
if (typeof error === 'string') {
|
||||
return { message: error };
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return { message: error.message, stack: error.stack };
|
||||
}
|
||||
if (typeof error === 'object') {
|
||||
const maybeError = error as { message?: unknown; stack?: unknown; details?: unknown };
|
||||
return {
|
||||
message: typeof maybeError.message === 'string' ? maybeError.message : undefined,
|
||||
stack: typeof maybeError.stack === 'string' ? maybeError.stack : undefined,
|
||||
details: maybeError.details,
|
||||
};
|
||||
}
|
||||
return { message: String(error) };
|
||||
};
|
||||
|
||||
const stringifyError = (error: unknown) => {
|
||||
if (error instanceof Error) {
|
||||
return JSON.stringify({ name: error.name, message: error.message, stack: error.stack }, null, 2);
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(error, null, 2);
|
||||
} catch {
|
||||
return String(error);
|
||||
}
|
||||
};
|
||||
|
||||
const ErrorDisplay = ({ error }: { error: unknown }) => {
|
||||
const { t } = useTranslation();
|
||||
const [feedbackMessageKey, setFeedbackMessageKey] = useState<string | null>(null);
|
||||
const errorDetails = getErrorDetails(error);
|
||||
const feedbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const originalDisplayMessage = error?.message ? `${t('error')}: ${error.message}` : null;
|
||||
const originalDisplayMessage = errorDetails?.message ? `${t('error')}: ${errorDetails.message}` : null;
|
||||
|
||||
const clearFeedbackTimeout = () => {
|
||||
if (feedbackTimeoutRef.current) {
|
||||
clearTimeout(feedbackTimeoutRef.current);
|
||||
feedbackTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleFeedbackReset = () => {
|
||||
clearFeedbackTimeout();
|
||||
feedbackTimeoutRef.current = setTimeout(() => {
|
||||
setFeedbackMessageKey(null);
|
||||
feedbackTimeoutRef.current = null;
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearFeedbackTimeout();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleMessageClick = async () => {
|
||||
if (!error || !error.message || feedbackMessageKey) return;
|
||||
if (!errorDetails?.message || feedbackMessageKey) return;
|
||||
|
||||
const errorString = JSON.stringify(error, null, 2);
|
||||
const errorString = stringifyError(error);
|
||||
try {
|
||||
await copyToClipboard(errorString);
|
||||
setFeedbackMessageKey('copied');
|
||||
setTimeout(() => {
|
||||
setFeedbackMessageKey(null);
|
||||
}, 1500);
|
||||
scheduleFeedbackReset();
|
||||
} catch (err) {
|
||||
console.error('Failed to copy error: ', err);
|
||||
setFeedbackMessageKey('failed');
|
||||
setTimeout(() => {
|
||||
setFeedbackMessageKey(null);
|
||||
}, 1500);
|
||||
scheduleFeedbackReset();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -44,8 +102,10 @@ const ErrorDisplay = ({ error }: { error: any }) => {
|
||||
classNames.push(styles.clickableErrorMessage);
|
||||
}
|
||||
|
||||
const shouldRender = Boolean(errorDetails?.message || errorDetails?.stack || errorDetails?.details || error);
|
||||
|
||||
return (
|
||||
(error?.message || error?.stack || error?.details || error) && (
|
||||
shouldRender && (
|
||||
<div className={styles.error}>
|
||||
{currentDisplayMessage && (
|
||||
<span
|
||||
|
||||
@@ -11,7 +11,7 @@ const InfoTooltip = ({ content, showTooltip = true }: InfoTooltipProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isExiting, setIsExiting] = useState(false);
|
||||
const exitTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const exitTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
|
||||
// Handle opening and closing with animations
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
|
||||
@@ -13,9 +13,14 @@ interface MarkdownProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface ExtendedComponents extends Components {
|
||||
spoiler: React.ComponentType<{ children: React.ReactNode }>;
|
||||
}
|
||||
type ExtendedComponents = Partial<Components> & {
|
||||
spoiler?: React.ComponentType<{ children?: React.ReactNode }>;
|
||||
a?: React.ComponentType<{ children?: React.ReactNode; href?: string }>;
|
||||
img?: React.ComponentType<{ src?: string; alt?: string }>;
|
||||
video?: React.ComponentType<{ src?: string }>;
|
||||
iframe?: React.ComponentType<{ src?: string }>;
|
||||
source?: React.ComponentType<{ src?: string }>;
|
||||
};
|
||||
|
||||
const MAX_LENGTH_FOR_GFM = 10000; // remarkGfm lags with large content
|
||||
|
||||
@@ -129,25 +134,25 @@ const Markdown = ({ content }: MarkdownProps) => {
|
||||
<ReactMarkdown
|
||||
children={preprocessedContent}
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={[[rehypeRaw as any], [rehypeSanitize, customSchema]]}
|
||||
rehypePlugins={[[rehypeRaw as any], [rehypeSanitize as any, customSchema]]}
|
||||
components={
|
||||
{
|
||||
a: ({ children, href }) => renderAnchorLink(children, href || ''),
|
||||
p: ({ children }) => {
|
||||
a: ({ children, href }: { children?: React.ReactNode; href?: string }) => renderAnchorLink(children, href || ''),
|
||||
p: ({ children }: { children?: React.ReactNode }) => {
|
||||
const isEmpty =
|
||||
!children ||
|
||||
(Array.isArray(children) && children.every((child) => child === null || child === undefined || (typeof child === 'string' && child.trim() === '')));
|
||||
|
||||
return !isEmpty && <p>{children}</p>;
|
||||
},
|
||||
img: ({ src, alt }) => {
|
||||
img: ({ src, alt }: { src?: string; alt?: string }) => {
|
||||
const displayText = src || alt || 'image';
|
||||
return <span>{displayText}</span>;
|
||||
},
|
||||
video: ({ src }) => <span>{src}</span>,
|
||||
iframe: ({ src }) => <span>{src}</span>,
|
||||
source: ({ src }) => <span>{src}</span>,
|
||||
spoiler: ({ children }) => <SpoilerText>{children}</SpoilerText>,
|
||||
video: ({ src }: { src?: string }) => <span>{src}</span>,
|
||||
iframe: ({ src }: { src?: string }) => <span>{src}</span>,
|
||||
source: ({ src }: { src?: string }) => <span>{src}</span>,
|
||||
spoiler: ({ children }: { children?: React.ReactNode }) => <SpoilerText>{children}</SpoilerText>,
|
||||
} as ExtendedComponents
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Author, useAccount, useComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
@@ -59,20 +59,34 @@ const ModOrReportButton = ({ cid, isAuthor, isAccountMod, isCommentAuthorMod }:
|
||||
const ShareButton = ({ cid, subplebbitAddress }: { cid: string; subplebbitAddress: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const [hasCopied, setHasCopied] = useState(false);
|
||||
const resetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasCopied) {
|
||||
setTimeout(() => setHasCopied(false), 2000);
|
||||
const clearResetTimeout = () => {
|
||||
if (resetTimeoutRef.current) {
|
||||
clearTimeout(resetTimeoutRef.current);
|
||||
resetTimeoutRef.current = null;
|
||||
}
|
||||
}, [hasCopied]);
|
||||
};
|
||||
|
||||
const scheduleReset = () => {
|
||||
clearResetTimeout();
|
||||
resetTimeoutRef.current = setTimeout(() => {
|
||||
setHasCopied(false);
|
||||
resetTimeoutRef.current = null;
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
useEffect(() => () => clearResetTimeout(), []);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
setHasCopied(true);
|
||||
scheduleReset();
|
||||
await copyShareLinkToClipboard(subplebbitAddress, cid);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy share link:', error);
|
||||
setHasCopied(false);
|
||||
clearResetTimeout();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -141,6 +141,10 @@ const SearchBar = ({ isFocused = false, onExpandoChange }: SearchBarProps) => {
|
||||
}
|
||||
const searchInput = searchInputRef.current?.value;
|
||||
if (searchInput) {
|
||||
if (searchInput.toLowerCase() === params.subplebbitAddress?.toLowerCase()) {
|
||||
alert(t('already_in_community'));
|
||||
return;
|
||||
}
|
||||
setInputValue('');
|
||||
navigate(`/s/${searchInput}`);
|
||||
}
|
||||
@@ -179,6 +183,10 @@ const SearchBar = ({ isFocused = false, onExpandoChange }: SearchBarProps) => {
|
||||
|
||||
const handleCommunitySelect = useCallback(
|
||||
(address: string) => {
|
||||
if (address.toLowerCase() === params.subplebbitAddress?.toLowerCase()) {
|
||||
alert(t('already_in_community'));
|
||||
return;
|
||||
}
|
||||
setInputValue('');
|
||||
setIsInputFocused(false);
|
||||
setActiveDropdownIndex(-1);
|
||||
@@ -186,7 +194,7 @@ const SearchBar = ({ isFocused = false, onExpandoChange }: SearchBarProps) => {
|
||||
searchInputRef.current?.blur();
|
||||
navigate(`/s/${address}`);
|
||||
},
|
||||
[navigate, setInputValue, setIsInputFocused, setActiveDropdownIndex],
|
||||
[navigate, setInputValue, setIsInputFocused, setActiveDropdownIndex, params.subplebbitAddress, t],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
@@ -230,7 +238,6 @@ const SearchBar = ({ isFocused = false, onExpandoChange }: SearchBarProps) => {
|
||||
autoCapitalize='off'
|
||||
placeholder={placeholder}
|
||||
ref={(instance) => {
|
||||
// @ts-expect-error Property 'current' is read-only.
|
||||
searchInputRef.current = instance;
|
||||
refs.setReference(instance);
|
||||
}}
|
||||
|
||||
@@ -84,9 +84,11 @@ const PostInfo = ({ comment }: { comment: Comment | undefined }) => {
|
||||
<span className={styles.postScoreWord}>{postScore === 1 ? t('point') : t('points')}</span>{' '}
|
||||
{`(${postScore === '?' ? '?' : `${upvotePercentage}`}% ${t('upvoted')})`}
|
||||
</div>
|
||||
<div className={styles.shareLink}>
|
||||
{t('share_link')}: <input type='text' value={`https://pleb.bz/s/${subplebbitAddress}/c/${cid}`} readOnly={true} />
|
||||
</div>
|
||||
{subplebbitAddress && cid && (
|
||||
<div className={styles.shareLink}>
|
||||
{t('share_link')}: <input type='text' value={`https://seedit.app/s/${subplebbitAddress}/c/${cid}`} readOnly={true} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,8 +35,8 @@ const useIsSubplebbitOffline = (subplebbit: Subplebbit) => {
|
||||
const offlineTitle = isLoading
|
||||
? t('loading')
|
||||
: updatedAt
|
||||
? isOffline && t('posts_last_synced_info', { time: getFormattedTimeAgo(updatedAt), interpolation: { escapeValue: false } })
|
||||
: t('subplebbit_offline_info');
|
||||
? isOffline && t('posts_last_synced_info', { time: getFormattedTimeAgo(updatedAt), interpolation: { escapeValue: false } })
|
||||
: t('subplebbit_offline_info');
|
||||
|
||||
// ensure isOffline is false until we have enough information
|
||||
const hasEnoughInfo = subplebbitOfflineStore.initialLoad === false || updatedAt !== undefined;
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import assert from 'assert';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useParams, Params } from 'react-router-dom';
|
||||
import { isSubplebbitView, isAllView, isModView, isHomeView, isDomainView } from '../lib/utils/view-utils';
|
||||
|
||||
// the timestamp the last time the user visited
|
||||
const lastVisitTimestamp = localStorage.getItem('seeditLastVisitTimestamp');
|
||||
|
||||
// update the last visited timestamp every n seconds
|
||||
setInterval(() => {
|
||||
localStorage.setItem('seeditLastVisitTimestamp', Date.now().toString());
|
||||
}, 60 * 1000);
|
||||
const VISIT_UPDATE_INTERVAL_MS = 60 * 1000;
|
||||
let lastVisitIntervalId: ReturnType<typeof setInterval> | null = null;
|
||||
let lastVisitIntervalUsers = 0;
|
||||
|
||||
const startLastVisitInterval = () => {
|
||||
if (lastVisitIntervalId) return;
|
||||
lastVisitIntervalId = setInterval(() => {
|
||||
localStorage.setItem('seeditLastVisitTimestamp', Date.now().toString());
|
||||
}, VISIT_UPDATE_INTERVAL_MS);
|
||||
};
|
||||
|
||||
const stopLastVisitIntervalIfUnused = () => {
|
||||
if (lastVisitIntervalUsers === 0 && lastVisitIntervalId) {
|
||||
clearInterval(lastVisitIntervalId);
|
||||
lastVisitIntervalId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const timeFilterNamesToSeconds: Record<string, number | undefined> = {
|
||||
'1h': 60 * 60,
|
||||
@@ -120,6 +134,16 @@ const useTimeFilter = () => {
|
||||
const isInDomainView = Boolean(params.domain);
|
||||
const sessionKey = getSessionKeyForView(location.pathname, params);
|
||||
|
||||
useEffect(() => {
|
||||
lastVisitIntervalUsers += 1;
|
||||
startLastVisitInterval();
|
||||
|
||||
return () => {
|
||||
lastVisitIntervalUsers -= 1;
|
||||
stopLastVisitIntervalIfUnused();
|
||||
};
|
||||
}, []);
|
||||
|
||||
let timeFilterName = params.timeFilterName;
|
||||
if (!timeFilterName) {
|
||||
const sessionPreference = getSessionTimeFilterPreference(sessionKey);
|
||||
|
||||
@@ -9,6 +9,12 @@ import './themes.css';
|
||||
import './preload-assets.css';
|
||||
import { App as CapacitorApp } from '@capacitor/app';
|
||||
import { registerSW } from 'virtual:pwa-register';
|
||||
import { Analytics } from '@vercel/analytics/react';
|
||||
|
||||
// Only enable analytics on seedit.app (Vercel deployment)
|
||||
// Exclude Electron (file:// or localhost), Capacitor/APK (capacitor:// or localhost), and IPFS (ipfs:// or different domain)
|
||||
const isVercelDeployment =
|
||||
typeof window !== 'undefined' && (window.location.hostname === 'seedit.app' || window.location.hostname === 'www.seedit.app') && !window.isElectron;
|
||||
|
||||
if (window.location.hostname.startsWith('p2p.')) {
|
||||
(window as any).defaultPlebbitOptions = {
|
||||
@@ -38,6 +44,7 @@ root.render(
|
||||
<React.StrictMode>
|
||||
<Router>
|
||||
<App />
|
||||
{isVercelDeployment && <Analytics />}
|
||||
</Router>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ export const isValidURL = (url: string) => {
|
||||
};
|
||||
|
||||
export const copyShareLinkToClipboard = async (subplebbitAddress: string, cid: string) => {
|
||||
const shareLink = `https://pleb.bz/s/${subplebbitAddress}/c/${cid}`;
|
||||
const shareLink = `https://seedit.app/s/${subplebbitAddress}/c/${cid}`;
|
||||
await copyToClipboard(shareLink);
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const Author = () => {
|
||||
|
||||
const { authorComments, error, lastCommentCid, hasMore, loadMore } = useAuthorComments({ commentCid, authorAddress });
|
||||
|
||||
const prevErrorMessageRef = useRef<string | undefined>();
|
||||
const prevErrorMessageRef = useRef<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (error && error.message !== prevErrorMessageRef.current) {
|
||||
console.log(error);
|
||||
|
||||
@@ -152,7 +152,7 @@ const Overview = () => {
|
||||
}
|
||||
}, [error, sortedComments]);
|
||||
|
||||
const prevErrorMessageRef = useRef<string | undefined>();
|
||||
const prevErrorMessageRef = useRef<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (error && error.message !== prevErrorMessageRef.current) {
|
||||
console.log(error);
|
||||
@@ -193,7 +193,7 @@ const Comments = () => {
|
||||
}
|
||||
}, [error, sortedComments]);
|
||||
|
||||
const prevErrorMessageRef = useRef<string | undefined>();
|
||||
const prevErrorMessageRef = useRef<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (error && error.message !== prevErrorMessageRef.current) {
|
||||
console.log(error);
|
||||
@@ -234,7 +234,7 @@ const Submitted = () => {
|
||||
}
|
||||
}, [error, sortedComments]);
|
||||
|
||||
const prevErrorMessageRef = useRef<string | undefined>();
|
||||
const prevErrorMessageRef = useRef<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (error && error.message !== prevErrorMessageRef.current) {
|
||||
console.log(error);
|
||||
@@ -279,7 +279,7 @@ const VotedComments = ({ voteType }: { voteType: 1 | -1 }) => {
|
||||
}
|
||||
}, [error, paginatedCids, hasMore]);
|
||||
|
||||
const prevErrorMessageRef = useRef<string | undefined>();
|
||||
const prevErrorMessageRef = useRef<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (error && error.message !== prevErrorMessageRef.current) {
|
||||
console.log(error);
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { requestNotificationPermission, showLocalNotification } from '../../../lib/push';
|
||||
import useContentOptionsStore from '../../../stores/use-content-options-store';
|
||||
import styles from './notifications-settings.module.css';
|
||||
|
||||
const clearTimeoutRef = (timeoutRef: { current: ReturnType<typeof setTimeout> | null }) => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const NotificationsSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const { enableLocalNotifications, setEnableLocalNotifications } = useContentOptionsStore();
|
||||
const [permissionStatus, setPermissionStatus] = useState<string | null>(null);
|
||||
const [platform, setPlatform] = useState<NodeJS.Platform | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showDeniedMessage, setShowDeniedMessage] = useState(false);
|
||||
const [showSuccessMessage, setShowSuccessMessage] = useState(false);
|
||||
const [flashStatus, setFlashStatus] = useState<'denied' | 'success' | null>(null);
|
||||
const flashTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const scheduleFlashReset = useCallback((status: 'denied' | 'success') => {
|
||||
setFlashStatus(status);
|
||||
clearTimeoutRef(flashTimeoutRef);
|
||||
flashTimeoutRef.current = setTimeout(() => {
|
||||
setFlashStatus(null);
|
||||
flashTimeoutRef.current = null;
|
||||
}, 5000);
|
||||
}, []);
|
||||
|
||||
// Function to check permission via API, memoized with useCallback
|
||||
const checkPermissionStatus = useCallback(async () => {
|
||||
@@ -28,8 +44,7 @@ const NotificationsSettings = () => {
|
||||
if (enableLocalNotifications) {
|
||||
setEnableLocalNotifications(false);
|
||||
}
|
||||
setShowDeniedMessage(true);
|
||||
setTimeout(() => setShowDeniedMessage(false), 5000);
|
||||
scheduleFlashReset('denied');
|
||||
} else if (nativeStatus === 'not-determined') {
|
||||
console.warn('[NotificationsSettings] Permission status is not-determined, cannot test.');
|
||||
} else if (nativeStatus === 'not-supported') {
|
||||
@@ -41,23 +56,39 @@ const NotificationsSettings = () => {
|
||||
console.error('[Electron Native] Error checking notification permissions:', err);
|
||||
setPermissionStatus('unknown');
|
||||
}
|
||||
}, [setEnableLocalNotifications, enableLocalNotifications]);
|
||||
}, [enableLocalNotifications, scheduleFlashReset, setEnableLocalNotifications]);
|
||||
|
||||
// Run the check on mount
|
||||
useEffect(() => {
|
||||
if (window.electronApi) {
|
||||
if (window.electronApi.getPlatform) {
|
||||
window.electronApi.getPlatform().then(setPlatform).catch(console.error);
|
||||
if (!window.electronApi) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const loadPlatform = async () => {
|
||||
if (!window.electronApi?.getPlatform) return;
|
||||
try {
|
||||
const value = await window.electronApi.getPlatform();
|
||||
if (!cancelled) {
|
||||
setPlatform(value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
checkPermissionStatus();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // Keep dependencies empty - runs only once
|
||||
};
|
||||
|
||||
loadPlatform();
|
||||
void checkPermissionStatus();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [checkPermissionStatus]);
|
||||
|
||||
useEffect(() => () => clearTimeoutRef(flashTimeoutRef), []);
|
||||
|
||||
const handleCheckboxChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const isEnabled = event.target.checked;
|
||||
setIsLoading(true);
|
||||
setShowSuccessMessage(false);
|
||||
setFlashStatus(null);
|
||||
|
||||
try {
|
||||
if (isEnabled) {
|
||||
@@ -67,12 +98,10 @@ const NotificationsSettings = () => {
|
||||
setPermissionStatus(currentStatus);
|
||||
if (currentStatus === 'granted') {
|
||||
setEnableLocalNotifications(true);
|
||||
setShowSuccessMessage(true);
|
||||
setTimeout(() => setShowSuccessMessage(false), 5000);
|
||||
scheduleFlashReset('success');
|
||||
} else if (currentStatus === 'denied') {
|
||||
setEnableLocalNotifications(false); // Ensure it's off if denied
|
||||
setShowDeniedMessage(true);
|
||||
setTimeout(() => setShowDeniedMessage(false), 5000);
|
||||
scheduleFlashReset('denied');
|
||||
} else if (currentStatus === 'not-determined') {
|
||||
setEnableLocalNotifications(false); // Keep it off until granted
|
||||
console.warn('[NotificationsSettings] Permission not determined. User must grant via OS prompt.');
|
||||
@@ -87,13 +116,11 @@ const NotificationsSettings = () => {
|
||||
if (granted) {
|
||||
setEnableLocalNotifications(true);
|
||||
setPermissionStatus('granted');
|
||||
setShowSuccessMessage(true);
|
||||
setTimeout(() => setShowSuccessMessage(false), 5000);
|
||||
scheduleFlashReset('success');
|
||||
} else {
|
||||
setEnableLocalNotifications(false);
|
||||
setPermissionStatus('denied');
|
||||
setShowDeniedMessage(true);
|
||||
setTimeout(() => setShowDeniedMessage(false), 5000);
|
||||
scheduleFlashReset('denied');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -121,8 +148,7 @@ const NotificationsSettings = () => {
|
||||
});
|
||||
} else if (status === 'denied') {
|
||||
alert('Notifications are denied in System Settings.');
|
||||
setShowDeniedMessage(true);
|
||||
setTimeout(() => setShowDeniedMessage(false), 5000);
|
||||
scheduleFlashReset('denied');
|
||||
} else if (status === 'not-determined') {
|
||||
alert('Permission not yet granted. The app will ask when it first tries to notify you (or test again).');
|
||||
} else {
|
||||
@@ -162,14 +188,14 @@ const NotificationsSettings = () => {
|
||||
)}
|
||||
|
||||
{/* Permission status messages */}
|
||||
{showDeniedMessage && permissionStatus === 'denied' && (
|
||||
{flashStatus === 'denied' && permissionStatus === 'denied' && (
|
||||
<span className={styles.permissionStatus} data-status={permissionStatus}>
|
||||
<span className={styles.permissionStatusDenied}>
|
||||
{window.electronApi?.isElectron && platform === 'darwin'
|
||||
? 'Permission denied. Please go to System Settings > Notifications > Seedit and enable notifications.'
|
||||
: window.electronApi?.isElectron
|
||||
? `Permission denied. Please check your system's ${platform && `(${platform}) `} notification settings for this application.`
|
||||
: `Permission denied. Please allow notifications for this site in your browser settings.`}
|
||||
? `Permission denied. Please check your system's ${platform ? `(${platform}) ` : ''} notification settings for this application.`
|
||||
: `Permission denied. Please allow notifications for this site in your browser settings.`}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
@@ -178,7 +204,7 @@ const NotificationsSettings = () => {
|
||||
<span className={styles.permissionStatusRequesting}>Click "Allow" to enable notifications</span>
|
||||
</span>
|
||||
)}
|
||||
{showSuccessMessage && permissionStatus === 'granted' && enableLocalNotifications && (
|
||||
{flashStatus === 'success' && permissionStatus === 'granted' && enableLocalNotifications && (
|
||||
<span className={styles.permissionStatus} data-status={permissionStatus}>
|
||||
<span className={styles.permissionStatusSuccess}>
|
||||
Success! You're done.
|
||||
|
||||
@@ -5,16 +5,16 @@ import { setAccount, useAccount, usePlebbitRpcSettings } from '@plebbit/plebbit-
|
||||
import styles from './plebbit-options.module.css';
|
||||
|
||||
interface SettingsProps {
|
||||
ipfsGatewayUrlsRef?: RefObject<HTMLTextAreaElement>;
|
||||
mediaIpfsGatewayUrlRef?: RefObject<HTMLInputElement>;
|
||||
pubsubProvidersRef?: RefObject<HTMLTextAreaElement>;
|
||||
ethRpcRef?: RefObject<HTMLTextAreaElement>;
|
||||
solRpcRef?: RefObject<HTMLTextAreaElement>;
|
||||
maticRpcRef?: RefObject<HTMLTextAreaElement>;
|
||||
avaxRpcRef?: RefObject<HTMLTextAreaElement>;
|
||||
httpRoutersRef?: RefObject<HTMLTextAreaElement>;
|
||||
plebbitRpcRef?: RefObject<HTMLInputElement>;
|
||||
plebbitDataPathRef?: RefObject<HTMLInputElement>;
|
||||
ipfsGatewayUrlsRef?: RefObject<HTMLTextAreaElement | null>;
|
||||
mediaIpfsGatewayUrlRef?: RefObject<HTMLInputElement | null>;
|
||||
pubsubProvidersRef?: RefObject<HTMLTextAreaElement | null>;
|
||||
ethRpcRef?: RefObject<HTMLTextAreaElement | null>;
|
||||
solRpcRef?: RefObject<HTMLTextAreaElement | null>;
|
||||
maticRpcRef?: RefObject<HTMLTextAreaElement | null>;
|
||||
avaxRpcRef?: RefObject<HTMLTextAreaElement | null>;
|
||||
httpRoutersRef?: RefObject<HTMLTextAreaElement | null>;
|
||||
plebbitRpcRef?: RefObject<HTMLInputElement | null>;
|
||||
plebbitDataPathRef?: RefObject<HTMLInputElement | null>;
|
||||
}
|
||||
|
||||
const IPFSGatewaysSettings = ({ ipfsGatewayUrlsRef, mediaIpfsGatewayUrlRef }: SettingsProps) => {
|
||||
|
||||
@@ -116,7 +116,7 @@ const SubplebbitDataEditor = () => {
|
||||
const subplebbitSettings = useMemo(() => JSON.stringify(currentSettings, null, 2), [currentSettings]);
|
||||
|
||||
// Update text when settings change, but not when user is actively typing
|
||||
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
// Clear any pending timeout
|
||||
if (timeoutRef.current) {
|
||||
|
||||
@@ -355,7 +355,7 @@ const Subplebbit = () => {
|
||||
const hasUnhiddenAnyNsfwCommunity = !hideAdultCommunities || !hideGoreCommunities || !hideAntiCommunities || !hideVulgarCommunities;
|
||||
const isBroadlyNsfwSubplebbit = useIsBroadlyNsfwSubplebbit(subplebbitAddress || '');
|
||||
|
||||
const prevErrorMessageRef = useRef<string | undefined>();
|
||||
const prevErrorMessageRef = useRef<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (error && error.message !== prevErrorMessageRef.current) {
|
||||
console.log(error);
|
||||
|
||||
@@ -581,7 +581,7 @@ const Subplebbits = () => {
|
||||
}, [documentTitle]);
|
||||
|
||||
const renderErrors = () => {
|
||||
const errorsToDisplay: JSX.Element[] = [];
|
||||
const errorsToDisplay: React.JSX.Element[] = [];
|
||||
Object.entries(errors).forEach(([source, errorObj]) => {
|
||||
if (!errorObj) return;
|
||||
|
||||
|
||||
5
vercel.json
Normal file
5
vercel.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"buildCommand": "yarn build",
|
||||
"outputDirectory": "build",
|
||||
"framework": "vite"
|
||||
}
|
||||
119
vite.config.js
119
vite.config.js
@@ -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,
|
||||
}),
|
||||
!isProduction &&
|
||||
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