Commit Graph

428 Commits

Author SHA1 Message Date
jared b5e7bcc0b8 chore: prettier-normalize page/style.css.ts (pre-existing debt)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:50:32 -04:00
jared bca371ad38 feat(a11y): label the moderation reason input (P3-4)
Missed from the form-labels commit — aria-label on the shared kick/ban/invite
reason input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:46:35 -04:00
jared 21dda93d1b feat(a11y): focus return, typing announcement, shortcuts help (P3-4)
- Focus returns to the trigger when closing 4 genuine dialogs (room-topic
  viewer, reaction viewer, header topic, Search) — 20 inline popouts/menus
  correctly left as-is (returning focus to a hover target would be wrong).
- Typing indicator announced via a visually-hidden role="status" region;
  the visual text is aria-hidden to avoid double announcement.
- New keyboard-shortcuts help dialog (press ?, ignored while typing),
  mounted in ClientNonUIFeatures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:45:22 -04:00
jared 4380041014 feat(a11y): label form controls + overlays (P3-4)
Accessible names for ~15 controls that lacked them: invite/join/create-room/
account-data/image-pack/private-note/power-level inputs (visible <label htmlFor>
where a label exists, else aria-label); the two range sliders (night-light
intensity, noise-gate threshold); the soundboard file input; media <video>
elements; and the Media Gallery (region) + Search (dialog) overlays. Hidden
notification/preview <audio> marked aria-hidden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:45:21 -04:00
jared 8729ccfcf5 feat(a11y): message semantics for screen readers (P3-4)
- Each message is role="article"; collapsed messages (consecutive from one
  sender) now carry an aria-label with sender + time — previously a screen
  reader heard only the body with no attribution (the biggest a11y gap).
  Pure messageAriaLabel() reuses the existing time utils (+3 tests).
- Editing a message announces "Editing message from <sender>" (ariaLabel
  threaded MessageEditor → CustomEditor; the main composer is unaffected).
- System emoji get role="img" + aria-label from the shortcode; custom
  emoticons always have an accessible name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:45:21 -04:00
jared 96f7187031 perf(audit): emojibase lazy-split, SW precache, Prism subset, lazy images
- emojibase (~965 KB) is now fully lazy: plugins/emoji.ts loads compact data +
  shortcode maps via a memoized dynamic import (rejections reset the memo so a
  mid-deploy chunk 404 can retry); reaction labels degrade to the raw glyph
  until loaded. Consumers get FRESH array references on load (the module arrays
  populate in place — same-ref state updates would skip re-render and leave
  emoji search empty; reviewer-caught). Verified out of the eager graph.
- Service worker precaches hashed assets (workbox precacheAndRoute, 82 entries
  ~10.8 MB incl. the crypto wasm): repeat visits stop re-downloading the app.
  index.html is NOT precached — navigations stay network-first so deploys are
  picked up immediately; the media-auth fetch handler is untouched.
- ReactPrism: curated 21-language set — chunk 574 KB → 71 KB.
- Timeline inline images get loading="lazy".
- Removed dead dompurify (+types); sanitize-html is the real sanitizer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:19:16 -04:00
jared a9505ca5b2 feat(soundboard): shared room/space packs (like emoji/stickers), grid picker, management
CI / Build & Quality Checks (push) Successful in 10m56s
CI / Trigger Desktop Build (push) Successful in 8s
Soundboard v2 — a near-parallel of the custom-emoji image-pack system for
in-call audio clips.

- Data model: 3-tier packs mirroring MSC2545 — room/space pack (state event
  io.lotus.soundboard, inherited by child rooms via parent-space aggregation),
  global refs (io.lotus.soundboard_rooms), and the personal pack
  (io.lotus.soundboard account data; the v1 flat-list content is migrated to the
  pack shape on read). New plugins/soundboard/ (readers, SoundboardPack, utils) +
  hooks/useSoundboardPacks (useRelevantSoundboardPacks = user U global U room,
  deduped). Unit-tested (migration + slug).
- Management: reusable SoundboardPackEditor (name + emoji + per-clip volume +
  delete + upload + batched save), power-level-gated for room packs like emoji
  packs; a Soundboard page wired into Room + Space settings.
- In-call: CallSoundboard rewritten as a Discord-style grid grouped by pack
  (emoji + name tiles), sourcing room+parent-space U personal clips; a Manage
  toggle embeds the editors; per-clip volume x master volume on playback.
- Spam guard: host gates on a playing key (fork enforces one clip at a time).
- Control bar: Mute-Screenshare moved next to the Screenshare button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 23:21:50 -04:00
jared 501d493ca4 feat(threads): Slack-style per-thread notifications (P4-1)
Default = Participating: thread replies notify only when you've posted in the
thread or are @mentioned; per-thread override to All / Mentions-only / Mute via
a bell menu in the thread panel header. Modes sync across devices in
io.lotus.thread_notifications account data (pruned on write: left rooms, >180d,
cap 200/room). Muted threads: no notifications/sounds, chip badge suppressed
(+BellMute glyph), and their counts are subtracted from the room's sidebar
badge (client-side; clamped ≥0).

Also fixes the thread notification path itself: thread replies are now owned by
exactly ONE handler (room-level ThreadEvent.NewReply via a new useRoomsListener
hook, with per-thread dedupe, panel-aware focus suppression, and per-thread OS
tag coalescing) — the existing RoomEvent.Timeline handlers in the notifier and
the unread binder are explicitly thread-guarded, eliminating the previously
un-gated/double path. Room badges now also refresh live on
RoomEvent.UnreadNotifications (surgical per-room PUT; fixes thread-badge lag).

Pure decision core shouldNotifyThreadReply (13-case matrix) + prune + unread
subtraction: +32 tests (648 total). E2EE caveat documented: mentions-only may
under-notify pre-decryption (same class as the existing path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 22:39:10 -04:00
jared aa62df9c75 feat(threads): Thread Panel — full side drawer (P3-8)
Right-side thread drawer (MembersDrawer pattern; mobile fullscreen):
- ThreadPanel: header + close/Escape, ThreadTimeline, its own RoomInput
  (threadRootId prop; drafts/replies/uploads isolated per roomId::threadId;
  schedule + slash-commands off in threads v1) and threaded mark-as-read.
- ThreadTimeline: lean reimplementation over thread.liveTimeline — copied
  useTimelinePagination pattern (/relations back-pagination + decryption),
  virtualized, root event emphasized + "N replies" divider, reactions/edits/
  redactions, and a pending strip (chronological local echo never enters the
  thread timelineSet — rendered from LocalEchoUpdated instead).
- ThreadSummary chips on root messages (server-aggregated bundle or live
  Thread; unread badge via getThreadUnreadNotificationCount) keep threads
  discoverable now that replies leave the main timeline.
- Reply-in-Thread menu + thread indicators open the panel; deep links to
  thread events redirect into it.
- State: roomIdToActiveThreadIdAtomFamily + getThreadDraftKey (+18 tests).

Gates: tsc clean, eslint 0 errors, build OK, 616/617 tests (1 IDB skip).
Awaiting live QA; release note: threaded replies no longer render inline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:45:20 -04:00
jared ed51c39fe7 feat(messages): KaTeX math rendering (P4-4)
Renders LaTeX via spec data-mx-maths spans/divs (KaTeX render of the attr,
children as fallback) and conservative $…$ / $$…$$ text detection (escape-aware,
currency-guarded, never inside code/pre). KaTeX + CSS load lazily on first math
(ReactPrism pattern) — verified absent from the eager bundle. Sanitizer
unchanged by design (we render post-sanitize from attr/text; no incoming MathML
accepted). +14 unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:19:02 -04:00
jared 4509a2b6d3 feat(desktop): Tier B web side — toast actions, Focus Assist gate, folder DnD
- P5-41/35 useTauriToastActions: native rich-toast click → navigate(path) (opens
  the room), quick reply → mx.sendMessage(roomId, m.text). The desktop bridge
  routes message notifications (tag=roomId) to show_rich_toast.
- P5-56 useTauriFocusAssist + focusAssistActiveAtom: a native focus-assist-changed
  event drives the atom, OR'd into the existing quiet-hours gate in
  ClientNonUIFeatures so notifications+sounds suppress during Windows Focus Assist.
- P5-48 recursive folder drag-drop: fileEntries.ts (sync webkitGetAsEntry capture
  → async batched readEntries traversal, path-prefixed names, 500-file cap) wired
  into useFileDrop, reusing the existing upload pipeline. +3 unit tests.

Hooks no-op in the browser. Gates: tsc/eslint/prettier clean, build OK, 559 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:01:10 -04:00
jared aab7e5ae20 feat(desktop): Tier A desktop features — web side (P5-46/36/44/43/49/47/55/57)
Web half of the desktop feature wave. A shared bridge (`hooks/useTauri.ts`:
invokeTauri/isTauri/useTauriEvent) backs per-feature hooks that no-op in the
browser and drive the native Tauri commands (compiled in cinny-desktop):

- P5-46 useTauriCallPower — hold system awake while a call is active.
- P5-36 useTauriJumpList — Windows jump list of recent rooms → matrix: deep links.
- P5-44 useTauriThumbbar — taskbar Mute/Deafen/End; events toggle mic/sound/hangup.
- P5-43 useTauriSmtc — SMTC call state + button events.
- P5-49 useTauriNetwork — react to native network-change → mx.retryImmediately().
- P5-47 window chrome — opt-in `customWindowChromeAtom` + TDS `TitleBar`; DesktopChrome
  wrapper in App.tsx (zero layout impact when off) + a desktop-only settings toggle.
- P5-55 composer toolbar drag-reorder (settings order[] + pragmatic-drag-and-drop).
- P5-57 DraftIndicator — subtle "draft saved" cue in the composer.

Client-scoped hooks mount via TauriDesktopFeatures in ClientNonUIFeatures; window
chrome mounts at App level. Gates: tsc/eslint/prettier clean, build OK, 556 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:07:03 -04:00
jared 7c06b27c73 feat(call): in-call soundboard, quality controls, room call-permissions
CI / Build & Quality Checks (push) Successful in 10m49s
CI / Trigger Desktop Build (push) Successful in 8s
Element Call is now consumed as our self-built fork
(@lotusguild/element-call-embedded); wire up its previously-dormant
capabilities and document the fork as live.

Soundboard (P5-15): a call-bar button plays user-uploaded audio clips into the
call as a real published track (io.lotus.inject_audio) plus local playback.
Clips are uploadable like emoji/sticker packs, stored in io.lotus.soundboard
account data (synced across devices). Gated by a Settings toggle + volume.

Quality controls (P5-31): per-user mic/screenshare bitrate + screenshare
framerate (Settings -> Calls), applied via io.lotus.set_quality clamped to any
room cap. Room admins set caps and hard call-permissions (allow_screenshare /
allow_camera) in Room Settings -> Voice; the call bar hides blocked buttons.

- New: CallSoundboard, useSoundboard, soundboardClips; RoomQuality,
  useCallQuality, callQuality (+ unit tests).
- Optimistic-write RoomQuality admin UI (no stale-state clobber).
- Docs: mark EC fork live across README/FEATURES/TODO/BUGS/TESTING; add D2
  manual-test steps.

Numeric quality caps are client-cooperative; screenshare/camera permissions are
hard-enforced server-side (see LotusGuild/matrix voice-limit-guard).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 22:34:17 -04:00
jared 26f998d243 feat(seasonal): redesign all 11 seasonal themes as modular per-theme overlays
CI / Build & Quality Checks (push) Successful in 11m7s
CI / Trigger Desktop Build (push) Successful in 12s
Split the 808-line SeasonalEffect monolith into one self-contained module per
theme under seasonal/themes/ (<Theme>.tsx + <Theme>.css.ts), and gave every
theme a premium, research-backed redesign (one Opus agent per theme against a
shared brief). SeasonalEffect now just imports the 11 overlays and dispatches;
the orphaned shared Seasonal.css.ts is removed (each theme owns its keyframes).

Each overlay: layered oklch palettes, GPU-only animation (transform/opacity),
`contain: layout paint style` to kill repaint flicker, ≤~40-element perf budget,
particles seeded once via useMemo (no per-frame state), a gorgeous STATIC
prefers-reduced-motion form (the settings preview thumbnail), WCAG-AA-preserving
low opacities, and no new deps / no external assets (inline SVG data-URIs,
Tauri/CSP-safe).

Themes: Halloween, Christmas, New Year, Autumn, April Fools, Lunar New Year,
Valentines, St. Patrick's, Earth Day, Deep Space, Arcade.

Gates: tsc clean, ESLint clean, Prettier clean, build OK, 551 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 19:41:58 -04:00
jared f816049fdf feat(seasonal): show Auto activation dates in settings + single-source schedule
Settings never told the user which days "Auto" turns each seasonal theme on.
Extracted the date windows out of getActiveSeason into a shared SEASON_SCHEDULE
(seasonSchedule.ts) — the single source of truth for both the runtime Auto
selector and the settings UI, so displayed dates can't drift from real activation.

- seasonal/types.ts: SeasonTheme + SeasonalOverlayProps (leaf module).
- seasonal/seasonSchedule.ts: priority-ordered SEASON_SCHEDULE with human date
  ranges + SEASON_DATE_RANGES + getActiveSeason (behavior-preserving refactor).
- SeasonalEffect.tsx: consume the shared type/selector; re-export SeasonTheme.
- General.tsx: per-theme date caption under each swatch ("Oct 15 – Nov 1"), Auto
  reads "By calendar", and the section description explains it.
- seasonSchedule.test.ts (6): representative day per theme, overlap priority
  (Deep Space > Autumn, New Year > Lunar), inclusive boundaries, off-season null.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 19:28:28 -04:00
jared 5ef0a1fd3e fix(call): ringtone loudness, caller decline notice, All-Muted badge
Three issues from live testing:
- A1: the 'classic' ringtone (call.ogg, mastered near full scale) was much
  louder than the synthesized styles. Attenuate it (CLASSIC_GAIN 0.45) so all
  ringtones sit at a comparable level.
- A3/A4: the caller had no indication when a DM/group callee declined — their
  UI kept "ringing" until the notification lifetime expired. IncomingCallListener
  now listens for RTCDecline events for a call we're hosting in the room and
  toasts the caller ("<name> declined your call").
- G1: the PiP "All muted" badge fired when any single remote participant muted.
  useRemoteAllMuted now returns true only when there is >=1 remote and every
  remote participant is muted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 19:13:40 -04:00
jared 2d71f2ce30 refactor(ui): name the global overlay z-index layers (native-cinny nit)
Centralized the global floating-UI stacking values into styles/zIndex.ts
(inCallBanner 9990 < seasonalEffect 9997 < nightLight 9998 < toast 10001;
folds modals sit at 9999 between). Same values, no behavior change — just
removes the magic numbers and documents the layering so future overlays don't
collide. Component-internal small z-index stays local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 16:10:29 -04:00
jared c7a04dcc70 fix(ui): poll checkmark uses folds Icon instead of Unicode glyph (native-cinny nit)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 15:20:52 -04:00
jared c5d7fcc303 fix(ui): timezone picker uses folds SettingsSelect (native-cinny audit 7/N)
Replaced the last raw native <select> (Profile timezone, colorScheme:'dark')
with SettingsSelect. Added an optional `disabled` prop to SettingsSelect for
the saving state. handleSubmit reads the `timezone` state (not the native form
field) so submission is unaffected; the now-unused handleSelectChange was
removed. No raw <select> elements remain in the settings UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:51:13 -04:00
jared d5ce56930b refactor(ui): extract shared SettingsSelect; replace raw <select> (native-cinny audit 6/N)
Extracted the folds-native dropdown (Button+PopOut+Menu) from General.tsx into a
shared components/settings-select/SettingsSelect.tsx, and used it to replace raw
native <select> elements (which render OS-styled and broke under non-default
themes via colorScheme:'dark'):
- Profile "auto-clear after" select
- PushRuleEditor add-rule mode select (dropped the now-unused handleModeChange)

The form-tied timezone <select> in Profile is left for a follow-up (it's wired
to native form submission + a disabled state and needs more care).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:43:18 -04:00
jared 349194e7e5 fix(ui): folds primitives for RouteError + PiP fullscreen button (native-cinny audit 5/N)
CI / Build & Quality Checks (push) Successful in 10m33s
CI / Trigger Desktop Build (push) Successful in 21s
- RouteError: raw <div>/<h2>/<p>/<button> (sans-serif, raw px) -> folds
  Box/Text/Button with config tokens.
- CallEmbedProvider PiP fullscreen control: raw <button> with ⊡/⛶ glyphs ->
  folds IconButton reusing the exported FullscreenIcon/ExitFullscreenIcon SVGs
  from Controls (consistent with the main fullscreen button). The intentional
  dark over-video scrim is kept.

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:54:42 -04:00
jared 49d9410e3a fix(calls): resolve EC mute hang, robust camera focus, PiP NaN guard (N122/N123/N126)
- 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>
2026-06-28 09:17:19 -04:00
jared 8912423aeb i18n: complete DeviceVerification + PasswordStage dialog translation
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>
2026-06-26 18:15:51 -04:00
jared bc85cd4984 fix(calls,matrix): address review findings from agent code review
- 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>
2026-06-26 18:15:51 -04:00
jared 1a5896ef84 i18n: localize hardcoded UI strings across 10 components
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>
2026-06-26 17:43:36 -04:00
jared caf6318a5d fix(poll): render vote buttons with folds tokens (N4)
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>
2026-06-25 04:02:13 -04:00
jared c67aed01dc feat(calls): non-intrusive incoming-call banner while already in a call (#4b)
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>
2026-06-24 17:45:18 -04:00
jared 4a87588435 feat(calls): selectable incoming-call ringtone (#4)
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>
2026-06-24 17:25:32 -04:00
jared b7e1f89c1d perf(room): memoize timeline/composer handlers and emoji-pack room lookups
- 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>
2026-06-24 08:22:00 -04:00
jared c0f9867218 Merge upstream v4.12.3 (Element Call 0.20.1) into lotus
CI / Build & Quality Checks (push) Successful in 10m42s
CI / Trigger Desktop Build (push) Successful in 10s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 04:11:41 -04:00
Krishan e37971456a chore(deps): update dependency element-call from 0.19.1 to v0.20.1 (#2992)
* chore(deps): update dependency @element-hq/element-call-embedded from 0.19.1 to v0.20.1

* fix reaction button click

* fix call settings button query

* fix screenshare and spotlight button state update

* fix incomming call text alignment

* prevent displaying call notification for voice rooms

* disable mic/video icon when toggle in progress

* update matrix-js-sdk

---------

Co-authored-by: Ajay Bura <32841439+ajbura@users.noreply.github.com>
2026-06-20 21:20:25 +10:00
jared 79f8fabb1b fix(ui): GIF picker surface tokens + background swatch chrome (N8, N70)
CI / Build & Quality Checks (push) Successful in 10m46s
CI / Trigger Desktop Build (push) Successful in 6s
- N8: GifPicker non-TDS container used undefined var(--bg-surface) + raw
  rgba/12px/boxShadow. Switch to folds tokens (color.Surface.Container,
  config.radii.R400, color.Surface.ContainerLine, color.Other.Shadow).
  TDS branch keeps its --lt-* glow chrome.
- N70: ChatBgGrid/SeasonalBgGrid swatch buttons moved chrome (radius, border,
  hover, keyboard :focus-visible ring, selected via data-selected) into shared
  BgSwatch.css.ts using design tokens; only per-swatch size + live preview
  background stay inline (custom preview tiles, not MenuItem/Chip candidates).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:01:57 -04:00
jared dfd2c9c49e fix(ui): mention color picker, send-animation conflict, DM virtualizer (N69,N10,N22)
- 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>
2026-06-19 20:54:32 -04:00
jared b361d43088 fix(ui): native inputs/checkboxes, QR fallback, focus + report modal cleanup
- N23 RoomServerACL: raw text input -> folds Input; raw checkbox -> folds Checkbox
- N24 PolicyListViewer: raw room-id input -> folds Input (Critical variant on error)
- N25 ExportRoomHistory: raw <input type="date"> x2 -> folds Input
- N26 RoomShareInvite: QR <img> gets loading="lazy" + onError fallback card
  ("QR code unavailable") instead of a broken-image icon
- N27 GifPicker: FocusTrap returnFocusOnDeactivate:false (matches EmojiBoard)
- N76 Report modals: drop redundant Cancel button (dismiss via header x /
  click-outside, like MessageReportItem)
- N5 ReadReceiptAvatars: hover/focus moved to co-located css :hover/:focus-visible
  (removed JS onMouseEnter/Leave .style mutation)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 18:12:25 -04:00
jared a33d28a7ae style: apply Prettier formatting to remaining files
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>
2026-06-19 16:41:57 -04:00
jared c54cb126ff fix(ui): settings modal sizing regression + 17 more folds audit findings
CI / Build & Quality Checks (push) Successful in 10m47s
CI / Trigger Desktop Build (push) Successful in 5s
Fix settings modal regression: Modal500 was wrapped in useModalStyle(560),
forcing maxWidth 560px and squishing the two-pane Settings layout (folds
size="500" is ~50rem). Restore desktop width to the folds recipe while
keeping mobile fullscreen.

N-series fixes:
- N13 ScheduledMessagesTray header: <Box as="button"> -> folds <Button>
- N28 composer char counter: drop undefined --tc-surface-low + opacity,
  use priority="300" and config.space token
- N31 collapsible "Read more" toggle: padded <Button> -> flush inline-button
  pattern matching (edited) link
- N41 UserPrivateNotes "Saving..." now shows a folds <Spinner>
- N43 Night Light slider: add accentColor; label opacity -> priority
- N44 mention-highlight Reset: bare <button> -> folds <Button> (drops
  undefined --border-interactive-normal); Boot button kept (TDS-only)
- N45 SelectTheme trigger variant -> Secondary to match SettingsSelect
- N49 RoomInsights StatTile emoji -> folds <Icon> (Photo/VideoCamera/
  Headphone/File)
- N54/N57 PiP overlay badges + fullscreen button: token discipline
  (config.radii/space, folds Text); dark scrim kept for video legibility
- N60 knock badge: match Pinned Messages pattern (no wrapper div, toRem
  insets, no hardcoded size overrides)
- N62 unverified-device banner: 3px left-accent -> standard border via
  color.Warning.ContainerLine; drop opacity hacks
- N65 Edit History: real "Load more" pagination (accumulate next_batch,
  de-dupe by id, re-sort by ts) replacing passive text
- N66 search date fields: raw <input type="date"> -> folds <Input>
- N67 SeasonalEffect z-index 9999 -> 9997 (below Night Light + modals)
- N73 Pending Requests header uses css.MembersGroupLabel
- N74 remove raw em-sized emoji <span> in RoomNavItem name
- N85/N86 RemindMeDialog: <Box role="dialog"> -> folds <Dialog>; preset
  MenuItems -> Buttons (fixes invalid menuitem-in-dialog ARIA)

Document deliberate WON'T FIX rationale for N9, N51, N61, N71, N75, N77.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 00:15:35 -04:00
jared 8dc4c4d072 fix(ui): resolve 29 native UI/UX inconsistencies from folds design audit
CI / Build & Quality Checks (push) Successful in 10m25s
CI / Trigger Desktop Build (push) Successful in 6s
Fixes N1–N94 findings from LOTUS_BUGS.md audit pass. Key changes:

- ProfileDecoration: raw <button> → folds <Button> for save/remove; remove
  undefined --accent-cyan var
- UserRoomProfile: textarea border uses color.SurfaceVariant.ContainerLine
  and config tokens instead of undefined --border-interactive var
- LotusToastContainer: z-index raised from 9997 → 10001 so toasts appear
  above Night Light overlay (9998) and modals (9999)
- Message.tsx: DeliveryStatus replaces Unicode glyphs with Icon components;
  MessageQuickReactions returns null instead of <span />; forward menu item
  gets correct size="100" on after icon
- AudioContent: speed chip variant/radii now matches Play chip (Secondary/300)
- ReadReceiptAvatars: pill border/radius/padding → folds config tokens;
  remove dead receipt-pill-btn className
- EventReaders: Header size 600→500; close button gets radii="300";
  borderBottom shorthand → borderBottomWidth token; remove raw fontSize
- General.tsx: selected background/seasonal picker border uses
  color.Primary.Main instead of color.Critical.Main (error red)
- RoomInsights: SectionHeader drops textTransform/letterSpacing/opacity;
  chart borderRadius → config tokens; remove raw fontSize:9;
  warning banner → SequenceCard
- RoomProfile.tsx: formatting toolbar raw <button> → folds <Button>;
  topic read-mode renders formatted_body via sanitizeCustomHtml
- MsgTypeRenderers: location Open button Chip→Button; opacity:0.65→priority
- UploadCardRenderer: caption raw <input> → folds <Input>
- VoiceMessageRecorder: replace undefined --bg-surface-variant/--tc-*
  vars with color.* tokens; replace bare <audio controls> with
  IconButton play/pause toggle
- App.tsx: mention highlight uses WCAG 2.1 relative luminance (gamma
  linearization) instead of simplified approximation; border now rgba
  semi-transparent instead of same color as background
- RoomNavItem: Mute MenuItem icon moved to before prop
- SearchFilters: HasLink chip variant="Success" outlined to match filter bar
- RoomViewHeader: Server Notice chip radii Pill→300; fix jotai import order
- Fix ESLint import/order errors in DeviceVerificationSetup, RoomTopicViewer,
  MediaGallery, and RoomViewHeader

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:46:19 -04:00
jared 9742eaea28 feat(call): full-screen camera broadcast parity
CI / Build & Quality Checks (push) Successful in 10m22s
CI / Trigger Desktop Build (push) Successful in 7s
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>
2026-06-18 20:39:58 -04:00
jared 9deeef6e8d fix(pip): correctly identify whose mic is muted in PiP overlay
Previously PipMuteOverlay fired on useRemoteAllMuted (any remote
muted) and rendered in the bottom-left corner — the conventional
position for local-user mic status — causing users to think their
own mic was muted when it wasn't.

Fix: split into two distinct indicators
- Bottom-left: local mic muted only (from useCallControlState),
  labelled "You" so attribution is unambiguous
- Top-right: "All muted" warning (warning color, not critical) when
  all remote participants are muted

UNTESTED — verify in a real call at chat.lotusguild.org.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 19:23:12 -04:00
jared e2b957b6bd fix: resilience, speaker animation, CSS variable fixes
CI / Build & Quality Checks (push) Successful in 10m39s
CI / Trigger Desktop Build (push) Successful in 6s
- Wrap RoomTimeline in ErrorBoundary — a single bad event no longer
  crashes the entire timeline; shows a graceful "Timeline unavailable"
  message instead
- Wrap RoomInput in ErrorBoundary — composer crashes show a fallback
  placeholder rather than a blank white section
- Animate SpeakerAvatarOutline with a 1.2s pulse keyframe so it's
  visually distinct from a static ring; respects prefers-reduced-motion
- Fix var(--border-surface-variant) undefined variable in UserRoomProfile
  device session rows; replaced with color.SurfaceVariant.ContainerLine

UNTESTED — verify at chat.lotusguild.org post-deploy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 19:19:54 -04:00
jared a77c4b6db5 feat(calls): configurable ringtone volume in Settings (Bug #4 partial)
CI / Build & Quality Checks (push) Successful in 10m29s
CI / Trigger Desktop Build (push) Successful in 15s
Adds 'Ringtone Volume' slider (0–100, default 70%) to Settings → Calls.
The IncomingCall audio element reads the setting and applies it as
audioElement.volume before playing, replacing the implicit browser
default of 1.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:56:04 -04:00
jared cb3d2c40e5 fix(a11y): descriptive aria-label on reaction buttons (P3-4)
Reaction.tsx now computes aria-label='{shortcode} reaction, N people'
using getShortcodeFor so screen readers announce emoji name and count
instead of an ambiguous button. Custom (mxc://) emoji falls back to
'custom emoji reaction'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:50:19 -04:00
jared 7d98b49a30 fix(a11y): add aria-label to exit-formatting and pin-menu buttons (P3-4)
Two icon-adjacent buttons were missing descriptive labels: the
"Exit formatting" key-symbol button in Toolbar.tsx and the "Pinned
messages" pin icon in RoomViewHeader.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:41:20 -04:00
jared 2b5c6fd606 perf(media): center-top focal point on cover-fit thumbnails (P5-6)
Adds objectPosition:'center top' to all cover-fit thumbnail surfaces so
portrait images show faces/subjects instead of the center-slice when
the 600px AttachmentBox height cap forces cropping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:33:36 -04:00
jared 1b4c6cab6d fix(a11y): add missing aria-labels to message buttons
- FallbackContent: '(edited)' button now has aria-label='View edit history'
- Reply: ThreadIndicator as=button gets aria-label='View thread'
- Reply: ReplyLayout as=button gets aria-label='Jump to original message'

Addresses WCAG 2.1 SC 4.1.2 (Name, Role, Value) for icon-only buttons
and polymorphic div-as-button patterns in the message timeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:14:01 -04:00
jared 176d5d0bb7 fix(mobile): apply useModalStyle to remaining dialog files (Bug #9)
Completes the mobile fullscreen modal pass — adds useModalStyle to
DeviceVerificationSetup, DeviceVerificationReset, AddExistingModal,
RoomEncryption prompt, RoomUpgradeDialog, Modal500, ReadReceiptAvatars,
and RoomTopicViewer. All floating Dialog/Modal components now go
fullscreen on mobile (≤750px). UIAFlowOverlay was already fullscreen
via <Overlay>; JoinRulesSwitcher/RoomNotificationSwitcher are dropdowns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:11:24 -04:00
jared 3df95adc52 perf(media): defer image/video decryption until near-viewport (P5-5)
Creates useNearViewport hook (IntersectionObserver, 200px rootMargin,
one-shot disconnect after first trigger). ImageContent and VideoContent
now gate loadSrc() on nearViewport — when autoPlay is enabled, encrypted
media is not decrypted until the element is within 200px of the visible
area, reducing initial page load cost on long timelines.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 17:56:34 -04:00
jared baa12823f7 fix: exponential backoff for 429 retries, cover thumbnails, 2 more modal dialogs
- matrix.ts: rateLimitedActions fallback delay now uses capped exponential
  backoff (min(1000 * 2^n, 30s)) instead of flat 3000ms when server omits
  Retry-After; server header still takes precedence
- RenderMessageContent: add objectFit:cover + 100% fill to video thumbnail
  <Image> so thumbnails fill their container without letterboxing (P5-6)
- CreateRoomModal, CreateSpaceModal: apply useModalStyle(480) for fullscreen
  on mobile
- LOTUS_BUGS: mark usePan memory leak + httpStatus check as FALSE POSITIVE;
  mark rateLimitedActions backoff as FIXED

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 15:32:17 -04:00