- fileEntries: a single unreadable file/dir in a dropped folder no longer aborts
the whole traversal (try/catch per entry, skip failures) — was discarding ALL
dropped files (incl. the flat-file path) + an unhandled rejection; also add
.catch in both useFileDrop consumers.
- RoomInput: mirror a localStorage-restored draft into the draft atom so the
P5-57 indicator reflects a persisted draft after a page reload, not only on
same-session room re-entry.
- useTauriThumbbar: swallow toggleMicrophone()/hangup() rejections (parity with
SMTC) — avoids an unhandled rejection when clicked mid-teardown.
- App/DesktopChrome: keep wrapper element types stable across the chrome toggle
(display:contents when off) so flipping it no longer remounts RouterProvider.
- settings: normalizeComposerToolbarOrder also appends missing keys from the
canonical key set (safety net if a new button is absent from the default order).
Gates: tsc/eslint/prettier clean, build OK, 559 tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Web half of the desktop feature wave. A shared bridge (`hooks/useTauri.ts`:
invokeTauri/isTauri/useTauriEvent) backs per-feature hooks that no-op in the
browser and drive the native Tauri commands (compiled in cinny-desktop):
- P5-46 useTauriCallPower — hold system awake while a call is active.
- P5-36 useTauriJumpList — Windows jump list of recent rooms → matrix: deep links.
- P5-44 useTauriThumbbar — taskbar Mute/Deafen/End; events toggle mic/sound/hangup.
- P5-43 useTauriSmtc — SMTC call state + button events.
- P5-49 useTauriNetwork — react to native network-change → mx.retryImmediately().
- P5-47 window chrome — opt-in `customWindowChromeAtom` + TDS `TitleBar`; DesktopChrome
wrapper in App.tsx (zero layout impact when off) + a desktop-only settings toggle.
- P5-55 composer toolbar drag-reorder (settings order[] + pragmatic-drag-and-drop).
- P5-57 DraftIndicator — subtle "draft saved" cue in the composer.
Client-scoped hooks mount via TauriDesktopFeatures in ClientNonUIFeatures; window
chrome mounts at App level. Gates: tsc/eslint/prettier clean, build OK, 556 tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Element Call is now consumed as our self-built fork
(@lotusguild/element-call-embedded); wire up its previously-dormant
capabilities and document the fork as live.
Soundboard (P5-15): a call-bar button plays user-uploaded audio clips into the
call as a real published track (io.lotus.inject_audio) plus local playback.
Clips are uploadable like emoji/sticker packs, stored in io.lotus.soundboard
account data (synced across devices). Gated by a Settings toggle + volume.
Quality controls (P5-31): per-user mic/screenshare bitrate + screenshare
framerate (Settings -> Calls), applied via io.lotus.set_quality clamped to any
room cap. Room admins set caps and hard call-permissions (allow_screenshare /
allow_camera) in Room Settings -> Voice; the call bar hides blocked buttons.
- New: CallSoundboard, useSoundboard, soundboardClips; RoomQuality,
useCallQuality, callQuality (+ unit tests).
- Optimistic-write RoomQuality admin UI (no stale-state clobber).
- Docs: mark EC fork live across README/FEATURES/TODO/BUGS/TESTING; add D2
manual-test steps.
Numeric quality caps are client-cooperative; screenshare/camera permissions are
hard-enforced server-side (see LotusGuild/matrix voice-limit-guard).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Toward MSC3861/MSC2965 next-gen-auth login (P4-6), client-only.
- cs-api.ts: type the stable `m.authentication` well-known key + getOidcIssuer()
(stable preferred over the unstable msc2965 key; {} for non-OIDC servers).
- useParsedLoginFlows.ts: getOidcCompatibilityFlag() (MSC3824 oauth_aware_preferred
/ delegated_oidc_compatibility) as a secondary OIDC hint.
- New pages/auth/oidc/oidcConfig.ts: dynamic-registration client metadata + the
non-hash callback URL (redirect_uris can't contain a fragment).
- paths.ts: OIDC_CALLBACK_PATH.
- 8 unit tests for the pure helpers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switch to @lotusguild/element-call-embedded@0.20.1-lotus.1 (our self-built
fork) and turn on the source-level features it adds:
- #1 denoise CUTOVER: in-source ML denoise (lotusDenoiseSource=1) replaces
the build-time getUserMedia shim — removed the shim injection from
vite.config.js (denoise/ assets still shipped; the processor loads them).
Survives reconnects (fixes A7).
- #2 call-state: CallEmbed consumes io.lotus.call_state; useCallSpeakers /
useRemoteAllMuted prefer it over scraping EC's DOM (DOM fallback kept;
empty payloads ignored).
- #4 focus: CallControl.focusCameraParticipant sends io.lotus.focus_participant
(works during screenshare), replacing the DOM tile-click hack.
- #5 theming: lotusTransparent=1 (native transparent background).
- #6 decorations: LotusDecorationPusher sends each member's decoration URL
via io.lotus.decorations -> rendered on in-call tiles.
#3 soundboard / #7 quality ship dormant (EC-ready; no host UI sends them yet).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three issues from live testing:
- A1: the 'classic' ringtone (call.ogg, mastered near full scale) was much
louder than the synthesized styles. Attenuate it (CLASSIC_GAIN 0.45) so all
ringtones sit at a comparable level.
- A3/A4: the caller had no indication when a DM/group callee declined — their
UI kept "ringing" until the notification lifetime expired. IncomingCallListener
now listens for RTCDecline events for a call we're hosting in the room and
toasts the caller ("<name> declined your call").
- G1: the PiP "All muted" badge fired when any single remote participant muted.
useRemoteAllMuted now returns true only when there is >=1 remote and every
remote participant is muted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
useAfkAutoMute opened its own getUserMedia capture for the whole call and only
stopped it on unmount, so the OS recording indicator stayed lit even when the
user was muted. The capture is now gated on the reactive mic-on state: it runs
only while unmuted (there's nothing to auto-mute when already muted), so muting
tears down the stream and clears the indicator, and unmuting re-acquires it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Five complete vanilla-extract themes registered in useTheme (useThemes +
useThemeNames), each spreading darkThemeData so Success/Warning/Critical keep
their semantic colors and only Background/Surface/Primary/Secondary are
recolored. A code-review pass computed WCAG contrast for every theme; all body
and accent pairs clear AA except Midnight's Primary.OnMain which was 4.49:1 —
fixed by changing OnMain #0d1320 -> #000000 (5.07:1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
useCallSpeakers rebuilt the speaker Set from only the mutated tiles in each
batch (so a still-speaking participant whose tile didn't mutate was dropped),
and observed a static querySelectorAll NodeList (so tiles for participants who
joined mid-call were never watched). Rewritten to mirror useRemoteAllMuted in
the same file: a single body-level MutationObserver (subtree+childList+attrs)
re-scans ALL [data-video-fit] tiles on each relevant mutation. The speaking
criterion (::before background-image !== 'none') and the id (aria-label +
isUserId) are unchanged, so behavior on real EC DOM is a strict superset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- N113: mutations compute from a local ref kept in sync with server echoes, and
writes serialize through a promise queue, so rapid add/remove no longer reads
a stale baseline and clobbers a prior write.
- N114: ReminderMonitor shows each toast once (firedRef) but retries the
account-data removal on later ticks if it fails (removingRef released on
error) — a failed removal no longer permanently swallows the reminder.
- N115: the 30s poll interval reads reminders/mDirects via refs and drops them
from the effect deps, so it's created once instead of resetting its countdown
on every reminder sync (which could indefinitely defer a near-due reminder).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CallEmbed: 25s load watchdog that fails fast on iframe error / preparing-error /
timeout instead of hanging on a permanent spinner; additive onLoadError API,
cleared on ready/capabilities/joined.
- CallView: user-visible "call failed to load" overlay with Retry/Leave (folds +
tokens) via a new useCallLoadError hook.
- CallMemberCard: wrap the participant avatar in AvatarDecoration so decorations
render in the call roster (the tile rendered UserAvatar bare while member lists
already wrapped it).
Addresses LOTUS_BUGS item 3 (avatar decorations in calls) and EC iframe failure monitoring.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- uploadContent: bounded retry (max 3) reusing rateLimitedActions' capped
exponential backoff; retries only transient failures (network/429/5xx), never 4xx.
- Robust MatrixError construction from UploadResponse / unknown error shapes.
- addRoomIdToMDirect/removeRoomIdFromMDirect: drop `as any`, use typed
EventType.Direct + MDirectContent.
- usePresenceUpdater: keep fetch({keepalive}) for the unload offline-presence
update (sendBeacon can't set the auth header) and log redacted warnings instead
of silently swallowing presence errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Avatar decorations: useAvatarDecoration cached ALL profile-field fetch
failures as "no decoration" permanently for the session. The member list
and timeline mount many avatars at once, so one rate-limited (429) burst
would wipe everyone's decoration until a full reload. Now only a genuine
404 (field unset) is cached; transient errors retry on the next mount.
Saved Messages panel — full redesign to match the canonical MembersDrawer:
- co-located BookmarksPanel.css.ts: toRem(266) + max-width:750px full-screen
media query, replacing the old position:absolute/zIndex:100 mobile "modal"
that had no backdrop or escape
- variant="Background" header; room avatars on each item (was a generic hash)
- priority tokens replace all raw opacity hacks; 3px borderLeft accent removed
- Escape-to-close; multi-line preview is now a proper folds Button (N38)
Media Gallery (N12): moved fixed positioning + width into MediaGallery.css.ts
using toRem(320) + a full-screen media query; border/header use config tokens;
added Escape-to-close on the panel (previously only the lightbox handled it).
Presence (SettingsTab / useUserPresence):
- N16: wrap presence-dot trigger in TooltipProvider; replace undefined
--bg-surface with color.Background.Container
- N17: add escapeDeactivates + isKeyForward/isKeyBackward to the FocusTrap
- N19: align reader labels (usePresenceLabel) to the setter vocabulary
(Online/Idle/Offline) so a chosen status matches the tooltip others see
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Creates useNearViewport hook (IntersectionObserver, 200px rootMargin,
one-shot disconnect after first trigger). ImageContent and VideoContent
now gate loadSrc() on nearViewport — when autoPlay is enabled, encrypted
media is not decrypted until the element is within 200px of the visible
area, reducing initial page load cost on long timelines.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bug #10: use `100dvh` on <html> so layout shrinks when mobile virtual keyboard
appears (prevents composer from being pushed off-screen)
- Bug #7: add `minWidth/minHeight: 44px` to all 8 composer toolbar IconButtons on
mobile via mobileOrTablet() check (WCAG 2.1 AA touch target requirement)
- Bug #8: add `@media (max-width: 750px) { width: 100% }` to PageNav recipe
variants so the nav panel fills full width on mobile instead of overflowing
with its fixed desktop width
- Bug #9: introduce `useModalStyle(maxWidth)` hook — returns fullscreen styles on
mobile (no border-radius, no max-width cap, height 100%) and desktop box styles
otherwise; applied to LeaveRoomPrompt, LeaveSpacePrompt, ReportRoomModal,
ReportUserModal
- Bug #11: mark as FALSE POSITIVE in LOTUS_BUGS.md — `useState(() => atom(...))`
is the correct Jotai pattern for stable local atom references
- Scheduled Messages persistence: mark as FIXED — already uses atomWithStorage +
createJSONStorage with error-safe JSON parsing
- UrlPreviewCard TDS colors: mark as BRAND EXCEPTION — SVG logo fills and site
badge backgrounds are official third-party brand colors; cannot convert without
inventing new CSS variables (violates TDS rule 3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implement a flexible, multi-model noise suppression pipeline for Element Call/LiveKit integration:
- ML Engines: Added support for RNNoise, Speex, DTLN, and DeepFilterNet 3 models.
- Pipeline Architecture: Implemented modular audio processing in lotus-denoise.js, supporting 'Series Suppression' (running browser-native NSNet2 before ML) and a hardware-style Noise Gate.
- UI & UX Enhancements:
- Settings UI: Added model comparison chart with CPU/Quality metadata.
- Tuning: Added Live Microphone Meter for calibrating Noise Gate thresholds.
- Reporting: Added LotusToast system to alert users when ML suppression fails or falls back to raw input.
- Robustness & Quality:
- Capture Fidelity: Removed forced 48kHz capture constraints to allow native-rate capture (solving static issues with high-end audio interfaces).
- Performance: Added WASM SIMD detection with transparent fallback.
- Capability Detection: Added browser feature detection to disable unsupported ML modes.
- Build Integration: Updated Vite config to self-host all model WASM/tflite assets in /denoise/ directory.
ML noise suppression produced loud static on real calls. RNNoise requires
mono 48kHz float input; feeding it stereo or wrong-rate data is the classic
cause of that static. Harden the shim:
- request mono (channelCount:1) + 48kHz capture
- run a 48kHz AudioContext and BAIL to the raw mic if the browser won't
give a true 48kHz context (wrong-rate data -> static)
- force the worklet node to explicit mono in/out
- use the non-SIMD rnnoise.wasm (SIMD build artifacts on some GPUs)
- share one AudioContext across captures
Also fix the two CI-blocking eslint errors (unused vars in UrlPreviewCard
and useLocalMessageSearch) and apply repo-wide prettier formatting so
check:eslint and check:prettier pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the boolean call noise-suppression setting with a 3-way control
(Off / Browser-native / ML beta) in Settings -> General -> Calls.
- Off: noiseSuppression=false to Element Call
- Browser-native: EC's built-in WebRTC suppressor (prior default)
- ML (beta): on-device RNNoise (@sapphi-red/web-noise-suppressor)
Element Call captures the mic inside its iframe and publishes to LiveKit,
so the host can't reach that track; LiveKit's Krisp filter is Cloud-only
(we self-host the SFU) and EC's own RNNoise PR #3892 is unmerged. The ML
tier instead injects a same-origin pre-init shim into the vendored EC
index.html (build/lotus-denoise.js, wired by the lotusDenoise vite plugin)
that patches getUserMedia and routes the captured mic through an RNNoise
AudioWorklet before LiveKit sees it -- the same post-capture pipeline as
#3892, with no EC fork/AGPL/rebase burden. Falls back to the raw mic if
setup fails; keeps echoCancellation/AGC on the raw capture.
- settings.ts: callNoiseSuppression -> 'off'|'browser'|'ml' + legacy
boolean migration (true->browser, false->off)
- CallEmbed/useCallEmbed: tier maps to noiseSuppression param and appends
lotusDenoise=ml (native suppressor off in ML mode)
- vite.config.js: copy RNNoise worklet/wasm + shim into the EC bundle and
inject the shim <script> before EC's module entry
- docs: LOTUS_FEATURES.md, LOTUS_TODO.md (P5-30 done)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
256×256 APNG overlays from avatardecoration.com (open CORS CDN).
Stored in the user's Matrix profile as io.lotus.avatar_decoration via
MSC4133 so all Lotus Chat users see each other's decorations.
- avatarDecorations.ts: curated catalog of 110 original-IP decorations
across 9 categories (Gaming, Cyber, Space, Fantasy, Elements,
Japanese, Nature, Spooky, Cozy)
- useAvatarDecoration: per-user profile fetch with module-level cache
and in-flight deduplication so concurrent renders for the same userId
share one HTTP request
- AvatarDecoration: position:relative wrapper that overlays the APNG
8px beyond the avatar on all sides; renders nothing when no decoration
is set (zero cost for undecorated users)
- ProfileDecoration: scrollable grid picker in Settings → Profile,
grouped by category with live preview; Save button appears only when
the selection differs from what's saved
- Applied at all five avatar display sites: message timeline, members
drawer, knock list, @mention autocomplete, notifications inbox
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- usePresenceUpdater: replace stale closure with readStatus() called at
invocation time so changing custom status in Profile Settings is never
silently overwritten by subsequent activity events
- CallEmbedProvider: fix m.space.parent state-key lookup by switching
getStateEvent → getStateEvents (plural); space channel voice rooms no
longer trigger the incoming-call ring/animation
- Add useUserNotes hook (io.lotus.user_notes account data, reactive via
useAccountDataCallback, 500-char limit, cross-device sync)
- UserRoomProfile: add UserPrivateNotes textarea with 800ms debounced
auto-save, saving indicator, char counter when <100 chars remain;
shown only when viewing another user's profile
- LOTUS_FEATURES.md: add Private Notes section, Status Revert fix note,
animation improvements subsection, Seasonal Themes section
- LOTUS_BUGS.md: mark presence revert + voice ringing bugs as resolved
- README.md + landing/index.html: document all new June 2026 features
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends useTauriNotificationBadge to also invoke set_tray_unread (mirror the
unread-mention state onto the tray icon, visible when minimized) and flash_window
(flash the taskbar button when new mentions arrive while the window is unfocused).
No-op outside Tauri.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds useDeepLinkNavigate (mounted in ClientNonUIFeatures): listens for the
'lotus-deeplink' DOM CustomEvent that the Tauri shell dispatches when a
matrix:/matrix.to link opens the app, converts matrix: URIs to matrix.to, and
navigates via the same path as useMentionClickHandler (reusing matrix-to.ts
parsers + useRoomNavigate). No-op outside Tauri.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sounds (P5-16): browsers block the Web Audio context until a user gesture
starts it, so join/leave sounds — which fire later with no gesture — were
silent. unlockCallSounds() now primes/resumes the shared AudioContext inside
the Join click (centralized in useCallStart so every join path is covered),
making the per-client sounds reliably audible to everyone in the call.
Voice limit (P5-10): the limit is now a hard, cross-client server-side cap
enforced by the voice-limit-guard sidecar (matrix repo) that fronts
lk-jwt-service and refuses LiveKit tokens when a room is full. Updated
LOTUS_FEATURES.md / README.md / LOTUS_TODO.md to reflect that the client
'Channel Full' check is UX only and the server is authoritative.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P5-10 Voice Channel User Limit:
- New StateEvent.LotusVoiceLimit (io.lotus.voice_limit) with { max_users }
- RoomVoiceLimit admin control in Room Settings > General > Voice
(power-level gated via permissions.stateEvent)
- CallPrescreen reads the limit reactively and disables Join with a
'Channel Full (N/N)' message at capacity; existing members can rejoin
P5-16 Custom Join/Leave Sound Effects:
- useCallJoinLeaveSounds hook wired into CallUtils; detects participant
join/leave via MatrixRTCSession membership changes (sender|deviceId),
filters out self, only fires while joined
- Sounds synthesized in-browser with Web Audio (callSounds.ts) - no
assets bundled; styles Off/Chime/Soft/Retro
- 'Join & Leave Sounds' selector in Settings > Calls (previews on change)
Docs: LOTUS_FEATURES.md, README.md, LOTUS_TODO.md (P5-10/P5-16 marked done)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P4-3 — Knock-to-join Notifications for Admins:
- usePendingKnocks() hook reactively tracks knocking members via
RoomMemberEvent.Membership; returns empty array if user lacks invite power
- Members icon in RoomViewHeader shows a Warning badge with the knock count
when there are pending requests; badge updates in real time without
needing to open the drawer; aria-label updated to describe pending count
P5-11 — AFK Auto-Mute in Voice:
- useAfkAutoMute() hook opens a monitoring-only getUserMedia stream,
connects it to an AnalyserNode, and polls RMS every 500ms
- If mic is on and RMS stays below threshold for the configured timeout,
calls callEmbed.control.setMicrophone(false) and shows an in-app toast
- Hook is called inside CallControls so monitoring is only active during calls
- Settings: afkAutoMute toggle + afkTimeoutMinutes select (1/5/10/20/30 min,
default 10) added to Settings → Calls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Status message: Synapse clears status_msg when a user goes offline/reconnects.
Fix by caching to localStorage and re-sending on setOnline(). The sync
effect no longer overwrites the local value with an empty server event.
Timezone: PUT /profile/{userId}/m.tz is MSC1769 (unstable) and not
supported by standard Synapse — save/load silently fails. Fix by using
Matrix account data (im.lotus.timezone) instead, which is fully
supported. Own profile falls back to account data; other users still
try the m.tz profile endpoint (for federated servers that support it).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Presence: subscribe on MatrixClient (mx) instead of individual User objects.
User EventEmitters have a 10-listener default; the same user appears in many
mounted components simultaneously (member list, avatars, presence rings) and
was accumulating 11+ listeners per event, causing MaxListenersExceededWarning
on User.presence, User.currentlyActive, and User.lastPresenceTs.
Fonts: download JetBrains Mono and Fira Code latin subsets to public/fonts/
and replace Google Fonts CDN links (two external origins) with a local CSS
file. Windows WebView2 tracking prevention was blocking googleapis.com and
gstatic.com, logging 18+ "blocked" warnings per session. VT323 remains on
Google Fonts as it's decorative and not a default option.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Tauri command (set_badge_count) that draws a red circle badge
with a highlight count onto the Windows taskbar button overlay icon,
matching Discord's behavior. Badge shows @mention/highlight count only
(not total messages), clears to zero when all highlights are read.
Frontend: useTauriNotificationBadge hook reads roomToUnreadAtom and
calls set_badge_count via window.__TAURI_INTERNALS__.invoke whenever
the unread map changes. No-ops silently in the browser (non-Tauri).
Mounted as TauriEffects inside JotaiProvider in App.tsx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove status_msg from setOnline/setUnavailable so server preserves
any custom status message the user has set
- Fix manifest icons array paths (./public/android/ -> ./res/android/)
Junior had it backwards: shortcut icon was correct, main icons were wrong
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- useFileDrop: reset drag overlay when mouse leaves browser window
(relatedTarget === null signals viewport exit, counter was getting stuck)
- useDeviceVerificationStatus: add member count to useMemo deps so new
room members' devices get checked, not just initial joined members
- index.css: define --bg-surface-variant used by VoiceMessageRecorder,
MessageSearch, SearchFilters, UserRoomProfile (was falling back to transparent)
- syntaxHighlight: fix Python inline comments — # after space/tab was
treated as plain text; only start-of-line was recognised
- usePresenceUpdater: replace internal baseUrl cast with mx.getHomeserverUrl()
- useLocalMessageSearch: scan all linked timelines via getUnfilteredTimelineSet()
not just the live window, so scrolled-back history is included in E2EE search
- RoomViewHeader: show search button in encrypted rooms — local search is
implemented and handles them; the guard was a holdover from before it existed
- recent-emoji: return emojis in recency order (array is already unshifted on
use) instead of sorting by total usage count
Skipped: media gallery memory leak (needs virtualization refactor),
bookmark race condition (needs queue/lock), Night Light portal coverage
(position:fixed already covers full viewport — not a real bug).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>