Compare commits
20 Commits
5204766276
...
lotus
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ace96f2cf | |||
| 2d71f2ce30 | |||
| 2c3dba55e6 | |||
| c7a04dcc70 | |||
| 4b14c15518 | |||
| c68ef346bf | |||
| c5d7fcc303 | |||
| 9bf56d5748 | |||
| d5ce56930b | |||
| 349194e7e5 | |||
| 24d6460e4c | |||
| 127e783f66 | |||
| 198fd12bb2 | |||
| 34d5209165 | |||
| 9684ab75bb | |||
| 0a6b035a67 | |||
| cbfd3e5632 | |||
| 3faf0866a0 | |||
| bab3a160c2 | |||
| 1778cd0009 |
@@ -1,2 +1 @@
|
||||
VITE_SENTRY_DSN=https://264a5e95c5d31fe080a2e92fb008294d@o4511430568378368.ingest.us.sentry.io/4511430571982849
|
||||
VITE_APP_VERSION=lotus
|
||||
|
||||
@@ -45,7 +45,6 @@ jobs:
|
||||
run: npm run build
|
||||
env:
|
||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
VITE_APP_VERSION: ${{ github.sha }}
|
||||
|
||||
# ── Quality checks (informational — pre-existing issues exist) ───────
|
||||
|
||||
+3
-3
@@ -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 |
|
||||
| #12 | PiP mute badge attribution (you vs. all-muted) | `CallEmbedProvider.tsx` | G1 |
|
||||
| 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 |
|
||||
| Gal | MediaGallery lazy-decrypt (true virtualization deferred) | `room/MediaGallery.tsx` | H1 |
|
||||
| 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
|
||||
|
||||
- **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.
|
||||
|
||||
### 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.
|
||||
- **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.
|
||||
- **`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).
|
||||
- **`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`.
|
||||
- **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.
|
||||
- **Git workflow (forward-looking):** keep commits scoped — past monolithic "fix all bugs" commits and inconsistent prefixes hurt `git bisect`.
|
||||
|
||||
|
||||
+12
-9
@@ -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**.
|
||||
2. Manually **mute your mic** using the call controls.
|
||||
3. Check the **OS recording indicator** (macOS: green dot top-right of menu bar; Windows: mic icon in taskbar).
|
||||
2. Manually **mute your mic** using the call controls → the **OS recording indicator should clear** within ~a second.
|
||||
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.
|
||||
**Expected after fix:** the indicator should clear when you mute and re-appear when you unmute.
|
||||
### L2. Maskable PWA icon (N108) — Android install
|
||||
|
||||
> **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,7 +2,7 @@
|
||||
|
||||
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)** | Forked from [Cinny](https://github.com/cinnyapp/cinny) v4.12.1
|
||||
**Deployed at [chat.lotusguild.org](https://chat.lotusguild.org)** | 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 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Generated
-496
@@ -21,7 +21,6 @@
|
||||
"@giphy/js-util": "5.2.0",
|
||||
"@giphy/react-components": "10.1.2",
|
||||
"@sapphi-red/web-noise-suppressor": "0.3.5",
|
||||
"@sentry/react": "10.53.1",
|
||||
"@tanstack/react-query": "5.100.13",
|
||||
"@tanstack/react-query-devtools": "5.100.13",
|
||||
"@tanstack/react-virtual": "3.13.25",
|
||||
@@ -81,7 +80,6 @@
|
||||
"@element-hq/element-call-embedded": "0.20.1",
|
||||
"@rollup/plugin-inject": "5.0.5",
|
||||
"@rollup/plugin-wasm": "6.2.2",
|
||||
"@sentry/vite-plugin": "5.3.0",
|
||||
"@types/chroma-js": "3.1.2",
|
||||
"@types/file-saver": "2.0.7",
|
||||
"@types/is-hotkey": "0.1.10",
|
||||
@@ -3782,403 +3780,6 @@
|
||||
"integrity": "sha512-jh3+V9yM+zxLriQexoGm0GatoPaJWjs6ypFIbFYwQp+AoUb55eUXrjKtKQyuC5zShzzeAQUl0M5JzqB7SSrsRA==",
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
||||
@@ -6634,19 +6223,6 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "9.1.7",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
||||
@@ -10599,26 +10162,6 @@
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"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": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
@@ -11178,16 +10721,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": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -11198,13 +10731,6 @@
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -12783,12 +12309,6 @@
|
||||
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
|
||||
"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": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||
@@ -13336,22 +12856,6 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"@giphy/js-util": "5.2.0",
|
||||
"@giphy/react-components": "10.1.2",
|
||||
"@sapphi-red/web-noise-suppressor": "0.3.5",
|
||||
"@sentry/react": "10.53.1",
|
||||
"@tanstack/react-query": "5.100.13",
|
||||
"@tanstack/react-query-devtools": "5.100.13",
|
||||
"@tanstack/react-virtual": "3.13.25",
|
||||
@@ -105,7 +104,6 @@
|
||||
"@element-hq/element-call-embedded": "0.20.1",
|
||||
"@rollup/plugin-inject": "5.0.5",
|
||||
"@rollup/plugin-wasm": "6.2.2",
|
||||
"@sentry/vite-plugin": "5.3.0",
|
||||
"@types/chroma-js": "3.1.2",
|
||||
"@types/file-saver": "2.0.7",
|
||||
"@types/is-hotkey": "0.1.10",
|
||||
|
||||
@@ -54,6 +54,18 @@
|
||||
"src": "./res/android/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"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"],
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -9,6 +9,7 @@ import {
|
||||
config,
|
||||
Dialog,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
@@ -51,6 +52,7 @@ import { mxcUrlToHttp, getMxIdLocalPart } from '../utils/matrix';
|
||||
import { RoomAvatar, RoomIcon } from './room-avatar';
|
||||
import { useRoomNavigate } from '../hooks/useRoomNavigate';
|
||||
import { getChatBg } from '../features/lotus/chatBackground';
|
||||
import { ExitFullscreenIcon, FullscreenIcon } from '../features/call/Controls';
|
||||
import { useTheme, ThemeKind } from '../hooks/useTheme';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
@@ -62,6 +64,7 @@ import { getRoomPermissionsAPI } from '../hooks/useRoomPermissions';
|
||||
import { useLivekitSupport } from '../hooks/useLivekitSupport';
|
||||
import { CallAvatarAnimation } from '../styles/Animations.css';
|
||||
import { webRTCSupported } from '../utils/rtc';
|
||||
import { zIndices } from '../styles/zIndex';
|
||||
|
||||
const PIP_MIN_W = 200;
|
||||
const PIP_MIN_H = 112;
|
||||
@@ -321,7 +324,7 @@ function IncomingCallBanner({ dm, info, onIgnore, onAnswer, onReject }: Incoming
|
||||
position: 'fixed',
|
||||
top: config.space.S400,
|
||||
right: config.space.S400,
|
||||
zIndex: 9990,
|
||||
zIndex: zIndices.inCallBanner,
|
||||
width: toRem(300),
|
||||
maxWidth: `calc(100vw - 2 * ${config.space.S400})`,
|
||||
padding: config.space.S300,
|
||||
@@ -1095,10 +1098,13 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
>
|
||||
<div style={{ display: 'flex', gap: config.space.S100, alignItems: 'center' }}>
|
||||
{document.fullscreenEnabled && (
|
||||
<button
|
||||
<IconButton
|
||||
type="button"
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Surface"
|
||||
fill="None"
|
||||
aria-label={pipIsFullscreen ? 'Exit fullscreen' : 'Fullscreen camera'}
|
||||
title={pipIsFullscreen ? 'Exit fullscreen' : 'Fullscreen camera'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePipFullscreen();
|
||||
@@ -1107,19 +1113,11 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
// Dark scrim is intentional for legibility over arbitrary video.
|
||||
background: 'rgba(0,0,0,0.65)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
border: 'none',
|
||||
borderRadius: config.radii.R300,
|
||||
padding: `${config.space.S100} ${config.space.S200}`,
|
||||
color: '#fff',
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer',
|
||||
lineHeight: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{pipIsFullscreen ? '⊡' : '⛶'}
|
||||
</button>
|
||||
{pipIsFullscreen ? <ExitFullscreenIcon /> : <FullscreenIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
type MemberVerificationBadgeProps = {
|
||||
@@ -9,8 +9,7 @@ type MemberVerificationBadgeProps = {
|
||||
export function MemberVerificationBadge({ userId }: MemberVerificationBadgeProps) {
|
||||
const vs = useUserVerifiedStatus(userId);
|
||||
if (vs === 'unknown') return null;
|
||||
const color =
|
||||
vs === 'verified' ? 'var(--tc-positive-normal, #5effc4)' : 'var(--tc-warning-normal, #ffcc55)';
|
||||
const iconColor = vs === 'verified' ? color.Success.Main : color.Warning.Main;
|
||||
const label = vs === 'verified' ? 'Identity verified' : 'Not verified';
|
||||
return (
|
||||
<TooltipProvider
|
||||
@@ -27,7 +26,7 @@ export function MemberVerificationBadge({ userId }: MemberVerificationBadgeProps
|
||||
title={label}
|
||||
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>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -529,7 +529,7 @@ export function MLocation({ content }: MLocationProps) {
|
||||
style={{
|
||||
width: '280px',
|
||||
height: '160px',
|
||||
border: '1px solid var(--bg-surface-border)',
|
||||
border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||
borderRadius: '8px',
|
||||
display: 'block',
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Box, color, config, Text, toRem } from 'folds';
|
||||
import { Box, color, config, Icon, Icons, Text, toRem } from 'folds';
|
||||
import { RelationsEvent } from 'matrix-js-sdk/lib/models/relations';
|
||||
import { RoomEvent } from 'matrix-js-sdk';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
@@ -339,11 +339,7 @@ export function PollContent({
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
>
|
||||
{selected && isMultiple ? (
|
||||
<Text as="span" size="T200" style={{ lineHeight: 1 }}>
|
||||
✓
|
||||
</Text>
|
||||
) : null}
|
||||
{selected && isMultiple ? <Icon size="50" src={Icons.Check} /> : null}
|
||||
</span>
|
||||
<Text as="span" size="T300" style={{ flexGrow: 1 }}>
|
||||
{text}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { zIndices } from '../../styles/zIndex';
|
||||
import {
|
||||
animSeasonFall,
|
||||
animLeafFall,
|
||||
@@ -758,7 +759,7 @@ function SeasonalOverlay({ theme, reduced }: { theme: SeasonTheme; reduced: bool
|
||||
pointerEvents: 'none',
|
||||
// Below the Night Light overlay (9998) so seasonal particles are tinted
|
||||
// by it, and below modals (9999) so dialogs are never obscured.
|
||||
zIndex: 9997,
|
||||
zIndex: zIndices.seasonalEffect,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import React, { MouseEventHandler, useState } from 'react';
|
||||
import { Box, Button, config, Icon, Icons, Menu, MenuItem, PopOut, RectCords, Text } from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
|
||||
export type SettingsSelectOption<T extends string> = {
|
||||
value: T;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A folds-native dropdown (Button + PopOut + Menu) matching Cinny's select
|
||||
* pattern — used instead of a raw `<select>`, which renders OS-styled and
|
||||
* breaks under non-default themes.
|
||||
*/
|
||||
export function SettingsSelect<T extends string>({
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
disabled,
|
||||
'aria-label': ariaLabel,
|
||||
}: {
|
||||
value: T;
|
||||
options: SettingsSelectOption<T>[];
|
||||
onChange: (v: T) => void;
|
||||
disabled?: boolean;
|
||||
'aria-label'?: string;
|
||||
}) {
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const selectedLabel = options.find((o) => o.value === value)?.label ?? value;
|
||||
|
||||
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleSelect = (v: T) => {
|
||||
onChange(v);
|
||||
setMenuCords(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
outlined
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
after={<Icon size="300" src={Icons.ChevronBottom} />}
|
||||
onClick={handleMenu}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={!!menuCords}
|
||||
>
|
||||
<Text size="T300">{selectedLabel}</Text>
|
||||
</Button>
|
||||
<PopOut
|
||||
anchor={menuCords}
|
||||
offset={5}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setMenuCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||
isKeyBackward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
{options.map((opt) => (
|
||||
<MenuItem
|
||||
key={opt.value}
|
||||
size="300"
|
||||
variant={opt.value === value ? 'Primary' : 'Surface'}
|
||||
radii="300"
|
||||
disabled={opt.disabled}
|
||||
onClick={() => !opt.disabled && handleSelect(opt.value)}
|
||||
>
|
||||
<Text size="T300">{opt.label}</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -17,10 +17,10 @@ export const Sidebar = 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)',
|
||||
WebkitBackdropFilter: 'blur(12px)',
|
||||
borderRight: '1px solid rgba(255,255,255,0.06)',
|
||||
borderRight: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||
});
|
||||
|
||||
export const SidebarStack = style([
|
||||
|
||||
@@ -91,10 +91,10 @@ function UserDeviceRow({ userId, device }: UserDeviceRowProps) {
|
||||
{(status) => {
|
||||
const deviceColor =
|
||||
status === VerificationStatus.Verified
|
||||
? 'var(--tc-positive-normal, #5effc4)'
|
||||
? color.Success.Main
|
||||
: status === VerificationStatus.Unverified
|
||||
? 'var(--tc-warning-normal, #ffcc55)'
|
||||
: 'var(--tc-surface-low-contrast)';
|
||||
? color.Warning.Main
|
||||
: color.SurfaceVariant.OnContainer;
|
||||
return (
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Icon size="100" src={Icons.ShieldUser} style={{ color: deviceColor, flexShrink: 0 }} />
|
||||
@@ -106,7 +106,7 @@ function UserDeviceRow({ userId, device }: UserDeviceRowProps) {
|
||||
<Text
|
||||
size="T200"
|
||||
truncate
|
||||
style={{ color: 'var(--tc-surface-low-contrast)', fontFamily: 'monospace' }}
|
||||
style={{ color: color.SurfaceVariant.OnContainer, fontFamily: 'monospace' }}
|
||||
>
|
||||
{device.deviceId}
|
||||
</Text>
|
||||
@@ -160,7 +160,7 @@ function UserDeviceSessions({ userId }: UserDeviceSessionsProps) {
|
||||
direction="Column"
|
||||
gap="100"
|
||||
style={{
|
||||
background: 'var(--bg-surface-variant)',
|
||||
background: color.SurfaceVariant.Container,
|
||||
borderRadius: config.radii.R300,
|
||||
padding: config.space.S300,
|
||||
}}
|
||||
@@ -171,7 +171,7 @@ function UserDeviceSessions({ userId }: UserDeviceSessionsProps) {
|
||||
<Text size="T300">
|
||||
<b>Sessions</b>
|
||||
</Text>
|
||||
<Text size="T200" style={{ color: 'var(--tc-surface-low-contrast)' }}>
|
||||
<Text size="T200" style={{ color: color.SurfaceVariant.OnContainer }}>
|
||||
{devices.length}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
color,
|
||||
config,
|
||||
Icon,
|
||||
IconButton,
|
||||
@@ -276,8 +277,8 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
bottom: '110%',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: 'var(--bg-surface)',
|
||||
border: '1px solid var(--bg-surface-border)',
|
||||
background: color.Surface.Container,
|
||||
border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
|
||||
borderRadius: '0.75rem',
|
||||
padding: '1rem 1.25rem',
|
||||
zIndex: 100,
|
||||
|
||||
@@ -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">
|
||||
<path d="M3 3h6v2H5v4H3V3zm12 0h6v6h-2V5h-4V3zM3 15h2v4h4v2H3v-6zm16 4h-4v2h6v-6h-2v4z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ExitFullscreenIcon = () => (
|
||||
export const ExitFullscreenIcon = () => (
|
||||
<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" />
|
||||
</svg>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 * as css from './styles.css';
|
||||
import { ChatButton, ControlDivider, MicrophoneButton, SoundButton, VideoButton } from './Controls';
|
||||
@@ -78,10 +78,7 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
|
||||
</Box>
|
||||
<Box grow="Yes" direction="Column" gap="200">
|
||||
{micDenied && (
|
||||
<Text
|
||||
size="T200"
|
||||
style={{ color: 'var(--tc-critical-high, #e53e3e)', textAlign: 'center' }}
|
||||
>
|
||||
<Text size="T200" style={{ color: color.Critical.Main, textAlign: 'center' }}>
|
||||
Microphone access is blocked. Enable it in your browser settings to join.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
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 { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
@@ -112,7 +124,7 @@ function EncryptedRoomCachePanel({ roomIds, onLoaded }: EncryptedRoomCachePanelP
|
||||
gap="200"
|
||||
style={{
|
||||
padding: config.space.S200,
|
||||
background: 'var(--bg-surface-variant)',
|
||||
background: color.SurfaceVariant.Container,
|
||||
borderRadius: config.radii.R300,
|
||||
}}
|
||||
>
|
||||
@@ -121,7 +133,7 @@ function EncryptedRoomCachePanel({ roomIds, onLoaded }: EncryptedRoomCachePanelP
|
||||
<Text size="T300" truncate>
|
||||
{room.name}
|
||||
</Text>
|
||||
<Text size="T200" style={{ opacity: 0.55 }}>
|
||||
<Text size="T200" priority="300">
|
||||
{msgEvents.length > 0
|
||||
? `${msgEvents.length} messages cached · oldest: ${new Date(oldest!.getTs()).toLocaleDateString()}`
|
||||
: 'No messages cached yet'}
|
||||
@@ -141,7 +153,7 @@ function EncryptedRoomCachePanel({ roomIds, onLoaded }: EncryptedRoomCachePanelP
|
||||
</Button>
|
||||
)}
|
||||
{!canLoadMore && events.length > 0 && (
|
||||
<Text size="T200" style={{ opacity: 0.5, flexShrink: 0 }}>
|
||||
<Text size="T200" priority="300" style={{ flexShrink: 0 }}>
|
||||
Fully cached
|
||||
</Text>
|
||||
)}
|
||||
@@ -644,7 +656,7 @@ export function MessageSearch({
|
||||
<Icon size="200" src={senderOnlyMode ? Icons.User : Icons.Lock} />
|
||||
<Text size="H5">{senderOnlyMode ? 'Messages from user' : 'Encrypted Rooms'}</Text>
|
||||
{!senderOnlyMode && (
|
||||
<Text size="T200" style={{ opacity: 0.55 }}>
|
||||
<Text size="T200" priority="300">
|
||||
{`${localResult.searchedRoomsCount} / ${localResult.encryptedRoomsCount} cached`}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -280,7 +280,8 @@ export function SearchInput({
|
||||
<Text
|
||||
size="T200"
|
||||
truncate
|
||||
style={{ opacity: 0.6, fontFamily: 'monospace', fontSize: '0.75em' }}
|
||||
priority="300"
|
||||
style={{ fontFamily: 'monospace', fontSize: '0.75em' }}
|
||||
>
|
||||
{user.userId}
|
||||
</Text>
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
Scroll,
|
||||
Spinner,
|
||||
Text,
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
config,
|
||||
} from 'folds';
|
||||
import { EventType, MatrixClient, MatrixEvent, MsgType, Room } from 'matrix-js-sdk';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import classNames from 'classnames';
|
||||
import { useNearViewport } from '../../hooks/useNearViewport';
|
||||
import { IEncryptedFile, IImageInfo, IThumbnailContent } from '../../../types/matrix/common';
|
||||
@@ -250,102 +253,112 @@ function Lightbox({
|
||||
});
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal
|
||||
aria-label="Media viewer"
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 1000,
|
||||
background: 'rgba(0,0,0,0.92)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{/* Header bar */}
|
||||
<Box
|
||||
alignItems="Center"
|
||||
gap="200"
|
||||
style={{
|
||||
padding: `${config.space.S200} ${config.space.S300}`,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||
flexShrink: 0,
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
clickOutsideDeactivates: false,
|
||||
escapeDeactivates: false,
|
||||
}}
|
||||
>
|
||||
<Box grow="Yes" direction="Column" style={{ overflow: 'hidden' }}>
|
||||
<Text size="T400" truncate style={{ color: '#fff', fontWeight: 500 }}>
|
||||
{item.body || (item.msgtype === MsgType.Video ? 'Video' : 'Image')}
|
||||
</Text>
|
||||
<Text size="T200" style={{ color: 'rgba(255,255,255,0.5)' }}>
|
||||
{item.sender} · {dateStr}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text size="T200" style={{ color: 'rgba(255,255,255,0.4)', flexShrink: 0 }}>
|
||||
{index + 1} / {items.length}
|
||||
</Text>
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
align="End"
|
||||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>Close</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal
|
||||
aria-label="Media viewer"
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 1000,
|
||||
background: 'rgba(0,0,0,0.92)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{(ref) => (
|
||||
<IconButton ref={ref} variant="Surface" aria-label="Close" onClick={onClose}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
</Box>
|
||||
{/* Header bar */}
|
||||
<Box
|
||||
alignItems="Center"
|
||||
gap="200"
|
||||
style={{
|
||||
padding: `${config.space.S200} ${config.space.S300}`,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Box grow="Yes" direction="Column" style={{ overflow: 'hidden' }}>
|
||||
<Text size="T400" truncate style={{ color: '#fff', fontWeight: 500 }}>
|
||||
{item.body || (item.msgtype === MsgType.Video ? 'Video' : 'Image')}
|
||||
</Text>
|
||||
<Text size="T200" style={{ color: 'rgba(255,255,255,0.5)' }}>
|
||||
{item.sender} · {dateStr}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text size="T200" style={{ color: 'rgba(255,255,255,0.4)', flexShrink: 0 }}>
|
||||
{index + 1} / {items.length}
|
||||
</Text>
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
align="End"
|
||||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>Close</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(ref) => (
|
||||
<IconButton ref={ref} variant="Surface" aria-label="Close" onClick={onClose}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
</Box>
|
||||
|
||||
{/* Media area with nav arrows */}
|
||||
<Box
|
||||
grow="Yes"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
style={{ overflow: 'hidden', padding: config.space.S400 }}
|
||||
>
|
||||
{index > 0 && (
|
||||
<IconButton
|
||||
variant="Surface"
|
||||
aria-label="Previous"
|
||||
onClick={prev}
|
||||
style={{ flexShrink: 0, marginRight: config.space.S200 }}
|
||||
{/* Media area with nav arrows */}
|
||||
<Box
|
||||
grow="Yes"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
style={{ overflow: 'hidden', padding: config.space.S400 }}
|
||||
>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
<Box
|
||||
grow="Yes"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
style={{ overflow: 'hidden', height: '100%' }}
|
||||
>
|
||||
<LightboxMedia
|
||||
key={`${item.mxcUrl}-${item.ts}`}
|
||||
item={item}
|
||||
useAuthentication={useAuthentication}
|
||||
/>
|
||||
</Box>
|
||||
{index < items.length - 1 && (
|
||||
<IconButton
|
||||
variant="Surface"
|
||||
aria-label="Next"
|
||||
onClick={next}
|
||||
style={{ flexShrink: 0, marginLeft: config.space.S200 }}
|
||||
>
|
||||
<Icon src={Icons.ArrowRight} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
{index > 0 && (
|
||||
<IconButton
|
||||
variant="Surface"
|
||||
aria-label="Previous"
|
||||
onClick={prev}
|
||||
style={{ flexShrink: 0, marginRight: config.space.S200 }}
|
||||
>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
<Box
|
||||
grow="Yes"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
style={{ overflow: 'hidden', height: '100%' }}
|
||||
>
|
||||
<LightboxMedia
|
||||
key={`${item.mxcUrl}-${item.ts}`}
|
||||
item={item}
|
||||
useAuthentication={useAuthentication}
|
||||
/>
|
||||
</Box>
|
||||
{index < items.length - 1 && (
|
||||
<IconButton
|
||||
variant="Surface"
|
||||
aria-label="Next"
|
||||
onClick={next}
|
||||
style={{ flexShrink: 0, marginLeft: config.space.S200 }}
|
||||
>
|
||||
<Icon src={Icons.ArrowRight} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
</FocusTrap>
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1106,7 +1106,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
<Text
|
||||
size="T200"
|
||||
style={{
|
||||
color: 'var(--tc-danger-normal)',
|
||||
color: color.Critical.Main,
|
||||
padding: '2px 6px',
|
||||
alignSelf: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
@@ -1119,7 +1119,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
<Text
|
||||
size="T200"
|
||||
style={{
|
||||
color: 'var(--tc-danger-normal)',
|
||||
color: color.Critical.Main,
|
||||
padding: '2px 6px',
|
||||
alignSelf: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
|
||||
@@ -241,7 +241,7 @@ export function ScheduleMessageModal({
|
||||
<Text size="L400">Send at</Text>
|
||||
<Box gap="200">
|
||||
<Box direction="Column" gap="100" style={{ flex: 1 }}>
|
||||
<Text as="label" htmlFor="schedule-date" size="T200" style={{ opacity: 0.7 }}>
|
||||
<Text as="label" htmlFor="schedule-date" size="T200" priority="400">
|
||||
Date
|
||||
</Text>
|
||||
<input
|
||||
@@ -253,7 +253,7 @@ export function ScheduleMessageModal({
|
||||
/>
|
||||
</Box>
|
||||
<Box direction="Column" gap="100" style={{ flex: 1 }}>
|
||||
<Text as="label" htmlFor="schedule-time" size="T200" style={{ opacity: 0.7 }}>
|
||||
<Text as="label" htmlFor="schedule-time" size="T200" priority="400">
|
||||
Time
|
||||
</Text>
|
||||
<input
|
||||
|
||||
@@ -140,17 +140,17 @@ export function ScheduledMessagesTray({ roomId }: ScheduledMessagesTrayProps) {
|
||||
>
|
||||
<Text
|
||||
size="T200"
|
||||
priority="400"
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{typeof msg.content.body === 'string' ? (msg.content.body as string) : '(message)'}
|
||||
</Text>
|
||||
<Text size="T200" style={{ opacity: 0.6, whiteSpace: 'nowrap', flexShrink: 0 }}>
|
||||
<Text size="T200" priority="300" style={{ whiteSpace: 'nowrap', flexShrink: 0 }}>
|
||||
{formatSendAt(msg.sendAt)}
|
||||
</Text>
|
||||
<IconButton
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from 'folds';
|
||||
import { Method } from 'matrix-js-sdk';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { SettingsSelect } from '../../../components/settings-select/SettingsSelect';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
@@ -482,9 +483,9 @@ function ProfileStatus() {
|
||||
opacity: statusMsg.length >= 56 ? 1 : 0.45,
|
||||
color:
|
||||
statusMsg.length >= 64
|
||||
? 'var(--tc-critical-normal)'
|
||||
? color.Critical.Main
|
||||
: statusMsg.length >= 56
|
||||
? 'var(--tc-warning-normal)'
|
||||
? color.Warning.Main
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
@@ -536,43 +537,20 @@ function ProfileStatus() {
|
||||
</Button>
|
||||
</Box>
|
||||
{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.
|
||||
</Text>
|
||||
)}
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Text size="T200" style={{ opacity: 0.6, whiteSpace: 'nowrap', flexShrink: 0 }}>
|
||||
<Text size="T200" priority="300" style={{ whiteSpace: 'nowrap', flexShrink: 0 }}>
|
||||
Auto-clear after:
|
||||
</Text>
|
||||
<select
|
||||
<SettingsSelect
|
||||
value={clearAfter}
|
||||
onChange={(e) => setClearAfter(e.target.value)}
|
||||
options={CLEAR_AFTER_OPTIONS}
|
||||
onChange={setClearAfter}
|
||||
aria-label="Auto-clear status after"
|
||||
style={{
|
||||
background: color.SurfaceVariant.Container,
|
||||
border: `1px solid ${color.SurfaceVariant.ContainerLine}`,
|
||||
borderRadius: config.radii.R300,
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
colorScheme: 'dark',
|
||||
fontSize: '0.82rem',
|
||||
padding: `${config.space.S100} ${config.space.S200}`,
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
}}
|
||||
>
|
||||
{CLEAR_AFTER_OPTIONS.map((opt) => (
|
||||
<option
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
style={{
|
||||
background: color.SurfaceVariant.Container,
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
</Box>
|
||||
{(presence?.status || statusMsg) && (
|
||||
<Button
|
||||
@@ -730,7 +708,7 @@ function ProfilePronouns() {
|
||||
</Button>
|
||||
</Box>
|
||||
{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.
|
||||
</Text>
|
||||
)}
|
||||
@@ -781,10 +759,6 @@ function ProfileTimezone() {
|
||||
);
|
||||
const saving = saveState.status === AsyncStatus.Loading;
|
||||
|
||||
const handleSelectChange = (evt: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setTimezone(evt.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setTimezone(savedTimezone);
|
||||
};
|
||||
@@ -813,39 +787,16 @@ function ProfileTimezone() {
|
||||
<Box direction="Column" grow="Yes" gap="100">
|
||||
<Box as="form" onSubmit={handleSubmit} gap="200" alignItems="Center" aria-disabled={saving}>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<select
|
||||
name="timezoneInput"
|
||||
aria-label="Timezone"
|
||||
<SettingsSelect
|
||||
value={timezone}
|
||||
onChange={handleSelectChange}
|
||||
options={[
|
||||
{ value: '', label: '— select timezone —' },
|
||||
...COMMON_TIMEZONES.map((tz) => ({ value: tz, label: tz })),
|
||||
]}
|
||||
onChange={setTimezone}
|
||||
disabled={saving}
|
||||
style={{
|
||||
background: color.SurfaceVariant.Container,
|
||||
border: `1px solid ${color.SurfaceVariant.ContainerLine}`,
|
||||
borderRadius: config.radii.R300,
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
colorScheme: 'dark',
|
||||
fontSize: '0.875rem',
|
||||
padding: `${config.space.S200} ${config.space.S300}`,
|
||||
width: '100%',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
}}
|
||||
>
|
||||
<option value="">— select timezone —</option>
|
||||
{COMMON_TIMEZONES.map((tz) => (
|
||||
<option
|
||||
key={tz}
|
||||
value={tz}
|
||||
style={{
|
||||
background: color.SurfaceVariant.Container,
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
}}
|
||||
>
|
||||
{tz}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
aria-label="Timezone"
|
||||
/>
|
||||
</Box>
|
||||
{hasChanges && !saving && (
|
||||
<IconButton
|
||||
@@ -873,7 +824,7 @@ function ProfileTimezone() {
|
||||
</Button>
|
||||
</Box>
|
||||
{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.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -37,12 +37,12 @@ function DecorationPreviewCell({
|
||||
width: CELL_SIZE,
|
||||
height: CELL_SIZE,
|
||||
flexShrink: 0,
|
||||
border: `2px solid ${selected ? 'var(--accent-cyan)' : 'transparent'}`,
|
||||
border: `2px solid ${selected ? color.Primary.Main : 'transparent'}`,
|
||||
borderRadius: '50%',
|
||||
background: 'var(--bg-surface-variant)',
|
||||
background: color.SurfaceVariant.Container,
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
boxShadow: selected ? '0 0 0 1px var(--accent-cyan)' : 'none',
|
||||
boxShadow: selected ? `0 0 0 1px ${color.Primary.Main}` : 'none',
|
||||
overflow: 'hidden',
|
||||
outline: 'none',
|
||||
}}
|
||||
@@ -142,7 +142,7 @@ export function ProfileDecoration() {
|
||||
height: CELL_SIZE,
|
||||
flexShrink: 0,
|
||||
borderRadius: '50%',
|
||||
background: 'var(--bg-surface-variant)',
|
||||
background: color.SurfaceVariant.Container,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
@@ -218,7 +218,7 @@ export function ProfileDecoration() {
|
||||
>
|
||||
{DECORATION_CATEGORIES.map((category) => (
|
||||
<div key={category.id} style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<Text size="L400" style={{ opacity: 0.7 }}>
|
||||
<Text size="L400" priority="400">
|
||||
{category.label}
|
||||
</Text>
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { DENOISE_MODELS } from '../../../utils/lotusDenoiseUtils';
|
||||
import {
|
||||
@@ -49,8 +49,8 @@ function DbMeter({ label, db, threshold }: { label: string; db: number; threshol
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: '12px',
|
||||
background: 'var(--bg-card)',
|
||||
border: '1px solid var(--border-color)',
|
||||
background: color.Surface.Container,
|
||||
border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
|
||||
borderRadius: '6px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
@@ -62,7 +62,7 @@ function DbMeter({ label, db, threshold }: { label: string; db: number; threshol
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: `${pct}%`,
|
||||
background: 'var(--accent-green)',
|
||||
background: color.Success.Main,
|
||||
transition: 'width 0.05s linear',
|
||||
}}
|
||||
/>
|
||||
@@ -74,7 +74,7 @@ function DbMeter({ label, db, threshold }: { label: string; db: number; threshol
|
||||
bottom: 0,
|
||||
left: `${markerPct}%`,
|
||||
width: '2px',
|
||||
background: 'var(--accent-orange)',
|
||||
background: color.Primary.Main,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -81,6 +81,7 @@ import { useDateFormatItems } from '../../../hooks/useDateFormat';
|
||||
import { playCallJoinSound } from '../../../utils/callSounds';
|
||||
import { previewRingtone, RINGTONE_OPTIONS } from '../../../utils/ringtones';
|
||||
import { DenoiseTester } from './DenoiseTester';
|
||||
import { SettingsSelect } from '../../../components/settings-select/SettingsSelect';
|
||||
|
||||
type ThemeSelectorProps = {
|
||||
themeNames: Record<string, string>;
|
||||
@@ -169,83 +170,6 @@ function SelectTheme({ disabled }: { disabled?: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
type SettingsSelectOption<T extends string> = { value: T; label: string; disabled?: boolean };
|
||||
|
||||
function SettingsSelect<T extends string>({
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
}: {
|
||||
value: T;
|
||||
options: SettingsSelectOption<T>[];
|
||||
onChange: (v: T) => void;
|
||||
}) {
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const selectedLabel = options.find((o) => o.value === value)?.label ?? value;
|
||||
|
||||
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleSelect = (v: T) => {
|
||||
onChange(v);
|
||||
setMenuCords(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
outlined
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
after={<Icon size="300" src={Icons.ChevronBottom} />}
|
||||
onClick={handleMenu}
|
||||
>
|
||||
<Text size="T300">{selectedLabel}</Text>
|
||||
</Button>
|
||||
<PopOut
|
||||
anchor={menuCords}
|
||||
offset={5}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setMenuCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||
isKeyBackward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
{options.map((opt) => (
|
||||
<MenuItem
|
||||
key={opt.value}
|
||||
size="300"
|
||||
variant={opt.value === value ? 'Primary' : 'Surface'}
|
||||
radii="300"
|
||||
disabled={opt.disabled}
|
||||
onClick={() => !opt.disabled && handleSelect(opt.value)}
|
||||
>
|
||||
<Text size="T300">{opt.label}</Text>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SystemThemePreferences() {
|
||||
const themeKind = useSystemThemeKind();
|
||||
const themeNames = useThemeNames();
|
||||
@@ -1372,8 +1296,8 @@ function Calls() {
|
||||
style={{
|
||||
padding: '16px',
|
||||
marginTop: '8px',
|
||||
borderTop: '1px solid var(--border-color)',
|
||||
background: 'var(--bg-card)',
|
||||
borderTop: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
|
||||
background: color.Surface.Container,
|
||||
}}
|
||||
>
|
||||
{/* ── Model selection ───────────────────────────────────────── */}
|
||||
@@ -1397,8 +1321,8 @@ function Calls() {
|
||||
style={{
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--border-color)',
|
||||
background: 'var(--bg-input)',
|
||||
border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
|
||||
background: color.SurfaceVariant.Container,
|
||||
}}
|
||||
>
|
||||
<Text size="T300">{selectedDenoiseModel.name}</Text>
|
||||
@@ -1436,7 +1360,7 @@ function Calls() {
|
||||
direction="Row"
|
||||
gap="100"
|
||||
style={{
|
||||
borderBottom: '1px solid var(--border-color)',
|
||||
borderBottom: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
|
||||
paddingBottom: '4px',
|
||||
}}
|
||||
>
|
||||
@@ -1489,7 +1413,10 @@ function Calls() {
|
||||
<Box
|
||||
direction="Column"
|
||||
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>
|
||||
<SettingTile
|
||||
@@ -1525,7 +1452,7 @@ function Calls() {
|
||||
step="1"
|
||||
value={callDenoiseGateThreshold}
|
||||
onChange={(e) => setCallDenoiseGateThreshold(parseInt(e.target.value, 10))}
|
||||
style={{ width: '100%', accentColor: 'var(--accent-orange)' }}
|
||||
style={{ width: '100%', accentColor: color.Primary.Main }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@@ -1535,7 +1462,10 @@ function Calls() {
|
||||
<Box
|
||||
direction="Column"
|
||||
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 & calibrate</Text>
|
||||
<Text size="T200" priority="300">
|
||||
@@ -1658,7 +1588,7 @@ function Calls() {
|
||||
value={ringtoneVolume}
|
||||
onChange={(e) => setRingtoneVolume(parseInt(e.target.value, 10))}
|
||||
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' }}>
|
||||
{ringtoneVolume}%
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { ChangeEventHandler, FormEventHandler, useCallback, useMemo, useState } from 'react';
|
||||
import { IPushRule, IPushRules, PushRuleKind } from 'matrix-js-sdk';
|
||||
import { Box, Text, Button, Input, config, IconButton, Icons, Icon, Spinner, Switch } from 'folds';
|
||||
import { SettingsSelect } from '../../../components/settings-select/SettingsSelect';
|
||||
import { useAccountData } from '../../../hooks/useAccountData';
|
||||
import { AccountDataEvent } from '../../../../types/matrix/accountData';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
@@ -193,10 +194,6 @@ function AddRuleForm({ kind, placeholder, label }: AddRuleFormProps) {
|
||||
setRuleId(evt.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleModeChange: ChangeEventHandler<HTMLSelectElement> = (evt) => {
|
||||
setMode(evt.target.value as NotificationMode);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box as="form" onSubmit={handleSubmit} direction="Column" gap="200">
|
||||
<Text size="T200" priority="300">
|
||||
@@ -217,24 +214,12 @@ function AddRuleForm({ kind, placeholder, label }: AddRuleFormProps) {
|
||||
/>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<select
|
||||
<SettingsSelect
|
||||
value={mode}
|
||||
onChange={handleModeChange}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: '1px solid currentColor',
|
||||
borderRadius: config.radii.R300,
|
||||
padding: `${config.space.S100} ${config.space.S200}`,
|
||||
color: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
}}
|
||||
>
|
||||
{ADD_MODES.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{MODE_LABELS[m]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
options={ADD_MODES.map((m) => ({ value: m, label: MODE_LABELS[m] }))}
|
||||
onChange={setMode}
|
||||
aria-label="Notification mode"
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
size="400"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React, { useEffect, useRef, CSSProperties } from 'react';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { color, config, Icon, IconButton, Icons } from 'folds';
|
||||
import { toastQueueAtom, dismissToastAtom, ToastNotif } from '../../state/toast';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { zIndices } from '../../styles/zIndex';
|
||||
|
||||
// Inject the keyframe animation once
|
||||
const STYLE_ID = 'lotus-toast-keyframes';
|
||||
@@ -29,6 +33,10 @@ type ToastCardProps = {
|
||||
|
||||
function ToastCard({ toast }: ToastCardProps) {
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -56,17 +64,29 @@ function ToastCard({ toast }: ToastCardProps) {
|
||||
dismiss(toast.id);
|
||||
};
|
||||
|
||||
const accent = toast.sticky ? color.Primary.Main : color.Surface.OnContainer;
|
||||
|
||||
const cardStyle: CSSProperties = {
|
||||
position: 'relative',
|
||||
background: 'var(--lt-bg-card)',
|
||||
border: toast.sticky
|
||||
? '1px solid var(--lt-accent-cyan-border)'
|
||||
: '1px solid var(--lt-border-color)',
|
||||
borderRadius: '12px',
|
||||
padding: '12px 14px',
|
||||
background: lotusTerminal ? 'var(--lt-bg-card)' : color.Surface.Container,
|
||||
border: `${config.borderWidth.B300} solid ${
|
||||
lotusTerminal
|
||||
? toast.sticky
|
||||
? 'var(--lt-accent-cyan-border)'
|
||||
: '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',
|
||||
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',
|
||||
animation: 'lotusToastIn 0.2s ease-out both',
|
||||
userSelect: 'none',
|
||||
@@ -75,8 +95,8 @@ function ToastCard({ toast }: ToastCardProps) {
|
||||
const rowStyle: CSSProperties = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginRight: '20px',
|
||||
gap: config.space.S200,
|
||||
marginRight: config.space.S500,
|
||||
};
|
||||
|
||||
const avatarStyle: CSSProperties = {
|
||||
@@ -91,19 +111,25 @@ function ToastCard({ toast }: ToastCardProps) {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
background: 'var(--lt-accent-orange-dim)',
|
||||
border: '1px solid var(--lt-accent-orange-border)',
|
||||
background: lotusTerminal ? 'var(--lt-accent-orange-dim)' : color.Primary.Container,
|
||||
border: `${config.borderWidth.B300} solid ${
|
||||
lotusTerminal ? 'var(--lt-accent-orange-border)' : color.Primary.ContainerLine
|
||||
}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
color: 'var(--lt-accent-orange)',
|
||||
color: lotusTerminal ? 'var(--lt-accent-orange)' : color.Primary.OnContainer,
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
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,
|
||||
fontSize: '0.85rem',
|
||||
overflow: 'hidden',
|
||||
@@ -111,22 +137,8 @@ function ToastCard({ toast }: ToastCardProps) {
|
||||
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 = {
|
||||
color: 'var(--lt-text-primary)',
|
||||
color: lotusTerminal ? 'var(--lt-text-primary)' : color.Surface.OnContainer,
|
||||
fontSize: '0.82rem',
|
||||
margin: '4px 0 2px',
|
||||
overflow: 'hidden',
|
||||
@@ -136,7 +148,7 @@ function ToastCard({ toast }: ToastCardProps) {
|
||||
};
|
||||
|
||||
const roomNameStyle: CSSProperties = {
|
||||
color: 'var(--lt-text-secondary)',
|
||||
color: lotusTerminal ? 'var(--lt-text-secondary)' : color.SurfaceVariant.OnContainer,
|
||||
fontSize: '0.75rem',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
@@ -161,14 +173,19 @@ function ToastCard({ toast }: ToastCardProps) {
|
||||
}}
|
||||
aria-label={`Notification from ${toast.displayName} in ${toast.roomName}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
style={dismissBtnStyle}
|
||||
onClick={handleDismiss}
|
||||
aria-label="Dismiss notification"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<span style={{ position: 'absolute', top: config.space.S100, right: config.space.S100 }}>
|
||||
<IconButton
|
||||
type="button"
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Surface"
|
||||
fill="None"
|
||||
onClick={handleDismiss}
|
||||
aria-label="Dismiss notification"
|
||||
>
|
||||
<Icon size="100" src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</span>
|
||||
<div style={rowStyle}>
|
||||
{toast.avatarUrl ? (
|
||||
<img src={toast.avatarUrl} alt="" style={avatarStyle} aria-hidden="true" />
|
||||
@@ -198,10 +215,10 @@ export function LotusToastContainer() {
|
||||
position: 'fixed',
|
||||
bottom: '1.5rem',
|
||||
right: '1.5rem',
|
||||
zIndex: 10001,
|
||||
zIndex: zIndices.toast,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
gap: config.space.S200,
|
||||
pointerEvents: 'auto',
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { CallEmbed } from '../plugins/call';
|
||||
import { CallEmbed, useCallControlState } from '../plugins/call';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { toastQueueAtom } from '../state/toast';
|
||||
@@ -9,17 +9,25 @@ const SILENCE_RMS_THRESHOLD = 0.008;
|
||||
const CHECK_INTERVAL_MS = 500;
|
||||
|
||||
/**
|
||||
* Monitors microphone audio while in a call. If the mic stays active but
|
||||
* silent for longer than the configured timeout, the mic is muted and a
|
||||
* toast is shown. Cleans up its own AudioContext and stream on unmount.
|
||||
* 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 toast
|
||||
* 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 {
|
||||
const [enabled] = useSetting(settingsAtom, 'afkAutoMute');
|
||||
const [timeoutMinutes] = useSetting(settingsAtom, 'afkTimeoutMinutes');
|
||||
const setToast = useSetAtom(toastQueueAtom);
|
||||
const { microphone } = useCallControlState(callEmbed?.control);
|
||||
|
||||
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 audioCtx: AudioContext | undefined;
|
||||
@@ -49,24 +57,20 @@ export function useAfkAutoMute(callEmbed: CallEmbed | undefined): void {
|
||||
const rms = Math.sqrt(buffer.reduce((sum, v) => sum + v * v, 0) / buffer.length);
|
||||
|
||||
if (rms > SILENCE_RMS_THRESHOLD) {
|
||||
// Audio detected — reset the silence timer
|
||||
// Audio detected — reset the silence timer.
|
||||
silenceStart = null;
|
||||
} else if (callEmbed.control.microphone) {
|
||||
// Mic is on but silent — start or advance the timer
|
||||
if (silenceStart === null) silenceStart = Date.now();
|
||||
else if (Date.now() - silenceStart >= timeoutMs) {
|
||||
callEmbed.control.setMicrophone(false);
|
||||
setToast({
|
||||
id: `afk-mute-${Date.now()}`,
|
||||
displayName: 'Lotus Chat',
|
||||
body: 'Your microphone was muted after inactivity.',
|
||||
roomName: 'Voice call',
|
||||
roomId: callEmbed.roomId,
|
||||
});
|
||||
silenceStart = null;
|
||||
}
|
||||
} else {
|
||||
// Mic is already muted — don't count silence
|
||||
} else if (silenceStart === null) {
|
||||
// Mic is unmuted (effect only runs while unmuted) but silent — start the timer.
|
||||
silenceStart = Date.now();
|
||||
} else if (Date.now() - silenceStart >= timeoutMs) {
|
||||
callEmbed.control.setMicrophone(false);
|
||||
setToast({
|
||||
id: `afk-mute-${Date.now()}`,
|
||||
displayName: 'Lotus Chat',
|
||||
body: 'Your microphone was muted after inactivity.',
|
||||
roomName: 'Voice call',
|
||||
roomId: callEmbed.roomId,
|
||||
});
|
||||
silenceStart = null;
|
||||
}
|
||||
}, CHECK_INTERVAL_MS);
|
||||
@@ -79,5 +83,5 @@ export function useAfkAutoMute(callEmbed: CallEmbed | undefined): void {
|
||||
stream?.getTracks().forEach((t) => t.stop());
|
||||
audioCtx?.close().catch(() => undefined);
|
||||
};
|
||||
}, [callEmbed, enabled, timeoutMinutes, setToast]);
|
||||
}, [callEmbed, enabled, timeoutMinutes, setToast, microphone]);
|
||||
}
|
||||
|
||||
+31
-37
@@ -1,7 +1,16 @@
|
||||
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 { OverlayContainerProvider, PopOutContainerProvider, TooltipContainerProvider } from 'folds';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
config,
|
||||
OverlayContainerProvider,
|
||||
PopOutContainerProvider,
|
||||
Text,
|
||||
toRem,
|
||||
TooltipContainerProvider,
|
||||
} from 'folds';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
@@ -18,6 +27,7 @@ import { LotusToastContainer } from '../features/toast/LotusToastContainer';
|
||||
import { useTauriNotificationBadge } from '../hooks/useTauriNotificationBadge';
|
||||
import { SeasonalEffect } from '../components/seasonal/SeasonalEffect';
|
||||
import { applyCustomAccent, removeCustomAccent } from '../utils/accentColor';
|
||||
import { zIndices } from '../styles/zIndex';
|
||||
|
||||
const FONT_MAP: Record<string, string> = {
|
||||
system: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||
@@ -86,7 +96,7 @@ function NightLightOverlay() {
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9998,
|
||||
zIndex: zIndices.nightLight,
|
||||
backgroundColor: `rgba(255, 140, 0, ${(settings.nightLightOpacity ?? 30) / 100})`,
|
||||
}}
|
||||
/>
|
||||
@@ -102,41 +112,25 @@ function App() {
|
||||
const portalContainer = document.getElementById('portalContainer') ?? undefined;
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary
|
||||
fallback={({ error, resetError }) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100vh',
|
||||
gap: '16px',
|
||||
fontFamily: 'sans-serif',
|
||||
padding: '24px',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
<ErrorBoundary
|
||||
fallbackRender={({ error, resetErrorBoundary }) => (
|
||||
<Box
|
||||
direction="Column"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
gap="400"
|
||||
style={{ height: '100vh', padding: config.space.S700, textAlign: 'center' }}
|
||||
>
|
||||
<h2 style={{ margin: 0 }}>Something went wrong</h2>
|
||||
<p style={{ margin: 0, color: '#666', maxWidth: '400px' }}>
|
||||
<Text size="H2">Something went wrong</Text>
|
||||
<Text size="T300" priority="300" style={{ maxWidth: toRem(400) }}>
|
||||
{error instanceof Error ? error.message : 'An unexpected error occurred.'}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetError}
|
||||
style={{
|
||||
padding: '8px 20px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: '#5865f2',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
</Text>
|
||||
<Button variant="Primary" onClick={resetErrorBoundary}>
|
||||
<Text as="span" size="B400">
|
||||
Try again
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<TooltipContainerProvider value={portalContainer}>
|
||||
@@ -171,7 +165,7 @@ function App() {
|
||||
</OverlayContainerProvider>
|
||||
</PopOutContainerProvider>
|
||||
</TooltipContainerProvider>
|
||||
</Sentry.ErrorBoundary>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useRouteError, isRouteErrorResponse } from 'react-router-dom';
|
||||
import { Box, Button, config, Text, toRem } from 'folds';
|
||||
|
||||
export function RouteError() {
|
||||
const error = useRouteError();
|
||||
@@ -11,33 +12,22 @@ export function RouteError() {
|
||||
: 'An unexpected error occurred.';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100dvh',
|
||||
gap: '16px',
|
||||
padding: '32px',
|
||||
fontFamily: 'sans-serif',
|
||||
}}
|
||||
<Box
|
||||
direction="Column"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
gap="400"
|
||||
style={{ height: '100dvh', padding: config.space.S700 }}
|
||||
>
|
||||
<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,
|
||||
}}
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
<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
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Global overlay stacking layers, centralized so floating Lotus UI doesn't
|
||||
* collide. (folds `Overlay`/`Dialog` modals resolve to 9999, which sits between
|
||||
* `nightLight` and `toast`.) Component-internal stacking uses small local
|
||||
* z-index values and is intentionally not listed here.
|
||||
*/
|
||||
export const zIndices = {
|
||||
/** In-call incoming-call banner — below seasonal/night-light/modals. */
|
||||
inCallBanner: 9990,
|
||||
/** Seasonal particle effect — below the night-light tint so particles tint. */
|
||||
seasonalEffect: 9997,
|
||||
/** Night Light tint overlay — above effects, below modals. */
|
||||
nightLight: 9998,
|
||||
/** Toasts — above everything, including modals. */
|
||||
toast: 10001,
|
||||
} as const;
|
||||
@@ -1,5 +1,4 @@
|
||||
/* eslint-disable import/first */
|
||||
import * as Sentry from '@sentry/react';
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { enableMapSet } from 'immer';
|
||||
@@ -7,31 +6,6 @@ import '@fontsource-variable/inter/index.css';
|
||||
import 'folds/dist/style.css';
|
||||
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();
|
||||
|
||||
import './index.css';
|
||||
|
||||
+1
-16
@@ -1,6 +1,5 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { sentryVitePlugin } from '@sentry/vite-plugin';
|
||||
import { wasm } from '@rollup/plugin-wasm';
|
||||
import inject from '@rollup/plugin-inject';
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||
@@ -261,20 +260,6 @@ export default defineConfig({
|
||||
react(),
|
||||
copyPdfWorker(),
|
||||
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({
|
||||
srcDir: 'src',
|
||||
filename: 'sw.ts',
|
||||
@@ -302,7 +287,7 @@ export default defineConfig({
|
||||
build: {
|
||||
target: 'esnext',
|
||||
outDir: 'dist',
|
||||
sourcemap: process.env.SENTRY_AUTH_TOKEN ? 'hidden' : false,
|
||||
sourcemap: false,
|
||||
copyPublicDir: false,
|
||||
// manualChunks must be in rolldownOptions (not rollupOptions) for Vite 8 / Rolldown
|
||||
rolldownOptions: {
|
||||
|
||||
Reference in New Issue
Block a user