Merge upstream v1.12.26 - reconnect the fork to Element Web (management #0099)

The repo had no upstream ancestry: a whole tree arrived in one commit in May, so
every update meant re-applying our patches by hand onto a fresh checkout, and a file
Element moved would take our lines with it silently.

The real base was found by measuring tree distance across develop rather than trusting
the changelog: deadd548, not the v1.12.17 tag. With that set as a temporary graft, this
merge computed as a proper three-way merge - 32 conflicts instead of 1757.

Resolutions, each decided rather than defaulted:

- 24 GitHub workflows stay deleted; we build on GitLab CI.
- MImageBody.tsx is gone upstream, migrated to MVVM. Our ClamAV error label moved into
  ImageBodyViewModel.computeErrorLabel, ahead of the DecryptError branch, matching what
  VideoBodyViewModel and FileBodyViewModel already do.
- Upstream extracted the room list item body into RoomListItemContent. Our call
  participants list and its getInitials helper moved there; both sides' CSS classes and
  both sides' props are kept.
- matrix-js-sdk follows upstream at 42.2.0 - our git ref pin was a workaround for a
  stale ref, and following upstream is the point of this merge.
- Element Call stays ours. Checked before deciding: @element-hq/element-call-embedded
  is referenced nowhere in the tree, while webpack.config.ts needs
  @sorb/threadnet-call-embedded, so taking upstream's line would have deleted the noise
  suppression from #0054 without a word.

The lockfile was regenerated with pnpm 11.20.0, which upstream now requires through
devEngines. CI already runs corepack enable, and onFail: download makes it fetch that
version by itself.

Not yet accepted: this needs a build and the ClamAV functional test - send an encrypted
file, receive a rejected one - before it goes near main.
This commit is contained in:
Thore Cimbal
2026-08-19 12:00:00 +00:00
2884 changed files with 111523 additions and 71192 deletions
-99
View File
@@ -1,99 +0,0 @@
/*
Copyright 2025 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
module.exports = {
root: true,
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
extends: [
"plugin:matrix-org/react",
"plugin:matrix-org/a11y",
"plugin:matrix-org/typescript",
"plugin:matrix-org/react",
"plugin:storybook/recommended",
],
parserOptions: {
project: ["./tsconfig.json"],
tsconfigRootDir: __dirname,
},
env: {
browser: true,
node: true,
},
rules: {
// Bind or arrow functions in props causes performance issues (but we
// currently use them in some places).
// It's disabled here, but we should using it sparingly.
"react/jsx-no-bind": "off",
"react/jsx-key": ["error"],
"matrix-org/require-copyright-header": "error",
"react-compiler/react-compiler": "error",
"no-restricted-imports": [
"error",
{
paths: [
{
name: "react",
importNames: ["act"],
message: "Please use @test-utils instead.",
},
],
},
],
"@typescript-eslint/unbound-method": ["error", { ignoreStatic: true }],
"@typescript-eslint/explicit-function-return-type": [
"error",
{
allowExpressions: true,
},
],
// We're okay being explicit at the moment
// "@typescript-eslint/no-empty-interface": "off",
// We'd rather not do this but we do
// "@typescript-eslint/ban-ts-comment": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-empty-object-type": [
"error",
{
// We do this sometimes to brand interfaces
allowInterfaces: "with-single-extends",
},
],
"storybook/meta-satisfies-type": "error",
"react/forbid-elements": [
"error",
{
forbid: [
{ element: "h1", message: "Use Compound <Heading> instead" },
{ element: "h2", message: "Use Compound <Heading> instead" },
{ element: "h3", message: "Use Compound <Heading> instead" },
{ element: "h4", message: "Use Compound <Heading> instead" },
{ element: "h5", message: "Use Compound <Heading> instead" },
{ element: "h6", message: "Use Compound <Heading> instead" },
],
},
],
},
overrides: [
{
files: ["src/**/*.test.{ts,tsx}", "src/**/*.stories.tsx"],
rules: {
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/no-explicit-any": "off",
"react/forbid-elements": "off",
},
},
],
settings: {
react: {
version: "detect",
},
},
};
@@ -1,5 +0,0 @@
// Even though this (at time of writing) is identical Element Web's
// .prettierrc.js, shared components needs its own because otherwise
// this refers to element web's copy of eslint-plugin-matrix-org which
// would require element-web's modules to be installed.
module.exports = require("eslint-plugin-matrix-org/.prettierrc.js");
@@ -0,0 +1,71 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/*
* Match the app/web shell baseline without importing app/web component styles.
* Shared component stories should inherit the same root typography and font
* feature settings as the real app, while still owning their component styling.
*/
@layer app-web {
:root[data-storybook-root-css="app-web"] {
[class^="cpd-theme"][class^="cpd-theme"] {
--cpd-font-family-sans:
"Inter", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif,
"Noto Color Emoji";
}
body {
font: var(--cpd-font-body-md-regular) !important;
letter-spacing: var(--cpd-font-letter-spacing-body-md) !important;
font-feature-settings:
"kern" 1,
"liga" 1,
"calt" 1 !important;
background-color: var(--cpd-color-bg-canvas-default);
color: var(--cpd-color-text-primary);
border: 0;
margin: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
pre,
code {
font-family:
"Fira Code", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace, "Noto Color Emoji";
font-size: 100% !important;
}
}
/* Username color classes - these are defined in the main app's _common.pcss
but need to be available in Storybook for components that use colorClass */
.mx_Username_color1 {
color: var(--cpd-color-text-decorative-1);
}
.mx_Username_color2 {
color: var(--cpd-color-text-decorative-2);
}
.mx_Username_color3 {
color: var(--cpd-color-text-decorative-3);
}
.mx_Username_color4 {
color: var(--cpd-color-text-decorative-4);
}
.mx_Username_color5 {
color: var(--cpd-color-text-decorative-5);
}
.mx_Username_color6 {
color: var(--cpd-color-text-decorative-6);
}
}
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import { Addon, types, useGlobals } from "storybook/manager-api";
import { type Addon, types, useGlobals } from "storybook/manager-api";
import { WithTooltip, IconButton, TooltipLinkList } from "storybook/internal/components";
import React from "react";
import { GlobeIcon } from "@storybook/icons";
@@ -24,6 +24,7 @@ export const languageAddon: Addon = {
title: "Language Selector",
type: types.TOOL,
render: ({ active }) => {
// oxlint-disable-next-line react-hooks/rules-of-hooks
const [globals, updateGlobals] = useGlobals();
const selectedLanguage = globals.language || "en";
@@ -9,15 +9,15 @@ import type { StorybookConfig } from "@storybook/react-vite";
import fs from "node:fs";
import { nodePolyfills } from "vite-plugin-node-polyfills";
import { mergeConfig, normalizePath, type Plugin } from "vite";
import { dirname, join } from "node:path";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const srcRoot = normalizePath(join(__dirname, "..", "src"));
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const srcRoot = normalizePath(path.join(__dirname, "..", "src"));
const sharedComponentsLayer = "shared-components";
// Get a list of available languages so the language selector can display them at runtime
const languageFiles = fs.readdirSync(join(__dirname, "..", "src", "i18n", "strings")).map((f) => f.slice(0, -5));
const languageFiles = fs.readdirSync(path.join(__dirname, "..", "src", "i18n", "strings")).map((f) => f.slice(0, -5));
const languages: Record<string, string> = {};
for (const lang of languageFiles) {
@@ -35,7 +35,7 @@ for (const lang of languageFiles) {
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
*/
function getAbsolutePath(value: string): any {
return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`)));
return path.dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`)));
}
function layerSharedComponentCssModules(): Plugin {
@@ -88,7 +88,7 @@ const config: StorybookConfig = {
name: "language-middleware",
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === "/i18n/languages.json") {
if (req.url?.split("?")[0] === "/i18n/languages.json") {
// Dynamically generate a languages.json file based on what files are available
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(languages));
@@ -9,28 +9,7 @@ Please see LICENSE files in the repository root for full details.
background: var(--cpd-color-bg-canvas-default);
}
/* Username color classes - these are defined in the main app's _common.pcss
but need to be available in Storybook for components that use colorClass */
.mx_Username_color1 {
color: var(--cpd-color-text-decorative-1);
}
.mx_Username_color2 {
color: var(--cpd-color-text-decorative-2);
}
.mx_Username_color3 {
color: var(--cpd-color-text-decorative-3);
}
.mx_Username_color4 {
color: var(--cpd-color-text-decorative-4);
}
.mx_Username_color5 {
color: var(--cpd-color-text-decorative-5);
}
.mx_Username_color6 {
color: var(--cpd-color-text-decorative-6);
[class*="blurhash"] > canvas {
animation: none !important;
opacity: 1 !important;
}
@@ -12,13 +12,16 @@ import "@fontsource/inter/600.css";
import "@fontsource/inter/700.css";
import "./compound.css";
import "./app-web-root.css";
import "./preview.css";
import React, { useLayoutEffect } from "react";
import { TooltipProvider } from "@vector-im/compound-web";
import type { StoryContext } from "storybook/internal/csf";
import { EventPresentationProvider, type EventDensity, type EventLayout, I18nApi, I18nContext } from "../src";
import { setLanguage } from "../src/core/i18n/i18n";
import { type StoryContext } from "storybook/internal/csf";
import { DragDropProvider } from "@dnd-kit/react";
import { PointerActivationConstraints, PointerSensor } from "@dnd-kit/dom";
export const globalTypes = {
theme: {
@@ -47,7 +50,7 @@ export const globalTypes = {
icon: "component",
title: "Event layout",
items: [
{ title: "Group", value: "group" },
{ title: "Modern", value: "group" },
{ title: "Bubble", value: "bubble" },
{ title: "IRC", value: "irc" },
],
@@ -65,30 +68,70 @@ export const globalTypes = {
],
},
},
rootCss: {
name: "Root CSS",
description: "Global root CSS for component previews",
toolbar: {
icon: "paintbrush",
title: "Root CSS",
items: [
{ title: "Default", value: "storybook" },
{ title: "Element Web", value: "app-web" },
],
},
},
initialGlobals: {
theme: "system",
theme: "light",
language: "en",
eventLayout: "group",
eventDensity: "default",
rootCss: "storybook",
},
} satisfies ArgTypes;
const allThemesClasses = globalTypes.theme.toolbar.items.map(({ value }) => `cpd-theme-${value}`);
const RootCssSwitcher: React.FC<{
rootCss: string;
}> = ({ rootCss }) => {
useLayoutEffect(() => {
if (rootCss === "app-web") {
document.documentElement.dataset.storybookRootCss = rootCss;
} else {
delete document.documentElement.dataset.storybookRootCss;
}
return () => {
delete document.documentElement.dataset.storybookRootCss;
};
}, [rootCss]);
return null;
};
const ThemeSwitcher: React.FC<{
theme: string;
}> = ({ theme }) => {
useLayoutEffect(() => {
document.documentElement.classList.remove(...allThemesClasses);
document.body.classList.remove(...allThemesClasses);
if (theme !== "system") {
document.documentElement.classList.add(`cpd-theme-${theme}`);
document.body.classList.add(`cpd-theme-${theme}`);
}
return () => document.documentElement.classList.remove(...allThemesClasses);
return () => document.body.classList.remove(...allThemesClasses);
}, [theme]);
return null;
};
const withRootCss: Decorator = (Story, context) => {
return (
<>
<RootCssSwitcher rootCss={context.globals.rootCss} />
<Story />
</>
);
};
const withThemeProvider: Decorator = (Story, context) => {
return (
<>
@@ -131,15 +174,44 @@ const withEventPresentationProvider: Decorator = (Story, context) => {
);
};
const preview = {
/**
* Wrap all stories in a DragDropProvider that excludes the Accessibility plugin.
* dnd-kit's Accessibility plugin adds aria attributes (tabindex, aria-pressed, etc.)
* that conflict with the existing ARIA roles used in the room list components.
*/
const withDragDropProvider: Decorator = (Story) => {
return (
<DragDropProvider
sensors={[
// By default, the PointerSensor activates dragging immediately on pointer down, which interferes with keyboard navigation.
// So we start dragging after the pointer has moved by 5 pixels, to allow for click without dragging
PointerSensor.configure({
activationConstraints: [new PointerActivationConstraints.Distance({ value: 5 })],
}),
]}
>
<Story />
</DragDropProvider>
);
};
const preview: Preview = {
tags: ["autodocs", "snapshot"],
initialGlobals: {
theme: "system",
rootCss: "storybook",
theme: "light",
language: "en",
eventLayout: "group",
eventDensity: "default",
},
decorators: [withThemeProvider, withEventPresentationProvider, withTooltipProvider, withI18nProvider],
decorators: [
withRootCss,
withThemeProvider,
withEventPresentationProvider,
withTooltipProvider,
withI18nProvider,
withDragDropProvider,
],
parameters: {
options: {
storySort: {
@@ -0,0 +1,37 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
/**
* A Storybook `play` helper that waits for every CSS `background-image` inside the
* rendered story to finish decoding before the visual-regression snapshot is taken.
*
* Components such as `LinkPreview` render their thumbnails as CSS `background-image`s
* rather than `<img>` elements, so there is no load event for the snapshot machinery
* to await. A larger image (e.g. the tall test image) can therefore still be decoding
* when the screenshot is captured, producing a non-deterministic placeholder frame.
* Decoding the images up-front populates the browser cache so the background paints
* synchronously on the next frame.
*/
export async function waitForBackgroundImages(root: HTMLElement): Promise<void> {
const urls = new Set<string>();
for (const el of root.querySelectorAll<HTMLElement>("*")) {
const match = /url\(["']?(.+?)["']?\)/.exec(getComputedStyle(el).backgroundImage);
if (match) urls.add(match[1]);
}
await Promise.all(
[...urls].map(async (src) => {
const img = new Image();
img.src = src;
try {
await img.decode();
} catch {
// Ignore images that fail to decode; the snapshot captures whatever renders.
}
}),
);
}
+15
View File
@@ -40,6 +40,21 @@ or in CSS file:
@import url("@element-hq/web-shared-components");
```
### Sub-path Imports
Callers running outside the browser DOM (e.g. inside an `AudioWorkletGlobalScope`
or a worker) can pull in the small standalone `numbers` utility bundle without
loading the rest of the package bundle, which transitively imports React,
dnd-kit, and other code that touches `window` / `document`:
```javascript
import { percentageOf, percentageWithin } from "@element-hq/web-shared-components/numbers";
```
The sub-path exposes the same functions listed under [Formatting](#formatting)
and ships as its own ES/CJS bundle in `dist/numbers.{js,umd.cjs}`. Prefer the
main package entry for everything else.
### Using Components
There are two kinds of components in this library:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Some files were not shown because too many files have changed in this diff Show More