- Remove revert-to-grid logic that was overriding EC's natural screenshare
spotlight, causing fullscreen to show user avatars instead of the screen
- Add fullscreen button to call controls (visible when screensharing) that
requests fullscreen on the call embed container
- Add FullscreenButton component with enter/exit SVG icons to Controls.tsx
- PIP mode: sync setPipMode to CallControl; auto-enable spotlight when
screenshare is active in pip so the screenshare fills the window
- Make useCallControlState accept undefined control for safe use in
CallEmbedProvider
- Add package-lock.json to .gitignore (generated by local npm install)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FocusTrap monitors focusin events and can redirect focus into the menu
container (blurring the editor) before individual MenuItem onMouseDown
handlers fire. Adding preventDefault at the container level ensures no
click anywhere inside the menu can steal focus from the editor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add onMouseDown preventDefault to all autocomplete suggestion MenuItems
so clicking a suggestion keeps the editor focused. Without this, the
mousedown event blurs the editor before onClick fires, causing Slate's
ReactEditor.toDOMNode to fail with "Cannot resolve a DOM node from Slate
node: {"text":""}" when Transforms.collapse tries to sync the selection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When selecting an autocomplete suggestion (user/room mention, emoticon,
command), Slate's replaceWithElement inserts a void inline node into the
model but React hasn't flushed the DOM update yet. Calling
Transforms.move + insertText immediately after causes ReactEditor.toDOMNode
to fail with "Cannot resolve a DOM node from slate node: {\"text\":\" \"}".
Fix: wrap moveCursor body in setTimeout(fn, 0) so React can flush the void
element's DOM node before Slate attempts to resolve the cursor position.
Also call ReactEditor.focus to restore editor focus after the autocomplete
menu item click blurs the editor.
Fixes all callers: UserMentionAutocomplete, RoomMentionAutocomplete,
EmoticonAutocomplete, CommandAutocomplete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- IncomingCall: Reject uses variant=Critical (red), Ignore uses variant=Secondary
— previously both were variant=Success (green), making them visually identical
to the Answer button
- EventReaders: TDS timestamp glow reduced from double-layer to single-layer
(was 0 0 6px + 0 0 14px, now just 0 0 5px at 0.45 opacity)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- VoiceMessageRecorder recording dot now pulses (reuses pttLivePulse keyframe)
- Added data-voice-recorder / data-voice-rec-dot / data-voice-waveform attributes
for TDS targeting: green pulsing dot, cyan waveform bars, subtle border in TDS dark
- Wire VoiceMessageRecorder onError to the same input-bar error display used by
location errors (mic denied, media error surfaces to user instead of silent fail)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reply: distinguish loading (placeholder) from not-found (null) — show
"Original message not available" instead of a stuck loading bar
- RoomInput: geolocation errors now surface inline (denied / timed out /
unsupported); location button shows Spinner during fetch and is disabled
- Message menu: Retry Send + Cancel Message items appear when a message
is in NOT_SENT or CANCELLED state, calling mx.resendEvent / cancelPendingEvent
- ReactionViewer: sidebar gains role=listbox / role=option and ArrowUp/Down
keyboard navigation between reactions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ForwardMessageDialog:
- Room list now shows small avatars (48px crop) + DM label beneath room name
- Forward is now async: spinner overlay while in-flight, '✓ Forwarded' only
shown after sendEvent resolves; error clears sending state so user can retry
- Search bar hidden in success state for cleaner confirmation view
DeliveryStatus:
- QUEUED state used ⏳ emoji breaking the ASCII/terminal aesthetic; changed
to ⟳ matching the SENDING/ENCRYPTING icon
PollContent:
- Added data-poll-content + data-poll-answer + data-selected attributes so
TDS CSS can override inline styles without JS branching
- Added data-poll-content-label on the ◉ Poll header
- TDS dark: answers get cyan dim bg/border, selected gets orange highlight
with subtle box-shadow; hover brightens border; label uses cyan glow
- TDS light: equivalent blue/orange variants
Caption input:
- Marked with data-caption-input; focus-visible ring added in index.css
(blue for default, dark-theme dark blue) and lotus-terminal.css.ts
(orange glow for TDS dark, orange for TDS light)
Boot sequence:
- Added '[ ESC ] skip' hint at bottom-right of overlay so users know
they can dismiss it without waiting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CallControls: screenshare confirm now closes on Escape or click-outside
(transparent fixed backdrop + window keydown listener); cleaned indentation
- GifPicker: TDS header rendered a JSX comment ({/* GIF_SEARCH */}) so the
// GIF_SEARCH label was invisible; changed to {'// GIF_SEARCH'}
- CallEmbedProvider: PiP resize clamping now works at initial bottom/right
position by normalising to top/left before parsing el.style.left
- CallEmbedProvider: incoming call subtitle now reads 'Incoming Video Call'
or 'Incoming Voice Call' based on m.call.intent
- PollContent: progress bar background now uses --bg-surface-active /
--bg-surface-low instead of hardcoded white (invisible in light mode)
- index.css + lotus-terminal.css.ts: define --bg-surface, --bg-surface-low,
--bg-surface-active, --bg-surface-border, --text-primary as global CSS vars
with vanilla fallbacks and TDS dark/light overrides; these were used by
poll, location map, upload card and GIF picker but never defined
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Synapse's thumbnail endpoint returns 400 Bad Request when the
allow_redirect=true query parameter is present (added by matrix-js-sdk
41.x for authenticated media). Default allowRedirects to false in our
mxcUrlToHttp wrapper so the parameter is never appended.
Also extend the downloadMedia legacy-URL fallback to cover 400 in
addition to 401, catching any encrypted-media fetches that still carry
the old URL shape after a cache refresh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
downloadMedia: on 401 (SW session race or allow_redirect hop stripping
auth), retry via /_matrix/media/v3/ which is public on this homeserver
(allow_public_access_to_media_repo: true). Fixes images not loading
after sending, and avatar 401s in call prescreen tiles.
CallEmbed: inject flex-centering CSS for EC 0.19.4 participant avatar
container so the initial letter is correctly centered in its circle.
CSS class names are scoped to _avatarContainer_1mrho_40 in EC 0.19.4.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the server returns a 4xx/5xx, downloadMedia was silently returning
the error response body as a blob. decryptAttachment would then fail with
a misleading 'Mismatched SHA-256 digest' instead of surfacing the real
HTTP error. Now throws immediately so callers (useAsyncCallback) can
show the correct error state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove [CallEmbed] state, container styles, and syncCallEmbedPlacement
debug logs that were flooding the console during call sessions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add online/offline/idle presence dots next to verification shields in
both the room members drawer and the common-settings members list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a collapsible "Sessions" section to the user profile card that
appears when cross-signing is active and the profile belongs to another
user. Each session shows a colour-coded shield (green = verified, yellow
= unverified) and a "Verify" button for unverified devices that
initiates the SAS emoji flow via crypto.requestDeviceVerification.
New hook useOtherUserDevices fetches the target user's device list via
crypto.getUserDeviceInfo and reacts to CryptoEvent.DevicesUpdated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spaces and unencrypted rooms have hasEncryptionStateEvent() = false,
causing all badges to be hidden. Cross-signing verification is a user
identity property, not room-specific — show badges whenever
crossSigningActive, keep the E2EE banner gated on isEncrypted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract MemberVerificationBadge into a shared component and render it in:
- UserRoomProfile: shield badge beside the display name on the profile card
- common-settings Members: badge next to each member in the room/space
settings members page (accessible from the lobby)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add VoiceMessageRecorder component: mic button in composer toolbar,
live waveform + timer, preview before send, MSC3245-compliant content
(org.matrix.msc3245.voice, org.matrix.msc1767.audio with waveform),
E2EE support via encryptFile before upload
- Add useUserVerifiedStatus hook: uses crypto.getUserVerificationStatus,
reacts live to CryptoEvent.UserTrustStatusChanged
- MembersDrawer: show green/yellow shield badge per member in encrypted
rooms (cross-signing verified/unverified), E2EE status banner in header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- RenderMessageContent: add case for m.key.verification.request msgtype
so it renders an informational card instead of "Unsupported message"
- MsgTypeRenderers/FallbackContent: add VerificationRequestContent and
MessageVerificationRequestContent components (lock icon + instructional text)
- DeviceVerification: remove isSelfVerification guard from
ReceiveSelfDeviceVerification so cross-user verification requests also
trigger the SAS emoji dialog (was silently dropped before)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auto-fixed by prettier --write. Patch scripts used in the previous session
wrote code without running the formatter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CallEmbed: inject :root { color-scheme } into iframe so EC respects Cinny
theme regardless of OS preference (fixes white background in dark mode)
- CallEmbed: store themeKind, update color-scheme CSS on live setTheme() calls
- CallEmbed: catch transport.send() rejection in setTheme() to prevent
unhandled promise rejection when widget not ready yet (fixes REACT-8)
- CallEmbed: html + body both set to background:none so wallpaper shows through
- CallEmbedProvider: apply chatBackground wallpaper style to call embed
container in full-view mode (not PiP) -- wallpapers carry over to calls
- useCallEmbed: pass themeKind through to CallEmbed constructor
- index.tsx: ignoreErrors: [Request timed out] to suppress matrixRTC
heartbeat timeouts (REACT-9) from Sentry noise
- README: document 0.19.4, positioning fix, dark mode fix, wallpaper,
millify Rolldown interop fix, Sentry noise filter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- millify.ts: use named import { millify as millifyPlugin } instead of
default import to fix Rolldown CJS interop bug where zc.default gets
set to the whole module object instead of the function (mode=1 forces
default=n instead of default=n.default, breaking MembersDrawer)
- useCallEmbed.ts: use getBoundingClientRect() for accurate fixed
positioning; add useEffect to trigger syncCallEmbedPlacement on mount
so embed is positioned before the first resize event
- CallEmbedProvider.tsx: fix [pipMode, callVisible] effect to NOT clear
top/left/width/height when callVisible changes (previously cleared
position set by syncCallEmbedPlacement every time joined changed);
only clear pip-specific styles when actually exiting pip; add debug
console logging for positioning state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the listenAction wrapper only called preventDefault() to stop
the switch default from firing an error, but it never sent a reply.
The widget transport would then wait for a response until it timed out.
Now the wrapper also calls transport.reply(ev.detail, {}) to return an
immediate success, fixing io.element.join, io.element.device_mute, and
set_always_on_screen.
The base WidgetDriver throws Failed to override function for these
methods. ClientWidgetApi routes update_delayed_event widget actions to
cancelScheduledDelayedEvent, restartScheduledDelayedEvent, or
sendScheduledDelayedEvent. Without these overrides every delayed-event
refresh from element-call fails, causing MembershipManager to drop the
call after retries.
Also make listenAction auto-call preventDefault so io.element.join and
other custom widget actions return success. Add set_always_on_screen
handler so element-call PiP requests are acknowledged.
- RoomTimeline.tsx: add eslint-disable comment for intentional eventsLength
dep on timelineSegments useMemo (needed to detect in-place timeline mutations)
- Remove ~47 stale eslint-disable-next-line comments across 28 files for rules
that are now off in the flat config (no-param-reassign, jsx-a11y/media-has-caption,
react/no-array-index-key, etc); run prettier to reformat
- vite.config.js: move manualChunks from rollupOptions.output to
rolldownOptions.output so Rolldown (Vite 8) actually applies it; main bundle
drops from 3.5 MB to 814 kB gzip-248 kB, matrix-sdk gets its own 1.16 MB
cacheable chunk
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Lazy-import CreateRoomForm/CreateSpaceForm in CreateRoom.tsx and Create.tsx
so create-room and create-space get their own chunks; eliminates
INEFFECTIVE_DYNAMIC_IMPORT warnings
- Add RouteError component wired to root route errorElement so crashes show
a reload button instead of React Router dev screen
- ci.yml: use secrets.SENTRY_AUTH_TOKEN so source maps upload on CI builds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix timelineSegments useMemo stale cache: the Perf-5 optimization used
timeline.linkedTimelines as its only dep, but that reference never changes
when events are added in-place; adding eventsLength as a dep makes it
recompute on every new live event so the binary search always finds the
new item
- Add LobbySkeleton: shimmer placeholder for space lobby (header + hero +
room list rows) shown while the Lobby chunk lazy-loads
- Add AuthSkeleton: shimmer placeholder for auth pages (logo + server
picker + form fields) shown while AuthLayout chunk lazy-loads
- Wire both into Router.tsx fallback props
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Automated cleanup removed const mx = useMatrixClient() from 3 more components
that use it (MessagePinItem, Message, Event) in addition to the 2 fixed in
the previous hotfix. Root cause: the cleanup script used substring matching
on indentation which removed declarations at any indent level, not just the
one targeted unused variable.
All 5 components that call mx.* now have their declarations restored.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The automated unused-var cleanup incorrectly removed const mx = useMatrixClient()
from MessageDeleteItem and ReportMessage components in Message.tsx. Both components
use mx inside their useCallback closures (mx.redactEvent, mx.reportEvent). This
caused a ReferenceError crash on the messages view in production.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ServerConfigsLoader: skip validateAuthMetadata when getAuthMetadata()
rejects (404 on /auth_issuer means server uses traditional SSO, not
native Matrix OIDC/MAS - this is expected and should not log errors)
- Router: use HydrateFallback={() => null} instead of hydrateFallbackElement={null}
so react-router v7 counts it as truthy and suppresses the spurious warning
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- react 18.2.0 to 19.2.6
- react-dom 18.2.0 to 19.2.6
- @types/react 18.2.39 to 19.2.15
- @types/react-dom 18.2.17 to 19.2.3
React 19 breaking changes fixed:
- useRef<T>(null) now returns RefObject<T | null>; cast to
RefObject<T> at 16 component call sites (safe, runtime unchanged)
- useRef<T>() without arg no longer valid; add | undefined>(undefined)
in useDebounce, useFileDrop, useThrottle, useVirtualPaginator hooks,
RoomInput, RoomTimeline, and ClientNonUIFeatures
- useReducer<typeof reducer> 1-arg form removed; drop explicit type arg
in useForceUpdate (inferred from reducer function)
- global JSX namespace removed; import type { JSX } from react in
react-custom-html-parser.tsx
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- typescript 5.9.3 to 6.0.3
- moduleResolution Node to bundler (correct for Vite projects)
- target/lib ES2016 to ES2020 (enables flatMap, Promise.allSettled)
- Fix global to globalThis in initMatrix.ts (browser env)
- Fix EventEmitter default to named import in CallControl.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolves all TS2345/TS2347/TS7006 type errors introduced by stricter TypeScript 5.x.
Fix Icons.Settings to Icons.Setting, cast account data returns, fix implicit any.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Icons.Settings is undefined in folds v2.6.2; only Icons.Setting exists.
This caused TypeError: i is not a function when rendering m.room.join_rules
or m.room.guest_access state events in the room timeline, crashing DMs with
those events visible in the initial view.
react-router v7's generatePath() now calls encodeURIComponent() on all
path params. pathUtils.ts was also calling encodeURIComponent() before
passing to generatePath, resulting in double-encoding (e.g. '#' became
'%2523' instead of '%23').
This caused spaces/rooms with alias paths to receive double-encoded
room IDs from useParams(), which were then re-encoded by matrix-sdk
when making HTTP requests (400 Bad Request from Synapse).
Remove the manual encodeURIComponent() calls -- generatePath handles it.
- Fix prettier formatting in useCall.ts and initMatrix.ts (unblocks CI)
- Fix viteStaticCopy stripBase so manifest.json and public/locales/ land
at correct output paths (was getting extra 'public/' prefix from v4 path
preservation behavior)
- Silence react-router v7 HydrateFallback warning on root route (SPA has
no SSR hydration, null is intentional)
When matrix-sdk is briefly upgraded then reverted, the local IndexedDB
schema version is higher than the SDK expects. Detect the VersionError
DOMException and show a clear 'Clear local data and reload' button
instead of a cryptic error message.