Commit Graph

2222 Commits

Author SHA1 Message Date
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 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 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 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 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 2c3dba55e6 fix(ui): use folds Text priority instead of raw opacity (native-cinny nit)
Replaced raw style={{ opacity: N }} de-emphasis on folds <Text> with the
`priority` prop across search, schedule, profile, and tray UI. Left the cases
that aren't Text-priority candidates (an Icon opacity, a Box-row opacity, and a
Text with an explicit color token).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 15:44:57 -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 c68ef346bf fix(ui): MediaGallery lightbox uses folds Overlay + FocusTrap (native-cinny audit 8/N)
The full-screen media viewer was a raw <div role="dialog"> rendered in place
with manual focus. Wrapped it in folds Overlay (portal + backdrop, proper
stacking) and FocusTrap (focus management), keeping its own arrow/Escape key
handling. The light-on-dark chrome (#fff over the forced-black media stage) is
kept — it's a justified, always-dark media-viewer scrim, not theme chrome.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 14:49:24 -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 24d6460e4c chore: remove Sentry.io entirely
We no longer use Sentry. Removed:
- @sentry/react + @sentry/vite-plugin (package.json + lockfile)
- Sentry.init in index.tsx and the VITE_SENTRY_DSN env (.env.production)
- @sentry/vite-plugin + the SENTRY_AUTH_TOKEN sourcemap-upload path in
  vite.config.js (sourcemap now always false) and the CI env var
- Sentry.ErrorBoundary in App.tsx -> react-error-boundary's ErrorBoundary with a
  folds-native fallback (Box/Text/Button + config tokens), which also resolves
  the native-cinny audit's raw-#hex/#5865f2 fallback finding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:21:09 -04:00
jared 127e783f66 fix(ui): toast cards render on stock themes; gate TDS glow (native-cinny audit 4/N)
LotusToastContainer was styled entirely with --lt-* CSS vars but rendered
unconditionally (not gated on lotusTerminal). Those vars only exist inside the
Lotus Terminal theme's scoped block with no global fallback, so in-app toast
notifications rendered with undefined background/border/colors on every stock
Cinny theme. Now the card uses folds tokens (color.Surface.*/Primary.*,
config.radii/space/borderWidth, color.Other.Shadow) by default, keeping the TDS
--lt-* glow/accents only when lotusTerminal is active. The raw <button> dismiss
control is now a folds IconButton.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:06:33 -04:00
jared 198fd12bb2 fix(ui): folds tokens for ML-denoise panel + screenshare popover (native-cinny audit 3/N)
- General ML noise-suppression panel: ungated --border-color/--bg-card/--bg-input/
  --accent-orange -> color.Surface.ContainerLine/Container, SurfaceVariant.Container,
  Primary.Main. (The lotusTerminal-gated Boot button keeps its TDS --accent-orange.)
- CallControls "Share your screen?" popover: --bg-surface/--bg-surface-border ->
  color.Surface.Container / ContainerLine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:01:17 -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 1778cd0009 fix(calls): release AFK-monitor mic capture when muted (N95)
useAfkAutoMute opened its own getUserMedia capture for the whole call and only
stopped it on unmount, so the OS recording indicator stayed lit even when the
user was muted. The capture is now gated on the reactive mic-on state: it runs
only while unmuted (there's nothing to auto-mute when already muted), so muting
tears down the stream and clears the indicator, and unmuting re-acquires it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:15:31 -04:00
jared 65e24bd446 feat(themes): 5 new dark theme presets — Cyberpunk/Ocean/Blood Red/Matrix/Midnight (P5-2)
Five complete vanilla-extract themes registered in useTheme (useThemes +
useThemeNames), each spreading darkThemeData so Success/Warning/Critical keep
their semantic colors and only Background/Surface/Primary/Secondary are
recolored. A code-review pass computed WCAG contrast for every theme; all body
and accent pairs clear AA except Midnight's Primary.OnMain which was 4.49:1 —
fixed by changing OnMain #0d1320 -> #000000 (5.07:1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 16:47:50 -04:00
jared de6cecaffc feat(search): "Pinned only" filter (composes with msgtype + local results)
Adds a "Pinned" toggle chip that narrows results to messages currently in
their room's m.room.pinned_events. Client-side post-filter mirroring the
has:image/file/video pattern: a pure filterGroupsByPinned(groups, enabled,
isPinned) helper consumes a predicate; MessageSearch builds a per-room
Map<roomId, Set<eventId>> from StateEvent.RoomPinnedEvents.

Review fix: the msgtype + pinned filters are now applied to BOTH the server
results AND the encrypted/local-cache results (via a shared applyResultFilters
useCallback), so the chips narrow the whole UI consistently — previously the
local/E2EE section bypassed them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 16:47:50 -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 1ee0f0b57a feat(search): has:image/file/video filters + recent searches
- Add three msgtype toggle chips (Images/Files/Video) to the search filter
  bar, mirroring the existing "Has link" chip. The Matrix search API can't
  filter by msgtype server-side, so results are post-filtered client-side
  (union match on event.content.msgtype, dropping now-empty groups); the
  server request is unchanged. Visible count may be lower than the server
  total — inherent to client-side filtering.
- Recent searches: last 10 distinct terms persisted via a new
  state/recentSearches.ts (atomWithStorage, error-safe, mirrors
  scheduledMessages). Shown as clickable chips when the search input is
  focused + empty, with a Clear affordance; clicking re-runs the search.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 14:22:08 -04:00
jared 33b33e685a feat(about): credit Cinny logo + upstream project in Settings → About
The current Lotus Chat icon overlaps the Cinny project logo with the Lotus
Guild emblem, but the in-app Credits list gave Cinny no attribution at all.
Add two credit entries (matching the wording already in README.md):
- the logo as a CC-BY 4.0 derivative of the Cinny logo by Ajay Bura and
  contributors (modified logo © Lotus Guild, also CC-BY 4.0);
- Lotus Chat as a fork of Cinny used under AGPL-3.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 12:54:30 -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 34997bcbd1 fix(client): preserve prefs on logout; recover from initial-sync failure (N98/N99)
- N98: logoutClient and handleLogout now call removeFallbackSession() (removes
  only the 4 session credential keys) instead of window.localStorage.clear(),
  so settings, unsent drafts, PiP position, and status are preserved across a
  normal logout. localStorage.clear() stays reserved for clearLoginData() (the
  explicit factory-reset path).
- N99: the useSyncState callback now handles ERROR/STOPPED. A sync failure
  before the first PREPARED (offline at startup, homeserver unreachable) shows
  a dedicated error splash with a Retry button (startMatrix) instead of an
  endless "Heating up" spinner alongside a contradictory "Connection Lost!"
  banner. Guarded by a hasPreparedRef so post-PREPARED transient errors still
  go through <SyncStatus>; PREPARED self-heals the splash on recovery, and the
  redundant banner is suppressed while the splash is shown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 11:27:36 -04:00
jared ce8a03ab16 fix(build,denoise): gate node leak, postMessage origin, fail-hard patch, CDN dedup (N124/N125/N128/N120)
- N124: denoise shim cleanup() now disconnects the noise gate AudioWorkletNode
  (var-scoped, guarded), releasing the gate processor thread instead of leaking
  it on every getUserMedia within a session.
- N125: denoise-status postMessage now targets the parent origin (derived from
  the parentUrl widget param via new URL(...).origin, falling back to this
  frame's origin) instead of broadcasting with '*'.
- N128: patch-folds.mjs fails hard (process.exit(1)) when the patch target is
  missing, so an unpatched folds can't silently ship. The idempotent
  "already applied" path still exits 0 (verified by re-run).
- N120: the avatar-decoration CDN URL is now single-sourced in
  avatarDecorations.ts (DECORATION_CDN); syncDecorations.mjs extracts it by
  regex (can't import across the build/app boundary) and fails hard if renamed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 10:55:19 -04:00
jared 19feca4964 fix(calls): make speaker detection scan full DOM via body observer (N116/N117)
useCallSpeakers rebuilt the speaker Set from only the mutated tiles in each
batch (so a still-speaking participant whose tile didn't mutate was dropped),
and observed a static querySelectorAll NodeList (so tiles for participants who
joined mid-call were never watched). Rewritten to mirror useRemoteAllMuted in
the same file: a single body-level MutationObserver (subtree+childList+attrs)
re-scans ALL [data-video-fit] tiles on each relevant mutation. The speaking
criterion (::before background-image !== 'none') and the id (aria-label +
isUserId) are unchanged, so behavior on real EC DOM is a strict superset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 10:55:19 -04:00
jared 7013da70bc fix(reminders): RMW race, reliable removal, stable poll interval (N113/N114/N115)
- N113: mutations compute from a local ref kept in sync with server echoes, and
  writes serialize through a promise queue, so rapid add/remove no longer reads
  a stale baseline and clobbers a prior write.
- N114: ReminderMonitor shows each toast once (firedRef) but retries the
  account-data removal on later ticks if it fails (removingRef released on
  error) — a failed removal no longer permanently swallows the reminder.
- N115: the 30s poll interval reads reminders/mDirects via refs and drops them
  from the effect deps, so it's created once instead of resetting its countdown
  on every reminder sync (which could indefinitely defer a near-due reminder).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 09:17:19 -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