Compare commits

...

11 Commits

Author SHA1 Message Date
jared 349194e7e5 fix(ui): folds primitives for RouteError + PiP fullscreen button (native-cinny audit 5/N)
CI / Build & Quality Checks (push) Successful in 10m33s
CI / Trigger Desktop Build (push) Successful in 21s
- RouteError: raw <div>/<h2>/<p>/<button> (sans-serif, raw px) -> folds
  Box/Text/Button with config tokens.
- CallEmbedProvider PiP fullscreen control: raw <button> with ⊡/⛶ glyphs ->
  folds IconButton reusing the exported FullscreenIcon/ExitFullscreenIcon SVGs
  from Controls (consistent with the main fullscreen button). The intentional
  dark over-video scrim is kept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:32:19 -04:00
jared 24d6460e4c chore: remove Sentry.io entirely
We no longer use Sentry. Removed:
- @sentry/react + @sentry/vite-plugin (package.json + lockfile)
- Sentry.init in index.tsx and the VITE_SENTRY_DSN env (.env.production)
- @sentry/vite-plugin + the SENTRY_AUTH_TOKEN sourcemap-upload path in
  vite.config.js (sourcemap now always false) and the CI env var
- Sentry.ErrorBoundary in App.tsx -> react-error-boundary's ErrorBoundary with a
  folds-native fallback (Box/Text/Button + config tokens), which also resolves
  the native-cinny audit's raw-#hex/#5865f2 fallback finding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:21:09 -04:00
jared 127e783f66 fix(ui): toast cards render on stock themes; gate TDS glow (native-cinny audit 4/N)
LotusToastContainer was styled entirely with --lt-* CSS vars but rendered
unconditionally (not gated on lotusTerminal). Those vars only exist inside the
Lotus Terminal theme's scoped block with no global fallback, so in-app toast
notifications rendered with undefined background/border/colors on every stock
Cinny theme. Now the card uses folds tokens (color.Surface.*/Primary.*,
config.radii/space/borderWidth, color.Other.Shadow) by default, keeping the TDS
--lt-* glow/accents only when lotusTerminal is active. The raw <button> dismiss
control is now a folds IconButton.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:06:33 -04:00
jared 198fd12bb2 fix(ui): folds tokens for ML-denoise panel + screenshare popover (native-cinny audit 3/N)
- General ML noise-suppression panel: ungated --border-color/--bg-card/--bg-input/
  --accent-orange -> color.Surface.ContainerLine/Container, SurfaceVariant.Container,
  Primary.Main. (The lotusTerminal-gated Boot button keeps its TDS --accent-orange.)
- CallControls "Share your screen?" popover: --bg-surface/--bg-surface-border ->
  color.Surface.Container / ContainerLine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:01:17 -04:00
jared 34d5209165 fix(ui): folds tokens for settings/profile/glass invented vars (native-cinny audit 2/N)
- DenoiseTester: --bg-card/--border-color/--accent-green/--accent-orange -> color.Surface.*/Success/Primary
- ProfileDecoration: --accent-cyan/--bg-surface-variant -> color.Primary.Main/SurfaceVariant.Container
- Profile: --tc-critical/warning-normal -> color.Critical/Warning.Main
- UserRoomProfile: --tc-positive/warning-normal/--tc-surface-low-contrast/--bg-surface-variant -> color tokens
- Sidebar glass: hardcoded rgba bg/border -> color-mix on color.Surface.Container + SurfaceVariant.ContainerLine
  (also fixes the glass looking wrong on light themes — was always near-black)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:58:57 -04:00
jared 9684ab75bb fix(ui): replace ungated invented CSS vars with folds tokens (native-cinny audit 1/N)
Audit of our delta vs Cinny v4.12.3 found invented CSS vars (--tc-*, --bg-*)
used outside Lotus-Terminal-gated code, which render unstyled/wrong on stock
themes. Batch 1:
- MemberVerificationBadge: --tc-positive/warning-normal -> color.Success/Warning.Main
- RoomInput (gif/location errors): --tc-danger-normal -> color.Critical.Main
- MsgTypeRenderers (location iframe): --bg-surface-border -> color.SurfaceVariant.ContainerLine
- MessageSearch (cached-room row): --bg-surface-variant -> color.SurfaceVariant.Container
- PrescreenControls (mic-denied): --tc-critical-high -> color.Critical.Main

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:54:42 -04:00
jared 0a6b035a67 docs(readme): correct fork-sync version (v4.12.3) and logo path
Two stale facts in README.md: it said "Forked from Cinny v4.12.1" (we've since
synced through v4.12.3) and referenced the logo as lotus_chat.png (the file is
public/res/Lotus.png). CONTRIBUTING.md is intentionally left as upstream
Cinny's and is not modified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:32:49 -04:00
jared cbfd3e5632 docs: N108 -> Needs-Verification; add L2 maskable-icon test
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:27:57 -04:00
jared 3faf0866a0 feat(pwa): add maskable icons for Android adaptive icons (N108)
The manifest had no purpose:"maskable" icon, so Android cropped a non-safe-zoned
icon (corners clipped / inconsistent shape). Added 192px + 512px maskable icons
(logo centered at ~62% on the app background_color #0a0a0a, inside the 80% safe
zone) generated from the existing logo, and registered them with
purpose:"maskable". They sit beside the existing android-chrome icons and use
the same manifest path convention, so they resolve identically to those.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:25:06 -04:00
jared bab3a160c2 docs: move N95 to Needs-Verification (fixed); update L1 test to verify-mode
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:18:08 -04:00
jared 1778cd0009 fix(calls): release AFK-monitor mic capture when muted (N95)
useAfkAutoMute opened its own getUserMedia capture for the whole call and only
stopped it on unmount, so the OS recording indicator stayed lit even when the
user was muted. The capture is now gated on the reactive mic-on state: it runs
only while unmuted (there's nothing to auto-mute when already muted), so muting
tears down the stream and clears the indicator, and unmuting re-acquires it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:15:31 -04:00
30 changed files with 230 additions and 741 deletions
-1
View File
@@ -1,2 +1 @@
VITE_SENTRY_DSN=https://264a5e95c5d31fe080a2e92fb008294d@o4511430568378368.ingest.us.sentry.io/4511430571982849
VITE_APP_VERSION=lotus VITE_APP_VERSION=lotus
-1
View File
@@ -45,7 +45,6 @@ jobs:
run: npm run build run: npm run build
env: env:
NODE_OPTIONS: '--max_old_space_size=4096' NODE_OPTIONS: '--max_old_space_size=4096'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
VITE_APP_VERSION: ${{ github.sha }} VITE_APP_VERSION: ${{ github.sha }}
# ── Quality checks (informational — pre-existing issues exist) ─────── # ── Quality checks (informational — pre-existing issues exist) ───────
+3 -3
View File
@@ -28,6 +28,8 @@ Implemented and gate-green; confirm each per `LOTUS_TESTING.md`, then delete the
| #10 | Composer not hidden by keyboard (`100dvh`) | `src/index.css` | E4 | | #10 | Composer not hidden by keyboard (`100dvh`) | `src/index.css` | E4 |
| #12 | PiP mute badge attribution (you vs. all-muted) | `CallEmbedProvider.tsx` | G1 | | #12 | PiP mute badge attribution (you vs. all-muted) | `CallEmbedProvider.tsx` | G1 |
| N96 | Call-recovery overlay single "Back" button | `call/CallView.tsx` | A7 | | N96 | Call-recovery overlay single "Back" button | `call/CallView.tsx` | A7 |
| N95 | AFK-monitor mic released on mute (OS indicator clears) | `hooks/useAfkAutoMute.ts` | L1 |
| N108 | Maskable PWA icons (Android adaptive) | `public/manifest.json` + `res/android/maskable-*` | L2 |
| EC | EC iframe load watchdog + self-heal + recovery UI | `plugins/call/CallEmbed.ts`, `CallView.tsx` | A7 | | EC | EC iframe load watchdog + self-heal + recovery UI | `plugins/call/CallEmbed.ts`, `CallView.tsx` | A7 |
| Gal | MediaGallery lazy-decrypt (true virtualization deferred) | `room/MediaGallery.tsx` | H1 | | Gal | MediaGallery lazy-decrypt (true virtualization deferred) | `room/MediaGallery.tsx` | H1 |
| a11y | aria-labels: edit-history / reaction / thread / reply | `message/*` (`FallbackContent`, `Reaction`, `Reply`) | I | | a11y | aria-labels: edit-history / reaction / thread / reply | `message/*` (`FallbackContent`, `Reaction`, `Reply`) | I |
@@ -38,7 +40,6 @@ Implemented and gate-green; confirm each per `LOTUS_TESTING.md`, then delete the
### Calls / Audio ### Calls / Audio
- **N95 — AFK auto-mute keeps the hardware mic active while muted.** `useAfkAutoMute.ts` holds its own `getUserMedia` stream independent of EC's; muting in the UI doesn't stop those tracks, so the OS recording indicator stays lit. Fix: stop the `MediaStream` tracks on mute, re-request on unmute. (Repro: `LOTUS_TESTING.md` L1.)
- **N127 — ML denoise shim is never injected in `vite dev`.** The `lotusDenoise` plugin injects only on `closeBundle` (build), so ML noise suppression is silently inactive during local dev. Add a dev-mode injection (`configureServer` / `transformIndexHtml`). Dev-only impact. - **N127 — ML denoise shim is never injected in `vite dev`.** The `lotusDenoise` plugin injects only on `closeBundle` (build), so ML noise suppression is silently inactive during local dev. Add a dev-mode injection (`configureServer` / `transformIndexHtml`). Dev-only impact.
### Security & Privacy ### Security & Privacy
@@ -51,7 +52,6 @@ Implemented and gate-green; confirm each per `LOTUS_TESTING.md`, then delete the
- **N105 — Service worker has no `notificationclick` handler** — notification clicks are broken when the tab is closed. Needs `showNotification()` via the SW + a `notificationclick` listener. - **N105 — Service worker has no `notificationclick` handler** — notification clicks are broken when the tab is closed. Needs `showNotification()` via the SW + a `notificationclick` listener.
- **N107 — SW has no `push` handler** — Web Push delivery is entirely non-functional. Needs a `push` listener + a Matrix push-gateway integration. - **N107 — SW has no `push` handler** — Web Push delivery is entirely non-functional. Needs a `push` listener + a Matrix push-gateway integration.
- **N108 — No maskable PWA icon** — Android adaptive icons render incorrectly. Needs a maskable icon asset + `purpose: "maskable"` manifest entry.
- **No app-asset caching strategy** (`src/sw.ts`) — no offline capability. - **No app-asset caching strategy** (`src/sw.ts`) — no offline capability.
- **`manifest: false`** in `vite.config.js` — may block correct PWA install if not handled externally. - **`manifest: false`** in `vite.config.js` — may block correct PWA install if not handled externally.
@@ -68,7 +68,7 @@ Implemented and gate-green; confirm each per `LOTUS_TESTING.md`, then delete the
- **Hardcoded CDN URL** should move to an env var (the decoration CDN is now single-sourced in `avatarDecorations.ts`, but the literal is still in-repo). - **Hardcoded CDN URL** should move to an env var (the decoration CDN is now single-sourced in `avatarDecorations.ts`, but the literal is still in-repo).
- **`patch-folds.mjs` edits `node_modules` directly** — consider `patch-package`. - **`patch-folds.mjs` edits `node_modules` directly** — consider `patch-package`.
- **Infra docs:** `contrib/nginx` lacks security headers (HSTS/CSP) + uses rewrites over `try_files`; `contrib/caddy` has a placeholder path. CI/CD (`prod-deploy.yml`): sequential deploy, aggressive 1-min Netlify timeout, `package-manager-cache: false`. - **Infra docs:** `contrib/nginx` lacks security headers (HSTS/CSP) + uses rewrites over `try_files`; `contrib/caddy` has a placeholder path. CI/CD (`prod-deploy.yml`): sequential deploy, aggressive 1-min Netlify timeout, `package-manager-cache: false`.
- **README / CONTRIBUTING:** stale upstream bug-tracker/donations/CLA links; README↔CONTRIBUTING misalignment. - **README:** keep the fork-sync version + logo path current. (`CONTRIBUTING.md` is intentionally left as upstream Cinny's — not a Lotus concern.)
- **Architecture notes (low priority):** deep `features/` + `hooks/` nesting, many small coupled hooks, possible dead CSS/components, `SpacingVariant` / `DropTarget` recipe simplification. - **Architecture notes (low priority):** deep `features/` + `hooks/` nesting, many small coupled hooks, possible dead CSS/components, `SpacingVariant` / `DropTarget` recipe simplification.
- **Git workflow (forward-looking):** keep commits scoped — past monolithic "fix all bugs" commits and inconsistent prefixes hurt `git bisect`. - **Git workflow (forward-looking):** keep commits scoped — past monolithic "fix all bugs" commits and inconsistent prefixes hurt `git bisect`.
+12 -9
View File
@@ -342,22 +342,25 @@ Trigger a desktop/browser notification for a new message.
--- ---
## L. Open bugs flagged by audit — reproduction needed before fix ## L. Fixed — verify
### L1. AFK auto-mute keeps the OS microphone indicator lit (N95) — 👥 live call ### L1. AFK auto-mute releases the OS microphone indicator on mute (N95) — 👥 live call
**Context:** `useAfkAutoMute.ts` calls `getUserMedia({ audio: true })` independently of Element Call's managed stream. When you mute in the Lotus UI, the LiveKit mic inside EC's iframe is muted via the widget API — but the separate `MediaStream` held by the AFK hook keeps its tracks running. The OS-level recording indicator (green dot on macOS, mic icon on Windows/Linux) therefore stays lit while your mic is muted. **Context (now FIXED):** `useAfkAutoMute.ts` opened its own `getUserMedia` level-monitor capture for the whole call, so the OS recording indicator (green dot on macOS, mic icon on Windows/Linux) stayed lit even when muted. The capture is now gated on the reactive mic-on state — it runs only while unmuted, so muting releases the stream.
**To reproduce:** **To verify:**
1. Enable **AFK auto-mute** in Settings → Calls and **join a call**. 1. Enable **AFK auto-mute** in Settings → Calls and **join a call**.
2. Manually **mute your mic** using the call controls. 2. Manually **mute your mic** using the call controls → the **OS recording indicator should clear** within ~a second.
3. Check the **OS recording indicator** (macOS: green dot top-right of menu bar; Windows: mic icon in taskbar). 3. **Unmute** → the indicator should re-appear (capture re-acquired).
4. Also confirm AFK still works end-to-end: stay unmuted and silent past the configured timeout → mic auto-mutes with the "muted after inactivity" toast, and the indicator clears.
**Expected (current broken behavior):** the OS recording indicator stays on even though your Lotus mic shows muted. ### L2. Maskable PWA icon (N108) — Android install
**Expected after fix:** the indicator should clear when you mute and re-appear when you unmute.
> **Note:** This is an **open bug** — no fix has been applied yet. Reproduce and confirm the symptom first. The fix involves stopping `MediaStream` tracks on mute and re-requesting `getUserMedia` on unmute (see LOTUS_BUGS.md N95 for full details). Once fixed, re-run this check to verify the indicator clears. 1. On **Android Chrome**, install Lotus Chat as a PWA (Add to Home Screen).
2. Look at the **home-screen icon**.
**Expected:** the icon fills the adaptive-icon shape cleanly (the logo centered with safe-zone padding on the dark background), **not** clipped at the corners or floating in an odd box. Also worth a quick check in Chrome DevTools → Application → Manifest that the two `purpose: maskable` icons load without a 404 (this also validates the manifest's icon paths resolve in production — a pre-existing path convention I couldn't verify statically).
--- ---
+2 -2
View File
@@ -2,7 +2,7 @@
A Matrix chat client built for Lotus Guild — fast, private, and packed with the features you actually want. A Matrix chat client built for Lotus Guild — fast, private, and packed with the features you actually want.
**Deployed at [chat.lotusguild.org](https://chat.lotusguild.org)** &nbsp;|&nbsp; Forked from [Cinny](https://github.com/cinnyapp/cinny) v4.12.1 **Deployed at [chat.lotusguild.org](https://chat.lotusguild.org)** &nbsp;|&nbsp; Forked from [Cinny](https://github.com/cinnyapp/cinny), synced through v4.12.3
--- ---
@@ -10,7 +10,7 @@ A Matrix chat client built for Lotus Guild — fast, private, and packed with th
The source code is licensed under [AGPLv3](LICENSE), the same license as the upstream Cinny project. The source for this fork is public at [code.lotusguild.org/LotusGuild/cinny](https://code.lotusguild.org/LotusGuild/cinny). The source code is licensed under [AGPLv3](LICENSE), the same license as the upstream Cinny project. The source for this fork is public at [code.lotusguild.org/LotusGuild/cinny](https://code.lotusguild.org/LotusGuild/cinny).
The Lotus Chat logo (`lotus_chat.png`) is a derivative work based on the original Cinny logo by Ajay Bura and contributors, used under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). The modified logo is © Lotus Guild and is also made available under CC BY 4.0. The Lotus Chat logo (`public/res/Lotus.png`) is a derivative work based on the original Cinny logo by Ajay Bura and contributors, used under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). The modified logo is © Lotus Guild and is also made available under CC BY 4.0.
--- ---
-496
View File
@@ -21,7 +21,6 @@
"@giphy/js-util": "5.2.0", "@giphy/js-util": "5.2.0",
"@giphy/react-components": "10.1.2", "@giphy/react-components": "10.1.2",
"@sapphi-red/web-noise-suppressor": "0.3.5", "@sapphi-red/web-noise-suppressor": "0.3.5",
"@sentry/react": "10.53.1",
"@tanstack/react-query": "5.100.13", "@tanstack/react-query": "5.100.13",
"@tanstack/react-query-devtools": "5.100.13", "@tanstack/react-query-devtools": "5.100.13",
"@tanstack/react-virtual": "3.13.25", "@tanstack/react-virtual": "3.13.25",
@@ -81,7 +80,6 @@
"@element-hq/element-call-embedded": "0.20.1", "@element-hq/element-call-embedded": "0.20.1",
"@rollup/plugin-inject": "5.0.5", "@rollup/plugin-inject": "5.0.5",
"@rollup/plugin-wasm": "6.2.2", "@rollup/plugin-wasm": "6.2.2",
"@sentry/vite-plugin": "5.3.0",
"@types/chroma-js": "3.1.2", "@types/chroma-js": "3.1.2",
"@types/file-saver": "2.0.7", "@types/file-saver": "2.0.7",
"@types/is-hotkey": "0.1.10", "@types/is-hotkey": "0.1.10",
@@ -3782,403 +3780,6 @@
"integrity": "sha512-jh3+V9yM+zxLriQexoGm0GatoPaJWjs6ypFIbFYwQp+AoUb55eUXrjKtKQyuC5zShzzeAQUl0M5JzqB7SSrsRA==", "integrity": "sha512-jh3+V9yM+zxLriQexoGm0GatoPaJWjs6ypFIbFYwQp+AoUb55eUXrjKtKQyuC5zShzzeAQUl0M5JzqB7SSrsRA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@sentry-internal/browser-utils": {
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.53.1.tgz",
"integrity": "sha512-X4d6y8sBMjmNhcDW4eMBU3ASsNIMz8dqaFkhyIMN/dkYr/yZKnbRZPaVuVUGvHKjnlficPpIH0/HK9KBjrYxPw==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.53.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.53.1.tgz",
"integrity": "sha512-vVpTI/aEYN5d9IgZeYJWMqVaN0+iFgidSrYNAsZTh1US5sJUzF/wrl+68KdpmCtFROrN3jiAn1oPSwL5CKvEJA==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.53.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.53.1.tgz",
"integrity": "sha512-wZNzTBYkgGUPWMuUQv7L64+OJmoCnz7GQNiTrTFK6EVAjJXFBCSsPp/nhif0bLhbk8+0g4xz633uOhpXuQbFdw==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "10.53.1",
"@sentry/core": "10.53.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.53.1.tgz",
"integrity": "sha512-aueLaf/2prExwA76BGU5/bOXCKWqtt6jQXWA6WJQNrmKpPEtZJB4ypnpsou0McXQCF8tur2Y8U0TEkwQP13yJQ==",
"license": "MIT",
"dependencies": {
"@sentry-internal/replay": "10.53.1",
"@sentry/core": "10.53.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/babel-plugin-component-annotate": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-5.3.0.tgz",
"integrity": "sha512-p4q8gn8wcFqZGP/s2MnJCAAd8fTikaU6A0mM97RDHQgStcrYiaS0Sc5zUNfb1V+UOLPuvdEdL6MwyxfzjYJQTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 18"
}
},
"node_modules/@sentry/browser": {
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.53.1.tgz",
"integrity": "sha512-zXF373hzUOGzUOrqd8xb1U3LQi5uYC3mwv+z5OMKUUinQlu30tTWBs7ypy6YTchtix9QlYaHWlayUF8vBZ5UjA==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "10.53.1",
"@sentry-internal/feedback": "10.53.1",
"@sentry-internal/replay": "10.53.1",
"@sentry-internal/replay-canvas": "10.53.1",
"@sentry/core": "10.53.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/bundler-plugin-core": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-5.3.0.tgz",
"integrity": "sha512-L5T60sWdAI3qWwdg3Ptwek/0TY59PERrxyqp4XMUkroayQvGd9r5dIW9Q1kSeXX9iJ442nXbFZKAOyCKV4Z13Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.18.5",
"@sentry/babel-plugin-component-annotate": "5.3.0",
"@sentry/cli": "^2.58.5",
"dotenv": "^16.3.1",
"find-up": "^5.0.0",
"glob": "^13.0.6",
"magic-string": "~0.30.8"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@sentry/bundler-plugin-core/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@sentry/bundler-plugin-core/node_modules/glob": {
"version": "13.0.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"minimatch": "^10.2.2",
"minipass": "^7.1.3",
"path-scurry": "^2.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@sentry/bundler-plugin-core/node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@sentry/bundler-plugin-core/node_modules/minipass": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/@sentry/cli": {
"version": "2.58.6",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.6.tgz",
"integrity": "sha512-baBcNPLLfUi9WuL+Tpri9BFaAdvugZIKelC5X0tt0Zdy+K0K+PCVSrnNmwMWU/HyaF/SEv6b6UHnXIdqanBlcg==",
"dev": true,
"hasInstallScript": true,
"license": "FSL-1.1-MIT",
"dependencies": {
"https-proxy-agent": "^5.0.0",
"node-fetch": "^2.6.7",
"progress": "^2.0.3",
"proxy-from-env": "^1.1.0",
"which": "^2.0.2"
},
"bin": {
"sentry-cli": "bin/sentry-cli"
},
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@sentry/cli-darwin": "2.58.6",
"@sentry/cli-linux-arm": "2.58.6",
"@sentry/cli-linux-arm64": "2.58.6",
"@sentry/cli-linux-i686": "2.58.6",
"@sentry/cli-linux-x64": "2.58.6",
"@sentry/cli-win32-arm64": "2.58.6",
"@sentry/cli-win32-i686": "2.58.6",
"@sentry/cli-win32-x64": "2.58.6"
}
},
"node_modules/@sentry/cli-darwin": {
"version": "2.58.6",
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.6.tgz",
"integrity": "sha512-udAVvcyfNa0R+95GvPz/+43/N3TC0TYKdkQ7D7jhPSzbcMc7l2fxRNN5yB3UpCA5fWFnW4toeaqwDBhb/Wh3LA==",
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-arm": {
"version": "2.58.6",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.6.tgz",
"integrity": "sha512-pD0LAt5PcUzAinBwvDqc66x9+2CabHEv486yP0gRjWO7SakbaxmfVq/EXd8VLq/Tzi39LAu422UYK1lpW3MILw==",
"cpu": [
"arm"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-arm64": {
"version": "2.58.6",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.6.tgz",
"integrity": "sha512-q8mEcNNmeXMy5i+jWT30TVpH7LcP4HD21CD5XRSPAd/a912HF6EpK0ybf/1USO14WOhoXbAGi9txwaWabSe33g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-i686": {
"version": "2.58.6",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.6.tgz",
"integrity": "sha512-q8vNJi1eOV/4vxAFWBsEwLHoSYapaZHIf4j76KJGJXFKTkEbsjCOOsKbwUIBTQQhRgV4DFWh3ryfsPS/que4Kg==",
"cpu": [
"x86",
"ia32"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-x64": {
"version": "2.58.6",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.6.tgz",
"integrity": "sha512-DZu956Mhi3ZRjTBe1WdbGV46ldVbA8d2rgp/fh51GsI25zjBHah4wZnPTSzpc+YqxU6pJpg579B/r3jrIK530Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-arm64": {
"version": "2.58.6",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.6.tgz",
"integrity": "sha512-nj0Ff/kmAB73EPDhR8B4O9r+NUHK5GkPCkGWC+kXVemqAJWL5jcJ5KdxG0l/S0z6RoEoltID8/43/B+TaMlT7A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-i686": {
"version": "2.58.6",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.6.tgz",
"integrity": "sha512-WNZiDzPbgsEMQWq4avsQ391v/xWKJDIWWWo9GYl+N/w5qcYKkoDW7wQG7T9FasI6ENn68phChTOAPXXxbfAdOg==",
"cpu": [
"x86",
"ia32"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-x64": {
"version": "2.58.6",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.6.tgz",
"integrity": "sha512-R35WJ17oF4D2eqI1DR2sQQqr0fjRTt5xoP16WrTu91XM2lndRMFsnjh+/GttbxapLCBNlrjzia99MJ0PZHZpgA==",
"cpu": [
"x64"
],
"dev": true,
"license": "FSL-1.1-MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/core": {
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.53.1.tgz",
"integrity": "sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/react": {
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.53.1.tgz",
"integrity": "sha512-lrwNq5T/zW84l60894TpKHPcvFuc1I/Hnohecc0TfYVpIcYYuw2orCHoU4v4wgkFaJUpegVetbgdOphViyLVjA==",
"license": "MIT",
"dependencies": {
"@sentry/browser": "10.53.1",
"@sentry/core": "10.53.1"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.14.0 || 17.x || 18.x || 19.x"
}
},
"node_modules/@sentry/rollup-plugin": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@sentry/rollup-plugin/-/rollup-plugin-5.3.0.tgz",
"integrity": "sha512-hgPGPYdQJ/G1cGYOxAb7d4z3V+/k/E5/P/5TFPEEBLuIbFFk+JG0CISUDJdzXJjO382Lb99PBJuXGbueBmO79w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sentry/bundler-plugin-core": "5.3.0",
"magic-string": "~0.30.8"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"rollup": ">=3.2.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@sentry/vite-plugin": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-5.3.0.tgz",
"integrity": "sha512-qcoSzo4n2MulVQ70UUPLq6dTleb2a2HwL2wuwvAgWhPChrYTuk6A6mDg6aQb9fairPAwFPiU9PzOANpoDJcz1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sentry/bundler-plugin-core": "5.3.0",
"@sentry/rollup-plugin": "5.3.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@simple-libs/stream-utils": { "node_modules/@simple-libs/stream-utils": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz",
@@ -4893,18 +4494,6 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
} }
}, },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.15.0", "version": "6.15.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
@@ -6634,19 +6223,6 @@
"url": "https://github.com/fb55/domhandler?sponsor=1" "url": "https://github.com/fb55/domhandler?sponsor=1"
} }
}, },
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -8473,19 +8049,6 @@
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://github.com/fb55/entities?sponsor=1"
} }
}, },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dev": true,
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/husky": { "node_modules/husky": {
"version": "9.1.7", "version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
@@ -10599,26 +10162,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true "dev": true
}, },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.19", "version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
@@ -11178,16 +10721,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/prop-types": { "node_modules/prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -11198,13 +10731,6 @@
"react-is": "^16.13.1" "react-is": "^16.13.1"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true,
"license": "MIT"
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -12783,12 +12309,6 @@
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
},
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
@@ -13336,22 +12856,6 @@
"defaults": "^1.0.3" "defaults": "^1.0.3"
} }
}, },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
-2
View File
@@ -45,7 +45,6 @@
"@giphy/js-util": "5.2.0", "@giphy/js-util": "5.2.0",
"@giphy/react-components": "10.1.2", "@giphy/react-components": "10.1.2",
"@sapphi-red/web-noise-suppressor": "0.3.5", "@sapphi-red/web-noise-suppressor": "0.3.5",
"@sentry/react": "10.53.1",
"@tanstack/react-query": "5.100.13", "@tanstack/react-query": "5.100.13",
"@tanstack/react-query-devtools": "5.100.13", "@tanstack/react-query-devtools": "5.100.13",
"@tanstack/react-virtual": "3.13.25", "@tanstack/react-virtual": "3.13.25",
@@ -105,7 +104,6 @@
"@element-hq/element-call-embedded": "0.20.1", "@element-hq/element-call-embedded": "0.20.1",
"@rollup/plugin-inject": "5.0.5", "@rollup/plugin-inject": "5.0.5",
"@rollup/plugin-wasm": "6.2.2", "@rollup/plugin-wasm": "6.2.2",
"@sentry/vite-plugin": "5.3.0",
"@types/chroma-js": "3.1.2", "@types/chroma-js": "3.1.2",
"@types/file-saver": "2.0.7", "@types/file-saver": "2.0.7",
"@types/is-hotkey": "0.1.10", "@types/is-hotkey": "0.1.10",
+12
View File
@@ -54,6 +54,18 @@
"src": "./res/android/android-chrome-512x512.png", "src": "./res/android/android-chrome-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
},
{
"src": "./res/android/maskable-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./res/android/maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
} }
], ],
"categories": ["social", "communication", "productivity"], "categories": ["social", "communication", "productivity"],
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

+9 -12
View File
@@ -9,6 +9,7 @@ import {
config, config,
Dialog, Dialog,
Icon, Icon,
IconButton,
Icons, Icons,
Overlay, Overlay,
OverlayBackdrop, OverlayBackdrop,
@@ -51,6 +52,7 @@ import { mxcUrlToHttp, getMxIdLocalPart } from '../utils/matrix';
import { RoomAvatar, RoomIcon } from './room-avatar'; import { RoomAvatar, RoomIcon } from './room-avatar';
import { useRoomNavigate } from '../hooks/useRoomNavigate'; import { useRoomNavigate } from '../hooks/useRoomNavigate';
import { getChatBg } from '../features/lotus/chatBackground'; import { getChatBg } from '../features/lotus/chatBackground';
import { ExitFullscreenIcon, FullscreenIcon } from '../features/call/Controls';
import { useTheme, ThemeKind } from '../hooks/useTheme'; import { useTheme, ThemeKind } from '../hooks/useTheme';
import { useSetting } from '../state/hooks/settings'; import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings'; import { settingsAtom } from '../state/settings';
@@ -1095,10 +1097,13 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
> >
<div style={{ display: 'flex', gap: config.space.S100, alignItems: 'center' }}> <div style={{ display: 'flex', gap: config.space.S100, alignItems: 'center' }}>
{document.fullscreenEnabled && ( {document.fullscreenEnabled && (
<button <IconButton
type="button" type="button"
size="300"
radii="300"
variant="Surface"
fill="None"
aria-label={pipIsFullscreen ? 'Exit fullscreen' : 'Fullscreen camera'} aria-label={pipIsFullscreen ? 'Exit fullscreen' : 'Fullscreen camera'}
title={pipIsFullscreen ? 'Exit fullscreen' : 'Fullscreen camera'}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handlePipFullscreen(); handlePipFullscreen();
@@ -1107,19 +1112,11 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
// Dark scrim is intentional for legibility over arbitrary video. // Dark scrim is intentional for legibility over arbitrary video.
background: 'rgba(0,0,0,0.65)', background: 'rgba(0,0,0,0.65)',
backdropFilter: 'blur(4px)', backdropFilter: 'blur(4px)',
border: 'none',
borderRadius: config.radii.R300,
padding: `${config.space.S100} ${config.space.S200}`,
color: '#fff', color: '#fff',
fontSize: '13px',
cursor: 'pointer',
lineHeight: 1,
display: 'flex',
alignItems: 'center',
}} }}
> >
{pipIsFullscreen ? '⊡' : '⛶'} {pipIsFullscreen ? <ExitFullscreenIcon /> : <FullscreenIcon />}
</button> </IconButton>
)} )}
<div <div
style={{ style={{
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Icon, Icons, Text, Tooltip, TooltipProvider } from 'folds'; import { color, Icon, Icons, Text, Tooltip, TooltipProvider } from 'folds';
import { useUserVerifiedStatus } from '../hooks/useUserVerifiedStatus'; import { useUserVerifiedStatus } from '../hooks/useUserVerifiedStatus';
type MemberVerificationBadgeProps = { type MemberVerificationBadgeProps = {
@@ -9,8 +9,7 @@ type MemberVerificationBadgeProps = {
export function MemberVerificationBadge({ userId }: MemberVerificationBadgeProps) { export function MemberVerificationBadge({ userId }: MemberVerificationBadgeProps) {
const vs = useUserVerifiedStatus(userId); const vs = useUserVerifiedStatus(userId);
if (vs === 'unknown') return null; if (vs === 'unknown') return null;
const color = const iconColor = vs === 'verified' ? color.Success.Main : color.Warning.Main;
vs === 'verified' ? 'var(--tc-positive-normal, #5effc4)' : 'var(--tc-warning-normal, #ffcc55)';
const label = vs === 'verified' ? 'Identity verified' : 'Not verified'; const label = vs === 'verified' ? 'Identity verified' : 'Not verified';
return ( return (
<TooltipProvider <TooltipProvider
@@ -27,7 +26,7 @@ export function MemberVerificationBadge({ userId }: MemberVerificationBadgeProps
title={label} title={label}
style={{ display: 'inline-flex', alignItems: 'center', flexShrink: 0 }} style={{ display: 'inline-flex', alignItems: 'center', flexShrink: 0 }}
> >
<Icon size="100" src={Icons.ShieldUser} style={{ color }} /> <Icon size="100" src={Icons.ShieldUser} style={{ color: iconColor }} />
</span> </span>
)} )}
</TooltipProvider> </TooltipProvider>
@@ -529,7 +529,7 @@ export function MLocation({ content }: MLocationProps) {
style={{ style={{
width: '280px', width: '280px',
height: '160px', height: '160px',
border: '1px solid var(--bg-surface-border)', border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
borderRadius: '8px', borderRadius: '8px',
display: 'block', display: 'block',
}} }}
+2 -2
View File
@@ -17,10 +17,10 @@ export const Sidebar = style([
]); ]);
export const SidebarGlass = style({ export const SidebarGlass = style({
backgroundColor: 'rgba(3, 5, 8, 0.55)', backgroundColor: `color-mix(in srgb, ${color.Surface.Container} 55%, transparent)`,
backdropFilter: 'blur(12px)', backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)',
borderRight: '1px solid rgba(255,255,255,0.06)', borderRight: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
}); });
export const SidebarStack = style([ export const SidebarStack = style([
@@ -91,10 +91,10 @@ function UserDeviceRow({ userId, device }: UserDeviceRowProps) {
{(status) => { {(status) => {
const deviceColor = const deviceColor =
status === VerificationStatus.Verified status === VerificationStatus.Verified
? 'var(--tc-positive-normal, #5effc4)' ? color.Success.Main
: status === VerificationStatus.Unverified : status === VerificationStatus.Unverified
? 'var(--tc-warning-normal, #ffcc55)' ? color.Warning.Main
: 'var(--tc-surface-low-contrast)'; : color.SurfaceVariant.OnContainer;
return ( return (
<Box alignItems="Center" gap="200"> <Box alignItems="Center" gap="200">
<Icon size="100" src={Icons.ShieldUser} style={{ color: deviceColor, flexShrink: 0 }} /> <Icon size="100" src={Icons.ShieldUser} style={{ color: deviceColor, flexShrink: 0 }} />
@@ -106,7 +106,7 @@ function UserDeviceRow({ userId, device }: UserDeviceRowProps) {
<Text <Text
size="T200" size="T200"
truncate truncate
style={{ color: 'var(--tc-surface-low-contrast)', fontFamily: 'monospace' }} style={{ color: color.SurfaceVariant.OnContainer, fontFamily: 'monospace' }}
> >
{device.deviceId} {device.deviceId}
</Text> </Text>
@@ -160,7 +160,7 @@ function UserDeviceSessions({ userId }: UserDeviceSessionsProps) {
direction="Column" direction="Column"
gap="100" gap="100"
style={{ style={{
background: 'var(--bg-surface-variant)', background: color.SurfaceVariant.Container,
borderRadius: config.radii.R300, borderRadius: config.radii.R300,
padding: config.space.S300, padding: config.space.S300,
}} }}
@@ -171,7 +171,7 @@ function UserDeviceSessions({ userId }: UserDeviceSessionsProps) {
<Text size="T300"> <Text size="T300">
<b>Sessions</b> <b>Sessions</b>
</Text> </Text>
<Text size="T200" style={{ color: 'var(--tc-surface-low-contrast)' }}> <Text size="T200" style={{ color: color.SurfaceVariant.OnContainer }}>
{devices.length} {devices.length}
</Text> </Text>
</Box> </Box>
+3 -2
View File
@@ -3,6 +3,7 @@ import {
Box, Box,
Button, Button,
Chip, Chip,
color,
config, config,
Icon, Icon,
IconButton, IconButton,
@@ -276,8 +277,8 @@ export function CallControls({ callEmbed }: CallControlsProps) {
bottom: '110%', bottom: '110%',
left: '50%', left: '50%',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
background: 'var(--bg-surface)', background: color.Surface.Container,
border: '1px solid var(--bg-surface-border)', border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
borderRadius: '0.75rem', borderRadius: '0.75rem',
padding: '1rem 1.25rem', padding: '1rem 1.25rem',
zIndex: 100, zIndex: 100,
+2 -2
View File
@@ -166,13 +166,13 @@ export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps)
); );
} }
const FullscreenIcon = () => ( export const FullscreenIcon = () => (
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor"> <svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 3h6v2H5v4H3V3zm12 0h6v6h-2V5h-4V3zM3 15h2v4h4v2H3v-6zm16 4h-4v2h6v-6h-2v4z" /> <path d="M3 3h6v2H5v4H3V3zm12 0h6v6h-2V5h-4V3zM3 15h2v4h4v2H3v-6zm16 4h-4v2h6v-6h-2v4z" />
</svg> </svg>
); );
const ExitFullscreenIcon = () => ( export const ExitFullscreenIcon = () => (
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor"> <svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 3H7v4H3v2h6V3zm6 0v6h6V7h-4V3h-2zM3 13v2h4v4h2v-6H3zm14 4v-4h2v6h-6v-2h4z" /> <path d="M9 3H7v4H3v2h6V3zm6 0v6h6V7h-4V3h-2zM3 13v2h4v4h2v-6H3zm14 4v-4h2v6h-6v-2h4z" />
</svg> </svg>
+2 -5
View File
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Box, Button, Icon, Icons, Spinner, Text } from 'folds'; import { Box, Button, color, Icon, Icons, Spinner, Text } from 'folds';
import { SequenceCard } from '../../components/sequence-card'; import { SequenceCard } from '../../components/sequence-card';
import * as css from './styles.css'; import * as css from './styles.css';
import { ChatButton, ControlDivider, MicrophoneButton, SoundButton, VideoButton } from './Controls'; import { ChatButton, ControlDivider, MicrophoneButton, SoundButton, VideoButton } from './Controls';
@@ -78,10 +78,7 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
</Box> </Box>
<Box grow="Yes" direction="Column" gap="200"> <Box grow="Yes" direction="Column" gap="200">
{micDenied && ( {micDenied && (
<Text <Text size="T200" style={{ color: color.Critical.Main, textAlign: 'center' }}>
size="T200"
style={{ color: 'var(--tc-critical-high, #e53e3e)', textAlign: 'center' }}
>
Microphone access is blocked. Enable it in your browser settings to join. Microphone access is blocked. Enable it in your browser settings to join.
</Text> </Text>
)} )}
@@ -1,5 +1,17 @@
import React, { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Text, Box, Icon, Icons, config, Spinner, IconButton, Line, toRem, Button } from 'folds'; import {
Text,
Box,
Icon,
Icons,
color,
config,
Spinner,
IconButton,
Line,
toRem,
Button,
} from 'folds';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import { useInfiniteQuery } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query';
@@ -112,7 +124,7 @@ function EncryptedRoomCachePanel({ roomIds, onLoaded }: EncryptedRoomCachePanelP
gap="200" gap="200"
style={{ style={{
padding: config.space.S200, padding: config.space.S200,
background: 'var(--bg-surface-variant)', background: color.SurfaceVariant.Container,
borderRadius: config.radii.R300, borderRadius: config.radii.R300,
}} }}
> >
+2 -2
View File
@@ -1106,7 +1106,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
<Text <Text
size="T200" size="T200"
style={{ style={{
color: 'var(--tc-danger-normal)', color: color.Critical.Main,
padding: '2px 6px', padding: '2px 6px',
alignSelf: 'center', alignSelf: 'center',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
@@ -1119,7 +1119,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
<Text <Text
size="T200" size="T200"
style={{ style={{
color: 'var(--tc-danger-normal)', color: color.Critical.Main,
padding: '2px 6px', padding: '2px 6px',
alignSelf: 'center', alignSelf: 'center',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
@@ -482,9 +482,9 @@ function ProfileStatus() {
opacity: statusMsg.length >= 56 ? 1 : 0.45, opacity: statusMsg.length >= 56 ? 1 : 0.45,
color: color:
statusMsg.length >= 64 statusMsg.length >= 64
? 'var(--tc-critical-normal)' ? color.Critical.Main
: statusMsg.length >= 56 : statusMsg.length >= 56
? 'var(--tc-warning-normal)' ? color.Warning.Main
: undefined, : undefined,
}} }}
> >
@@ -536,7 +536,7 @@ function ProfileStatus() {
</Button> </Button>
</Box> </Box>
{saveState.status === AsyncStatus.Error && ( {saveState.status === AsyncStatus.Error && (
<Text size="T200" style={{ color: 'var(--tc-critical-normal)' }}> <Text size="T200" style={{ color: color.Critical.Main }}>
Failed to save status server may be rate limiting. Try again. Failed to save status server may be rate limiting. Try again.
</Text> </Text>
)} )}
@@ -730,7 +730,7 @@ function ProfilePronouns() {
</Button> </Button>
</Box> </Box>
{saveState.status === AsyncStatus.Error && ( {saveState.status === AsyncStatus.Error && (
<Text size="T200" style={{ color: 'var(--tc-critical-normal)' }}> <Text size="T200" style={{ color: color.Critical.Main }}>
Failed to save pronouns. Try again. Failed to save pronouns. Try again.
</Text> </Text>
)} )}
@@ -873,7 +873,7 @@ function ProfileTimezone() {
</Button> </Button>
</Box> </Box>
{saveState.status === AsyncStatus.Error && ( {saveState.status === AsyncStatus.Error && (
<Text size="T200" style={{ color: 'var(--tc-critical-normal)' }}> <Text size="T200" style={{ color: color.Critical.Main }}>
Failed to save timezone. Try again. Failed to save timezone. Try again.
</Text> </Text>
)} )}
@@ -37,12 +37,12 @@ function DecorationPreviewCell({
width: CELL_SIZE, width: CELL_SIZE,
height: CELL_SIZE, height: CELL_SIZE,
flexShrink: 0, flexShrink: 0,
border: `2px solid ${selected ? 'var(--accent-cyan)' : 'transparent'}`, border: `2px solid ${selected ? color.Primary.Main : 'transparent'}`,
borderRadius: '50%', borderRadius: '50%',
background: 'var(--bg-surface-variant)', background: color.SurfaceVariant.Container,
cursor: 'pointer', cursor: 'pointer',
padding: 0, padding: 0,
boxShadow: selected ? '0 0 0 1px var(--accent-cyan)' : 'none', boxShadow: selected ? `0 0 0 1px ${color.Primary.Main}` : 'none',
overflow: 'hidden', overflow: 'hidden',
outline: 'none', outline: 'none',
}} }}
@@ -142,7 +142,7 @@ export function ProfileDecoration() {
height: CELL_SIZE, height: CELL_SIZE,
flexShrink: 0, flexShrink: 0,
borderRadius: '50%', borderRadius: '50%',
background: 'var(--bg-surface-variant)', background: color.SurfaceVariant.Container,
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Box, Button, Text } from 'folds'; import { Box, Button, color, config, Text } from 'folds';
import { DenoiseModelId } from '../../../state/settings'; import { DenoiseModelId } from '../../../state/settings';
import { DENOISE_MODELS } from '../../../utils/lotusDenoiseUtils'; import { DENOISE_MODELS } from '../../../utils/lotusDenoiseUtils';
import { import {
@@ -49,8 +49,8 @@ function DbMeter({ label, db, threshold }: { label: string; db: number; threshol
style={{ style={{
position: 'relative', position: 'relative',
height: '12px', height: '12px',
background: 'var(--bg-card)', background: color.Surface.Container,
border: '1px solid var(--border-color)', border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
borderRadius: '6px', borderRadius: '6px',
overflow: 'hidden', overflow: 'hidden',
}} }}
@@ -62,7 +62,7 @@ function DbMeter({ label, db, threshold }: { label: string; db: number; threshol
left: 0, left: 0,
bottom: 0, bottom: 0,
width: `${pct}%`, width: `${pct}%`,
background: 'var(--accent-green)', background: color.Success.Main,
transition: 'width 0.05s linear', transition: 'width 0.05s linear',
}} }}
/> />
@@ -74,7 +74,7 @@ function DbMeter({ label, db, threshold }: { label: string; db: number; threshol
bottom: 0, bottom: 0,
left: `${markerPct}%`, left: `${markerPct}%`,
width: '2px', width: '2px',
background: 'var(--accent-orange)', background: color.Primary.Main,
}} }}
/> />
)} )}
+15 -9
View File
@@ -1372,8 +1372,8 @@ function Calls() {
style={{ style={{
padding: '16px', padding: '16px',
marginTop: '8px', marginTop: '8px',
borderTop: '1px solid var(--border-color)', borderTop: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
background: 'var(--bg-card)', background: color.Surface.Container,
}} }}
> >
{/* ── Model selection ───────────────────────────────────────── */} {/* ── Model selection ───────────────────────────────────────── */}
@@ -1397,8 +1397,8 @@ function Calls() {
style={{ style={{
padding: '12px', padding: '12px',
borderRadius: '8px', borderRadius: '8px',
border: '1px solid var(--border-color)', border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
background: 'var(--bg-input)', background: color.SurfaceVariant.Container,
}} }}
> >
<Text size="T300">{selectedDenoiseModel.name}</Text> <Text size="T300">{selectedDenoiseModel.name}</Text>
@@ -1436,7 +1436,7 @@ function Calls() {
direction="Row" direction="Row"
gap="100" gap="100"
style={{ style={{
borderBottom: '1px solid var(--border-color)', borderBottom: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
paddingBottom: '4px', paddingBottom: '4px',
}} }}
> >
@@ -1489,7 +1489,10 @@ function Calls() {
<Box <Box
direction="Column" direction="Column"
gap="300" gap="300"
style={{ paddingTop: '12px', borderTop: '1px solid var(--border-color)' }} style={{
paddingTop: '12px',
borderTop: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
}}
> >
<Text size="L400">Enhancements</Text> <Text size="L400">Enhancements</Text>
<SettingTile <SettingTile
@@ -1525,7 +1528,7 @@ function Calls() {
step="1" step="1"
value={callDenoiseGateThreshold} value={callDenoiseGateThreshold}
onChange={(e) => setCallDenoiseGateThreshold(parseInt(e.target.value, 10))} onChange={(e) => setCallDenoiseGateThreshold(parseInt(e.target.value, 10))}
style={{ width: '100%', accentColor: 'var(--accent-orange)' }} style={{ width: '100%', accentColor: color.Primary.Main }}
/> />
</Box> </Box>
)} )}
@@ -1535,7 +1538,10 @@ function Calls() {
<Box <Box
direction="Column" direction="Column"
gap="200" gap="200"
style={{ paddingTop: '12px', borderTop: '1px solid var(--border-color)' }} style={{
paddingTop: '12px',
borderTop: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
}}
> >
<Text size="L400">Test &amp; calibrate</Text> <Text size="L400">Test &amp; calibrate</Text>
<Text size="T200" priority="300"> <Text size="T200" priority="300">
@@ -1658,7 +1664,7 @@ function Calls() {
value={ringtoneVolume} value={ringtoneVolume}
onChange={(e) => setRingtoneVolume(parseInt(e.target.value, 10))} onChange={(e) => setRingtoneVolume(parseInt(e.target.value, 10))}
aria-label="Ringtone volume" aria-label="Ringtone volume"
style={{ flex: 1, accentColor: 'var(--accent-orange)' }} style={{ flex: 1, accentColor: color.Primary.Main }}
/> />
<Text size="T200" style={{ minWidth: '32px', textAlign: 'right' }}> <Text size="T200" style={{ minWidth: '32px', textAlign: 'right' }}>
{ringtoneVolume}% {ringtoneVolume}%
+50 -34
View File
@@ -1,6 +1,9 @@
import React, { useEffect, useRef, CSSProperties } from 'react'; import React, { useEffect, useRef, CSSProperties } from 'react';
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import { color, config, Icon, IconButton, Icons } from 'folds';
import { toastQueueAtom, dismissToastAtom, ToastNotif } from '../../state/toast'; import { toastQueueAtom, dismissToastAtom, ToastNotif } from '../../state/toast';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
// Inject the keyframe animation once // Inject the keyframe animation once
const STYLE_ID = 'lotus-toast-keyframes'; const STYLE_ID = 'lotus-toast-keyframes';
@@ -29,6 +32,10 @@ type ToastCardProps = {
function ToastCard({ toast }: ToastCardProps) { function ToastCard({ toast }: ToastCardProps) {
const dismiss = useSetAtom(dismissToastAtom); const dismiss = useSetAtom(dismissToastAtom);
// Lotus Terminal (TDS) gets its bespoke glow/accents; every other theme uses
// folds tokens so toasts render correctly on stock Cinny themes (the --lt-*
// vars only exist while Terminal mode is active).
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => { useEffect(() => {
@@ -56,17 +63,29 @@ function ToastCard({ toast }: ToastCardProps) {
dismiss(toast.id); dismiss(toast.id);
}; };
const accent = toast.sticky ? color.Primary.Main : color.Surface.OnContainer;
const cardStyle: CSSProperties = { const cardStyle: CSSProperties = {
position: 'relative', position: 'relative',
background: 'var(--lt-bg-card)', background: lotusTerminal ? 'var(--lt-bg-card)' : color.Surface.Container,
border: toast.sticky border: `${config.borderWidth.B300} solid ${
? '1px solid var(--lt-accent-cyan-border)' lotusTerminal
: '1px solid var(--lt-border-color)', ? toast.sticky
borderRadius: '12px', ? 'var(--lt-accent-cyan-border)'
padding: '12px 14px', : 'var(--lt-border-color)'
: toast.sticky
? color.Primary.Main
: color.Surface.ContainerLine
}`,
borderRadius: config.radii.R400,
padding: `${config.space.S300} ${config.space.S400}`,
minWidth: '280px', minWidth: '280px',
maxWidth: '340px', maxWidth: '340px',
boxShadow: toast.sticky ? 'var(--lt-box-glow-cyan)' : 'var(--lt-box-glow-orange)', boxShadow: lotusTerminal
? toast.sticky
? 'var(--lt-box-glow-cyan)'
: 'var(--lt-box-glow-orange)'
: `0 8px 24px ${color.Other.Shadow}`,
cursor: 'pointer', cursor: 'pointer',
animation: 'lotusToastIn 0.2s ease-out both', animation: 'lotusToastIn 0.2s ease-out both',
userSelect: 'none', userSelect: 'none',
@@ -75,8 +94,8 @@ function ToastCard({ toast }: ToastCardProps) {
const rowStyle: CSSProperties = { const rowStyle: CSSProperties = {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '8px', gap: config.space.S200,
marginRight: '20px', marginRight: config.space.S500,
}; };
const avatarStyle: CSSProperties = { const avatarStyle: CSSProperties = {
@@ -91,19 +110,25 @@ function ToastCard({ toast }: ToastCardProps) {
width: '24px', width: '24px',
height: '24px', height: '24px',
borderRadius: '50%', borderRadius: '50%',
background: 'var(--lt-accent-orange-dim)', background: lotusTerminal ? 'var(--lt-accent-orange-dim)' : color.Primary.Container,
border: '1px solid var(--lt-accent-orange-border)', border: `${config.borderWidth.B300} solid ${
lotusTerminal ? 'var(--lt-accent-orange-border)' : color.Primary.ContainerLine
}`,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
fontSize: '10px', fontSize: '10px',
fontWeight: 700, fontWeight: 700,
color: 'var(--lt-accent-orange)', color: lotusTerminal ? 'var(--lt-accent-orange)' : color.Primary.OnContainer,
flexShrink: 0, flexShrink: 0,
}; };
const nameStyle: CSSProperties = { const nameStyle: CSSProperties = {
color: toast.sticky ? 'var(--lt-accent-cyan)' : 'var(--lt-accent-orange)', color: lotusTerminal
? toast.sticky
? 'var(--lt-accent-cyan)'
: 'var(--lt-accent-orange)'
: accent,
fontWeight: 600, fontWeight: 600,
fontSize: '0.85rem', fontSize: '0.85rem',
overflow: 'hidden', overflow: 'hidden',
@@ -111,22 +136,8 @@ function ToastCard({ toast }: ToastCardProps) {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}; };
const dismissBtnStyle: CSSProperties = {
position: 'absolute',
top: '8px',
right: '10px',
background: 'none',
border: 'none',
color: 'var(--lt-text-secondary)',
cursor: 'pointer',
fontSize: '14px',
lineHeight: 1,
padding: '2px 4px',
borderRadius: '4px',
};
const bodyStyle: CSSProperties = { const bodyStyle: CSSProperties = {
color: 'var(--lt-text-primary)', color: lotusTerminal ? 'var(--lt-text-primary)' : color.Surface.OnContainer,
fontSize: '0.82rem', fontSize: '0.82rem',
margin: '4px 0 2px', margin: '4px 0 2px',
overflow: 'hidden', overflow: 'hidden',
@@ -136,7 +147,7 @@ function ToastCard({ toast }: ToastCardProps) {
}; };
const roomNameStyle: CSSProperties = { const roomNameStyle: CSSProperties = {
color: 'var(--lt-text-secondary)', color: lotusTerminal ? 'var(--lt-text-secondary)' : color.SurfaceVariant.OnContainer,
fontSize: '0.75rem', fontSize: '0.75rem',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
@@ -161,14 +172,19 @@ function ToastCard({ toast }: ToastCardProps) {
}} }}
aria-label={`Notification from ${toast.displayName} in ${toast.roomName}`} aria-label={`Notification from ${toast.displayName} in ${toast.roomName}`}
> >
<button <span style={{ position: 'absolute', top: config.space.S100, right: config.space.S100 }}>
<IconButton
type="button" type="button"
style={dismissBtnStyle} size="300"
radii="300"
variant="Surface"
fill="None"
onClick={handleDismiss} onClick={handleDismiss}
aria-label="Dismiss notification" aria-label="Dismiss notification"
> >
× <Icon size="100" src={Icons.Cross} />
</button> </IconButton>
</span>
<div style={rowStyle}> <div style={rowStyle}>
{toast.avatarUrl ? ( {toast.avatarUrl ? (
<img src={toast.avatarUrl} alt="" style={avatarStyle} aria-hidden="true" /> <img src={toast.avatarUrl} alt="" style={avatarStyle} aria-hidden="true" />
@@ -201,7 +217,7 @@ export function LotusToastContainer() {
zIndex: 10001, zIndex: 10001,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: '8px', gap: config.space.S200,
pointerEvents: 'auto', pointerEvents: 'auto',
}; };
+19 -15
View File
@@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { CallEmbed } from '../plugins/call'; import { CallEmbed, useCallControlState } from '../plugins/call';
import { useSetting } from '../state/hooks/settings'; import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings'; import { settingsAtom } from '../state/settings';
import { toastQueueAtom } from '../state/toast'; import { toastQueueAtom } from '../state/toast';
@@ -9,17 +9,25 @@ const SILENCE_RMS_THRESHOLD = 0.008;
const CHECK_INTERVAL_MS = 500; const CHECK_INTERVAL_MS = 500;
/** /**
* Monitors microphone audio while in a call. If the mic stays active but * Monitors microphone audio while in a call. If the mic stays unmuted but
* silent for longer than the configured timeout, the mic is muted and a * silent for longer than the configured timeout, the mic is muted and a toast
* toast is shown. Cleans up its own AudioContext and stream on unmount. * is shown.
*
* The level-monitoring capture (`getUserMedia`) is opened ONLY while the mic is
* unmuted — there is nothing to auto-mute once you are already muted, so
* holding the capture would keep the OS recording indicator lit even though the
* UI shows you as muted (N95). Muting therefore releases our stream; unmuting
* re-acquires it. The AudioContext + stream are also torn down on unmount.
*/ */
export function useAfkAutoMute(callEmbed: CallEmbed | undefined): void { export function useAfkAutoMute(callEmbed: CallEmbed | undefined): void {
const [enabled] = useSetting(settingsAtom, 'afkAutoMute'); const [enabled] = useSetting(settingsAtom, 'afkAutoMute');
const [timeoutMinutes] = useSetting(settingsAtom, 'afkTimeoutMinutes'); const [timeoutMinutes] = useSetting(settingsAtom, 'afkTimeoutMinutes');
const setToast = useSetAtom(toastQueueAtom); const setToast = useSetAtom(toastQueueAtom);
const { microphone } = useCallControlState(callEmbed?.control);
useEffect(() => { useEffect(() => {
if (!callEmbed || !enabled) return; // Only capture while in a call, enabled, AND unmuted (see N95 note above).
if (!callEmbed || !enabled || !microphone) return undefined;
let stream: MediaStream | undefined; let stream: MediaStream | undefined;
let audioCtx: AudioContext | undefined; let audioCtx: AudioContext | undefined;
@@ -49,12 +57,12 @@ export function useAfkAutoMute(callEmbed: CallEmbed | undefined): void {
const rms = Math.sqrt(buffer.reduce((sum, v) => sum + v * v, 0) / buffer.length); const rms = Math.sqrt(buffer.reduce((sum, v) => sum + v * v, 0) / buffer.length);
if (rms > SILENCE_RMS_THRESHOLD) { if (rms > SILENCE_RMS_THRESHOLD) {
// Audio detected — reset the silence timer // Audio detected — reset the silence timer.
silenceStart = null; silenceStart = null;
} else if (callEmbed.control.microphone) { } else if (silenceStart === null) {
// Mic is on but silent — start or advance the timer // Mic is unmuted (effect only runs while unmuted) but silent — start the timer.
if (silenceStart === null) silenceStart = Date.now(); silenceStart = Date.now();
else if (Date.now() - silenceStart >= timeoutMs) { } else if (Date.now() - silenceStart >= timeoutMs) {
callEmbed.control.setMicrophone(false); callEmbed.control.setMicrophone(false);
setToast({ setToast({
id: `afk-mute-${Date.now()}`, id: `afk-mute-${Date.now()}`,
@@ -65,10 +73,6 @@ export function useAfkAutoMute(callEmbed: CallEmbed | undefined): void {
}); });
silenceStart = null; silenceStart = null;
} }
} else {
// Mic is already muted — don't count silence
silenceStart = null;
}
}, CHECK_INTERVAL_MS); }, CHECK_INTERVAL_MS);
}) })
.catch(() => undefined); .catch(() => undefined);
@@ -79,5 +83,5 @@ export function useAfkAutoMute(callEmbed: CallEmbed | undefined): void {
stream?.getTracks().forEach((t) => t.stop()); stream?.getTracks().forEach((t) => t.stop());
audioCtx?.close().catch(() => undefined); audioCtx?.close().catch(() => undefined);
}; };
}, [callEmbed, enabled, timeoutMinutes, setToast]); }, [callEmbed, enabled, timeoutMinutes, setToast, microphone]);
} }
+28 -35
View File
@@ -1,7 +1,16 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import * as Sentry from '@sentry/react'; import { ErrorBoundary } from 'react-error-boundary';
import { Provider as JotaiProvider, useAtomValue } from 'jotai'; import { Provider as JotaiProvider, useAtomValue } from 'jotai';
import { OverlayContainerProvider, PopOutContainerProvider, TooltipContainerProvider } from 'folds'; import {
Box,
Button,
config,
OverlayContainerProvider,
PopOutContainerProvider,
Text,
toRem,
TooltipContainerProvider,
} from 'folds';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
@@ -102,41 +111,25 @@ function App() {
const portalContainer = document.getElementById('portalContainer') ?? undefined; const portalContainer = document.getElementById('portalContainer') ?? undefined;
return ( return (
<Sentry.ErrorBoundary <ErrorBoundary
fallback={({ error, resetError }) => ( fallbackRender={({ error, resetErrorBoundary }) => (
<div <Box
style={{ direction="Column"
display: 'flex', alignItems="Center"
flexDirection: 'column', justifyContent="Center"
alignItems: 'center', gap="400"
justifyContent: 'center', style={{ height: '100vh', padding: config.space.S700, textAlign: 'center' }}
height: '100vh',
gap: '16px',
fontFamily: 'sans-serif',
padding: '24px',
textAlign: 'center',
}}
> >
<h2 style={{ margin: 0 }}>Something went wrong</h2> <Text size="H2">Something went wrong</Text>
<p style={{ margin: 0, color: '#666', maxWidth: '400px' }}> <Text size="T300" priority="300" style={{ maxWidth: toRem(400) }}>
{error instanceof Error ? error.message : 'An unexpected error occurred.'} {error instanceof Error ? error.message : 'An unexpected error occurred.'}
</p> </Text>
<button <Button variant="Primary" onClick={resetErrorBoundary}>
type="button" <Text as="span" size="B400">
onClick={resetError}
style={{
padding: '8px 20px',
borderRadius: '6px',
border: 'none',
background: '#5865f2',
color: '#fff',
cursor: 'pointer',
fontSize: '14px',
}}
>
Try again Try again
</button> </Text>
</div> </Button>
</Box>
)} )}
> >
<TooltipContainerProvider value={portalContainer}> <TooltipContainerProvider value={portalContainer}>
@@ -171,7 +164,7 @@ function App() {
</OverlayContainerProvider> </OverlayContainerProvider>
</PopOutContainerProvider> </PopOutContainerProvider>
</TooltipContainerProvider> </TooltipContainerProvider>
</Sentry.ErrorBoundary> </ErrorBoundary>
); );
} }
+16 -26
View File
@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { useRouteError, isRouteErrorResponse } from 'react-router-dom'; import { useRouteError, isRouteErrorResponse } from 'react-router-dom';
import { Box, Button, config, Text, toRem } from 'folds';
export function RouteError() { export function RouteError() {
const error = useRouteError(); const error = useRouteError();
@@ -11,33 +12,22 @@ export function RouteError() {
: 'An unexpected error occurred.'; : 'An unexpected error occurred.';
return ( return (
<div <Box
style={{ direction="Column"
display: 'flex', alignItems="Center"
flexDirection: 'column', justifyContent="Center"
alignItems: 'center', gap="400"
justifyContent: 'center', style={{ height: '100dvh', padding: config.space.S700 }}
height: '100dvh',
gap: '16px',
padding: '32px',
fontFamily: 'sans-serif',
}}
>
<h2 style={{ margin: 0, fontSize: '1.25rem' }}>Something went wrong</h2>
<p style={{ margin: 0, opacity: 0.7, textAlign: 'center' }}>{message}</p>
<button
type="button"
onClick={() => window.location.reload()}
style={{
padding: '8px 20px',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
fontWeight: 600,
}}
> >
<Text size="H3">Something went wrong</Text>
<Text size="T300" priority="300" style={{ textAlign: 'center', maxWidth: toRem(400) }}>
{message}
</Text>
<Button variant="Primary" onClick={() => window.location.reload()}>
<Text as="span" size="B400">
Reload Reload
</button> </Text>
</div> </Button>
</Box>
); );
} }
-26
View File
@@ -1,5 +1,4 @@
/* eslint-disable import/first */ /* eslint-disable import/first */
import * as Sentry from '@sentry/react';
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';
@@ -7,31 +6,6 @@ import '@fontsource-variable/inter/index.css';
import 'folds/dist/style.css'; import 'folds/dist/style.css';
import { configClass, varsClass } from 'folds'; import { configClass, varsClass } from 'folds';
const sentryDsn = import.meta.env.VITE_SENTRY_DSN;
if (sentryDsn) {
Sentry.init({
dsn: sentryDsn,
environment: import.meta.env.MODE,
release: import.meta.env.VITE_APP_VERSION,
// browserTracingIntegration omitted — it injects sentry-trace/baggage headers
// into outgoing fetch calls, which breaks Synapse CORS on matrix.lotusguild.org
// No propagation targets — we don't control the Matrix server's CORS allow-list
tracePropagationTargets: [],
tracesSampleRate: 0,
// Don't send PII (IPs, usernames) — this is a private chat app
sendDefaultPii: false,
// Forward Sentry logs to the dashboard
enableLogs: true,
// Suppress benign PostmessageTransport / matrixRTC heartbeat timeouts (upstream library noise)
ignoreErrors: ['Request timed out'],
beforeSend(event) {
// Drop any event that may have leaked an access token into breadcrumbs/data
if (JSON.stringify(event).includes('access_token')) return null;
return event;
},
});
}
enableMapSet(); enableMapSet();
import './index.css'; import './index.css';
+1 -16
View File
@@ -1,6 +1,5 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { sentryVitePlugin } from '@sentry/vite-plugin';
import { wasm } from '@rollup/plugin-wasm'; import { wasm } from '@rollup/plugin-wasm';
import inject from '@rollup/plugin-inject'; import inject from '@rollup/plugin-inject';
import { viteStaticCopy } from 'vite-plugin-static-copy'; import { viteStaticCopy } from 'vite-plugin-static-copy';
@@ -261,20 +260,6 @@ export default defineConfig({
react(), react(),
copyPdfWorker(), copyPdfWorker(),
lotusDenoise(), lotusDenoise(),
...(process.env.SENTRY_AUTH_TOKEN
? [
sentryVitePlugin({
org: 'lotus-guild',
project: 'javascript-react',
authToken: process.env.SENTRY_AUTH_TOKEN,
sourcemaps: {
filesToDeleteAfterUpload: ['./dist/**/*.map'],
},
release: { name: process.env.VITE_APP_VERSION ?? 'lotus' },
telemetry: false,
}),
]
: []),
VitePWA({ VitePWA({
srcDir: 'src', srcDir: 'src',
filename: 'sw.ts', filename: 'sw.ts',
@@ -302,7 +287,7 @@ export default defineConfig({
build: { build: {
target: 'esnext', target: 'esnext',
outDir: 'dist', outDir: 'dist',
sourcemap: process.env.SENTRY_AUTH_TOKEN ? 'hidden' : false, sourcemap: false,
copyPublicDir: false, copyPublicDir: false,
// manualChunks must be in rolldownOptions (not rollupOptions) for Vite 8 / Rolldown // manualChunks must be in rolldownOptions (not rollupOptions) for Vite 8 / Rolldown
rolldownOptions: { rolldownOptions: {