The main chunk had crept to 356.9 kB gzip against a 349.9 kB budget (a
hard gate on pull requests, only a warning on pushes, so it went unnoticed
until PR #241). jsQR (camera scanning) and qrcode (drawing the QR) were
statically imported by the device-verification components, so every
startup loaded them. Both are now dynamic imports: jsQR when the scanner
opens (fetched in parallel with the camera permission), qrcode when a QR is
drawn.
Main chunk 356.9 → 302.5 kB gzip (largest chunk is now matrix-sdk at
304.7 kB, within budget); chromium e2e 19 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
Most modals rendered a folds Dialog/Modal with no role and no name, and
their focus traps used `initialFocus: false`, so focus stayed behind the
modal and a screen reader never announced it.
- 32 dialogs with a visible heading: role="dialog", aria-modal,
aria-labelledby → the heading (given an id), tabIndex=-1.
- 4 dialogs that already had a name (Leave Room, room topic viewer,
server ACL, room-nav prompt): role + aria-modal.
- Their focus traps drop `initialFocus: false` for focus-trap's default
(keep an already-focused autoFocus field, else the first tabbable
element) with the dialog itself as fallbackFocus, so a dialog without
a tabbable node can't crash the trap. Traps that live in a parent
(UIA stages, Logout, Forward, Invite) get the semantics only.
- The file drop overlay is deliberately left alone (not a dialog).
Checked at runtime: Join with Address, Delete Message, Report Message,
Leave Room and Logout open as named dialogs with focus inside and close
with Escape (Tab first when a text field has focus — the shared
stopPropagation keeps Escape from discarding typed text, by design).
The axe e2e spec (6 tests) passes; eslint warnings unchanged (46).
17 modals with no heading (image/file viewers, loading screens) remain.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
B2 of the Matrix protocol-gaps roadmap, gate-green (688 tests):
- Enable QR verification methods (show/scan/reciprocate) in initMatrix.
- Extend DeviceVerification: the Ready step offers your own QR (byte-mode encode
via qrcode), a camera 'Scan their QR code' flow, and an emoji fallback; the
Started step routes reciprocate → a confirm step (useVerifierShowReciprocateQr)
or SAS as before.
- New QrScanner component: getUserMedia + jsQR, handing the raw binaryData bytes
to request.scanQRCode (BarcodeDetector is string-only, so can't be used).
- Adds qrcode + jsqr (small, pure-JS, client-only); build-verified under rolldown.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review flagged that wrapping only the buttons left the dialog body copy
hardcoded (mixed-language dialogs once a non-en locale ships). Wrap the
remaining body/waiting strings ("Please accept…", "Confirm the emoji…",
"Do not Match", "Your device is verified.", etc.) and the PasswordStage
prompt, adding hooks to the sub-components that lacked one. Keys added to
en.json; all t() keys verified to resolve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wraps the hardcoded strings flagged in LOTUS_BUGS.md (Localization rows)
in t() via react-i18next, and adds the keys to public/locales/en.json
under the existing Organisms.* namespace. de.json intentionally left to
fall back to en for now (fallbackLng: 'en') rather than fabricate
translations.
Files: CreateRoomTypeSelector, ImageViewer, MsgTypeRenderers (MLocation),
Reply (ThreadIndicator), ImageContent, DeviceVerification (5 subcomponents),
UrlPreviewCard (DiscordCard), InviteUserPrompt, UploadBoard, PasswordStage.
Co-Authored-By: Claude Opus 4.8 <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>
- 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>
Prettier: auto-formatted 103 files to fix baseline. Prettier check in CI
is now a hard gate (removed continue-on-error).
Brotli: installed libnginx-mod-http-brotli-filter/static. Enabled in nginx
with brotli_static on for pre-compressed assets and comp_level 6.
Sentry releases: deploy script now exports VITE_APP_VERSION=<git-short-sha>
before building so each Sentry release maps to an exact commit.
CI also passes github.sha as VITE_APP_VERSION.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>