feat: upgrade toolchain and soften sync recovery after network blips

Bump Vite 8, matrix-js-sdk, and related deps; fix immer/vanilla-extract imports for new majors. Replace blocking sync error dialog with background recovery on wake/offline, and disable Vite auto-open in dev. Add feature handoff documentation index.
This commit is contained in:
2026-07-09 01:55:09 +10:00
parent f67f08f1db
commit e058165830
61 changed files with 8456 additions and 5547 deletions

112
docs/README.md Normal file
View File

@@ -0,0 +1,112 @@
# Paarrot documentation
Documentation for the Paarrot Matrix client (Cinny fork). Use this index to find feature handoffs, API references, and developer guides.
## Handoff docs (start here)
Per-feature handoff folders live under [`handoff/features/`](./handoff/features/). Each folder has a `HANDOFF.md` with architecture, key files, data model, and maintenance notes.
New feature? Copy [`handoff/TEMPLATE.md`](./handoff/TEMPLATE.md) into `handoff/features/<feature-name>/HANDOFF.md` and add a row below.
### Platform & shell
| Feature | Handoff |
|---------|---------|
| Electron desktop shell | [electron-shell](./handoff/features/electron-shell/HANDOFF.md) |
| Android (Capacitor/Tauri) | [android](./handoff/features/android/HANDOFF.md) |
| Client chrome (title bar, splash, welcome) | [client-chrome](./handoff/features/client-chrome/HANDOFF.md) |
| Local HTTP API | [local-api](./handoff/features/local-api/HANDOFF.md) |
| Stream Deck integration | [streamdeck](./handoff/features/streamdeck/HANDOFF.md) |
| Background notifications | [background-notifications](./handoff/features/background-notifications/HANDOFF.md) |
### Core client
| Feature | Handoff |
|---------|---------|
| Authentication | [authentication](./handoff/features/authentication/HANDOFF.md) |
| Matrix client (init, sync, providers) | [matrix-client](./handoff/features/matrix-client/HANDOFF.md) |
| State management (Jotai) | [state-management](./handoff/features/state-management/HANDOFF.md) |
| Routing & navigation | [routing-and-navigation](./handoff/features/routing-and-navigation/HANDOFF.md) |
| E2E encryption & devices | [e2e-and-devices](./handoff/features/e2e-and-devices/HANDOFF.md) |
### Navigation & panels
| Feature | Handoff |
|---------|---------|
| Home & sidebar | [home-and-sidebar](./handoff/features/home-and-sidebar/HANDOFF.md) |
| Room nav (channel rows) | [room-nav](./handoff/features/room-nav/HANDOFF.md) |
| Direct messages | [direct-messages](./handoff/features/direct-messages/HANDOFF.md) |
| Inbox & invites | [inbox-and-invites](./handoff/features/inbox-and-invites/HANDOFF.md) |
| Explore (public rooms) | [explore](./handoff/features/explore/HANDOFF.md) |
| Join before navigate | [join-before-navigate](./handoff/features/join-before-navigate/HANDOFF.md) |
### Messaging
| Feature | Handoff |
|---------|---------|
| Room timeline | [room-timeline](./handoff/features/room-timeline/HANDOFF.md) |
| Editor & composer | [editor-and-composer](./handoff/features/editor-and-composer/HANDOFF.md) |
| Message rendering (markdown, HTML) | [message-rendering](./handoff/features/message-rendering/HANDOFF.md) |
| Media & URL preview | [media-and-url-preview](./handoff/features/media-and-url-preview/HANDOFF.md) |
| Message search | [message-search](./handoff/features/message-search/HANDOFF.md) |
| Global search (quick nav) | [global-search](./handoff/features/global-search/HANDOFF.md) |
### Rooms, spaces & creation
| Feature | Handoff |
|---------|---------|
| Create flows (room/space/DM) | [create-flows](./handoff/features/create-flows/HANDOFF.md) |
| Room settings | [room-settings](./handoff/features/room-settings/HANDOFF.md) |
| Space settings | [space-settings](./handoff/features/space-settings/HANDOFF.md) |
| Common settings (shared pages) | [common-settings](./handoff/features/common-settings/HANDOFF.md) |
| Sub-rooms | [sub-rooms](./handoff/features/sub-rooms/HANDOFF.md) |
| Lobby (space card view) | [lobby-forums](./handoff/features/lobby-forums/HANDOFF.md) |
| Forum spaces (post feed UI) | [forum](./handoff/features/forum/HANDOFF.md) |
### Settings & customization
| Feature | Handoff |
|---------|---------|
| User settings | [user-settings](./handoff/features/user-settings/HANDOFF.md) |
| Themes & appearance | [themes-and-appearance](./handoff/features/themes-and-appearance/HANDOFF.md) |
| Emojis & stickers | [emoji-stickers](./handoff/features/emoji-stickers/HANDOFF.md) |
| Custom emoji packs | [custom-emoji-packs](./handoff/features/custom-emoji-packs/HANDOFF.md) |
| Avatar metadata | [avatar-metadata](./handoff/features/avatar-metadata/HANDOFF.md) |
| Embed filters | [embed-filters](./handoff/features/embed-filters/HANDOFF.md) |
| Developer tools | [developer-tools](./handoff/features/developer-tools/HANDOFF.md) |
### Real-time & extensions
| Feature | Handoff |
|---------|---------|
| Voice & video calls | [voice-calls](./handoff/features/voice-calls/HANDOFF.md) |
| Plugin system | [plugins](./handoff/features/plugins/HANDOFF.md) |
## API & plugin references
| Doc | Description |
|-----|-------------|
| [API.md](./API.md) | Full local HTTP API reference (port 33384) |
| [API-QUICKSTART.md](./API-QUICKSTART.md) | Quick start for the local API |
| [PLUGINS.md](./PLUGINS.md) | Plugin system user guide |
| [PLUGIN_API.md](./PLUGIN_API.md) | Full plugin API reference |
| [PLUGIN_BUTTON_API.md](./PLUGIN_BUTTON_API.md) | UI button injection API |
| [PLUGIN_SYSTEM_IMPLEMENTATION.md](./PLUGIN_SYSTEM_IMPLEMENTATION.md) | Internal plugin architecture |
Plugin development guide (in-app): [`src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md`](../src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md)
## Repo layout (docs-related)
```
cinny/docs/
README.md ← you are here
handoff/
README.md ← how to write handoff docs
TEMPLATE.md ← copy for new features
features/
<feature-name>/
HANDOFF.md
API.md ← reference docs (kept at root for links)
PLUGINS.md
...
```

53
docs/handoff/README.md Normal file
View File

@@ -0,0 +1,53 @@
# Handoff documentation
Handoff docs capture what a new maintainer needs to own a feature: behavior, architecture, data model, sharp edges, and how to verify changes.
## When to write one
Create or update a handoff when:
- A feature is Paarrot-specific (not vanilla Cinny upstream)
- The feature spans multiple layers (UI + Matrix + Electron/Android)
- You are about to hand work to another person or pause development
Inherited Cinny features (basic room timeline, auth flows, etc.) do not need handoffs unless Paarrot has materially changed them.
## Folder convention
```
handoff/features/<kebab-case-name>/
HANDOFF.md
```
Use short, product-facing names: `voice-calls`, `sub-rooms`, `local-api`.
## How to fill in a handoff
1. Copy [`TEMPLATE.md`](./TEMPLATE.md) to `features/<name>/HANDOFF.md`
2. Fill every section — delete "Add your extra things here" placeholders
3. Link to existing reference docs instead of duplicating them
4. Add the feature to the table in [`../README.md`](../README.md)
5. List real file paths (grep the codebase; do not guess)
## Sections explained
| Section | Purpose |
|---------|---------|
| **Summary** | One paragraph: what it does and why it exists |
| **User-facing behavior** | What users see and configure |
| **Architecture** | Layers, main modules, request/event flow |
| **Key files** | Paths a maintainer will touch first |
| **Data model** | Matrix state events, account data, local storage |
| **Dependencies** | External services, npm packages, OS APIs |
| **Integration points** | Other features, IPC, plugins, API |
| **Testing** | Manual steps and any automated tests |
| **Known issues** | Bugs, limitations, tech debt |
| **Related docs** | Links to API refs, READMEs, upstream specs |
## Keeping handoffs current
Update the handoff in the same PR when you:
- Add a new Matrix event type or account data key
- Change IPC or HTTP API contracts
- Move or rename core modules

67
docs/handoff/TEMPLATE.md Normal file
View File

@@ -0,0 +1,67 @@
# <Feature name> — handoff
> Copy this file to `handoff/features/<feature-name>/HANDOFF.md` and replace all placeholders.
## Summary
<!-- One paragraph: what this feature does and why Paarrot has it. -->
## User-facing behavior
<!-- What users see, where in the UI, and what settings exist. -->
## Architecture
<!-- High-level flow: UI → hooks/services → Matrix/Electron/OS. -->
```
<!-- Optional ASCII or mermaid diagram -->
```
## Key files
| Path | Role |
|------|------|
| `path/to/file` | <!-- what it does --> |
## Data model
<!-- Matrix state events, account data types, local storage keys. -->
| Key / event | Scope | Shape |
|-------------|-------|-------|
| | | |
## Dependencies
<!-- npm packages, external APIs, OS permissions. -->
## Integration points
<!-- Other features, local API, plugins, IPC channels. -->
## Testing
### Manual
1. <!-- step -->
### Automated
<!-- test files or "none yet" -->
## Known issues & gotchas
<!-- Sharp edges, race conditions, platform differences. -->
## Future work
<!-- Planned improvements or open questions. -->
## Related docs
- <!-- links -->
## Add your extra things here
<!-- Feature-specific notes: rollout flags, server requirements, MSC numbers, etc. -->

View File

@@ -0,0 +1,88 @@
# Android (Capacitor / Tauri) — handoff
## Summary
Paarrot ships an Android build via Capacitor and/or Tauri (`src-tauri/`, `cinny/capacitor.config.json`). Mobile-specific code handles native notifications, UnifiedPush background sync, share intents, and platform detection branching away from Electron APIs.
## User-facing behavior
- Native Android app with Paarrot UI (WebView)
- Share into Paarrot from other apps (images/files → room upload draft)
- Background notifications via UnifiedPush (not FCM-only)
- No local HTTP API on mobile (Electron-only)
## Architecture
```
Capacitor/Tauri shell
→ WebView loads cinny bundle
ClientNonUIFeatures.tsx
→ isCapacitorNative / isTauri branches
backgroundSync.ts → UnifiedPush + Matrix pusher
androidShare.ts → share intent → room draft
tauri.ts → native notification APIs
```
## Key files
| Path | Role |
|------|------|
| `cinny/capacitor.config.json` | Capacitor app id, web dir |
| `src-tauri/src/main.rs` | Tauri Android entry |
| `cinny/src/app/utils/tauri.ts` | Platform detection, notifications |
| `cinny/src/app/utils/backgroundSync.ts` | UnifiedPush registration |
| `cinny/src/app/utils/androidShare.ts` | Share target handling |
| `cinny/src/app/pages/client/ClientNonUIFeatures.tsx` | Share listener wiring |
## Data model
Same as background notifications (`paarrot.unifiedpush.*` localStorage).
Share payloads: ephemeral in-memory until user picks room.
## Dependencies
- Capacitor plugins / Tauri mobile toolchain
- UnifiedPush distributor (ntfy, etc.)
- Android notification icon resources
## Integration points
- **background-notifications**: primary overlap
- **Room input drafts**: `roomIdToUploadItemsAtomFamily` for shared files
- **NOT Electron**: `initPaarrotAPI` skips on mobile webview
## Testing
### Manual
1. Build Android APK/AAB per project scripts.
2. Share image from Gallery → Paarrot → pick room → verify upload draft.
3. Background app; trigger mention; verify UP notification.
4. Tap notification → opens correct room.
### Automated
- None on device CI in repo (check Gitea workflow)
## Known issues & gotchas
- `ANDROID_NOTIFICATION_SMALL_ICON = 'ic_stat_paarrot'` must exist in Android res
- UnifiedPush without distributor shows user-facing error string
- Feature parity gap vs desktop (plugins, Stream Deck, local API)
- Test `isTauri` vs `isCapacitorNative` — both may be true in some builds
## Future work
- iOS Capacitor target
- Play Store release pipeline docs
- Mobile plugin sandbox policy
## Related docs
- [background-notifications HANDOFF](../background-notifications/HANDOFF.md)
## Add your extra things here
- `listenForAndroidShares`, `getPendingAndroidShare`, `materializeSharedFile` in `androidShare.ts`
- Capacitor app name in `capacitor.config.json` — verify `appId` matches push config

View File

@@ -0,0 +1,97 @@
# Authentication — handoff
## Summary
Auth covers login, registration, password reset, SSO, and server selection before the Matrix client starts. Sessions are persisted locally and restored on app launch via `ClientRoot`.
## User-facing behavior
- Pick homeserver (ServerPicker) or use default from config
- Login: password, token, SSO redirect
- Register: password-based with optional email flows
- Reset password flow
- Auth layout shows Paarrot branding
- Device display name defaults to `Paarrot`
## Architecture
```
Unauthenticated routes (AuthLayout)
→ Login / Register / ResetPassword / SSOLogin
→ session written to localStorage (sessions.ts)
ClientRoot (authenticated)
→ initClient(session) → startClient(mx)
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/pages/auth/login/Login.tsx` | Login hub |
| `cinny/src/app/pages/auth/login/PasswordLoginForm.tsx` | Password login |
| `cinny/src/app/pages/auth/login/TokenLogin.tsx` | Access token login |
| `cinny/src/app/pages/auth/SSOLogin.tsx` | SSO redirect |
| `cinny/src/app/pages/auth/register/Register.tsx` | Registration |
| `cinny/src/app/pages/auth/register/PasswordRegisterForm.tsx` | Password register |
| `cinny/src/app/pages/auth/reset-password/ResetPassword.tsx` | Password reset |
| `cinny/src/app/pages/auth/ServerPicker.tsx` | Homeserver selection |
| `cinny/src/app/pages/auth/AuthLayout.tsx` | Auth chrome + logo |
| `cinny/src/app/pages/auth/login/loginUtil.ts` | Login helpers |
| `cinny/src/app/pages/auth/register/registerUtil.ts` | Register helpers |
| `cinny/src/app/state/sessions.ts` | Session storage |
| `cinny/src/client/initMatrix.ts` | Client init from session |
| `cinny/src/app/pages/afterLoginRedirectPath.ts` | Post-login redirect |
## Data model
| Key | Storage | Content |
|-----|---------|---------|
| Session store | localStorage | access token, user id, device id, hs url |
| `setAfterLoginRedirectPath` | sessionStorage/memory | deep link target after login |
## Dependencies
- `matrix-js-sdk` login/register APIs
- Homeserver `.well-known` / login flows (`useParsedLoginFlows`, `useAuthFlows`)
- OIDC/SSO via redirect URLs
## Integration points
- **Router**: unauthenticated vs `ClientRoot` split
- **matrix-client**: `initClient` consumes session
- **Device verification**: post-login verification UI in router
## Testing
### Manual
1. Login with password on test homeserver.
2. Token login with valid access token.
3. SSO flow if server supports it.
4. Register new account; verify email if required.
5. Logout clears session and returns to login.
### Automated
- None
## Known issues & gotchas
- `initial_device_display_name: 'Paarrot'` hardcoded in login/register forms
- Session logout from server triggers full localStorage clear + reload
- Server picker URL must match actual homeserver capabilities
## Future work
- Multi-account without full reload (see account-switcher)
- Passkey / WebAuthn if upstream adds support
## Related docs
- [matrix-client HANDOFF](../matrix-client/HANDOFF.md)
- [e2e-and-devices HANDOFF](../e2e-and-devices/HANDOFF.md)
## Add your extra things here
- Auth routes: `LOGIN_PATH`, `REGISTER_PATH`, `RESET_PASSWORD_PATH` in `paths.ts`
- `UnAuthRouteThemeManager` vs `AuthRouteThemeManager` in ThemeManager.tsx

View File

@@ -0,0 +1,92 @@
# Avatar metadata — handoff
## Summary
Paarrot embeds profile accent metadata **inside image files** (PNG, JPEG, WebP, GIF) so avatar colors, banners, gradients, and border colors travel with the MXC upload. Other Paarrot clients read this metadata to theme profile UI without extra Matrix account data fields.
## User-facing behavior
- **Settings → Account → Profile**: pick accent color (and related styling) applied to avatar image on save
- Profile viewers extract color/banner from avatar HTTP URL for themed headers
- Works across image formats users commonly upload
## Architecture
```
Profile settings UI
→ embedMetadata / embedColor (imageMetadata.ts)
→ format-specific: pngMetadata, jpegMetadata, webpMetadata, gifMetadata
→ upload to Matrix
Other clients / Profile.tsx
→ extractMetadataFromImage(fetch avatar)
→ apply CSS variables / banner URL
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/utils/imageMetadata.ts` | Unified embed/extract API |
| `cinny/src/app/utils/pngMetadata.ts` | PNG tEXt chunks |
| `cinny/src/app/utils/jpegMetadata.ts` | JPEG XMP APP1 |
| `cinny/src/app/utils/webpMetadata.ts` | WebP XMP |
| `cinny/src/app/utils/gifMetadata.ts` | GIF application extension |
| `cinny/src/app/features/settings/account/Profile.tsx` | Settings UI + upload pipeline |
## Data model
Metadata keys (embedded in binary image, not Matrix):
| Key | Format | Notes |
|-----|--------|-------|
| `paarrot:color` | All | Accent color hex |
| `paarrot:banner` | PNG (+ unified API) | Banner image URL |
| `paarrot:borderColor` | PNG | Border accent |
| `paarrot:gradient` | PNG | Gradient spec |
XMP namespace: `http://paarrot.app/ns/1.0/`
## Dependencies
- Browser `ArrayBuffer` / `Uint8Array` parsing
- No server-side processing — client-only before upload
## Integration points
- **Profile display**: user profile cards read metadata from avatar URL
- **Image upload flow**: must re-encode image with metadata before `uploadContent`
## Testing
### Manual
1. Set profile color in settings; save avatar.
2. Download avatar MXC; verify metadata with hex dump or Paarrot on second account.
3. Repeat for PNG, JPEG, WebP, GIF uploads.
4. Remove color; verify strip functions work.
### Automated
- None — good candidate for unit tests on chunk parsers
## Known issues & gotchas
- Metadata stripped if user uploads through non-Paarrot client
- Large images: re-encoding memory cost on mobile
- Console logging in `pngMetadata.ts` (`extractColorFromPNG`) — noisy in prod
- Federation: only Paarrot-aware clients interpret metadata
## Future work
- Unit tests per format parser
- Fallback to account data if metadata missing
- Document binary spec in standalone spec doc
## Related docs
- None
## Add your extra things here
- Entry points: `extractPaarrotMetadata`, `embedPaarrotMetadata`, `embedPaarrotColor` in `imageMetadata.ts`
- Profile comment: "Stored in avatar image metadata, visible to other Paarrot users"

View File

@@ -0,0 +1,93 @@
# Background notifications — handoff
## Summary
Paarrot keeps users notified of Matrix activity when the app is in the background: desktop tray/favicon badges, system notifications (Electron/Tauri/Android), and Android **UnifiedPush** for reliable mobile push without Google FCM dependency.
## User-facing behavior
- Unread counts update favicon (Paarrot / unread / highlight variants)
- System notifications for mentions/DMs per notification settings
- Android: requires UnifiedPush distributor (e.g. ntfy); user prompted if missing
- Foreground/background state affects sync strategy
## Architecture
```
ClientNonUIFeatures.tsx
→ FaviconUpdater (roomToUnread atom)
→ notification listeners (tauri.ts sendNotification)
→ initPaarrotAPI + background sync lifecycle
backgroundSync.ts
→ UnifiedPush registration, pusher setup
→ setAppForegroundState / startBackgroundSync / stopBackgroundSync
tauri.ts / electron main
→ OS notification APIs, Android small icon ic_stat_paarrot
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/pages/client/ClientNonUIFeatures.tsx` | Favicon, notifications, API init, background sync hooks |
| `cinny/src/app/utils/backgroundSync.ts` | Pushers, UnifiedPush, foreground state |
| `cinny/src/app/utils/tauri.ts` | Tauri/Capacitor notification helpers |
| `cinny/src/app/features/settings/notifications/SystemNotification.tsx` | Permission UX |
| `electron/main.js` | Tray, notification IPC (if applicable) |
| `src-tauri/src/main.rs` | Tauri Android shell |
## Data model
| Key | Storage | Purpose |
|-----|---------|---------|
| `paarrot.unifiedpush.*` | localStorage prefix | UP endpoint state |
| `com.paarrot.app.android` | Pusher app id base | Android pushers |
| Matrix pusher | Homeserver | HTTP push gateway |
## Dependencies
- UnifiedPush distributor app on Android
- Notification permission (browser/OS)
- Homeserver pusher support
- `matrix-js-sdk` sync
## Integration points
- **Settings → Notifications**: rules + system permission
- **DirectTab / sidebar**: custom Paarrot SVG favicon states
- **Deep links**: notification tap → `setupNotificationTapListener`
## Testing
### Manual
1. Desktop: background app; send mention; verify system notification.
2. Verify favicon switches unread/highlight with counts.
3. Android: install ntfy; verify push when app killed.
4. Toggle notification settings; verify respect mute rules.
### Automated
- None
## Known issues & gotchas
- UnifiedPush user education string in `backgroundSync.ts` if no distributor
- Multiple runtimes: `isTauri`, `isElectron`, `isCapacitorNative` branch logic
- Pusher registration races on fast login/logout
- Android small icon: `ic_stat_paarrot` must exist in Android resources
## Future work
- iOS push strategy
- Notification action buttons (reply, mark read)
- Per-room notification channels on Android
## Related docs
- Matrix push gateway docs (upstream)
## Add your extra things here
- Constants: `PUSHER_APP_ID_BASE`, `PUSHER_STORAGE_PREFIX` in `backgroundSync.ts`
- Logo assets: `paarrot.svg`, `paarrot-unread.svg`, `paarrot-highlight.svg` in `public/res/svg/`

View File

@@ -0,0 +1,95 @@
# Client chrome — handoff
## Summary
App shell UI outside core chat: custom title bar (Electron), splash screen, welcome page, account switcher, update notifications, sync status, spec version warnings, and feature capability checks.
## User-facing behavior
- **Title bar**: drag region, window controls, room title, account menu (desktop)
- **Splash**: loading spinner during client init
- **Welcome page**: first-run / empty state hints
- **Account switcher**: switch between logged-in accounts (if multi-session)
- **Update notification**: new version available (electron-updater)
- **Feature check**: block unsupported browsers
- **Spec versions**: warn on old homeserver
## Architecture
```
ClientRoot → TitleBar + AccountSwitcher + children
SplashScreen during matrix init
WelcomePage on empty states
UpdateNotification component
FeatureCheck wraps App
SpecVersions in ClientRoot
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/components/title-bar/TitleBar.tsx` | Desktop title bar |
| `cinny/src/app/components/splash-screen/SplashScreen.tsx` | Splash |
| `cinny/src/app/pages/client/WelcomePage.tsx` | Welcome / download links |
| `cinny/src/app/components/account-switcher/AccountSwitcher.tsx` | Multi-account |
| `cinny/src/app/components/update-notification/UpdateNotification.tsx` | Auto-update UI |
| `cinny/src/app/pages/FeatureCheck.tsx` | Browser capability gate |
| `cinny/src/app/pages/client/SpecVersions.tsx` | HS version warning |
| `cinny/src/app/pages/client/SyncStatus.tsx` | Sync indicator |
| `cinny/src/app/pages/ConfigConfig.tsx` | Config load error UI |
| `cinny/src/app/components/ClientConfigLoader.tsx` | Load config.json |
## Data model
| Key | Storage |
|-----|---------|
| Sessions | multi-account in sessions.ts |
| electron-store | window bounds (main process) |
## Dependencies
- Electron IPC for window controls
- `electron-updater` for updates
- `clientConfig` JSON (homeserver default, hash router, etc.)
## Integration points
- **electron-shell**: title bar IPC
- **matrix-client**: splash until sync ready
- **authentication**: account switcher sessions
## Testing
### Manual
1. Desktop: minimize/maximize/close via title bar.
2. Slow network: splash shows until ready.
3. Trigger update check in dev (if configured).
4. Unsupported browser: FeatureCheck message.
### Automated
- None
## Known issues & gotchas
- Title bar only on Electron — web uses browser chrome
- Welcome page links point to GitHub releases — verify URLs match your distribution (Gitea vs GitHub)
- `FeatureCheck` may block older WebViews on Android
## Future work
- Unified status bar (sync + update + call status)
- macOS traffic lights styling
## Related docs
- [electron-shell HANDOFF](../electron-shell/HANDOFF.md)
- [matrix-client HANDOFF](../matrix-client/HANDOFF.md)
- [authentication HANDOFF](../authentication/HANDOFF.md)
## Add your extra things here
- Title bar shows `Paarrot` prefix + dynamic room/space title suffix
- `ClientConfigLoader` reads public config — document config.json schema in client config handoff if split later

View File

@@ -0,0 +1,82 @@
# Common settings — handoff
## Summary
Shared setting pages used by both room and space settings overlays: general (profile, join rules, encryption, embed filters), members, permissions editors, emojis/stickers, and developer tools (raw state events).
## User-facing behavior
- Same UI whether editing a room or space context
- Members list with kick/ban/invite
- Power level editor
- Room emoji packs
- Developer tools: send custom state events
## Architecture
```
common-settings/
general/ — RoomProfile, RoomJoinRules, RoomEncryption, EmbedFilters
members/ — Members.tsx
permissions/ — PowersEditor, PermissionGroups
emojis-stickers/
developer-tools/ — StateEventEditor, SendRoomEvent
Imported by room-settings and space-settings shells
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/common-settings/general/RoomProfile.tsx` | Name/topic/avatar |
| `cinny/src/app/features/common-settings/general/RoomJoinRules.tsx` | Join rules |
| `cinny/src/app/features/common-settings/general/RoomEncryption.tsx` | E2E toggle |
| `cinny/src/app/features/common-settings/general/EmbedFilters.tsx` | Room-wide embed filters |
| `cinny/src/app/features/common-settings/members/Members.tsx` | Member management |
| `cinny/src/app/features/common-settings/permissions/PowersEditor.tsx` | Power levels |
| `cinny/src/app/features/common-settings/developer-tools/` | Dev event tools |
## Data model
Standard Matrix room/space state events. Embed filters: `im.paarrot.room.embed_filters`.
## Dependencies
- Permission hooks, member list hooks
## Integration points
- **room-settings** / **space-settings**: parent shells
- **embed-filters**: room-wide pattern UI
- **emoji-stickers**: room pack management
## Testing
### Manual
1. Open members from room settings; invite user.
2. Set embed filter pattern as admin.
3. Developer tools: send test state event (staging only).
### Automated
- None
## Known issues & gotchas
- `DevelopTools` naming typo inherited from upstream
- Encryption enable is irreversible — UI must warn
## Future work
- Extract embed filters to own settings section
## Related docs
- [room-settings HANDOFF](../room-settings/HANDOFF.md)
- [space-settings HANDOFF](../space-settings/HANDOFF.md)
- [embed-filters HANDOFF](../embed-filters/HANDOFF.md)
## Add your extra things here
- `PermissionGroups.tsx` — grouped permission UI

View File

@@ -0,0 +1,88 @@
# Create flows — handoff
## Summary
UI for creating rooms, spaces, direct chats, and adding existing rooms to spaces. Includes modal renderers mounted at client root and join/create page routes.
## User-facing behavior
- Create room modal (optionally as sub-room of parent)
- Create space modal
- Create DM / group DM
- Add existing room to space
- Create tab in sidebar for quick actions
## Architecture
```
CreateRoomModalRenderer → CreateRoomModal.tsx
CreateSpaceModalRenderer → CreateSpaceModal.tsx
create-chat/ — DM creation
add-existing/ — pick room to add to space
pages/client/create/Create.tsx — create hub
pages/client/direct/DirectCreate.tsx
State: createRoomModal.ts, createSpaceModal.ts
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/create-room/CreateRoomModal.tsx` | Room create + sub-room parent update |
| `cinny/src/app/features/create-space/CreateSpaceModal.tsx` | Space create |
| `cinny/src/app/features/create-chat/` | DM flows |
| `cinny/src/app/features/add-existing/AddExisting.tsx` | Add to space |
| `cinny/src/app/features/create-room/CreateRoom.tsx` | Standalone create page |
| `cinny/src/app/features/create-space/CreateSpace.tsx` | Standalone space create |
| `cinny/src/app/state/createRoomModal.ts` | Modal state + parent room id |
| `cinny/src/app/state/createSpaceModal.ts` | Space modal state |
| `cinny/src/app/pages/client/create/Create.tsx` | Create hub route |
| `cinny/src/app/pages/client/home/CreateRoom.tsx` | Home create entry |
## Data model
Creates standard Matrix rooms/spaces. Sub-room creation also updates `im.paarrot.sub_rooms` on parent.
## Dependencies
- matrix-js-sdk `createRoom` APIs
- Space parent/child events
## Integration points
- **sub-rooms**: parent id in create room modal
- **room-nav**: opens create sub-room modal
- **routing-and-navigation**: Create routes, modal renderers
## Testing
### Manual
1. Create public room; verify join.
2. Create sub-room under parent; appears in home tree.
3. Create space; add existing room.
4. Start DM from DirectCreate.
### Automated
- None
## Known issues & gotchas
- Sub-room failure after room created leaves orphan — see sub-rooms handoff
- `CreateRoomKindSelector` for room type variants
- Forum room type may need specific create options
## Future work
- Create forum space template
- Batch add rooms to space
## Related docs
- [sub-rooms HANDOFF](../sub-rooms/HANDOFF.md)
- [routing-and-navigation HANDOFF](../routing-and-navigation/HANDOFF.md)
## Add your extra things here
- `useOpenCreateSubRoomModal` hook opens modal with `parentRoomId`

View File

@@ -0,0 +1,87 @@
# Custom emoji packs — handoff
## Summary
Built-in (non-plugin) custom emoji system: read image packs from room state, resolve shortcodes to images in messages and autocomplete. Distinct from user-installed JS plugins but shares pack format concepts.
## User-facing behavior
- Room/space emoji packs in settings
- `:shortcode:` in composer resolves to pack image
- Pack viewer for browsing stickers/emojis
- Integrates with `im.ponies.room_emotes` and pack MXC structure
## Architecture
```
plugins/custom-emoji/
PackMetaReader, PackImageReader, PackImagesReader
ImagePack.ts, PackAddress.ts
emoji plugin (plugins/emoji.ts) — render shortcodes
settings emojis-stickers + common-settings emojis
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/plugins/custom-emoji/PackMetaReader.ts` | Read pack metadata |
| `cinny/src/app/plugins/custom-emoji/PackImageReader.ts` | Single image |
| `cinny/src/app/plugins/custom-emoji/PackImagesReader.ts` | Bulk images |
| `cinny/src/app/plugins/custom-emoji/ImagePack.ts` | Pack model |
| `cinny/src/app/plugins/custom-emoji/types.ts` | Types |
| `cinny/src/app/plugins/emoji.ts` | Shortcode rendering |
| `cinny/src/app/plugins/recent-emoji.ts` | Recent emoji helper |
| `cinny/src/app/components/image-pack-view/ImagePackView.tsx` | Pack browser UI |
| `cinny/src/app/hooks/useImagePackRooms.ts` | Rooms with packs |
## Data model
| Event | Purpose |
|-------|---------|
| `im.ponies.room_emotes` | Room emoji pack definition |
| Image pack account data | User/global packs (see emoji-stickers) |
## Dependencies
- MXC media URLs
- `useMediaAuthentication` for protected media
## Integration points
- **emoji-stickers**: settings UI for packs + Telegram import
- **message-rendering**: emoji.ts in parse pipeline
- **editor-and-composer**: EmoticonAutocomplete
## Testing
### Manual
1. Add emoji pack to room.
2. Type `:shortcode:` in composer; image inserts.
3. View pack in ImagePackView.
4. Encrypted room pack visibility.
### Automated
- None
## Known issues & gotchas
- Pack address parsing in `PackAddress.ts` — MXC format sensitive
- Large packs slow autocomplete — consider lazy load
- Telegram import creates separate pack ids (`telegram_*`)
## Future work
- Pack CDN cache
- Animated emoji support audit
## Related docs
- [emoji-stickers HANDOFF](../emoji-stickers/HANDOFF.md)
- [message-rendering HANDOFF](../message-rendering/HANDOFF.md)
## Add your extra things here
- `StateEvent.PoniesRoomEmotes = 'im.ponies.room_emotes'` in room.ts

View File

@@ -0,0 +1,75 @@
# Developer tools — handoff
## Summary
Power-user tools for debugging Matrix state: view/edit account data, send custom room events, and inspect raw JSON. Available in user settings and room/space common-settings.
## User-facing behavior
- **Settings → Developer tools**: account data viewer
- **Room settings → Developer tools**: send state events, room account data
- Raw JSON editors with validation warnings
## Architecture
```
settings/developer-tools/
DevelopTools.tsx, AccountData.tsx
common-settings/developer-tools/
DevelopTools.tsx, StateEventEditor.tsx, SendRoomEvent.tsx
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/settings/developer-tools/DevelopTools.tsx` | User-level dev tools |
| `cinny/src/app/features/settings/developer-tools/AccountData.tsx` | Account data viewer |
| `cinny/src/app/features/common-settings/developer-tools/DevelopTools.tsx` | Room dev tools entry |
| `cinny/src/app/features/common-settings/developer-tools/StateEventEditor.tsx` | Edit state events |
| `cinny/src/app/features/common-settings/developer-tools/SendRoomEvent.tsx` | Send events |
## Data model
Direct read/write to Matrix account data and state events — **production danger**.
## Dependencies
- matrix-js-sdk low-level APIs
## Integration points
- **user-settings**: developer tab
- **common-settings**: per-room tools
## Testing
### Manual
1. View account data keys on staging account.
2. Send harmless custom state event in test room.
3. Verify permission denied without power level.
### Automated
- None
## Known issues & gotchas
- No guard rails — can break rooms with bad state events
- Should be hidden or disabled in production builds for normal users (currently settings-gated only)
- Typo `DevelopTools` vs `DeveloperTools` in filenames
## Future work
- Feature flag via client config
- Event diff viewer
## Related docs
- [user-settings HANDOFF](../user-settings/HANDOFF.md)
- [common-settings HANDOFF](../common-settings/HANDOFF.md)
## Add your extra things here
- Only use on test homeservers — document internal MSC event types here when experimenting

View File

@@ -0,0 +1,83 @@
# Direct messages — handoff
## Summary
The DMs panel lists direct chats and provides flows to start new conversations. Uses `m.direct` account data to identify DM rooms.
## User-facing behavior
- Sidebar Direct tab shows DM rooms sorted
- Create new DM / group chat
- DM room opens in direct route layout
- Paarrot logo badge states on tab (shared with home tab styling)
## Architecture
```
client/direct/
Direct.tsx — DM room list page
DirectCreate.tsx — start new chat
useDirectRooms.ts — room list from m.direct
RoomProvider.tsx — route-level room context
mDirectAtom — direct room index
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/pages/client/direct/Direct.tsx` | DM list |
| `cinny/src/app/pages/client/direct/DirectCreate.tsx` | Create DM |
| `cinny/src/app/pages/client/direct/useDirectRooms.ts` | List logic |
| `cinny/src/app/pages/client/sidebar/DirectTab.tsx` | Sidebar tab |
| `cinny/src/app/state/mDirectList.ts` | `mDirectAtom` |
| `cinny/src/app/hooks/useDirectUsers.ts` | DM user helpers |
## Data model
| Key | Scope |
|-----|-------|
| `m.direct` | User account data — map userId → roomIds[] |
## Dependencies
- matrix-js-sdk DM APIs
- `is_direct` on member events
## Integration points
- **home-and-sidebar**: DirectTab
- **routing-and-navigation**: `DIRECT_PATH`
- **create-flows**: create-chat feature
- **room-timeline**: same Room component for DM rooms
## Testing
### Manual
1. Start DM with user; appears in direct list.
2. Group DM with multiple users.
3. Unread badge on Direct tab.
### Automated
- None
## Known issues & gotchas
- `m.direct` can be stale — client may repair on sync
- DM vs private room distinction relies on account data
## Future work
- Pin favorite DMs
- DM search filter
## Related docs
- [home-and-sidebar HANDOFF](../home-and-sidebar/HANDOFF.md)
- [create-flows HANDOFF](../create-flows/HANDOFF.md)
## Add your extra things here
- `getDirectRoomPath` in pathUtils for DM room URLs

View File

@@ -0,0 +1,92 @@
# E2E encryption & devices — handoff
## Summary
End-to-end encryption support via matrix-js-sdk crypto store: device verification, cross-signing, secret storage, key backup/export, and session management. Critical for secure DM and encrypted rooms.
## User-facing behavior
- Verify new logins / other sessions
- Export room keys backup (`paarrot-keys.txt`)
- Restore backup after verification
- View and rename devices; logout remote sessions
- Unverified sessions tab in sidebar
## Architecture
```
secretStorageKeys.ts + cryptoCallbacks
Devices.tsx, Verification.tsx, LocalBackup.tsx
DeviceVerification.tsx — incoming verification
BackupRestore.tsx — auto restore on verify
useSecretStorage, useKeyBackup, useVerificationRequest hooks
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/client/secretStorageKeys.ts` | SSSS key handling |
| `cinny/src/app/features/settings/devices/Devices.tsx` | Device list |
| `cinny/src/app/features/settings/devices/Verification.tsx` | Verification UI |
| `cinny/src/app/features/settings/devices/LocalBackup.tsx` | Key export |
| `cinny/src/app/features/settings/devices/OtherDevices.tsx` | Remote sessions |
| `cinny/src/app/components/DeviceVerification.tsx` | Incoming verify modal |
| `cinny/src/app/components/BackupRestore.tsx` | Auto restore |
| `cinny/src/app/components/SecretStorage.tsx` | Passphrase setup |
| `cinny/src/app/hooks/useSecretStorage.ts` | Secret storage API |
| `cinny/src/app/hooks/useKeyBackup.ts` | Backup API |
| `cinny/src/app/pages/client/sidebar/UnverifiedTab.tsx` | Unverified badge |
## Data model
| Store | Purpose |
|-------|---------|
| IndexedDB crypto | Olm account, Megolm sessions |
| SSSS | Cross-signing keys, backup key |
| Matrix account data | `m.cross_signing.*`, `m.secret_storage.*` |
## Dependencies
- matrix-js-sdk crypto module
- Secure random / Web Crypto
## Integration points
- **matrix-client**: crypto store init in initMatrix
- **authentication**: first login bootstrap
- **user-settings**: devices tab
## Testing
### Manual
1. Login second device; verify via emoji/SAS from first.
2. Export keys backup; file downloads.
3. Encrypted room: send message; decrypt on other device.
4. Logout remote session from devices list.
### Automated
- None
## Known issues & gotchas
- `handlingKeyConflict` in initMatrix on duplicate device keys
- Logout clears crypto store — must re-verify
- Backup passphrase loss = message history loss
## Future work
- Dehydrated devices support
- Better recovery key UX
## Related docs
- [matrix-client HANDOFF](../matrix-client/HANDOFF.md)
- [user-settings HANDOFF](../user-settings/HANDOFF.md)
## Add your extra things here
- Local backup filename: `paarrot-keys.txt` in LocalBackup.tsx
- `ReceiveSelfDeviceVerification` route component in Router.tsx

View File

@@ -0,0 +1,92 @@
# Editor & composer — handoff
## Summary
The message composer is a **Slate**-based rich text editor with autocomplete for users, rooms, and emoticons. Converts between editor state and Matrix HTML/plaintext. Supports toolbar formatting, intents, and file paste/drop integration.
## User-facing behavior
- Type messages with bold/italic/code etc. (toolbar)
- `@user` and `#room` mentions with autocomplete popup
- `:emoji` emoticon autocomplete
- Paste images/files into composer
- Draft persistence per room
## Architecture
```
Editor.tsx (Slate)
→ Elements.tsx, Toolbar.tsx
→ autocomplete/ (User, Room, Emoticon)
→ input.ts / output.ts — Matrix format conversion
RoomInput.tsx wires Editor to send pipeline + uploads
text-area plugin (legacy/plain areas in some UI)
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/components/editor/Editor.tsx` | Main Slate editor |
| `cinny/src/app/components/editor/Toolbar.tsx` | Formatting toolbar |
| `cinny/src/app/components/editor/input.ts` | Parse into editor |
| `cinny/src/app/components/editor/output.ts` | Serialize to Matrix |
| `cinny/src/app/components/editor/autocomplete/` | Mention/emoji menus |
| `cinny/src/app/features/room/RoomInput.tsx` | Room composer wrapper |
| `cinny/src/app/plugins/text-area/` | Plain text area utilities |
| `cinny/src/app/hooks/useFilePasteHandler.ts` | Paste files |
| `cinny/src/app/hooks/useFileDrop.ts` | Drag-drop files |
| `cinny/src/app/state/room/roomInputDrafts.ts` | Draft persistence |
## Data model
| Key | Storage |
|-----|---------|
| `roomIdToMsgDraftAtomFamily` | localStorage per room |
| `roomIdToUploadItemsAtomFamily` | pending uploads |
## Dependencies
- `slate`, `slate-react`
- `folds` UI primitives
## Integration points
- **room-timeline**: send on submit
- **emoji-stickers**: emoticon autocomplete + custom packs
- **plugins**: `text-composer-toolbar`, `composer-actions` slots
- **media-and-url-preview**: upload before send
## Testing
### Manual
1. Mention user and room; verify pill rendering in sent message.
2. Format bold/italic; verify HTML in `formatted_body`.
3. Reload mid-compose; draft restores.
4. Paste image; upload card appears.
### Automated
- `Editor.preview.tsx` exists for dev preview
## Known issues & gotchas
- `useCompositionEndTracking` in App.tsx for IME composition
- Slate types in `slate.d.ts` — keep custom elements typed
- Plain text vs HTML fallback for clients without HTML support
## Future work
- WYSIWYG markdown mode
- Draft sync across devices via account data
## Related docs
- [room-timeline HANDOFF](../room-timeline/HANDOFF.md)
- [emoji-stickers HANDOFF](../emoji-stickers/HANDOFF.md)
## Add your extra things here
- `plainToEditorInput`, `toPlainText` helpers exported from editor index
- Forum has separate composers: `ForumReplyComposer`, `ForumChatComposer`

View File

@@ -0,0 +1,96 @@
# Electron desktop shell — handoff
## Summary
The desktop app wraps the Cinny/Paarrot web UI in Electron: custom title bar, system tray, deep links (`paarrot://`), auto-updates, plugin filesystem access, screen capture for calls, and the local HTTP API server.
## User-facing behavior
- Frameless/custom **title bar** with window controls (Windows/Linux)
- Minimize to **tray**; close may minimize instead of quit (platform config)
- `paarrot://` URL protocol for room links
- Auto-update checks (electron-updater)
- Plugins read/write under `%APPDATA%\paarrot\`
## Architecture
```
electron/main.js (main process)
→ BrowserWindow → Vite dev server or built cinny dist
→ IPC: plugins, screen capture, clipboard, updates
→ PaarrotAPIServer
→ Tray + protocol handler paarrot://
preload / contextBridge → window.electron in renderer
```
## Key files
| Path | Role |
|------|------|
| `electron/main.js` | Main process entry |
| `electron/api-server.js` | Local HTTP API |
| `electron/preload.js` (if exists) | IPC bridge |
| `electron-builder.json5` | Packaging config |
| `package.json` (root) | `npm run dev`, build scripts |
| `dev-app-update.yml` | Update channel config |
| `cinny/src/app/components/title-bar/TitleBar.tsx` | Custom title bar UI |
| `build-resources/` | Linux after-install, icons |
## Data model
| Store | Tool | Contents |
|-------|------|----------|
| `electron-store` | main.js | Window bounds, user prefs |
| UserData | Electron | plugins, caches |
Protocol: `PROTOCOL_SCHEME = 'paarrot'` in main.js
## Dependencies
- `electron`, `electron-updater`, `electron-store`
- `electron-builder` for CI releases
- Vite dev server port **38347**; static server **44548** in dev
## Integration points
- **local-api**: started with main window
- **Plugins**: filesystem paths under `paarrot` app name
- **Voice calls**: `desktopCapturer` for screen share
- **Deep links**: pending deep link queue until client ready
## Testing
### Manual
1. `npm run dev` — window loads, title bar draggable.
2. Register `paarrot://` link; opens correct room.
3. Tray icon present; restore from tray.
4. Build installer; auto-update check (staging).
5. Install plugin to AppData path; loader finds it.
### Automated
- CI: `.gitea/workflows/build.yml`
## Known issues & gotchas
- `isDev` detection: `NODE_ENV` or `!app.isPackaged`
- Protocol registration differs Windows vs Linux (`after-install.sh`)
- Single instance lock for deep links — see `pendingDeepLink` in main.js
- Title bar hit-testing CSS sensitive on Windows 11
## Future work
- macOS menu bar parity
- Code signing documentation for Windows
- Consolidate magic ports into shared config
## Related docs
- [local-api HANDOFF](../local-api/HANDOFF.md)
- Root [README.md](../../../../README.md) — build instructions
## Add your extra things here
- Dev URLs: `VITE_DEV_SERVER = http://localhost:38347`, `PORT = 44548`
- App userData folder name: `paarrot` (matches plugin paths in README)

View File

@@ -0,0 +1,84 @@
# Embed filters — handoff
## Summary
Embed filters let users and room admins suppress URL preview/embeds for matching links. **Personal** filters live in per-room account data; **room-wide** filters are a room state event admins control. Patterns are regex strings tested against URLs before rendering embeds in the timeline and threads.
## User-facing behavior
- Per-user: configure patterns for a room (settings or thread UI — see consumers)
- Room admins: room-wide disabled patterns via `im.paarrot.room.embed_filters`
- Matching URLs skip embed cards (Giphy, Open Graph, etc.)
## Architecture
```
useRoomEmbedFilters (personal, account data)
useRoomWideEmbedFilters (room state)
→ combineEmbedFilters()
→ ThreadView / RoomTimeline embed rendering
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/hooks/useRoomEmbedFilters.ts` | Hooks, types, combine logic |
| `cinny/src/types/matrix/room.ts` | `StateEvent.RoomEmbedFilters` |
| `cinny/src/app/features/room/ThreadView.tsx` | Consumer |
| `cinny/src/app/features/room/RoomTimeline.tsx` | Consumer |
| `cinny/src/app/features/common-settings/general/EmbedFilters.tsx` | Room-wide settings UI |
| `cinny/src/app/components/RenderMessageContent.tsx` | Embed render gating |
| `cinny/src/app/features/forum/useForumMessageRenderOptions.ts` | Forum post embed options |
## Data model
| Key | Scope | Content |
|-----|-------|---------|
| `im.paarrot.embed_filters` | Room account data (per user) | `{ disabledPatterns: string[] }` |
| `im.paarrot.room.embed_filters` | Room state | `{ disabledPatterns: string[] }` |
Patterns are JavaScript `RegExp` strings — invalid regex can throw; handle at input time.
## Dependencies
- `matrix-js-sdk` account data + state events
## Integration points
- **URL preview components**: `url-preview/` — embed gating happens before render
- **Thread view**: combines personal + room-wide filters
## Testing
### Manual
1. Add personal pattern blocking `example.com` in a room.
2. Post link; verify no embed.
3. As admin set room-wide pattern; verify all members affected.
4. Test thread view uses combined filters.
### Automated
- None
## Known issues & gotchas
- Two storage locations — document clearly for users (personal vs room-wide)
- Regex validation on save recommended — bad patterns break matching try/catch sites
- Account data type constant: `EMBED_FILTERS_ACCOUNT_DATA_TYPE` in hook file
## Future work
- UI in room settings for room-wide filters (if not complete)
- Import/export pattern lists
- Non-regex simple domain blocklist mode
## Related docs
- None dedicated
## Add your extra things here
- `combineEmbedFilters` merges personal + room-wide with dedup logic — read implementation before changing precedence
- `EmbedFiltersContent` interface in `useRoomEmbedFilters.ts`

View File

@@ -0,0 +1,92 @@
# Emojis & stickers — handoff
## Summary
Paarrot extends Cinny's emoji/sticker system with **usage tracking** (frequently used emojis), **Telegram sticker pack import**, custom emoji pack plugins, and configurable **emoji font** (System / Apple / Twemoji). Pack management lives under Settings and room/space settings.
## User-facing behavior
- **Settings → Emojis & stickers**: global packs, user packs, Telegram import
- Frequently-used emojis sorted by usage stats
- **Settings → General → Emoji style**: System, Apple, Twemoji fonts
- Telegram: bot token stored locally, import packs by name
- Room/space emoji packs via common-settings (inherited Cinny patterns)
## Architecture
```
useEmojiUsage → account data paarrot.favemojis
EmojiStyleFeature (ClientNonUIFeatures) → CSS --font-emoji
TelegramImport.tsx → telegramStickerImport.ts → Matrix image pack upload
custom-emoji plugin (plugins/custom-emoji/) → PackMetaReader
Settings / common-settings EmojisStickers pages
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/hooks/useEmojiUsage.ts` | Usage stats in account data |
| `cinny/src/app/utils/telegramStickerImport.ts` | Telegram Bot API import |
| `cinny/src/app/features/settings/emojis-stickers/TelegramImport.tsx` | Import UI |
| `cinny/src/app/features/settings/emojis-stickers/EmojisStickers.tsx` | Settings hub |
| `cinny/src/app/features/settings/emojis-stickers/UserPack.tsx` | User pack editor |
| `cinny/src/app/features/settings/emojis-stickers/GlobalPacks.tsx` | Global packs |
| `cinny/src/app/plugins/custom-emoji/` | Custom emoji pack reader |
| `cinny/src/app/pages/client/ClientNonUIFeatures.tsx` | `EmojiStyleFeature` |
| `cinny/src/app/state/settings.ts` | `EmojiStyle` enum |
## Data model
| Key | Scope | Content |
|-----|-------|---------|
| `paarrot.favemojis` | User account data | Emoji usage counts |
| `cinny_telegram_bot_token` | localStorage | Telegram bot token (TelegramImport) |
| `im.ponies.room_emotes` | Room state | Standard Matrix emoji packs |
## Dependencies
- Telegram Bot API (`api.telegram.org`)
- Twemoji / Apple Color Emoji fonts (bundled or system)
- `matrix-js-sdk` for image pack events
## Integration points
- **Emoji board / composer autocomplete**: reads usage ordering
- **Image pack viewer**: preview imported packs
- **Plugins**: custom-emoji pack format
## Testing
### Manual
1. Send messages with varied emoji; open picker — frequent ones rise.
2. Import Telegram pack with valid bot token; verify stickers in room.
3. Switch emoji style General setting; verify font change in messages.
4. Add room emoji pack in space settings.
### Automated
- None
## Known issues & gotchas
- Telegram token in **localStorage** — not encrypted; user-provided bot
- Telegram import uses network to Telegram servers — rate limits apply
- `PAARROT_EMOJI_USAGE_EVENT = 'paarrot.favemojis'` — must stay stable or migrate
- Pack id prefix `telegram_${packName}` for imported packs
## Future work
- Encrypt or secure-store Telegram token
- Usage stats sync across devices (account data already does)
- Sticker pack marketplace
## Related docs
- Cinny upstream emoji pack docs (if any)
## Add your extra things here
- Emoji style values: `EmojiStyle.System | Apple | Twemoji` in settings atom
- `useEmojiGroupLabels` for picker section headers

View File

@@ -0,0 +1,79 @@
# Explore — handoff
## Summary
Discover public rooms and featured communities on Matrix homeservers. Browse by server, search public room directory, and preview/join rooms.
## User-facing behavior
- Explore tab in sidebar
- Pick server / homeserver directory
- Featured rooms list
- Search public rooms on server
- Join room from preview
## Architecture
```
client/explore/
Explore.tsx — main explore page
Server.tsx — server-specific directory
Featured.tsx — featured rooms
PublicRooms via matrix directory API
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/pages/client/explore/Explore.tsx` | Explore hub |
| `cinny/src/app/pages/client/explore/Server.tsx` | Server directory |
| `cinny/src/app/pages/client/explore/Featured.tsx` | Featured list |
| `cinny/src/app/pages/client/sidebar/ExploreTab.tsx` | Sidebar entry |
| `cinny/src/app/hooks/useRoomDirectoryVisibility.ts` | Directory visibility |
## Data model
Public room directory from homeserver HTTP API (ephemeral).
## Dependencies
- Homeserver `/publicRooms` or sliding sync directory
- `matrix-js-sdk` room directory helpers
## Integration points
- **routing-and-navigation**: `EXPLORE_PATH`, `_SERVER_PATH`, `_FEATURED_PATH`
- **join-before-navigate**: may route here before join
## Testing
### Manual
1. Open explore; list loads for default server.
2. Switch server; directory refreshes.
3. Join public room from explore.
4. Server without public directory — error handling.
### Automated
- None
## Known issues & gotchas
- Public directory disabled on many private homeservers
- Featured rooms may be config-dependent (`clientConfig`)
## Future work
- Federated universal search
- Room preview with topic/avatar before join
## Related docs
- [routing-and-navigation HANDOFF](../routing-and-navigation/HANDOFF.md)
- [join-before-navigate HANDOFF](../join-before-navigate/HANDOFF.md)
## Add your extra things here
- `getExploreFeaturedPath` in pathUtils

View File

@@ -0,0 +1,105 @@
# Forum spaces — handoff
## Summary
Forum spaces provide a board-style UI for `m.forum` rooms and `forum_space` room kinds: topic sections, post feeds, thread detail, rich replies, filters, and dedicated composers. This is a large Paarrot-specific feature area separate from the card-based **Lobby** view.
## User-facing behavior
- Forum spaces open to a **feed** of posts (not the lobby card grid)
- Sections group topics (child rooms / subspaces)
- Create posts with title + rich body; threaded replies
- Board detail view, thread detail, topic search/filter bar
- Distinct composers: new post, reply, chat-style
## Architecture
```
ForumAwareSpaceLayout / ForumSpaceLayout
→ ForumFeedPage (feed sidebar + post list)
→ ForumBoardDetail / ForumThreadDetail
→ forumFeed.ts (aggregate posts from topic rooms)
→ forumTopicHelpers, forumRichText, topicSearch
Hooks: useForumBoard, useForumRoomLiveUpdates, useForumMessageReactions
```
Routing: `getSpaceDefaultPath` + `shouldShowForumLobby` in path utils / room utils.
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/forum/ForumFeedPage.tsx` | Main feed page |
| `cinny/src/app/features/forum/ForumSpaceLayout.tsx` | Layout shell |
| `cinny/src/app/features/forum/ForumAwareSpaceLayout.tsx` | Forum vs non-forum switch |
| `cinny/src/app/features/forum/forumFeed.ts` | Post aggregation, root detection |
| `cinny/src/app/features/forum/types.ts` | ForumTopic, ForumPost, etc. |
| `cinny/src/app/features/forum/ForumBoardView.tsx` | Board listing |
| `cinny/src/app/features/forum/ForumThreadDetail.tsx` | Thread view |
| `cinny/src/app/features/forum/ForumNewPostModal.tsx` | Create post |
| `cinny/src/app/features/forum/ForumReplyComposer.tsx` | Replies |
| `cinny/src/app/features/forum/useForumBoard.ts` | Board data hook |
| `cinny/src/app/features/forum/useForumRoomLiveUpdates.ts` | Live timeline sync |
| `cinny/src/app/pages/pathUtils.ts` | Forum routing |
## Data model
| Signal | Purpose |
|--------|---------|
| `m.room.create` `type: m.forum` | Matrix forum room type |
| `im.paarrot.room.kind: forum_space` | Paarrot forum classification |
| `com.matrixsso.title` on messages | Forum post title (legacy/custom) |
| `formatted_body` with `<h1>` | Root post detection |
| `m.relates_to` threads | Replies and edits |
| `im.paarrot.sub_rooms` | Topic room hierarchy (see sub-rooms) |
## Dependencies
- `matrix-js-sdk` relations, threads, space hierarchy
- Rich text / Slate editor integration for composers
- Embed filters via `useForumMessageRenderOptions.ts`
## Integration points
- **lobby-forums**: routing chooses forum feed vs lobby
- **sub-rooms**: topic rooms may be sub-rooms
- **embed-filters**: forum message render options
- **local-api**: `isSpaceLikeRoom` treats forum as space-like
## Testing
### Manual
1. Open a `m.forum` space; verify feed UI loads.
2. Create post with title; verify appears in feed and topic room.
3. Reply in thread; verify counts and detail view.
4. Filter/search topics; verify results.
5. Compare behavior with `forum_space` room kind.
### Automated
- None
## Known issues & gotchas
- Root post detection uses **two** heuristics: `com.matrixsso.title` and `<h1>` in formatted_body
- Large `forumFeed.ts` — main complexity hotspot
- Forum vs lobby split scattered across path utils and layout components
- Edit/replace relations need careful handling (`isAnnotationRoomMessage`)
## Future work
- Standardize on one title field (MSC alignment)
- Unit tests for `isForumRootContent` and feed aggregation
- Performance for large spaces (pagination)
## Related docs
- [lobby-forums HANDOFF](../lobby-forums/HANDOFF.md) — lobby card UI vs forum feed routing
- [sub-rooms HANDOFF](../sub-rooms/HANDOFF.md)
- [embed-filters HANDOFF](../embed-filters/HANDOFF.md)
## Add your extra things here
- ~40 files under `features/forum/` — start with `forumFeed.ts` and `ForumFeedPage.tsx` when onboarding
- `annotatePostsWithRoomScope` in `forumTopicHelpers.ts` for cross-topic feed display

View File

@@ -0,0 +1,75 @@
# Global search — handoff
## Summary
Quick global search modal for jumping to rooms, users, and recent conversations. Distinct from full message search — optimized for navigation, not event content search.
## User-facing behavior
- Keyboard shortcut / sidebar tab opens search modal
- Type to filter rooms and users
- Select result to navigate
## Architecture
```
SearchModalRenderer (in Router.tsx)
→ features/search/Search.tsx
→ searchModal.ts atom for open state
useAsyncSearch or similar for filtering
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/search/Search.tsx` | Modal UI |
| `cinny/src/app/state/searchModal.ts` | Open/close state |
| `cinny/src/app/pages/client/sidebar/SearchTab.tsx` | Sidebar entry |
## Data model
| Key | Storage |
|-----|---------|
| `searchModal` atom | memory |
## Dependencies
- Local room list + user index from sync
## Integration points
- **routing-and-navigation**: `SearchModalRenderer` in router
- **home-and-sidebar**: SearchTab
- **message-search**: different feature — don't merge UIs
## Testing
### Manual
1. Open search modal from sidebar.
2. Filter rooms by name.
3. Select room; navigates and modal closes.
### Automated
- None
## Known issues & gotchas
- Two "search" features confuse users — message search vs global nav search
- Modal focus trap via folds Overlay patterns
## Future work
- Unify search entry points with tabs (rooms / messages / users)
- Recent searches persistence
## Related docs
- [message-search HANDOFF](../message-search/HANDOFF.md)
- [routing-and-navigation HANDOFF](../routing-and-navigation/HANDOFF.md)
## Add your extra things here
- `SearchModalRenderer` mounted at client root level in Router.tsx

View File

@@ -0,0 +1,100 @@
# Home & sidebar — handoff
## Summary
The **Home** panel lists joined rooms (with sub-room nesting), and the **sidebar** switches between Home, DMs, Explore, Inbox, Search, Create, and Settings tabs. Room ordering, categories, and unread badges are rendered here.
## User-facing behavior
- Home: hierarchical room list with categories, drag reorder, unread/highlight states
- Sidebar tabs with Paarrot logo states (normal/unread/highlight) on Home
- Space tabs when inside a space context
- Threads category on home (`HomeThreadsCategory`)
- Sync status in client chrome
## Architecture
```
SidebarNav → tab routes (HomeTab, DirectTab, ExploreTab, ...)
Home.tsx
→ useHomeRooms.ts (room list + sub-room flattening)
→ RoomNavItem / RoomNavCategoryButton / UnjoinedSubRoomItem
DirectTab.tsx → useDirectRooms.ts
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/pages/client/home/Home.tsx` | Home room list UI |
| `cinny/src/app/pages/client/home/useHomeRooms.ts` | Room list logic + Paarrot room kind |
| `cinny/src/app/pages/client/SidebarNav.tsx` | Sidebar tab bar |
| `cinny/src/app/pages/client/sidebar/HomeTab.tsx` | Home tab |
| `cinny/src/app/pages/client/sidebar/DirectTab.tsx` | DMs tab + favicon logos |
| `cinny/src/app/pages/client/sidebar/InboxTab.tsx` | Inbox tab |
| `cinny/src/app/pages/client/sidebar/ExploreTab.tsx` | Explore tab |
| `cinny/src/app/pages/client/sidebar/SearchTab.tsx` | Search tab |
| `cinny/src/app/pages/client/sidebar/SettingsTab.tsx` | Settings tab |
| `cinny/src/app/pages/client/sidebar/SpaceTabs.tsx` | Space context tabs |
| `cinny/src/app/pages/client/home/HomeThreadsCategory.tsx` | Threads grouping |
| `cinny/src/app/hooks/useRoomListReorder.ts` | DnD reorder |
| `cinny/src/app/hooks/useSidebarItems.ts` | Sidebar item model |
## Data model
| Source | Data |
|--------|------|
| `allRoomsAtom` | Joined rooms |
| `roomToUnreadAtom` | Badge state |
| `mDirectAtom` | DM room mapping |
| `im.paarrot.sub_rooms` | Nested children (see sub-rooms) |
| `im.paarrot.room.kind` | Room classification |
| localStorage | Category collapse, room order |
## Dependencies
- Jotai room list atoms
- `matrix-js-sdk` room summaries
## Integration points
- **sub-rooms**: tree flattening in Home.tsx
- **room-nav**: nav item components
- **routing-and-navigation**: tab routes
- **background-notifications**: unread → favicon in DirectTab
## Testing
### Manual
1. Verify room order drag persists.
2. Sub-room nesting indent and unjoined state.
3. Unread badge on sidebar tab.
4. Category collapse/expand.
5. Switch tabs; active state correct.
### Automated
- None
## Known issues & gotchas
- `useHomeRooms` filters top-level vs sub-room IDs — easy to break tree
- DirectTab uses custom Paarrot SVGs for badge states
- Mobile hides sidebar in favor of bottom/full-page nav
## Future work
- Unified sidebar component between home and space contexts
- Virtualize long room lists
## Related docs
- [sub-rooms HANDOFF](../sub-rooms/HANDOFF.md)
- [room-nav HANDOFF](../room-nav/HANDOFF.md)
- [routing-and-navigation HANDOFF](../routing-and-navigation/HANDOFF.md)
## Add your extra things here
- `RoomNavCategoryButton` for collapsible sections
- `useClosedNavCategoriesAtom` for collapsed state

View File

@@ -0,0 +1,83 @@
# Inbox & invites — handoff
## Summary
The inbox area covers invites, notification feed, and notification preferences. Sidebar Inbox tab routes to invites list and notifications history.
## User-facing behavior
- View and accept/decline room invites
- Browse notification events (mentions, etc.)
- Per-room notification preference overrides
## Architecture
```
client/inbox/
Inbox.tsx — layout
Invites.tsx — invite list (allInvitesAtom)
Notifications.tsx — notification feed
ClientRoomsNotificationPreferences.tsx — bulk prefs
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/pages/client/inbox/Inbox.tsx` | Inbox layout |
| `cinny/src/app/pages/client/inbox/Invites.tsx` | Invites UI |
| `cinny/src/app/pages/client/inbox/Notifications.tsx` | Notifications list |
| `cinny/src/app/state/room-list/inviteList.ts` | `allInvitesAtom` |
| `cinny/src/app/pages/client/ClientRoomsNotificationPreferences.tsx` | Room notif prefs |
| `cinny/src/app/pages/client/sidebar/InboxTab.tsx` | Sidebar entry |
| `cinny/src/app/hooks/useRoomsNotificationPreferences.ts` | Prefs hook |
## Data model
| Source | Data |
|--------|------|
| Sync | `m.room.member` invite membership |
| Account data | push rules, per-room notification prefs |
## Dependencies
- matrix-js-sdk sync invite events
- Push rule APIs
## Integration points
- **background-notifications**: same push rules drive system notifications
- **user-settings**: notification settings tab
- **routing-and-navigation**: `INBOX_PATH`, `_INVITES_PATH`, `_NOTIFICATIONS_PATH`
## Testing
### Manual
1. Receive invite; appears in inbox.
2. Accept invite; room joins home list.
3. Decline invite; removed from list.
4. Notifications page shows recent highlights.
### Automated
- None
## Known issues & gotchas
- Invite list can desync briefly during sync gap
- Notification feed scope depends on push rules setup
## Future work
- Mark all invites read
- Notification grouping by room
## Related docs
- [background-notifications HANDOFF](../background-notifications/HANDOFF.md)
- [user-settings HANDOFF](../user-settings/HANDOFF.md)
## Add your extra things here
- `getInboxInvitesPath`, `getInboxNotificationsPath` in pathUtils

View File

@@ -0,0 +1,78 @@
# Join before navigate — handoff
## Summary
When a user opens a room link for a room they have not joined, this preview page shows room info (name, topic, member count) and offers join before entering the timeline.
## User-facing behavior
- matrix.to / internal join links land here if not member
- Room card preview with topic
- Join button → membership then navigate to room/space
- Back navigation on mobile
## Architecture
```
JoinBeforeNavigate.tsx
→ RoomSummaryLoader for preview data
→ useRoomNavigate after join
Route: _JOIN_PATH in Router.tsx
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/join-before-navigate/JoinBeforeNavigate.tsx` | Preview + join UI |
| `cinny/src/app/components/RoomSummaryLoader.tsx` | Fetch room preview |
| `cinny/src/app/hooks/useRoomNavigate.ts` | Post-join navigation |
| `cinny/src/app/state/joiningProgress.ts` | Join in progress UX |
## Data model
Preview from `/summary` API or peek room state. No local persistence.
## Dependencies
- matrix-js-sdk `joinRoom`
- `via` servers param for federated joins
## Integration points
- **routing-and-navigation**: join route with `roomIdOrAlias`, `viaServers`
- **explore**: similar preview patterns
- **electron-shell**: `paarrot://` deep links may target join flow
## Testing
### Manual
1. Open link to unjoined public room.
2. Preview shows name/topic.
3. Join succeeds; lands in timeline.
4. Space link navigates to space not room.
### Automated
- None
## Known issues & gotchas
- Private rooms without peek show limited preview
- `viaServers` required for some federated aliases
- `isSpace` check routes to space navigation after join
## Future work
- Knock rooms support
- Invite-only room messaging
## Related docs
- [routing-and-navigation HANDOFF](../routing-and-navigation/HANDOFF.md)
- [electron-shell HANDOFF](../electron-shell/HANDOFF.md)
## Add your extra things here
- Props: `roomIdOrAlias`, optional `eventId`, `viaServers[]`

View File

@@ -0,0 +1,95 @@
# Lobby & forums — handoff
## Summary
Spaces in Paarrot can show a **Lobby** view: a card-based hierarchy of child rooms with drag-and-drop ordering, hero header, and category collapse. **Forum** spaces (`m.forum` room type or `im.paarrot.room.kind` = `forum_space`) use a post-feed style navigation instead of the standard lobby when opened.
## User-facing behavior
- Opening a space lands on lobby (or forum feed for forum spaces)
- Lobby: hero, categories, room cards, DnD reorder (with permissions)
- Forum spaces: routed to post feed per `getSpaceDefaultPath` / `shouldShowForumLobby`
- Category open/closed state persisted per space
## Architecture
```
Space route → Lobby.tsx (SpaceCardLobby)
→ LobbyHeader, LobbyHero, SpaceHierarchy
→ DnD reorder → m.space.child order updates
→ closedLobbyCategories atom (per-space category collapse)
→ shouldShowForumLobby(space) gates forum vs lobby behavior
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/lobby/Lobby.tsx` | Main lobby view |
| `cinny/src/app/features/lobby/LobbyHeader.tsx` | Space header |
| `cinny/src/app/features/lobby/LobbyHero.tsx` | Hero banner area |
| `cinny/src/app/features/lobby/SpaceHierarchy.tsx` | Room tree/cards |
| `cinny/src/app/features/lobby/DnD.tsx` | Drag and drop |
| `cinny/src/app/features/lobby/RoomItem.tsx` | Room card |
| `cinny/src/app/state/closedLobbyCategories.ts` | Collapsed categories |
| `cinny/src/app/state/hooks/closedLobbyCategories.ts` | Jotai hook |
| `cinny/src/app/pages/pathUtils.ts` | `getSpaceLobbyPath`, forum routing |
| `cinny/src/app/utils/room.ts` | `shouldShowForumLobby`, `getStateEvent` |
| `cinny/src/types/matrix/room.ts` | `RoomType.Forum`, `PaarrotRoomKind` |
## Data model
| Event / field | Purpose |
|---------------|---------|
| `m.space.child` | Child room membership + `order` |
| `im.paarrot.sub_rooms` | Paarrot nested children (see sub-rooms handoff) |
| `im.paarrot.room.kind` | e.g. `forum_space` for forum behavior |
| `m.room.create` `type` | `m.forum` for forum rooms |
| Local: `closedLobbyCategories` | UI collapse state (not Matrix) |
## Dependencies
- Matrix space hierarchy events
- DnD library (see Lobby imports)
- Room power levels for reorder permission (`useCanDropLobbyItem`)
## Integration points
- **Sub-rooms**: `SpaceHierarchy` merges `im.paarrot.sub_rooms` children
- **Navigation**: `getSpaceDefaultPath` chooses lobby vs forum path
- **local-api**: `isSpaceLikeRoom` treats forum kind as space-like for channel API
## Testing
### Manual
1. Open a normal space → lobby with child rooms.
2. Collapse/expand category; reload — state should persist.
3. Drag reorder child (as admin); verify `m.space.child` order.
4. Open forum-type space; verify feed navigation not lobby cards.
5. Test with sub-rooms under a child space card.
### Automated
- None
## Known issues & gotchas
- Forum detection spans **two** signals: `m.forum` create type and `im.paarrot.room.kind`
- DnD permission logic is custom per `useCanDropLobbyItem` — read before changing
- `makeLobbyCategoryId(spaceId, childId)` keys collapse state
## Future work
- Unified hierarchy model (space.child + sub_rooms)
- Lobby theming from space avatar metadata
## Related docs
- [forum HANDOFF](../forum/HANDOFF.md) — post feed UI for forum spaces
- [sub-rooms HANDOFF](../sub-rooms/HANDOFF.md)
## Add your extra things here
- Route constant: `SPACE_LOBBY_PATH` in path utils
- `SpaceCardLobby` is the default export wrapper in `Lobby.tsx`

View File

@@ -0,0 +1,102 @@
# Local HTTP API — handoff
## Summary
Paarrot exposes a localhost HTTP API (port **33384**) so external tools — Stream Deck, scripts, macros — can control the running desktop app: mute/deafen, switch rooms, send messages, and read status. The Electron main process runs an Express server; actions are forwarded to the renderer via IPC and executed against the live Matrix client and call service.
## User-facing behavior
- API starts automatically when the Electron app launches (no user setting).
- Only binds to `127.0.0.1` — not reachable from the network.
- No authentication today; security relies on localhost-only binding.
## Architecture
```
External client (curl, Stream Deck, script)
→ GET/POST http://127.0.0.1:33384/...
→ electron/api-server.js (Express)
→ IPC to renderer (executeAction)
→ cinny/src/app/paarrot-api.ts (initPaarrotAPI)
→ Matrix client / CallService / router navigate
```
Navigation for room changes uses `setPaarrotNavigate()` registered from the React router in `ClientNonUIFeatures.tsx`.
## Key files
| Path | Role |
|------|------|
| `electron/api-server.js` | Express routes, IPC bridge to renderer |
| `electron/main.js` | Starts `PaarrotAPIServer` with the main window |
| `cinny/src/app/paarrot-api.ts` | Action handlers (mute, deafen, channel, message, status) |
| `cinny/src/app/pages/client/ClientNonUIFeatures.tsx` | Calls `initPaarrotAPI()` and `setPaarrotNavigate()` |
| `test-api.js` / `test-api.sh` | Smoke tests for endpoints |
| `paarrot-api.postman_collection.json` | Postman collection (generated) |
## Data model
No persistent storage. Actions read/write ephemeral app state (current room, call mute/deafen).
### IPC actions (renderer)
| Action | Params | Returns |
|--------|--------|---------|
| `get-status` | — | mute, deafen, currentRoom, connected, userId |
| `toggle-mute` / `set-mute` | `{ muted? }` | `{ muted }` |
| `toggle-deafen` / `set-deafen` | `{ deafened? }` | `{ deafened }` |
| `change-channel` | `{ roomId }` | navigation result |
| `get-channels` | — | room list |
| `send-message` | `{ roomId, message }` | send result |
| `send-message-current` | `{ message }` | send to active room |
| `get-current-room` | — | current room id |
## Dependencies
- `express`, `cors`, `body-parser` (Electron)
- `matrix-js-sdk` (renderer)
- Call feature: `getCallService()` from `features/call/useCall.ts`
## Integration points
- **Voice calls**: mute/deafen actions delegate to `CallService`
- **Stream Deck**: `streamdeck-plugin/` consumes this API
- **Room navigation**: uses `getHomeRoomPath`, `getDirectRoomPath`, `getSpaceRoomPath`
- **Space detection**: `isSpaceLikeRoom()` checks `im.paarrot.room.kind` and `m.forum` type
## Testing
### Manual
1. Start Paarrot desktop (`npm run dev` or built app).
2. `curl http://127.0.0.1:33384/health``{ "status": "ok" }`
3. `curl http://127.0.0.1:33384/status` while logged in.
4. Join a voice call; `POST /mute/toggle` and verify UI state.
5. `POST /channel` with a room id; verify navigation.
### Automated
- `node test-api.js` from repo root
## Known issues & gotchas
- Mute/deafen only work when `CallService` has an active call; otherwise handlers return a message, not an error.
- API is Electron-only; `initPaarrotAPI` no-ops when `window.electron.api` is missing (web/Tauri builds).
- Adding endpoints requires changes in **both** `api-server.js` and `paarrot-api.ts`.
## Future work
- Optional auth token for local API
- WebSocket or SSE for status push (Stream Deck polling today)
- Document all endpoints in Postman collection generator (`scripts/generate-postman-collection.js`)
## Related docs
- [API.md](../../API.md) — full endpoint reference
- [API-QUICKSTART.md](../../API-QUICKSTART.md)
- [streamdeck HANDOFF](../streamdeck/HANDOFF.md)
## Add your extra things here
- Default port: **33384** (`PaarrotAPIServer` constructor in `api-server.js`)
- `isSpaceLikeRoom` treats `forum_space` room kind and `m.forum` create type as spaces for channel listing

View File

@@ -0,0 +1,93 @@
# Matrix client — handoff
## Summary
The Matrix client layer wraps `matrix-js-sdk`: client creation, IndexedDB crypto/sync stores, start/stop sync, logout, and React context providers. `ClientRoot` is the authenticated app bootstrapper.
## User-facing behavior
- Splash screen ("Heating up") while client initializes
- Sync status reflected in UI (`SyncStatus`, splash)
- Logout from settings or forced server logout
- Mobile: visibility-based sync restart
## Architecture
```
ClientRoot.tsx
→ getCurrentSession() → initClient(session)
→ startClient(mx)
→ Providers: MatrixClient, Capabilities, MediaConfig, Call, PluginLoader
initMatrix.ts
→ createClient + IndexedDBStore + IndexedDBCryptoStore
→ secretStorageKeys / cryptoCallbacks
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/client/initMatrix.ts` | createClient, start/stop, logout, cache clear |
| `cinny/src/client/secretStorageKeys.ts` | SSSS / secret storage callbacks |
| `cinny/src/app/pages/client/ClientRoot.tsx` | Bootstrap, providers, title bar |
| `cinny/src/app/hooks/useMatrixClient.ts` | React context for `MatrixClient` |
| `cinny/src/app/hooks/useSyncState.ts` | Sync state subscription |
| `cinny/src/app/pages/client/SyncStatus.tsx` | Sync indicator UI |
| `cinny/src/app/components/splash-screen/SplashScreen.tsx` | Loading shell |
## Data model
| Store | Backend | Purpose |
|-------|---------|---------|
| IndexedDB sync store | matrix-js-sdk | Room timeline cache |
| IndexedDB crypto store | matrix-js-sdk | Olm/Megolm keys |
| Session | localStorage | Auth credentials |
## Dependencies
- `matrix-js-sdk`
- IndexedDB (browser/Electron)
## Integration points
- **authentication**: session → initClient
- **state-management**: sync events feed atoms
- **background-notifications**: pushers registered after sync
- **e2e-and-devices**: crypto store, verification
- **plugins**: Matrix client exposed to plugin API
## Testing
### Manual
1. Fresh login; verify sync completes (timeline loads).
2. Reload app; session restores without re-login.
3. Logout; IndexedDB cleared appropriately.
4. Airplane mode → sync error → recovery on reconnect.
### Automated
- None
## Known issues & gotchas
- `handlingKeyConflict` flag in initMatrix for crypto key conflicts
- `deleteAllMatrixDatabases` on certain logout/clear paths — destructive
- Mobile `useVisibilitySync` restarts sync on app resume (Tauri)
- `clearCacheAndReload` utility for support scenarios
## Future work
- Lazy sync / sliding sync if migrating MSC
- Better sync error UX with retry backoff
## Related docs
- [authentication HANDOFF](../authentication/HANDOFF.md)
- [state-management HANDOFF](../state-management/HANDOFF.md)
- [e2e-and-devices HANDOFF](../e2e-and-devices/HANDOFF.md)
## Add your extra things here
- `logoutClient`, `clearLoginData`, `clearCacheAndReload` exported from initMatrix.ts
- `HttpApiEvent.SessionLoggedOut` listener in ClientRoot clears everything

View File

@@ -0,0 +1,90 @@
# Media & URL preview — handoff
## Summary
Handles file uploads, encrypted media, image/video viewers, thumbnails, and URL preview cards (Open Graph, Giphy, etc.). Upload progress shown via upload cards in the composer.
## User-facing behavior
- Attach files via `+` button, paste, or drag-drop
- Image/video lightbox viewers
- Link URLs expand to preview cards in timeline
- Encrypted attachments decrypted client-side
- Upload progress and cancel
## Architecture
```
UploadCardRenderer / upload state atoms
→ encryptFile (utils/matrix) for encrypted rooms
→ matrix uploadContent
url-preview/ components
→ fetch OG metadata, Giphy embed
→ gated by embed-filters
image-viewer/, video-viewer/ — fullscreen media
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/components/upload-card/UploadCardRenderer.tsx` | Upload UI |
| `cinny/src/app/state/upload.ts` | Upload state |
| `cinny/src/app/state/room/roomInputDrafts.ts` | `TUploadItem` |
| `cinny/src/app/utils/matrix.ts` | `encryptFile`, upload helpers |
| `cinny/src/app/components/url-preview/` | Link embed cards |
| `cinny/src/app/components/image-viewer/` | Image lightbox |
| `cinny/src/app/components/video-viewer/` | Video player |
| `cinny/src/app/hooks/useMediaAuthentication.ts` | Authenticated media URLs |
| `cinny/src/app/hooks/useAuthenticatedMediaUrl.ts` | MXC → HTTP with auth |
| `cinny/src/app/components/message/content/` | Image/video/thumbnail content |
## Data model
Upload queue in Jotai atom families (per room). Media itself on homeserver as MXC.
## Dependencies
- Homeserver media repository config (`useMediaConfig`)
- `matrix-js-sdk` encryption APIs
## Integration points
- **room-timeline**: attachment messages + URL embeds
- **embed-filters**: suppress previews
- **editor-and-composer**: upload from composer
- **avatar-metadata**: separate pipeline for profile images
## Testing
### Manual
1. Upload image in encrypted room; verify decrypt on receive.
2. Post URL; preview card appears.
3. Block URL pattern via embed filter; no card.
4. Open image in viewer; zoom/navigate.
### Automated
- None
## Known issues & gotchas
- Media authentication required on some homeservers (`useMediaAuthentication`)
- Large file memory on mobile
- Giphy embed has separate CSS module
## Future work
- Resumable uploads
- Blurhash placeholders
## Related docs
- [embed-filters HANDOFF](../embed-filters/HANDOFF.md)
- [room-timeline HANDOFF](../room-timeline/HANDOFF.md)
## Add your extra things here
- `ThumbnailContent.tsx`, `VideoContent.tsx` in message/content/
- Android share materializes files into upload draft (see android handoff)

View File

@@ -0,0 +1,89 @@
# Message rendering — handoff
## Summary
Built-in plugins parse and render message bodies: custom markdown (block + inline), HTML sanitization, syntax highlighting, matrix.to links, emoji shortcodes, bad-word filter hooks, and PDF preview support.
## User-facing behavior
- Messages render formatted text, code blocks with Prism highlighting
- Clickable matrix.to / room links
- Custom emoji from packs inline
- Spoiler/highlight/color extensions where enabled
## Architecture
```
RenderMessageContent.tsx
→ react-custom-html-parser.tsx
→ plugins/markdown/ (block + inline parsers)
→ plugins/react-prism/ReactPrism.tsx
→ plugins/emoji.ts, matrix-to.ts, color.ts
msgContent.ts — content type detection
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/components/RenderMessageContent.tsx` | Main render entry |
| `cinny/src/app/plugins/react-custom-html-parser.tsx` | HTML → React |
| `cinny/src/app/plugins/markdown/` | Markdown rules + parsers |
| `cinny/src/app/plugins/react-prism/ReactPrism.tsx` | Code highlighting |
| `cinny/src/app/plugins/emoji.ts` | Emoji shortcodes |
| `cinny/src/app/plugins/matrix-to.ts` | matrix.to link handling |
| `cinny/src/app/plugins/bad-words.ts` | Content filter hook |
| `cinny/src/app/plugins/color.ts` | Text color |
| `cinny/src/app/utils/sanitize.ts` | HTML sanitization |
| `cinny/src/app/features/room/msgContent.ts` | Message content helpers |
## Data model
None — pure functions over message content JSON.
## Dependencies
- `prismjs` (via react-prism)
- `pdfjs-dist` wrapper for PDF thumbnails
- `millify` for number formatting in embeds
## Integration points
- **room-timeline**: all message bodies
- **forum**: `forumRichText.ts`, `ForumMessageBody.tsx`
- **custom-emoji-packs**: emoji plugin reads pack data
- **embed-filters**: separate from body render — URL preview gating
## Testing
### Manual
1. Send markdown: bold, code fence, link.
2. Verify matrix.to link navigates in-app.
3. Custom emoji shortcode renders image.
4. XSS attempt in message — should sanitize.
### Automated
- None
## Known issues & gotchas
- Markdown rules split block vs inline — order matters in parser
- `bad-words.ts` may be no-op depending on config
- HTML from other clients may not match Paarrot markdown subset
## Future work
- CommonMark test suite
- Lazy-load Prism languages
## Related docs
- [room-timeline HANDOFF](../room-timeline/HANDOFF.md)
- [custom-emoji-packs HANDOFF](../custom-emoji-packs/HANDOFF.md)
## Add your extra things here
- `plugins/markdown/block/rules.ts` and `inline/rules.ts` — start here for new syntax
- `via-servers.ts` for link join hints

View File

@@ -0,0 +1,79 @@
# Message search — handoff
## Summary
In-room and cross-room message search UI: query input, filters, result grouping, and navigation to found events. Uses Matrix `/search` or client-side APIs via `useMessageSearch`.
## User-facing behavior
- Search modal or page with text query
- Filters: room, sender, date (see SearchFilters)
- Results grouped by room/period
- Click result → jump to event in timeline
## Architecture
```
MessageSearch.tsx
→ useMessageSearch.ts
→ SearchInput, SearchFilters, SearchResultGroup
Router may mount search under client routes
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/message-search/MessageSearch.tsx` | Main UI |
| `cinny/src/app/features/message-search/useMessageSearch.ts` | Search logic |
| `cinny/src/app/features/message-search/SearchInput.tsx` | Query field |
| `cinny/src/app/features/message-search/SearchFilters.tsx` | Filters |
| `cinny/src/app/features/message-search/SearchResultGroup.tsx` | Result list |
## Data model
Ephemeral search state in component hooks. No persistence.
## Dependencies
- `matrix-js-sdk` search API
- Homeserver must support message search (server-dependent)
## Integration points
- **routing-and-navigation**: search routes in home/space
- **room-timeline**: navigation to event id
- **global-search**: separate modal feature — see that handoff
## Testing
### Manual
1. Search known keyword; results appear.
2. Apply room filter; results scoped.
3. Click result; timeline scrolls to event.
4. Test on homeserver without search — expect graceful error.
### Automated
- None
## Known issues & gotchas
- Server-side search availability varies by deployment
- Encrypted room search limited by server/indexing policy
- Pagination in `useMessageSearch` — check for duplicate results
## Future work
- Client-side fallback search for small rooms
- Highlight match in timeline
## Related docs
- [global-search HANDOFF](../global-search/HANDOFF.md)
- [room-timeline HANDOFF](../room-timeline/HANDOFF.md)
## Add your extra things here
- Exported from `features/message-search/index.ts`

View File

@@ -0,0 +1,98 @@
# Plugin system — handoff
## Summary
Paarrot extends the Matrix client with JavaScript plugins loaded from disk or the marketplace. Plugins can register slash commands, inject UI buttons in 11 locations, hook Matrix events, register themes, and run background tasks. The runtime is `@paarrot/plugin-manager`; the app wires slots in settings, nav, composer, and room UI.
## User-facing behavior
- **Settings → Plugins**: Installed, Marketplace, enable/disable, uninstall
- Plugins live in OS-specific dirs (`%APPDATA%\paarrot\plugins\`, `~/.config/Paarrot/plugins/`, etc.)
- Marketplace pulls from [Plugin Directory](https://github.com/Paarrot/Plugin-Directory) JSON index
## Architecture
```
Plugin folder (index.js + plugin-metadata.json)
→ PluginLoader (reads, validates, enables)
→ @paarrot/plugin-manager (lifecycle, API surface)
→ PluginAPI.ts (app-specific bindings: Matrix client, navigate, notifications)
→ UI slots: PluginNavSlot, PluginButtonSlot, PluginSidebarSlot, ...
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/settings/plugins/PluginLoader.tsx` | Load/enable/disable plugins |
| `cinny/src/app/features/settings/plugins/PluginAPI.ts` | Re-exports plugin-manager + app hooks |
| `cinny/src/app/features/settings/plugins/Plugins.tsx` | Settings UI (installed + marketplace) |
| `cinny/src/app/features/settings/plugins/PluginButtonSlot.tsx` | Icon button injection |
| `cinny/src/app/features/settings/plugins/PluginNavSlot.tsx` | Nav row injection |
| `cinny/src/app/features/settings/plugins/PluginSidebarSlot.tsx` | Sidebar icon injection |
| `cinny/src/app/features/settings/plugins/types.ts` | Local types |
| `cinny/src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md` | Author-facing dev guide |
| `example-plugin/` | Reference plugin at repo root |
| `cinny/docs/PLUGIN_*.md` | API reference docs |
## Data model
- Plugin metadata: `plugin-metadata.json` per plugin (id, version, permissions, `paarrot.utils` deps)
- Enabled state: persisted via plugin-manager (Electron userData)
- No Matrix events for plugins themselves
### UI locations (`UILocation`)
`channel-list`, `home-section`, `direct-messages`, `sidebar-actions`, `text-composer-toolbar`, `composer-actions`, `room-header`, `room-menu`, `message-actions`, `user-menu`, `search-notification-section`
## Dependencies
- `@paarrot/plugin-manager` (npm)
- Electron `fs` / IPC for plugin install paths
- GitHub raw URLs for marketplace index (`PluginAPI.ts`)
## Integration points
- **Matrix client**: exposed to plugins for send/receive
- **Local API**: separate from plugins; plugins do not use HTTP API by default
- **Settings**: plugin tabs via `PluginTab`
- **All major UI surfaces**: search codebase for `PluginButtonSlot` / `PluginNavSlot` usages
## Testing
### Manual
1. Copy `example-plugin/` to plugins directory.
2. Enable in Settings → Plugins.
3. Verify slash command and injected button appear.
4. Disable plugin; verify UI cleans up.
### Automated
- None dedicated; validate via example-plugin manually
## Known issues & gotchas
- Plugin API version must match `@paarrot/plugin-manager` expectations
- `ctx.require("other-plugin-id")` for inter-plugin deps
- Marketplace cache-bust query param on index fetch
- Plugin dev guide lives inside `src/` — consider linking from handoff only
## Future work
- Sandboxing / permission prompts per capability
- Hot reload without app restart
- Signed plugin packages
## Related docs
- [PLUGINS.md](../../PLUGINS.md)
- [PLUGIN_API.md](../../PLUGIN_API.md)
- [PLUGIN_BUTTON_API.md](../../PLUGIN_BUTTON_API.md)
- [PLUGIN_SYSTEM_IMPLEMENTATION.md](../../PLUGIN_SYSTEM_IMPLEMENTATION.md)
- [PLUGIN_DEVELOPMENT.md](../../../../src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md)
## Add your extra things here
- Marketplace base: `https://raw.githubusercontent.com/Paarrot/Plugin-Directory/refs/heads/main/plugins/`
- 11 UI injection points — see README plugin table in repo root

View File

@@ -0,0 +1,82 @@
# Room nav — handoff
## Summary
Room nav components render individual channel rows in sidebars and space channel lists: room items, category buttons, call participant indicators, sub-room actions, and unjoined sub-room join prompts.
## User-facing behavior
- Room row: name, avatar, unread badge, active highlight
- Context menu: add sub-room, room actions
- Call participants indicator when call active in room
- Unjoined sub-room row with join button
- Category headers with collapse
## Architecture
```
features/room-nav/
RoomNavItem → per-room row + menus
RoomNavCategoryButton → section headers
UnjoinedSubRoomItem → join flow
CallParticipantsIndicator → active call badge
Used by Home.tsx, space sidebars, forum sidebars
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/room-nav/RoomNavItem.tsx` | Main room row |
| `cinny/src/app/features/room-nav/RoomNavCategoryButton.tsx` | Category header |
| `cinny/src/app/features/room-nav/UnjoinedSubRoomItem.tsx` | Unjoined sub-room |
| `cinny/src/app/features/room-nav/CallParticipantsIndicator.tsx` | In-call indicator |
| `cinny/src/app/features/room-nav/styles.css.ts` | Nav styles |
## Data model
Uses room state, unread atoms, `im.paarrot.sub_rooms` permissions — no own persistence.
## Dependencies
- `usePermission` for sub-room create
- `useOpenCreateSubRoomModal` from state hooks
## Integration points
- **sub-rooms**: Add Sub-Room menu action
- **voice-calls**: participant indicator
- **create-flows**: opens create room modal with parent id
- **home-and-sidebar**: primary consumer
## Testing
### Manual
1. Right-click/context menu on room row.
2. Add sub-room when permitted / disabled when not.
3. Active call shows participant indicator.
4. Unjoined sub-room join navigates correctly.
### Automated
- None
## Known issues & gotchas
- Depth indentation for sub-rooms passed as prop — styling in CSS
- `canAddSubRoom` checks `StateEvent.PaarrotSubRooms` power level
## Future work
- Consolidate with forum sidebar items if duplicated
## Related docs
- [sub-rooms HANDOFF](../sub-rooms/HANDOFF.md)
- [home-and-sidebar HANDOFF](../home-and-sidebar/HANDOFF.md)
- [voice-calls HANDOFF](../voice-calls/HANDOFF.md)
## Add your extra things here
- Export barrel: `features/room-nav/index.ts`

View File

@@ -0,0 +1,81 @@
# Room settings — handoff
## Summary
Per-room settings overlay: general info, permissions, members (via common-settings), sub-rooms (Paarrot), and developer tools. Opened from room header/menu without leaving the room.
## User-facing behavior
- Edit room name, topic, avatar, address, join rules, encryption
- Permissions and power levels
- Sub-rooms management (Paarrot)
- Member list and moderation actions
## Architecture
```
RoomSettingsRenderer (Router overlay)
→ RoomSettings.tsx — page nav
→ room-settings/general/, permissions/, sub-rooms/
→ delegates to common-settings/ for shared pages
roomSettings.ts atom — which page open
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/room-settings/RoomSettings.tsx` | Settings shell |
| `cinny/src/app/features/room-settings/general/` | General tab |
| `cinny/src/app/features/room-settings/permissions/` | Permissions |
| `cinny/src/app/features/room-settings/sub-rooms/SubRooms.tsx` | Sub-rooms (Paarrot) |
| `cinny/src/app/state/roomSettings.ts` | Modal/page state |
| `cinny/src/app/state/hooks/roomSettings.ts` | Open/close hooks |
| `cinny/src/app/features/common-settings/` | Shared setting pages |
## Data model
Standard Matrix room state events (`m.room.name`, `m.room.power_levels`, etc.) plus `im.paarrot.sub_rooms`.
## Dependencies
- `useRoomPermissions`, `usePowerLevels`
- common-settings components
## Integration points
- **sub-rooms**: Paarrot-specific tab
- **common-settings**: members, emojis, developer tools reuse
- **embed-filters**: `EmbedFilters.tsx` in common-settings general
## Testing
### Manual
1. Open room settings from ⋮ menu.
2. Change topic; verify state event.
3. Sub-rooms tab: reorder children.
4. Permissions: change power level.
### Automated
- None
## Known issues & gotchas
- Overlay vs full page on mobile — check ScreenSize branches
- `RoomSettingsRenderer` must stay mounted in Router for deep links
## Future work
- Room settings deep link URL
## Related docs
- [sub-rooms HANDOFF](../sub-rooms/HANDOFF.md)
- [common-settings HANDOFF](../common-settings/HANDOFF.md)
- [embed-filters HANDOFF](../embed-filters/HANDOFF.md)
## Add your extra things here
- `SubRoomsPage` enum in `roomSettings.ts`

View File

@@ -0,0 +1,110 @@
# Room timeline — handoff
## Summary
The room view is the core messaging experience: virtualized timeline, message rendering, composer input, threads, reactions, pins, members drawer, and room header. Includes Paarrot-specific embed filter integration and command autocomplete.
## User-facing behavior
- Scrollable timeline with read markers, edits, redactions, reactions
- Rich composer with mentions, emoji, attachments, slash commands
- Thread side panel (`ThreadView`)
- Members drawer, room header actions, typing indicators
- Jump to time, pinned messages menu, reaction viewer
- Following mode / tombstone for upgraded rooms
## Architecture
```
Room.tsx / RoomView.tsx
→ RoomTimeline.tsx (virtualized events)
→ RoomInput.tsx + Editor
→ Message.tsx / RenderMessageContent.tsx
→ ThreadView.tsx
→ RoomViewHeader.tsx, MembersDrawer
Hooks: useVirtualPaginator, useRoomLatestRenderedEvent, useMarkAsRead
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/room/Room.tsx` | Room entry |
| `cinny/src/app/features/room/RoomView.tsx` | Main layout |
| `cinny/src/app/features/room/RoomTimeline.tsx` | Timeline |
| `cinny/src/app/features/room/RoomInput.tsx` | Composer |
| `cinny/src/app/features/room/ThreadView.tsx` | Thread panel |
| `cinny/src/app/features/room/RoomViewHeader.tsx` | Header bar |
| `cinny/src/app/features/room/message/Message.tsx` | Single message |
| `cinny/src/app/features/room/message/Reactions.tsx` | Reaction row |
| `cinny/src/app/features/room/message/MessageEditor.tsx` | Edit message |
| `cinny/src/app/features/room/CommandAutocomplete.tsx` | Slash commands |
| `cinny/src/app/features/room/reaction-viewer/ReactionViewer.tsx` | Who reacted |
| `cinny/src/app/features/room/jump-to-time/JumpToTime.tsx` | Time jump UI |
| `cinny/src/app/features/room/room-pin-menu/RoomPinMenu.tsx` | Pinned messages |
| `cinny/src/app/components/RenderMessageContent.tsx` | Content render + embeds |
| `cinny/src/app/hooks/useVirtualPaginator.ts` | Timeline pagination |
## Data model
| Source | Data |
|--------|------|
| Matrix sync | `m.room.message`, edits, reactions, pins |
| Room account data | drafts (`roomInputDrafts`) |
| `im.paarrot.embed_filters` | Personal embed blocks |
| `im.paarrot.room.embed_filters` | Room-wide embed blocks |
## Dependencies
- `matrix-js-sdk` timeline APIs
- Slate editor (composer)
- Embed filter hooks
- Plugin button slots in header/composer/message actions
## Integration points
- **editor-and-composer**: RoomInput uses Editor
- **embed-filters**: timeline + thread embed gating
- **message-rendering**: markdown/HTML parse pipeline
- **media-and-url-preview**: attachments and link cards
- **plugins**: message-actions, composer-actions slots
- **voice-calls**: header call controls
## Testing
### Manual
1. Send text, image, reply, edit, delete.
2. Open thread; reply in thread.
3. Add reaction; open reaction viewer.
4. Pin message; open pin menu.
5. Block embed domain; verify no preview.
6. Slash command from plugin.
### Automated
- None
## Known issues & gotchas
- Virtual paginator complexity — scroll position bugs common regression area
- Encrypted rooms: render path differs for `m.room.encrypted`
- `RoomViewFollowing` auto-scroll behavior vs manual scroll
- Large threads performance
## Future work
- Timeline unit tests for edit/redaction grouping
- Improved read receipt aggregation
## Related docs
- [editor-and-composer HANDOFF](../editor-and-composer/HANDOFF.md)
- [embed-filters HANDOFF](../embed-filters/HANDOFF.md)
- [message-rendering HANDOFF](../message-rendering/HANDOFF.md)
- [media-and-url-preview HANDOFF](../media-and-url-preview/HANDOFF.md)
## Add your extra things here
- `msgContent.ts` — message content helpers
- `useRoomNavigate` for permalink jumps

View File

@@ -0,0 +1,96 @@
# Routing & navigation — handoff
## Summary
React Router drives all app navigation: auth routes, client shell, home/direct/space/inbox/explore paths, room views, lobby/forum feeds, and modal overlays (settings, search, create). Supports hash router mode via client config.
## User-facing behavior
- URL reflects current room, space, inbox tab, explore server
- Mobile vs desktop route layouts (`MobileFriendly` wrappers)
- Deep links and `matrix.to` style joins via join route
- After-login redirect preserves intended destination
- View transitions on supported platforms
## Architecture
```
App.tsx → createRouter(clientConfig, screenSize)
→ Auth routes | ClientLayout + SidebarNav + Outlet
paths.ts — route constants
pathUtils.ts — path builders (getHomeRoomPath, getSpaceDefaultPath, ...)
Router.tsx — route tree + loaders/guards
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/pages/Router.tsx` | Full route tree |
| `cinny/src/app/pages/paths.ts` | Path constants |
| `cinny/src/app/pages/pathUtils.ts` | Path builder utilities |
| `cinny/src/app/pages/App.tsx` | RouterProvider setup |
| `cinny/src/app/pages/client/ClientLayout.tsx` | Authenticated layout shell |
| `cinny/src/app/pages/client/SidebarNav.tsx` | Left sidebar tabs |
| `cinny/src/app/pages/MobileFriendly.tsx` | Mobile nav adapters |
| `cinny/src/app/hooks/useRoomNavigate.ts` | Imperative room navigation |
| `cinny/src/app/hooks/useNavToActivePathMapper.ts` | Restore last path |
| `cinny/src/app/pages/afterLoginRedirectPath.ts` | Login redirect |
| `cinny/src/app/components/AnimatedOutlet.tsx` | Route transitions |
## Data model
| Key | Purpose |
|-----|---------|
| `navToActivePath` atom | Per-context last URL |
| URL path params | roomId, spaceId, eventId |
## Dependencies
- `react-router-dom` v6
- `clientConfig.hashRouter` for Electron/file protocol
## Integration points
- **forum**: `ForumAwareSpaceLayout`, `ForumFeedPage` routes
- **lobby-forums**: `_LOBBY_PATH`, `Lobby` component
- **join-before-navigate**: `_JOIN_PATH` route
- **local-api**: `setPaarrotNavigate` uses same path utils
- **settings/room-settings**: overlay renderers on client routes
## Testing
### Manual
1. Navigate home → room → back; URL updates correctly.
2. Open space; forum vs lobby path per room type.
3. Hash router mode (if enabled in config).
4. Mobile breakpoints: sidebar vs full-page nav.
5. Refresh on deep room URL — lands in same room.
### Automated
- None
## Known issues & gotchas
- `getSpaceDefaultPath` branches forum vs lobby — must stay in sync with forum handoff
- Hash router needed for some Electron `file://` deployments
- Many path helpers — prefer using `pathUtils` over string concat
## Future work
- Route-level code splitting audit
- Unified join/link handler
## Related docs
- [forum HANDOFF](../forum/HANDOFF.md)
- [lobby-forums HANDOFF](../lobby-forums/HANDOFF.md)
- [join-before-navigate HANDOFF](../join-before-navigate/HANDOFF.md)
- [home-and-sidebar HANDOFF](../home-and-sidebar/HANDOFF.md)
## Add your extra things here
- Key paths: `HOME_PATH`, `DIRECT_PATH`, `SPACE_PATH`, `INBOX_PATH`, `EXPLORE_PATH`, `_FEED_PATH`, `_LOBBY_PATH`
- `getAppPathFromHref` for parsing external links

View File

@@ -0,0 +1,73 @@
# Space settings — handoff
## Summary
Per-space settings overlay analogous to room settings: general space profile, permissions, and shared common-settings pages. Used for Matrix spaces (and forum spaces).
## User-facing behavior
- Edit space name, topic, avatar, visibility
- Space permissions and power levels
- Child space management via common flows
## Architecture
```
SpaceSettingsRenderer (Router overlay)
→ space-settings/SpaceSettings.tsx
→ general/, permissions/
spaceSettings.ts atom
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/space-settings/SpaceSettings.tsx` | Settings shell |
| `cinny/src/app/features/space-settings/general/` | General tab |
| `cinny/src/app/features/space-settings/permissions/` | Permissions |
| `cinny/src/app/state/spaceSettings.ts` | Open state |
| `cinny/src/app/state/hooks/spaceSettings.ts` | Hooks |
## Data model
Matrix space state: `m.space.child`, `m.room.name`, power levels, etc.
## Dependencies
- Same permission patterns as room settings
## Integration points
- **lobby-forums**: space metadata shown in lobby hero
- **create-flows**: add existing rooms to space
- **common-settings**: shared pages
## Testing
### Manual
1. Open space settings.
2. Edit space name and avatar.
3. Change join rules / visibility.
### Automated
- None
## Known issues & gotchas
- Forum spaces may expose different settings entry points — verify `ForumSpaceLayout`
## Future work
- Space-level embed filter defaults
## Related docs
- [common-settings HANDOFF](../common-settings/HANDOFF.md)
- [lobby-forums HANDOFF](../lobby-forums/HANDOFF.md)
## Add your extra things here
- `usePermissionItems` in space-settings/permissions/

View File

@@ -0,0 +1,99 @@
# State management — handoff
## Summary
Client UI state uses **Jotai** atoms: room lists, unread counts, settings, modals, drafts, navigation memory, and sidebar collapse. Many atoms sync to localStorage via `atomWithLocalStorage`.
## User-facing behavior
- Room list order, collapsed categories, sidebar folders persist across sessions
- Message drafts and upload queues per room persist locally
- Settings (theme, emoji style, zoom) persist locally
- Unread badges drive favicon and sidebar indicators
## Architecture
```
JotaiProvider (App.tsx)
→ atoms in state/
→ hooks in state/hooks/ wrap atoms for components
ClientBindAtoms.tsx binds Matrix events → atom updates
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/state/settings.ts` | User settings atom |
| `cinny/src/app/state/sessions.ts` | Auth sessions |
| `cinny/src/app/state/room-list/roomList.ts` | `allRoomsAtom`, ordering |
| `cinny/src/app/state/room/roomToUnread.ts` | Unread counts |
| `cinny/src/app/state/room/roomInputDrafts.ts` | Composer drafts + uploads |
| `cinny/src/app/state/room/roomToParents.ts` | Space parent mapping |
| `cinny/src/app/state/mDirectList.ts` | Direct message index |
| `cinny/src/app/state/navToActivePath.ts` | Last active path per context |
| `cinny/src/app/state/closedNavCategories.ts` | Collapsed nav sections |
| `cinny/src/app/state/closedLobbyCategories.ts` | Collapsed lobby categories |
| `cinny/src/app/state/createRoomModal.ts` | Create room modal state |
| `cinny/src/app/state/createSpaceModal.ts` | Create space modal state |
| `cinny/src/app/state/searchModal.ts` | Global search modal |
| `cinny/src/app/state/utils/atomWithLocalStorage.ts` | Persistence helper |
| `cinny/src/app/pages/client/ClientBindAtoms.ts` | Matrix → atom bindings |
## Data model
Most state is **local only** (not Matrix). Exceptions: settings that also write account data (notification prefs via separate hooks).
| Atom area | Persists? |
|-----------|-----------|
| settings | localStorage |
| room list order | localStorage |
| drafts | localStorage |
| unread | memory (derived from sync) |
| modals | memory |
## Dependencies
- `jotai`
- `@tanstack/react-query` (separate from Jotai, used in App.tsx)
## Integration points
- **matrix-client**: sync updates room/unread atoms via ClientBindAtoms
- **home-and-sidebar**: room list atoms
- **room-timeline**: draft atoms
- **routing-and-navigation**: navToActivePath
## Testing
### Manual
1. Type draft, reload — draft should restore.
2. Collapse category, reload — stays collapsed.
3. Reorder rooms; verify order persists.
4. Logout — verify which atoms clear vs persist.
### Automated
- None
## Known issues & gotchas
- Logout clears **all** localStorage in session-logout handler — wipes settings too
- Atom families (`roomIdToMsgDraftAtomFamily`) — memory growth in many rooms
- `ClientBindAtoms` must mount early or UI shows stale unreads
## Future work
- Selective localStorage clear on logout (keep non-sensitive prefs)
- Document atom dependency graph
## Related docs
- [matrix-client HANDOFF](../matrix-client/HANDOFF.md)
- [home-and-sidebar HANDOFF](../home-and-sidebar/HANDOFF.md)
## Add your extra things here
- `useSetting` hook in `state/hooks/settings.ts` — primary settings accessor
- `joiningProgress.ts` — room join UX state

View File

@@ -0,0 +1,82 @@
# Stream Deck integration — handoff
## Summary
The Elgato Stream Deck plugin (`streamdeck-plugin/`) controls Paarrot via the local HTTP API: toggle mute/deafen, switch channels, send messages, and show status. It is a separate package from the main app but ships in the same monorepo.
## User-facing behavior
- User installs `.streamDeckPlugin` or copies `com.paarrot.streamdeck.sdPlugin` into Stream Deck plugins folder
- Requires Paarrot desktop running with API on `http://127.0.0.1:33384`
- Actions: Toggle Mute, Toggle Deafen, Change Channel, Send Message, Get Status
## Architecture
```
Stream Deck button press
→ com.paarrot.streamdeck.sdPlugin/plugin/index.js
→ fetch http://127.0.0.1:33384/...
→ Paarrot Electron API (see local-api handoff)
```
## Key files
| Path | Role |
|------|------|
| `streamdeck-plugin/com.paarrot.streamdeck.sdPlugin/manifest.json` | Plugin manifest |
| `streamdeck-plugin/com.paarrot.streamdeck.sdPlugin/plugin/index.js` | Action implementations |
| `streamdeck-plugin/com.paarrot.streamdeck.sdPlugin/plugin/index.html` | Property inspector UI |
| `streamdeck-plugin/build.js` | Build `.streamDeckPlugin` bundle |
| `streamdeck-plugin/validate.js` | Manifest validation |
| `streamdeck-plugin/README.md` | User install guide |
| `streamdeck-plugin/QUICKSTART.md` | Dev quickstart |
## Data model
None — stateless HTTP client. Channel list and status come from Paarrot API responses.
## Dependencies
- Elgato Stream Deck software 6.0+
- Paarrot local API (port 33384)
- Node build tooling in `streamdeck-plugin/package.json`
## Integration points
- **local-api**: sole backend; breaking API changes require Stream Deck plugin update
- Icons: `streamdeck-plugin/.../images/paarrot.svg`, `status.svg`
## Testing
### Manual
1. Run Paarrot; confirm `/health` responds.
2. Load plugin in Stream Deck developer mode.
3. Map Toggle Mute to a key; join a call; press key.
4. Map Change Channel; verify room switch in Paarrot.
### Automated
- `node streamdeck-plugin/validate.js`
## Known issues & gotchas
- Plugin repo README still references placeholder GitHub URLs in places — update on publish
- No auth on API; same machine trust model
- Deafen/mute only meaningful during active calls
## Future work
- Auto-discover rooms without manual room id entry in property inspector
- Connection status indicator on Stream Deck key
## Related docs
- [local-api HANDOFF](../local-api/HANDOFF.md)
- [API.md](../../API.md)
- [streamdeck-plugin/README.md](../../../../streamdeck-plugin/README.md)
## Add your extra things here
- Bundle id: `com.paarrot.streamdeck`
- Build output: `.streamDeckPlugin` via `npm run build` in `streamdeck-plugin/`

View File

@@ -0,0 +1,93 @@
# Sub-rooms — handoff
## Summary
Sub-rooms let a parent room own an ordered list of child rooms, displayed nested in the home channel list and manageable in room settings. Hierarchy is stored in Matrix room state (`im.paarrot.sub_rooms`), not only `m.space.child`, so channels can nest under a room without full space semantics.
## User-facing behavior
- Nested channels under a room in **Home** sidebar (indented tree)
- **Add Sub-Room** from room nav context menu (requires power level on `im.paarrot.sub_rooms`)
- **Room settings → Sub-rooms** to reorder/remove children
- Creating a sub-room from parent updates parent's `children` array
- Unjoined sub-rooms show as `UnjoinedSubRoomItem` with join affordance
## Architecture
```
Room state: im.paarrot.sub_rooms { children: string[] }
→ useRoomSubRooms hook
→ Home.tsx flatten tree (addRoomAndSubRooms)
→ RoomNavItem "Add Sub-Room" → CreateRoomModal
→ SubRooms.tsx settings editor
→ SpaceHierarchy.tsx (lobby) reads same state
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/hooks/useRoomSubRooms.ts` | Hook + `PaarrotSubRoomsContent` type |
| `cinny/src/types/matrix/room.ts` | `StateEvent.PaarrotSubRooms` |
| `cinny/src/app/pages/client/home/Home.tsx` | Renders nested list |
| `cinny/src/app/features/room-nav/RoomNavItem.tsx` | Add sub-room action |
| `cinny/src/app/features/room-nav/UnjoinedSubRoomItem.tsx` | Unjoined child UI |
| `cinny/src/app/features/room-settings/sub-rooms/SubRooms.tsx` | Settings page |
| `cinny/src/app/features/create-room/CreateRoomModal.tsx` | Appends new room to parent state |
| `cinny/src/app/features/lobby/SpaceHierarchy.tsx` | Lobby hierarchy integration |
## Data model
| Event | State key | Content |
|-------|-----------|---------|
| `im.paarrot.sub_rooms` | `""` (empty) | `{ children: string[] }` — room IDs in display order |
Power levels: treat like other state events; editors need `state_default` or explicit level for `im.paarrot.sub_rooms`.
## Dependencies
- `matrix-js-sdk` state events
- Parent room must exist and user must have permission to send state
## Integration points
- **Home navigation**: primary consumer of tree flattening
- **Lobby/forums**: `SpaceHierarchy` reads sub-rooms for display
- **Create room flow**: auto-updates parent on sub-room create
- Distinct from **m.space.child** space hierarchy (may coexist)
## Testing
### Manual
1. In a room where you have admin, add sub-room from nav menu.
2. Verify child appears indented under parent in Home.
3. Reorder in Room settings → Sub-rooms.
4. Leave child room; verify unjoined state UI.
5. Check lobby view shows hierarchy when applicable.
### Automated
- None
## Known issues & gotchas
- Only **joined** sub-rooms appear in home tree; unjoined listed separately
- Top-level filter: rooms that appear in any parent's `children` are excluded from root list
- Creating sub-room sends state event on **parent** — failure leaves orphan room
- `StateEvent.PaarrotSubRooms as any` casts in sendStateEvent — typing debt
## Future work
- Sync with `m.space.child` for federation-friendly hierarchy
- Drag-and-drop reorder in nav (DnD exists in lobby, not necessarily sub-rooms settings)
- Depth limits / circular reference guards
## Related docs
- [lobby-forums HANDOFF](../lobby-forums/HANDOFF.md)
## Add your extra things here
- Interface: `PaarrotSubRoomsContent { children: string[] }` in `useRoomSubRooms.ts`
- Permission check pattern: `permissions.stateEvent(StateEvent.PaarrotSubRooms, userId)`

View File

@@ -0,0 +1,89 @@
# Themes & appearance — handoff
## Summary
Theming covers light/dark/system themes, custom CSS variables, font choices, page zoom, message spacing, and view transitions. Themes can be extended by plugins.
## User-facing behavior
- Theme toggle in settings (light / dark / system)
- Page zoom slider (General settings)
- Message spacing density
- View transitions on navigation (where supported)
- Plugin-registered custom themes
## Architecture
```
ThemeManager.tsx — auth vs client theme application
settings.ts — theme, zoom, spacing in settingsAtom
useTheme.ts — read/apply theme
ClientNonUIFeatures — PageZoomFeature, EmojiStyleFeature
vanilla-extract / folds tokens in colors.css.ts
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/pages/ThemeManager.tsx` | Route-level theme |
| `cinny/src/app/state/settings.ts` | Settings including theme |
| `cinny/src/app/hooks/useTheme.ts` | Theme hook |
| `cinny/src/colors.css.ts` | Color tokens |
| `cinny/src/app/pages/client/ClientNonUIFeatures.tsx` | Page zoom CSS |
| `cinny/src/app/hooks/useViewTransitions.ts` | View transitions |
| `cinny/src/app/hooks/useMessageSpacing.ts` | Message density |
| `cinny/public/themes/` | Bundled theme files (if present) |
## Data model
| Key | Storage |
|-----|---------|
| `settingsAtom` | theme, pageZoom, messageSpacing, emojiStyle |
## Dependencies
- `folds` design system
- `@vanilla-extract/css` for component styles
- CSS `prefers-color-scheme` for system theme
## Integration points
- **plugins**: `registerTheme` API
- **user-settings**: General tab controls
- **forum**: `forumTheme.css.ts` forum-specific styling
## Testing
### Manual
1. Switch dark/light; all pages consistent.
2. System theme follows OS.
3. Page zoom 80150%; layout usable.
4. Plugin theme applies without breaking contrast.
### Automated
- None
## Known issues & gotchas
- Separate `AuthRouteThemeManager` and client theme — auth pages may differ
- Zoom uses `font-size` on `documentElement` — may affect rem layout globally
- Forum theme partially separate from global tokens
## Future work
- High contrast accessibility theme
- Per-room wallpaper from avatar metadata banner
## Related docs
- [user-settings HANDOFF](../user-settings/HANDOFF.md)
- [plugins HANDOFF](../plugins/HANDOFF.md)
- [avatar-metadata HANDOFF](../avatar-metadata/HANDOFF.md)
## Add your extra things here
- `EmojiStyle` enum affects `--font-emoji` CSS variable
- `useViewTransitions` tied to browser View Transitions API support

View File

@@ -0,0 +1,94 @@
# User settings — handoff
## Summary
Global user settings modal: general preferences, account profile, notifications, audio devices, sessions/devices, emojis, plugins, developer tools, and about. Accessed from sidebar Settings tab.
## User-facing behavior
- **General**: theme, language, page zoom, emoji style, message spacing
- **Account**: display name, avatar (with Paarrot metadata), matrix id
- **Notifications**: push rules, keywords, system permission
- **Audio**: input/output device selection for calls
- **Devices**: verify sessions, key backup, cross-signing
- **Plugins**: see plugins handoff
- **About**: version info
## Architecture
```
Settings.tsx — tabbed modal
→ general/, account/, notifications/, audio/, devices/, emojis-stickers/, plugins/, developer-tools/, about/
SettingsPages enum
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/settings/Settings.tsx` | Settings modal shell |
| `cinny/src/app/features/settings/general/General.tsx` | General prefs |
| `cinny/src/app/features/settings/account/Profile.tsx` | Profile + avatar metadata |
| `cinny/src/app/features/settings/notifications/` | Notification rules |
| `cinny/src/app/features/settings/audio/Audio.tsx` | Call audio devices |
| `cinny/src/app/features/settings/devices/` | Sessions & verification |
| `cinny/src/app/features/settings/about/About.tsx` | About page |
| `cinny/src/app/state/settings.ts` | Settings atom |
| `cinny/src/app/pages/client/sidebar/SettingsTab.tsx` | Entry point |
## Data model
| Key | Storage |
|-----|---------|
| `settingsAtom` | localStorage (theme, zoom, emoji style, etc.) |
| Profile | Matrix account data + avatar MXC |
| Push rules | Homeserver account data |
## Dependencies
- `folds` modal/page components
- Notification API, MediaDevices API
## Integration points
- **avatar-metadata**: profile color in avatar upload
- **emoji-stickers**: emojis tab
- **plugins**: plugins tab
- **e2e-and-devices**: devices tab overlaps
- **background-notifications**: notification settings
- **voice-calls**: audio device prefs
## Testing
### Manual
1. Change theme; persists reload.
2. Page zoom 125%; UI scales.
3. Notification keyword add; trigger match.
4. Audio device switch during call.
### Automated
- None
## Known issues & gotchas
- Settings modal vs mobile full-page — `useScreenSizeContext`
- Logout dialog from settings clears session
## Future work
- Settings search
- Export/import settings JSON
## Related docs
- [avatar-metadata HANDOFF](../avatar-metadata/HANDOFF.md)
- [emoji-stickers HANDOFF](../emoji-stickers/HANDOFF.md)
- [plugins HANDOFF](../plugins/HANDOFF.md)
- [e2e-and-devices HANDOFF](../e2e-and-devices/HANDOFF.md)
- [themes-and-appearance HANDOFF](../themes-and-appearance/HANDOFF.md)
## Add your extra things here
- `SettingsPages` enum lists all tabs — add new pages here + menu item

View File

@@ -0,0 +1,97 @@
# Voice & video calls — handoff
## Summary
Paarrot supports Matrix RTC voice/video calls via MSC3401 (`org.matrix.msc3401.call.member`) and LiveKit. Calls can be docked in the sidebar, support screen share, remote cursors, mute/deafen, and are wired to the local HTTP API for external control.
## User-facing behavior
- Join/start calls from room UI (when homeserver exposes RTC/LiveKit via `.well-known`)
- Docked call panel in sidebar; full overlay mode
- Mute, deafen, camera, screen share controls
- Call participants shown in room nav (`CallParticipantsIndicator`)
## Architecture
```
useCallService (CallProvider)
→ CallService (LiveKit room, Matrix call member state)
→ useRtcConfig: .well-known m.rtc + LiveKit focus
→ UI: CallOverlay, DockedCallPanel, SidebarDockedCallPanel
→ getCallService() global ref for Electron API (paarrot-api.ts)
```
## Key files
| Path | Role |
|------|------|
| `cinny/src/app/features/call/useCall.ts` | React context, global CallService ref |
| `cinny/src/app/features/call/CallService.ts` | Core call logic |
| `cinny/src/app/features/call/types.ts` | CallState, ActiveCall, events |
| `cinny/src/app/features/call/useRtcConfig.ts` | Homeserver RTC / LiveKit discovery |
| `cinny/src/app/features/call/CallOverlay.tsx` | Full-screen call UI |
| `cinny/src/app/features/call/DockedCallPanel.tsx` | Docked UI |
| `cinny/src/app/features/call/SidebarDockedCallPanel.tsx` | Sidebar variant |
| `cinny/src/app/features/call/useRemoteCursor.ts` | Screen-share cursor sync |
| `cinny/src/app/features/call/CallSounds.ts` | Join/leave sounds |
| `cinny/src/app/features/room-nav/CallParticipantsIndicator.tsx` | Nav badge |
| `cinny/src/types/matrix/room.ts` | `StateEvent.CallMember` |
## Data model
| Key | Scope | Notes |
|-----|-------|-------|
| `org.matrix.msc3401.call.member` | Room state | Call membership |
| LiveKit JWT / room | Ephemeral | From homeserver RTC focus |
Call mute/deafen/video state lives in `CallService` memory, not Matrix account data.
## Dependencies
- `livekit-client` (verify in package.json)
- Homeserver `.well-known` RTC configuration
- Browser/Electron media permissions (mic, camera, screen capture)
- Electron `desktopCapturer` for screen share (main process)
## Integration points
- **local-api**: mute/deafen/status endpoints
- **Stream Deck**: uses call mute/deafen via API
- **Room nav**: participant indicators
- **Electron main**: screen capture IPC if applicable
## Testing
### Manual
1. Use a homeserver with Matrix RTC + LiveKit configured.
2. Start voice call in a room; verify audio both ways.
3. Toggle mute/deafen; verify API `/status` reflects state.
4. Screen share; verify remote cursor if enabled.
5. Dock/undock call panel.
### Automated
- None identified
## Known issues & gotchas
- `callSupported` false until `.well-known` RTC fetch completes — UI should hide call buttons when unsupported
- `getCallService()` must be set before API mute actions work
- Platform differences: screen capture APIs differ Electron vs browser vs Android
## Future work
- Call recording, noise suppression settings
- Better reconnection / call transfer
- MSC3401 spec tracking as it stabilizes
## Related docs
- [local-api HANDOFF](../local-api/HANDOFF.md)
- Matrix MSC3401 (upstream spec)
## Add your extra things here
- `fetchWellKnownWithRTC`, `getLiveKitFocus`, `getLiveKitHomeserverPriority` in `useRtcConfig.ts`
- Global singleton: `globalCallServiceInstance` in `useCall.ts` for non-React consumers

9954
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
"node": ">=16.0.0" "node": ">=16.0.0"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite --open false",
"build": "vite build", "build": "vite build",
"lint": "yarn check:eslint && yarn check:prettier", "lint": "yarn check:eslint && yarn check:prettier",
"check:eslint": "eslint src/*", "check:eslint": "eslint src/*",
@@ -20,106 +20,107 @@
"author": "Ajay Bura", "author": "Ajay Bura",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "1.1.6", "@atlaskit/pragmatic-drag-and-drop": "2.0.1",
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0", "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "3.0.0",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "2.0.0",
"@fontsource/inter": "4.5.14", "@fontsource-variable/inter": "5.2.8",
"@fontsource/inter": "5.2.8",
"@paarrot/plugin-manager": "git+http://synbox.ruv.wtf:8418/litruv/plugin-manager.git", "@paarrot/plugin-manager": "git+http://synbox.ruv.wtf:8418/litruv/plugin-manager.git",
"@tanstack/react-query": "5.24.1", "@tanstack/react-query": "5.101.2",
"@tanstack/react-query-devtools": "5.24.1", "@tanstack/react-query-devtools": "5.101.2",
"@tanstack/react-virtual": "3.2.0", "@tanstack/react-virtual": "3.14.5",
"@tauri-apps/api": "2.9.1", "@tauri-apps/api": "2.11.1",
"@tauri-apps/plugin-dialog": "2.6.0", "@tauri-apps/plugin-dialog": "2.7.1",
"@tauri-apps/plugin-fs": "2.4.5", "@tauri-apps/plugin-fs": "2.5.1",
"@tauri-apps/plugin-http": "2.5.7", "@tauri-apps/plugin-http": "2.5.9",
"@tauri-apps/plugin-notification": "2.3.3", "@tauri-apps/plugin-notification": "2.3.3",
"@tauri-apps/plugin-opener": "2", "@tauri-apps/plugin-opener": "2",
"@tauri-apps/plugin-process": "2.3.1", "@tauri-apps/plugin-process": "2.3.1",
"@tauri-apps/plugin-updater": "2.10.0", "@tauri-apps/plugin-updater": "2.10.1",
"@types/pako": "2.0.4", "@types/pako": "2.0.4",
"@vanilla-extract/css": "1.9.3", "@vanilla-extract/css": "1.21.1",
"@vanilla-extract/recipes": "0.3.0", "@vanilla-extract/recipes": "0.5.7",
"@vanilla-extract/vite-plugin": "3.7.1", "@vanilla-extract/vite-plugin": "5.2.4",
"await-to-js": "3.0.0", "await-to-js": "3.0.0",
"badwords-list": "2.0.1-4", "badwords-list": "2.0.1-4",
"blurhash": "2.0.4", "blurhash": "2.0.5",
"browser-encrypt-attachment": "0.3.0", "browser-encrypt-attachment": "0.3.0",
"chroma-js": "3.1.2", "chroma-js": "3.2.0",
"classnames": "2.3.2", "classnames": "2.5.1",
"dateformat": "5.0.3", "dateformat": "5.0.3",
"dayjs": "1.11.10", "dayjs": "1.11.21",
"domhandler": "5.0.3", "domhandler": "6.0.1",
"emojibase": "15.3.1", "emojibase": "17.0.0",
"emojibase-data": "15.3.2", "emojibase-data": "17.0.0",
"file-saver": "2.0.5", "file-saver": "2.0.5",
"focus-trap-react": "10.0.2", "focus-trap-react": "12.0.3",
"folds": "2.4.0", "folds": "2.7.0",
"gif.js": "0.2.0", "gif.js": "0.2.0",
"html-dom-parser": "4.0.0", "html-dom-parser": "8.0.0",
"html-react-parser": "4.2.0", "html-react-parser": "6.1.4",
"i18next": "23.12.2", "i18next": "26.3.5",
"i18next-browser-languagedetector": "8.0.0", "i18next-browser-languagedetector": "8.2.1",
"i18next-http-backend": "2.5.2", "i18next-http-backend": "4.0.0",
"immer": "9.0.16", "immer": "11.1.11",
"is-hotkey": "0.2.0", "is-hotkey": "0.2.0",
"jotai": "2.6.0", "jotai": "2.20.1",
"linkify-react": "4.1.3", "linkify-react": "4.3.3",
"linkifyjs": "4.1.3", "linkifyjs": "4.3.3",
"livekit-client": "2.17.0", "livekit-client": "2.20.0",
"lottie-web": "5.13.0", "lottie-web": "5.13.0",
"lucide-react": "0.511.0", "lucide-react": "1.23.0",
"matrix-js-sdk": "38.2.0", "matrix-js-sdk": "41.9.0",
"millify": "6.1.0", "millify": "6.1.0",
"pako": "2.1.0", "pako": "3.0.1",
"pdfjs-dist": "4.2.67", "pdfjs-dist": "6.1.200",
"prismjs": "1.30.0", "prismjs": "1.30.0",
"react": "18.2.0", "react": "19.2.7",
"react-aria": "3.29.1", "react-aria": "3.50.0",
"react-blurhash": "0.2.0", "react-blurhash": "0.3.0",
"react-colorful": "5.6.1", "react-colorful": "5.7.0",
"react-dom": "18.2.0", "react-dom": "19.2.7",
"react-error-boundary": "4.0.13", "react-error-boundary": "6.1.2",
"react-google-recaptcha": "2.1.0", "react-google-recaptcha": "3.1.0",
"react-i18next": "15.0.0", "react-i18next": "17.0.8",
"react-range": "1.8.14", "react-range": "1.10.0",
"react-router-dom": "6.20.0", "react-router-dom": "7.18.1",
"sanitize-html": "2.12.1", "sanitize-html": "2.17.5",
"slate": "0.112.0", "slate": "0.124.1",
"slate-dom": "0.112.2", "slate-dom": "0.124.1",
"slate-history": "0.110.3", "slate-history": "0.113.1",
"slate-react": "0.112.1", "slate-react": "0.125.1",
"ua-parser-js": "1.0.35" "ua-parser-js": "2.0.10"
}, },
"devDependencies": { "devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@rollup/plugin-inject": "5.0.3", "@rollup/plugin-inject": "5.0.5",
"@rollup/plugin-wasm": "6.1.1", "@rollup/plugin-wasm": "6.2.2",
"@types/chroma-js": "3.1.1", "@types/chroma-js": "3.1.2",
"@types/file-saver": "2.0.5", "@types/file-saver": "2.0.7",
"@types/is-hotkey": "0.1.10", "@types/is-hotkey": "0.1.10",
"@types/node": "18.11.18", "@types/node": "26.1.1",
"@types/prismjs": "1.26.0", "@types/prismjs": "1.26.6",
"@types/react": "18.2.39", "@types/react": "19.2.17",
"@types/react-dom": "18.2.17", "@types/react-dom": "19.2.3",
"@types/react-google-recaptcha": "2.1.8", "@types/react-google-recaptcha": "2.1.9",
"@types/sanitize-html": "2.9.0", "@types/sanitize-html": "2.16.1",
"@types/ua-parser-js": "0.7.36", "@types/ua-parser-js": "0.7.39",
"@typescript-eslint/eslint-plugin": "5.46.1", "@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "5.46.1", "@typescript-eslint/parser": "8.63.0",
"@vitejs/plugin-react": "4.2.0", "@vitejs/plugin-react": "6.0.3",
"buffer": "6.0.3", "buffer": "6.0.3",
"eslint": "8.29.0", "eslint": "10.6.0",
"eslint-config-airbnb": "19.0.4", "eslint-config-airbnb": "19.0.4",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.32.0",
"eslint-plugin-jsx-a11y": "6.6.1", "eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react": "7.31.11", "eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-hooks": "7.1.1",
"prettier": "2.8.1", "prettier": "3.9.4",
"typescript": "4.9.4", "typescript": "6.0.3",
"vite": "5.4.19", "vite": "8.1.3",
"vite-plugin-pwa": "0.20.5", "vite-plugin-pwa": "1.3.0",
"vite-plugin-static-copy": "1.0.4", "vite-plugin-static-copy": "4.1.1",
"vite-plugin-top-level-await": "1.4.4" "vite-plugin-top-level-await": "1.6.0"
} }
} }

View File

@@ -1,20 +1,20 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { css } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
const transitionContainer = css({ const transitionContainer = style({
position: 'relative', position: 'relative',
width: '100%', width: '100%',
height: '100%', height: '100%',
overflow: 'hidden', overflow: 'hidden',
}); });
const transitionContent = css({ const transitionContent = style({
width: '100%', width: '100%',
height: '100%', height: '100%',
}); });
const animateIn = css({ const animateIn = style({
animation: 'routeFadeSlideIn 0.25s cubic-bezier(0.4, 0, 0.2, 1)', animation: 'routeFadeSlideIn 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
'@keyframes': { '@keyframes': {
routeFadeSlideIn: { routeFadeSlideIn: {
@@ -30,7 +30,7 @@ const animateIn = css({
}, },
}); });
const animateOut = css({ const animateOut = style({
animation: 'routeFadeSlideOut 0.2s cubic-bezier(0.4, 0, 0.2, 1) forwards', animation: 'routeFadeSlideOut 0.2s cubic-bezier(0.4, 0, 0.2, 1) forwards',
'@keyframes': { '@keyframes': {
routeFadeSlideOut: { routeFadeSlideOut: {

View File

@@ -2,7 +2,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Badge, Box, Button, Chip, config, Menu, Spinner, Text } from 'folds'; import { Badge, Box, Button, Chip, config, Menu, Spinner, Text } from 'folds';
import { Icon, Icons } from '../../../components/icons'; import { Icon, Icons } from '../../../components/icons';
import produce from 'immer'; import { produce } from 'immer';
import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css'; import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile'; import { SettingTile } from '../../../components/setting-tile';

View File

@@ -7,7 +7,7 @@ import { useNavigate } from 'react-router-dom';
import { JoinRule, RestrictedAllowType, Room } from 'matrix-js-sdk'; import { JoinRule, RestrictedAllowType, Room } from 'matrix-js-sdk';
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types'; import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces'; import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
import produce from 'immer'; import { produce } from 'immer';
import { useSpace } from '../../hooks/useSpace'; import { useSpace } from '../../hooks/useSpace';
import { Page, PageContent, PageContentCenter, PageHeroSection } from '../../components/page'; import { Page, PageContent, PageContentCenter, PageHeroSection } from '../../components/page';
import { import {

View File

@@ -1,6 +1,6 @@
import { MatrixEvent, Room } from 'matrix-js-sdk'; import { MatrixEvent, Room } from 'matrix-js-sdk';
import { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useMemo, useState } from 'react';
import produce from 'immer'; import { produce } from 'immer';
import { useStateEvent } from './useStateEvent'; import { useStateEvent } from './useStateEvent';
import { StateEvent } from '../../types/matrix/room'; import { StateEvent } from '../../types/matrix/room';
import { useStateEventCallback } from './useStateEventCallback'; import { useStateEventCallback } from './useStateEventCallback';

View File

@@ -143,13 +143,11 @@ const useBackgroundSync = (mx?: MatrixClient) => {
type ClientRootProps = { type ClientRootProps = {
children: ReactNode; children: ReactNode;
}; };
const MAX_SYNC_RETRIES = 3;
export function ClientRoot({ children }: ClientRootProps) { export function ClientRoot({ children }: ClientRootProps) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [syncError, setSyncError] = useState<Error | null>(null);
const syncRetryCount = useRef(0);
const mxRef = useRef<MatrixClient | undefined>(undefined); const mxRef = useRef<MatrixClient | undefined>(undefined);
const recoverInFlight = useRef(false);
const { baseUrl } = getCurrentSession() ?? {}; const { baseUrl } = getCurrentSession() ?? {};
// Enable view transitions for smooth navigation // Enable view transitions for smooth navigation
@@ -202,31 +200,77 @@ export function ClientRoot({ children }: ClientRootProps) {
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, [mx, loading]); }, [mx, loading]);
// Soft-recover from sync ERROR (e.g. sleep/wake with no network).
// Never block the UI — SyncStatus / title bar already show "Connection Lost".
useSyncState( useSyncState(
mx, mx,
useCallback((state, prevState, data) => { useCallback((state) => {
const client = mxRef.current; const client = mxRef.current;
if (state === 'PREPARED') { if (state === SyncState.Prepared || state === SyncState.Syncing) {
setLoading(false); setLoading(false);
setSyncError(null); recoverInFlight.current = false;
syncRetryCount.current = 0; return;
} else if (state === 'ERROR') {
if (syncRetryCount.current < MAX_SYNC_RETRIES) {
syncRetryCount.current += 1;
const attempt = syncRetryCount.current;
console.warn(`[ClientRoot] Sync error, auto-retrying (${attempt}/${MAX_SYNC_RETRIES})...`);
setTimeout(() => {
if (client) startClient(client).catch(() => {});
}, 2000 * attempt);
} else {
const error = data instanceof Error ? data : new Error('Sync failed');
console.error('[ClientRoot] Sync error after max retries:', error);
setSyncError(error);
} }
if (state !== SyncState.Error || !client || recoverInFlight.current) {
return;
} }
recoverInFlight.current = true;
console.warn('[ClientRoot] Sync error after sleep/network blip, soft-recovering...');
const recover = () => {
if (!mxRef.current) {
recoverInFlight.current = false;
return;
}
const c = mxRef.current;
if (!c.clientRunning) {
startClient(c)
.catch((err) => console.warn('[ClientRoot] Soft recover startClient failed:', err))
.finally(() => {
recoverInFlight.current = false;
});
return;
}
try {
c.retryImmediately();
} catch (err) {
console.warn('[ClientRoot] Soft recover retryImmediately failed:', err);
}
recoverInFlight.current = false;
};
// Give WiFi a moment to come back after lid open / resume
window.setTimeout(recover, 1500);
}, []) }, [])
); );
// Also nudge sync when the laptop wakes / network returns
useEffect(() => {
if (!mx) return;
const nudge = () => {
if (document.visibilityState !== 'visible') return;
const syncState = mx.getSyncState();
if (syncState === SyncState.Error || syncState === SyncState.Reconnecting || !mx.clientRunning) {
console.log('[ClientRoot] Online/visible again, nudging sync', syncState);
if (!mx.clientRunning) {
startClient(mx).catch(() => {});
} else {
mx.retryImmediately();
}
}
};
window.addEventListener('online', nudge);
document.addEventListener('visibilitychange', nudge);
return () => {
window.removeEventListener('online', nudge);
document.removeEventListener('visibilitychange', nudge);
};
}, [mx]);
return ( return (
<SpecVersions baseUrl={baseUrl!}> <SpecVersions baseUrl={baseUrl!}>
<TitleBar mx={mx} /> <TitleBar mx={mx} />
@@ -251,23 +295,6 @@ export function ClientRoot({ children }: ClientRootProps) {
</Box> </Box>
</SplashScreen> </SplashScreen>
)} )}
{syncError && (
<SplashScreen>
<Box direction="Column" grow="Yes" alignItems="Center" justifyContent="Center" gap="400">
<Dialog>
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
<Text>{`Sync failed: ${syncError.message}`}</Text>
<Text size="T300">Check your internet connection and homeserver status.</Text>
<Button variant="Critical" onClick={() => window.location.reload()}>
<Text as="span" size="B400">
Retry
</Text>
</Button>
</Box>
</Dialog>
</Box>
</SplashScreen>
)}
{loading || !mx ? ( {loading || !mx ? (
<ClientRootLoading /> <ClientRootLoading />
) : ( ) : (

View File

@@ -1,5 +1,5 @@
import { WritableAtom, atom } from 'jotai'; import { WritableAtom, atom } from 'jotai';
import produce from 'immer'; import { produce } from 'immer';
import { import {
atomWithLocalStorage, atomWithLocalStorage,
getLocalStorageItem, getLocalStorageItem,

View File

@@ -1,5 +1,5 @@
import { WritableAtom, atom } from 'jotai'; import { WritableAtom, atom } from 'jotai';
import produce from 'immer'; import { produce } from 'immer';
import { import {
atomWithLocalStorage, atomWithLocalStorage,
getLocalStorageItem, getLocalStorageItem,

View File

@@ -1,5 +1,5 @@
import { WritableAtom, atom } from 'jotai'; import { WritableAtom, atom } from 'jotai';
import produce from 'immer'; import { produce } from 'immer';
import { Path } from 'react-router-dom'; import { Path } from 'react-router-dom';
import { import {
atomWithLocalStorage, atomWithLocalStorage,

View File

@@ -1,5 +1,5 @@
import { WritableAtom, atom } from 'jotai'; import { WritableAtom, atom } from 'jotai';
import produce from 'immer'; import { produce } from 'immer';
import { import {
atomWithLocalStorage, atomWithLocalStorage,
getLocalStorageItem, getLocalStorageItem,

View File

@@ -1,4 +1,4 @@
import produce from 'immer'; import { produce } from 'immer';
import { atom, useSetAtom } from 'jotai'; import { atom, useSetAtom } from 'jotai';
import { import {
ClientEvent, ClientEvent,

View File

@@ -1,4 +1,4 @@
import produce from 'immer'; import { produce } from 'immer';
import { atom, useSetAtom } from 'jotai'; import { atom, useSetAtom } from 'jotai';
import { import {
IRoomTimelineData, IRoomTimelineData,

View File

@@ -1,5 +1,5 @@
import { atom } from 'jotai'; import { atom } from 'jotai';
import produce from 'immer'; import { produce } from 'immer';
import { import {
atomWithLocalStorage, atomWithLocalStorage,
getLocalStorageItem, getLocalStorageItem,

View File

@@ -1,4 +1,4 @@
import produce from 'immer'; import { produce } from 'immer';
import { atom, useSetAtom } from 'jotai'; import { atom, useSetAtom } from 'jotai';
import { MatrixClient, RoomMemberEvent, RoomMemberEventHandlerMap } from 'matrix-js-sdk'; import { MatrixClient, RoomMemberEvent, RoomMemberEventHandlerMap } from 'matrix-js-sdk';
import { useEffect } from 'react'; import { useEffect } from 'react';

View File

@@ -1,7 +1,7 @@
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import lottie from 'lottie-web/build/player/lottie_light'; import lottie from 'lottie-web/build/player/lottie_light';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import pako from 'pako'; import { inflate } from 'pako';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import GIF from 'gif.js'; import GIF from 'gif.js';
@@ -26,7 +26,7 @@ const TRANSPARENT_COLOR = 0xFF00FF;
* @returns The Lottie animation JSON object * @returns The Lottie animation JSON object
*/ */
function decompressTgs(tgsData: ArrayBuffer): object { function decompressTgs(tgsData: ArrayBuffer): object {
const decompressed = pako.inflate(new Uint8Array(tgsData), { to: 'string' }); const decompressed = inflate(new Uint8Array(tgsData), { to: 'string' });
return JSON.parse(decompressed); return JSON.parse(decompressed);
} }

View File

@@ -2,7 +2,7 @@
import React from 'react'; import React from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { enableMapSet } from 'immer'; import { enableMapSet } from 'immer';
import '@fontsource/inter/variable.css'; import '@fontsource-variable/inter';
import 'folds/dist/style.css'; import 'folds/dist/style.css';
import { configClass, varsClass } from 'folds'; import { configClass, varsClass } from 'folds';

View File

@@ -1,8 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"sourceMap": true, "sourceMap": true,
"jsx": "react", "jsx": "react-jsx",
"target": "ES2016", "target": "ES2020",
"module": "ES2020", "module": "ES2020",
"allowJs": true, "allowJs": true,
"strict": true, "strict": true,
@@ -12,7 +12,7 @@
"outDir": "dist", "outDir": "dist",
"rootDir": "./src", "rootDir": "./src",
"skipLibCheck": true, "skipLibCheck": true,
"lib": ["ES2016", "DOM"] "lib": ["ES2020", "DOM"]
}, },
"exclude": ["node_modules", "dist"], "exclude": ["node_modules", "dist"],
"include": ["src"] "include": ["src"]

View File

@@ -258,12 +258,12 @@ export default defineConfig({
}), }),
], ],
optimizeDeps: { optimizeDeps: {
esbuildOptions: { rolldownOptions: {
define: { define: {
global: 'globalThis', global: 'globalThis',
}, },
plugins: [ plugins: [
// Enable esbuild polyfill plugins // Enable polyfill plugins
NodeGlobalsPolyfillPlugin({ NodeGlobalsPolyfillPlugin({
process: false, process: false,
buffer: true, buffer: true,
@@ -272,6 +272,7 @@ export default defineConfig({
}, },
}, },
build: { build: {
target: 'es2022',
outDir: 'dist', outDir: 'dist',
sourcemap: true, sourcemap: true,
copyPublicDir: false, copyPublicDir: false,