diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index 536f78e18d..c2f3028176 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -34,27 +34,6 @@ jobs: - name: Typecheck run: "yarn run lint:types" - - name: Switch js-sdk to release mode - working-directory: node_modules/matrix-js-sdk - run: | - scripts/switch_package_to_release.cjs - yarn install - yarn run build:compile - yarn run build:types - - - name: Typecheck (release mode) - run: "yarn run lint:types" - - # Temporary while we directly import matrix-js-sdk/src/* which means we need - # certain @types/* packages to make sense of matrix-js-sdk types. - #- name: Typecheck (release mode; no yarn link) - # if: github.event_name != 'pull_request' && github.ref_name != 'master' - # run: | - # yarn unlink matrix-js-sdk - # yarn add github:matrix-org/matrix-js-sdk#develop - # yarn install --force - # yarn run lint:types - i18n_lint: name: "i18n Check" uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main diff --git a/components.json b/components.json index cc5046ed69..0967ef424b 100644 --- a/components.json +++ b/components.json @@ -1,5 +1 @@ -{ - "src/components/views/auth/AuthFooter.tsx": "src/components/views/auth/VectorAuthFooter.tsx", - "src/components/views/auth/AuthHeaderLogo.tsx": "src/components/views/auth/VectorAuthHeaderLogo.tsx", - "src/components/views/auth/AuthPage.tsx": "src/components/views/auth/VectorAuthPage.tsx" -} +{} diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index a1c35b274c..6434e70f48 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for synapse docker image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:df06607d21965639cb7dd72724fd610731c13bed95d3334746f53668a36c6cda"; +const DOCKER_TAG = "develop@sha256:6c33604ee62f009f3b34454a3c3e85f7e3ff5de63e45011fcd79e0ddc54a4e51"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 8bbad339c7..0017d00dac 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -31,6 +31,8 @@ import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; import { IConfigOptions } from "./IConfigOptions"; import SdkConfig from "./SdkConfig"; import { buildAndEncodePickleKey, encryptPickleKey } from "./utils/tokens/pickling"; +import Favicon from "./favicon.ts"; +import { getVectorConfig } from "./vector/getconfig.ts"; export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url"; export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url"; @@ -66,14 +68,20 @@ const UPDATE_DEFER_KEY = "mx_defer_update"; export default abstract class BasePlatform { protected notificationCount = 0; protected errorDidOccur = false; + protected _favicon?: Favicon; protected constructor() { dis.register(this.onAction); this.startUpdateCheck = this.startUpdateCheck.bind(this); } - public abstract getConfig(): Promise; + public async getConfig(): Promise { + return getVectorConfig(); + } + /** + * Get a sensible default display name for the device Element is running on + */ public abstract getDefaultDeviceDisplayName(): string; protected onAction = (payload: ActionPayload): void => { @@ -89,11 +97,15 @@ export default abstract class BasePlatform { public abstract getHumanReadableName(): string; public setNotificationCount(count: number): void { + if (this.notificationCount === count) return; this.notificationCount = count; + this.updateFavicon(); } public setErrorStatus(errorDidOccur: boolean): void { + if (this.errorDidOccur === errorDidOccur) return; this.errorDidOccur = errorDidOccur; + this.updateFavicon(); } /** @@ -456,4 +468,34 @@ export default abstract class BasePlatform { url.hash = ""; return url; } + + /** + * Delay creating the `Favicon` instance until first use (on the first notification) as + * it uses canvas, which can trigger a permission prompt in Firefox's resist fingerprinting mode. + * See https://github.com/element-hq/element-web/issues/9605. + */ + public get favicon(): Favicon { + if (this._favicon) { + return this._favicon; + } + this._favicon = new Favicon(); + return this._favicon; + } + + private updateFavicon(): void { + let bgColor = "#d00"; + let notif: string | number = this.notificationCount; + + if (this.errorDidOccur) { + notif = notif || "×"; + bgColor = "#f00"; + } + + this.favicon.badge(notif, { bgColor }); + } + + /** + * Begin update polling, if applicable + */ + public startUpdater(): void {} } diff --git a/src/components/views/auth/AuthFooter.tsx b/src/components/views/auth/AuthFooter.tsx index c81617b9db..8d27a04c83 100644 --- a/src/components/views/auth/AuthFooter.tsx +++ b/src/components/views/auth/AuthFooter.tsx @@ -7,18 +7,36 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { ReactElement } from "react"; +import SdkConfig from "../../../SdkConfig"; import { _t } from "../../../languageHandler"; -export default class AuthFooter extends React.Component { - public render(): React.ReactNode { - return ( - +const AuthFooter = (): ReactElement => { + const brandingConfig = SdkConfig.getObject("branding"); + const links = brandingConfig?.get("auth_footer_links") ?? [ + { text: "Blog", url: "https://element.io/blog" }, + { text: "Twitter", url: "https://twitter.com/element_hq" }, + { text: "GitHub", url: "https://github.com/element-hq/element-web" }, + ]; + + const authFooterLinks: JSX.Element[] = []; + for (const linkEntry of links) { + authFooterLinks.push( + + {linkEntry.text} + , ); } -} + + return ( + + ); +}; + +export default AuthFooter; diff --git a/src/components/views/auth/AuthHeaderLogo.tsx b/src/components/views/auth/AuthHeaderLogo.tsx index 3ff11ba3f2..07cc2f978a 100644 --- a/src/components/views/auth/AuthHeaderLogo.tsx +++ b/src/components/views/auth/AuthHeaderLogo.tsx @@ -1,5 +1,6 @@ /* Copyright 2019-2024 New Vector Ltd. +Copyright 2015, 2016 OpenMarket Ltd SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. @@ -7,8 +8,17 @@ Please see LICENSE files in the repository root for full details. import React from "react"; +import SdkConfig from "../../../SdkConfig"; + export default class AuthHeaderLogo extends React.PureComponent { - public render(): React.ReactNode { - return ; + public render(): React.ReactElement { + const brandingConfig = SdkConfig.getObject("branding"); + const logoUrl = brandingConfig?.get("auth_header_logo_url") ?? "themes/element/img/logos/element-logo.svg"; + + return ( + + ); } } diff --git a/src/components/views/auth/AuthPage.tsx b/src/components/views/auth/AuthPage.tsx index e9beb6d2a0..2782d0a641 100644 --- a/src/components/views/auth/AuthPage.tsx +++ b/src/components/views/auth/AuthPage.tsx @@ -7,15 +7,69 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ReactNode } from "react"; +import React from "react"; +import SdkConfig from "../../../SdkConfig"; import AuthFooter from "./AuthFooter"; -export default class AuthPage extends React.PureComponent<{ children: ReactNode }> { - public render(): React.ReactNode { +export default class AuthPage extends React.PureComponent { + private static welcomeBackgroundUrl?: string; + + // cache the url as a static to prevent it changing without refreshing + private static getWelcomeBackgroundUrl(): string { + if (AuthPage.welcomeBackgroundUrl) return AuthPage.welcomeBackgroundUrl; + + const brandingConfig = SdkConfig.getObject("branding"); + AuthPage.welcomeBackgroundUrl = "themes/element/img/backgrounds/lake.jpg"; + + const configuredUrl = brandingConfig?.get("welcome_background_url"); + if (configuredUrl) { + if (Array.isArray(configuredUrl)) { + const index = Math.floor(Math.random() * configuredUrl.length); + AuthPage.welcomeBackgroundUrl = configuredUrl[index]; + } else { + AuthPage.welcomeBackgroundUrl = configuredUrl; + } + } + + return AuthPage.welcomeBackgroundUrl; + } + + public render(): React.ReactElement { + const pageStyle = { + background: `center/cover fixed url(${AuthPage.getWelcomeBackgroundUrl()})`, + }; + + const modalStyle: React.CSSProperties = { + position: "relative", + background: "initial", + }; + + const blurStyle: React.CSSProperties = { + position: "absolute", + top: 0, + right: 0, + bottom: 0, + left: 0, + filter: "blur(40px)", + background: pageStyle.background, + }; + + const modalContentStyle: React.CSSProperties = { + display: "flex", + zIndex: 1, + background: "rgba(255, 255, 255, 0.59)", + borderRadius: "8px", + }; + return ( -
-
{this.props.children}
+
+
+
+
+ {this.props.children} +
+
); diff --git a/src/components/views/auth/VectorAuthFooter.tsx b/src/components/views/auth/VectorAuthFooter.tsx deleted file mode 100644 index 234c6b127b..0000000000 --- a/src/components/views/auth/VectorAuthFooter.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2019-2024 New Vector Ltd. -Copyright 2015, 2016 OpenMarket Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { ReactElement } from "react"; - -import SdkConfig from "../../../SdkConfig"; -import { _t } from "../../../languageHandler"; - -const VectorAuthFooter = (): ReactElement => { - const brandingConfig = SdkConfig.getObject("branding"); - const links = brandingConfig?.get("auth_footer_links") ?? [ - { text: "Blog", url: "https://element.io/blog" }, - { text: "Twitter", url: "https://twitter.com/element_hq" }, - { text: "GitHub", url: "https://github.com/element-hq/element-web" }, - ]; - - const authFooterLinks: JSX.Element[] = []; - for (const linkEntry of links) { - authFooterLinks.push( - - {linkEntry.text} - , - ); - } - - return ( - - ); -}; - -export default VectorAuthFooter; diff --git a/src/components/views/auth/VectorAuthHeaderLogo.tsx b/src/components/views/auth/VectorAuthHeaderLogo.tsx deleted file mode 100644 index 3cdf30cafc..0000000000 --- a/src/components/views/auth/VectorAuthHeaderLogo.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2019-2024 New Vector Ltd. -Copyright 2015, 2016 OpenMarket Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import * as React from "react"; - -import SdkConfig from "../../../SdkConfig"; - -export default class VectorAuthHeaderLogo extends React.PureComponent { - public render(): React.ReactElement { - const brandingConfig = SdkConfig.getObject("branding"); - const logoUrl = brandingConfig?.get("auth_header_logo_url") ?? "themes/element/img/logos/element-logo.svg"; - - return ( - - ); - } -} diff --git a/src/components/views/auth/VectorAuthPage.tsx b/src/components/views/auth/VectorAuthPage.tsx deleted file mode 100644 index 969cc560a3..0000000000 --- a/src/components/views/auth/VectorAuthPage.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright 2019-2024 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import * as React from "react"; - -import SdkConfig from "../../../SdkConfig"; -import VectorAuthFooter from "./VectorAuthFooter"; - -export default class VectorAuthPage extends React.PureComponent { - private static welcomeBackgroundUrl?: string; - - // cache the url as a static to prevent it changing without refreshing - private static getWelcomeBackgroundUrl(): string { - if (VectorAuthPage.welcomeBackgroundUrl) return VectorAuthPage.welcomeBackgroundUrl; - - const brandingConfig = SdkConfig.getObject("branding"); - VectorAuthPage.welcomeBackgroundUrl = "themes/element/img/backgrounds/lake.jpg"; - - const configuredUrl = brandingConfig?.get("welcome_background_url"); - if (configuredUrl) { - if (Array.isArray(configuredUrl)) { - const index = Math.floor(Math.random() * configuredUrl.length); - VectorAuthPage.welcomeBackgroundUrl = configuredUrl[index]; - } else { - VectorAuthPage.welcomeBackgroundUrl = configuredUrl; - } - } - - return VectorAuthPage.welcomeBackgroundUrl; - } - - public render(): React.ReactElement { - const pageStyle = { - background: `center/cover fixed url(${VectorAuthPage.getWelcomeBackgroundUrl()})`, - }; - - const modalStyle: React.CSSProperties = { - position: "relative", - background: "initial", - }; - - const blurStyle: React.CSSProperties = { - position: "absolute", - top: 0, - right: 0, - bottom: 0, - left: 0, - filter: "blur(40px)", - background: pageStyle.background, - }; - - const modalContentStyle: React.CSSProperties = { - display: "flex", - zIndex: 1, - background: "rgba(255, 255, 255, 0.59)", - borderRadius: "8px", - }; - - return ( -
-
-
-
- {this.props.children} -
-
- -
- ); - } -} diff --git a/src/components/views/elements/PersistedElement.tsx b/src/components/views/elements/PersistedElement.tsx index 2c87c8e7c6..3feb856145 100644 --- a/src/components/views/elements/PersistedElement.tsx +++ b/src/components/views/elements/PersistedElement.tsx @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { MutableRefObject, ReactNode, StrictMode } from "react"; -import ReactDOM from "react-dom"; +import { createRoot, Root } from "react-dom/client"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -24,7 +24,7 @@ export const getPersistKey = (appId: string): string => "widget_" + appId; // We contain all persisted elements within a master container to allow them all to be within the same // CSS stacking context, and thus be able to control their z-indexes relative to each other. function getOrCreateMasterContainer(): HTMLDivElement { - let container = getContainer("mx_PersistedElement_container"); + let container = document.getElementById("mx_PersistedElement_container") as HTMLDivElement; if (!container) { container = document.createElement("div"); container.id = "mx_PersistedElement_container"; @@ -34,18 +34,10 @@ function getOrCreateMasterContainer(): HTMLDivElement { return container; } -function getContainer(containerId: string): HTMLDivElement { - return document.getElementById(containerId) as HTMLDivElement; -} - function getOrCreateContainer(containerId: string): HTMLDivElement { - let container = getContainer(containerId); - - if (!container) { - container = document.createElement("div"); - container.id = containerId; - getOrCreateMasterContainer().appendChild(container); - } + const container = document.createElement("div"); + container.id = containerId; + getOrCreateMasterContainer().appendChild(container); return container; } @@ -83,6 +75,8 @@ export default class PersistedElement extends React.Component { private childContainer?: HTMLDivElement; private child?: HTMLDivElement; + private static rootMap: Record = {}; + public constructor(props: IProps) { super(props); @@ -99,14 +93,16 @@ export default class PersistedElement extends React.Component { * @param {string} persistKey Key used to uniquely identify this PersistedElement */ public static destroyElement(persistKey: string): void { - const container = getContainer("mx_persistedElement_" + persistKey); - if (container) { - container.remove(); + const pair = PersistedElement.rootMap[persistKey]; + if (pair) { + pair[0].unmount(); + pair[1].remove(); } + delete PersistedElement.rootMap[persistKey]; } public static isMounted(persistKey: string): boolean { - return Boolean(getContainer("mx_persistedElement_" + persistKey)); + return Boolean(PersistedElement.rootMap[persistKey]); } private collectChildContainer = (ref: HTMLDivElement): void => { @@ -179,7 +175,14 @@ export default class PersistedElement extends React.Component { ); - ReactDOM.render(content, getOrCreateContainer("mx_persistedElement_" + this.props.persistKey)); + let rootPair = PersistedElement.rootMap[this.props.persistKey]; + if (!rootPair) { + const container = getOrCreateContainer("mx_persistedElement_" + this.props.persistKey); + const root = createRoot(container); + rootPair = [root, container]; + PersistedElement.rootMap[this.props.persistKey] = rootPair; + } + rootPair[0].render(content); } private updateChildVisibility(child?: HTMLDivElement, visible = false): void { diff --git a/src/components/views/messages/EditHistoryMessage.tsx b/src/components/views/messages/EditHistoryMessage.tsx index dcb8b82774..8316d0835b 100644 --- a/src/components/views/messages/EditHistoryMessage.tsx +++ b/src/components/views/messages/EditHistoryMessage.tsx @@ -13,8 +13,8 @@ import classNames from "classnames"; import * as HtmlUtils from "../../../HtmlUtils"; import { editBodyDiffToHtml } from "../../../utils/MessageDiffUtils"; import { formatTime } from "../../../DateUtils"; -import { pillifyLinks, unmountPills } from "../../../utils/pillify"; -import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify"; +import { pillifyLinks } from "../../../utils/pillify"; +import { tooltipifyLinks } from "../../../utils/tooltipify"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import RedactedBody from "./RedactedBody"; @@ -23,6 +23,7 @@ import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog"; import ViewSource from "../../structures/ViewSource"; import SettingsStore from "../../../settings/SettingsStore"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { ReactRootManager } from "../../../utils/react"; function getReplacedContent(event: MatrixEvent): IContent { const originalContent = event.getOriginalContent(); @@ -47,8 +48,8 @@ export default class EditHistoryMessage extends React.PureComponent; private content = createRef(); - private pills: Element[] = []; - private tooltips: Element[] = []; + private pills = new ReactRootManager(); + private tooltips = new ReactRootManager(); public constructor(props: IProps, context: React.ContextType) { super(props, context); @@ -103,7 +104,7 @@ export default class EditHistoryMessage extends React.PureComponent { private readonly contentRef = createRef(); - private pills: Element[] = []; - private tooltips: Element[] = []; - private reactRoots: Element[] = []; + private pills = new ReactRootManager(); + private tooltips = new ReactRootManager(); + private reactRoots = new ReactRootManager(); private ref = createRef(); @@ -82,7 +82,7 @@ export default class TextualBody extends React.Component { // tooltipifyLinks AFTER calculateUrlPreview because the DOM inside the tooltip // container is empty before the internal component has mounted so calculateUrlPreview // won't find any anchors - tooltipifyLinks([content], this.pills, this.tooltips); + tooltipifyLinks([content], [...this.pills.elements, ...this.reactRoots.elements], this.tooltips); if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") { // Handle expansion and add buttons @@ -113,12 +113,11 @@ export default class TextualBody extends React.Component { private wrapPreInReact(pre: HTMLPreElement): void { const root = document.createElement("div"); root.className = "mx_EventTile_pre_container"; - this.reactRoots.push(root); // Insert containing div in place of
 block
         pre.parentNode?.replaceChild(root, pre);
 
-        ReactDOM.render(
+        this.reactRoots.render(
             
                 {pre}
             ,
@@ -137,16 +136,9 @@ export default class TextualBody extends React.Component {
     }
 
     public componentWillUnmount(): void {
-        unmountPills(this.pills);
-        unmountTooltips(this.tooltips);
-
-        for (const root of this.reactRoots) {
-            ReactDOM.unmountComponentAtNode(root);
-        }
-
-        this.pills = [];
-        this.tooltips = [];
-        this.reactRoots = [];
+        this.pills.unmount();
+        this.tooltips.unmount();
+        this.reactRoots.unmount();
     }
 
     public shouldComponentUpdate(nextProps: Readonly, nextState: Readonly): boolean {
@@ -204,7 +196,8 @@ export default class TextualBody extends React.Component {
                     
                 );
 
-                ReactDOM.render(spoiler, spoilerContainer);
+                this.reactRoots.render(spoiler, spoilerContainer);
+
                 node.parentNode?.replaceChild(spoilerContainer, node);
 
                 node = spoilerContainer;
diff --git a/src/components/views/settings/discovery/DiscoverySettings.tsx b/src/components/views/settings/discovery/DiscoverySettings.tsx
index d240d53d7c..6fbc91874c 100644
--- a/src/components/views/settings/discovery/DiscoverySettings.tsx
+++ b/src/components/views/settings/discovery/DiscoverySettings.tsx
@@ -51,7 +51,6 @@ export const DiscoverySettings: React.FC = () => {
     const [emails, setEmails] = useState([]);
     const [phoneNumbers, setPhoneNumbers] = useState([]);
     const [idServerName, setIdServerName] = useState(abbreviateUrl(client.getIdentityServerUrl()));
-    const [canMake3pidChanges, setCanMake3pidChanges] = useState(false);
 
     const [requiredPolicyInfo, setRequiredPolicyInfo] = useState({
         // This object is passed along to a component for handling
@@ -88,11 +87,6 @@ export const DiscoverySettings: React.FC = () => {
             try {
                 await getThreepidState();
 
-                const capabilities = await client.getCapabilities();
-                setCanMake3pidChanges(
-                    !capabilities["m.3pid_changes"] || capabilities["m.3pid_changes"].enabled === true,
-                );
-
                 // By starting the terms flow we get the logic for checking which terms the user has signed
                 // for free. So we might as well use that for our own purposes.
                 const idServerUrl = client.getIdentityServerUrl();
@@ -166,7 +160,7 @@ export const DiscoverySettings: React.FC = () => {
                         medium={ThreepidMedium.Email}
                         threepids={emails}
                         onChange={getThreepidState}
-                        disabled={!canMake3pidChanges}
+                        disabled={!hasTerms}
                         isLoading={isLoadingThreepids}
                     />
                 
@@ -180,7 +174,7 @@ export const DiscoverySettings: React.FC = () => {
                         medium={ThreepidMedium.Phone}
                         threepids={phoneNumbers}
                         onChange={getThreepidState}
-                        disabled={!canMake3pidChanges}
+                        disabled={!hasTerms}
                         isLoading={isLoadingThreepids}
                     />
                 
diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json
index 50880392cf..7647377196 100644
--- a/src/i18n/strings/cs.json
+++ b/src/i18n/strings/cs.json
@@ -207,7 +207,6 @@
         "failed_query_registration_methods": "Nepovedlo se načíst podporované způsoby přihlášení.",
         "failed_soft_logout_auth": "Nepovedlo se autentifikovat",
         "failed_soft_logout_homeserver": "Kvůli problémům s domovským server se nepovedlo autentifikovat znovu",
-        "footer_powered_by_matrix": "používá protokol Matrix",
         "forgot_password_email_invalid": "E-mailová adresa se nezdá být platná.",
         "forgot_password_email_required": "Musíte zadat e-mailovou adresu spojenou s vaším účtem.",
         "forgot_password_prompt": "Zapomněli jste heslo?",
@@ -3525,7 +3524,6 @@
     "truncated_list_n_more": {
         "other": "A %(count)s dalších..."
     },
-    "unknown_device": "Neznámé zařízení",
     "unsupported_server_description": "Tento server používá starší verzi Matrix. Chcete-li používat %(brand)s bez možných problémů, aktualizujte Matrixu na %(version)s .",
     "unsupported_server_title": "Váš server není podporován",
     "update": {
diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index f349f59103..abe4566f8c 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -203,7 +203,6 @@
         "failed_query_registration_methods": "Konnte unterstützte Registrierungsmethoden nicht abrufen.",
         "failed_soft_logout_auth": "Erneute Authentifizierung fehlgeschlagen",
         "failed_soft_logout_homeserver": "Erneute Authentifizierung aufgrund eines Problems des Heim-Servers fehlgeschlagen",
-        "footer_powered_by_matrix": "Betrieben mit Matrix",
         "forgot_password_email_invalid": "E-Mail-Adresse scheint ungültig zu sein.",
         "forgot_password_email_required": "Es muss die mit dem Benutzerkonto verbundene E-Mail-Adresse eingegeben werden.",
         "forgot_password_prompt": "Passwort vergessen?",
@@ -3500,7 +3499,6 @@
     "truncated_list_n_more": {
         "other": "Und %(count)s weitere …"
     },
-    "unknown_device": "Unbekanntes Gerät",
     "unsupported_server_description": "Dieser Server nutzt eine ältere Matrix-Version. Aktualisiere auf Matrix %(version)s, um %(brand)s fehlerfrei nutzen zu können.",
     "unsupported_server_title": "Dein Server wird nicht unterstützt",
     "update": {
diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json
index 94fb2221fb..2c042c0dc3 100644
--- a/src/i18n/strings/el.json
+++ b/src/i18n/strings/el.json
@@ -182,7 +182,6 @@
         "failed_query_registration_methods": "Αδυναμία λήψης των υποστηριζόμενων μεθόδων εγγραφής.",
         "failed_soft_logout_auth": "Απέτυχε ο εκ νέου έλεγχος ταυτότητας",
         "failed_soft_logout_homeserver": "Απέτυχε ο εκ νέου έλεγχος ταυτότητας λόγω προβλήματος με τον κεντρικό διακομιστή",
-        "footer_powered_by_matrix": "λειτουργεί με το Matrix",
         "forgot_password_email_invalid": "Η διεύθυνση email δε φαίνεται να είναι έγκυρη.",
         "forgot_password_email_required": "Πρέπει να εισηχθεί η διεύθυνση ηλ. αλληλογραφίας που είναι συνδεδεμένη με τον λογαριασμό σας.",
         "forgot_password_prompt": "Ξεχάσετε τον κωδικό σας;",
@@ -2829,7 +2828,6 @@
     "truncated_list_n_more": {
         "other": "Και %(count)s ακόμα..."
     },
-    "unknown_device": "Άγνωστη συσκευή",
     "update": {
         "changelog": "Αλλαγές",
         "check_action": "Έλεγχος για ενημέρωση",
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 1ad73fff8a..b17233c9d2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -209,7 +209,6 @@
         "failed_query_registration_methods": "Unable to query for supported registration methods.",
         "failed_soft_logout_auth": "Failed to re-authenticate",
         "failed_soft_logout_homeserver": "Failed to re-authenticate due to a homeserver problem",
-        "footer_powered_by_matrix": "powered by Matrix",
         "forgot_password_email_invalid": "The email address doesn't appear to be valid.",
         "forgot_password_email_required": "The email address linked to your account must be entered.",
         "forgot_password_prompt": "Forgotten your password?",
@@ -3706,7 +3705,6 @@
     "truncated_list_n_more": {
         "other": "And %(count)s more..."
     },
-    "unknown_device": "Unknown device",
     "unsupported_browser": {
         "description": "If you continue, some features may stop working and there is a risk that you may lose data in the future. Update your browser to continue using %(brand)s.",
         "title": "%(brand)s does not support this browser"
diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json
index fa73a0963d..33d8e57f48 100644
--- a/src/i18n/strings/eo.json
+++ b/src/i18n/strings/eo.json
@@ -171,7 +171,6 @@
         "failed_query_registration_methods": "Ne povas peti subtenatajn registrajn metodojn.",
         "failed_soft_logout_auth": "Malsukcesis reaŭtentikigi",
         "failed_soft_logout_homeserver": "Malsukcesis reaŭtentikigi pro hejmservila problemo",
-        "footer_powered_by_matrix": "funkciigata de Matrix",
         "forgot_password_email_invalid": "La retpoŝtadreso ŝajnas ne valida.",
         "forgot_password_email_required": "Vi devas enigi retpoŝtadreson ligitan al via konto.",
         "forgot_password_prompt": "Ĉu vi forgesis vian pasvorton?",
@@ -2544,7 +2543,6 @@
     "truncated_list_n_more": {
         "other": "Kaj %(count)s pliaj…"
     },
-    "unknown_device": "Nekonata aparato",
     "update": {
         "changelog": "Protokolo de ŝanĝoj",
         "check_action": "Kontroli ĝisdatigojn",
diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json
index 37ca0c1ca6..cb6a8557b3 100644
--- a/src/i18n/strings/es.json
+++ b/src/i18n/strings/es.json
@@ -193,7 +193,6 @@
         "failed_query_registration_methods": "No se pueden consultar los métodos de registro admitidos.",
         "failed_soft_logout_auth": "No se pudo volver a autenticar",
         "failed_soft_logout_homeserver": "No ha sido posible volver a autenticarse debido a un problema con el servidor base",
-        "footer_powered_by_matrix": "con el poder de Matrix",
         "forgot_password_email_invalid": "La dirección de correo no parece ser válida.",
         "forgot_password_email_required": "Debes ingresar la dirección de correo electrónico vinculada a tu cuenta.",
         "forgot_password_prompt": "¿Olvidaste tu contraseña?",
@@ -3224,7 +3223,6 @@
     "truncated_list_n_more": {
         "other": "Y %(count)s más…"
     },
-    "unknown_device": "Dispositivo desconocido",
     "update": {
         "changelog": "Registro de cambios",
         "check_action": "Comprobar si hay actualizaciones",
diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 3e3469d2f5..b05d9024c0 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -207,7 +207,6 @@
         "failed_query_registration_methods": "Ei õnnestunud pärida toetatud registreerimismeetodite loendit.",
         "failed_soft_logout_auth": "Uuesti autentimine ei õnnestunud",
         "failed_soft_logout_homeserver": "Uuesti autentimine ei õnnestunud koduserveri vea tõttu",
-        "footer_powered_by_matrix": "põhineb Matrix'il",
         "forgot_password_email_invalid": "See e-posti aadress ei tundu olema korrektne.",
         "forgot_password_email_required": "Sa pead sisestama oma kontoga seotud e-posti aadressi.",
         "forgot_password_prompt": "Kas sa unustasid oma salasõna?",
@@ -3465,7 +3464,6 @@
     "truncated_list_n_more": {
         "other": "Ja %(count)s muud..."
     },
-    "unknown_device": "Tundmatu seade",
     "unsupported_server_description": "See server kasutab Matrixi vanemat versiooni. Selleks, et %(brand)s'i kasutamisel vigu ei tekiks palun uuenda serverit nii, et kasutusel oleks Matrixi %(version)s.",
     "unsupported_server_title": "Sinu server ei ole toetatud",
     "update": {
diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json
index 4a013a6795..5541bbbfbd 100644
--- a/src/i18n/strings/fa.json
+++ b/src/i18n/strings/fa.json
@@ -165,7 +165,6 @@
         "failed_query_registration_methods": "درخواست از روش‌های پشتیبانی‌شده‌ی ثبت‌نام میسر نیست.",
         "failed_soft_logout_auth": "احراز هویت مجدد موفیت‌آمیز نبود",
         "failed_soft_logout_homeserver": "به دلیل مشکلی که در سرور وجود دارد ، احراز هویت مجدد انجام نشد",
-        "footer_powered_by_matrix": "قدرت‌یافته از ماتریکس",
         "forgot_password_email_required": "آدرس ایمیلی که به حساب کاربری شما متصل است، باید وارد شود.",
         "forgot_password_prompt": "گذرواژه‌ی خود را فراموش کردید؟",
         "identifier_label": "نحوه ورود",
@@ -2230,7 +2229,6 @@
     "truncated_list_n_more": {
         "other": "و %(count)s مورد بیشتر ..."
     },
-    "unknown_device": "دستگاه ناشناخته",
     "update": {
         "changelog": "تغییراتِ به‌وجودآمده",
         "check_action": "بررسی برای به‌روزرسانی جدید",
diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json
index c77eac1cef..091761af4b 100644
--- a/src/i18n/strings/fi.json
+++ b/src/i18n/strings/fi.json
@@ -194,7 +194,6 @@
         "failed_query_registration_methods": "Tuettuja rekisteröitymistapoja ei voitu kysellä.",
         "failed_soft_logout_auth": "Uudelleenautentikointi epäonnistui",
         "failed_soft_logout_homeserver": "Uudelleenautentikointi epäonnistui kotipalvelinongelmasta johtuen",
-        "footer_powered_by_matrix": "moottorina Matrix",
         "forgot_password_email_invalid": "Sähköpostiosoite ei vaikuta kelvolliselta.",
         "forgot_password_email_required": "Sinun pitää syöttää tiliisi liitetty sähköpostiosoite.",
         "forgot_password_prompt": "Unohditko salasanasi?",
@@ -3100,7 +3099,6 @@
     "truncated_list_n_more": {
         "other": "Ja %(count)s muuta..."
     },
-    "unknown_device": "Tuntematon laite",
     "update": {
         "changelog": "Muutosloki",
         "check_action": "Tarkista päivitykset",
diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index 26136a9cf0..905aa11fa7 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -209,7 +209,6 @@
         "failed_query_registration_methods": "Impossible de demander les méthodes d’inscription prises en charge.",
         "failed_soft_logout_auth": "Échec de la ré-authentification",
         "failed_soft_logout_homeserver": "Échec de la ré-authentification à cause d’un problème du serveur d’accueil",
-        "footer_powered_by_matrix": "propulsé par Matrix",
         "forgot_password_email_invalid": "L’adresse e-mail semble être invalide.",
         "forgot_password_email_required": "L’adresse e-mail liée à votre compte doit être renseignée.",
         "forgot_password_prompt": "Mot de passe oublié ?",
@@ -3625,7 +3624,6 @@
     "truncated_list_n_more": {
         "other": "Et %(count)s autres…"
     },
-    "unknown_device": "Appareil inconnu",
     "unsupported_server_description": "Ce serveur utilise une ancienne version de Matrix. Mettez-le à jour vers Matrix %(version)s pour utiliser %(brand)s sans erreurs.",
     "unsupported_server_title": "Votre serveur n’est pas pris en charge",
     "update": {
diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index 0edf2d71a3..00277a5f3e 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -184,7 +184,6 @@
         "failed_query_registration_methods": "Non se puido consultar os métodos de rexistro soportados.",
         "failed_soft_logout_auth": "Fallo na reautenticación",
         "failed_soft_logout_homeserver": "Fallo ó reautenticar debido a un problema no servidor",
-        "footer_powered_by_matrix": "funciona grazas a Matrix",
         "forgot_password_email_invalid": "O enderezo de email non semella ser válido.",
         "forgot_password_email_required": "Debe introducir o correo electrónico ligado a súa conta.",
         "forgot_password_prompt": "¿Esqueceches o contrasinal?",
@@ -2993,7 +2992,6 @@
     "truncated_list_n_more": {
         "other": "E %(count)s máis..."
     },
-    "unknown_device": "Dispositivo descoñecido",
     "update": {
         "changelog": "Rexistro de cambios",
         "check_action": "Comprobar actualización",
diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json
index f1d25e7e40..c98d59c659 100644
--- a/src/i18n/strings/he.json
+++ b/src/i18n/strings/he.json
@@ -167,7 +167,6 @@
         "failed_query_registration_methods": "לא ניתן לשאול לשיטות רישום נתמכות.",
         "failed_soft_logout_auth": "האימות מחדש נכשל",
         "failed_soft_logout_homeserver": "האימות מחדש נכשל עקב בעיית שרת בית",
-        "footer_powered_by_matrix": "מופעל ע\"י Matrix",
         "forgot_password_email_required": "יש להזין את כתובת הדוא\"ל המקושרת לחשבונך.",
         "forgot_password_prompt": "שכחת את הסיסמה שלך?",
         "forgot_password_send_email": "שלח אימייל",
@@ -2396,7 +2395,6 @@
     "truncated_list_n_more": {
         "other": "ו%(count)s עוד..."
     },
-    "unknown_device": "מכשיר לא ידוע",
     "update": {
         "changelog": "דו\"ח שינויים",
         "check_action": "בדוק עדכונים",
diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 8cfd560620..cf410fb82f 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -202,7 +202,6 @@
         "failed_query_registration_methods": "A támogatott regisztrációs módokat nem lehet lekérdezni.",
         "failed_soft_logout_auth": "Újra bejelentkezés sikertelen",
         "failed_soft_logout_homeserver": "Az újbóli hitelesítés a Matrix-kiszolgáló hibájából sikertelen",
-        "footer_powered_by_matrix": "a gépházban: Matrix",
         "forgot_password_email_invalid": "Az e-mail cím nem tűnik érvényesnek.",
         "forgot_password_email_required": "A fiókodhoz kötött e-mail címet add meg.",
         "forgot_password_prompt": "Elfelejtetted a jelszavad?",
@@ -3436,7 +3435,6 @@
     "truncated_list_n_more": {
         "other": "És még %(count)s..."
     },
-    "unknown_device": "Ismeretlen eszköz",
     "unsupported_server_description": "Ez a kiszolgáló a Matrix régebbi verzióját használja. Frissítsen a Matrix %(version)s verzióra az %(brand)s hibamentes használatához.",
     "unsupported_server_title": "A kiszolgálója nem támogatott",
     "update": {
diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json
index 9dc9979d52..77cdd8a78f 100644
--- a/src/i18n/strings/id.json
+++ b/src/i18n/strings/id.json
@@ -202,7 +202,6 @@
         "failed_query_registration_methods": "Tidak dapat menanyakan metode pendaftaran yang didukung.",
         "failed_soft_logout_auth": "Gagal untuk mengautentikasi ulang",
         "failed_soft_logout_homeserver": "Gagal untuk mengautentikasi ulang karena masalah homeserver",
-        "footer_powered_by_matrix": "diberdayakan oleh Matrix",
         "forgot_password_email_invalid": "Alamat email ini tidak terlihat absah.",
         "forgot_password_email_required": "Alamat email yang tertaut ke akun Anda harus dimasukkan.",
         "forgot_password_prompt": "Lupa kata sandi Anda?",
@@ -3469,7 +3468,6 @@
     "truncated_list_n_more": {
         "other": "Dan %(count)s lagi..."
     },
-    "unknown_device": "Perangkat tidak diketahui",
     "unsupported_server_description": "Server ini menjalankan sebuah versi Matrix yang lama. Tingkatkan ke Matrix %(version)s untuk menggunakan %(brand)s tanpa eror.",
     "unsupported_server_title": "Server Anda tidak didukung",
     "update": {
diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json
index cc51c25b21..c746caef1d 100644
--- a/src/i18n/strings/is.json
+++ b/src/i18n/strings/is.json
@@ -184,7 +184,6 @@
         "failed_connect_identity_server": "Næ ekki sambandi við auðkennisþjón",
         "failed_soft_logout_auth": "Tókst ekki að endurauðkenna",
         "failed_soft_logout_homeserver": "Tókst ekki að endurauðkenna vegna vandamála með heimaþjón",
-        "footer_powered_by_matrix": "keyrt með Matrix",
         "forgot_password_email_invalid": "Tölvupóstfangið lítur ekki út fyrir að vera í lagi.",
         "forgot_password_email_required": "Það þarf að setja inn tölvupóstfangið sem tengt er notandaaðgangnum þínum.",
         "forgot_password_prompt": "Gleymdirðu lykilorðinu þínu?",
@@ -2902,7 +2901,6 @@
     "truncated_list_n_more": {
         "other": "Og %(count)s til viðbótar..."
     },
-    "unknown_device": "Óþekkt tæki",
     "update": {
         "changelog": "Breytingaskrá",
         "check_action": "Athuga með uppfærslu",
diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index 540799f69a..9f0ab6d430 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -207,7 +207,6 @@
         "failed_query_registration_methods": "Impossibile richiedere i metodi di registrazione supportati.",
         "failed_soft_logout_auth": "Riautenticazione fallita",
         "failed_soft_logout_homeserver": "Riautenticazione fallita per un problema dell'homeserver",
-        "footer_powered_by_matrix": "offerto da Matrix",
         "forgot_password_email_invalid": "L'indirizzo email non sembra essere valido.",
         "forgot_password_email_required": "Deve essere inserito l'indirizzo email collegato al tuo account.",
         "forgot_password_prompt": "Hai dimenticato la password?",
@@ -3518,7 +3517,6 @@
     "truncated_list_n_more": {
         "other": "E altri %(count)s ..."
     },
-    "unknown_device": "Dispositivo sconosciuto",
     "unsupported_server_description": "Questo server usa una versione più vecchia di Matrix. Aggiorna a Matrix %(version)s per usare %(brand)s senza errori.",
     "unsupported_server_title": "Il tuo server non è supportato",
     "update": {
diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json
index 89d029a6e5..963de355ac 100644
--- a/src/i18n/strings/ja.json
+++ b/src/i18n/strings/ja.json
@@ -3231,7 +3231,6 @@
     "truncated_list_n_more": {
         "other": "他%(count)s人以上…"
     },
-    "unknown_device": "不明な端末",
     "update": {
         "changelog": "更新履歴",
         "check_action": "更新を確認",
diff --git a/src/i18n/strings/lo.json b/src/i18n/strings/lo.json
index e03afdc2f4..6310faf200 100644
--- a/src/i18n/strings/lo.json
+++ b/src/i18n/strings/lo.json
@@ -181,7 +181,6 @@
         "failed_query_registration_methods": "ບໍ່ສາມາດສອບຖາມວິທີການລົງທະບຽນໄດ້.",
         "failed_soft_logout_auth": "ການພິສູດຢືນຢັນຄືນໃໝ່ບໍ່ສຳເລັດ",
         "failed_soft_logout_homeserver": "ການພິສູດຢືນຢັນຄືນໃໝ່ເນື່ອງຈາກບັນຫາ homeserver ບໍ່ສຳເລັດ",
-        "footer_powered_by_matrix": "ຂັບເຄື່ອນໂດຍ Matrix",
         "forgot_password_email_invalid": "ທີ່ຢູ່ອີເມວບໍ່ຖືກຕ້ອງ.",
         "forgot_password_email_required": "ຕ້ອງໃສ່ທີ່ຢູ່ອີເມວທີ່ເຊື່ອມຕໍ່ກັບບັນຊີຂອງທ່ານ.",
         "forgot_password_prompt": "ລືມລະຫັດຜ່ານຂອງທ່ານບໍ?",
@@ -2845,7 +2844,6 @@
     "truncated_list_n_more": {
         "other": "ແລະ %(count)sອີກ..."
     },
-    "unknown_device": "ທີ່ບໍ່ຮູ້ຈັກອຸປະກອນນີ້",
     "update": {
         "changelog": "ບັນທຶກການປ່ຽນແປງ",
         "check_action": "ກວດເບິ່ງເພຶ່ອອັບເດດ",
diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json
index f1e66cc43f..8f503bacdd 100644
--- a/src/i18n/strings/lt.json
+++ b/src/i18n/strings/lt.json
@@ -152,7 +152,6 @@
         "failed_connect_identity_server_register": "Jūs galite registruotis, tačiau kai kurios funkcijos bus nepasiekiamos, kol tapatybės serveris prisijungs. Jei ir toliau matote šį įspėjimą, patikrinkite savo konfigūraciją arba susisiekite su serverio administratoriumi.",
         "failed_connect_identity_server_reset_password": "Jūs galite iš naujo nustatyti savo slaptažodį, tačiau kai kurios funkcijos bus nepasiekiamos, kol tapatybės serveris prisijungs. Jei ir toliau matote šį įspėjimą, patikrinkite savo konfigūraciją arba susisiekite su serverio administratoriumi.",
         "failed_homeserver_discovery": "Nepavyko atlikti serverio radimo",
-        "footer_powered_by_matrix": "veikia su Matrix",
         "forgot_password_email_required": "Privalo būti įvestas su jūsų paskyra susietas el. pašto adresas.",
         "forgot_password_prompt": "Pamiršote savo slaptažodį?",
         "identifier_label": "Prisijungti naudojant",
@@ -2285,7 +2284,6 @@
     "truncated_list_n_more": {
         "other": "Ir dar %(count)s..."
     },
-    "unknown_device": "Nežinomas įrenginys",
     "update": {
         "changelog": "Keitinių žurnalas",
         "check_action": "Tikrinti, ar yra atnaujinimų",
diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index 35de7da36c..1231a9a330 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -196,7 +196,6 @@
         "failed_query_registration_methods": "Kan ondersteunde registratiemethoden niet opvragen.",
         "failed_soft_logout_auth": "Opnieuw inloggen is mislukt",
         "failed_soft_logout_homeserver": "Opnieuw inloggen is mislukt wegens een probleem met de homeserver",
-        "footer_powered_by_matrix": "draait op Matrix",
         "forgot_password_email_invalid": "Dit e-mailadres lijkt niet geldig te zijn.",
         "forgot_password_email_required": "Het aan jouw account gekoppelde e-mailadres dient ingevoerd worden.",
         "forgot_password_prompt": "Wachtwoord vergeten?",
@@ -3046,7 +3045,6 @@
     "truncated_list_n_more": {
         "other": "En %(count)s meer…"
     },
-    "unknown_device": "Onbekend apparaat",
     "update": {
         "changelog": "Wijzigingslogboek",
         "check_action": "Controleren op updates",
diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json
index 890fa93d34..e148c873f1 100644
--- a/src/i18n/strings/pl.json
+++ b/src/i18n/strings/pl.json
@@ -209,7 +209,6 @@
         "failed_query_registration_methods": "Nie można uzyskać wspieranych metod rejestracji.",
         "failed_soft_logout_auth": "Nie udało się uwierzytelnić ponownie",
         "failed_soft_logout_homeserver": "Nie udało się uwierzytelnić ponownie z powodu błędu serwera domowego",
-        "footer_powered_by_matrix": "napędzany przez Matrix",
         "forgot_password_email_invalid": "Adres e-mail nie wygląda na prawidłowy.",
         "forgot_password_email_required": "Musisz wpisać adres e-mail połączony z twoim kontem.",
         "forgot_password_prompt": "Nie pamiętasz hasła?",
@@ -2049,14 +2048,6 @@
             "button_view_all": "Pokaż wszystkie",
             "description": "Ten pokój ma przypięte wiadomości. Kliknij, aby je wyświetlić.",
             "go_to_message": "Wyświetl przypiętą wiadomość na osi czasu.",
-            "prefix": {
-                "audio": "Audio",
-                "file": "Plik",
-                "image": "Obraz",
-                "poll": "Ankieta",
-                "video": "Wideo"
-            },
-            "preview": "%(prefix)s: %(preview)s",
             "title": "%(index)s z %(length)s przypiętych wiadomości"
         },
         "read_topic": "Kliknij, aby przeczytać temat",
@@ -3720,7 +3711,6 @@
     "truncated_list_n_more": {
         "other": "I %(count)s więcej…"
     },
-    "unknown_device": "Nieznane urządzenie",
     "unsupported_browser": {
         "description": "Jeśli kontynuujesz, niektóre funkcje mogą przestać działać, jak i istnieje ryzyko utraty danych w przyszłości. Zaktualizuj przeglądarkę, aby nadal używać %(brand)s.",
         "title": "%(brand)s nie wspiera tej przeglądarki"
diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json
index 274512774e..6789fb4ee6 100644
--- a/src/i18n/strings/pt_BR.json
+++ b/src/i18n/strings/pt_BR.json
@@ -169,7 +169,6 @@
         "failed_query_registration_methods": "Não foi possível consultar as opções de registro suportadas.",
         "failed_soft_logout_auth": "Falha em autenticar novamente",
         "failed_soft_logout_homeserver": "Falha em autenticar novamente devido à um problema no servidor local",
-        "footer_powered_by_matrix": "oferecido por Matrix",
         "forgot_password_email_required": "O e-mail vinculado à sua conta precisa ser informado.",
         "forgot_password_prompt": "Esqueceu sua senha?",
         "identifier_label": "Entrar com",
@@ -2446,7 +2445,6 @@
     "truncated_list_n_more": {
         "other": "E %(count)s mais..."
     },
-    "unknown_device": "Dispositivo desconhecido",
     "update": {
         "changelog": "Registro de alterações",
         "check_action": "Verificar atualizações",
diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index e37e37c52f..50a3ee50e9 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -202,7 +202,6 @@
         "failed_query_registration_methods": "Невозможно запросить поддерживаемые методы регистрации.",
         "failed_soft_logout_auth": "Ошибка повторной аутентификации",
         "failed_soft_logout_homeserver": "Ошибка повторной аутентификации из-за проблем на сервере",
-        "footer_powered_by_matrix": "основано на Matrix",
         "forgot_password_email_invalid": "Адрес электронной почты не является действительным.",
         "forgot_password_email_required": "Введите адрес электронной почты, связанный с вашей учётной записью.",
         "forgot_password_prompt": "Забыли Ваш пароль?",
@@ -3503,7 +3502,6 @@
     "truncated_list_n_more": {
         "other": "Еще %(count)s…"
     },
-    "unknown_device": "Неизвестное устройство",
     "unsupported_server_description": "На этом сервере используется старая версия Matrix. Перейдите на Matrix%(version)s, чтобы использовать %(brand)s ее без ошибок.",
     "unsupported_server_title": "Ваш сервер не поддерживается",
     "update": {
diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json
index d48879ac38..34ba4789bf 100644
--- a/src/i18n/strings/sk.json
+++ b/src/i18n/strings/sk.json
@@ -202,7 +202,6 @@
         "failed_query_registration_methods": "Nie je možné požiadať o podporované metódy registrácie.",
         "failed_soft_logout_auth": "Nepodarilo sa opätovne overiť",
         "failed_soft_logout_homeserver": "Opätovná autentifikácia zlyhala kvôli problému domovského servera",
-        "footer_powered_by_matrix": "používa protokol Matrix",
         "forgot_password_email_invalid": "Zdá sa, že e-mailová adresa nie je platná.",
         "forgot_password_email_required": "Musíte zadať emailovú adresu prepojenú s vašim účtom.",
         "forgot_password_prompt": "Zabudli ste heslo?",
@@ -3532,7 +3531,6 @@
     "truncated_list_n_more": {
         "other": "A %(count)s ďalších…"
     },
-    "unknown_device": "Neznáme zariadenie",
     "unsupported_server_description": "Tento server používa staršiu verziu systému Matrix. Ak chcete používať %(brand)s bez chýb, aktualizujte na Matrix %(version)s.",
     "unsupported_server_title": "Váš server nie je podporovaný",
     "update": {
diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index 419129c9c8..b7258c26cb 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -195,7 +195,6 @@
         "failed_query_registration_methods": "S’arrihet të kërkohet për metoda regjistrimi që mbulohen.",
         "failed_soft_logout_auth": "S’u arrit të ribëhej mirëfilltësimi",
         "failed_soft_logout_homeserver": "S’u arrit të ribëhej mirëfilltësimi, për shkak të një problemi me shërbyesin Home",
-        "footer_powered_by_matrix": "bazuar në Matrix",
         "forgot_password_email_invalid": "Adresa email s’duket të jetë e vlefshme.",
         "forgot_password_email_required": "Duhet dhënë adresa email e lidhur me llogarinë tuaj.",
         "forgot_password_prompt": "Harruat fjalëkalimin tuaj?",
@@ -3298,7 +3297,6 @@
     "truncated_list_n_more": {
         "other": "Dhe %(count)s të tjerë…"
     },
-    "unknown_device": "Pajisje e panjohur",
     "update": {
         "changelog": "Regjistër ndryshimesh",
         "check_action": "Kontrollo për përditësime",
diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index af236f8ed9..bb4e489952 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -207,7 +207,6 @@
         "failed_query_registration_methods": "Kunde inte fråga efter stödda registreringsmetoder.",
         "failed_soft_logout_auth": "Misslyckades att återautentisera",
         "failed_soft_logout_homeserver": "Misslyckades att återautentisera p.g.a. ett hemserverproblem",
-        "footer_powered_by_matrix": "drivs av Matrix",
         "forgot_password_email_invalid": "Den här e-postadressen ser inte giltig ut.",
         "forgot_password_email_required": "E-postadressen som är kopplad till ditt konto måste anges.",
         "forgot_password_prompt": "Glömt ditt lösenord?",
@@ -3518,7 +3517,6 @@
     "truncated_list_n_more": {
         "other": "Och %(count)s till…"
     },
-    "unknown_device": "Okänd enhet",
     "unsupported_server_description": "Servern använder en äldre version av Matrix. Uppgradera till Matrix %(version)s för att använda %(brand)s utan fel.",
     "unsupported_server_title": "Din server stöds inte",
     "update": {
diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index 798798e5de..7d438ec2a4 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -201,7 +201,6 @@
         "failed_query_registration_methods": "Не вдалося запитати підтримувані способи реєстрації.",
         "failed_soft_logout_auth": "Не вдалося перезайти",
         "failed_soft_logout_homeserver": "Не вдалося перезайти через проблему з домашнім сервером",
-        "footer_powered_by_matrix": "працює на Matrix",
         "forgot_password_email_invalid": "Хибна адреса е-пошти.",
         "forgot_password_email_required": "Введіть е-пошту, прив'язану до вашого облікового запису.",
         "forgot_password_prompt": "Забули свій пароль?",
@@ -3430,7 +3429,6 @@
     "truncated_list_n_more": {
         "other": "І ще %(count)s..."
     },
-    "unknown_device": "Невідомий пристрій",
     "unsupported_server_description": "Цей сервер використовує стару версію Matrix. Оновіть Matrix до %(version)s, щоб використовувати %(brand)s без помилок.",
     "unsupported_server_title": "Ваш сервер не підтримується",
     "update": {
diff --git a/src/i18n/strings/vi.json b/src/i18n/strings/vi.json
index d27ee1fbac..5ce36aed05 100644
--- a/src/i18n/strings/vi.json
+++ b/src/i18n/strings/vi.json
@@ -195,7 +195,6 @@
         "failed_query_registration_methods": "Không thể truy vấn các phương pháp đăng ký được hỗ trợ.",
         "failed_soft_logout_auth": "Không xác thực lại được",
         "failed_soft_logout_homeserver": "Không xác thực lại được do sự cố máy chủ",
-        "footer_powered_by_matrix": "cung cấp bởi Matrix",
         "forgot_password_email_invalid": "Địa chỉ thư điện tử dường như không hợp lệ.",
         "forgot_password_email_required": "Địa chỉ thư điện tử được liên kết đến tài khoản của bạn phải được nhập.",
         "forgot_password_prompt": "Quên mật khẩu của bạn?",
@@ -3173,7 +3172,6 @@
     "truncated_list_n_more": {
         "other": "Và %(count)s thêm…"
     },
-    "unknown_device": "Thiết bị không xác định",
     "update": {
         "changelog": "Lịch sử thay đổi",
         "check_action": "Kiểm tra cập nhật",
diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json
index bcffb66f60..99d5586a5c 100644
--- a/src/i18n/strings/zh_Hans.json
+++ b/src/i18n/strings/zh_Hans.json
@@ -202,7 +202,6 @@
         "failed_query_registration_methods": "无法查询支持的注册方法。",
         "failed_soft_logout_auth": "重新认证失败",
         "failed_soft_logout_homeserver": "由于家服务器的问题,重新认证失败",
-        "footer_powered_by_matrix": "由 Matrix 驱动",
         "forgot_password_email_invalid": "电子邮件地址似乎无效。",
         "forgot_password_email_required": "必须输入和你账户关联的邮箱地址。",
         "forgot_password_prompt": "忘记你的密码了吗?",
@@ -3153,7 +3152,6 @@
     "truncated_list_n_more": {
         "other": "和 %(count)s 个其他…"
     },
-    "unknown_device": "未知设备",
     "update": {
         "changelog": "更改日志",
         "check_action": "检查更新",
diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index 40147ac43c..68b3694ee8 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -201,7 +201,6 @@
         "failed_query_registration_methods": "無法查詢支援的註冊方法。",
         "failed_soft_logout_auth": "無法重新驗證",
         "failed_soft_logout_homeserver": "因為家伺服器的問題,所以無法重新驗證",
-        "footer_powered_by_matrix": "由 Matrix 提供",
         "forgot_password_email_invalid": "電子郵件地址似乎無效。",
         "forgot_password_email_required": "必須輸入和您帳號綁定的電子郵件地址。",
         "forgot_password_prompt": "忘記您的密碼了?",
@@ -3421,7 +3420,6 @@
     "truncated_list_n_more": {
         "other": "與更多 %(count)s 個…"
     },
-    "unknown_device": "未知裝置",
     "unsupported_server_description": "此伺服器正在使用較舊版本的 Matrix。升級至 Matrix %(version)s 以在沒有錯誤的情況下使用 %(brand)s。",
     "unsupported_server_title": "您的伺服器不支援",
     "update": {
diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx
index 2870ccafd3..9a6bb93bba 100644
--- a/src/utils/exportUtils/HtmlExport.tsx
+++ b/src/utils/exportUtils/HtmlExport.tsx
@@ -7,12 +7,13 @@ Please see LICENSE files in the repository root for full details.
 */
 
 import React from "react";
-import ReactDOM from "react-dom";
+import { createRoot } from "react-dom/client";
 import { Room, MatrixEvent, EventType, MsgType } from "matrix-js-sdk/src/matrix";
 import { renderToStaticMarkup } from "react-dom/server";
 import { logger } from "matrix-js-sdk/src/logger";
 import escapeHtml from "escape-html";
 import { TooltipProvider } from "@vector-im/compound-web";
+import { defer } from "matrix-js-sdk/src/utils";
 
 import Exporter from "./Exporter";
 import { mediaFromMxc } from "../../customisations/Media";
@@ -263,7 +264,7 @@ export default class HTMLExporter extends Exporter {
         return wantsDateSeparator(prevEvent.getDate() || undefined, event.getDate() || undefined);
     }
 
-    public getEventTile(mxEv: MatrixEvent, continuation: boolean): JSX.Element {
+    public getEventTile(mxEv: MatrixEvent, continuation: boolean, ref?: () => void): JSX.Element {
         return (
             
@@ -287,6 +288,7 @@ export default class HTMLExporter extends Exporter { layout={Layout.Group} showReadReceipts={false} getRelationsForEvent={this.getRelationsForEvent} + ref={ref} /> @@ -298,7 +300,10 @@ export default class HTMLExporter extends Exporter { const avatarUrl = this.getAvatarURL(mxEv); const hasAvatar = !!avatarUrl; if (hasAvatar) await this.saveAvatarIfNeeded(mxEv); - const EventTile = this.getEventTile(mxEv, continuation); + // We have to wait for the component to be rendered before we can get the markup + // so pass a deferred as a ref to the component. + const deferred = defer(); + const EventTile = this.getEventTile(mxEv, continuation, deferred.resolve); let eventTileMarkup: string; if ( @@ -308,9 +313,12 @@ export default class HTMLExporter extends Exporter { ) { // to linkify textual events, we'll need lifecycle methods which won't be invoked in renderToString // So, we'll have to render the component into a temporary root element - const tempRoot = document.createElement("div"); - ReactDOM.render(EventTile, tempRoot); - eventTileMarkup = tempRoot.innerHTML; + const tempElement = document.createElement("div"); + const tempRoot = createRoot(tempElement); + tempRoot.render(EventTile); + await deferred.promise; + eventTileMarkup = tempElement.innerHTML; + tempRoot.unmount(); } else { eventTileMarkup = renderToStaticMarkup(EventTile); } diff --git a/src/utils/pillify.tsx b/src/utils/pillify.tsx index 063012d16f..1859e90fd6 100644 --- a/src/utils/pillify.tsx +++ b/src/utils/pillify.tsx @@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details. */ import React, { StrictMode } from "react"; -import ReactDOM from "react-dom"; import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { MatrixClient, MatrixEvent, RuleId } from "matrix-js-sdk/src/matrix"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -16,6 +15,7 @@ import SettingsStore from "../settings/SettingsStore"; import { Pill, pillRoomNotifLen, pillRoomNotifPos, PillType } from "../components/views/elements/Pill"; import { parsePermalink } from "./permalinks/Permalinks"; import { PermalinkParts } from "./permalinks/PermalinkConstructor"; +import { ReactRootManager } from "./react"; /** * A node here is an A element with a href attribute tag. @@ -48,7 +48,7 @@ const shouldBePillified = (node: Element, href: string, parts: PermalinkParts | * to turn into pills. * @param {MatrixEvent} mxEvent - the matrix event which the DOM nodes are * part of representing. - * @param {Element[]} pills: an accumulator of the DOM nodes which contain + * @param {ReactRootManager} pills - an accumulator of the DOM nodes which contain * React components which have been mounted as part of this. * The initial caller should pass in an empty array to seed the accumulator. */ @@ -56,7 +56,7 @@ export function pillifyLinks( matrixClient: MatrixClient, nodes: ArrayLike, mxEvent: MatrixEvent, - pills: Element[], + pills: ReactRootManager, ): void { const room = matrixClient.getRoom(mxEvent.getRoomId()) ?? undefined; const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); @@ -64,7 +64,7 @@ export function pillifyLinks( while (node) { let pillified = false; - if (node.tagName === "PRE" || node.tagName === "CODE" || pills.includes(node)) { + if (node.tagName === "PRE" || node.tagName === "CODE" || pills.elements.includes(node)) { // Skip code blocks and existing pills node = node.nextSibling as Element; continue; @@ -83,9 +83,9 @@ export function pillifyLinks( ); - ReactDOM.render(pill, pillContainer); + pills.render(pill, pillContainer); + node.parentNode?.replaceChild(pillContainer, node); - pills.push(pillContainer); // Pills within pills aren't going to go well, so move on pillified = true; @@ -147,9 +147,8 @@ export function pillifyLinks( ); - ReactDOM.render(pill, pillContainer); + pills.render(pill, pillContainer); roomNotifTextNode.parentNode?.replaceChild(pillContainer, roomNotifTextNode); - pills.push(pillContainer); } // Nothing else to do for a text node (and we don't need to advance // the loop pointer because we did it above) @@ -165,20 +164,3 @@ export function pillifyLinks( node = node.nextSibling as Element; } } - -/** - * Unmount all the pill containers from React created by pillifyLinks. - * - * It's critical to call this after pillifyLinks, otherwise - * Pills will leak, leaking entire DOM trees via the event - * emitter on BaseAvatar as per - * https://github.com/vector-im/element-web/issues/12417 - * - * @param {Element[]} pills - array of pill containers whose React - * components should be unmounted. - */ -export function unmountPills(pills: Element[]): void { - for (const pillContainer of pills) { - ReactDOM.unmountComponentAtNode(pillContainer); - } -} diff --git a/src/utils/react.tsx b/src/utils/react.tsx new file mode 100644 index 0000000000..164d704d91 --- /dev/null +++ b/src/utils/react.tsx @@ -0,0 +1,37 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import { ReactNode } from "react"; +import { createRoot, Root } from "react-dom/client"; + +/** + * Utility class to render & unmount additional React roots, + * e.g. for pills, tooltips and other components rendered atop user-generated events. + */ +export class ReactRootManager { + private roots: Root[] = []; + private rootElements: Element[] = []; + + public get elements(): Element[] { + return this.rootElements; + } + + public render(children: ReactNode, element: Element): void { + const root = createRoot(element); + this.roots.push(root); + this.rootElements.push(element); + root.render(children); + } + + public unmount(): void { + while (this.roots.length) { + const root = this.roots.pop()!; + this.rootElements.pop(); + root.unmount(); + } + } +} diff --git a/src/utils/tooltipify.tsx b/src/utils/tooltipify.tsx index bcda256a9c..fc319b2024 100644 --- a/src/utils/tooltipify.tsx +++ b/src/utils/tooltipify.tsx @@ -7,11 +7,11 @@ Please see LICENSE files in the repository root for full details. */ import React, { StrictMode } from "react"; -import ReactDOM from "react-dom"; import { TooltipProvider } from "@vector-im/compound-web"; import PlatformPeg from "../PlatformPeg"; import LinkWithTooltip from "../components/views/elements/LinkWithTooltip"; +import { ReactRootManager } from "./react"; /** * If the platform enabled needsUrlTooltips, recurses depth-first through a DOM tree, adding tooltip previews @@ -19,12 +19,16 @@ import LinkWithTooltip from "../components/views/elements/LinkWithTooltip"; * * @param {Element[]} rootNodes - a list of sibling DOM nodes to traverse to try * to add tooltips. - * @param {Element[]} ignoredNodes: a list of nodes to not recurse into. - * @param {Element[]} containers: an accumulator of the DOM nodes which contain + * @param {Element[]} ignoredNodes - a list of nodes to not recurse into. + * @param {ReactRootManager} tooltips - an accumulator of the DOM nodes which contain * React components that have been mounted by this function. The initial caller * should pass in an empty array to seed the accumulator. */ -export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Element[], containers: Element[]): void { +export function tooltipifyLinks( + rootNodes: ArrayLike, + ignoredNodes: Element[], + tooltips: ReactRootManager, +): void { if (!PlatformPeg.get()?.needsUrlTooltips()) { return; } @@ -32,7 +36,7 @@ export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Ele let node = rootNodes[0]; while (node) { - if (ignoredNodes.includes(node) || containers.includes(node)) { + if (ignoredNodes.includes(node) || tooltips.elements.includes(node)) { node = node.nextSibling as Element; continue; } @@ -62,26 +66,11 @@ export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Ele ); - ReactDOM.render(tooltip, node); - containers.push(node); + tooltips.render(tooltip, node); } else if (node.childNodes?.length) { - tooltipifyLinks(node.childNodes as NodeListOf, ignoredNodes, containers); + tooltipifyLinks(node.childNodes as NodeListOf, ignoredNodes, tooltips); } node = node.nextSibling as Element; } } - -/** - * Unmount tooltip containers created by tooltipifyLinks. - * - * It's critical to call this after tooltipifyLinks, otherwise - * tooltips will leak. - * - * @param {Element[]} containers - array of tooltip containers to unmount - */ -export function unmountTooltips(containers: Element[]): void { - for (const container of containers) { - ReactDOM.unmountComponentAtNode(container); - } -} diff --git a/src/vector/app.tsx b/src/vector/app.tsx index da0f3f3941..426163db0b 100644 --- a/src/vector/app.tsx +++ b/src/vector/app.tsx @@ -27,7 +27,6 @@ import MatrixChat from "../components/structures/MatrixChat"; import { ValidatedServerConfig } from "../utils/ValidatedServerConfig"; import { ModuleRunner } from "../modules/ModuleRunner"; import { parseQs } from "./url_utils"; -import VectorBasePlatform from "./platform/VectorBasePlatform"; import { getInitialScreenAfterLogin, getScreenFromLocation, init as initRouting, onNewScreen } from "./routing"; import { UserFriendlyError } from "../languageHandler"; @@ -64,7 +63,7 @@ export async function loadApp(fragParams: {}, matrixChatRef: React.Ref -Copyright 2016 Aviral Dasgupta -Copyright 2016 OpenMarket Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import type { IConfigOptions } from "../../IConfigOptions"; -import BasePlatform from "../../BasePlatform"; -import { getVectorConfig } from "../getconfig"; -import Favicon from "../../favicon"; -import { _t } from "../../languageHandler"; - -/** - * Vector-specific extensions to the BasePlatform template - */ -export default abstract class VectorBasePlatform extends BasePlatform { - protected _favicon?: Favicon; - - public async getConfig(): Promise { - return getVectorConfig(); - } - - public getHumanReadableName(): string { - return "Vector Base Platform"; // no translation required: only used for analytics - } - - /** - * Delay creating the `Favicon` instance until first use (on the first notification) as - * it uses canvas, which can trigger a permission prompt in Firefox's resist fingerprinting mode. - * See https://github.com/element-hq/element-web/issues/9605. - */ - public get favicon(): Favicon { - if (this._favicon) { - return this._favicon; - } - this._favicon = new Favicon(); - return this._favicon; - } - - private updateFavicon(): void { - let bgColor = "#d00"; - let notif: string | number = this.notificationCount; - - if (this.errorDidOccur) { - notif = notif || "×"; - bgColor = "#f00"; - } - - this.favicon.badge(notif, { bgColor }); - } - - public setNotificationCount(count: number): void { - if (this.notificationCount === count) return; - super.setNotificationCount(count); - this.updateFavicon(); - } - - public setErrorStatus(errorDidOccur: boolean): void { - if (this.errorDidOccur === errorDidOccur) return; - super.setErrorStatus(errorDidOccur); - this.updateFavicon(); - } - - /** - * Begin update polling, if applicable - */ - public startUpdater(): void {} - - /** - * Get a sensible default display name for the - * device Vector is running on - */ - public getDefaultDeviceDisplayName(): string { - return _t("unknown_device"); - } -} diff --git a/src/vector/platform/WebPlatform.ts b/src/vector/platform/WebPlatform.ts index 0bbc7a0a5a..bb573c89c0 100644 --- a/src/vector/platform/WebPlatform.ts +++ b/src/vector/platform/WebPlatform.ts @@ -11,12 +11,11 @@ import UAParser from "ua-parser-js"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "../../MatrixClientPeg"; -import { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform"; +import BasePlatform, { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform"; import dis from "../../dispatcher/dispatcher"; import { hideToast as hideUpdateToast, showToast as showUpdateToast } from "../../toasts/UpdateToast"; import { Action } from "../../dispatcher/actions"; import { CheckUpdatesPayload } from "../../dispatcher/payloads/CheckUpdatesPayload"; -import VectorBasePlatform from "./VectorBasePlatform"; import { parseQs } from "../url_utils"; import { _t } from "../../languageHandler"; @@ -31,7 +30,7 @@ function getNormalizedAppVersion(version: string): string { return version; } -export default class WebPlatform extends VectorBasePlatform { +export default class WebPlatform extends BasePlatform { private static readonly VERSION = process.env.VERSION!; // baked in by Webpack public constructor() { diff --git a/test/test-utils/utilities.ts b/test/test-utils/utilities.ts index 4278b73f74..29b25fda21 100644 --- a/test/test-utils/utilities.ts +++ b/test/test-utils/utilities.ts @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import EventEmitter from "events"; +import { act } from "jest-matrix-react"; import { ActionPayload } from "../../src/dispatcher/payloads"; import defaultDispatcher from "../../src/dispatcher/dispatcher"; @@ -119,7 +120,7 @@ export function untilEmission( }); } -export const flushPromises = async () => await new Promise((resolve) => window.setTimeout(resolve)); +export const flushPromises = () => act(async () => await new Promise((resolve) => window.setTimeout(resolve))); // with jest's modern fake timers process.nextTick is also mocked, // flushing promises in the normal way then waits for some advancement diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 4e04025f55..4b396b66a9 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -953,7 +953,7 @@ describe("", () => { const getComponentAndWaitForReady = async (): Promise => { const renderResult = getComponent(); // wait for welcome page chrome render - await screen.findByText("powered by Matrix"); + await screen.findByText("Powered by Matrix"); // go to login page defaultDispatcher.dispatch({ @@ -1480,7 +1480,7 @@ describe("", () => { const getComponentAndWaitForReady = async (): Promise => { const renderResult = getComponent(); // wait for welcome page chrome render - await screen.findByText("powered by Matrix"); + await screen.findByText("Powered by Matrix"); // go to mobile_register page defaultDispatcher.dispatch({ @@ -1500,7 +1500,7 @@ describe("", () => { it("should render welcome screen if mobile registration is not enabled in settings", async () => { await getComponentAndWaitForReady(); - await screen.findByText("powered by Matrix"); + await screen.findByText("Powered by Matrix"); }); it("should render mobile registration", async () => { diff --git a/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap index 71bde418ff..e074958144 100644 --- a/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap @@ -114,46 +114,56 @@ exports[` Multi-tab lockout waits for other tab to stop during sta >
+
-

- Hello -

-
-
-
- +
+ -
@@ -162,12 +172,33 @@ exports[` Multi-tab lockout waits for other tab to stop during sta class="mx_AuthFooter" role="contentinfo" > + + Blog + + + Twitter + + + GitHub + - powered by Matrix + Powered by Matrix
@@ -201,116 +232,150 @@ exports[` with a soft-logged-out session should show the soft-logo >
+
-
- -
-
-
-

- You're signed out -

-

- Sign in -

-
-
-

- Enter your password to sign in and regain access to your account. -

-
- - -
-
- Sign in -
- -
-
-

- Clear personal data -

-

- Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account. -

-
+
- Clear all data +
-
+
+

+ You're signed out +

+

+ Sign in +

+
+
+

+ Enter your password to sign in and regain access to your account. +

+
+ + +
+
+ Sign in +
+ +
+
+

+ Clear personal data +

+

+ Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account. +

+
+
+ Clear all data +
+
+
+
diff --git a/test/unit-tests/components/views/auth/VectorAuthPage-test.tsx b/test/unit-tests/components/views/auth/AuthFooter-test.tsx similarity index 73% rename from test/unit-tests/components/views/auth/VectorAuthPage-test.tsx rename to test/unit-tests/components/views/auth/AuthFooter-test.tsx index 2c5cb461b1..f8d0d8fd5e 100644 --- a/test/unit-tests/components/views/auth/VectorAuthPage-test.tsx +++ b/test/unit-tests/components/views/auth/AuthFooter-test.tsx @@ -9,16 +9,16 @@ Please see LICENSE files in the repository root for full details. import * as React from "react"; import { render } from "jest-matrix-react"; -import VectorAuthPage from "../../../../../src/components/views/auth/VectorAuthPage"; +import AuthFooter from "../../../../../src/components/views/auth/AuthFooter"; import { setupLanguageMock } from "../../../../setup/setupLanguage"; -describe("", () => { +describe("", () => { beforeEach(() => { setupLanguageMock(); }); it("should match snapshot", () => { - const { asFragment } = render(); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); }); diff --git a/test/unit-tests/components/views/auth/VectorAuthHeaderLogo-test.tsx b/test/unit-tests/components/views/auth/AuthHeaderLogo-test.tsx similarity index 64% rename from test/unit-tests/components/views/auth/VectorAuthHeaderLogo-test.tsx rename to test/unit-tests/components/views/auth/AuthHeaderLogo-test.tsx index 6b3839a5b1..ce187805e4 100644 --- a/test/unit-tests/components/views/auth/VectorAuthHeaderLogo-test.tsx +++ b/test/unit-tests/components/views/auth/AuthHeaderLogo-test.tsx @@ -9,11 +9,11 @@ Please see LICENSE files in the repository root for full details. import * as React from "react"; import { render } from "jest-matrix-react"; -import VectorAuthHeaderLogo from "../../../../../src/components/views/auth/VectorAuthHeaderLogo"; +import AuthHeaderLogo from "../../../../../src/components/views/auth/AuthHeaderLogo"; -describe("", () => { +describe("", () => { it("should match snapshot", () => { - const { asFragment } = render(); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); }); diff --git a/test/unit-tests/components/views/auth/AuthPage-test.tsx b/test/unit-tests/components/views/auth/AuthPage-test.tsx new file mode 100644 index 0000000000..836b08f20b --- /dev/null +++ b/test/unit-tests/components/views/auth/AuthPage-test.tsx @@ -0,0 +1,36 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2022 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +import * as React from "react"; +import { render } from "jest-matrix-react"; + +import AuthPage from "../../../../../src/components/views/auth/AuthPage"; +import { setupLanguageMock } from "../../../../setup/setupLanguage"; +import SdkConfig from "../../../../../src/SdkConfig.ts"; + +describe("", () => { + beforeEach(() => { + setupLanguageMock(); + SdkConfig.reset(); + // @ts-ignore private access + AuthPage.welcomeBackgroundUrl = undefined; + }); + + it("should match snapshot", () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should use configured background url", () => { + SdkConfig.add({ branding: { welcome_background_url: ["https://example.com/image.png"] } }); + const { container } = render(); + expect(container.querySelector(".mx_AuthPage")).toHaveStyle({ + background: "center/cover fixed url(https://example.com/image.png)", + }); + }); +}); diff --git a/test/unit-tests/components/views/auth/VectorAuthFooter-test.tsx b/test/unit-tests/components/views/auth/VectorAuthFooter-test.tsx deleted file mode 100644 index ebd2a6ffe4..0000000000 --- a/test/unit-tests/components/views/auth/VectorAuthFooter-test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import * as React from "react"; -import { render } from "jest-matrix-react"; - -import VectorAuthFooter from "../../../../../src/components/views/auth/VectorAuthFooter"; -import { setupLanguageMock } from "../../../../setup/setupLanguage"; - -describe("", () => { - beforeEach(() => { - setupLanguageMock(); - }); - - it("should match snapshot", () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); -}); diff --git a/test/unit-tests/components/views/auth/__snapshots__/VectorAuthFooter-test.tsx.snap b/test/unit-tests/components/views/auth/__snapshots__/AuthFooter-test.tsx.snap similarity index 92% rename from test/unit-tests/components/views/auth/__snapshots__/VectorAuthFooter-test.tsx.snap rename to test/unit-tests/components/views/auth/__snapshots__/AuthFooter-test.tsx.snap index d1a16c081b..f1321ece2a 100644 --- a/test/unit-tests/components/views/auth/__snapshots__/VectorAuthFooter-test.tsx.snap +++ b/test/unit-tests/components/views/auth/__snapshots__/AuthFooter-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should match snapshot 1`] = ` +exports[` should match snapshot 1`] = `