Commit Graph

1767 Commits

Author SHA1 Message Date
jared 50e4da8ea5 fix: GIFs, topic display, formatting toolbar, Copy Link in Invite, support URL
CI / Build & Quality Checks (push) Successful in 10m32s
- RoomInput: GIF domain check uses endsWith('.giphy.com') — handles all
  Giphy CDN shards; all silent failure paths now show user-facing error
- RoomProfile: topic plain-text field is now HTML-stripped (no markdown
  syntax visible in header); formatting toolbar (B/I/S/code) above the
  textarea wraps selected text with correct markdown syntax
- InviteUserPrompt: Copy Link button added to dialog header with
  'Copied!' confirmation; removed Copy Link from both three-dot menus
- RoomViewHeader/RoomNavItem: unused copy-link imports removed
- nginx (live): support_page URL updated from lotusguild.org →
  matrix.lotusguild.org

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 09:55:04 -04:00
jared c852b6f121 fix: P0 post-deploy bug fixes from live testing
CI / Build & Quality Checks (push) Successful in 10m36s
- EditHistoryModal: use raw fetch with /_matrix/client/v1/ path for
  relations API — Synapse only supports relations at v1, not v3 (fixes
  Sentry JAVASCRIPT-REACT-K / 404 error)
- RoomViewHeader: remove useReportRoomSupported spec-version gate —
  Synapse 1.114+ has the endpoint but only advertises spec v1.12;
  button now always shows for non-creator, non-server-notice rooms
- ReportRoomModal: handle M_UNRECOGNIZED/404 with "not supported by
  your homeserver" message
- RoomNavItem: add isServerNotice guard to Room Settings in sidebar
  context menu (was only guarded in header three-dots menu)
- initMatrix: bump setMaxListeners from 50 → 150 to prevent
  MaxListenersExceededWarning with large room lists
- RoomProfile: save topic with formatted_body + format when markdown
  syntax is detected; add markdown hint below topic textarea

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 22:14:21 -04:00
jared 6fbde91fc9 fix: remove unused useRoomName import, run prettier on all changed files
CI / Build & Quality Checks (push) Successful in 10m25s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 21:38:35 -04:00
jared 051d207079 fix: comprehensive P0 quality pass — audit findings resolved
CI / Build & Quality Checks (push) Failing after 5m43s
- 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 ebdc6fc581 chore: prettier formatting for P0 feature files
CI / Build & Quality Checks (push) Failing after 5m39s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 20:55:06 -04:00
jared 683159bed8 feat: personal room name overrides (MSC4431-style)
Users can right-click any room and 'Rename for me...' to set a local
display name visible only to them. Stored in account data under
io.lotus.room_names. Shows a pencil indicator on renamed rooms.
useLocalRoomName() hook overrides useRoomName() when a local name exists.

Also includes:
- Rich room topic rendering via RoomTopicContent object (formatted_body
  support in RoomTopicViewer with HTML sanitization via sanitizeCustomHtml)
- Edit history viewer: clicking '(edited)' on a message opens a modal
  showing all prior versions with timestamps (EditHistoryModal.tsx)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 17:21:11 -04:00
jared 6135db3405 feat: server support contact display (MSC1929) and server notices UI
- Settings Help/About fetches /.well-known/matrix/support and displays
  admin contact + support page link (graceful 404 degradation)
- Server notice rooms (m.server_notice) now show a Warning badge in the
  room header and hide the message composer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 17:15:01 -04:00
jared dc5570f5f7 feat: add Report Room (MSC4151) and fix URL preview default for encrypted rooms
- Report Room: new ReportRoomModal with reason + category, POST /rooms/{id}/report
- URL preview: encUrlPreview default changed to true; security note added to settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 17:11:21 -04:00
jared a893b13f97 docs: final audit pass — all 17 remaining audits resolved
CI / Build & Quality Checks (push) Failing after 5m49s
New findings:
- Service worker EXISTS at src/sw.ts — task #95 just needs notificationclick handler
- Highlight animation EXISTS in layout.css.ts:44-66 — task #81 just wires it to @mentions
- CallControl.toggleSound() EXISTS — task #100 push-to-deafen trivial
- Sanitizer strips <math>/<mml> — task #56 needs sanitizer changes too
- Folds uses vanilla-extract not CSS vars in non-TDS — task #74 needs theme variant
- Cinny cannot inject audio into EC stream — task #88 redesigned as local-only soundboard
- Policy list code: zero existing code, completely additive
- Notification dispatch: only 2 code points — task #12 is 4-line addition
- Upload preview: UploadCardRenderer.tsx:19-98 — task #36 insertion point found
- Room stats cache limited to ~80 events — task #45 must label clearly
- MSC3489/3672 live location: BLOCKED on server
- Profile banner: DROPPED — not in matrix-js-sdk or any Matrix standard

Server checks:
- /.well-known/matrix/support: 404 (needs server-side file creation)
- MSC3489 live location: false → task #64 added to BLOCKED section
- preview_url: requires auth (endpoint exists, works with user token)

Stale [AUDIT REQUIRED] tags scrubbed from P0 section.
All upstream-confirmed items removed or marked clearly.
Architecture reference table expanded with 15 new confirmed facts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:42:50 -04:00
jared 22e7252de8 docs: comprehensive audit pass — all 5 agent findings integrated into TODO
CI / Build & Quality Checks (push) Failing after 5m42s
Key findings:
- Jump to Date (task #7): DELETED — JumpToTime.tsx fully implemented upstream,
  wired in RoomViewHeader.tsx:215
- useUnverifiedDeviceCount() hook EXISTS (useDeviceVerificationStatus.ts:65) —
  task #65 is trivial
- useCallMembersChange() hook (useCall.ts:37-52) handles join/leave sounds
  via MatrixRTCSessionEvent.MembershipsChanged — correct approach for #89
- MessageQuickReactions already in hover toolbar (Message.tsx:146-184) — #92
  simpler than expected
- knockSupported() utility exists (matrix.ts:376) — #58 only needs RoomIntro button
- StateEventEditor in DevTools can already edit m.room.server_acl — #69 is a UX wrapper
- getMatrixToRoom() in matrix-to.ts already generates invite URLs — #24 just needs QR
- Glassmorphism: sidebar container safe for backdrop-filter (translateX only on items)
- Animated backgrounds: must use ::before pseudo-element, not backgroundImage
- matrix-js-sdk has no arbitrary profile field setters — #62 needs raw HTTP
- Toast system: build from scratch, insert at App.tsx:65 after OverlayContainerProvider
- JetBrains Mono already loaded via Google Fonts CDN in index.html:33
- MSC4151 report room endpoint confirmed live (405 on GET = POST-only endpoint exists)
- /.well-known/matrix/support not configured — needs server file creation + client reads

Full file reference table added with 30+ key file paths for all planned features.
Corrected 5 previously wrong architecture assumptions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 13:34:34 -04:00
jared c9a3edc142 docs: add blocked features section, upstream improvements, TDS design law
CI / Build & Quality Checks (push) Failing after 5m43s
- Blocked section: MSC3892 (reaction redaction), MSC3266 (room preview),
  MSC4306 (thread subscriptions), MSC4260 (report user) — all noted with
  unblock conditions and next steps
- 5 new [IMPROVE] tasks for upstream features: Jump to Latest (#104),
  spoiler transition (#105), report category (#106), speaking ring (#107),
  typing dots TDS (#108)
- TDS design law banner added at top of file and repeated in Implementation Notes
- Pinned messages count badge confirmed upstream (no action needed)
- Audits 1, 2, 4 marked complete; audit 3 (profile banner) remains open
- Upstream features table updated with improvement task cross-references

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 13:21:39 -04:00
jared ee17f6257e docs: complete audit pass — server checks, upstream checks, code architecture
Server findings:
- Synapse 1.153.0 is FULLY UP TO DATE (latest as of 2026-05-19)
- MSC4140 (scheduled msgs), MSC3771 (thread receipts), MSC4133 (extended profiles)
  all CONFIRMED supported via unstable_features flags
- MSC3892 (reaction redaction), MSC3266 (room summary) BLOCKED — not supported
- MSC4306 (thread subscriptions) BLOCKED — not supported

Upstream Cinny confirms (removed from build queue):
- Back to Latest button (RoomTimeline.tsx:2180), Mark rooms as read (Home.tsx:73),
  Tombstone/upgrade banner (RoomTombstone.tsx), Speaking indicator (useCallSpeakers.ts),
  Spoiler rendering (ImageContent/VideoContent — blur+click-reveal), Report message

Architecture facts documented:
- AvatarImage child constraint (no children — wrap externally)
- Sidebar translateX blocks backdrop-filter
- EC bridge: no participant events (use m.call.member state events instead)
- No in-app toast system (must build from scratch)
- Voice player at AudioContent.tsx:44, notification sounds hardcoded in ClientNonUIFeatures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 13:09:57 -04:00
jared 3ae99b9a69 docs: expand LOTUS_TODO.md with gamer/aesthetic feature wave and 30+ new items
CI / Build & Quality Checks (push) Failing after 5m42s
Adds Priority 5 section covering:
- Visual/theme: custom accent color, 5 new theme presets, glassmorphism,
  animated wallpapers, night light, font selector, seasonal themes
- Gamer UX: LFG command, quick reaction bar, soundboard, join/leave sounds,
  voice channel limit, AFK auto-mute, push-to-deafen hotkey
- Profile/avatar: frames, animated overlays, status-based border
- Chat polish: mention animation, collapsible messages, send animation,
  message length counter, quick reply from notification, custom mention color
- Utility: room context menu improvements, notification profiles
- Bug fix: drag-and-drop overlay doesn't dismiss on hover-away
- 4 pending audits added (profile banner, speaking indicator, etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 01:02:27 -04:00
jared 685da48a6e docs: remove upstream Cinny features from README (stickers, pins, who-reacted)
CI / Build & Quality Checks (push) Successful in 10m47s
README.md only tracks Lotus Chat custom additions. Emoji/sticker picker,
pinned messages, and who-reacted viewer all ship with upstream Cinny main
and should not appear here. Custom status message (not in upstream) stays.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:07:40 -04:00
jared 2cbf38fe11 docs: document who reacted, sticker/emoji panel, pinned messages, custom status
Add proper README entries for four features that were implemented but
undocumented or only mentioned incidentally:
- Emoji & sticker picker in composer (sends m.sticker via mx.sendEvent)
- Pinned messages panel (header icon + context menu pin/unpin)
- Who reacted: hover tooltip + right-click ReactionViewer modal
- Custom status message: emoji picker, auto-clear timer, 64-char limit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:06:59 -04:00
jared c96e0a26f6 fix: clear status_msg when leaving DND presence state
CI / Build & Quality Checks (push) Successful in 10m21s
Pass status_msg: '' explicitly on setOnline/setOffline/setUnavailable(idle)
so the Matrix server overwrites the 'dnd' status_msg left from DND mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:57:27 -04:00
jared 8be0c646e3 style: fix Prettier formatting in SettingsTab
CI / Build & Quality Checks (push) Successful in 10m23s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:59:46 -04:00
jared a3dd873d36 fix: restore avatar rendering and split presence dot into separate button
CI / Build & Quality Checks (push) Failing after 5m41s
The previous version wrapped UserAvatar in a div inside SidebarAvatar,
which broke the folds Avatar CSS (expects AvatarImage/AvatarFallback as
direct child) — causing the white circle instead of the avatar.

New approach:
- SidebarAvatar has only UserAvatar as its direct child (restored)
- Clicking the avatar opens Settings directly (original behavior)
- PresencePicker renders a small absolutely-positioned button in the
  bottom-right corner of SidebarItem (which already has position:relative)
- Clicking the presence dot opens the status picker menu

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:52:15 -04:00
jared 7de0dfa3c6 fix(eslint): add missing useCallback deps, remove stale disable directives
- useMessageSearch: add fromTs/toTs to useCallback dep array (exhaustive-deps error)
- useMessageSearch: restore eslint-disable on the correct line for the `as any` cast
- VoiceMessageRecorder: remove two eslint-disable directives for rules that are
  globally off (jsx-a11y/media-has-caption) or not enabled (react/no-array-index-key)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:40:08 -04:00
jared b2a9040b60 docs: update README for wave-3 features and presence selector
CI / Build & Quality Checks (push) Successful in 10m20s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:30:10 -04:00
jared 7d223d8d45 feat: Discord-style presence status selector
Adds a manual presence picker to the sidebar user avatar. Clicking the
avatar opens a popout menu with Online, Idle, Do Not Disturb, Invisible,
and Auto (activity-based) options. The selected status is shown as a
colored badge on the avatar and stored in settings (survives reloads).

usePresenceUpdater now short-circuits for manual states and only runs
the full activity-tracking logic in Auto mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:29:29 -04:00
jared dfedba9ef8 feat: document title unread count, draft persistence, search date range
CI / Build & Quality Checks (push) Successful in 10m30s
E1 - Document title unread count: FaviconUpdater now also sets
     document.title to '(N) Lotus Chat' for mentions, '· Lotus Chat'
     for plain unreads, and 'Lotus Chat' when clear. Reuses the
     existing roomToUnread forEach loop.

E2 - Draft persistence across reloads: on room unmount, unsent message
     is written to localStorage as 'draft-msg-<roomId>'. On mount, if
     the Jotai atom is empty (page reload), the localStorage draft is
     restored. Cleared on send. Uses the existing Slate node JSON format.

E5 - Search date range filter: new DateRangeButton in SearchFilters
     with From/To date inputs in a PopOut. Dates stored as epoch ms in
     ?fromTs=&toTs= URL params. Passed to Matrix /search as from_ts /
     to_ts filter fields (valid spec fields, cast via 'as any' since
     SDK types don't include them yet).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 22:22:40 -04:00
jared 7b14eb539f feat(D3): apply terminal theme to voice message recorder
CI / Build & Quality Checks (push) Successful in 10m22s
When lotusTerminal is enabled, the recording dot turns orange (#FF6B00),
the duration timer uses JetBrains Mono in green (#00FF88), and the
waveform bars match green — consistent with the PTT badge and GIF
picker terminal aesthetics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:48:49 -04:00
jared 32384e9820 fix: resolve ESLint no-shadow errors in CallEmbedProvider
CI / Build & Quality Checks (push) Successful in 10m24s
The rect variable in the onUp and onTouchEnd closures was shadowing the
outer rect declaration in handlePipMouseDown and handlePipTouchStart.
Renamed inner declarations to savedRect. Also renamed rect → elRect in
handlePipDoubleClick for the same reason.

Removed unused eslint-disable-next-line comment in MessageSearch.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:31:38 -04:00
jared 403ec3d80c fix/polish: wave 1+2 improvements across six features
CI / Build & Quality Checks (push) Successful in 10m25s
B1 - GIF upload progress: spinner on GIF button + disabled state while
     fetch+upload is in flight; clears on success or error
B2 - PiP position persistence: drag end saves left/top to localStorage;
     entering PiP restores saved position (clamped to current viewport)
B3 - PiP snap-to-corner: double-click the PiP overlay snaps to nearest
     corner with a 180ms CSS transition; saves new position
B4 - Device sessions loading state: useOtherUserDevices now returns
     {status:'loading'|'error'|'success', devices} instead of bare
     array; UserDeviceSessions shows spinner while loading
B5 - Device sessions error state: catch in hook sets status:'error';
     panel shows warning icon + 'Could not load sessions' message
B6 - Screenshare fullscreen Safari guard: hide button when
     document.fullscreenEnabled is false (iOS Safari, some mobile)
B7 - Status save error: show critical-coloured error text below Save
     button when saveState.status === AsyncStatus.Error
B9 - Encrypted search coverage counter: 'X / Y cached' badge next to
     'Encrypted Rooms' heading using existing localResult fields
D2 - PiP screenshare spotlight: track auto-spotlight in a ref; release
     spotlight when screenshare ends while in PiP mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:13:54 -04:00
jared e2b8e162e3 fix: rank from: autocomplete suggestions by homeserver + shared rooms
CI / Build & Quality Checks (push) Successful in 10m23s
Users on the same homeserver (matrix.lotusguild.org) get +1000 to their
score, everyone else starts at +1 per shared room. Sorting by score
descending means @jared:matrix.lotusguild.org always appears before
@jared:matrix.org when both match the query.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 23:05:56 -04:00
jared 25828cc05a fix: search bar size and from: autocomplete actually working
CI / Build & Quality Checks (push) Successful in 10m18s
Two root causes identified:

1. Layout: PopOut renders a Fragment, but the Fragment's children are not
   true flex items of the parent Box in practice — the Input lost its
   full-width stretch. Replaced PopOut with a relative-positioned Box
   wrapper + absolute-positioned dropdown div (top:100%, left:0, right:0).
   Input is now a direct flex child of the wrapper and stretches normally.

2. Autocomplete empty: mx.getUsers() returns almost nothing with lazy
   member loading enabled — only users seen in presence events, not room
   members. Switched to iterating mx.getRooms() + room.getMembers() and
   deduplicating by userId, which covers everyone in every room.

Also: removed PopOut/FocusTrap/RectCords imports no longer needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 22:48:55 -04:00
jared e3507766f6 fix: search bar layout, autocomplete FocusTrap, and from: regex
CI / Build & Quality Checks (push) Successful in 10m28s
Three bugs introduced in 6957e890:

1. Layout: extra Box wrapper around Input wasn't stretching to full
   width. Removed the wrapper — Input is now a direct PopOut child,
   restoring its original full-width flex behaviour.

2. FocusTrap: the autocomplete dropdown had a FocusTrap that immediately
   deactivated because the search input (outside the trap) was focused.
   Removed the FocusTrap entirely; onMouseDown+preventDefault on each
   suggestion item already prevents input blur on click, and onBlur
   with a 120ms delay handles dismissal when clicking truly outside.

3. from: regex: @ was required (from:@user) but users naturally type
   from:user without it. Updated FROM_REGEX and FROM_TYPING_REGEX to
   make @ optional; userId construction already prepends @ if missing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 22:30:38 -04:00
jared 54c1a2733e fix: remove redundant E2EE banner from members drawer
CI / Build & Quality Checks (push) Successful in 10m43s
The banner saying 'E2EE · Shield = verified identity' was cluttering
the top of the member list. Hovering the lock icon in the room header
already communicates encryption status, and tooltips on the shield
badges already explain what verified means. Removed the entire block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 22:09:10 -04:00
jared 6957e890df feat: sender filter for message search with from:@user autocomplete
CI / Build & Quality Checks (push) Successful in 10m17s
Type 'from:@name' in the search box to filter by sender — a dropdown
of matching users (avatar + display name + full ID) appears as you type
and selecting one converts it into a removable sender chip in the filter
bar. Multiple senders supported. Also works via manual entry on submit.

- SearchInput: detects trailing 'from:@...' pattern on every keystroke,
  shows PopOut autocomplete from mx.getUsers(), onMouseDown prevents
  input blur when selecting, cleans up fragment after selection
- SearchFilters: selectedSenders/onSelectedSendersChange props, sender
  chips rendered with user icon and X to remove
- useLocalMessageSearch: filters cached events by sender set when senders
  param is provided (encrypted room search respects the filter too)
- MessageSearch: handleSenderAdd deduplicates and writes to ?senders= URL
  param; localResult now passes senders to the local search

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 22:07:53 -04:00
jared ec110d4ef7 feat: add encrypted room cache panel with load/load-more buttons
CI / Build & Quality Checks (push) Successful in 10m14s
Each encrypted room in scope shows:
- Message count and oldest cached date
- 'Load messages' if no cache, 'Load more' if more history available
- 'Fully cached' label when all history is loaded

Clicking load/load-more paginates backwards 100 messages at a time.
localResult re-computes via cacheVersion after each load so search
results update automatically without re-typing the query.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 20:04:16 -04:00
jared dd2123da4b feat: encrypted room search via local cache scan
No Matrix web client supports E2EE message search server-side — the
homeserver only sees ciphertext. This is the same approach FluffyChat
takes: scan locally decrypted events already in the live timeline.

Changes:
- useLocalMessageSearch: searches getLiveTimeline().getEvents() in
  encrypted rooms using decrypted content (getContent(), not event.content)
- MessageSearch: runs client-side search in parallel with server search,
  shows results in a dedicated 'Encrypted Rooms' section with clear notice
  about scope (only cached/recently viewed messages)
- Encryption notice shown when encrypted rooms are in scope — explains
  why results may be missing and what to do
- Server result limit raised from 20 → 50

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 20:01:21 -04:00
jared 3485a4c118 style: fix Prettier formatting in usePresenceUpdater
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 19:40:15 -04:00
jared fb51b8264c fix: suppress unhandled rejection from saveStatus on rate limit
CI / Build & Quality Checks (push) Failing after 5m34s
useAsync re-throws after setting error state, so callers that don't
await or catch the returned promise get an unhandled rejection. Fixes
JAVASCRIPT-REACT-E (429 on presence endpoint).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 19:36:26 -04:00
jared 845c564618 refactor: move privacy settings into dedicated Privacy section
CI / Build & Quality Checks (push) Failing after 5m35s
Hide Typing & Read Receipts and Hide Online Status were buried in
the Editor section. Extracted into a new Privacy section that sits
between Messages and Calls, where users would naturally look.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 19:31:36 -04:00
jared f184f72286 feat: full Discord-style presence tracking
CI / Build & Quality Checks (push) Failing after 5m42s
- Announce online immediately on app startup
- Idle detection: unavailable after 10 min of no input, online on return
- Tab visibility: unavailable when hidden, online when focused again
- Page close: offline via fetch+keepalive (survives unload without bfcache penalty)
- hidePresence setting: broadcasts offline and stops all tracking
- Added 'Hide Online Status' toggle in General settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 19:28:52 -04:00
jared f3023b34c8 feat: add character counter to status message input
CI / Build & Quality Checks (push) Successful in 10m12s
Shows X/64 below the input. Fades in at 56 chars (warning colour) and
turns critical red at the limit so users always know where they stand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 16:58:37 -04:00
jared 718fb53da1 fix: cap status message length and constrain display overflow
CI / Build & Quality Checks (push) Successful in 10m23s
Add maxLength=128 to the status input to prevent absurdly long statuses.
In UserHeroName (profile panel), tighten the status display to LineClamp2,
add overflow:hidden on the container, and use overflowWrap:anywhere so
unbroken strings (emoji chains, URLs) wrap instead of overflowing.
Member list already truncates via the truncate prop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 16:29:11 -04:00
jared 603c9ec892 fix: import EmojiBoard directly in ProfileStatus to fix emoji selection
CI / Build & Quality Checks (push) Successful in 10m18s
React.lazy + Suspense interacted badly with the nested FocusTraps (the
settings Modal500 outer trap and EmojiBoard's inner trap). During the
suspend/resolve cycle targetFromEvent returned undefined, causing
handleGroupItemClick to bail before calling onEmojiSelect.

Switched to a direct import matching MessageEditor and PowersEditor
which both use EmojiBoard inside settings panels without lazy loading.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 16:01:17 -04:00
jared 9fdc6160eb fix: emoji picker works + silence 429 presence rate-limit errors
CI / Build & Quality Checks (push) Successful in 10m24s
Emoji bug root cause: EmojiBoard wraps itself in a FocusTrap with
clickOutsideDeactivates:true. When the picker was rendered inside
Input's 'after' prop, the FocusTrap treated clicks on the emoji items
as outside-clicks and deactivated (calling requestClose) before the
onEmojiSelect callback fired. Fixed by moving the emoji PopOut to be
a direct sibling of Input in the form row instead of nesting it inside
Input.after — matching the established pattern used in MessageEditor.

429 rate-limit: mx.setPresence() calls in handleClear and the
auto-clear timer effect had no rejection handling, causing unhandled
promise rejections logged to Sentry when Synapse rate-limits presence
updates. Added .catch(() => undefined) to both call sites. Sentry
issue JAVASCRIPT-REACT-E resolved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 15:20:42 -04:00
jared 0ed3c0a384 fix: emoji selection appends correctly to status input
CI / Build & Quality Checks (push) Successful in 10m32s
When focus moves to the emoji picker the input loses focus, making
selectionStart/selectionEnd unreliable. Replace the cursor-tracking
insertion with a simple functional state updater that always appends
the emoji to the end — reliable and appropriate for a short status field.

Also removes the now-unused inputRef and useRef import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 13:41:48 -04:00
jared b086be3def feat: auto-clear status after configurable duration
CI / Build & Quality Checks (push) Successful in 10m39s
Adds an 'Auto-clear after' dropdown to the Status Message settings tile
with options: Never / 30 min / 1 hr / 4 hr / 8 hr / Until midnight /
1 day / 7 days.

How it works:
- On save, stores the expiry timestamp in localStorage keyed by userId
  (lotus-status-expiry-<userId>) and sets expiryTs state
- A single useEffect on expiryTs drives the timer — re-saving cancels
  the previous timer cleanly via useEffect cleanup
- On mount, reads stored expiry from localStorage so auto-clear
  survives page reloads (fires immediately if already expired)
- Manual Clear Status also removes the stored expiry and cancels any
  active timer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 13:10:46 -04:00
jared 2707b59e20 fix: capture emoji button rect before state updater to avoid null currentTarget
React nullifies synthetic event's currentTarget before async state
updater callbacks run. Capture getBoundingClientRect() synchronously
in the onClick handler, then pass the already-computed rect into
setEmojiAnchor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 13:06:31 -04:00
jared c36401db7e feat: custom status message — display + editor with emoji picker
CI / Build & Quality Checks (push) Successful in 10m23s
- MembersDrawer: show presence.status as small muted text below
  username in every member row (live via useUserPresence)
- UserHero/UserHeroName: accept optional status prop; render below
  the @username handle in user profile popouts
- UserRoomProfile: pass presence?.status down to UserHeroName
- Profile settings: new ProfileStatus tile below Display Name
  * Input with inline emoji picker (lazy-loaded EmojiBoard)
  * Cursor-aware emoji insertion (preserves caret position)
  * Save via mx.setPresence({ status_msg }) / Clear button
  * Pre-fills from current presence; syncs on remote update

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 12:39:51 -04:00
jared 1c6df604b1 feat: mute screenshare audio independently + fix CI lint/prettier
CI / Build & Quality Checks (push) Successful in 10m41s
- Add screenshareAudioMuted state to CallControlState and CallControl
- setSound() now preserves screenshare audio mute when un-deafening
- Add toggleScreenshareAudio() targeting audio[data-lk-source="screen_share_audio"]
- Add ScreenshareAudioButton (volume icon, warns when muted) to controls bar
- Fix unused prevScreenshare variable (ESLint error from prior commit)
- Run Prettier on Controls.tsx and CallControl.ts (CI formatting failures)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:52:57 -04:00
jared b8f1cc3c08 chore: track package-lock.json, revert gitignore change
CI / Build & Quality Checks (push) Failing after 5m42s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:18:15 -04:00
jared eeba02aeca feat: screenshare fullscreen button + pip spotlight, fix screenshare view
- Remove revert-to-grid logic that was overriding EC's natural screenshare
  spotlight, causing fullscreen to show user avatars instead of the screen
- Add fullscreen button to call controls (visible when screensharing) that
  requests fullscreen on the call embed container
- Add FullscreenButton component with enter/exit SVG icons to Controls.tsx
- PIP mode: sync setPipMode to CallControl; auto-enable spotlight when
  screenshare is active in pip so the screenshare fills the window
- Make useCallControlState accept undefined control for safe use in
  CallEmbedProvider
- Add package-lock.json to .gitignore (generated by local npm install)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:16:43 -04:00
jared e07c9cc491 fix: prevent editor blur on any click inside autocomplete menu
CI / Build & Quality Checks (push) Successful in 10m44s
FocusTrap monitors focusin events and can redirect focus into the menu
container (blurring the editor) before individual MenuItem onMouseDown
handlers fire. Adding preventDefault at the container level ensures no
click anywhere inside the menu can steal focus from the editor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:35:38 -04:00
jared 2623e2af93 fix: prevent editor blur on mouse click in autocomplete menus
CI / Build & Quality Checks (push) Successful in 10m29s
Add onMouseDown preventDefault to all autocomplete suggestion MenuItems
so clicking a suggestion keeps the editor focused. Without this, the
mousedown event blurs the editor before onClick fires, causing Slate's
ReactEditor.toDOMNode to fail with "Cannot resolve a DOM node from Slate
node: {"text":""}" when Transforms.collapse tries to sync the selection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:10:01 -04:00
jared dde75ee389 fix: defer moveCursor to next tick to prevent Slate DOM resolution crash
CI / Build & Quality Checks (push) Successful in 11m12s
When selecting an autocomplete suggestion (user/room mention, emoticon,
command), Slate's replaceWithElement inserts a void inline node into the
model but React hasn't flushed the DOM update yet. Calling
Transforms.move + insertText immediately after causes ReactEditor.toDOMNode
to fail with "Cannot resolve a DOM node from slate node: {\"text\":\" \"}".

Fix: wrap moveCursor body in setTimeout(fn, 0) so React can flush the void
element's DOM node before Slate attempts to resolve the cursor position.
Also call ReactEditor.focus to restore editor focus after the autocomplete
menu item click blurs the editor.

Fixes all callers: UserMentionAutocomplete, RoomMentionAutocomplete,
EmoticonAutocomplete, CommandAutocomplete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 17:16:37 -04:00