1
0
mirror of https://github.com/element-hq/element-web.git synced 2026-10-23 11:09:01 +03:00

129 Commits

Author SHA1 Message Date
Skye Elliot
df9d6798d0 Allow generating a recovery key when logging out and this is the last device (#35401)
* feat(web): add a success style to `EncryptionCard`

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(web): generate the recovery key inside the logout dialog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(web): cover generating the recovery key inside the logout dialog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: Group translations under settings|encryption|recovery

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:12:45 +00:00
Milton Moura
db69764acd Fix PiP dragger when displaying over widgets (#35395)
Signed-off-by: Milton Moura <milton.moura@nordeck.net>
Co-authored-by: Milton Moura <milton.moura@nordeck.net>
2026-10-14 13:53:28 +00:00
Richard van der Hoff
5455765fe5 Remove introductory screen from "create recovery key" flow (#35451)
* Remove introductory screen from "create recovery key" flow.

Between them, https://github.com/element-hq/element-meta/issues/3188 and
https://github.com/element-hq/element-meta/issues/3185 move all the information
about "what is a recovery key" into the toasts and modals that lead into the
"create recovery key" flow, thus making the introductory screen redundant.

Fixes https://github.com/element-hq/element-web/issues/35428

* update broken vitest

* playwright: Move body of `enableKeyBackup` into a `step`

... to make the logs a bit clearer

* Fix broken playwright tests
2026-10-13 14:32:33 +00:00
David Baker
1ddb4049e0 Update compound-web to 10.3.1 (#35390)
* Update compound-web to 10.3.1

To fix tooltip overflow bug

* Fix success to accent

* snapshot/test updates

* Gah the snapshots

* more snapshots

* Last lot of snapshots, hopefully

* Change the brittle CSS override to reflect the new CSS

* Same hack on date picker context menu

* s/success/accent/ and another padding override

* Update for really tiny font rendering differences
2026-10-09 12:20:01 +00:00
Will Hunt
56ec209d49 Drop support for migrating libolm sessions (#35138)
* Remove the libolm to rust crypto store migration

* Add LegacyCryptoUnsupportedView to shared-components

* Show LegacyCryptoUnsupportedView when an unmigrated libolm crypto store is found

* Replace crypto migration e2e test with legacy crypto store test

* Cleanup loadSession, reduce congivitive complexity.

* Improve centering and spacing of view

* update snap

* changes for review

* lint

* Don't format the dump file

* Re-add missing files
2026-10-08 11:27:39 +00:00
Florian Duros
0046435cc8 Improve invite button UX in member list (#35270)
* Clean up memberlist invte container

It's cleaner to rely on the container padding and flexbox rules to place
the children.

* Use primary button for invit button in memberlist

* Update e2e test
2026-10-01 21:52:27 +00:00
Timo
5fae057a60 Element call as a react component (#34935)
* Add an end-to-end Element Call test with a real MatrixRTC backend

Element Web's Playwright coverage of Element Call has only ever used a stand-in:
`sample-files/fake-element-call.html`, a page that does just enough for the widget
plumbing to be exercised. Nothing in the suite has ever carried media.

This adds one real call between two users, through the Element Call the build under
test ships (`@element-hq/element-call-embedded`, copied to `widgets/element-call/`),
against a real MatrixRTC backend:

- `testcontainers/livekit.ts` — a LiveKit SFU advertising loopback so the browser
  reaches it through published ports on macOS as well as Linux.
- `testcontainers/lk-jwt-service.ts` — the authorisation service, validating OpenID
  tokens over Synapse's federation API.
- `testcontainers/matrix-rtc.ts` — starts the two and produces the Synapse
  configuration that announces the transport (MSC4143) and enables the MSCs Element
  Call needs.
- `services.ts` — a `matrixRTC` worker option turning all of that on.

`routeConfigJson` no longer hard-codes the dev server's origin: it resolves
`config.json` against the context's `baseURL`, so a `BASE_URL` other than the default
works, and other origins a test loads keep their own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Make Renovate actually see the new testcontainer images

The custom manager's `depName` group is `[^@]+`, which matches newlines and
quotes, so it is only bounded by the next `@` in the file. In `synapse.ts` and
`mas.ts` an `@element-hq/…` import happens to sit above the image literal and
caps it; `livekit.ts` and `lk-jwt-service.ts` import `testcontainers` and `yaml`,
so the match instead starts at that import and swallows everything up to the
image name. Renovate gets a nonsense package name, finds nothing, and the pins
quietly rot.

Exclude quotes and whitespace from both groups. All four digest-pinned files now
extract correctly, synapse and mas unchanged.

Bump lk-jwt-service to 0.7.0 while here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Address the bundled Element Call by directory, not by index.html

The full-call spec passed against the webpack dev server and failed in CI, where
the production build is hosted by `serve`.

`ElementCall.generateWidgetUrl` built `./widgets/element-call/index.html#…`.
`serve` has `cleanUrls` on by default, so it 301s that to the extensionless
`/widgets/element-call`. Element Call's `index.html` references its chunks
relatively (`./assets/…`), and against a base with no trailing slash the browser
resolves those to `/widgets/assets/…` — 404. Element Call never boots, so the
lobby never appears and the test times out.

Address the directory instead. It is the same deployment, the trailing slash
sidesteps the redirect, and the hash is overwritten further down either way. This
is not only a test concern: Netlify also strips `.html` by default, and that is
what serves the PR previews and develop.element.io.

Doing it here rather than in the spec keeps the test honest — it exercises the
widget URL Element Web really generates, with no `Developer.elementCallUrl`
override. Setting `cleanUrls` in a `serve.json` was the other candidate and is
not viable: any value other than the default also stops `serve` resolving `/` to
`index.html`, which breaks every other test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* EC react component Plan

* The implemantation Element Call React Component with mocked EC react
component

* final plan

* tests

* update plan to not do e2e tests in EW but in EC

* Start using the real react component instead of the mock (activation
react EC)

* add playwright e2e tests

* fix transport discovery (pass in via config. Hack the well-known source
for servers not supporting get_transports)

* stylesheet fixes

* Update element-call-react-plan.md

* add docuemnt pip

* Depend on the Element Call component from its git branch

`@element-hq/element-call-component` was a `link:` to a hand-built
`dist/` in a machine-local worktree. It is now a git dependency on the
`component` directory of element-hq/element-call, on the
`valere/component_ec_M1` branch (element-call#4233), whose `prepare`
script builds the component on install; pnpm has to be allowed to run
it (`allowBuilds`). The ref goes back to `main` once that PR has merged.

The branch first needs the commit that adds the component's manifest
and build (`toger5/component-package-manifest` in the EC checkout);
until it is pushed, `path:/component` resolves to no manifest and the
lockfile keeps the old `link:` entry. Both are written up in the plan.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* use the component_ec_m1 dep lock file entries

* fix element call reloading

* pass the right theme to element-call component

* bump element-call-component

* Return a call from the browser PiP window to where it left from

Closing the Document Picture-in-Picture window always dropped the call
into Element Web's floating PiP, even when it had filled the room's call
view a moment earlier and the user was still looking at that room.

The store now remembers whether the call left from the call view, and
on release opens the call view again when it did — unless the call has
ended, the user has moved to another room, or the user opened the call
view themselves (which is what closed the window). A call that was
floating in Element Web's PiP before goes back there, as it did.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Draw the call header's outline icons a little smaller

The room header's icons are Compound's solid set, but minimise,
maximise and pop-out exist only as outlines: Compound has no filled
counterparts, and nothing else in the set says what they say. At the
solid icons' size their open strokes look heavy-handed next to them.

Scaling the glyphs down slightly inside the buttons lets them sit
comfortably next to their neighbours without changing the buttons'
footprint.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Hand a persisted tree to the remaining placeholder on unmount

When two PersistedElements for the same key mount in one React commit
(a call returning from the browser PiP window into the call view, while
Element Web's floating PiP briefly shows it as well), whichever rendered
the tree last owns the child's ref. If that one is then the one to
unmount, it hid the tree, and the survivor, never having been handed the
child, could not show it again.

On unmount, if another placeholder is mounted, render the tree once more
with the most recently mounted survivor's wrapper so it takes over the
child and positions it; only hide the tree when nothing is left to show
it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Adjust to updates done in EC
(simpler public api. More resilient to prop changes)

* package.json back to EC branch valere/component_ec_M1

* fix closing in component mode

* Configure pnpm store location in workflows

Keep the pnpm store out of node_modules to prevent incorrect
bundling of element-call-component when building git dependencies.

* update docs on EC packaging improvements

* fix ci

* Update pnpm-lock and tsconfig

* Move the browser Picture-in-Picture window and call header changes to a follow-up

Keeps this PR to Element Call as a React component. The Document
Picture-in-Picture window (DocumentPipStore, useDocumentPip, the
PersistedElement detach/reattach and placeholder hand-over, the
PipContainer awareness of it), the call header's minimise, maximise and
pop-out buttons with their outline-icon sizing, the browser API typings,
their strings, tests and e2e spec move to toger5/ec-document-pip.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* bump to main branch EC

* fix CI failing due to pnpm store

* remove docs and dev journals

* review

* CI: silence zizmor's github-env finding on the pnpm store action

zizmor flags every $GITHUB_ENV write inside a composite action, because it
cannot see which triggers the action will be used from. The value written
here is a constant built from RUNNER_TEMP with nothing attacker-controlled
in it, so suppress the finding the way the repo does elsewhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep the ready promise's rejection from going unobserved

close() rejects the pending ready promise so that a start() still waiting on
the component learns the call went away instead of hanging until the timeout.
Usually nothing is waiting, and the unobserved rejection then surfaced as an
unhandled promise rejection, which crashed the Vitest worker running
Call.test.ts. Mark the promise as handled before rejecting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* review

* review: double import

* move types to EC repo (just import them here)

* review: refactor ElementCallAppTile to a MVVM shared compoenent

* merge useEffectiveTheme and useTheme hooks

* cnvert the ElementCallMock to MVVM

* use unstable EC branch. DO NOT MERGE UNTI THIS COMMIT IS REVERTED
this branch is used to test CI with the new exports from EC.
We need to first merge:
https://github.com/element-hq/element-call/pull/4269 before we can put
this back to main (revert this commit).

* Move Node and pnpm setup into a single action

The setup-node-pnpm action now includes pnpm/action-setup and
actions/setup-node alongside the store relocation, eliminating
boilerplate across workflows and fixing the setup order in one place.

* fix broken lock file

* test for review change "reject on promise race"

* review: letter spacing

* Import the EC CSS layered

* Use self-repository syntax

* Fix import path

* Fix webpack

* Revert actions syntax as Sonar doesn't seem compatible with it

* Switch to Flex

* Switch to a11y label

* Take client from SdkContext

* Make strings in mock view translatable

* Keep class names in EW

* Move mock view to shared components and add stories

* Exempt voip|element_call_mock|title

* Fix tests

* Clarify doc string

* Add labs.md entry

* Use text element

* Remove redundant test

* Remove redundant test

* Delint

* Format

* Update snapshots

* Bump element-call

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-09-25 15:12:19 +00:00
Zack
3d13a19360 Render PDFs in a usercontent iframe (#35067)
* Render PDFs in a usercontent iframe

Move pdf.js into an opaque-origin iframe at usercontent/pdf/, as the download link does, so a document cannot reach the app's origin.

* Check the origin of messages from the PDF iframe

* Address PDF iframe messages to the app's origin

* Talk to the PDF iframe over a MessageChannel

The iframe hands the app one end of a channel with ready; everything else travels over it, so no postMessage needs a wildcard target.

* Give the PDF iframe page a title

* Look up the wheel delta scale instead of nesting ternaries

* Use PropsWithChildren for PdfViewerView

* Remove the PDF iframe's listeners when the page unloads

* Tear down pdf.js, the blob URLs and the port with the PDF iframe

* Note how the gesture listeners are removed
2026-09-25 09:46:32 +00:00
Nathanaël
8978841c64 Add cross-room thread panels to Threads Activity Centre (#32851)
* feat(TAC): add cross-room thread panels to Threads Activity Centre

Adds two new tabs to the TAC popup (Rooms / All threads / My threads):

- "Rooms" tab: existing behaviour — rooms with unread threads
- "All threads" tab: flat list of all unread threads from those rooms,
shown as two-line rows (room avatar + name, sender + message preview)
- "My threads" tab: same layout but filtered to threads the user
participated in (as root sender or replier), across all rooms

Implementation notes:
- Export doesTimelineHaveUnreadMessages from Unread.ts for per-thread
unread detection
- Participation check covers both thread root and replies
- Uses server-side notification counts as primary unread signal,
falling back to timeline inspection for activity-level unread
- settingTACOnlyNotifs applies to the Rooms tab only; thread tabs show
all unread participated threads regardless of notification level

Warning:
I know threads and unread are very difficult topics. But in daily job
we are many to strungle to find and follow threads, in which rooms
and where. So I started this not to provide a perfect solution, but
as starting point to sustain the improvment regarding this issue.
(And to see if at least we could have something in-house)
Honestly I think this would benefit a lot from the ongoing work
on element-web new architecture - which I did not took time to dive in -
and on every MSCs and synapse that will provide threads subscribe and
unsubscribe mechanisms.
Finally, this definetly needs design!

Closes #28036

* feat(TAC): replace hideChevron with compound nav-hint; dot yields to chevron on hover

Move the notification dot to the trailing edge of the thread row as a
direct MenuItem child (order: 998). The compound nav-hint chevron sits
at order: 999 in the same position. On hover, compound's `~ *` rule
naturally hides the dot; a targeted override keeps the text content
visible. This removes the hideChevron workaround entirely.

* feat(TAC): redesign per Figma — My threads / Other threads tabs

Replace 3 tabs (Rooms | All | My) with 2 mutually exclusive tabs:
"My threads" (default) and "Other threads". Categorise via server data
(thread.hasCurrentUserParticipated, highlight count); mentions/keywords
land in My threads even when not participated.

The tac_only_notifications setting is scoped to "Other threads" only:
My threads always shows participated or highlighted threads regardless
of the setting. In Other threads, setting=true drops activity-only
(local-detected) entries; setting=false keeps them. Default flipped to
false. The setting label now reads "In the Other threads tab, only
show notifications". The rooms list and greatestNotificationLevel only
reflect rooms that contribute at least one displayed thread.

Drop the doesRoomHaveUnreadThreads() room-level pre-filter; it
short-circuited on muted rooms and on rooms with no local timeline
activity, hiding participated threads in muted rooms and missing
server-pushed highlights that hadn't surfaced locally. The per-thread
loop now decides inclusion based on server counts (authoritative) plus
a lazy local fallback. Mute is applied per-room inside the Other
threads branch only, preserving the "muted rooms don't clutter Other
threads" guarantee while letting them still contribute to My threads.

UI: Compound NavBar/NavItem for tabs, hide the menu title, fixed popup
height for ~6 cells, 40px avatars. Zero Compound FloatingMenu's
padding-block so the tabs + rows containers control their own spacing.
Fix hover regression by overriding Compound's chevron-replaces-content
sibling rule so the dot and content stay visible.

* fix(TAC): don't surface read participated threads in My threads

The local unread heuristic (doesTimelineHaveUnreadMessages) reports a
participated thread as unread when the user replied but isn't the literal
last sender — e.g. a later reaction/edit/redaction landed after their
reply. The js-sdk's read shortcut only fires for the very last event (see
the "second-last event" TODO in room-receipts.ts), so the latest incoming
message, older than the user's reply, still reads as unread.

Guard the local fallback with hasUnreadAfterMyLatestReply: treat the thread
as read once the user's own latest reply is at/after the latest incoming
message. Server notification counts still take precedence and the
muted-room fallback is preserved.

* feat(TAC): use shared message-preview store and Compound typography

Render thread-row previews via MessagePreviewStore (consistent edit/reply/
emote/HTML handling with the room list) and resolve sender names with
getSenderName. Apply Compound Text typography to the room name, sender and
preview, dropping the equivalent font rules from CSS.

Open a thread with a two-card stack (thread list beneath the thread view) so
the thread view gets a working back button, mirroring Action.ShowThread.
Extract evaluateThreadUnread from computeUnreadThreadRooms and collapse the
per-tab render branches into a single active-tab selection.

* fix(TAC): keep locally-replied threads in My threads

Thread.hasCurrentUserParticipated is set only from the homeserver's
bundled current_user_participated flag, which lags behind local activity.
Right after the user replies in someone else's thread, the flag is still
false, so the thread was wrongly categorised under Other threads.

Complement the server flag by inspecting the local timeline: if the
current user authored a sent m.thread reply, the thread is theirs. This
matches the server's participated semantics (isRelation(m.thread) &&
!status), the same test the rest of the app uses for a real reply.

* test(TAC): port unit tests to colocated vitest after upstream migration

Upstream migrated the Threads Activity Centre unit tests from jest
(test/unit-tests/.../*-test.tsx) to colocated vitest (src/.../*.test.tsx)
in #34800. Re-apply our redesigned My threads / Other threads test suite
on top of that migration:

- Convert jest.* -> vi.* and jest-matrix-react -> test-utils-rtl imports.
- Replace the pre-redesign test bodies with the participated/other-threads
  suite matching the current component API.
- Update notification-badge assertions to the shared NotificationBadgeView
  data-notification-level attribute (NotificationBadge MVVM refactor).
- Drop the obsolete ThreadsActivityCentre snapshot (redesign uses explicit
  assertions, no snapshots).

* fixes requested in review

- replace `enum TACView` with a string union
- extract tab selection into `getActiveTabContent`
- `Array<ThreadData>` -> `ThreadData[]`, `Result` -> exported `UnreadThreadRooms`
- add tsdoc to `ThreadData`
- trim `computeUnreadThreadRooms` tsdoc
- restore `tac_only_notifications` default to true
- use Compound spacing tokens, incl. `--cpd-space-0-5x` for the 2px gap
- use Compound semantic colour tokens instead of `$*-content`
- use the Compound `Text` component for the empty caption
- drop all `!important` added by this PR, scoping rules under `[role="menu"]` instead
- restore `hideChevron` on `MenuItem` rather than hiding the chevron in CSS
- drop the dead `mx_ThreadsActivityCentreRow` class
- link the upstream issues/PRs from both workaround tsdocs

* fix(TAC): focus on threadsPanel

* fix(TAC): use 444px width to match Figma design

* feat(TAC): use the room list's notification decoration on thread rows

Thread rows rendered `StatelessNotificationBadge` with `forceDot`, which
collapsed three states into three 8px dots differing only in hue — grey for
activity, green for a notification, red for a mention. That encodes state by
colour alone (WCAG 1.4.1), red/green being the worst pair to pick, and
`forceDot` discarded the count so 1 and 30 unread looked identical.

Use the same decoration as the room list instead, so the states differ by
shape: an `@` icon for a mention, a counter for a notification, a plain dot
for activity, and the bell for a thread in a muted room. This reuses the
presentational component from shared-components rather than the app-side one,
which needs a `RoomNotificationState` that has no per-thread equivalent.

The count and the room's muted flag are now carried on `ThreadData`, and the
row's `aria-label` spells out the unread state — the label overrides the row's
content, so the new visible counter would otherwise go unannounced.

Also fixes `assertThreadsInTac`, which matched `mx_NotificationBadge_level_*`
classes that exist nowhere in the tree; it goes back to the
`data-notification-level` selector, kept alive by the row wrapper.

* fix(TAC): drop a thread as soon as it is read

A thread stayed listed, counter and all, for as long as the homeserver took
to push fresh notification counts — the js-sdk never recomputes `total`
locally, and only zeroes counts when we sent the last event ourselves.
Since presence was `hasServerNotifs || <local checks>`, reading changed
nothing until the next sync said so.

Check our read receipt before the server counts, so an explicit receipt on
the thread's latest known event wins over a stale count, and recompute on
`RoomEvent.Receipt` rather than waiting for the next sync.

* fix(TAC): drop the workarounds now fixed in the js-sdk

`hasUnreadAfterMyLatestReply` guarded a false positive in
`doesTimelineHaveUnreadMessages`, and `hasCurrentUserSentInThread` covered
`Thread.hasCurrentUserParticipated` being stale right after we reply. Both
root causes are fixed upstream, so the guards go and their coverage moves to
the js-sdk specs.

Fixed by matrix-org/matrix-js-sdk#5525 (treat an event as read if the user
sent a later one) and matrix-org/matrix-js-sdk#5516 (set
hasCurrentUserParticipated when the current user sends a thread reply).

Note both land after the js-sdk commit this repo currently pins, so the two
bugs reappear until that pin advances.

* test(TAC): update playwright tests for the redesigned panel

The e2e expectations still described the pre-redesign TAC, and the
workflows never ran on this PR, so none of it showed up as red:

- a mention now makes a thread ours, so the mentioning thread is asserted
  in "My threads" and the two plain ones in "Other threads", newest first
- clicking a row opens the thread itself, so "mark all as read" steps back
  to the list first, via the card's back button
- one call site still used `clickRoomInTac`, renamed to `clickThreadInTac`
  when the TAC moved from one row per room to one per thread

Screenshots regenerated with `pnpm test:playwright:screenshots`.

* fix(TAC): restore the e2e helper's types

The playwright project is type-checked separately from the app
(`tsc --noEmit --project playwright`), and this branch had loosened the TAC
helper in ways that only fail there: optional fixture names, dropped non-null
assertions, a MessageBuilder constructor whose three fields are never read,
and a nullable messageFinder.

Put those back the way develop has them, keeping only the changes this PR
needs: clickThreadInTac, switchToOtherThreadsTab, assertThreadsInTac and
clickBackToThreadList.

* test: update notification settings snapshots for the renamed setting

This PR rescoped `Notifications.tac_only_notifications` to the Other threads
tab and renamed its label accordingly, which changes the notification settings
screen. Regenerate the two vitest snapshots and the two playwright baselines
that still carried the old wording.

* Address review comments

---------

Co-authored-by: Florian Duros <florianduros@element.io>
2026-09-25 08:17:19 +00:00
David Langley
d3dafba29c Make Spotlight result text and badges legible at rest and in high contrast (#35037)
Two colour defects in Spotlight results, plus a test that could not see either:

- The public-room alias and the room context details are 12px text in
  $tertiary-content at rest, which is 3.5:1 against the dialog background in the
  light and dark themes - below the WCAG AA 4.5:1 for text that size. Use
  $secondary-content at rest (5.2:1 light and dark, 8.9:1 high contrast).
  Hovered and selected rows keep $primary-content, since secondary is only
  4.2:1 against the hover background.

- In the light high-contrast theme a leftover override painted the notification
  badge with the dialog background whenever its row was hovered or selected,
  turning a red count badge into a white disc and hiding the count. Remove it;
  the badge's own colours are fine (count text is 5.3:1 on red and grey).

The legibility test claimed to cover "text across Spotlight surfaces" but only
ever scanned a single hovered row, so neither defect could fail it. It now
returns two matching rooms per search so a row at rest sits beside the selected
one, puts the joined room in a space so context details are rendered, and leaves
an unread mention so the first result - selected by default - carries a badge
with a count. axe scans all rows for contrast; the selected badged row and a row
at rest are also screenshotted, the former because axe reports the overlapping
badge as incomplete rather than as a violation, the latter to pin the resting
colours. No hover and no retry is needed. Against the previous CSS the test
fails in all three themes on exactly these defects; with the fix it passes 30/30
across them.

Fixes #34991
Fixes #34841

Signed-off-by: David Langley <davidl@element.io>
2026-09-23 17:18:36 +00:00
David Baker
10567509ee User status in RTE (wysiwyg) composer (#35139)
Surpridingly not too complex as long as we are happy that
 1) The user status doesn't update if the target updates their status
    once the completion has been chosen, and
 2) We hope that the user status has been fetched by tbe time the
    user accepts the completion, and if it isn't, it won't appear
    in the pill if the fetch later succeeds.
2026-09-23 13:35:31 +00:00
Will Hunt
9ba257efee Do not make a thread bright red when mentioned. (#35132)
* Do not make a thread bright red when mentioned.

* Fix this for threads explicitly
2026-09-22 10:53:51 +00:00
Zack
5fc4f4090c Restore spacing between message text and URL previews (#35127)
* Restore spacing between message text and URL previews

* Update snapshot to reflect correct change

* New correct snapshot
2026-09-22 03:18:34 +00:00
Will Hunt
789083069b Fix autocomplete/formatting bar being hidden when editing a message (#35126)
* Fix elements clipping out of the edit message composer

* trim down excessively chatty robot
2026-09-21 13:05:56 +00:00
Will Hunt
e7d6fd6473 Move GenericToast to shared-components (#35122)
* Port GenericToast to a shared component

* Update imports of GenericToast

* Remove legacy styles

* update test ids

* Move from classes to testIds

* update snap

* update toasts

* Add a test for coverage
2026-09-21 10:54:12 +00:00
David Baker
7af7c87e00 Bound user status length in room header (#35104)
* Bound user status length in room header

* Add story for long text

Long text is long
2026-09-21 09:03:14 +00:00
David Baker
78deb25d61 Fix user status overflowing in settings dialog (#35096)
Seems the lack of a min-width here was causing the max-width: 100%
to not work.
2026-09-17 13:02:09 +00:00
David Langley
26771fd3b1 New timeline panel in the room view (labs) (#34859)
* Fix virtual-core scroll compensation during in-flight scrolls

Patch fixes so the timeline holds its place when rows change size while a
scroll is still settling: decisions use the up-to-date scroll position, the
stay-at-bottom correction retries if the browser clamped it, and corrections
now also apply while scrolling upwards. All candidates for upstreaming.

* Use the timeline's own 4px tolerance when deciding it is at the bottom

* Fix full-width timeline rows overflowing the right-hand edge

The list kept the browser's default 40px indent, which pushed every row 40px
too wide. Only full-width rows (the read marker line, date separators) showed
it.

* Add a labelled "New" style to the read marker

The new design: a green line with a "New" label at its right-hand end. Opt-in
via a new label prop, so the legacy timeline keeps its plain line. Also adds an
as="div" option for callers that already provide the list item.

* Fix the reserved image box collapsing while the image loads

The box meant to hold a photo's space before it downloads was being sized to
zero by the browser, so the message grew when the photo arrived and shoved the
timeline. Failed images also keep the reserved height now instead of collapsing
to one line.

* Fix the timeline stalling at the top with more history available

A request to load more history that arrived while a fetch was already running
was silently dropped, and the view would sometimes never re-ask — leaving the
timeline stuck at the top until the user jiggled the scroll. Such requests are
now remembered and run when the current fetch finishes, matching what the old
ScrollPanel did.

* Render polls at full size straight away

The question and options are in the event we already have; only votes need
fetching. Rendering them immediately stops a freshly loaded poll growing ~180px
when its votes arrive and shoving the timeline.

* Add a compact reply preview that keeps one height while it loads

For the new timeline: the quoted message shows immediately when it is already
loaded, a fixed-height skeleton stands in while one is fetched, and the rows are
pinned to one height so the preview never resizes and shoves the messages around
it. Opt-in via a new EventTile prop; the legacy timeline is unchanged.

* Add the new timeline to the room view behind a labs flag

The new virtualised timeline panel, rendering events through the legacy
EventTile for now. Enabled by the "Improved timeline scrolling and navigation"
labs setting; the existing timeline is untouched when the flag is off.

* Address SonarCloud findings

Mark component props read-only, hoist the timeline row renderer to module
level, pass the quoted event into the reply header fetch instead of re-reading
state after the await, and drop a TODO tag.

* Fix image overflow in narrow panes, and tidy up timeline rows

The clamp stopping a reserved image box from overflowing its container lived in
new-timeline CSS, but the box is drawn by a shared component the legacy timeline
and thread panel use too, so images could overflow there. Moved it onto the
component's own link wrapper, beside the width that makes it necessary: a 757px
image in a 350px pane rendered at 757px before and 350px after, and still takes
its natural size when there is room.

Also, for the new timeline's rows: skip a row whose event has gone from the room
rather than letting the tile crash and lose the whole timeline, drop a leftover
placeholder that drew the word "Gap", and give the pagination spinner a label
screen readers can announce.

* Add vis baseline and padding for the labelled read marker story

The story had no left/right inset, so the label rendered flush against the edge
of the frame and was clipped. Pads it like the timeline does, and adds the
baseline image the visual test needs.

* Add tests for the new timeline panel, tile adapter and reply preview

Covers how the panel draws each kind of row — messages, the read marker, date
separators, the loading spinner, gaps — along with the layout fallback, edit
state scoping, and skipping a row whose event has gone. Also covers the tile
adapter's pass-through and the reply preview's three states (already loaded,
skeleton while fetching, and error).

The panel's tests stand in a stub for the virtualised TimelineView, which needs
real layout the test environment doesn't have and has its own tests.

* Show the read marker properly in its stories

The stories dropped the marker into a plain list with no room around it, so the
browser's list styling came along for the ride: every baseline had a bullet in
it, and the rule was pressed against the top edge of the frame, hard to see and
liable to be clipped by the marker's own 1px offset. The hidden-marker story was
a picture of a bullet and nothing else.

Resetting the list and insetting the stories draws the marker the way a timeline
does — a hairline, evenly inset, with room above it.

* Update the bubble media baseline for the reserved-size fix

The second media box in that story was being shrunk to fit its content (255px
wide) rather than taking the size the event declares; with the reserved box now
holding, it renders at 297px and its spinner and timestamp sit inside it instead
of overflowing. Every other story in the timeline suite is unchanged.

* Create new-timeline.spec.ts

* Move panel CSS to mx_NewTimelinePanel and use new DateSeperator

* fix tanstack virtial patch and remove reply clamping to 1 line

clamping the reply to one line is awkward and not real needed now that above the fold compensation works. it will visually expand when on screen.

One of our tanstack fixes was fixed upstreamed so can be removed.

* Update media---bubble---default-auto.png
2026-09-16 21:42:40 +00:00
Sirius
5f310305c0 Unified media preview 4 - upload confirm dialog uses the previews (#34892)
* collapsed and uncollapsed image tile

* using the new preview tiles in upload confirm dialog

* audio and video tag

* remove old urlpreview code

* force media width in confirm dialog to be max

* fixed upload audio upload preview failing, and hide preview box when preview fails

* undo the removal of the legacy mediabody

* cleaning up PR for review

* fixed oxlint issues

* regenerate stuff for end to end tests

* upload dialogue use tallbanner instead of full images

* updated snapshots

* updated snapshots

* regenerated snapshot

* fixed tests

* Cover the non-image previews in the upload dialog

computePreviewContent only ever ran its image case: the video, audio and
plain-file cases were untested.

Claude-Session: https://claude.ai/code/session_01Gr8zCXinjjNTmAHpZDGNtW

* applied some reviews

* moved conform dialog to be a functional component

* applied review comments

* Follow the renamed media preview entry API in UploadConfirmDialog

The entry discriminator is now `type` rather than `style`, and image
entries must carry `imageAlt` — the upload preview labels it with the file
name, matching the accessible name the pre-existing dialog gave the image.

* Regenerate image-upload-preview snapshot against the production build
2026-09-16 13:00:04 +00:00
Sirius
5c43a6e81e Unified media preview 1 - defining the shared components (#34889)
* collapsed media preview component

* expanded preview tile

* file body uses new previews

* collapsed and uncollapsed image tile

* using the new preview tiles in upload confirm dialog

* audio and video tag

* slapped on the styles for url previews as well

* uses css tokens

* refactored so all media types use the same tile declaration

* remove old urlpreview code

* refactored textpreview tile to be a normal media preview tile

* force media width in confirm dialog to be max

* only display displayable images

* fixed upload audio upload preview failing, and hide preview box when preview fails

* undo the removal of the legacy mediabody

* button hover style

* cleaning up PR for review

* add the collapse previews button back

* cleaning up the PR more

* fixed oxlint issues

* regenerate storybook screenshots

* give old media previews the same gaps as the new previews

* updated jest test

* claude fixed playwright tests

* generated even more images

* coverage tests

* regenerate stuff for end to end tests

* jest and linter fix

* added gaps between buttons and text

* pass lint

* upload dialogue use tallbanner instead of full images

* added comments

* added tallbanner variant to be used for upload confirm dialog

* cleaned up pr for reviews

* minwidth 0

* left group need some minwidth as well

* applied some of the reviews

* applied more reviews

* updated snapshots

* fixed vitest

* applied reviews

* image alt text is now required

* css tokens

* regenerated snapshot

* regen one more image
2026-09-16 10:27:16 +00:00
Michael Telatynski
45ec3ee841 Actually run stylelint on all EW pcss files (#35021)
* Actually run stylelint on all EW pcss files

And make them comply with the linter
Noticed as husky would run stylelint on all pcss files, but `lint:styles` would not.

* Delint
2026-09-15 13:20:35 +00:00
Zack
56281fc3a6 Move and MVVM ReplyTile to shared component (#34617)
* Move ReplyTile to shared component

* Add ReplyTile Storybook story

* Add ReplyTile visual snapshots

* Update ReplyTile snapshots

* Fix ReplyTile refactor

* Fix ReplyTile styles

* Update snapshot

* Use ReplyTile classes

* Fix ReplyTile layouts

* Refactor ReplyTile

* Fix ReplyTile sender

* Fix playwright images to refect the correct design

* Update snapshot

* Remove ReplyTile wrapper

* Remove ReplyTile layout context

* Remove ReplyPreview test

* Re-record reply screenshots after merging develop

The IRC baselines were capturing a bug. _EventTile.pcss had

    [data-layout="irc"] .mx_EventTile_line .mx_MTextBody {
        padding: 1px 0; display: inline-block;
    }

which reached into reply tiles, since the reply chain renders inside
mx_EventTile_line. At 0,3,0 it outranked ReplyTileView's
`.body > :where(*) { display: -webkit-box; -webkit-line-clamp: 2 }`
(0,1,0, as :where() adds no specificity), so reply previews never
clamped to two lines in IRC layout.

Develop's #34727 replaced that rule with .root[data-event-layout="irc"]
in the shared TextualBody module, and ReplyTileView reports the default
presentation, so it no longer applies inside replies. Reply blocks are
now tighter and the two-line clamp works.

The modern and bubble long-strings baselines move by 238px each from
develop's DisambiguatedProfile line-height/letter-spacing change. CI
never reported those because the IRC assertion runs first and aborted
the test.

* Re-record long strings reply screenshots

Regenerated all three baselines for the 'should send, reply, and display
long strings without overflowing' timeline test in a single run so they
stay mutually consistent.

* Update test by feedback request

* Fix ReplyChain snapshots

* Update apps/web/src/components/views/elements/ReplyChain.tsx

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>

* Update apps/web/src/components/views/rooms/ReplyPreview.tsx

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>

* Import useMatrixClientContext

* Add tsdoc to ReplyTileViewModelProps

* Chain reply tile sender locator

* Document ReplyTileViewModel

* Use role locator for reply file button

* Show reply avatar in long strings screenshots

---------

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
2026-09-15 08:32:39 +00:00
Sirius
e2555b7a8d min-width: 0 (#35048) 2026-09-11 15:34:37 +00:00
Michael Telatynski
3288fac6a6 Update opengraph image (#35042) 2026-09-11 10:54:16 +00:00
hayyaksi
6421c50610 Stop telling people leaving a restricted room that they need an invite to return (#34659)
* Stop telling people leaving a restricted room that they need an invite to return

The leave confirmation lists the complications of going, and one of them is that the
room cannot be re-entered. It decided that by testing the join rule against `public`
alone, so every rule that is not `public` got the same sentence: that you will not be
able to rejoin without an invite. For a restricted room that is untrue. Its whole
point is that membership of an authorised space is the ticket in, and Element itself
offers such a person a join button in the space hierarchy. Somebody who is still in
the space was being told they had burnt a bridge that is in fact still standing, in
red, at the moment they were deciding whether to leave.

Restricted rooms now get their own sentence naming the authorised spaces as the way
back, and spaces get the matching one, since a subspace is restricted whenever it is
created as anything other than public. It leads with what the room is - restricted -
rather than with what it is not: "not public" is the very fact that produced the wrong
advice, and opening with it again invites the same reading. Saying "cannot be rejoined
unless" in a single clause also spares the reader a contraction that is easy to
misread as its opposite when scanned or heard. The existing sentence is untouched and
still covers invite and knock: knocking would be the same kind of overstatement, but
asking to join sits behind a labs flag that is off by default, so for almost everyone
an invite really is the only route back into a knock room.

The new strings are added rather than the old ones reworded, so no existing
translation is invalidated.

* Keep the invite warning for a restricted room with no authorised spaces

Membership of an authorised space is what lets somebody back into a restricted
room, and the allow list is where those spaces are named. A restricted room
whose allow list is empty names none of them, so nothing can satisfy the rule
and the server turns every join away. Element already reads that state the same
way: the room's own security settings render zero allowed spaces as invite only.

The warning now asks for an authorised space rather than for the join rule
alone, so such a room keeps the sentence about needing an invite. Offering the
space route there would have been the same overstatement this branch set out to
remove, only pointed the other way.

Tests: two more cases in MatrixChat.test.tsx, a room and a space whose allow
list is empty, both asserting the invite sentence. The existing restricted cases
now carry an allow list, which is what a real restricted room has.

* Ask the leave question in the dialog's title, with the room's name

The confirmation opened on "Leave room" and then spent its first sentence asking
whether you were sure you wanted to leave the room you had just chosen to leave.
The title now carries the name and the question mark, which is where a reader
looks first, and the sentence that made redundant is gone.

That leaves the body to the warnings alone, so what is actually at stake — the
last member, the way back in, the only administrator — is the whole of it rather
than the tail of a sentence. A room with nothing to warn about has no body, and
an empty dialog body no longer holds open the 68px it was reserving for text.

Tests: both leave-dialog snapshots re-recorded, and the right-panel end-to-end
spec now looks for the dialog under the title it has.

* Size the leave dialog to its message

The question moved into the title, so the body is now a couple of short sentences at most and the
shared 60vw dialog width left them stranded in whitespace. The join failure dialog had the same
problem and was sized to its message; this follows it, down to the same 320px and the same
secondary text colour.

Colouring every sentence to match the Leave button left the dialog with no ordinary text in it at
all, so the red now sits only on the button that does the destructive thing. The empty-body rule
moves out of the shared stylesheet in the same change, since this is the only dialog it was ever
written for.

* Say a private room is private

"This room is not public" defines the room by what it is not, and "you will not be able to rejoin"
takes seven words to say "cannot". Element already calls this kind of room private everywhere else
it names it, so the warning now does too: "This room is private and cannot be rejoined without an
invite." That also puts it in step with the restricted sentence beside it, which was already built
on "cannot be rejoined".

The keys are unchanged, so the forty translations stay attached and follow the source through
Localazy. None of them go blank in the meantime, and the wording they carry today means the same
thing.

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2026-09-11 10:15:55 +00:00
Michael Telatynski
7c1c993d1b Redesign poll dialog busy state (#35020)
* Redesign poll dialog busy state

In order to fix its dark mode, see issue
Fixes #31876
Figma https://www.figma.com/design/MAUsalKv7bRRNKlAAigtNd/Community-contributions---Product-Polish?node-id=210-4021&t=stbsKUAxUXBGLvv0-0

* Improve coverage
2026-09-10 16:58:20 +00:00
Zack
dce3902a1b Implement PDF viewer to the current filepreview behind labs (#34928)
* Reopen the right panel when the current card is given new state

setCard's "same phase, new state" branch swapped the state of the card at
the top of the history but left the panel's own isOpen alone. If the panel
had been closed, the card was updated behind it and nothing appeared, so
the action looked like it had done nothing.

Show the panel in that branch, as the other two branches already do.

Also drop the unguarded index into a possibly empty history while we are
here: a matching phase can only have come from this room's own history, so
guard on it rather than trusting `hist[hist.length - 1]` to exist.

* Add a PDF viewer to the right panel

Open a PDF attachment in the right panel instead of having to download it
first. The viewer is pdf.js, rendering continuously with fit-to-width zoom,
pinch and ctrl-wheel zoom, and a page selector; the reading position is
remembered per file so reopening a document comes back where you left off.

The file body gains an optional "open" control next to download, which the
view model only asks for on PDFs shown as files in a timeline — not in an
export, and not in the download-only panels. FileBodyView renders nothing
for it otherwise, so every other file body is unchanged.

Encrypted attachments work unchanged: the handle the viewer is given reads
the bytes through MediaEventHelper's sourceBlob, which decrypts first.

The card stores only the source event id, so a restored panel re-derives
the media and drops the card if the event is no longer in the room.

* Add icon into the container instead of outside.

* Update translate + new images

* Load the right panel store lazily when opening a PDF

utils/pdfViewer is reached from the message body view models, and importing
RightPanelStore from it closed a cycle back to the message bodies:

  utils/pdfViewer -> RightPanelStore -> SDKContextClass -> LegacyCallHandler
    -> utils/notifications -> Unread -> EventTileFactory -> MessageEvent
    -> MVoiceOrAudioBody -> MVoiceMessageBody -> extends MAudioBody

MVoiceMessageBody evaluated `extends MAudioBody` while MAudioBody was still
initialising, so the whole MAudioBody suite failed to load with "Class
extends value undefined is not a constructor or null".

Pull the store in through a dynamic import instead, as StoresApi already
does for the room list store. Nothing else under views/messages or
viewmodels imports RightPanelStore, which is what kept this cycle closed
until now.

openPdfViewer becomes async as a result; the click handler fires it with
`void` since opening the panel is not something the caller waits on.

* Code split the PDF viewer behind Suspense

Importing PdfViewer from the card put pdf.js into every module graph that
reaches the right panel, so RoomView pulled it in and the Jest suites failed
to parse it:

  SyntaxError: Cannot use 'import.meta' outside a module

pdf.js locates its worker with `new URL(..., import.meta.url)`, which Jest
cannot compile, since it transpiles to CJS. That took out ExtrasApi-test and
anything else reaching RoomView.

Load the viewer with React.lazy instead, as the beacon components already do
for maplibre-gl. pdf.js is large and of no use until someone opens a PDF, so
keeping it out of the main bundle is worth doing on its own account; the Jest
suites stop seeing it as a side effect.

* Put the PDF viewer behind a lab

Ship the viewer disabled by default, as feature_pdf_viewer under the
Messaging group in Labs, alongside the other flags that change how message
content renders.

Two gates:

 - FileBodyViewModel leads showOpen with the flag, and showInlineDownload
   follows it, so with the lab off the file pill renders exactly as it did
   before: attachment icon, file name, no actions.

 - RightPanelStore drops a stored PdfViewer card when the flag is off, ahead
   of the check for a displayable event. Without it, someone who had a PDF
   open when they turned the lab off would get the viewer back on reload.

The existing "drops a PdfViewer card with no event to display" test now
enables the lab first; the gate had made it pass for the wrong reason.

* Drop the brand name from the PDF viewer lab

The i18n check forbids hardcoding "Element" in translations, since the brand
is configurable, and "Open PDFs in Element" tripped it:

  "labs|pdf_viewer": contains forbidden hardcoded word

Keys that genuinely need the brand are allow-listed in the i18n job, but this
one does not: the label reads fine without it, and naming a panel the viewer
happens to use is the description's job anyway.

* Update card header to show file name + change edit icon to expend icon on filepreview

* Update storybook images to reflect new icon

* Update apps/web/src/viewmodels/message-body/FileBodyViewModel.ts

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>

* Removed @link as it did not go anywhere.

* Drop the self-referential @link in PdfViewerState docs

* Comment why the file tile actions are hidden in forward previews

* Test the RightPanel PdfViewer phase renders the viewer card

* Take the PDF viewer lab as a prop on FileBodyViewModel

* Dispatch an action to open the PDF viewer instead of importing the store

* Check the PDF signature before parsing the attachment

* MVVM PDF Viewer

* Fix Sonar

* Cap decoded image size and pin XFA off when opening a PDF

* Update apps/web/src/i18n/strings/en_EN.json

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>

* Use the real SettingsStore for the PDF viewer lab in tests

* Read the PDF viewer lab in the view, not the view model

* Declare the mock viewer's page field with the others

* Use the minified pdf.js worker

* Allow clicking on links in the PDF.

* Force use of a PDF worker

* Reduce logging noise.

* Cap PDF size

* Check blob size too

* Add support for clicking links in the canvas

* Report file size

* Tests

* cleanup

---------

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>
Co-authored-by: Half-Shot <will@half-shot.uk>
2026-09-10 13:12:54 +00:00
rbondesson
9a8943e5ba Fix / Improve the EventTileView storybook (#34886)
* Move thread summary max-width application override to the shared component

* Make the storybook use more relevant data and also add descriptions for each story

* Fix the hover boundaries

* Apply application theme background colors when selection Element Web as global styling

* Updated screenshots

* Fixes for pre migration issues in Card view

* Better card and search stories. Also mimic the 'narrow' logic from ScopedRoomContext and view model in the storybook

* Add timestamps to all single event stories

* Updated snapshots

* Updated screenshots

* Only apply opacity for Search story in interactive mode

* Avoid the search result opacity to avoid a11y failure

* Simplified the tiles rendering logic
2026-09-03 10:25:57 +00:00
Will Hunt
41d818c6c6 Remove global button styling on mx_Dialog (#34833)
* Apply .mx_LegacyDialogButton to all buttons that rely on the cascading rule.

* Fixup a few cases of old rules winning out.

* update snaps

* More snapshots

* Also apply to input[type="submit"]

* Add snapshot tests for ones that miss coverage

* More coverage for the coverage gods

* stabilize

* Update SessionRestoreErrorDialog.test.tsx

* copyright update
2026-09-01 11:52:16 +00:00
rbondesson
3028880631 Fix/theme colors - Legacy color overrides where not applied to EventTileView (#34865)
* Apply all legacy theme color constants to the EventTileView

* Updated snapshots

* More snapshots
2026-08-31 18:03:17 +00:00
rbondesson
4dacb8b4ca Move EventTile to shared components - #5b (#34781)
* Align view model with the current EventTileView contract

* Use presentation context at every application call for EventTileView

* Render EventTileView from UnwrappedEventTile

* Remove the legacy styling

* Added missing receipt gutter logic

* Adding Card as a new shape, used in TimelineCard panel

* Added visual tests for the Card story

* Migrate EventTile layout styling and enhanced the story book rendering

* Migrate preview, notification, thread, and file shapes

* Use MemberAvatar and RoomAvatar in storybook

* Make slot styling self-contained and clean up corresponding application styling

* Clean up action bar styling

* Clean up event tile styling overrides

* RoomView realted styling fixes

* Adjustments for threads list

* Adjustments to File view

* Adjustments to Card shape

* More styling cleanup

* Updated Playwright test selectors

* Update the story book with more relevant data

* Fix oxlint and update story screenshots

* Restored conditional notification-badge slot rendering to avoid empty wrappers

* Fixed tests and snapshots

* Remove the unused UnreadNotificationBadge

* Updated snapshots and removed a11y failing story test

* Fix test locator hooks

* Fixed styling and logic issues in appearance settings

* Fixes to slotSender and slotBody placement for group layout

* Reorder style sheet for better readability

* Fix for spacing between slotSender, slotBody and reply-chain

* Fix more Playwright test loacators

* Clean up event tile class overrides

* Fix the slotBody styling

* More clean up of unused locators

* Styling fixes

* Fixes to test locators and styling for bubble and irc layout

* Locator fixes, updated snapshots, layout adjustements

* Fix for selected and editing markers and timestamp pos

* Styling fixes to bubble layout. Also dismiss releaseAnnouncements

* Styling updates and dont wait for release announcements anymore

* Styling adjustments

* Make the Playwright test wait for UI to settle before taking screenshot.

* Updated screenshot - the only difference is scroll pos

* Fixing test locators and styling adjustments

* Updated the test to reflect ellipsis, hence the screenshot update

* More test locators and styling adjustments

* Updated snapshots and screenshots

* More locator fixes and snapshots

* Restore the original screenshot

* Added Search result specific styling and make reply chain density aware

* Test locators and styling adjustments

* More styling fixes

* Updated snapshot

* oxfmt

* Styling adjustment and updated snapshots

* Styling adjustement

* Restore the font-12px override on thread view ViewSourceEvent

* Adjust styles for thread view bubble layout of ViewEventSoucrce

* More adjustments

* Styling adjustments

* Fix the bubble  layout thread view event source alignment

* Fix the thread view without destroying the room view

* Removing !important

* Updated snapshots after merge

* Put back !important until MemberAvatarView is actually used in event tiles

* Updated snapshots

* Use hideJumpToBottomButton to avoid flakiness

* More hideJumpToBottomButton
2026-08-31 04:28:13 +00:00
David Baker
73a21cc78f Add user status in timeline pills (#34832)
Which, naturally, are entirely separate to composer pills.
2026-08-27 14:20:56 +00:00
hayyaksi
ba8945a704 Report a failure to update notification settings to the user (#34521)
Saving a notification setting could fail without saying so: reconciliation
runs in the background and a rejection went nowhere, so a change the user
had made simply did not take effect and nothing on screen admitted it. The
tab now surfaces the failure, and offers a Try again action rather than
telling the user to try again without giving them any way to do so.

Making that action honest took two fixes underneath it. The queue that
serialises reconciliation chained each task onto the fulfilment of the
previous one, so a single rejection parked a rejected promise at the head
of the queue permanently: every later task skipped its own body and
re-reported that first error, which meant nothing could be retried and no
further settings change would take effect until the tab was reopened.
Chaining onto settlement instead lets the queue carry on.

The cached copy of the server's push rules was also only refreshed after a
successful apply. Changes are applied in stages, so a failure part-way
through leaves earlier stages committed while the cache still describes
the state before them; a retry would recompute the same diff and ask the
server to delete rules it had already deleted, which it answers with a
404. Re-reading the rules when an apply fails lets the retry pick up the
work that is actually outstanding. The user's own choice is deliberately
left untouched by that refresh, since that is what the retry re-applies.

The error is also cleared when a reconciliation succeeds and not only when
one starts, so a change that went through cannot leave the failure notice
standing behind it.

The notice itself is spaced away from the labs banner below it, and its
action sits beside the message while there is room and drops underneath
once there is not, instead of at the fixed breakpoint Compound would
otherwise use. Compound tints only the icon on a critical alert and leaves
the message in the default body colour, so the message takes the semantic
critical text token and reads like warnings elsewhere; the action is a
sibling of it and keeps its own colour.

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-08-26 10:26:01 +00:00
hayyaksi
5fddfd24e9 Stop blaming permissions when the homeserver refuses an invite (#34590)
* Say why the homeserver refused an invite instead of blaming permissions

Every M_FORBIDDEN from an invite was reported as "You do not have
permission to invite people to this room", and the rest of the batch was
abandoned on the spot. That is only one of the reasons a server has for
refusing: the invitee may have invites switched off, or their homeserver
may not accept ours. Someone who did have the power to invite was told
they did not, and given nothing to act on.

The claim is now checked before it is made. If the room says we cannot
invite, the message and the abandoned batch are exactly as they were,
because that will hold for everyone in the batch. If we can invite, the
server's own reason is shown instead and the remaining invites are still
attempted, since a refusal aimed at one person says nothing about the
next.

The unfederated case comes out of the same branch unchanged. It was
reached through a nested conditional that had to spell itself out twice,
once for rooms and once for spaces, and reads better checked first.

Tests: a refusal we are allowed to send through now surfaces the server's
text and lets the batch continue, while a genuine lack of power still
gives the permission message and stops.

* Drop the homeserver's own wording from the invite refusal message

The reason a server gives for an M_FORBIDDEN comes back in English whatever
language the client is running in, so showing it leaves the user reading a
sentence they may not understand inside an otherwise translated dialog.
Nothing else in the app passes server text through to the UI.

The message now says only that the invite was refused, which is fully
translatable. The reason is not lost: the catch block already logs the whole
error, so it stays in the console and in a rageshake, where an untranslated
string does no harm and is where anyone diagnosing the refusal would look
anyway.

The rest of the change stands. Permissions are blamed only when the room says
we genuinely cannot invite, and a refusal aimed at one invitee no longer
abandons the rest of the batch.

* Reword the dialog that lists the invites a batch could not send

The dialog headed "Some invites couldn't be sent" goes on to say "We sent the others, but the below
people couldn't be invited to <room>". "Some" reads as more than one, though a batch of two can fail
on exactly one; "below" names the list by where it sits on the screen rather than by what it is; and
the room name is repeated back from the very room the user opened the invite dialog in.

The heading now reports what happened rather than what was possible, the list is introduced as the
following people, and the contractions are spelled out. The room name goes with them: nothing reaches
this dialog except invites the user has just sent into a room they are already looking at.

Dropping the name drops the RoomName substitution too, which is why the key is renamed rather than
reworded in place. The forty translations still carry the tag, and under the unchanged key they would
render it as literal text until each language is retranslated.

* Report a refused invite in place of the invite dialog, not on top of it

Design review asked for the failure report to take the invite dialog's place:
once the batch has run there is nothing left to go back to, because everyone who
could be invited already has been. The dialog now finishes before the report
opens, which also lets showAnyInviteErrors stop doubling as the decision about
whether to close it.

The reason line now reads as the reason rather than as a report of machinery.
The client cannot tell a person's own invite filter apart from a server that
refuses invites wholesale — both come back as a bare M_FORBIDDEN with nothing to
key off — so the wording states the effect, which is the same either way, rather
than guessing at a cause the way the permission message used to.

The report is capped at 600px, and each reason starts at the avatar's left edge
with a space above it, so it reads as a note about the person rather than as a
third line of their name.

* Keep the refusal report's own button inside its 600px box

BaseDialog puts the fixed-width class on the focus lock inside .mx_Dialog rather
than on .mx_Dialog itself, so capping the outer box alone left the body at its
old 704px and hung the OK button 144px past the right edge — measured in the
browser, not guessed. Pinning the outer box and letting the body size to it puts
them back together, and the viewport cap keeps a narrow window from overflowing.

* Stop the refusal line from pushing a name away from its avatar

Spanning the reason across both columns made it a grid item of the avatar
track, and that track was sized max-content, so the longest reason set the
avatar column's width and left a 158px hole between each avatar and its name.
Sizing the track from the avatar itself takes the reason out of that
calculation; the gap is back to the grid's own 12px and the reason still starts
at the avatar's left edge. The avatar size now has one definition instead of
being repeated as the track width.

* State each refusal once, under the people it applies to

Repeating an identical sentence under every name read as three failures with
three causes rather than one cause with three people behind it, and the eye had
to compare the lines to find out which. The failures are now grouped by the text
they would have printed, in the order the batch hit them, and each group says
its reason once beneath its own list.

Grouping also takes the reason out of the tile, which undoes the layout work the
earlier rounds needed: the tile is back to upstream's avatar-and-name grid, so
the spanning item that was inflating the max-content avatar column is gone along
with the track override that compensated for it. Spacing now comes from the
group — 8px between people, 16px above the reason — instead of a margin on every
tile.

* Play down the reason an invite could not be sent

The reason sat under the people it applies to in alert red, at the size of their names, which reads
as something gone wrong that the reader must act on. It is neither: nobody can do anything about a
homeserver turning an invite down, and the invites that did go out are unaffected. It now takes the
size and colour of the tertiary lines around it, so it explains without competing with the names
above it.

The reasons themselves repeated a subject the list had already given. Each one sits directly under
the person it describes, so "User is already in the room" said "user" twice over; the already-invited
and space wordings carried the same redundancy and lose it too. "They are not accepting invitations"
becomes "Not accepting invites", in the terser register the app already uses for a state the reader
cannot change.

* Keep the refusal reason in alert red

The reason was toned down to tertiary size and colour on the assumption that a homeserver turning an
invite down is informational. Review settled the opposite: the layout americanrefugee approved states
the reason once beneath the people it applies to, and at that position it is the only thing telling
the reader those invites did not happen. Tertiary grey at 12px reads as a caption on the list above
it rather than as the outcome, so it goes back to the alert red and the size of the names it explains.

The wording simplifications stay. Dropping the redundant subject was a separate ask and does not
depend on how the line is styled.

* Put the refusal reason above the people it applies to

The reason trailed the list it explains, so a reader met the names first and had to look back up once the
explanation arrived. Leading with it makes the line a heading for its group, and the 8px that had held it
clear of the names above now holds it clear of the names below.

A heading takes a colon rather than a full stop, but the reason is a translated string that the
single-invite failure dialog, the /invite command and the user-info invite button each show on its own,
where a trailing colon would dangle. The colon is appended where the report renders it instead, which in
turn means every reason has to arrive without a terminal stop: leave one on the wordier reasons this
branch did not introduce and they read ".:" as soon as two differently-caused failures land in the same
report. The four "already in the room" reasons line up with their existing translations again by losing
it, since upstream's English never carried one. The longer sentences will show ".:" in the locales that
have already translated them until Localazy picks the change up, which is the cheaper blemish; renaming
those keys to dodge it would throw away thirty-six translations apiece.

The alert red stays. TemoCrevlis read the line as informational and asked for tertiary grey; review
settled it the other way, on the grounds that it is the only thing on screen saying why those invites did
not happen, and red is the likelier to be read.

* Style the invite error dialog with Compound tokens

The refusal reason and the summary heading above it were still reaching
for the legacy $font-15px, $alert and $secondary-content Sass variables.
Each of those resolves to a Compound token one-for-one in every theme, so
naming the token directly costs nothing at render time and stops the
dialog depending on the compatibility layer.

The heading collapses into the --cpd-font-body-md-regular shorthand,
which already carries the regular weight and Compound's 1.5 line height.
That trades the hand-written 24px leading for 22.5px, the value Compound
intends for body-md text, rather than keeping a legacy variable alive
just to preserve a leading nobody chose deliberately.
2026-08-26 09:03:30 +00:00
Will Hunt
0368e7470b Remove global styles for input[type="password] (#34806)
* Remove input[type="password"] rules

* Apply to relevant css classes

* Fix one dialog

* one more focus change
2026-08-25 13:37:54 +00:00
Will Hunt
68f0393a75 Remove global styles for fieldset, legend (#34805)
* Remove fieldset,legend from _common.pcss

* Apply rules directly to components.
2026-08-25 12:50:05 +00:00
Will Hunt
511e0427a2 Promote Custom Themes labs feature to devtools (#34703)
* Move custom themes to devtools

* Move devtools test to vitest

* Remove old tests

* Remove unrequired param

* Remove unused imports

* Migrate to vitest + update LabsUserSettingsTab

* Update snap

* Do not awaity

* Handful of updates

* fmt

* Update tests

* update settings labels again

* one last snap update
2026-08-24 08:28:09 +00:00
Florian Duros
2d1c4c798e Room list: add room selection on section creation/edition (#34387)
* Add to shared components the content of section creation dialog

* Add a vm for the section creation view

* Use new vm and view in section creation dialog

* Clean up lang, test and css

* Move section name step into its own component

* Add a room picker view component

* Use room picker in section creation

* Add shared component screenshots

* Can add rooms when creating or editing a section

* Fix rich list taking too much space

* Fix exisiting e2e test

* Wire up the the selected room when creating the section from the room item menu

* Add e2e tests

* Rename "edition" to "editing"
2026-08-20 08:12:47 +00:00
rbondesson
d03b0cff0f Fix - Make shared components layout aware (#34727)
* Make components layout aware and fix IRC styling

* Move styling to shared components

* Move styling to shared and clean up unused application overrides

* Fix linter errors and update snapshots and screenshots

* Add missing EventPresentationContextProvider ad adjust IRC styling

* IRC liine height fix

* The lesser of two evils for IRC layout issue

* Fix IRC layout for ViewSourceEvent

* Cleanup application styling overrides and add bubble and compact styling to DisambiguatedProfile

* Udpated screenshots for irc layout

* Let the parent position DisambiguatedProfile in bubble layout. More screenshot updates for IRC layout

* Updated screenshots

* Updated screenshot
2026-08-19 13:36:27 +00:00
Prince Panchani
8df56706b5 Fix illegible text on hovered Spotlight results in high-contrast theme (#34465)
* Fix illegible text on hovered Spotlight results in high-contrast theme

* Update Spotlight tests to use Axe for accessibility checks instead of custom contrast ratio calculations

* Refactor Spotlight accessibility tests to cover all themes and improve legibility checks across various UI elements

* Remove high-contrast theme from Spotlight legibility tests to streamline accessibility checks

* Fix invalid CSS comment breaking Oxfmt and scope out unrelated axe violation

_SpotlightDialog.pcss used // line comments, which are not valid CSS and
crashed the Oxfmt parser, causing spurious failures in Oxfmt and the
Static Analysis gate. Switched to a block comment.

The new Spotlight legibility test's broad axe scans also caught a
pre-existing nested-interactive violation (endAdornment buttons nested
inside clickable option rows), unrelated to the colour-contrast fix.
Scoped it out with axe.disableRules("nested-interactive") to keep the
contrast checks active without failing on the unrelated issue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Update Room Directory screenshot baseline for Spotlight hover-color fix

The high-contrast legibility fix changes hover/selected text color in
Spotlight results, which is reflected in the Room Directory dialog
screenshot test.

* Use CI-rendered baseline for the Room Directory screenshot

The previous baseline still had the pre-fix colours for
.mx_SpotlightDialog_result_publicRoomAlias and
.mx_SpotlightDialog_result_publicRoomDescription, so the Linux CI run
reported a 418px diff on filtered-one-result.png. Replace it with the
image the Linux CI runner actually produced with the fix applied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Retrigger CI (transient Electron install failure in Vitest job)

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 14:27:35 +00:00
Michael Telatynski
eadc43ddf4 Enable oxlint rule typescript/no-floating-promises (#34586)
* Enable oxlint rule `typescript/no-floating-promises`

* Iterate
2026-08-13 10:24:13 +00:00
hayyaksi
03faca3626 Open the quick settings theme menu upwards so every theme is reachable (#34501)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-08-12 11:22:55 +00:00
hayyaksi
9e8a57c81c Use the dark code highlighting stylesheet in the dark-custom theme (#34491)
dark-custom imported highlight.js's atom-one-light palette while every other
dark theme imports atom-one-dark, so syntax-highlighted code blocks rendered
dark grey on the theme's dark background. The neighbouring import on the next
line already pulls in the dark GitHub markdown stylesheet.

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-08-11 16:04:33 +00:00
hayyaksi
46d55f25a2 Tell the user when registration is being rate limited (#34519)
* Tell the user when registration is being rate limited

* Disable the submit button while registration is rate limited

The warning now sits with the button it is disabling, below the server picker,
rather than above it, so it reads as the reason you cannot continue.

The rate limit message moves out of errorText into its own state field to get
there, which also leaves the position of every other registration error alone.

When the server tells us how long to wait, we ask again ourselves once that
time is up, so the form comes back without the user having to guess when a
reload is worth it.

* Set the rate limit warning to medium weight

Scoped to the registration rate limit warning so the other auth errors keep
the bold they have always had.
2026-08-11 15:39:39 +00:00
hayyaksi
22b0758bfb Round the room preview dialog like everything around it (#34597)
The preview shown for a room you have not joined — the one carrying the
join and ask-to-join buttons — has 4px corners, where the room preview
card it stands in place of, the panel form of the same bar, and the
surfaces beside it all have 8px. Sitting among them it reads as sharp,
which is what was reported.

Nothing else in the rule changes, and the panel form already had the right
value, so the two forms of the bar now agree with each other as well.

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2026-08-10 09:24:26 +00:00
Zack
ce74b5f614 Refactor NotificationBadge to shared MVVM (#33843)
* Refactor NotificationBadge to shared MVVM

* Added snapshot images

* Replace legacy NotificationBadge with adapter

* Fix Eslint

* Use regular NotificationBadge prop names

* Remove legacy NotificationBadge selectors

* Update TAC badge Playwright selectors

* Rename NotificationBadgeAdapter to NotificationBadge

Addresses review feedback: the component is the app-facing badge, not an
adapter, so name it NotificationBadge.

* Make NotificationBadgeView isClickable a required boolean

Addresses review feedback: there is no semantic difference between
`isClickable: false` and `isClickable: undefined` (both render the
non-interactive variant), so the field should be a plain boolean. The
view model always provides it.

* Drop redundant comparison guards in NotificationBadge view model setters

Addresses review feedback: Snapshot.merge already compares each field
with Object.is and only emits when something actually changed, so the
manual equality guards in each setter were redundant. Recompute the
snapshot unconditionally and let merge dedupe.

* Resolve notification badge labels inside NotificationBadgeView

Addresses review feedback: static UI labels belong in the view, not the
snapshot. The view now resolves the knock and unsent-message strings via
useI18n, and the snapshot exposes a showUnsentTooltip boolean instead of
a prebuilt tooltipLabel (and drops knockLabel entirely). Adds the
room|knock_sent and notifications|message_didnt_send keys to the
shared-components i18n catalogue so they resolve in tests and storybook.

* Remove unused translation because of shared components movement

* Fix typescript issue

* Fix lint issue

* Remove unused notification badge count setting

* Pass notification badge class names explicitly
2026-08-10 08:32:23 +00:00
Prince Panchani
9826a664dc Increase Threads panel header height to 64px to match Pinned Message banner (#34566)
* Increase Threads panel header height to 64px to match Pinned Messages banner

* Add test to verify Threads panel header height matches Pinned Messages banner height

* Update Threads panel screenshot to reflect recent design changes
2026-08-07 16:50:34 +00:00
Adi-Beker
f6e88f4699 fix: align room timeline scrollbar to viewport edge (#33788)
* fix: align room timeline scrollbar to viewport edge

* update many screenshots

* Lots more screenshots

* screenshots

* Manual screenshot update

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2026-08-07 14:02:11 +00:00
hayyaksi
eab7704b98 Keep emoji at code size inside code blocks so line numbers stay aligned (#34497) 2026-08-05 14:50:25 +00:00
David Baker
a4f63cdd22 Emoji picker to shared components (#34337)
* Exploration of a virtuoso-powered emoji picker

moved to shared components

Fable generated

* fix pnpm lock

* format & fix some lint issues

* wrong import

* fix lint warning

* Fix off-by-one

and remove manual overflow adjustment: let's leave the default unless
it turns out to be necessary. Emoji should not take that long to load.

* Convert to functional component

* WIP: change to one big virtuoso scroller

* Change to use virtuoso's own onRangeChanged

and santitise category data and how it's passed around

* Convert Tabs to functional component

and put the focusing behaviour back with it just keeping track of
refs by itself.

* Absorb two line config file into main component

* Actually add the config to the main file

* Convert emoji to functional

Also make selected always defined and use useCallback.

* QuickReactions to functional component

* Non-default exports & doc

* Search to functional component

* Well it seems to work just fine now

* Use ref prop

* fix lockfile AGAIN

* lint

* Remove default export

* Remove some mx_ classnames and fix the inputRef

to make the arrow keys in the search box work (well, work as much as
they ever did).

* Remove last of the mx_ id / classnames

(except the one in the test)

* Use useMemo to memoize

* No need to export props interface (I think?)

and fix comment now we don't do the mutation stuff anymore

* Fix test

* Fix axe violations & add screenshots

* Avoid comparing dom snapshots in test

* Allow more before or after, just compare order of the ones present in both.

* Switch existing usages to new emoji picker

and kill the old one with fire

* Unused stuff

* Remove i18n strings

* Fix some tests

* Update screenshots

* Fix test

by removing the last of the weird memoized-but-mutated data structure

* Move the string somewhere more sensible than 'a11y'

* i18n lint

* Give the emojis IDs so aria-activedescendant works

* Fix more tests

* Add a small wrapper emoji picker component

This lets us easily memoize the recent emojis when the emoji picker is opened.
Also it saves a bit of boilerplate.

* Remove old emojipicker css

* Typos

Co-authored-by: David Langley <davidl@element.io>

* Use compound constants

* Rethemendex

* Use catalog version for emojibase

* Add comments

* More comments

* Fix comment

* More comments

* more comments (and make them uniform)

* More comments

* Fix pnpm lock again

* Another comment

* Apply button types to new version

* Add comment

* Disable screenshot

as per comment

---------

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>
Co-authored-by: David Langley <davidl@element.io>
2026-08-05 13:44:35 +00:00