Commit Graph

3630 Commits

Author SHA1 Message Date
jared 7e38baa7b6 docs(todo): mark desktop Tier A wave; P5-40 done, P5-50 won't-fix
- P5-36/43/44/46/47/49/55/57 → [~] IMPLEMENTED (web verified; native
  CI-compile-pending, runtime-verify on Windows).
- P5-40 → [x] DONE (TauriUpdateFeature already ships the update toast).
- P5-50 → [WON'T FIX] (can't inject Media Foundation into WebView2's WebRTC
  pipeline; Chromium already HW-decodes).
- P5-35 → note the "can't compile-test without Windows" premise is outdated
  (CI compiles Windows now); remains Tier B (rides with P5-41).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:10:04 -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 a0fcdf74da feat(denoise): autoGainControl=false for the ML tier + docs
CI / Build & Quality Checks (push) Successful in 11m16s
CI / Trigger Desktop Build (push) Successful in 10s
- CallEmbed sets `autoGainControl=false` for the ML noise-suppression tier so
  the browser's auto gain control doesn't fight the in-source ML model; the
  browser/off tiers keep AGC on.
- Docs: refresh the LOTUS_FEATURES noise-suppression section (browser-native
  default, quality-ordered dropdown, DFN3 ML default, attenuation floor,
  gate-after-ML, DFN level 60, AGC-off, the reliability fixes) and LOTUS_TODO
  P5-30 (mark tuning/reliability/AGC done; record GTCRN as researched-and-deferred).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:46:39 -04:00
jared ebc782b16c feat(denoise): browser-native default, quality-ordered model picker, wire native-NS
CI / Build & Quality Checks (push) Successful in 11m15s
CI / Trigger Desktop Build (push) Successful in 18s
- Model dropdown is now ordered by quality/CPU, best first (DeepFilterNet 3 →
  DTLN → RNNoise → Speex); fix RNNoise's inaccurate "High" voice-quality label.
- When a user opts into the ML tier, default to the highest-quality model
  (DeepFilterNet 3). The tier default stays browser-native (known-good, best
  perceived in testing so far).
- Wire the "Series Suppression" (native-NS-before-ML) toggle into the real call
  path — it was applied only in the settings tester, so the tester could sound
  better than the actual call. Default it OFF (a single NS stage is best
  practice; it's an opt-in test aid).
- isMLDenoiseSupported now also requires WebAssembly, so ML isn't offered on
  strict-CSP shells where it would silently fall back to the raw mic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:02:41 -04:00
jared 7939dc92d4 docs(call): cover soundboard/quality/permissions in user-facing docs
- README "Calls & Voice": add the in-call soundboard, per-user call quality
  settings, and admin room call-permissions bullets.
- LOTUS_TODO: mark the soundboard UI as built (was "cinny UI remains / dormant").
- HANDOFF_ELEMENT_CALL_FORK: add a COMPLETE status banner to the §12.1 host
  checklist; fix stale denoise specifics (all four models are in-source;
  flag is lotusDenoiseSource=1, not lotusDenoise=ml).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 22:43:49 -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 02b2ce8109 feat(chat-bg): redesign 19 chat backgrounds as modular per-pattern files
Same treatment as the seasonal themes: split the 502-line chatBackground.ts
Record into one premium module per background under lotus/backgrounds/ (each
exposes a tuned dark + light ChatBgVariants), one Opus agent per background
against a shared brief. chatBackground.ts now assembles DARK/LIGHT from the
modules; getChatBg is unchanged. Carbon + Aurora are kept inline as-is (user
favorites); none stays the empty layer.

Every redesign: layered oklch palettes, seamless tiling with worked-out tile
math (integer-multiple periods; edge-wrapping inline-SVG data-URIs for
circuit/hexgrid/waves/herringbone/chevron/tactical), independently-tuned
dark+light (not a recolor), and low "felt-not-read" opacity so chat text stays
WCAG-AA legible. The 5 animated backgrounds (rain, star drift, grid pulse,
aurora flow, fireflies) each colocate a vanilla-extract keyframe .css.ts,
animate only background-position for a jump-free loop, and — since getChatBg
strips animation for reduced-motion — render a finished static frame too.

Redesigned: blueprint, stars, topographic, herringbone, crosshatch, chevron,
polka, triangles, plaid, tactical, circuit, hexgrid, waves, neon, anim-rain,
anim-stars, anim-pulse, anim-aurora, anim-fireflies.

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 20:23:54 -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 eafa353364 feat(decorations): allow VITE_DECORATION_CDN override; close N127
- avatarDecorations: resolve the decoration CDN base from VITE_DECORATION_CDN at
  runtime, falling back to the DECORATION_CDN literal (kept intact so the sync
  script + tests still parse it). Lets a deploy repoint the CDN without a code
  edit. Guarded for the tsx test runner (import.meta.env undefined there).
- LOTUS_BUGS: close N127 — the denoise dev-injection gap dissolved with the A7
  cutover (no getUserMedia shim is injected anymore; denoise is in-source in the
  EC fork), so there is nothing to inject in dev.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 19:18:09 -04:00
jared 353bb59393 test(utils): cover scheduledMessages + lotusDenoiseUtils; fix AudioWorklet detect
- scheduledMessages.test.ts (9): pins the MSC4140 request shape (PUT to the room
  send endpoint with the org.matrix.msc4140.delay query, POST cancel/restart to
  /delayed_events with the unstable prefix), the delay-floor math (Math.max(1000,
  round(sendAt-now)) — "now"/past targets still yield a valid >=1000ms delay),
  rounding, and url-encoding.
- lotusDenoiseUtils.test.ts (9): model-catalog data integrity + isMLDenoiseSupported
  feature detection across AudioContext/webkit/getUserMedia.
- Bug found + fixed: isMLDenoiseSupported used `!!AudioWorkletNode`, a bare global
  reference that throws ReferenceError (not returns false) on a browser with
  AudioContext but no AudioWorkletNode binding. Switched to `typeof` so the
  detection helper reports unsupported instead of throwing. Regression test proven
  to fail on the old code.

Suite now 545 tests (4th real bug caught by the prevention work).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 19:01:25 -04:00
jared 1daa8aa9b1 test(callSounds): cover join/leave sound design + AudioContext lifecycle
callSounds.ts had no tests despite 106 lines of user-facing audio logic. Adds 13
tests (mocking AudioContext) pinning: the chime/soft/retro join+leave melodies
(frequencies, oscillator types, stagger), the click-avoidance gain envelope and
osc->gain->destination wiring, and the defensive contracts — unknown style is a
no-op that never creates a context, a throwing AudioContext constructor is
swallowed, and the shared context is reused / recreated-when-closed / resumed-
when-suspended. Suite is now 527 tests; refreshed the stale count in LOTUS_BUGS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:49:02 -04:00
jared 5af024f7e7 docs: consolidate EC test checklist into LOTUS_TESTING.md (§D2)
CI / Build & Quality Checks (push) Successful in 11m28s
CI / Trigger Desktop Build (push) Successful in 20s
Fold the Element Call fork Phase-2 feature tests into the canonical testing
guide as §D2 (denoise reconnect/device-switch/4 models, event-driven
speaker/mute, focus-during-screenshare, in-call decorations, transparency,
+ the dormant #3/#7). Each item keeps a plain / outcome for non-dev
testers, so the standalone ELEMENT_CALL_TEST_CHECKLIST.md is removed — all
in one place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:40:02 -04:00
jared 84ce9843ff docs: log E2EE key-sync issues (KE-1..4) + tester checklist
CI / Build & Quality Checks (push) Failing after 12m1s
CI / Trigger Desktop Build (push) Has been skipped
LOTUS_BUGS.md: new Encryption/E2EE section tagged EXTREME complexity +
planning-session-required for a senior-engineer deep dive — OTK upload
conflict storm (KE-1), Element Call media-key distribution failures causing
audio/video dropouts (KE-2), a timeline decryption error (KE-3), and
MatrixRTC delayed-event timeouts (KE-4). All observed live 2026-06-30; not
caused by the EC fork work. Plus a non-developer ELEMENT_CALL_TEST_CHECKLIST.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:37:01 -04:00
jared efcee88f05 docs(test): add OIDC/MSC3861 test section + local MAS dev loop
CI / Build & Quality Checks (push) Successful in 13m42s
CI / Trigger Desktop Build (push) Successful in 33s
LOTUS_TESTING.md section N (N1-N6): OIDC login flow, session-persist-on-reload,
token refresh, logout revocation, account-management link, and the non-OIDC
regression check. Backed by dev/oidc-test/ — a runnable local Matrix
Authentication Service + Synapse(msc3861) loop (compose skeleton, the Synapse
experimental_features delta, and the public/config.json override) so the flow
can be verified without a mozilla.org tester.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:04:24 -04:00
jared 0b307037e0 docs(todo): P4-6 OIDC client-side built, awaiting live verification
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:13:40 -04:00
jared 67bd05fc96 feat(auth): OIDC phase 4/5/6 — token refresh, logout revocation, account link
- initMatrix.ts: import the shared Session type; when a session has a refresh
  token + oidc metadata, wire a LotusOidcTokenRefresher via createClient's
  refreshToken + tokenRefreshFunction (reactive 401 refresh). Rust crypto is
  unaffected (still keyed on userId/deviceId).
- client/oidcTokenRefresher.ts: OidcTokenRefresher subclass that persists rotated
  tokens back to the fallback session.
- client/oidcLogout.ts + logoutClient: best-effort revoke access+refresh tokens at
  the issuer's revocation_endpoint on logout (tolerant of failure).
- settings/account/OidcManageAccount.tsx: MSC2965 "Manage account" deep-link,
  shown only when authMetadata is present (OIDC servers); mirrors OtherDevices.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:12:13 -04:00
jared dd6b0bccb3 feat(auth): OIDC phase 3 — authorization-code callback route
- oidc/OidcCallback.tsx: standalone page that exchanges code+state via
  completeAuthorizationCodeGrant (SDK validates state = CSRF), derives
  user_id/device_id from the new access token via whoami(), persists the OIDC
  session (refresh token + expiry + issuer/clientId/redirectUri/idTokenClaims),
  then full-page-reloads at the app root. Minimal UI (no Overlay/portal) so it
  needs no app providers.
- App.tsx: short-circuit — render OidcCallback before the RouterProvider when the
  path is the OIDC callback (redirect_uris can't contain a fragment, so it must
  live outside the hash router). The nginx SPA catch-all already serves index.html
  for it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:05:22 -04:00
jared a50d3e7ca7 feat(auth): OIDC phase 2 — login initiation (discover/register/authorize)
- oidc/oidcState.ts (pure, +3 tests): dynamic-registration cache (by issuer +
  redirectUri, corrupt-tolerant) and parseOidcCallbackParams (success/error/invalid).
- oidc/oidcLoginUtil.ts: getOrRegisterClientId (cache + registerOidcClient) and
  startOidcLogin (discoverAndValidateOIDCIssuerWellKnown -> generateOidcAuthorization
  Url -> redirect; invalidates the cache on failure). redirectUri is the
  deterministic getOidcCallbackUrl(), and the SDK returns clientId/issuer on
  callback, so no hand-rolled transient state is needed.
- login/OidcLogin.tsx: native-OIDC button mirroring SSOLogin + TokenLogin async/error.
- login/Login.tsx: issuer-gated — when discovery advertises an issuer, render
  OidcLogin and suppress password/legacy-SSO; non-OIDC servers unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:01:35 -04:00
jared d3d2f9a448 feat(auth): OIDC phase 4a — session persistence for refresh/expiry/oidc metadata
setFallbackSession gains an optional `extra` arg (password call sites unchanged)
persisting cinny_refresh_token, cinny_expires_at (absolute), and
cinny_oidc_{issuer,client_id,redirect_uri,id_token_claims}. getFallbackSession
reads them back (expiry as remaining lifetime); removeFallbackSession + re-save
clear stale OIDC keys. Session type gains `oidc?: OidcSessionMeta`. +2 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:55:30 -04:00
jared 98ad5674a8 feat(auth): OIDC phase 0+1 — discovery, flow detection, client config
Toward MSC3861/MSC2965 next-gen-auth login (P4-6), client-only.
- cs-api.ts: type the stable `m.authentication` well-known key + getOidcIssuer()
  (stable preferred over the unstable msc2965 key; {} for non-OIDC servers).
- useParsedLoginFlows.ts: getOidcCompatibilityFlag() (MSC3824 oauth_aware_preferred
  / delegated_oidc_compatibility) as a secondary OIDC hint.
- New pages/auth/oidc/oidcConfig.ts: dynamic-registration client metadata + the
  non-hash callback URL (redirect_uris can't contain a fragment).
- paths.ts: OIDC_CALLBACK_PATH.
- 8 unit tests for the pure helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:51:23 -04:00
jared 30d0331174 fix(ui): isMacOS always returned false on Macs + plugin-logic tests (+49)
Coverage work found a 3rd real bug: isMacOS() compared os.name against the
legacy 'Mac OS' string, but ua-parser-js v2 reports 'macOS' — so it was dead,
and Mac users saw "Ctrl + k" instead of "⌘ + k" in the editor toolbar, search,
and settings shortcut hints. Now accepts both 'macOS' and 'Mac OS'.

Suites (via subagent, verified): via-servers (10 — power/popularity server
selection), bad-words (9), syntaxHighlight tokenize (14), plugins/utils
getEmoticonSearchStr (5), imageCompression formatFileSize/isCompressible (5),
user-agent (6, now asserting the fixed behavior).

Full suite now 501 tests, all passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:58:06 -04:00
jared 24662fa994 test: localStorage-backed state modules (+38)
CI / Build & Quality Checks (push) Successful in 11m15s
CI / Trigger Desktop Build (push) Successful in 10s
Via subagent, no bugs:
- state/utils/atomWithLocalStorage (9): get/set helpers + atom write-through.
- state/scheduledMessages (6): Map<->Record round-trip, persistence, mount-gated
  hydration (atomWithStorage w/o getOnInit — modeled with a subscription).
- state/spaceRooms (9): Set dedupe + no-write-when-unchanged + serialization.
- state/navToActivePath (8): per-user Map<->Object serialization.
- state/callPreferences (6): the privacy rule forcing video=false on load+persist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:53:52 -04:00
jared 230ef8ed7c test: markdown parser subsystem (58) + custom-emoji readers (32)
Via subagents, probe-verified against real output, no bugs:
- markdown: internal/utils (11), inline/runner (7), inline/parser (21 — bold/
  italic/underline/strike/code/spoiler/link, nesting, precedence, URL lookbehind),
  block/parser (19 — headings/code-fences/quotes/lists/<br>/escapes). Closes the
  biggest coverage hole (core message rendering).
- custom-emoji: PackMetaReader (6), PackImageReader (7), PackImagesReader (4),
  utils equality+makeImagePacks (5), recent-emoji promote/increment/100-cap (10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:52:48 -04:00
jared 160c09e525 test: add suites for 8 simple state reducers + msgContent (+50)
Via subagent, all verified, no bugs:
- state/toast (7), room-list/roomList (6), inviteList (6), room-list/utils
  compareRoomsEqual (6), backupRestore (6), callEmbed dispose-on-replace (6),
  closedNavCategories factory + makeNavCategoryId (8).
- features/room/msgContent (5): getAudioMsgContent/getFileMsgContent incl.
  encrypted (content.file) vs plain (content.url) branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:43:33 -04:00
jared 589d45e0a0 test: add suites for list, roomToParents, roomToUnread reducers (+43)
Via subagent, verified against real behavior (all use jotai store + enableMapSet):
- state/list (11): createListAtom PUT/DELETE/REPLACE (single + array, identity).
- state/room/roomToParents (10): INITIALIZE/PUT/DELETE incl. cycle-skip and
  orphan-cleanup pruning of zero-parent children.
- state/room/roomToUnread (22): unreadInfoToUnread, unreadEqual, and the
  roomToUnreadAtom reducer — leaf/overwrite/equal-guard, multi-level parent
  roll-up with `from` recording, RESET rebuild, DELETE subtract/prune.

No bugs (noted a latent never-hit string-spread in deleteUnreadInfo's `from ??
roomId` fallback; left as-is). Suite growing toward full pure-logic coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:42:36 -04:00
jared acd355bb5a docs(bugs): test suite at 231 tests; 2 real bugs caught by coverage
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:35:05 -04:00
jared 6e59395fb8 test: lotus decorations, call caps, crypto, featureCheck, typing, markdown (+34)
Subagent batch (no bugs found) + markdown:
- lotus/avatarDecorations (8): decorationUrl, CDN shape, ALL_DECORATIONS
  flattening, data invariants (unique category ids + slugs, slug charset).
- plugins/call/utils (7): getCallCapabilities — static caps + room/user/device
  scoped state-keys.
- utils/matrix-crypto (3): verifiedDevice via a stubbed CryptoApi.
- utils/featureCheck (3): checkIndexedDBSupport success/error/throw paths.
- state/typingMembers (8): add/dedup-by-latest-ts/per-room-scope/delete reducer
  via a jotai store (enableMapSet, mirroring app startup).
- plugins/markdown/utils (5): inline + block escape/unescape round-trips.

Full suite now 231 tests, all passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:32:53 -04:00
jared 9f4516c6a8 test: add suites for state/sessions, recentSearches, upload (+17)
Via subagent, all verified against real behavior:
- state/sessions (5): fallback-session round-trip across the four cinny_* keys,
  missing-key → undefined for each required key, removeFallbackSession clears all.
- state/recentSearches (6): addRecentSearch prepend, case-sensitive dedupe +
  move-to-front, trim, ignore empty/whitespace, cap at 10.
- state/upload (6): the createUploadAtom reducer driven through a real jotai
  store — idle→loading→progress(gated)→success/error, file ref preserved.

No bugs found.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:29:36 -04:00
jared 0bd2273bee test: add suites for utils/room (40) + plugins/matrix-to (7)
- utils/room (40, via subagent): 28 helpers — state-event accessors, m.direct
  parsing, space/room classification, parent/child graph (incl. cycle safety),
  mute-rule + notification logic, unread info, reply trimming, member display/
  avatar/search, reaction/edit/mention extraction, room-icon branches. SDK/
  crypto-heavy helpers skipped. No bugs found.
- plugins/matrix-to (7): matrix.to permalink build + parse for user/room/event
  including via-server round-trips and negative cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:28:32 -04:00
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 e17cb09269 fix(settings): don't crash on load when localStorage is blocked + tests (+6)
Prevention work found a real bug: getSettings() runs at module load, and its
catch block called localStorage.removeItem() — but we often reach that catch
*because* localStorage access threw (blocked storage / private mode / sandboxed
context). The removeItem then re-threw, producing an uncaught error that crashed
the whole app at startup. Guarded the cleanup in its own try/catch.

New state/settings suite (6) covers the legacy-boolean callNoiseSuppression
migration, denoise-model/ringtone-id coercion of unknown values, default merge,
malformed JSON, and the blocked-storage regression.

Full suite now 129 tests, all passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:46:51 -04:00
jared 4d55e45962 docs(bugs): test suite at 123 tests, now a hard CI gate
CI / Build & Quality Checks (push) Successful in 10m49s
CI / Trigger Desktop Build (push) Successful in 11s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:26:58 -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 1e37b20c6a ci: promote unit tests to a hard gate
108 deterministic pure-logic tests now block the build job (and thus deploy) on
failure, alongside the Build step. Moved out of the informational quality-checks
section and dropped continue-on-error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:17:20 -04:00
jared 4f03775e04 docs(bugs): test suite at 108 tests across 11 modules
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:07:41 -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 a7d145aa70 docs(bugs): manifest:false verified OK (not a bug)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:39:06 -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 2a0478cad8 docs(bugs): N105 fixed → Needs Verification
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:13:34 -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 68b6ffffd7 docs(bugs): test-suite gap now seeded (harness + 19 tests)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 08:50:50 -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 36343baecc call: lint/format cleanup for lotus EC wiring
CI / Build & Quality Checks (push) Successful in 10m27s
CI / Trigger Desktop Build (push) Successful in 25s
Resolve the eslint/prettier failures from the previous commit (non-blocking
in CI, but real): drop the banned `void` operator on fire-and-forget
transport.send().catch() calls, prefix the now-unused _denoiseNativeNS
param, and run prettier on the touched files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:52:45 -04:00
jared 89cf171efc call: consume self-built Element Call fork + activate Lotus features
CI / Build & Quality Checks (push) Successful in 11m5s
CI / Trigger Desktop Build (push) Successful in 25s
Switch to @lotusguild/element-call-embedded@0.20.1-lotus.1 (our self-built
fork) and turn on the source-level features it adds:

- #1 denoise CUTOVER: in-source ML denoise (lotusDenoiseSource=1) replaces
  the build-time getUserMedia shim — removed the shim injection from
  vite.config.js (denoise/ assets still shipped; the processor loads them).
  Survives reconnects (fixes A7).
- #2 call-state: CallEmbed consumes io.lotus.call_state; useCallSpeakers /
  useRemoteAllMuted prefer it over scraping EC's DOM (DOM fallback kept;
  empty payloads ignored).
- #4 focus: CallControl.focusCameraParticipant sends io.lotus.focus_participant
  (works during screenshare), replacing the DOM tile-click hack.
- #5 theming: lotusTransparent=1 (native transparent background).
- #6 decorations: LotusDecorationPusher sends each member's decoration URL
  via io.lotus.decorations -> rendered on in-call tiles.

#3 soundboard / #7 quality ship dormant (EC-ready; no host UI sends them yet).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:33:52 -04:00
jared 149ec8e4e4 docs: add Element Call fork handoff + tag all EC-FORK references
CI / Build & Quality Checks (push) Successful in 10m28s
CI / Trigger Desktop Build (push) Successful in 7s
Captures the plan to fork element-hq/element-call and build it from source for
true ownership of the in-call experience (decorations, focus/screenshare,
reconnect mic, native theming, call-audio injection) — none of which are fixable
against the prebuilt @element-hq/element-call-embedded bundle we ship today.

- New HANDOFF_ELEMENT_CALL_FORK.md: self-contained plan for a fresh session
  (current architecture, full file inventory, phases, new-repo decision, the
  denoise-shim interaction, doc corrections).
- Tagged every related note with [EC-FORK] + links: README (For Developers),
  LOTUS_BUGS (EC limitations), LOTUS_TODO (soundboard, denoise, soundboard
  cross-origin correction), LOTUS_FEATURES (call section).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:50:10 -04:00
jared d1cd963e4b docs(bugs): record live-test results + EC iframe limitations
Verified passing: A2, B1-B4, C1, C3, D. Re-fixed and awaiting re-test: A1
(ringtone loudness), A3/A4 (caller decline notice), G1 (All-muted badge).
Documented A5/A6/A7 as known Element Call iframe-boundary limitations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 19:23:13 -04:00