Merge branch 'develop' of ssh://github.com/element-hq/element-web into t3chguy/merge-modules

# Conflicts:
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
This commit is contained in:
Michael Telatynski
2026-06-09 12:39:57 +01:00
46 changed files with 439 additions and 422 deletions
+1 -1
View File
@@ -14,8 +14,8 @@ WORKDIR /src
# Install dependencies
COPY --parents package.json pnpm-lock.yaml pnpm-workspace.yaml patches scripts **/package.json /src/
RUN corepack enable
RUN --mount=type=bind,source=.git,target=/src/.git /src/scripts/docker-link-repos.sh
RUN pnpm install --frozen-lockfile
RUN --mount=type=bind,source=.git,target=/src/.git /src/scripts/docker-link-repos.sh
# Build
COPY --link --exclude=.git --exclude=apps/web/docker . /src
+1 -1
View File
@@ -91,7 +91,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.376.6",
"posthog-js": "1.381.0",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "catalog:",
@@ -371,5 +371,33 @@ test.describe("Room list custom sections", () => {
// Room is back in the Chats section
await assertRoomInSection(page, "Chats", "my room");
});
test("should remove a room from a custom section via the 'Remove from section' menu entry", async ({
page,
app,
}) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
const roomList = getRoomList(page);
// Move the room to the Work section
let roomItem = roomList.getByRole("row", { name: "Open room my room" });
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "Move to" }).hover();
await page.getByRole("menuitem", { name: "Work" }).click();
await assertRoomInSection(page, "Work", "my room");
// Open the More Options menu and click "Remove from section"
roomItem = roomList.getByRole("row", { name: "Open room my room" });
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "Remove from section" }).click();
// Room is back in the Chats section
await assertRoomInSection(page, "Chats", "my room");
});
});
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js";
const DOCKER_IMAGE =
"ghcr.io/element-hq/synapse:develop@sha256:3e079baccf55f491bd40c8b218789f0e8b31f466f6e23b14baab4a52ca845284";
"ghcr.io/element-hq/synapse:develop@sha256:e5e02e25d3f3c38ae399acf74c359e971c2fd1381b88e0031a1faa9c785a0d91";
/**
* SynapseContainer which freezes the docker digest to stabilise tests,
@@ -21,3 +21,7 @@
width: 100%;
}
}
.mx_CreateSectionDialog_edition > .mx_Dialog_header {
margin-bottom: var(--cpd-space-6x);
}
@@ -35,6 +35,9 @@ Please see LICENSE files in the repository root for full details.
.mx_DevTools_content {
overflow-y: auto;
/* Give room for focus outlines */
padding-inline-start: var(--cpd-space-0-5x);
padding-block-end: var(--cpd-space-0-5x);
}
.mx_DevTools_RoomStateExplorer_query {
@@ -202,8 +202,18 @@ class LoggedInView extends React.Component<IProps, IState> {
OwnProfileStore.instance.on(UPDATE_EVENT, this.refreshBackgroundImage);
this.refreshBackgroundImage();
}
this.resizerViewModel = new ResizerViewModel();
private getResizerViewModel(): ResizerViewModel {
if (!this.resizerViewModel) {
this.resizerViewModel = new ResizerViewModel();
}
return this.resizerViewModel;
}
private disposeResizerViewModel(): void {
this.resizerViewModel?.dispose();
this.resizerViewModel = undefined;
}
/**
@@ -739,6 +749,8 @@ class LoggedInView extends React.Component<IProps, IState> {
break;
default: {
if (moduleRenderer) {
// Since the view will be removed, remove the vm as well
this.disposeResizerViewModel();
pageElement = moduleRenderer();
} else {
console.warn(`Couldn't render page type "${this.props.page_type}"`);
@@ -797,14 +809,15 @@ class LoggedInView extends React.Component<IProps, IState> {
const roomView = <div className="mx_RoomView_wrapper">{pageElement}</div>;
let content: React.ReactNode;
if (useNewRoomList && this.resizerViewModel && !moduleRenderer) {
const resizerViewModel = !moduleRenderer ? this.getResizerViewModel() : undefined;
if (useNewRoomList && resizerViewModel && !moduleRenderer) {
// New room list owned by element-web: resizable layout with a draggable separator.
// The SpacePanel lives inside GroupView (leftPanel omits it when the new room list is enabled).
content = (
<GroupView vm={this.resizerViewModel}>
<GroupView vm={resizerViewModel}>
<SpacePanel />
<LeftResizablePanelView
vm={this.resizerViewModel}
vm={resizerViewModel}
className="mx_LeftPanel_panel"
minSize="200px"
maxSize="370px"
@@ -812,7 +825,7 @@ class LoggedInView extends React.Component<IProps, IState> {
>
{leftPanel}
</LeftResizablePanelView>
<SeparatorView className="mx_Separator" vm={this.resizerViewModel} />
<SeparatorView className="mx_Separator" vm={resizerViewModel} />
<Panel className="mx_LeftPanel_panel">{roomView}</Panel>
</GroupView>
);
@@ -8,6 +8,7 @@
import React, { useState, type JSX } from "react";
import { Flex } from "@element-hq/web-shared-components";
import { Form, Text } from "@vector-im/compound-web";
import classNames from "classnames";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
@@ -37,15 +38,19 @@ export function CreateSectionDialog({ onFinished, sectionToEdit }: CreateSection
return (
<BaseDialog
className="mx_CreateSectionDialog"
className={classNames("mx_CreateSectionDialog", {
mx_CreateSectionDialog_edition: isEdition,
})}
onFinished={() => onFinished(false, value)}
title={isEdition ? _t("create_section_dialog|title_edition") : _t("create_section_dialog|title")}
hasCancel={true}
>
<Flex gap="var(--cpd-space-6x)" direction="column" className="mx_CreateSectionDialog_content">
<Text as="span" weight="medium">
{_t("create_section_dialog|description")}
</Text>
{!isEdition && (
<Text as="span" weight="medium">
{_t("create_section_dialog|description")}
</Text>
)}
<Form.Root
className="mx_CreateSectionDialog_form"
onSubmit={(e) => {
+16 -3
View File
@@ -246,7 +246,9 @@
"check_code_heading": "Sisesta teises seadmes kuvatav number",
"check_code_input_label": "2-kohaline kood",
"check_code_mismatch": "Numbrid ei klapi",
"choose_desktop_computer": "Vali „%(desktopComputer)s“",
"completing_setup": "Lõpetame uue seadme seadistamise",
"desktop_computer": "Lauaarvuti",
"error_etag_missing": "Tekkis ootamatu viga. Selle põhjuseks võivad olla brauseri lisamoodul, proksiserveri seadistused või koduserveri vigased seadistused.",
"error_expired": "Sisselogimine aegus. Palun proovi uuesti.",
"error_expired_title": "Sisselogimine ei jõudnud õigeaegselt lõpule",
@@ -267,15 +269,22 @@
"error_user_declined": "Sa keeldusid teises seadmes sisselogimispäringust.",
"error_user_declined_title": "Sa keeldusid sisselogimast",
"follow_remaining_instructions": "Teise seadme verifitseerimiseks järgi ülejäänud juhiseid",
"open_element_mobile_device": "Ava oma nutiseadmes %(brand)s",
"open_element_other_device": "Ava %(brand)s oma teises seadmes",
"point_the_camera": "Suuna kaamera siin näidatud QR-koodi peale",
"scan_code_instruction": "Skaneeri QR-koodi teise seadmega",
"ready_to_scan": "Skaneerimiseks valmis",
"scan_code_instruction": "Skaneeri seda QR-koodi",
"scan_code_instruction_reciprocate": "Skaneeri seda QR-koodi teise seadmega",
"scan_qr_code": "Logi sisse QR-koodi alusel",
"security_code_prompt": "Kui seda küsitakse, sisesta teises seadmes allolev kood.",
"security_code_prompt": "Sinu teenusepakkuja võib sisselogimise verifitseerimiseks küsida seda koodi.",
"security_code_title": "Sinu verifitseerimiskood",
"select_qr_code": "Val i „%(scanQRCode)s“",
"select_ready_to_scan": "Vali „%(readyToScan)s“ ja skaneeri siinnäidatud QR-koodi",
"start_over": "Alusta otsast peale",
"tap_avatar_link_new_device": "Klõpsa oma tunnuspilti ja vali „%(linkNewDevice)s“",
"unsupported_explainer": "Sinu teenusepakkuja ei toeta võimalust logida sisse QR-koodi abil.",
"unsupported_heading": "QR-koodi kasutamine pole toetatud",
"waiting_for_device": "Ootame, et teine seade logiks võrku"
"waiting_for_device": "Ootame sinu teise seadme järgi"
},
"register_action": "Loo konto",
"registration": {
@@ -334,8 +343,10 @@
"sign_in_description": "Jätkamaks kasuta oma kontot.",
"sign_in_instead": "Pigem logi sisse",
"sign_in_instead_prompt": "Sul juba on kasutajakonto olemas? <a>Siis logi siin sisse</a>",
"sign_in_manually": "Logi sisse käsitsi",
"sign_in_or_register": "Logi sisse või loo uus konto",
"sign_in_or_register_description": "Jätkamaks kasuta oma kontot või loo uus konto.",
"sign_in_with_qr": "Logi sisse QR-koodi alusel",
"sign_in_with_sso": "Logi sisse ühekordse sisselogimise abil",
"signing_in": "Login sisse…",
"soft_logout": {
@@ -1517,6 +1528,7 @@
"bridge_state_manager": "Seda võrgusilda haldab <user />.",
"bridge_state_workspace": "Tööruum: <networkLink/>",
"click_for_info": "Lisateabe jaoks klõpsi",
"config_only": "Saab sisselülitada ainult config.json failist.",
"currently_experimental": "Parasjagu katsejärgus.",
"custom_themes": "Kohandatud kujunduste lisamise võimalus",
"dynamic_room_predecessors": "Jututoa dünaamilised eellased",
@@ -1561,6 +1573,7 @@
"leave_beta_reload": "Beeta-funktsionaalsuste kasutamise lõpetamisel laadime uuesti rakenduse %(brand)s.",
"location_share_live": "Asukoha jagamine reaalajas",
"location_share_live_description": "Tegemist on ajutise ja esialgse lahendusega: asukohad on jututoa ajaloos näha.",
"login_with_qr": "Logi sisse QR-koodiga",
"mjolnir": "Uued võimalused osalejate eiramiseks",
"msc3531_hide_messages_pending_moderation": "Luba modereerimist ootavate sõnumite peitmist.",
"new_room_list": "Võta kasutusele uus jututubade loend",
+2 -2
View File
@@ -270,11 +270,11 @@
"point_the_camera": "Scannez le QR code affiché ici",
"scan_code_instruction": "Scannez le code QR",
"scan_qr_code": "Se connecter avec un QR code",
"security_code_prompt": "Si vous y êtes invité, saisissez le code ci-dessous sur votre autre appareil.",
"security_code_prompt": "Votre fournisseur de compte peut vous demander le code suivant pour vérifier la connexion.",
"select_qr_code": "Sélectionnez « %(scanQRCode)s »",
"unsupported_explainer": "Votre fournisseur de compte ne prend pas en charge la connexion à un nouvel appareil à laide dun code QR.",
"unsupported_heading": "Le code QR n'est pas pris en charge",
"waiting_for_device": "En attente de connexion de lappareil"
"waiting_for_device": "En attente de votre autre appareil"
},
"register_action": "Créer un compte",
"registration": {
+16 -3
View File
@@ -246,7 +246,9 @@
"check_code_heading": "Wprowadź numer wyświetlany na drugim urządzeniu",
"check_code_input_label": "2-cyfrowy kod",
"check_code_mismatch": "Liczby się nie zgadzają",
"choose_desktop_computer": "Wybierz \"%(desktopComputer)s\"",
"completing_setup": "Kończenie konfiguracji nowego urządzenia",
"desktop_computer": "Komputer stacjonarny",
"error_etag_missing": "Wystąpił nieoczekiwany błąd. Może to być spowodowane rozszerzeniem przeglądarki, serwerem proxy lub błędną konfiguracją serwera.",
"error_expired": "Logowanie wygasło. Spróbuj ponownie.",
"error_expired_title": "Logowanie nie zostało zakończone na czas",
@@ -267,15 +269,22 @@
"error_user_declined": "Ty lub Twój dostawca konta odrzucił żądanie logowania.",
"error_user_declined_title": "Logowanie odrzucone",
"follow_remaining_instructions": "Postępuj zgodnie z pozostałymi instrukcjami, aby zweryfikować Twoje inne urządzenie",
"open_element_mobile_device": "Otwórz %(brand)s na swoim urządzeniu mobilnym",
"open_element_other_device": "Otwórz %(brand)s na innym urządzeniu",
"point_the_camera": "Skieruj kamerę na pokazany tutaj kod QR",
"scan_code_instruction": "Skanuj kod QR za pomocą innego urządzenia",
"ready_to_scan": "Gotowy do skanowania",
"scan_code_instruction": "Skanuj kod QR",
"scan_code_instruction_reciprocate": "Zeskanuj kod QR innym urządzeniem",
"scan_qr_code": "Zaloguj się kodem QR",
"security_code_prompt": "Jeśli zostaniesz poproszony, wprowadź poniższy kod na drugim urządzeniu.",
"security_code_prompt": "Twój dostawca konta może poprosić o podany kod w celu weryfikacji logowania.",
"security_code_title": "Twój kod weryfikacyjny",
"select_qr_code": "Wybierz '%(scanQRCode)s'",
"select_ready_to_scan": "Wybierz \"%(readyToScan)s\" i zeskanuj kod QR widoczny tutaj",
"start_over": "Zacznij od nowa",
"tap_avatar_link_new_device": "Kliknij swój awatar i wybierz \"%(linkNewDevice)s\"",
"unsupported_explainer": "Twój dostawca konta nie obsługuje logowania nowego urządzenia za pomocą kodu QR.",
"unsupported_heading": "Kod QR nie jest wspierany",
"waiting_for_device": "Oczekiwanie na logowanie urządzenia"
"waiting_for_device": "Oczekiwanie na drugie urządzenie"
},
"register_action": "Utwórz konto",
"registration": {
@@ -334,8 +343,10 @@
"sign_in_description": "Użyj swojego konta, aby kontynuować.",
"sign_in_instead": "Zamiast tego zaloguj się",
"sign_in_instead_prompt": "Masz już konto? <a>Zaloguj się tutaj </a>",
"sign_in_manually": "Zaloguj się ręcznie",
"sign_in_or_register": "Zaloguj się lub utwórz konto",
"sign_in_or_register_description": "Użyj konta lub utwórz nowe, aby kontynuować.",
"sign_in_with_qr": "Zaloguj się kodem QR",
"sign_in_with_sso": "Zaloguj się za pomocą pojedynczego logowania",
"signing_in": "Logowanie…",
"soft_logout": {
@@ -1528,6 +1539,7 @@
"bridge_state_manager": "Ten mostek jest zarządzany przez <user />.",
"bridge_state_workspace": "Obszar roboczy: <networkLink/>",
"click_for_info": "Kliknij po więcej informacji",
"config_only": "Może zostać włączony tylko poprzez config.json.",
"currently_experimental": "Aktualnie eksperymentalne.",
"custom_themes": "Obsługa dodawania niestandardowych motywów",
"dynamic_room_predecessors": "Dynamiczne poprzedniki pokoju",
@@ -1572,6 +1584,7 @@
"leave_beta_reload": "Opuszczenie bety, wczyta ponownie %(brand)s.",
"location_share_live": "Udostępnianie lokalizacji na żywo",
"location_share_live_description": "Implementacja tymczasowa. Lokalizacje są zapisywane w historii pokoju.",
"login_with_qr": "Zaloguj się za pomocą kodu QR",
"mjolnir": "Nowe sposoby na ignorowanie osób",
"msc3531_hide_messages_pending_moderation": "Daj moderatorom ukrycie wiadomości które są sprawdzane.",
"new_room_list": "Włącz nową listę pokojów",
+1 -1
View File
@@ -1140,7 +1140,7 @@ export const SETTINGS: Settings = {
"urlPreviewsEnabled": {
// Enabled by default and client configurable as this setting only allows unencrypted
// messages to be previewed.
supportedLevels: [SettingLevel.DEVICE, SettingLevel.ACCOUNT, SettingLevel.CONFIG],
supportedLevels: [SettingLevel.ROOM_DEVICE, SettingLevel.DEVICE, SettingLevel.ACCOUNT, SettingLevel.CONFIG],
supportedLevelsAreOrdered: true,
displayName: _td("settings|inline_url_previews_default"),
default: true,
@@ -37,6 +37,8 @@ export const PREVIEW_WIDTH_PX = 478;
export const PREVIEW_HEIGHT_PX = 200;
export const MIN_PREVIEW_PX = 96;
export const MIN_IMAGE_SIZE_BYTES = 8192;
// From https://github.com/matrix-org/matrix-spec-proposals/pull/4095
export const BUNDLED_LINK_PREVIEWS = "com.beeper.linkpreviews";
export enum PreviewVisibility {
/**
@@ -387,6 +389,18 @@ export class UrlPreviewGroupViewModel
* for the previously-calculated links.
*/
private async computeSnapshot(): Promise<void> {
// This uses MSC4095. If the sender has sent us an empty URL previews bundle
// then they do not want to have URL previews be visible.
const bundledLinkPreviews = this.props.mxEvent.getContent()[BUNDLED_LINK_PREVIEWS];
if (Array.isArray(bundledLinkPreviews) && bundledLinkPreviews.length === 0) {
return this.snapshot.merge({
previews: [],
totalPreviewCount: 0,
previewsLimited: false,
overPreviewLimit: false,
});
} // otherwise, we do not support bundled previews yet so will fallback to old behaviour.
const previews =
this.visibility <= PreviewVisibility.UserHidden
? []
@@ -415,6 +415,14 @@ export class RoomListItemViewModel
tagRoom(this.props.room, tag);
};
public onRemoveFromSection = (): void => {
const roomTags = this.props.room.tags;
const sectionTag = RoomListStoreV3.instance.orderedSectionTags.find((tag) => Boolean(roomTags[tag]));
if (sectionTag) {
tagRoom(this.props.room, sectionTag);
}
};
private onOrderedCustomSectionsChange = (): void => {
// Rebuild sections list to reflect new order
const sections = RoomListItemViewModel.buildSections(this.props.room.tags);
@@ -9,7 +9,10 @@ import { expect } from "@jest/globals";
import type { MockedObject } from "jest-mock";
import type { MatrixClient, IPreviewUrlResponse } from "matrix-js-sdk/src/matrix";
import { UrlPreviewGroupViewModel } from "../../../src/viewmodels/message-body/UrlPreviewGroupViewModel";
import {
BUNDLED_LINK_PREVIEWS,
UrlPreviewGroupViewModel,
} from "../../../src/viewmodels/message-body/UrlPreviewGroupViewModel";
import type { UrlPreview } from "@element-hq/web-shared-components";
import { getMockClientWithEventEmitter, mkEvent } from "../../test-utils";
@@ -22,7 +25,9 @@ const BASIC_PREVIEW_OGDATA = {
"og:site_name": "Example.org",
};
function getViewModel({ mediaVisible, visible } = { mediaVisible: true, visible: true }): {
function getViewModel(
{ mediaVisible, visible, showPreview } = { mediaVisible: true, visible: true, showPreview: true },
): {
vm: UrlPreviewGroupViewModel;
client: MockedObject<MatrixClient>;
onImageClicked: jest.Mock<void, [UrlPreview]>;
@@ -41,7 +46,9 @@ function getViewModel({ mediaVisible, visible } = { mediaVisible: true, visible:
event: true,
user: "@foo:bar",
type: "m.room.message",
content: {},
content: {
...(showPreview ? undefined : { [BUNDLED_LINK_PREVIEWS]: [] }),
},
id: "$id",
}),
});
@@ -94,7 +101,7 @@ describe("UrlPreviewGroupViewModel", () => {
]);
});
it("should hide preview when invisible", async () => {
const { vm, client } = getViewModel({ visible: false, mediaVisible: true });
const { vm, client } = getViewModel({ visible: false, mediaVisible: true, showPreview: true });
const msg = document.createElement("div");
msg.innerHTML = '<a href="https://example.org">Test</a>';
await vm.updateEventElement(msg);
@@ -152,7 +159,7 @@ describe("UrlPreviewGroupViewModel", () => {
expect(vm.getSnapshot().previews[0].siteIcon).toBeTruthy();
});
it("should ignore media when mediaVisible is false", async () => {
const { vm, client } = getViewModel({ mediaVisible: false, visible: true });
const { vm, client } = getViewModel({ mediaVisible: false, visible: true, showPreview: true });
client.getUrlPreview.mockResolvedValueOnce({
"og:title": "This is an example!",
"og:type": "document",
@@ -225,6 +232,21 @@ describe("UrlPreviewGroupViewModel", () => {
await vm.onShowClick();
expect(vm.getSnapshot()).toMatchSnapshot();
});
it("should hide a preview if the message requests it", async () => {
const { vm, client } = getViewModel({ showPreview: false, mediaVisible: true, visible: true });
client.getUrlPreview.mockResolvedValueOnce(BASIC_PREVIEW_OGDATA);
const msg = document.createElement("div");
msg.innerHTML = '<a href="https://example.org">Test</a>';
await vm.updateEventElement(msg);
expect(vm.getSnapshot()).toMatchInlineSnapshot(`
{
"overPreviewLimit": false,
"previews": [],
"previewsLimited": false,
"totalPreviewCount": 0,
}
`);
});
describe("calculates author", () => {
it("should use the profile:username if provided", async () => {