Previously every visible Message subscribed to settingsAtom via useSetting,
creating O(80) active atom subscriptions. Now RoomTimeline reads it once
and passes it down as a prop, reducing subscriptions to 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
C-1 complete sweep across all components and features:
- Call controls: mic mute/unmute, deafen/undeafen, video, screenshare, chat
- RoomInput: dismiss reply, attach file, sticker, emoji, GIF, location, toolbar
- Media viewers: close in image/pdf/text viewers and editors
- Settings dialogs: close buttons in all room/space/common settings panels
- Lobby: back, toggle member list, scroll to top, pack add/remove
- Auth: server picker, UIA flow cancel
- Upload cards: cancel uploads
- URL preview: prev/next buttons
- Members drawer: close + scroll to top
- RoomViewHeader: back, start call, toggle member list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Perf-3: Replace raw roomToUnreadAtom subscription in Home, Direct, Space with
selectAtom-derived Set<string> — components now only re-render when rooms
gain/lose unread presence, not on every notification count update
Perf-5: RoomTimeline eventRenderer now uses binary search on precomputed
timelineSegments instead of O(N×T) linear scan per visible message
A11y L-1: Add as=h2 semantic heading to Home, Direct, Inbox, Space page nav
titles so screen readers announce page sections correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BUG-18: clearTimeout cleanup in focusItem useLayoutEffect prevents leaked timers
BUG-24: Room timeline listener catches first poll vote before Relations object exists
BUG-25: Use timelineRef.current in handleOpenEvent to prevent stale index on rapid navigation
Perf-6: React.lazy + Suspense for GifPicker and EmojiBoard (initial bundle -114 kB)
Perf-7: React.memo on RoomNavItem to prevent re-renders on unrelated state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When enabling PTT mode, save current mic state before muting.
When disabling PTT, restore saved state instead of unconditionally unmuting.
Prevents surprising unmute if user had manually muted before switching to PTT.
- Message.tsx: show delivery status (sending/sent/failed) on own messages when
no read receipts yet; hidden once server confirms (status null); TDS-styled
- GifPicker.tsx: move terminal CSS from runtime <style> tag into lotus-terminal.css.ts
eliminating flash of unstyled content (M-6)
- lotus-terminal.css.ts: add [data-gif-terminal] selector rules for GIF picker
PollContent now:
- Reads existing m.poll.response / org.matrix.msc3381.poll.response events
from the room timeline on mount to restore vote state across refreshes
- Counts votes per answer (per-sender latest-wins deduplication)
- Shows percentage bars and vote totals in real time
- Subscribes to RelationsEvent.Add/Remove/Redaction so counts update live
when other users vote without requiring a page reload
- Optimistic local update keeps the UI snappy while the send request flies
- CallEmbed: fix memory leak — mx event listeners were never removed
because dispose() called .bind(this) again, creating new function
objects. Now uses arrow class fields so start()/dispose() share the
exact same reference.
- callPreferences: toggleVideo is a no-op when cameraOnJoin=false,
preventing internal state drift from the returned value.
- CallControls: PTT key guard now blocks on SELECT elements and walks
the DOM for inherited contentEditable to prevent key interception
inside dropdowns and custom editors.
- RoomInput: GIF fetch validates Giphy CDN domain allow-list,
HTTP Content-Type header, and enforces 20 MB size cap.
These are superseded by IncomingCallListener in CallEmbedProvider (merged from v4.12.1). IncomingCallNotification was already removed from Router.tsx in a previous commit.
- ForwardMessageDialog: use sendEvent instead of sendMessage to preserve
the original event type (stickers, polls, etc.)
- PollContent: use m.selections for stable m.poll.response (per spec),
was incorrectly using m.responses
- Router.tsx: remove IncomingCallNotification (CallEmbedProvider.IncomingCallListener now handles all calls)
- CallEmbedProvider: restore touch drag (handlePipTouchStart), grip dots on resize handles, fix normaliseToTopLeft width/height
- FallbackContent/MsgTypeRenderers: add originalBody prop to show struck-through original text on deleted messages
- RoomTimeline: cache text message bodies so they can be shown after redaction
- Poll voting: PollContent sends m.poll.response on answer click
- Location: MLocation shows OSM map embed + share-location button in toolbar
- Image captions: caption field on media uploads sets message body
- Message forwarding: ForwardMessageDialog with searchable room picker
- Also includes ring timeout fix and earlier session patches
Bug fixes:
- IncomingCallNotification: track ring setTimeout ID and clear it on
cleanup/dismiss — prevents orphaned callbacks after unmount
- RoomTimeline: allow redacted m.room.message, m.room.encrypted and
m.sticker events past the early-return filter so they hit the
existing RedactedContent renderer showing the trash-icon placeholder
New features:
- PollContent component: read-only display of m.poll.start and
org.matrix.msc3381.poll.start events (both stable Matrix 1.7 and
MSC3381 unstable content keys); renders poll question + answer
options inside the standard Message bubble; registered both as
top-level event renderers and inside EncryptedContent callback
so encrypted polls also render after decryption
- Deleted message placeholder: m.room.message and m.room.encrypted
redacted events now show the existing MessageDeletedContent
component (trash icon + italic notice) instead of disappearing
entirely — matches Element, FluffyChat, Commet, Nheko behaviour
When another user starts a call in a DM room, show a fixed-position
notification with caller avatar, name, and Answer/Decline buttons.
A Web Audio API double-pulse ring tone plays until answered, declined,
or the 30-second auto-dismiss fires.
- useIncomingDmCall hook: listens to MatrixRTC SessionStarted/SessionEnded,
filters DM rooms (encrypted, ≤2 members), auto-dismisses after 30s,
stops if caller leaves or user joins another call
- IncomingCallNotification component: ring tone, caller info, themed UI
for LotusGuild Terminal TDS (navy bg, orange border, neon-green Answer)
and standard Cinny dark/light (CSS vars, folds Button Success/Critical)
- Router.tsx: mount IncomingCallNotification inside CallEmbedProvider
Add 4 corner resize handles to the PiP window (SE/SW/NE/NW).
Each handle shows a 3-dot grip indicator and sets the appropriate
resize cursor. Resize handles sit above the drag overlay (zIndex 2)
and stop propagation so they do not trigger drag-to-move.
On resize start the element is normalised to top/left positioning
so math is consistent regardless of whether bottom/right was active.
Minimum size 200x112px. Viewport clamped.
EC 0.19.3 changed the toolbar layout. The old previousElementSibling
traversal from the leave button pointed at wrong elements:
- settingsButton was finding the raise-hand button
- reactionsButton was finding the screenshare button
Fix: use stable selectors instead:
- settingsButton: data-testid=settings-bottom-center (new in EC 0.19.3)
- reactionsButton: [class*=raiseHand] (CSS module class, consistent in 0.19.x)
Drag the PiP window anywhere on screen to move it out of the way.
Click (without dragging) still navigates back to the call room.
5px movement threshold distinguishes drag from click.
Mouse and touch both supported. Cursor shows grab/grabbing cues.
- Camera no longer starts enabled when user disables it in prescreen
- When PTT mode is enabled, call starts muted so PTT works immediately
without requiring a manual mute first
- CallControlState also updated to match the forced-off audio for PTT
EC 0.16.x ignores io.element.device_mute for initial state at startup,
so audio= and video= URL params are the only reliable way to set the
initial device state before the call begins.