Commit Graph
100 Commits
Author SHA1 Message Date
jaredandClaude Sonnet 4.6 6ec908407b fix: call ring modal only fires for DMs and private group chats
Space voice channels send room-level RTC notifications (m.mentions.room)
whenever any member joins. Previously this triggered the incoming call
ring modal for every member of the space, as if they were personally
being called.

Fix: before setting callInfo, check whether the room is a DM (in
mDirectAtom) or a private non-space group (invite/knock/restricted join
rule AND no m.space.parent state event). Space channels and public rooms
are excluded — joining them should never ring other members.

Added JoinRule to the matrix-js-sdk import. Added directs to the
useCallback dependency array so the closure sees the current DM set.

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 20:22:38 -04:00
jaredandClaude Sonnet 4.6 a78cb46bfe config: add matrix.org + mozilla.org homeservers, allow custom
matrix.lotusguild.org remains the default (index 0). matrix.org and
mozilla.org added as dropdown options. allowCustomHomeservers: true
lets users type any homeserver address for friends on other servers.

Also applied directly to /var/www/html/config.json — live immediately,
no deploy needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 14:49:22 -04:00
jaredandClaude Sonnet 4.6 78090d1c2d fix: glassmorphism actually visible + ESLint import/order
Glassmorphism root cause (from audit): the body-background useEffect
had `lotusTerminal && chatBackground === 'none' ? 'tactical'` as the
fallback guard — meaning the tactical grid only appeared when Lotus
Terminal mode was active. With default settings (TDS off, no chat
background chosen), the body got no background at all, so
backdrop-filter had a flat solid colour to blur — identical to unblurred.
Fix: drop the `lotusTerminal &&` guard so the tactical dot-grid is
always the fallback when chatBackground is 'none', regardless of theme.
Glassmorphism is now visible in all themes without any additional setup.

ESLint: RoomProfile.tsx had `../../../components/emoji-board` imported
before `matrix-js-sdk` violating import/order. Moved it after.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 14:47:18 -04:00
jaredandClaude Sonnet 4.6 08937c6278 fix: ESLint duplicate import + glassmorphism child opacity
ESLint: UploadCardRenderer.tsx had two separate imports from
../../utils/matrix — merged tryDeleteMxcContent into the existing
import statement. Removed now-unnecessary eslint-disable directive from
chatBackground.ts (the _anim prefix already suppresses the rule).

Glassmorphism: the Scroll inside SidebarNav had variant="Background"
which set a solid backgroundColor on the entire scrollable area,
completely hiding the sidebar's semi-transparent glass + backdrop-filter.
Fix: pass variant={undefined} when glassmorphismSidebar is on so the
inner scroll area is transparent and the blur effect is visible through
it. The document.body background (set by the previous useEffect fix)
now shows through the frosted glass as intended.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:41:29 -04:00
jaredandClaude Sonnet 4.6 dc01480175 docs: update LOTUS_TODO + README for P5-4 and glassmorphism fix
Mark P5-4 (animated chat backgrounds) complete with implementation
notes. Update README Settings section with animated backgrounds entry
and correct glassmorphism description (now explains the body-background
fix that makes backdrop-filter actually visible).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:49:30 -04:00
jaredandClaude Sonnet 4.6 9447e64e5e feat(P5-4): animated chat backgrounds + pause toggle
5 new CSS-only animated backgrounds in the chat background picker:
- Digital Rain: two-layer vertical stripe scroll with parallax (wide
  stripes at 8s, narrow at 4s via single keyframe with split positions)
- Star Drift: three-layer radial-gradient star field drifting diagonally
- Grid Pulse: neon grid lines that expand/contract (backgroundSize keyframe)
- Aurora Flow: large radial gradient bands sweeping across 200% canvas
- Fireflies: three layers of glowing dots drifting across the viewport

All use vanilla-extract keyframes (GPU-composited transforms/positions,
no canvas, no JS timers). prefers-reduced-motion is respected in
getChatBg() by stripping the animation property at call time. A "Pause
Background Animations" toggle in Settings → Appearance provides an
in-app override for the same purpose.

BG labels de-duplicated ("Digital Rain", "Star Drift", "Aurora Flow")
to avoid the duplicate "Stars" and "Aurora" entries that had appeared.
LIGHT anim-fireflies background corrected from near-black #0a0a10 to
warm white #fffdf0. Four unused keyframe exports removed from
Animations.css.ts.

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:25:19 -04:00
jaredandClaude Sonnet 4.6 5d525d4246 docs: update TODO and README for ring fix, E2EE edit history fix, reaction count
- P5-17: 5 → 3 emoji count
- P5-18: box-shadow/borderRadius → outline/cloneElement implementation note
- P0-6 edit history: document getClearContent() E2EE fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 00:33:51 -04:00
jaredandClaude Sonnet 4.6 b5d831cc12 fix: presence ring shape mismatch, edit history (no text) on E2EE, reaction count
- PresenceRingAvatar: replace circular wrapper div (borderRadius 50%) with
  React.cloneElement injecting outline+outlineOffset directly onto the child
  Avatar element — outline follows the child's actual border-radius so the
  ring matches the avatar shape in every context

- EditHistoryModal: use getClearContent() for the Original entry instead of
  evt.event.content, which is still the ciphertext for E2EE messages and has
  no body field. getClearContent() returns decrypted content bypassing the
  _replacingEvent chain, fixing the "(no text)" shown for encrypted originals

- MessageQuickReactions: 5 → 3 emoji (toolbar too wide with 5)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 00:30:42 -04:00
jaredandClaude Sonnet 4.6 8866da0a82 fix: reduce quick reaction emoji count from 5 to 3 (toolbar too wide)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 00:22:09 -04:00
jaredandClaude Sonnet 4.6 d1de438f67 ci: make Prettier check continue-on-error like TypeScript and ESLint
Prettier formatting issues blocked two deploys today. Build is the only
hard CI gate that should block deployment — style checks are informational.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 00:16:56 -04:00
jaredandClaude Sonnet 4.6 c6932b45fb feat: glassmorphism sidebar toggle (P5-3)
Settings → Appearance: "Glassmorphism Sidebar" toggle (off by default).
When enabled, applies backdrop-filter: blur(12px) and a semi-transparent
background to the left sidebar so chat background patterns show through.
SidebarGlass vanilla-extract class in Sidebar.css.ts; wired in
SidebarNav.tsx via classNames conditional.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 22:02:18 -04:00
jaredandClaude Sonnet 4.6 517e992dec fix: prettier formatting in toast.ts (trailing commas)
Fixes CI Prettier check failure on commit 4876c2e4.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 22:01:54 -04:00
jaredandClaude Sonnet 4.6 c5fbc20394 feat: presence avatar border rings (P5-18) + room emoji prefix support (P5-6)
P5-18: PresenceRingAvatar wrapper component applies a 2px box-shadow
ring to user avatars — green (online), yellow (idle/unavailable), red
(DND via status_msg='dnd'), no ring (offline). Applied to: message
timeline sender avatars, members drawer (members + knock requests),
@mention autocomplete, and inbox notification senders.

P5-6: Leading emoji in room names renders at 1.15× in the sidebar via
Unicode emoji regex detection in RoomNavItem. Emoji picker (EmojiBoard
in PopOut) added to all three room-name inputs: Create Room dialog
(converted to controlled input), Room Settings name field (shown only
when canEditName), and the "Rename for me" local rename dialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 21:56:19 -04:00
jaredandClaude Sonnet 4.6 4876c2e4ca feat: quick emoji reactions on hover, in-app notification toasts, mention pulse audit
P5-17: MessageQuickReactions moved from 3-dots menu to hover toolbar;
shows 5 recent emoji directly on hover. Clicking a quick-reaction also
closes any open emoji picker (setEmojiBoardAnchor). Line separator
removed from component.

P5-7: LotusToastContainer slides in from bottom-right when window is
focused — replaces OS notification for in-focus events. Correct room
path (DM vs home) derived from mDirectAtom. Invite toast routes to
inbox. 4s auto-dismiss. Full TDS styling via CSS custom properties.

P5-8: Confirmed already implemented upstream (MentionHighlightPulse,
0.6s scale+glow, one-shot, prefers-reduced-motion). Marked complete.

Code-review fixes: toast navigation used nonexistent /room/ route;
emoji picker stayed open after toolbar quick-reaction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 21:32:37 -04:00
jaredandClaude Sonnet 4.6 24e6882e72 docs: mark P3-5, P3-9, P5-19, P5-23, P5-26 complete in TODO and README
Inline GIF preview, policy list viewer, collapsible long messages,
message send animation, and right-click room context menu improvements
documented and checked off.

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 17:59:39 -04:00
jaredandClaude Sonnet 4.6 08e7f33cba fix: remove copy link, convert mute to PopOut submenu
Copy Link removed — invite link is already in the invite modal.

Flat mute duration items replaced with a single "Mute →" MenuItem
that opens a PopOut submenu (Right/Start) with the 5 durations:
15 minutes / 1 hour / 8 hours / 24 hours / Indefinitely.
Anchor uses RectCords pattern (e.currentTarget.getBoundingClientRect)
matching the existing menu pattern in this file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 17:51:43 -04:00
jaredandClaude Sonnet 4.6 657ca3a5ca feat: GIF previews, room context menu, policy lists, mention pulse, collapsible messages, send animation, D&D fix
P3-5: Giphy/Tenor URL preview cards — full-width thumbnail from og:image
mxc URL, GIF badge overlay, site badge + title footer; GifCard shared by
both; BadgeGiphy (teal) and BadgeTenor (blue) CSS classes

P3-9: Policy list viewer — read-only panel in Room Settings + Space
Settings (admin/50+ PL only); enter room ID or alias; tabs for Users /
Rooms / Servers; glob pattern warning color; Ban badge; entity + reason

P5-8: Mention highlight pulse — 0.6s scale+glow keyframe on incoming
@mention messages; prefers-reduced-motion aware; only fires on new
incoming messages (isNewRef), not on history load; onAnimationEnd cleanup

P5-19: Collapsible long messages — ResizeObserver clamps text bodies
>320px with gradient fade + "Read more ↓" / "Show less ↑" button; resets
on eventId change; skips images/video/audio/file; smooth CSS transition

P5-23: Message send animation — own messages fade+scale in (0.97→1,
0.4→1 opacity, 150ms ease-out); prefers-reduced-motion aware; one-shot
via isNewRef + onAnimationEnd clear

P5-26: Room context menu — Copy Link (matrix.to URL, 1.5s Copied!
feedback), Mute with duration (15m/1h/8h/24h/indefinite, localStorage
timer key io.lotus.mute_timers), Mark as read; Icons.Link + Icons.BellMute

BUG D&D: dragCounter ref replaces fragile dragState machine — enter
increments, leave decrements (hides at 0), drop resets to 0; fixes
spurious dragleave from child element boundary crossings

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 15:05:09 -04:00
jaredandClaude Sonnet 4.6 c80f8c6427 fix: unverified device warning — rounded corners, left accent border
Replaced full-width sharp-cornered flat block with a compact rounded
notice: R300 border-radius, 3px amber left border accent, slight side
margins (S300) and bottom gap (S100) so it sits naturally above the
composer without looking jarring.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 12:48:19 -04:00
jaredandClaude Sonnet 4.6 696e958a00 docs: mark P2-4 through P3-7 complete in LOTUS_TODO and README
LOTUS_TODO.md: 12 features changed [  ] → [x] with COMPLETED June 2026
summaries: P2-4 export history, P2-6 activity log, P2-7 link previews
(13 domains), P2-8 extended profile fields, P2-9 local time display,
P2-10 unverified device warning, P2-11 push rule editor, P2-12 server
ACL editor, P3-1 bookmarks, P3-2 message scheduling, P3-3 compression,
P3-7 room insights

README.md: added entries for all 12 new features in their respective
sections; added 11 new rows to Key Custom Files table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 12:40:18 -04:00
jaredandClaude Sonnet 4.6 d3dcf93f1a fix: compression UI — proper before/after display with folds tokens
Always shows Original size pill even before checkbox is checked.
After checking: shows 'compressing...' then reveals Compressed pill
with exact size and percentage saved. Green tint on compressed pill
when >5% saving; neutral when minimal gain.

Replaced all var(--bg-*) / var(--tc-*) CSS vars with folds
color.Surface.* and color.SurfaceVariant.* tokens so the UI renders
correctly in all themes. Blob cleanup is automatic — the compressed
Blob is referenced only during upload and GC'd with the upload atom.

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

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

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 12:07:12 -04:00
jaredandClaude Sonnet 4.6 8f5afcda08 fix: prettier formatting on UploadCardRenderer.tsx
CI caught missing prettier pass on the file modified by P3-3
image compression feature.

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

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 10:26:08 -04:00
jaredandClaude Sonnet 4.6 ad508ac61e feat(P2-7): deep link preview cards for TikTok, X, Twitch, Reddit, YT Shorts
YouTube Shorts: portrait 9:16 thumbnail, red Shorts badge, channel parse
TikTok: portrait thumbnail, @user extract, caption parse (3 OG formats),
  hashtag chips, dark ♫ placeholder fallback
Twitter/X: tweet text parse from all og:title formats, media image when
  og:image:width>=300, profile vs tweet URL distinction, 𝕏 SVG badge
Twitch: live/clip/VOD detection, pulsing LIVE badge with CSS keyframes,
  game extraction from og:description, channel from URL
Reddit: r/subreddit badge, u/author + upvote + comment count parsed from
  og:description, post thumbnail 80x60, redd.it short URL support

Shared PortraitThumbnail (80x142) reused by TikTok + Shorts.
All brand hex colors in CSS file only, never in TSX.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 09:42:26 -04:00
jaredandClaude Sonnet 4.6 73921cb2a1 feat(P2-7): expand link previews to 13 domain-specific card types
YouTube, Vimeo (shared VideoCard with thumbnail + play overlay),
GitHub (repo name/description parse), Twitter/X (tweet text parse),
Reddit (subreddit + post title), Spotify (artwork + track/artist),
Twitch (thumbnail + LIVE badge), Steam (game header image),
Wikipedia (clean text card), Discord (server icon + member count),
npm (package name/description), Stack Overflow (question excerpt),
IMDb (portrait poster + title/rating)

Generic fallback gains favicon from google/s2/favicons; empty cards
(no title or description) are suppressed. Shared SiteBadge component
with brand-colour CSS classes per domain in UrlPreview.css.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 23:34:23 -04:00
jaredandClaude Sonnet 4.6 01ba24df12 feat: richer link preview cards and user local time display
P2-7: Domain-specific URL preview cards — YouTube shows mqdefault.jpg
thumbnail with ▶ play overlay + title; GitHub shows inline SVG icon +
owner/repo parsed from og:title + star/language info from og:description;
generic cards get a Google favicon when og:image is absent; empty cards
(no title or description) are suppressed entirely

P2-9: Live local time in user profiles — useLocalTime(timezone, hour12)
uses Intl.DateTimeFormat with the user's m.tz IANA zone; updates every
60s; shows clock icon + formatted time + timezone abbreviation (EST/JST
etc.) in dim text; respects existing hour24Clock setting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 23:15:29 -04:00
jaredandClaude Sonnet 4.6 51a355fe77 feat: extended profile fields, push rule editor, server ACL editor
P2-8: Pronouns (m.pronouns) and Timezone (m.tz) fields in Settings →
Account → Profile; saved via MSC4133 PUT /profile/{userId}/{field};
useExtendedProfile hook fetches both in parallel; UserHero displays
pronouns below display name and timezone string below username

P2-11: Full push rule editor in Settings → Notifications below keyword
rules; covers override/room/sender/underride rule kinds; enable/disable
toggle per rule, human-readable labels for built-in rules, delete button
for custom rules, add-rule form for room and sender rules

P2-12: Server ACL viewer/editor in room settings (Server ACL tab);
reads m.room.server_acl state event; allow/deny server lists with
wildcard validation; allow IP literals toggle; power-level gated
(edit requires sufficient PL, otherwise read-only view)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 23:13:33 -04:00
jaredandClaude Sonnet 4.6 160db1eaef fix: export deduplication and PiP remote mute detection
Export: timeline.getEvents() returns the entire growing window on every
pagination step, causing the same events to be added multiple times.
Fixed by tracking seen eventIds in a Set and skipping duplicates.

PiP mute: replace silence-inference with real remote participant mute
state. EC renders a [data-muted] attribute per participant tile with
aria-label=userId. Watch attribute changes via MutationObserver,
filter out local user, show overlay when any remote is muted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 22:44:11 -04:00
jaredandClaude Sonnet 4.6 ee717e8361 feat: PiP mute indicator, export history, activity log, unverified device warning
- PiP call window: mute overlay using MutationObserver on EC iframe's
  [data-testid="incall_mute"] button (data-kind="primary" = muted),
  same pattern as screenshare detection in CallControl.ts

- P2-4 Export Room History: new tab in room settings — Plain Text / JSON /
  HTML formats, optional date range, progress counter, paginated via
  paginateEventTimeline, blob download; E2EE-aware (skips failed decryptions)

- P2-6 Room Activity Log: new tab in room settings — filterable log of
  m.room.member, m.room.power_levels, m.room.name/topic/avatar/server_acl
  events with human-readable descriptions, relative timestamps, Load More
  pagination

- P2-10 Unverified Device Warning: warnOnUnverifiedDevices setting (default
  off); Warning.Container banner above composer in encrypted rooms with
  unverified devices; toggle in Settings → General → Privacy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 22:13:22 -04:00
jaredandClaude Sonnet 4.6 6d0b778755 docs: mark P2-1/2/3/5 complete; update README and landing page
- LOTUS_TODO.md: [x] P2-1 (upstream JumpToTime), P2-2 (custom sounds),
  P2-3 (sort rooms), P2-5 (quiet hours)
- README.md: new Notification Enhancements section (custom sounds, quiet
  hours); Room sort order added to UX & Composer section
- landing/index.html: three new comparison rows — custom notification
  sounds, quiet hours, room sort order

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 19:41:02 -04:00
jaredandClaude Sonnet 4.6 6d2bf9a582 fix: invite modal QR toggle — replace {} icon with "QR Code" button label
Icons.BlockCode is the {} curly-braces icon, which is unintuitive for a
QR code toggle. No QR-specific icon exists in folds, so replace the
IconButton with a labeled Button ("QR Code") that clearly communicates
its purpose. Also fix var(--bg-surface-border) → color.Surface.ContainerLine
in the QR panel border (the CSS variable doesn't exist in Cinny's theme).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 16:59:32 -04:00
jaredandClaude Sonnet 4.6 c798625a79 fix: DM filter input full width — add direction="Column" to wrapper Box
Without direction="Column" the flex container defaults to row, so the
Input only takes its intrinsic width instead of stretching. Matches the
identical Box in Home.tsx which already had direction="Column".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 16:38:42 -04:00
jaredandClaude Sonnet 4.6 194d52a808 fix: poll creator modal — replace non-existent CSS vars with folds tokens
The modal was built with raw <div>/<input> and CSS custom properties
(--bg-surface, --bg-surface-low, --tc-surface-high, etc.) that don't
exist in Cinny's vanilla-extract theme, causing invisible/unstyled
inputs and a transparent background.

Rewrite to match the ReportRoomModal pattern:
- Overlay + OverlayBackdrop + OverlayCenter for the backdrop
- FocusTrap (clickOutsideDeactivates, escapeDeactivates via stopPropagation)
- Box as="form" with color.Surface.Container background and color.Other.Shadow
- Header variant="Surface" for the title bar
- folds Input variant="Background" for all text fields (replaces raw <input>)
- color.Critical.Main for error text
- Spinner before prop on submit button while submitting
- All spacing/radii from config.* tokens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 16:03:45 -04:00
jaredandClaude Sonnet 4.6 3eabc8f4dd fix: auto-clear dropdown unreadable in dark mode
var(--bg-surface-variant) and var(--border-surface-variant) are not
defined in Cinny's vanilla-extract theme, so the select element renders
with a transparent/white background. Also native <option> elements ignore
most inherited CSS.

Fix: use folds color tokens (color.SurfaceVariant.*) for background,
border, and text color; add colorScheme:'dark' so the browser renders
the OS popup with dark styling; apply background+color to <option>
elements as a belt-and-suspenders fallback for browsers that honour it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 15:50:05 -04:00
jaredandClaude Sonnet 4.6 ba659bc157 feat: gallery — video lightbox, auto-scroll load, month separators; fix 3 review bugs
New features:
- Video playback: lightbox renders <video controls autoPlay> for MsgType.Video;
  thumbnail tiles show a play-button badge overlay; LightboxImage renamed to
  LightboxMedia to handle both types.
- Auto-load on scroll: IntersectionObserver on a sentinel div replaces the
  manual "Load More" button. Detaches while loading or when history is exhausted.
- Month separators: image/video grid grouped by month ("June 2026", etc.) with
  a hairline divider; separator only shown when more than one month is present.

Bugs fixed by code review:
- flatIdx++: index was incremented before the !thumbMxc null-guard, causing
  tiles rendered after a skipped event to open the wrong lightbox item. Guard
  is now checked first; flatIdx only increments when a tile actually renders.
- lightboxIndex never reset on tab switch: stale index kept the lightbox open
  (or opened the wrong item) after switching tabs. handleTabChange() now calls
  setLightboxIndex(null) alongside setTab().
- Silent catch retry storm: pagination errors left canLoadMore=true, causing
  the IntersectionObserver to re-fire handleLoadMore on every render cycle
  when the sentinel was still visible. Error state now sets loadError=true,
  removes the sentinel, and shows a manual Retry button instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 15:38:53 -04:00
jaredandClaude Sonnet 4.6 face24f2f4 feat: gallery — decrypt E2EE thumbnails, lightbox, hover overlays
Root cause of padlock: all images in E2EE rooms have content.file so the
gallery skipped thumbnails for all of them. Fix:

- useDecryptedMediaUrl hook: downloads + decrypts encrypted media using
  downloadEncryptedMedia/decryptFile, creates a blob URL, revokes on
  unmount. For unencrypted media returns the HTTP URL directly.
- GalleryTile: prefers content.info.thumbnail_file (smaller encrypted
  thumb) over content.file; falls back gracefully. Shows spinner while
  decrypting, broken-image icon on error. Hover overlay shows sender
  name + relative date with a gradient.
- Lightbox: full-screen overlay with ← → keyboard/button navigation,
  filename/sender/date header, image counter. Full-res decryption done
  in LightboxImage (separate component per item so keys reset the hook).
- File list: shows sender name + file size (formatted KB/MB).
- Empty states: distinct messages for "nothing in recent events" vs
  "nothing found after loading more". "Beginning of history" shown when
  pagination exhausts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 13:14:28 -04:00
jaredandClaude Sonnet 4.6 0f4f33119b chore: remove stale agent worktree tracking files
These were left behind by previous Claude Code agent sessions and were
causing "No url found for submodule path" warnings in CI.

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:23:44 -04:00
jaredandClaude Sonnet 4.6 9232e1ec8e docs: mark all P1 features complete in TODO; update README and landing page
LOTUS_TODO.md:
- P1-1: marked UPSTREAM REMOVED (Ctrl+K already exists as full room switcher)
- P1-2 through P1-11: all marked [x] with implementation notes

README.md:
- New sections: UX & Composer, Settings (Appearance), Calls (Extended)
- Documents: media gallery, sidebar filter, DM previews, favorites, poll
  creation, voice speed, invite QR, private receipts, knock-to-join,
  syntax highlighting, night light, push-to-deafen, typing dots, char counter
- Key Custom Files table updated with 4 new entries

landing/index.html (matrix repo):
- Polls row: "display & vote" → "create, vote & display"
- Voice messages: add speed control note
- 4 new UX table rows: Media Gallery, Sidebar filter, Favorite rooms, Invite link+QR
- also-available paragraph updated with all P1 additions
- Comparison date updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 10:36:53 -04:00
jaredandClaude Sonnet 4.6 906884e434 feat: QR code in invite modal; fix CSP to allow api.qrserver.com
InviteUserPrompt: add QR code toggle button (Icons.BlockCode) in header.
When toggled, shows a 180x180 QR code image (api.qrserver.com) and the
raw invite URL below it, between the header and the search form.
inviteUrl computed once and shared between Copy Link and QR display.

Server: added https://api.qrserver.com to img-src in CSP header on
LXC 106 (/etc/nginx/sites-available/cinny) — nginx reloaded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 10:10:25 -04:00
jaredandClaude Sonnet 4.6 da35278b30 fix: media gallery thumbnails — skip auth URL, handle encrypted media
- Use useAuthentication=false for thumbnail requests: the v1 authenticated
  URL adds allow_redirect=true which Synapse rejects with 400
- Encrypted events (content.file set) show a lock+filename placeholder
  since server can't thumbnail encrypted blobs
- Unencrypted thumbnails add onError handler to hide broken images gracefully

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 01:13:18 -04:00
jaredandClaude Sonnet 4.6 e18e089043 revert: remove redundant QuickSwitcher (Ctrl+K already does this better)
The existing SearchModalRenderer (Ctrl+K) is already a polished room/DM
switcher with avatars, unread badges, fuzzy search, and keyboard nav.
Our QuickSwitcher was an inferior duplicate. Removing it entirely:
- Delete QuickSwitcher.tsx
- Remove QuickSwitcherFeature from ClientNonUIFeatures
- Remove quickSwitcherKey from settingsAtom
- Remove Keyboard Shortcuts section from General settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 01:03:50 -04:00
jaredandClaude Sonnet 4.6 977fa8aa1b fix: sidebar filter inputs full width (grow=Yes on container Box)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 01:00:33 -04:00
jaredandClaude Sonnet 4.6 986e4bb93a fix: poll multiple-choice toggle + Sentry JAVASCRIPT-REACT-N
PollCreator: replace maxSelections/options.length stale-closure pattern
with isMultiple: boolean state. max_selections computed from filledOptions
at submit time. Radio inputs replaced with styled toggle buttons that
visually highlight the active selection.

PollContent: catch getPendingEvents error (Sentry JAVASCRIPT-REACT-N).
SDK throws Cannot call getPendingEvents with pendingEventOrdering ==
chronological when sending poll vote events with m.reference relation.
Silently catch so optimistic UI update stands — vote will retry on next
sync if needed.

Fixes JAVASCRIPT-REACT-N

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 00:55:50 -04:00
jaredandClaude Sonnet 4.6 9d4679d260 feat: configurable keybindings for push-to-deafen and quick switcher
- Add deafenKey (default M) and quickSwitcherKey (default P) to settingsAtom
- Settings → Calls: Push to Deafen keybind tile using shared useKeyBind hook
- Settings → Keyboard Shortcuts: new section with Quick Room Switcher keybind
- Extract useKeyBind + keyLabel helpers to reduce duplication in Calls section
- CallControls reads deafenKey from settings (reactive, re-registers on change)
- ClientNonUIFeatures reads quickSwitcherKey from settings (same pattern)
- QuickSwitcher now toggles open/closed on repeat press (Ctrl+key again closes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 00:45:43 -04:00
jaredandClaude Sonnet 4.6 3cbc5112a7 fix: DM preview shows message body for E2EE rooms; filter inputs match members panel style
- RoomNavItem: change isEncrypted() to isDecryptionFailure() so DM
  previews show actual message body for successfully decrypted E2EE
  events instead of always showing 'Encrypted message'
- Home.tsx / Direct.tsx: upgrade filter inputs to size 400 / radii 400
  with search icon prefix to match the members list search bar style

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 00:39:39 -04:00
jaredandClaude Sonnet 4.6 ed67641dd5 fix: media gallery encrypted rooms + Ctrl+K double-menu
- MediaGallery: switch from createMessagesRequest (returns raw encrypted
  events) to room.getLiveTimeline().getEvents() which gives already-
  decrypted MatrixEvent objects. Load More uses paginateEventTimeline().
- QuickSwitcher: change hotkey from Ctrl+K to Ctrl+P to avoid conflict
  with the existing SearchModalRenderer mod+k handler

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 00:36:54 -04:00
jaredandClaude Sonnet 4.6 7cf751a3a5 fix: CI Prettier, P1-6 poll button, P1-11 stale knock state
- LOTUS_TODO.md: Prettier formatting (CI gate fix)
- P1-6: Wire PollCreator into RoomInput — poll button (Icons.OrderList)
  opens modal, renders PollCreator when pollOpen is true
- P1-11: Reset knocked + knockError on room.roomId change via useEffect;
  add missing useEffect import to RoomIntro.tsx

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 19:31:30 -04:00
jaredandClaude Sonnet 4.6 f07ff63ac1 docs: mark P5-5, P5-24, P5-25, #108 completed in LOTUS_TODO
Night Light filter, push-to-deafen hotkey, message length counter,
and typing indicator orange dots all shipped June 2026.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 18:47:36 -04:00
jaredandClaude Sonnet 4.6 a552e49ece fix: prefer-arrow-callback ESLint rule and Prettier formatting
Convert TypingIndicator named function expression to arrow function to
satisfy prefer-arrow-callback lint rule. Auto-format RoomInput.tsx to
resolve Prettier check failures in CI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 18:43:02 -04:00
jaredandClaude Sonnet 4.6 dedbd54199 feat: typing indicator orange dots, push-to-deafen hotkey, night light filter, message length counter
- #108: TypingIndicator reads lotusTerminal setting; applies var(--lt-accent-orange)
  to container so dots inherit via backgroundColor:currentColor
- #100: CallControls registers KeyM as push-to-deafen (e.code, e.repeat guard,
  ownerDocument.body iframe-safe editable check, [callEmbed] dep array)
- P5-5: nightLightEnabled/nightLightOpacity settings; position:fixed rgba(255,140,0)
  overlay inside JotaiProvider; Night Light tile + intensity slider (5–80%) in
  Settings → Appearance
- #101: RoomInput charCount state via Slate onChange + toPlainText; resets on
  room switch; displayed before send button when count > 0

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 14:47:48 -04:00
jaredandClaude Sonnet 4.6 742132661f docs: remove resolved bug fixes section from LOTUS_TODO.md — it's a TODO list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 14:19:21 -04:00
jaredandClaude Sonnet 4.6 8c4a863c84 docs: LOTUS_TODO.md — mark all P0 tasks complete, add June 2026 bug fix section
- P0-1 through P0-12: all marked [x] with implementation notes
- P0-4, P0-7: remain [BLOCKED] (MSC3892, MSC3266 not on server)
- P0-9, P0-10: remain [UPSTREAM — REMOVED]
- New INFRASTRUCTURE/BUG FIXES section documents: GIF CSP fix, unhandled
  rejection pattern, Copy Link relocation, MaxListeners bump, TDS hex
  violations fixed, CORS for well-known/matrix/support, relations API
  v1 vs v3 path fix
- Architecture facts table updated with 6 new confirmed findings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 14:18:09 -04:00
jaredandClaude Sonnet 4.6 2b115390f2 fix: edit history original shows pre-edit content
mEvent.getContent() returns post-edit content because matrix-js-sdk applies
the latest replace event in-memory. Reading mEvent.event.content gives the
raw server content (the true original before any edits). Edit entries from
the relations API correctly use m.new_content per Matrix spec.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 10:51:21 -04:00
jaredandClaude Sonnet 4.6 7b01cfebf5 fix: GIF CSP + edit history HTML rendering + unhandled rejection cleanup
- nginx (LXC 106, live): added https://*.giphy.com to connect-src CSP —
  browser was blocking fetch() to media2.giphy.com CDN with CSP violation
- EditHistoryModal: render formatted_body as sanitized HTML (via
  html-react-parser + sanitizeCustomHtml) with linkification for plain
  text, matching how messages render in the timeline
- useAsyncCallback + ThumbnailContent + ImageContent + VideoContent +
  ClientConfigLoader: use .catch(() => undefined) instead of void to
  silence unhandled promise rejections from fire-and-forget useEffect
  calls — errors already captured in AsyncState.Error for UI display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 10:34:46 -04:00
jaredandClaude Sonnet 4.6 7bafefa5e7 fix: suppress unhandled promise rejections from fire-and-forget useEffect loads
useAsync re-throws errors after storing them in state — correct for awaited
callers but causes unhandled rejections when load() is called without .catch()
in useEffects. The error is already captured in AsyncState.Error so the
re-throw provides no additional value in these fire-and-forget patterns.

Fixes JAVASCRIPT-REACT-M (Sentry: Media download failed: 401 Unauthorized)

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 21:30:27 -04:00
jaredandClaude Sonnet 4.6 3db87db03f chore: prettier formatting for P0 feature files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 20:55:06 -04:00
jaredandClaude Sonnet 4.6 3a72b7c1c5 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
jaredandClaude Sonnet 4.6 134ebb231d 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
jaredandClaude Sonnet 4.6 572bc6a4c0 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
jaredandClaude Sonnet 4.6 4223ac85d3 docs: final audit pass — all 17 remaining audits resolved
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
jaredandClaude Sonnet 4.6 3a58d4f94d docs: comprehensive audit pass — all 5 agent findings integrated into TODO
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
jaredandClaude Sonnet 4.6 c1f7e4c21d docs: add blocked features section, upstream improvements, TDS design law
- 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
jaredandClaude Sonnet 4.6 74cde50df7 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
jaredandClaude Sonnet 4.6 faddde65fa docs: expand LOTUS_TODO.md with gamer/aesthetic feature wave and 30+ new items
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
jaredandClaude Sonnet 4.6 79c3b664ba docs: remove upstream Cinny features from README (stickers, pins, who-reacted)
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
jaredandClaude Sonnet 4.6 5435741d42 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
jaredandClaude Sonnet 4.6 8e3acf8d00 fix: clear status_msg when leaving DND presence state
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
jaredandClaude Sonnet 4.6 bdac3b2171 style: fix Prettier formatting in SettingsTab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:59:46 -04:00
jaredandClaude Sonnet 4.6 51d45088c3 fix: restore avatar rendering and split presence dot into separate button
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
jaredandClaude Sonnet 4.6 b243a18e01 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
jaredandClaude Sonnet 4.6 41ff5276ca docs: update README for wave-3 features and presence selector
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:30:10 -04:00
jaredandClaude Sonnet 4.6 bd8e116cf3 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
jaredandClaude Sonnet 4.6 582839fddb feat: document title unread count, draft persistence, search date range
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
jaredandClaude Sonnet 4.6 10f0093f8e feat(D3): apply terminal theme to voice message recorder
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
jaredandClaude Sonnet 4.6 48f9221f1c fix: resolve ESLint no-shadow errors in CallEmbedProvider
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
jaredandClaude Sonnet 4.6 bc63714a07 fix/polish: wave 1+2 improvements across six features
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
jaredandClaude Sonnet 4.6 2255795cae fix: rank from: autocomplete suggestions by homeserver + shared rooms
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
jaredandClaude Sonnet 4.6 64029d9e8e fix: search bar size and from: autocomplete actually working
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
jaredandClaude Sonnet 4.6 6056aa9632 fix: search bar layout, autocomplete FocusTrap, and from: regex
Three bugs introduced in af3155e1:

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
jaredandClaude Sonnet 4.6 8c0383ab7f fix: remove redundant E2EE banner from members drawer
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
jaredandClaude Sonnet 4.6 af3155e169 feat: sender filter for message search with from:@user autocomplete
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
jaredandClaude Sonnet 4.6 e5ae77a99d feat: add encrypted room cache panel with load/load-more buttons
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
jaredandClaude Sonnet 4.6 b13297ce3b 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
jaredandClaude Sonnet 4.6 5957b1cf55 style: fix Prettier formatting in usePresenceUpdater
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 19:40:15 -04:00