Commit Graph

112 Commits

Author SHA1 Message Date
jared d37fa1584c test: add suite for utils/keyboard handlers (+4)
Covers onTabPress (Tab-only), preventScrollWithArrowKey (arrows-only),
onEnterOrSpace (Enter/Space gate the callback), and stopPropagation's
editable-element check (does not swallow keys when an input/textarea/
contenteditable is focused) via mock events + a document.activeElement stub.
Full suite now 133 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:50:55 -04:00
jared e3532064b5 test: add suites for accentColor (color math) + matrix-uia (auth flows) (+15)
- utils/accentColor (8): hexToRgb parsing, lighten/darken channel math, rgba
  clamping, WCAG relativeLuminance (black=0/white=1), contrastingText threshold,
  varNameFromToken, and derivePrimaryPalette's full 10-token output.
- utils/matrix-uia (7): UIA flow helpers — getSupportedUIAFlows,
  completed/params/session/errcode/error accessors, getUIAFlowForStages
  (incl. the single-extra-dummy rule), has/requiredStageInFlows, and
  getLoginTermUrl language fallback.

Full suite now 123 tests, all passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:23:36 -04:00
jared 9678b02aba test: add suite for utils/sort room-list comparators (+5)
Covers byTsOldToNew, byOrderKey (undefined-last + the no-equality-branch
quirk for two present keys), and the factory comparators
factoryRoomIdByUnreadCount / factoryRoomIdByActivity / factoryRoomIdByAtoZ
(activity-desc, unread-desc, A–Z case-insensitive with leading-# stripped)
using minimal MatrixClient mocks. Full suite now 108 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:04:45 -04:00
jared a926487f5e fix(utils): findAndReplace infinite loop on non-global regex + tests (+28)
Prevention work surfaced a real latent bug: findAndReplace looped forever
(OOM) on any non-global regex with a match — `match` was only reassigned
inside `if (regex.global)`, so a non-global regex never advanced. Fixed by
treating a non-global regex as a single match (`match = regex.global ?
regex.exec(text) : null`) and added a regression test. Latent in practice
(all current callers pass global regexes), but a crash waiting to happen.

New suites (tsx + node:test), verified empirically:
- utils/findAndReplace (10, incl. the regression)
- utils/AsyncSearch (9): normalize + matchQuery (the timer-based class is
  skipped — needs window.performance/setTimeout, unavailable in node)
- utils/ASCIILexicalTable (10): orderKeys gap-filling + invariants

Full suite now 103 tests, all passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:15:39 -04:00
jared ae1d30bc5a test: add suites for time, matrix, mimeTypes, and search filters (+47 tests)
Expands pure-logic coverage (harness: tsx + node:test):
- utils/time (21): date/time formatters — exact values where timezone-independent,
  structure/regex where locale/tz-sensitive (written via subagent).
- utils/matrix (13): pure id/mxc helpers (isUserId/isRoomId/isRoomAlias/
  getMxIdLocalPart/getMxIdServer/isServerName + room-version gates). (subagent)
- utils/mimeTypes (7): getBlobSafeMimeType allowlist+remap, safeFile rewrap,
  mimeTypeToExt, getFileNameExt/WithoutExt edge cases.
- message-search filters (6): filterGroupsByMsgType (union, empty-group drop,
  non-string msgtype) + filterGroupsByPinned (disabled passthrough, pinned-only).

All assertions verified against actual runtime behavior. Suite now 74 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:27:57 -04:00
jared 472d4ba008 test: add XSS-prevention suite for utils/sanitize
8 tests locking in security-critical behavior of sanitizeCustomHtml /
sanitizeText: script-content removal, event-handler stripping, javascript:
link neutralization, anchor hardening (noreferrer/noopener/_blank), non-mxc
<img> → link conversion, and the N100 <pre class> language-* restriction.
Verified against actual sanitize-html behavior. Suite now 27 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:29:08 -04:00
jared cee0c591e2 fix(pwa): N105 — notification clicks work after the tab is closed
OS notifications were shown via page-level `new Notification()` whose onclick
only works while the originating tab is alive — clicking a notification after
closing the tab did nothing.

- New `showOsNotification()` (utils/dom) prefers `registration.showNotification()`
  so the notification is service-worker-owned and persists; falls back to
  `new Notification()` (with the previous onclick) when no SW is available, so
  worst case is unchanged behaviour.
- sw.ts gains a `notificationclick` handler: focuses an existing app window and
  forwards the target path, or opens the app if none is open.
- ClientNonUIFeatures forwards the SW `notificationClick` message to react-router
  `navigate()` (works for both hash and browser router configs), and uses a
  per-room `tag` to coalesce notifications (replacing the old notifRef.close()
  dedup a SW notification can't hold).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:09:57 -04:00
jared 9bc8c4b47f test: add suite for utils/regex (sanitizeForRegex, URL/EMAIL/JUMBO_EMOJI)
Second pure-logic suite — another zero-import module. 4 tests covering regex
metacharacter escaping (with round-trip), the http(s) URL pattern, email
validation, and the jumbo-emoji matcher. Total suite now 19 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 08:49:54 -04:00
jared e80ebd35cb test: add unit-test harness (tsx + node:test) + first suite for utils/common
Addresses the "no automated test suite" gap. Chose Node's built-in test runner
via tsx rather than vitest: the project is on Vite 8.0.14, ahead of vitest's
supported Vite range, so vitest would fight peer deps. tsx is build-independent.

- `npm test` → `node --import tsx --test $(find src -name '*.test.ts')` (works on
  Node 20 local + 24 CI without relying on --test glob support).
- src/app/utils/common.test.ts: 15 tests covering the pure helpers (bytesToSize,
  time formatters, binarySearch, parseGeoUri, slash trimmers, nameInitials,
  randomStr, suffixRename, splitWithSpace, promise-settled helpers, etc.) —
  asserts actual behavior, traced from source.
- common.ts: folds import made `import type` (it's types only) so the module is
  pure and testable without loading folds/CSS.
- tsconfig excludes *.test.ts (tsx transpiles tests; eslint isn't type-aware so
  it still lints them); added an informational CI "Unit tests" step (promote to a
  hard gate by dropping continue-on-error).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 08:45:22 -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 3c4842df1e feat(settings): custom accent color picker for non-TDS themes (P5-1)
Adds a customAccentColor setting + a HexColorPickerPopOut in Settings →
Appearance. When set (and Lotus Terminal/TDS is OFF), it derives a full folds
Primary palette (Main/hover/active/line, contrasting OnMain, alpha-tiered
Container set, OnContainer) from the chosen color and overrides the folds
Primary CSS variables on document.body — resolving each var name from the
imported folds color.Primary.* token strings (e.g. "var(--oq6d07f)"), the
same body-level injection pattern used for mentionHighlightColor. The theme
class is on document.body, so an inline override on body wins over it.
Reverts to theme defaults when unset or when Lotus Terminal is enabled (TDS
keeps its fixed palette); the picker is disabled with a note in TDS mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 14:22:08 -04:00
jared 51d468fbcc fix(security,notifications): pre class allowlist, notification privacy + icon, sync-script safety (N100/N106/N109/N119)
- N100: restrict <pre> classes to language-* in sanitize-html allowedClasses;
  previously `class` was allowed on <pre> with no allowedClasses entry, so a
  remote sender could inject arbitrary class names that activate site CSS.
- N106: OS notifications for E2EE rooms no longer carry decrypted plaintext
  (which persists in the OS notification center / lock screen). Encrypted rooms
  show only the sender; the in-page toast still previews while focused.
- N109: OS notification icon/badge use the static app logo instead of an
  authenticated-media avatar URL the OS can't fetch (was 401 / no icon). The
  in-app toast keeps the real room avatar (it can fetch via the SW).
- N119: syncDecorations.mjs distinguishes a confirmed 404 (remove) from a
  network/5xx failure (abort) so a transient CDN outage can't silently wipe the
  whole decoration catalog from source control.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 12:35:33 -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 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 d2946c00ce fix(matrix): upload retry/backoff, robust MatrixError, typed m.direct, reliable presence on unload
- 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>
2026-06-24 08:22:00 -04:00
jared e713d47319 fix(ui): forward-dialog header, notification presets, syntax-highlight tokens
CI / Build & Quality Checks (push) Successful in 10m39s
CI / Trigger Desktop Build (push) Successful in 8s
- 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>
2026-06-19 18:22:25 -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
jared cf7c66b99a Reorganize ML noise suppression settings UI
Move the model comparison out of the always-visible Noise Suppression
description and into the ML-only sub-settings. Add a compact info card
for the selected model (CPU / voice quality / transients / download) plus
a collapsible 4-model comparison. Group ML sub-settings into Model,
Enhancements, and Test & calibrate sections with clear labels and
separators. Fix invented --lt-border-color token and hardcoded
rgba background to real TDS tokens. Build the model dropdown and
DenoiseTester labels/compare buttons from DENOISE_MODELS so
DeepFilterNet 3 is handled correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:02:16 -04:00
jared 04b56ffacd feat(denoise): add self-hosted DeepFilterNet 3 ML noise-suppression model
Integrate DeepFilterNet 3 (deepfilternet3-noise-filter@1.2.1) as a new
client-side denoise model id 'deepfilternet', mirroring the DTLN pattern.

The npm package ships only an ESM whose AudioWorklet processor + wasm-bindgen
glue are inlined as a string (loaded via a Blob URL — no CDN for the worklet).
Its only runtime fetches are a single-threaded df_bg.wasm and an ONNX model
tarball, which previously loaded from an external CDN. We now VENDOR both
(build/denoise-vendor/deepfilternet/v2/...) and self-host them under
denoise/deepfilternet/, overriding the package's cdnUrl so nothing hits the
upstream CDN — keeping it self-hosted / Tauri-CSP safe.

The wasm is single-threaded (no SharedArrayBuffer / atomics / imported shared
memory), so it needs no COOP/COEP cross-origin isolation and runs fine in EC's
non-isolated iframe. Runs at 48 kHz fullband. Any init/runtime failure falls
back to the raw mic, like the other models.

- vite.config.js: copy ESM + vendored wasm/model into the EC denoise dir with a
  required-asset guard that aborts the build if any entry is missing.
- build/lotus-denoise.js: 'deepfilternet' branch — dynamic-import the ESM, build
  a DeepFilterNet3Core pointed at the self-hosted base, await init, return the
  worklet node; 48 kHz; raw-mic fail-safe preserved.
- denoisePipeline.ts: 'deepfilternet' branch for the in-app tester + sampleRate.
- settings.ts: add 'deepfilternet' to DenoiseModelId + getSettings whitelist.
- lotusDenoiseUtils.ts: add the comparison-chart row.
- General.tsx: add the "DeepFilterNet 3 (beta)" dropdown option.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 19:57:08 -04:00
jared abb7f743b8 fix(calls): DTLN at 16kHz + raw-capture A/B; explains weak/robotic results
CI / Build & Quality Checks (push) Successful in 10m23s
CI / Trigger Desktop Build (push) Successful in 5s
Two issues found from real testing of the in-app tester:

1. Raw ≈ RNNoise ≈ Speex sounded identical in Record & compare because the clip
   was captured with browser noise suppression ON (the user's native-NS
   setting), so "Raw" was already cleaned and the models had nothing left to
   remove. Record & compare now captures fully raw audio (noiseSuppression /
   AGC / echoCancellation off) so each model's effect on real noise is audible.
   (Friends still heard differences in calls — the models work; the test was
   feeding them pre-cleaned audio.)

2. DTLN was robotic/choppy/quiet because @workadventure/noise-suppression
   targets 16 kHz (AUDIO_CONFIG.sampleRate) and does NOT resample internally,
   while we ran it at 48 kHz. Run DTLN's whole graph in a 16 kHz context:
   - denoisePipeline: add sampleRateFor(model) (16k for dtln, 48k otherwise);
     tester live-monitor + playback contexts use it (bufferSource resamples the
     48k clip down for DTLN).
   - shim (build/lotus-denoise.js): SAMPLE_RATE is now model-aware, so DTLN is
     correct in real calls too (it was previously broken at 48 kHz). The 16 kHz
     processed track is still published to LiveKit (WebRTC/Opus resamples).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 17:27:15 -04:00
jared 14cfa021c5 feat(calls): in-app denoise tester to audition models + calibrate gate
CI / Build & Quality Checks (push) Successful in 10m24s
CI / Trigger Desktop Build (push) Successful in 17s
The previous "Test Microphone" meter only showed a raw 0-100% level bar — it
never ran the gate or any model, and its scale wasn't dBFS, so it couldn't tell
you which threshold to pick or let you hear the models solo. Replace it with a
real tester that reuses the shipped worklets (/public/element-call/denoise/) in
a main-app AudioContext, mirroring the call pipeline (source -> gate -> model).

- denoisePipeline.ts: shared loader for the RNNoise/Speex flat worklets and the
  DTLN @workadventure helper, the noise gate, and a dBFS RMS meter helper.
- DenoiseTester.tsx:
  - Live monitor: hear yourself through the selected model (+gate) in real time
    (headphones) with In/Out dBFS meters and a threshold marker on the In meter
    so the gate value is meaningful to calibrate.
  - Record & compare: capture a short clip, then A/B the same audio Raw vs
    RNNoise vs Speex vs DTLN.
- Wire it into the ML settings block; remove the old raw-only MicMeter. Use real
  TDS tokens (--accent-*, --border-color, --bg-card) instead of the invented
  --lt-* names + hardcoded hex the old meter used.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:53:57 -04:00
jared 86272b6b08 fix(calls): wire DTLN ML denoise correctly via @workadventure JS API
CI / Build & Quality Checks (push) Successful in 10m25s
CI / Trigger Desktop Build (push) Successful in 6s
The prior DTLN attempt (89a2321d) broke the build (missing dep, wrong
`cinny/` asset paths) and typecheck (`'dtln'` not in DenoiseModelId), and was
wired against an API the package doesn't expose. @workadventure/noise-
suppression is not a flat AudioWorklet — it's a self-contained ES module whose
processor name is `workadventure-noise-suppression` and which resolves its own
LiteRT WASM + TFLite models via import.meta.url. Driving it by hand-rolled
addModule + processorOptions cannot work.

- Re-add @workadventure/noise-suppression@0.0.4 (package.json + lockfile).
- vite: copy the package's whole dist/ tree intact to
  denoise/workadventure/ (preserving assets/ + vendor/litert) so import.meta
  resolution works at runtime; fail the build if the entry module is missing.
- shim: for the DTLN model, dynamic-import denoise/workadventure/audio-worklet
  .js and use createNoiseSuppressionAudioWorklet(ctx, { bypassUntilReady })
  to build the node; RNNoise/Speex keep their direct flat-worklet path. Async
  init errors are logged + reported and fall back to the raw mic.
- Restore 'dtln' in DenoiseModelId (+ settings coercion), the model chart, and
  the settings dropdown, labelled "(beta)".

DTLN builds and is fully self-hosted, but its in-call audio is UNVERIFIED in
this environment — needs a real-call test. DeepFilterNet stays excluded (CDN
asset loading, incompatible with self-hosting / Tauri CSP).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:11:45 -04:00
jared 6634b2b8a2 fix(calls): make ML denoise build-honest + gate desktop trigger on CI
CI / Build & Quality Checks (push) Successful in 10m41s
CI / Trigger Desktop Build (push) Successful in 6s
Audit/repair of the multi-model denoise work so it actually builds and only
exposes working, self-hosted models.

- Complete the DTLN/DFN3 revert: uninstall @workadventure/noise-suppression
  and deepfilternet3-noise-filter (package.json + lockfile), drop the unused
  DTLN asset-copy block from vite.config.js (was shipping ~2MB of unused
  tflite/wasm), and narrow DenoiseModelId to the bundled models (rnnoise,
  speex). Coerce any retired persisted model value back to the default.
- Fix General.tsx CI typecheck failures introduced by the denoise UI: restore
  three imports the rewrite deleted (useDateFormatItems, SequenceCardStyle,
  useTauriUpdater), add the missing denoise/sound imports, and correct
  hallucinated Folds props (Text has no variant/bold; Box uses
  alignItems/justifyContent). tsc now passes with 0 errors.
- Harden the vite denoise plugin: required RNNoise/Speex/gate assets and the
  shim now fail the build loudly if missing (instead of a silent warn that
  shipped a broken ML feature), and the index.html shim injection is verified.
- CI: move the cinny-desktop submodule bump into ci.yml as a `trigger-desktop`
  job gated on `needs: build`, and delete the standalone trigger-desktop.yml.
  A failing push no longer kicks off the slow Tauri builds in parallel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:42:21 -04:00
jared 5d5f5f4516 feat(calls): implement advanced multi-model ML noise suppression system
CI / Build & Quality Checks (push) Failing after 4m49s
Trigger Desktop Build / trigger (push) Successful in 11s
Implement a flexible, multi-model noise suppression pipeline for Element Call/LiveKit integration:

- ML Engines: Added support for RNNoise, Speex, DTLN, and DeepFilterNet 3 models.
- Pipeline Architecture: Implemented modular audio processing in lotus-denoise.js, supporting 'Series Suppression' (running browser-native NSNet2 before ML) and a hardware-style Noise Gate.
- UI & UX Enhancements:
  - Settings UI: Added model comparison chart with CPU/Quality metadata.
  - Tuning: Added Live Microphone Meter for calibrating Noise Gate thresholds.
  - Reporting: Added LotusToast system to alert users when ML suppression fails or falls back to raw input.
- Robustness & Quality:
  - Capture Fidelity: Removed forced 48kHz capture constraints to allow native-rate capture (solving static issues with high-end audio interfaces).
  - Performance: Added WASM SIMD detection with transparent fallback.
  - Capability Detection: Added browser feature detection to disable unsupported ML modes.
- Build Integration: Updated Vite config to self-host all model WASM/tflite assets in /denoise/ directory.
2026-06-16 00:50:12 -04:00
jared 2c5f0b8b28 fix: make call join/leave sounds audible to all participants + server-side hard voice limit docs
CI / Build & Quality Checks (push) Successful in 10m29s
Trigger Desktop Build / trigger (push) Successful in 5s
Sounds (P5-16): browsers block the Web Audio context until a user gesture
starts it, so join/leave sounds — which fire later with no gesture — were
silent. unlockCallSounds() now primes/resumes the shared AudioContext inside
the Join click (centralized in useCallStart so every join path is covered),
making the per-client sounds reliably audible to everyone in the call.

Voice limit (P5-10): the limit is now a hard, cross-client server-side cap
enforced by the voice-limit-guard sidecar (matrix repo) that fronts
lk-jwt-service and refuses LiveKit tokens when a room is full. Updated
LOTUS_FEATURES.md / README.md / LOTUS_TODO.md to reflect that the client
'Channel Full' check is UX only and the server is authoritative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 23:45:31 -04:00
jared 702e2e00eb feat: voice channel user limit (P5-10) + call join/leave sounds (P5-16)
CI / Build & Quality Checks (push) Successful in 10m54s
Trigger Desktop Build / trigger (push) Successful in 6s
P5-10 Voice Channel User Limit:
- New StateEvent.LotusVoiceLimit (io.lotus.voice_limit) with { max_users }
- RoomVoiceLimit admin control in Room Settings > General > Voice
  (power-level gated via permissions.stateEvent)
- CallPrescreen reads the limit reactively and disables Join with a
  'Channel Full (N/N)' message at capacity; existing members can rejoin

P5-16 Custom Join/Leave Sound Effects:
- useCallJoinLeaveSounds hook wired into CallUtils; detects participant
  join/leave via MatrixRTCSession membership changes (sender|deviceId),
  filters out self, only fires while joined
- Sounds synthesized in-browser with Web Audio (callSounds.ts) - no
  assets bundled; styles Off/Chime/Soft/Retro
- 'Join & Leave Sounds' selector in Settings > Calls (previews on change)

Docs: LOTUS_FEATURES.md, README.md, LOTUS_TODO.md (P5-10/P5-16 marked done)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 22:20:22 -04:00
jared 6a57c13c56 fix: fall back to DOM traversal in targetFromEvent when composedPath is empty
CI / Build & Quality Checks (push) Successful in 10m44s
Trigger Desktop Build / trigger (push) Successful in 22s
composedPath() returns an empty array once the native event is no longer
dispatching. In React 19, events from a portal-within-portal (EmojiBoard
inside #portalContainer, which already hosts the Settings Overlay) can reach
the synthetic event handler after the native dispatch window closes.

The fallback walks up from evt.target so handleGroupItemClick still finds
the emoji button and calls onEmojiSelect — fixing emoji selection in the
status-message editor in Settings > Account > Profile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 21:31:07 -04:00
jared bafd9cbe75 fix: address confirmed bugs from LOTUS_BUGS.md audit
CI / Build & Quality Checks (push) Successful in 10m54s
Trigger Desktop Build / trigger (push) Failing after 8s
- useFileDrop: reset drag overlay when mouse leaves browser window
  (relatedTarget === null signals viewport exit, counter was getting stuck)
- useDeviceVerificationStatus: add member count to useMemo deps so new
  room members' devices get checked, not just initial joined members
- index.css: define --bg-surface-variant used by VoiceMessageRecorder,
  MessageSearch, SearchFilters, UserRoomProfile (was falling back to transparent)
- syntaxHighlight: fix Python inline comments — # after space/tab was
  treated as plain text; only start-of-line was recognised
- usePresenceUpdater: replace internal baseUrl cast with mx.getHomeserverUrl()
- useLocalMessageSearch: scan all linked timelines via getUnfilteredTimelineSet()
  not just the live window, so scrolled-back history is included in E2EE search
- RoomViewHeader: show search button in encrypted rooms — local search is
  implemented and handles them; the guard was a holdover from before it existed
- recent-emoji: return emojis in recency order (array is already unshifted on
  use) instead of sorting by total usage count

Skipped: media gallery memory leak (needs virtualization refactor),
bookmark race condition (needs queue/lock), Night Light portal coverage
(position:fixed already covers full viewport — not a real bug).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:56:06 -04:00
jared 58b19995b8 fix: image compression checkbox now shows for all raster image types
The checkbox was only shown for image/jpeg and image/png. Users
uploading WebP, GIF, AVIF, BMP, TIFF, HEIC (iPhone photos) or any
other raster format never saw the checkbox at all.

Fix: isCompressible now checks file.type.startsWith('image/') and
excludes only image/svg+xml (vector — would rasterise) and empty type
strings. compressImage signature widened to File | Blob so it matches
the TUploadContent type without unsafe casts.

The send-path guard in handleSendUpload was also widened from the
hardcoded jpeg/png check to use isCompressible(), keeping the two gates
in sync. The Blob-safe id attribute uses the .name fallback so it
doesn't break when originalFile is a Blob without a name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 20:22:38 -04:00
jared 8ac63e3771 fix: glassmorphism sidebar background visibility + image upload cleanup
Glassmorphism: the sidebar is a flex sibling of the room view, so
backdrop-filter had nothing behind it to blur. Fix: apply the active
chat background to document.body when glassmorphismSidebar is on
(cleaned up when it's turned off or the component unmounts). Now the
sidebar blurs through the same background pattern as the room view,
making the frosted-glass effect obvious.

Image upload cleanup: delete the pre-uploaded original MXC from the
homeserver after the compressed version is successfully uploaded
(Synapse 1.97+ DELETE /_matrix/client/v1/media/{server}/{mediaId}).
Also delete on cancel when a successful upload is removed by the user.
Both are best-effort — failures are swallowed so UX is unaffected.
Added tryDeleteMxcContent() utility to src/app/utils/matrix.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:25:19 -04:00
jared 6a91904469 fix: MSC4140 correct endpoint — standard v3 send + org.matrix.msc4140.delay param
Synapse 1.153 MSC4140 implementation does NOT use an unstable-prefix rooms
send path. The correct API is:
  PUT /_matrix/client/v3/rooms/{id}/send/{type}/{txnId}
      ?org.matrix.msc4140.delay={ms}
which returns { delay_id } instead of { event_id }.

Cancel/restart remain at:
  POST /_matrix/client/unstable/org.matrix.msc4140/delayed_events/{id}
  body: { action: 'cancel' | 'restart' }

Also: context menu — copy link removed, mute durations converted to
PopOut submenu using RectCords pattern (e.currentTarget.getBoundingClientRect)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 17:59:39 -04:00
jared fbdd0e7083 fix: message scheduling 404 and date/time picker UX
API fix: delay was embedded in the path string causing 404 — moved to
proper query param object; prefix changed from '' to the full MSC4140
unstable prefix so authedRequest builds the correct URL. Cancel and
restart endpoints fixed the same way.

Date/time picker: replaced single datetime-local (hard to use time
portion) with separate date + time inputs side by side; colorScheme:'dark'
hints the browser to render calendar/clock popups in dark mode to match
the app. Preview row now shows as a styled Primary.Container chip with
clock icon. Relative time ("in 2h 15m") shown below the label.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 15:05:09 -04:00
jared c6760b0ba4 fix: schedule button, compression visibility, activity log, insights overflow, bookmarks UI
Schedule message: modal now always opens (even with empty composer);
includes its own message textarea pre-filled from editor content;
removed null-content early-return guard from handleScheduleClick;
fixed error text to use color.Critical.Main not CSS var

Image compression: removed 200KB size threshold — checkbox now shows
for all JPEG/PNG uploads (not just large ones); 'no significant saving'
message handles already-small files gracefully

Activity log: auto-paginate on mount — state events are absent from
initial sync window, so the log was always empty until Load More was
clicked manually

Insights summary: Text size H4→H5 with toLocaleString() formatting and
overflow:ellipsis so large numbers don't push tiles off screen

Bookmarks panel: replaced var(--bg-*) CSS vars (undefined in folds
themes) with color.Surface/SurfaceVariant/Primary folds tokens; added
left accent border on message preview block, count badge, clear button
in search, improved empty state, cleaner button hierarchy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 12:07:12 -04:00
jared 9273eb5f2e feat: bookmarks, message scheduling, image compression, room insights
P3-1: Message Bookmarks — right-click any message to bookmark; saved to
io.lotus.bookmarks account data (max 500, syncs across devices); star
icon in sidebar opens BookmarksPanel with filter, Jump-to-message, and
remove buttons; reactive to AccountData events

P3-2: Message Scheduling (MSC4140) — clock button next to send opens
ScheduleMessageModal with datetime-local picker; validates ≥1 min future;
calls PUT org.matrix.msc4140 delayed event API; collapsible
ScheduledMessagesTray above composer lists pending messages with cancel;
local Jotai atom tracks scheduled messages per room

P3-3: File Upload Compression — opt-in checkbox per JPEG/PNG file ≥200KB
in upload preview; canvas API compresses at 0.82 quality; shows before/
after size estimate; compressed blob used in upload when checked

P3-7: Room Insights — new Insights tab in room settings; top 5 active
members (bar chart), top 5 reactions (chips), media breakdown (4 tiles),
24-hour activity heatmap (CSS bar chart); all from local cache only with
disclaimer banner; never the first tab shown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 10:26:08 -04:00
jared 5ae84cbeaf feat: P2-3 sort rooms, P2-5 quiet hours, P2-2 custom notification sounds
P2-3 — Sort Non-Space Rooms:
- homeRoomSort: 'recent' | 'alpha' | 'unread' setting (default 'recent')
- factoryRoomIdByUnread comparator: unread rooms first, tie-break by count
- Sort icon button in Rooms NavCategoryHeader opens PopOut menu with
  three options (Recent Activity / A→Z / Unread First), checkmark on active
- Collapsed state still filters to unread-only regardless of sort choice

P2-5 — Notification Quiet Hours:
- quietHoursEnabled / quietHoursStart / quietHoursEnd added to settings
  (defaults: false, '23:00', '08:00')
- isInQuietHours() helper handles both normal and overnight spans;
  start===end treated as zero-length window (disabled) to avoid silent no-op
- Both InviteNotifications and MessageNotifications gate notify() and
  playSound() behind the quiet-hours check
- Settings → Notifications: new Quiet Hours card with Switch + two
  <input type="time"> fields (only shown when enabled)

P2-2 — Custom Notification Sounds:
- messageSoundId / inviteSoundId settings: 'notification'|'invite'|'call'|'none'
- notificationSounds.ts: shared NOTIFICATION_SOUND_MAP (removes duplication
  between ClientNonUIFeatures and SystemNotification — code review fix)
- Audio source updated reactively via useEffect when sound ID changes
- Settings → Notifications: Message Sound + Invite Sound selects expand
  when the master sound toggle is on; each has a ▶ preview button
- playPreview() catches audio.play() rejections (code review fix)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 19:41:02 -04:00
jared a3f776134f fix: ctrl+p print dialog, gallery 400 error, poll multi-choice UX
- Suppress Ctrl+P browser print dialog via SuppressPrintShortcut in
  ClientNonUIFeatures (no UI opened, just preventDefault)
- mxcUrlToHttp: build URL manually instead of delegating to SDK.
  The SDK forces allow_redirect=true when useAuthentication=true;
  Synapse's /_matrix/client/v1/media/thumbnail rejects that with 400.
  Manual construction omits allow_redirect entirely.
- Gallery: redesign using folds color tokens (color.Surface.*) instead
  of non-existent CSS custom properties; add ThumbState so broken
  images show an icon placeholder; use useAuthentication for thumbnails
  now that the URL builder is fixed; "Load More" always visible.
- PollCreator: replace raw <button> with folds Button components so the
  Single/Multiple choice toggle renders with actual visual difference.
- PollContent: support multiple-choice polls end-to-end —
  myVote:string → myVotes:Set<string>; computeVotes collects all
  m.selections (not just [0]); toggle-select for multi, radio for
  single; checkbox/radio indicator icons next to each option;
  "◉ Poll · Multiple choice" / "Single choice" label in header;
  sends full selections array on every vote event.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:23:44 -04:00
jared d43044ccbf feat: P1 features — quick switcher, media gallery, DM previews, knock-to-join, syntax highlighting
P1-1: Quick room switcher (Ctrl+K/Cmd+K) — QuickSwitcher.tsx + ClientNonUIFeatures hotkey
P1-2: Media gallery drawer (images/videos/files) — MediaGallery.tsx + RoomViewHeader toggle
P1-4: DM last message preview + relative timestamp in RoomNavItem when direct=true
P1-7: Code syntax highlighting — TDS tokenizer (syntaxHighlight.ts), custom CSS theme
       (.prism-tds-dark/.prism-tds-light), applied in react-custom-html-parser.tsx
P1-11: Knock-to-join — "Request to Join" in RoomIntro + Pending Requests in MembersDrawer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 19:45:57 -04:00
jared afe957015b feat: P1 features — voice speed, private receipts, room filter, favorites, invite link, poll creation
P1-5: Voice message playback speed toggle (0.75×/1×/1.5×/2×) in AudioContent.tsx
P1-10: Private read receipts toggle in Privacy settings; wired to notifications.ts
P1-3: Room filter input on Home tab and DMs tab (client-side, clears on tab switch)
P1-8: Favorite rooms via m.favourite tag — Favorites section in Home sidebar, star/unstar in right-click menu
P1-9: Room invite link + QR code in room settings (Share Room tile, api.qrserver.com QR)
P1-6: Poll creation modal in composer (PollCreator.tsx, sends m.poll.start)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 19:31:30 -04:00
jared 26f1e234a2 fix: add m.server_notice icon case to getRoomIconSrc
Server notice rooms were falling through to the default hash/lock icon
in the room list. Now return Icons.Warning before any other type checks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 14:47:48 -04:00
jared 16dddcb9f0 fix: comprehensive P0 quality pass — audit findings resolved
- ReportRoomModal: use mx.reportRoom() SDK method, fix undefined CSS vars
  (--mx-surface/border → folds color tokens), add role/aria-modal/aria-labelledby,
  accessible select/input labels, per-error-code messages, auto-close on success
- About.tsx: clickable matrix_id + email_address links (Text as="a"), AbortController
  cleanup, runtime JSON type guard, loading state, role display for all role values,
  remove classList theming hack, use mx.getHomeserverUrl()
- RoomViewHeader: useLocalRoomName for header title, useReportRoomSupported gate,
  hide Invite/Settings/Report for server notice rooms, isCreator guard on Report,
  FocusTrap returnFocusOnDeactivate on topic overlay, Server Notice chip tooltip
- RoomInput: replace raw <div> with folds <Box> for server notice read-only message
- EditHistoryModal: isRawEditEvent type guard, handle next_batch truncation,
  getVersionBody handles formatted_body (strips HTML for text display),
  role/aria-modal/aria-labelledby accessibility, guard for undefined eventId,
  use config.space tokens (remove var(--mx-spacing-*) strings)
- RoomNavItem: remove duplicate getExistingContent (use exported getLocalRoomNamesContent),
  maxLength={255} on rename input, fix FocusTrap nesting (renameDialog state moved to
  RoomNavItem_, RenameRoomDialog rendered outside menu, menu closes before dialog opens),
  pencil icon opacity via config.opacity.P300
- useRoomMeta: export getLocalRoomNamesContent for reuse
- RoomIntro: useLocalRoomName, formatted topic viewer with Overlay/FocusTrap/RoomTopicViewer
- CallRoomName: useLocalRoomName for consistent rename display in call overlay
- General.tsx: fix #980000/#FF6B00 hardcoded hex → color tokens/CSS vars, URL Preview
  capitalization, improved encrypted preview warning text + Warning chip, add
  description to plain urlPreview setting
- sanitize.ts: fix hex color regex to support 3/4/6/8 digit hex (CSS4 #RGBA, #RRGGBBAA)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 21:30:27 -04:00
jared db0b083a3e fix: prevent allow_redirect=true on media URLs; fallback on 400 too
Synapse's thumbnail endpoint returns 400 Bad Request when the
allow_redirect=true query parameter is present (added by matrix-js-sdk
41.x for authenticated media). Default allowRedirects to false in our
mxcUrlToHttp wrapper so the parameter is never appended.

Also extend the downloadMedia legacy-URL fallback to cover 400 in
addition to 401, catching any encrypted-media fetches that still carry
the old URL shape after a cache refresh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 22:25:13 -04:00
jared 73420242d0 fix: fallback to legacy media URL on 401, fix EC avatar letter centering
downloadMedia: on 401 (SW session race or allow_redirect hop stripping
auth), retry via /_matrix/media/v3/ which is public on this homeserver
(allow_public_access_to_media_repo: true). Fixes images not loading
after sending, and avatar 401s in call prescreen tiles.

CallEmbed: inject flex-centering CSS for EC 0.19.4 participant avatar
container so the initial letter is correctly centered in its circle.
CSS class names are scoped to _avatarContainer_1mrho_40 in EC 0.19.4.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 22:01:45 -04:00
jared 6c915d03c3 fix: throw on non-OK response in downloadMedia (Fixes JAVASCRIPT-REACT-B)
When the server returns a 4xx/5xx, downloadMedia was silently returning
the error response body as a blob. decryptAttachment would then fail with
a misleading 'Mismatched SHA-256 digest' instead of surfacing the real
HTTP error. Now throws immediately so callers (useAsyncCallback) can
show the correct error state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:38:24 -04:00
Lotus Bot 266e47f240 chore: upgrade TypeScript 4.9 to 5.9, ESLint 8.29 to 8.57, @typescript-eslint 5 to 7
Resolves all TS2345/TS2347/TS7006 type errors introduced by stricter TypeScript 5.x.
Fix Icons.Settings to Icons.Setting, cast account data returns, fix implicit any.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 11:16:11 -04:00
Lotus Bot 61a1f008d0 chore: upgrade i18next 26, prettier 3, fontsource-variable, domhandler 6, lint-staged 17
- i18next 23->26 + react-i18next 15->17
- prettier 2->3, reformat all files
- replace @fontsource/inter with @fontsource-variable/inter 5, update import path
- domhandler 5->6 (aligns with transitive deps)
- lint-staged 16->17
2026-05-21 23:30:50 -04:00
Lotus Bot 42b9cc2b64 chore: prettier format all files, brotli, Sentry release tagging, CI gates
Prettier: auto-formatted 103 files to fix baseline. Prettier check in CI
  is now a hard gate (removed continue-on-error).

Brotli: installed libnginx-mod-http-brotli-filter/static. Enabled in nginx
  with brotli_static on for pre-compressed assets and comp_level 6.

Sentry releases: deploy script now exports VITE_APP_VERSION=<git-short-sha>
  before building so each Sentry release maps to an exact commit.
  CI also passes github.sha as VITE_APP_VERSION.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 20:49:33 -04:00
Lotus Bot ff7c2ed941 Bug fixes, security hardening, and performance improvements
- BUG-16: Fixed pagination deadlock (fetching flag stuck on error path)
- BUG-17: Fixed absoluteIndex===0 falsy check skipping unread jump
- BUG-19: Fixed mEvt.getRoomId()! non-null assertion crash
- BUG-20: Wrapped getSettings()/setSettings() in try/catch for corrupt localStorage
- SEC: Replaced randomStr() Math.random() with crypto.getRandomValues() CSPRNG
- SEC: Fixed afterLoginRedirectPath open redirect validation
- SEC: Narrowed OSM iframe sandbox to scripts-only (removed allow-same-origin)
- Perf-2: Memoized selectAtom in useSetting (prevented new atom ref per render)
- Perf-4: Fixed typingMembers setTimeout leak (tracked timers per user/room)
- Perf-8: Memoized getChatBg() result in RoomView (not inline in JSX)
- Perf-12: Replaced body.class * font-family with body.class (inherited)
- Perf-15: Memoized typingNames array chain in RoomViewTyping
- Perf-9: Added blob URL cleanup useEffect in AudioContent
- BUG: Fixed forEach(async) -> Promise.all in useCommands join handler
- BUG: Fixed useCompositionEndTracking missing dependency array
- A11y: Fixed spoiler button aria-pressed + keyboard handler
- A11y: Added aria-label to Send message button
- Build: Set sourcemap:false, removed netlify.toml from copyFiles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 21:11:38 -04:00
Lotus Bot 8666daaf9d Security, performance, bug fixes, and TDS improvements
Security:
- HIGH-1: Validate hex color format before CSS interpolation in sanitize.ts
- HIGH-5: Add sandbox attribute to OpenStreetMap iframe
- MED-1: Fix permissive URL scheme regex in LINKIFY_OPTS
- MED-3/HIGH-4: Add .js.map blocking + CSP header to nginx config
- LOW-2: Validate OIDC authUrl scheme before window.open
- Accessibility: Remove maximum-scale=1.0 from viewport meta (WCAG 1.4.4)

Performance:
- O(1) Map index in computePositions (was O(M×T) findIndex per member)
- Add RoomMemberEvent.Membership subscription so positions update on join/leave
- Fix uncleaned 2000ms setTimeout in RoomTimeline useLayoutEffect

Bug fixes:
- BUG-5: Add QUEUED/CANCELLED cases to DeliveryStatus component
- BUG-6: Guard DeliveryStatus against state events via isState() check
- BUG-10: Clamp PiP position on window resize
- BUG-14: Separate runLotusBootSequence into dedicated useEffect([lotusTerminal])
- Fix aria-live on typing indicator (WCAG 4.1.3)
- Add aria-label + aria-multiline to message editor

TDS (Lotus Terminal Design System):
- Add reaction chip styles (dark + light mode)
- Add GIF picker CSS via globalStyle instead of runtime injection
- Add URL preview styles (dark + light mode)
- Add complete GIF picker light-mode TDS block (was missing)
2026-05-19 16:26:25 -04:00
Ajay Bura ecdc7e9e8a fix: notification cause crash on android (#2938)
fix notification cause crash on android
2026-05-15 16:30:23 +10:00
LeaPhant 3c10fcddc8 fix: support audio with ogg filetype (#2924)
fix: ogg audio workaround
2026-05-14 15:16:54 +05:30