From 93a78184d4bef9227efbf3f8a5127bd633a87052 Mon Sep 17 00:00:00 2001 From: Lotus Bot Date: Sat, 23 May 2026 00:28:37 -0400 Subject: [PATCH] feat: dark mode fix, call wallpaper, setTheme error handling, Sentry filter - CallEmbed: inject :root { color-scheme } into iframe so EC respects Cinny theme regardless of OS preference (fixes white background in dark mode) - CallEmbed: store themeKind, update color-scheme CSS on live setTheme() calls - CallEmbed: catch transport.send() rejection in setTheme() to prevent unhandled promise rejection when widget not ready yet (fixes REACT-8) - CallEmbed: html + body both set to background:none so wallpaper shows through - CallEmbedProvider: apply chatBackground wallpaper style to call embed container in full-view mode (not PiP) -- wallpapers carry over to calls - useCallEmbed: pass themeKind through to CallEmbed constructor - index.tsx: ignoreErrors: [Request timed out] to suppress matrixRTC heartbeat timeouts (REACT-9) from Sentry noise - README: document 0.19.4, positioning fix, dark mode fix, wallpaper, millify Rolldown interop fix, Sentry noise filter Co-Authored-By: Claude Sonnet 4.6 --- README.md | 11 ++++++-- src/app/components/CallEmbedProvider.tsx | 13 +++++++++ src/app/hooks/useCallEmbed.ts | 2 +- src/app/plugins/call/CallEmbed.ts | 36 ++++++++++++++++++------ src/index.tsx | 2 ++ 5 files changed, 52 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7210d0f1a..80b435c98 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ A full custom theme engine layered on top of Cinny's vanilla-extract theming: ### Voice / Video Call Improvements -- **Element Call 0.19.3**: Upgraded from 0.16.3. Dist copied to `public/element-call/` by vite at build time. +- **Element Call 0.19.4**: Upgraded from 0.16.3. Dist copied to `public/element-call/` by vite at build time. - **Camera default OFF**: Camera no longer persists across sessions via localStorage. Always starts disabled. Optional `cameraOnJoin` setting for explicit opt-in. - **Deafen button**: Tooltip corrected to "Deafen" / "Undeafen" (was "Turn Off Sound" / "Turn On Sound") - **Screenshare confirmation**: A confirm dialog appears before screenshare is broadcast to call participants @@ -63,6 +63,9 @@ A full custom theme engine layered on top of Cinny's vanilla-extract theming: - **Poll display**: `m.poll.start` events (both stable Matrix 1.7 `m.poll` content key and MSC3381 unstable `org.matrix.msc3381.poll.start`) render as read-only poll cards inside the standard message bubble — question and answer options shown. Registered as top-level event renderers AND inside the `EncryptedContent` callback so encrypted polls also display after decryption. "Open in Element to vote" note displayed. Implemented in `PollContent.tsx`. - **Deleted message placeholder**: Redacted `m.room.message`, `m.room.encrypted`, and `m.sticker` events no longer disappear from the timeline. Instead they reach the existing `RedactedContent` component (trash icon + italic "This message has been deleted" with reason if provided), matching Element, FluffyChat, Commet, and Nheko behaviour. One-line change in the `eventRenderer` filter in `RoomTimeline.tsx`. - **Picture-in-picture (PiP)**: When navigating away from a call room while in an active call, the call embed shrinks to a 280x158px floating window in the bottom-right corner. The PiP window is **draggable** — drag it anywhere on screen to move it out of the way. Clicking (without dragging) navigates back to the call room. Drag vs click distinguished by a 5px movement threshold; touch drag supported. Imperative style overrides on `callEmbedRef.current` via `useEffect` — a wrapper div cannot be used because `useCallEmbedPlacementSync` writes `top/left/width/height` directly onto that element. +- **Call embed positioning**: `useCallEmbedPlacementSync` uses `getBoundingClientRect()` (not `offsetTop/Left`) for accurate viewport-relative coordinates on the `position:fixed` container. Position is synced immediately on mount via `useEffect` in addition to the ResizeObserver, so the embed is placed correctly the instant the call view renders. The `[pipMode, callVisible]` effect in `CallEmbedProvider` only clears pip-specific styles when actually exiting pip mode — no longer clobbers the position set by `syncCallEmbedPlacement` on every `callVisible` toggle. +- **Dark mode in element-call**: After joining, `CallEmbed.applyStyles()` injects `:root { color-scheme: dark|light }` into the iframe document so `@media (prefers-color-scheme)` rules inside element-call resolve to the correct Cinny theme regardless of the OS system preference. `themeKind` is stored on the `CallEmbed` instance and updated on every `setTheme()` call, so live theme switching also re-injects the CSS. Without this, users with OS light mode would see a white background even when Cinny is in dark mode. +- **Call embed wallpaper**: The user's `chatBackground` pattern (Blueprint, Carbon, Stars…) is applied as the `backgroundImage`/`backgroundColor` of `div[data-call-embed-container]` when the call is in full view (not PiP). The iframe `html, body` is forced to `background: none !important` so the pattern shows through. When `chatBackground` is `none`, behaviour is unchanged. ### Messaging Enhancements @@ -116,6 +119,8 @@ Emoji reaction buttons styled for terminal mode via `button[data-reaction-key]` - **Authenticated media**: All avatar/media loads use `mxcUrlToHttp(mx, mxcUrl, useAuthentication, w, h, 'crop')` from `../../utils/matrix` — the Lotus utility that handles MSC3916 authenticated media. (Upstream Cinny uses the SDK method with incorrect argument order for authenticated endpoints.) - **Upstream tracking**: `git remote add upstream https://github.com/cinnyapp/cinny.git`. Merge strategy: `git fetch upstream && git merge upstream/main`. Daily check via `cinny-upstream-check.sh` on LXC 106 — notifies Matrix on new upstream commits. +- **Rolldown CJS interop — millify**: `src/app/plugins/millify.ts` uses a named import (`import { millify as millifyPlugin } from 'millify'`) instead of a default import. Rolldown's `__toESM` helper with `mode=1` sets `a.default = module_object` (not the function itself) when `hasOwnProperty` prevents the copy — calling `millifyPlugin()` would throw `(0, zc.default) is not a function`. Named import bypasses the interop entirely. +- **Sentry noise filter**: `ignoreErrors: ['Request timed out']` added to `Sentry.init` in `src/index.tsx` to suppress unhandled rejections from the matrixRTC delayed-event heartbeat (matrix-sdk) and the widget PostmessageTransport initial-load race (matrix-widget-api). Neither is actionable from client code. --- @@ -157,4 +162,6 @@ Built files are served from `/var/www/html/` on LXC 106 (nginx). Config lives at | `src/app/components/GifPicker.tsx` | GIF search + send | | `src/app/features/call/CallControls.tsx` | PTT badge + keybind logic | | `src/app/plugins/call/CallControl.ts` | EC widget bridge (screenshare revert, PTT mic) | -| `src/app/components/CallEmbedProvider.tsx` | PiP + draggable call embed | +| `src/app/components/CallEmbedProvider.tsx` | PiP + draggable call embed, call wallpaper carry-over | +| `src/app/plugins/call/CallEmbed.ts` | EC widget bridge: iframe setup, `color-scheme` dark/light injection, built-in control hiding, theme sync | +| `src/app/plugins/millify.ts` | Named import fix for Rolldown CJS interop (prevents `zc.default is not a function` crash) | diff --git a/src/app/components/CallEmbedProvider.tsx b/src/app/components/CallEmbedProvider.tsx index d3dc5ef83..5c8431fed 100644 --- a/src/app/components/CallEmbedProvider.tsx +++ b/src/app/components/CallEmbedProvider.tsx @@ -47,6 +47,10 @@ import { useMediaAuthentication } from '../hooks/useMediaAuthentication'; import { mxcUrlToHttp, getMxIdLocalPart } from '../utils/matrix'; import { RoomAvatar, RoomIcon } from './room-avatar'; import { useRoomNavigate } from '../hooks/useRoomNavigate'; +import { getChatBg } from '../features/lotus/chatBackground'; +import { useTheme, ThemeKind } from '../hooks/useTheme'; +import { useSetting } from '../state/hooks/settings'; +import { settingsAtom } from '../state/settings'; import { getStateEvent, getMemberDisplayName } from '../utils/room'; import { StateEvent } from '../../types/matrix/room'; import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels'; @@ -412,6 +416,14 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) { const pipMode = callActive && !inCallRoom; const { navigateRoom } = useRoomNavigate(); + const theme = useTheme(); + const isDark = theme.kind === ThemeKind.Dark; + const [chatBackground] = useSetting(settingsAtom, 'chatBackground'); + const wallpaperStyle = React.useMemo( + () => getChatBg(chatBackground, isDark), + [chatBackground, isDark], + ); + const pipDragRef = React.useRef<{ startX: number; startY: number; @@ -692,6 +704,7 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) { left: 0, width: '100%', height: '50%', + ...(callVisible && !pipMode ? wallpaperStyle : {}), }} ref={callEmbedRef} > diff --git a/src/app/hooks/useCallEmbed.ts b/src/app/hooks/useCallEmbed.ts index e23ebfded..94552c4ae 100644 --- a/src/app/hooks/useCallEmbed.ts +++ b/src/app/hooks/useCallEmbed.ts @@ -65,7 +65,7 @@ export const createCallEmbed = ( const controlState = pref && new CallControlState(forceAudioOff ? false : pref.microphone, pref.video, pref.sound); - const embed = new CallEmbed(mx, room, widget, container, controlState); + const embed = new CallEmbed(mx, room, widget, container, controlState, themeKind); return embed; }; diff --git a/src/app/plugins/call/CallEmbed.ts b/src/app/plugins/call/CallEmbed.ts index 362c08f90..68064974e 100644 --- a/src/app/plugins/call/CallEmbed.ts +++ b/src/app/plugins/call/CallEmbed.ts @@ -52,6 +52,8 @@ export class CallEmbed { private styleRetryObserver?: MutationObserver; + private themeKind: ElementCallThemeKind = 'dark'; + // Arrow-function class fields so dispose() passes the exact same reference to mx.off() private readonly boundOnEvent = (ev: MatrixEvent) => this.onEvent(ev); @@ -175,6 +177,7 @@ export class CallEmbed { widget: Widget, container: HTMLElement, initialControlState?: CallControlState, + themeKind: ElementCallThemeKind = 'dark', ) { const iframe = CallEmbed.getIframe( widget.getCompleteUrl({ currentUserId: mx.getSafeUserId() }), @@ -189,6 +192,7 @@ export class CallEmbed { this.room = room; this.iframe = iframe; this.container = container; + this.themeKind = themeKind; const controlState = initialControlState ?? new CallControlState(true, false, true); this.control = new CallControl(controlState, call, iframe); @@ -218,9 +222,17 @@ export class CallEmbed { } public setTheme(theme: ElementCallThemeKind) { - return this.call.transport.send(WidgetApiToWidgetAction.ThemeChange, { - name: theme, - }); + this.themeKind = theme; + const doc = this.document; + if (doc && this.joined) { + const styleEl = doc.getElementById('lotus-ec-styles'); + if (styleEl) styleEl.textContent = this.buildStyleContent(); + } + return this.call.transport + .send(WidgetApiToWidgetAction.ThemeChange, { name: theme }) + .catch(() => { + // Widget transport not ready yet; theme URL param is the fallback + }); } public hangup() { @@ -304,22 +316,28 @@ export class CallEmbed { this.control.forceState(this.initialState); } + private buildStyleContent(): string { + return [ + 'html, body { background: none !important; }', + `:root { color-scheme: ${this.themeKind}; }`, + '[style*="height: 0"][style*="z-index: 1"][style*="align-self: center"] { display: none !important; }', + ].join('\n'); + } + private applyStyles(): void { const doc = this.document; if (!doc) return; doc.body.style.setProperty('background', 'none', 'important'); - // Inject CSS for things that can't be reliably caught by DOM timing if (!doc.getElementById('lotus-ec-styles')) { const style = doc.createElement('style'); style.id = 'lotus-ec-styles'; - style.textContent = [ - 'body { background: none !important; }', - // Hide "using to Device key transport" status line - '[style*="height: 0"][style*="z-index: 1"][style*="align-self: center"] { display: none !important; }', - ].join('\n'); + style.textContent = this.buildStyleContent(); (doc.head ?? doc.body).appendChild(style); + } else { + const styleEl = doc.getElementById('lotus-ec-styles'); + if (styleEl) styleEl.textContent = this.buildStyleContent(); } // Hide EC built-in controls (we provide our own) diff --git a/src/index.tsx b/src/index.tsx index 5cef77a89..b307a8265 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -22,6 +22,8 @@ if (sentryDsn) { 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;