- N113: mutations compute from a local ref kept in sync with server echoes, and
writes serialize through a promise queue, so rapid add/remove no longer reads
a stale baseline and clobbers a prior write.
- N114: ReminderMonitor shows each toast once (firedRef) but retries the
account-data removal on later ticks if it fails (removingRef released on
error) — a failed removal no longer permanently swallows the reminder.
- N115: the 30s poll interval reads reminders/mDirects via refs and drops them
from the effect deps, so it's created once instead of resetting its countdown
on every reminder sync (which could indefinitely defer a near-due reminder).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- N122: setMediaState resolves on EC's transport ACK instead of waiting for a
DeviceMute state-echo that EC may elide or skip during teardown — which
previously stranded the promise forever and silently skipped the initial
deafen state + first StateUpdate on join. Dropped the single-slot
mediaStatePromiseResolver; onMediaState remains the authoritative sync path.
- N123: focusCameraParticipant now waits for a spotlight videoTile to mount via
a MutationObserver (with a 600ms hard-timeout fallback) instead of a fixed
2-frame delay that EC's React commit can exceed on slower devices.
- N126: PiP position restored from localStorage is shape+finiteness validated,
so corrupt data can't feed NaN into the position math (invalid 'NaNpx' CSS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add grow="Yes" to ChatBgGrid and SeasonalBgGrid containers so they
expand to fill their flex parent — without it the Box shrank to one
column (~76px wide) because folds Box defaults to display:flex and
the wrapper is a flex-row with no explicit width.
- Mark N4 (PollContent) FIXED ✅ VERIFIED in LOTUS_BUGS.md after
confirmed pass on default Cinny themes and Lotus TDS.
- Mark B1 and B4 PASS in LOTUS_TESTING.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add sticky?: boolean to ToastNotif — sticky toasts skip the 4s auto-dismiss
timer entirely, staying until the user clicks or manually dismisses. Sticky
toasts also use cyan accent/glow (vs orange for messages) and allow the body
to wrap rather than truncate, so longer action-oriented copy is fully readable.
Update the Tauri update toast to: sticky: true, ⬆ prefix on the title,
"Click to install and restart" as explicit call to action.
Fixes: auto-dismiss before user noticed it, no visual distinction from
a regular message notification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Security & data persistence (N97–N100): plaintext access token storage
detail, normal logout wiping user prefs via localStorage.clear(), sync
ERROR freezing the loading screen, unrestricted CSS classes on <pre>.
PWA/SW/notifications (N105–N109): missing SW notificationclick + push
handlers, decrypted E2EE message body leaked to OS notification center,
missing maskable PWA icon, auth media URLs producing 401 in notification
icon/badge fetches.
Lotus feature internals (N113–N120, N128): reminder read-modify-write
race, fire-and-forget removeReminder silently drops on network failure,
setInterval restart on every reminder state change, useCallSpeakers
rebuilds speaker set from mutation batch only (drops current speakers),
static NodeList misses mid-call tile additions, CDN outage silently wipes
decoration catalog, CDN URL drift between two source files, patch-folds
silent exit-0 when patch target not found.
Call system & noise suppression (N122–N127): setMediaState Promise hangs
forever if EC omits DeviceMute echo, focusCameraParticipant drops tile
click if spotlight isn't ready in 2 rAFs, denoise cleanup() leaks
AudioWorklet gateNode, postMessage wildcard '*' origin, PiP position
NaN on corrupt localStorage, denoise shim inactive in vite dev.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both Retry and Leave called the same dismiss function; Retry implied a
reconnect attempt that never happened. Collapsed to a single Back button
that honestly describes returning to the prescreen.
docs: correct Gemini audit entries — sanitize-html not DOMPurify (Claim A),
retract inaccurate LiveKit replaceTrack soundboard approach (Claim B,
contradicts confirmed cross-origin iframe constraint), expand N95 fix note
to clarify track-stop vs AudioContext-suspend distinction.
docs(testing): add L1 N95 reproduction guide; update A7 to reflect single Back button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review flagged that wrapping only the buttons left the dialog body copy
hardcoded (mixed-language dialogs once a non-en locale ships). Wrap the
remaining body/waiting strings ("Please accept…", "Confirm the emoji…",
"Do not Match", "Your device is verified.", etc.) and the PasswordStage
prompt, adding hooks to the sub-components that lacked one. Keys added to
en.json; all t() keys verified to resolve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CallEmbed watchdog now SELF-HEALS: a genuine ready/joined signal arriving
after the 25s timeout clears the error and notifies subscribers with
undefined, so a slow-but-successful EC load no longer strands the user on
the recovery screen over a live call. Listener dispatch wrapped in try/catch.
- ringtones: synth notes route through a per-session master gain; stop() ramps
it to 0 so the ring is silenced instantly on answer instead of letting the
last scheduled phrase ring out over call audio.
- IncomingCallBanner: ping fires exactly once per incoming call (guarded by
refEventId) instead of re-pinging when ringtone settings change mid-banner.
- focusCameraParticipant: try multiple tile selectors (EC labels vary by
version), defer the tile click past EC's async spotlight layout switch
(rAF x2), and dev-warn when no tile matches so testers get signal.
- uploadContent: a cancelled upload (mx.cancelUpload -> AbortError) is no
longer treated as retryable — previously the retry loop could resurrect an
upload the user just cancelled. Also retry on 408.
- addRoomIdToMDirect/removeRoomIdFromMDirect: guard against a corrupt m.direct
whose values aren't arrays.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wraps the hardcoded strings flagged in LOTUS_BUGS.md (Localization rows)
in t() via react-i18next, and adds the keys to public/locales/en.json
under the existing Organisms.* namespace. de.json intentionally left to
fall back to en for now (fallbackLng: 'en') rather than fabricate
translations.
Files: CreateRoomTypeSelector, ImageViewer, MsgTypeRenderers (MLocation),
Reply (ThreadIndicator), ImageContent, DeviceVerification (5 subcomponents),
UrlPreviewCard (DiscordCard), InviteUserPrompt, UploadBoard, PasswordStage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
N53: removed the duplicate lotusTerminal PTT-badge branch (raw <Box> with
--lt-* vars + bespoke rem/animation styling). The standard folds <Chip>
path now renders in all modes; TDS theming still flows through the CSS var
layer. Dropped the now-unused lotusTerminal read.
N81: ChatBgGrid / SeasonalBgGrid containers switched from flex-wrap with
fixed-width cells to a responsive CSS grid (repeat(auto-fill, minmax(76px,
1fr))), so swatches fill the row evenly instead of orphaning a lopsided
last row at arbitrary widths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sweeps every remaining "FIXED ⚠️ UNTESTED" item from LOTUS_BUGS.md and
LOTUS_TODO.md into the testing guide, grouped by environment (mobile,
theming, calls, media/perf, accessibility/screen-reader, desktop/Tauri,
features) so each category can be verified in one pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Poll answer buttons referenced undefined CSS vars (--accent-cyan,
--accent-cyan-dim, --accent-cyan-border, --border-color) plus hardcoded
rgba()/#fff and raw rem font sizes, so they rendered unstyled on every
non-TDS theme (invisible borders, no selected/progress state).
Replace all colors with always-defined folds tokens (Primary.* for the
selected/indicator state, SurfaceVariant.* for the resting surface +
progress fill), size/spacing/radii with config.* tokens, and the
checkbox/radio glyphs + percentage/label text with folds <Text>. The
progress-bar-behind-text affordance is preserved (folds Button has no
equivalent), now theme-reactive. Merged the duplicate checkbox/radio
indicator spans into one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously a second incoming call was dropped from the UI entirely when the
user was already in a call (`!joined && callInfo`). Now, when joined to a
different call, a compact corner banner (caller avatar + name + Answer/Reject)
is shown instead of the full-screen IncomingCall overlay, with a single soft
ping (one-shot ringtone) rather than the looping ring so it doesn't talk over
the active call. The full overlay still shows when not in any call; being in
the ringing room's own call still shows nothing.
Built with folds primitives + TDS tokens (no hardcoded colors).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#1 documented as implemented (focusCameraParticipant + MemberGlance
"Focus camera" menu); #4 ringtone selection landed, with the remaining
active-call non-intrusive-notification work scoped and deferred.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a ringtoneId setting (classic | chime | soft | retro | none) so the
incoming-call ring is no longer hardcoded to call.ogg. The three synth
styles are generated in-browser via a new utils/ringtones.ts module
(mirroring the existing callSounds.ts WebAudio pattern), so no new binary
assets are bundled; 'classic' keeps the existing call.ogg clip and 'none'
is a silent, visual-only incoming-call UI.
- ringtones.ts: startRingtone() loops until stopped; previewRingtone()
plays a single non-looping preview and auto-cancels the prior preview.
- IncomingCall: ring driven by the setting; <audio> element removed.
- Settings > Calls: Ringtone selector with on-select preview, beside the
existing Ringtone Volume slider.
- settings.ts: persisted value whitelisted back to a known id.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark items resolved by commits b7e1f89c / d2946c00 / 0394fce9 / 203568c9
as FIXED, and record the false-positives surfaced during the audit
(useMatrixEventRenderer null contract, Lobby getRoom already memoized,
RoomTimeline/RoomInput already wrapped by RoomView's ErrorBoundary).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
msgContent media load/thumbnail failures now log only the error name+message,
not the full error/event object that may carry content data.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CallEmbed: 25s load watchdog that fails fast on iframe error / preparing-error /
timeout instead of hanging on a permanent spinner; additive onLoadError API,
cleared on ready/capabilities/joined.
- CallView: user-visible "call failed to load" overlay with Retry/Leave (folds +
tokens) via a new useCallLoadError hook.
- CallMemberCard: wrap the participant avatar in AvatarDecoration so decorations
render in the call roster (the tile rendered UserAvatar bare while member lists
already wrapped it).
Addresses LOTUS_BUGS item 3 (avatar decorations in calls) and EC iframe failure monitoring.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- uploadContent: bounded retry (max 3) reusing rateLimitedActions' capped
exponential backoff; retries only transient failures (network/429/5xx), never 4xx.
- Robust MatrixError construction from UploadResponse / unknown error shapes.
- addRoomIdToMDirect/removeRoomIdFromMDirect: drop `as any`, use typed
EventType.Direct + MDirectContent.
- usePresenceUpdater: keep fetch({keepalive}) for the unload offline-presence
update (sendBeacon can't set the auth header) and log redacted warnings instead
of silently swallowing presence errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- RoomTimeline: wrap jump-to-latest/unread + mark-as-read handlers in useCallback
(the handlers passed to memoized message children were already memoized).
- RoomInput: wrap file/upload/emoji/sticker/location callbacks in useCallback so
the editor and toolbar don't re-render needlessly.
- EmojiBoard: hoist repeated mx.getRoom() pack-label lookups into a useMemo'd map
in the emoji and sticker sidebars (previously called per-render in map loops).
Behavior unchanged. (RoomTimeline/RoomInput already have ErrorBoundary wrappers
in RoomView, so no boundary added.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- N69: @mention highlight color now uses HexColorPickerPopOut + react-colorful
HexColorPicker behind a folds Button (color swatch); built-in onRemove
replaces the separate Reset, dropping the OS-native <input type="color">
- N10: mentionPulseKeyframes animates only box-shadow (dropped the imperceptible
scale(1.003)) so it no longer fights MsgAppearClass over `transform` on
self-sent @mention messages
- N22: Direct.tsx virtualizer estimateSize 38 -> 52 (two-line DM row height) to
avoid the initial-render jump before measureElement corrects each row
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract a shared ReportCategorySelect: folds Button trigger + PopOut +
FocusTrap + Menu + MenuItem (escape + arrow-key nav, like OrderButton),
replacing the OS-styled native <select> in both ReportRoomModal and
ReportUserModal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Selecting a join/leave sound auto-plays a preview, but nothing communicated
that. Add it to the SettingTile description.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both rendered as <Box as="form" role="dialog"> with manually assembled
background/borderRadius(R400)/boxShadow. Switch to <Dialog as="form"
variant="Surface"> so the surface comes from the design system (R300 radius),
matching the other message-action dialogs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ReportRoomModal/ReportUserModal rendered as <Box as="form" role="dialog">
with inline background/borderRadius(R400)/boxShadow. Switch both to
<Dialog as="form" variant="Surface"> so the surface (background, R300 radius,
shadow) comes from the design system, matching MessageReportItem and every
other message-action modal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- N14 ForwardMessageDialog: add folds <Header> with title + close IconButton
(was closeable only by clicking outside)
- N20 Notification presets: bare <button> with undefined --border-interactive-
normal / --bg-surface-low vars -> folds <Button variant="Secondary" fill="Soft">
- N68 syntaxHighlight tokenStyle: use the theme-aware --prism-* variable family
(keyword/selector/boolean/atrule/comment) instead of TDS-only --lt-accent-*
vars with dark-only Monokai fallbacks; comment uses --prism-comment not opacity
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pure formatting reflows (multi-line wrapping of long lines/imports/tables);
no behavior change. Clears the working tree of pending prettier diffs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The real reason the gallery didn't look or function like the Members drawer
or Saved Messages: it was a position:fixed overlay floating over the timeline,
mounted from RoomViewHeader. Now it docks into the room layout row exactly like
MembersDrawer.
- new mediaGalleryAtom (mirrors bookmarksPanelAtom) holds the open state
- RoomViewHeader toggles the atom instead of local useState and no longer
renders the panel
- Room.tsx renders <MediaGallery> as a flex sibling of the timeline with a
vertical Line separator on desktop and key={room.roomId} to reset per room
- MediaGallery.css: static width on desktop, position:fixed inset:0 full-screen
only on mobile (identical strategy to MembersDrawer.css); root Box shrink="No"
The panel now shares the row with the timeline instead of overlapping it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Media Gallery panel worked but didn't look like a first-party Cinny
drawer. Redesign the chrome to match MembersDrawer / Saved Messages and the
PolicyListViewer tab precedent:
- panel + header: Surface -> Background variant; header uses Text size="H4"
and a plain close IconButton (dropped the bespoke tooltip-wrapped button)
- tabs: moved into a bordered toolbar strip; adopt the repo's
variant={active?'Primary':'Secondary'} fill={active?'Solid':'Soft'} pattern
and show per-tab counts (Images (N) / Videos (N) / Files (N))
- month grouping: replaced the centered "lines + label" divider with a
left-aligned group label (the Cinny group-label pattern)
- thumbnail tiles: hover/focus border + caption overlay are now CSS-driven
(:hover / :focus-visible) instead of React state, and live in
MediaGallery.css.ts; grid + file rows tokenized
- caption overlay also reveals on keyboard focus (a11y)
All styling consolidated into MediaGallery.css.ts; no inline grid/tile styles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Avatar decorations: useAvatarDecoration cached ALL profile-field fetch
failures as "no decoration" permanently for the session. The member list
and timeline mount many avatars at once, so one rate-limited (429) burst
would wipe everyone's decoration until a full reload. Now only a genuine
404 (field unset) is cached; transient errors retry on the next mount.
Saved Messages panel — full redesign to match the canonical MembersDrawer:
- co-located BookmarksPanel.css.ts: toRem(266) + max-width:750px full-screen
media query, replacing the old position:absolute/zIndex:100 mobile "modal"
that had no backdrop or escape
- variant="Background" header; room avatars on each item (was a generic hash)
- priority tokens replace all raw opacity hacks; 3px borderLeft accent removed
- Escape-to-close; multi-line preview is now a proper folds Button (N38)
Media Gallery (N12): moved fixed positioning + width into MediaGallery.css.ts
using toRem(320) + a full-screen media query; border/header use config tokens;
added Escape-to-close on the panel (previously only the lightbox handled it).
Presence (SettingsTab / useUserPresence):
- N16: wrap presence-dot trigger in TooltipProvider; replace undefined
--bg-surface with color.Background.Container
- N17: add escapeDeactivates + isKeyForward/isKeyBackward to the FocusTrap
- N19: align reader labels (usePresenceLabel) to the setter vocabulary
(Online/Idle/Offline) so a chosen status matches the tooltip others see
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four changes to match screenshare full-screen UX for camera feeds:
1. Fullscreen button always visible
CallControls.tsx: remove `screenshare &&` gate — the ⛶ fullscreen
button now appears in camera-only calls, not just during screenshare.
2. Per-participant camera focus (CallControl.focusCameraParticipant)
Finds the target's video tile in the EC iframe DOM via:
[data-testid="videoTile"] / [data-video-fit]
closest ancestor of [aria-label="${userId}"]
Enables spotlight mode if not already active, then clicks the tile
so EC's internal focus handler runs. Falls back gracefully if the
tile is not in the DOM (camera off).
3. MemberGlance participant popup
Clicking a participant avatar in the call status bar now shows a
small menu: "Focus camera" (calls focusCameraParticipant) and
"View profile" (existing behaviour). Previously it opened the
profile immediately with no way to focus the camera.
4. PiP fullscreen button
A ⛶/⊡ icon button appears in the PiP overlay top-right area,
letting users go fullscreen directly from PiP mode without
navigating back to the call room first.
UNTESTED — requires a real multi-participant call to verify tile
clicking behaviour and fullscreen transitions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace raw error object logging (which may contain Matrix event
payloads, user IDs, or message bodies) with e.message-only strings
in three files:
- CallEmbed.ts: state update and event widget feed errors
- msgContent.ts: image/video element load failures and thumb errors
- RoomInput.tsx: GIF send failure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>