Compare commits

...

41 Commits

Author SHA1 Message Date
Robin
a6a02778a1 Performance: Avoid re-rendering entire spotlight layout so often
This removes a lot of unnecessary jank in spotlight layout when swapping individual speakers in and out of the spotlight tile.
2026-08-10 11:16:35 +02:00
renovate[bot]
978ffee183 Update ghcr.io/element-hq/element-web:develop Docker digest to a23cced (#4155)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-07 09:28:35 +02:00
renovate[bot]
2d64cf4b30 Update ghcr.io/element-hq/element-web:develop Docker digest to 6fb12e1 (#4154)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-06 09:38:48 +02:00
Robin
eb65a28a75 Merge pull request #4146 from element-hq/performance
Performance: Enable React Compiler
2026-08-05 11:10:40 +02:00
Robin
9d400323d0 Clarify why certain tests disable the React Compiler 2026-08-05 10:54:49 +02:00
renovate[bot]
6d7d3093b3 Update ghcr.io/element-hq/element-web:develop Docker digest to b4e2d2d (#4149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 10:16:05 +02:00
Robin
2b07e9e822 Merge pull request #4148 from element-hq/renovate/i18next-26.x
Update dependency i18next to v26
2026-08-04 22:49:21 +02:00
renovate[bot]
7ae1fc6cd8 Update dependency i18next to v26 2026-08-04 17:21:53 +00:00
Robin
608695fc95 Performance: Enable React Compiler
With the React Compiler, our component code is transformed at build time to automatically apply various forms of memoization. This changes the runtime semantics of our code a little bit and therefore could surface new bugs in the next release cycle in case any components fail to follow the rules of React. See https://react.dev/learn/react-compiler for more information.

This results in a small bundle size increase and modest performance gains, but I think it's worth it given the potential for larger performance gains once we tune the component structure a bit more.
2026-08-04 17:57:23 +02:00
Robin
a08a577e18 Merge pull request #4138 from element-hq/renovate/all-minor-patch
Update all non-major dependencies
2026-08-04 17:19:28 +02:00
Robin
55b2d2b130 Remove possibility for CI Playwright version to get out of sync 2026-08-04 17:04:07 +02:00
renovate[bot]
5b84b2c04d Update ghcr.io/element-hq/element-web:develop Docker digest to 3daefff (#4116)
* Update ghcr.io/element-hq/element-web:develop Docker digest to 0183c0a

* Update end-to-end tests to work with auto-collapsing room list

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Robin <robin@robin.town>
2026-08-04 12:35:18 +00:00
Robin
3af0744324 Merge branch 'livekit' into renovate/all-minor-patch 2026-08-04 14:20:13 +02:00
Robin
07567ed31d Merge pull request #4118 from element-hq/renovate/livekit-components
Update LiveKit components
2026-08-04 14:05:47 +02:00
Robin
d6e097476e Address linter finding 2026-08-04 13:51:56 +02:00
Robin
540eb306f4 Fix unit tests 2026-08-04 13:48:52 +02:00
Robin
f62428ba0d Update code formatting 2026-08-04 13:43:38 +02:00
renovate[bot]
d579169d54 Update all non-major dependencies 2026-08-04 11:39:13 +00:00
Robin
8daaa649eb Merge pull request #4147 from element-hq/renovate/npm-postcss-vulnerability
Update dependency postcss to v8.5.23 [SECURITY]
2026-08-04 13:36:56 +02:00
Robin
2d5a4eefec Merge pull request #4139 from element-hq/renovate/posthog
Update dependency posthog-js to v1.408.2
2026-08-04 13:26:18 +02:00
renovate[bot]
d550430c8a Update dependency postcss to v8.5.23 [SECURITY] 2026-08-04 11:11:52 +00:00
Robin
78724acfcf Merge pull request #4140 from element-hq/renovate/compound
Update dependency @vector-im/compound-web to v9.9.0
2026-08-04 13:10:48 +02:00
Robin
20c7112812 Merge pull request #4141 from element-hq/renovate/matrix-widget-api
Update dependency matrix-widget-api to v1.18.0
2026-08-04 13:09:58 +02:00
renovate[bot]
a7c996f3ea Update dependency posthog-js to v1.408.2 2026-08-04 11:07:26 +00:00
Valere Fedronic
4fbcbb4849 Merge pull request #4136 from element-hq/valere/fix_4132
fix: FF mirrored self video lose rounding
2026-08-03 13:53:04 +02:00
Robin
74102ac6c2 Merge pull request #3075 from hughns/defaut-server-config
Remove default server config as it is not likely to be correct
2026-08-03 10:49:49 +02:00
Robin
11a6fa9b45 Fix tests 2026-08-03 10:32:52 +02:00
Robin
507a6f29a5 Merge branch 'livekit' into defaut-server-config 2026-08-03 10:29:41 +02:00
renovate[bot]
e725bc5de3 Update dependency matrix-widget-api to v1.18.0 2026-08-03 03:03:18 +00:00
renovate[bot]
c15d4c50a1 Update dependency @vector-im/compound-web to v9.9.0 2026-08-03 03:02:53 +00:00
Valere
e1bc4a096b fix: FF mirrored self video lose rounding 2026-07-31 12:20:32 +02:00
renovate[bot]
b524de9298 Update LiveKit components 2026-07-30 20:51:22 +00:00
Robin
bfb5fb922f Merge pull request #4134 from element-hq/renovate/npm-postcss-vulnerability
Update dependency postcss to v8.5.18 [SECURITY]
2026-07-30 11:01:41 +02:00
renovate[bot]
3f4d55343a Update dependency postcss to v8.5.18 [SECURITY] 2026-07-30 08:45:15 +00:00
Robin
73d8d2c2c3 Merge pull request #4120 from element-hq/johannes/i18n-download-fixes
Apply correct GH label and fix ordering issue in i18n download
2026-07-30 10:45:09 +02:00
Robin
fcfe73c50d Merge pull request #4117 from element-hq/renovate/livekit-client
Update dependency livekit-client to v2.21.0
2026-07-30 10:43:24 +02:00
renovate[bot]
5564015a8b Update dependency livekit-client to v2.21.0 2026-07-28 18:56:05 +00:00
Robin
62eeb10d38 Merge pull request #4130 from element-hq/landscape-default-grid
Only show layout switch when it has an effect on the layout
2026-07-28 15:28:06 +02:00
Johannes Marbach
46cb89b232 Apply correct GH label and fix ordering issue in i18n download 2026-07-20 17:38:14 +02:00
Timo
2a3271b8b3 Merge branch 'livekit' into defaut-server-config 2025-04-04 23:27:45 +02:00
Hugh Nimmo-Smith
f45aa85aef Remove default server config as it is not likely to be correct 2025-03-10 18:14:25 +00:00
20 changed files with 2113 additions and 2357 deletions

View File

@@ -7,10 +7,6 @@ jobs:
vitest:
name: Run unit tests
runs-on: ubuntu-latest
container:
# Make sure to grab the latest version of the Playwright image
# https://playwright.dev/docs/docker#pull-the-image
image: mcr.microsoft.com/playwright:v1.61.1-noble
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@@ -26,6 +22,18 @@ jobs:
- name: Install dependencies
# ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present)
run: "pnpm install --frozen-lockfile --ignore-pnpmfile"
- name: Get Playwright version
run: echo "PLAYWRIGHT_VERSION=$(pnpm list @playwright/test --depth=0 --json | jq -r '.[0].devDependencies["@playwright/test"].version')" >> $GITHUB_ENV
- name: Cache Playwright binaries
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
id: playwright-cache
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
- name: Install Playwright binaries
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm playwright install --with-deps
- name: Vitest
run: "pnpm run test:coverage"
- name: Upload to codecov
@@ -52,8 +60,18 @@ jobs:
- name: Install dependencies
# ignore-pnpmfile should never be commited. Make CI crash if it happened (`pnpmfileChecksum` is present)
run: pnpm install --frozen-lockfile --ignore-pnpmfile
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
- name: Get Playwright version
run: echo "PLAYWRIGHT_VERSION=$(pnpm list @playwright/test --depth=0 --json | jq -r '.[0].devDependencies["@playwright/test"].version')" >> $GITHUB_ENV
- name: Cache Playwright binaries
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
id: playwright-cache
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
- name: Install Playwright binaries
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm playwright install --with-deps
- name: Run backend components
run: |
docker compose -f docker-compose-dev.yml -f docker-compose-playwright.yml pull

View File

@@ -40,6 +40,11 @@ jobs:
- name: Fix the owner of the downloaded files
run: "sudo chown runner:docker -R locales"
# Localazy doesn't write file contents in the same order that i18next-cli's
# extractor uses. We re-run the extractor after downloading to fix the order.
- name: i18n
run: pnpm i18n
- name: Formatting
run: pnpm format
@@ -53,7 +58,7 @@ jobs:
title: Localazy Download
commit-message: Translations updates
labels: |
T-Task
PR-Task
- name: Enable automerge
run: gh pr merge --merge --auto "$PR_NUMBER"

View File

@@ -47,7 +47,7 @@ services:
- ecbackend
livekit:
image: livekit/livekit-server:v1.10.1
image: livekit/livekit-server:v1.13.4
pull_policy: always
hostname: livekit-sfu
command: --dev --config /etc/livekit.yaml
@@ -70,7 +70,7 @@ services:
- ecbackend
livekit-1:
image: livekit/livekit-server:v1.10.1
image: livekit/livekit-server:v1.13.4
pull_policy: always
hostname: livekit-sfu-1
command: --dev --config /etc/livekit.yaml

View File

@@ -13,7 +13,7 @@ services:
- ./backend/playwright_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
element-web:
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
image: ghcr.io/element-hq/element-web:develop@sha256:d1841828a390d96da2ee64232949db641f11766fb506ec561374819d10533b9a
image: ghcr.io/element-hq/element-web:develop@sha256:a23cced80588306c66bc7cfa859c591be03ece31f25d0a6a2e1e935943a35c92
element-web-1:
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
image: ghcr.io/element-hq/element-web:develop@sha256:d1841828a390d96da2ee64232949db641f11766fb506ec561374819d10533b9a
image: ghcr.io/element-hq/element-web:develop@sha256:a23cced80588306c66bc7cfa859c591be03ece31f25d0a6a2e1e935943a35c92

View File

@@ -86,10 +86,11 @@ to implement
In the context of MatrixRTC, we suggest using a single hostname for backend
communication by implementing endpoint routing within a reverse proxy setup. For
the example above, this results in:
| Service | Endpoint | Example |
| -------- | ------- | ------- |
| Service | Endpoint | Example |
| --------------------------------------------------------------------------------- | -------------- | ------------------------------------ |
| [Livekit SFU](https://github.com/livekit/livekit) WebSocket signalling connection | `/livekit/sfu` | `matrix-rtc.example.com/livekit/sfu` |
| [MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service) | `/livekit/jwt` | `matrix-rtc.example.com/livekit/jwt` |
| [MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service) | `/livekit/jwt` | `matrix-rtc.example.com/livekit/jwt` |
Using Nginx, you can achieve this by:

View File

@@ -54,6 +54,7 @@
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-visually-hidden": "^1.0.3",
"@react-spring/web": "^10.0.0",
"@rolldown/plugin-babel": "^0.2.3",
"@sentry/react": "^8.0.0",
"@sentry/vite-plugin": "^3.0.0",
"@storybook/addon-docs": "^10.3.6",
@@ -80,13 +81,14 @@
"@vitejs/plugin-react": "^6.0.2",
"@vitest/browser-playwright": "^4.1.5",
"@vitest/coverage-v8": "^4.0.18",
"@vitest/ui": "4.1.9",
"@vitest/ui": "4.1.10",
"babel-plugin-react-compiler": "^1.0.0",
"classnames": "^2.3.1",
"copy-to-clipboard": "^3.3.3",
"eslint-plugin-element-call": "link:eslint",
"eslint-plugin-storybook": "^10.3.6",
"fetch-mock": "11.1.5",
"i18next": "^25.0.0",
"i18next": "^26.0.0",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-cli": "^1.61.0",
"jsdom": "^26.0.0",
@@ -99,13 +101,13 @@
"node-stdlib-browser": "^1.3.1",
"normalize.css": "^8.0.1",
"observable-hooks": "^4.2.3",
"oxfmt": "^0.56.0",
"oxfmt": "^0.61.0",
"oxlint": "^1.70.0",
"oxlint-tsgolint": "^0.23.0",
"oxlint-tsgolint": "^0.25.0",
"pako": "^2.0.4",
"postcss": "^8.4.41",
"postcss-preset-env": "^10.0.0",
"posthog-js": "1.374.0",
"posthog-js": "1.408.2",
"qrcode": "^1.5.4",
"react": "19",
"react-dom": "19",

View File

@@ -36,6 +36,7 @@ widgetTest("Footer interaction in PiP", async ({ addUser, browserName }) => {
await valere.page.waitForTimeout(600);
// Switch to the other room, the call should go to PIP
await TestHelpers.expandRoomList(valere.page);
await TestHelpers.switchToRoomNamed(valere.page, "OtherRoom");
// We should see the PIP overlay

View File

@@ -53,6 +53,7 @@ widgetTest("Put call in PIP", async ({ addUser, browserName }) => {
});
// Switch to the other room, the call should go to PIP
await TestHelpers.expandRoomList(valere.page);
await TestHelpers.switchToRoomNamed(valere.page, "DoubleTask");
// We should see the PIP overlay

View File

@@ -346,6 +346,12 @@ export class TestHelpers {
}
}
public static async expandRoomList(page: Page): Promise<void> {
await page
.getByRole("separator", { name: "Click or drag to expand" })
.click();
}
/**
* Switches to a room in the room list by its name.
* @param page - The EW page

4317
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,12 +16,15 @@ import {
HeaderStyle,
getUrlParams,
} from "../src/UrlParams";
import { mockConfig } from "./utils/test";
const ROOM_NAME = "roomNameHere";
const ROOM_ID = "!d45f138fsd";
const ORIGIN = "https://call.element.io";
const HOMESERVER = "localhost";
mockConfig();
describe("UrlParams", () => {
describe("handles URL with /room/", () => {
it("and nothing else", () => {

View File

@@ -183,12 +183,6 @@ export interface ConfigOptions {
// Overrides members from ConfigOptions that are always provided by the
// default config and are therefore non-optional.
export interface ResolvedConfigOptions extends ConfigOptions {
default_server_config: {
["m.homeserver"]: {
base_url: string;
server_name: string;
};
};
sync_disconnect_grace_period_ms: number;
ssla: string;
matrix_rtc_session: {
@@ -202,12 +196,6 @@ export interface ResolvedConfigOptions extends ConfigOptions {
}
export const DEFAULT_CONFIG: ResolvedConfigOptions = {
default_server_config: {
["m.homeserver"]: {
base_url: "http://localhost:8008",
server_name: "localhost",
},
},
features: {
feature_use_device_session_member_events: true,
},

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type ReactNode } from "react";
import { type FC, type ReactNode } from "react";
import { useObservableEagerState } from "observable-hooks";
import classNames from "classnames";
@@ -13,6 +13,9 @@ import { type CallLayout } from "./CallLayout";
import { type SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/layout-types.ts";
import styles from "./SpotlightLandscapeLayout.module.css";
import { useUpdateLayout, useVisibleTiles } from "./Grid";
import { type MediaViewModel } from "../state/media/MediaViewModel.ts";
import { type Behavior } from "../state/Behavior.ts";
import { useBehavior } from "../useBehavior.ts";
/**
* An implementation of the "spotlight landscape" layout, in which the spotlight
@@ -54,16 +57,10 @@ export const makeSpotlightLandscapeLayout: CallLayout<
useUpdateLayout();
useVisibleTiles(model.setVisibleTiles);
useObservableEagerState(minBounds$);
const withIndicators =
useObservableEagerState(model.spotlight.media$).length > 1;
return (
<div ref={ref} className={styles.layer}>
<div
className={classNames(styles.spotlight, {
[styles.withIndicators]: withIndicators,
})}
/>
<SpotlightSlot media$={model.spotlight.media$} />
<div className={styles.grid}>
{model.grid.map((m) => (
<Slot key={m.id} className={styles.slot} id={m.id} model={m} />
@@ -73,3 +70,20 @@ export const makeSpotlightLandscapeLayout: CallLayout<
);
},
});
interface SpotlightSlotProps {
media$: Behavior<MediaViewModel[]>;
}
// This component isolates the subscription to the spotlight media so that it
// can change without causing the whole layout to re-render
const SpotlightSlot: FC<SpotlightSlotProps> = ({ media$ }) => {
const withIndicators = useBehavior(media$).length > 1;
return (
<div
className={classNames(styles.spotlight, {
[styles.withIndicators]: withIndicators,
})}
/>
);
};

View File

@@ -15,8 +15,7 @@ Please see LICENSE in the repository root for full details.
@import url("@fontsource/inconsolata/700.css");
@import url("normalize.css/normalize.css") layer(normalize);
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css")
layer(compound);
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound);
@import url("@vector-im/compound-web/dist/style.css") layer(compound.components);
:root {

View File

@@ -99,7 +99,7 @@ function renderTestComponent(
),
} as unknown as Room;
if (explicitTracks?.length ?? 0 > 0) {
if ((explicitTracks?.length ?? 0) > 0) {
tracks = explicitTracks!.map(({ participantId, source, kind }) => {
const participant =
liveKitParticipants.find((p) => p.identity === participantId) ??

View File

@@ -33,6 +33,9 @@ Please see LICENSE in the repository root for full details.
.media.mirror video {
transform: scaleX(-1);
/* In FF if you add a transform: scale/translate/matrix filter on an element,
it'll ignore the parents' border-radius, so force back the radius to avoid UI glitch*/
border-radius: inherit;
}
.media[data-video-fit="cover"] video {

View File

@@ -32,6 +32,7 @@ import {
global.IntersectionObserver = class MockIntersectionObserver {
public observe(): void {}
public unobserve(): void {}
public disconnect(): void {}
} as unknown as typeof IntersectionObserver;
test("SpotlightTile is accessible", async () => {

View File

@@ -28,6 +28,10 @@ test("useTypedEventEmitterState reacts to events", async () => {
const emitter = new TestEmitter();
const Test: FC = () => {
// Disable the React Compiler to work around the following bug:
// https://github.com/react/react/issues/34901
"use no memo";
const value = useTypedEventEmitterState(
emitter,
"change",
@@ -51,6 +55,10 @@ test("useTypedEventEmitterState reacts to changes made by an effect mounted on t
const emitter = new TestEmitter();
const Test: FC = () => {
// Disable the React Compiler to work around the following bug:
// https://github.com/react/react/issues/34901
"use no memo";
useEffect(() => emitter.setState(2), []);
const value = useTypedEventEmitterState(
emitter,
@@ -69,6 +77,10 @@ test("useTypedEventEmitterState reacts to changes in getState", async () => {
const emitter = new TestEmitter();
const Test: FC = () => {
// Disable the React Compiler to work around the following bug:
// https://github.com/react/react/issues/34901
"use no memo";
const [fn, setFn] = useState(() => emitter.getState);
const value = useTypedEventEmitterState(emitter, "change", fn);
return (

View File

@@ -439,6 +439,12 @@ export function mockConfig(
const spy = vi.spyOn(Config, "get").mockReturnValue({
...DEFAULT_CONFIG,
...config,
default_server_config: {
["m.homeserver"]: {
base_url: "http://localhost:8008",
server_name: "localhost",
},
},
});
// simulate loading the config
vi.spyOn(Config, "init").mockResolvedValue(void 0);

View File

@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
import {
loadEnv,
PluginOption,
type PluginOption,
searchForWorkspaceRoot,
type ConfigEnv,
type UserConfig,
@@ -19,8 +19,8 @@ import { codecovVitePlugin } from "@codecov/vite-plugin";
import { sentryVitePlugin } from "@sentry/vite-plugin";
import { nodePolyfills } from "vite-plugin-node-polyfills";
import wasm from "vite-plugin-wasm";
import react from "@vitejs/plugin-react";
import babel from "@rolldown/plugin-babel";
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
import { realpathSync } from "fs";
import * as fs from "node:fs";
@@ -29,6 +29,9 @@ export const vitePluginsConfig = ({
}: Pick<ConfigEnv, "mode">): UserConfig => {
const env = loadEnv(mode, process.cwd());
const plugins: PluginOption[] = [
babel({
presets: [reactCompilerPreset()],
}),
react(),
wasm(),
nodePolyfills({