Merge branch 'develop' into dbkr/stateafter

This commit is contained in:
Michael Telatynski 2024-11-06 15:42:28 +00:00 committed by GitHub
commit 931edd7419
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 556 additions and 644 deletions

View file

@ -34,27 +34,6 @@ jobs:
- name: Typecheck - name: Typecheck
run: "yarn run lint:types" 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: i18n_lint:
name: "i18n Check" name: "i18n Check"
uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main

View file

@ -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"
}

View file

@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
// Docker tag to use for synapse docker image. // 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. // 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. // 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<Omit<HomeserverConfig, "dockerUrl">> { async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template); const templateDir = path.join(__dirname, "templates", opts.template);

View file

@ -31,6 +31,8 @@ import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
import { IConfigOptions } from "./IConfigOptions"; import { IConfigOptions } from "./IConfigOptions";
import SdkConfig from "./SdkConfig"; import SdkConfig from "./SdkConfig";
import { buildAndEncodePickleKey, encryptPickleKey } from "./utils/tokens/pickling"; 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_HOMESERVER_URL_KEY = "mx_sso_hs_url";
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_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 { export default abstract class BasePlatform {
protected notificationCount = 0; protected notificationCount = 0;
protected errorDidOccur = false; protected errorDidOccur = false;
protected _favicon?: Favicon;
protected constructor() { protected constructor() {
dis.register(this.onAction); dis.register(this.onAction);
this.startUpdateCheck = this.startUpdateCheck.bind(this); this.startUpdateCheck = this.startUpdateCheck.bind(this);
} }
public abstract getConfig(): Promise<IConfigOptions | undefined>; public async getConfig(): Promise<IConfigOptions | undefined> {
return getVectorConfig();
}
/**
* Get a sensible default display name for the device Element is running on
*/
public abstract getDefaultDeviceDisplayName(): string; public abstract getDefaultDeviceDisplayName(): string;
protected onAction = (payload: ActionPayload): void => { protected onAction = (payload: ActionPayload): void => {
@ -89,11 +97,15 @@ export default abstract class BasePlatform {
public abstract getHumanReadableName(): string; public abstract getHumanReadableName(): string;
public setNotificationCount(count: number): void { public setNotificationCount(count: number): void {
if (this.notificationCount === count) return;
this.notificationCount = count; this.notificationCount = count;
this.updateFavicon();
} }
public setErrorStatus(errorDidOccur: boolean): void { public setErrorStatus(errorDidOccur: boolean): void {
if (this.errorDidOccur === errorDidOccur) return;
this.errorDidOccur = errorDidOccur; this.errorDidOccur = errorDidOccur;
this.updateFavicon();
} }
/** /**
@ -456,4 +468,34 @@ export default abstract class BasePlatform {
url.hash = ""; url.hash = "";
return url; 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 {}
} }

View file

@ -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. 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"; import { _t } from "../../../languageHandler";
export default class AuthFooter extends React.Component { const AuthFooter = (): ReactElement => {
public render(): React.ReactNode { const brandingConfig = SdkConfig.getObject("branding");
return ( const links = brandingConfig?.get("auth_footer_links") ?? [
<footer className="mx_AuthFooter" role="contentinfo"> { text: "Blog", url: "https://element.io/blog" },
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener"> { text: "Twitter", url: "https://twitter.com/element_hq" },
{_t("auth|footer_powered_by_matrix")} { text: "GitHub", url: "https://github.com/element-hq/element-web" },
</a> ];
</footer>
const authFooterLinks: JSX.Element[] = [];
for (const linkEntry of links) {
authFooterLinks.push(
<a href={linkEntry.url} key={linkEntry.text} target="_blank" rel="noreferrer noopener">
{linkEntry.text}
</a>,
); );
} }
}
return (
<footer className="mx_AuthFooter" role="contentinfo">
{authFooterLinks}
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">
{_t("powered_by_matrix")}
</a>
</footer>
);
};
export default AuthFooter;

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019-2024 New Vector Ltd. Copyright 2019-2024 New Vector Ltd.
Copyright 2015, 2016 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. 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 React from "react";
import SdkConfig from "../../../SdkConfig";
export default class AuthHeaderLogo extends React.PureComponent { export default class AuthHeaderLogo extends React.PureComponent {
public render(): React.ReactNode { public render(): React.ReactElement {
return <aside className="mx_AuthHeaderLogo">Matrix</aside>; const brandingConfig = SdkConfig.getObject("branding");
const logoUrl = brandingConfig?.get("auth_header_logo_url") ?? "themes/element/img/logos/element-logo.svg";
return (
<aside className="mx_AuthHeaderLogo">
<img src={logoUrl} alt="Element" />
</aside>
);
} }
} }

View file

@ -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. 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"; import AuthFooter from "./AuthFooter";
export default class AuthPage extends React.PureComponent<{ children: ReactNode }> { export default class AuthPage extends React.PureComponent<React.PropsWithChildren> {
public render(): React.ReactNode { 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 ( return (
<div className="mx_AuthPage"> <div className="mx_AuthPage" style={pageStyle}>
<div className="mx_AuthPage_modal">{this.props.children}</div> <div className="mx_AuthPage_modal" style={modalStyle}>
<div className="mx_AuthPage_modalBlur" style={blurStyle} />
<div className="mx_AuthPage_modalContent" style={modalContentStyle}>
{this.props.children}
</div>
</div>
<AuthFooter /> <AuthFooter />
</div> </div>
); );

View file

@ -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(
<a href={linkEntry.url} key={linkEntry.text} target="_blank" rel="noreferrer noopener">
{linkEntry.text}
</a>,
);
}
return (
<footer className="mx_AuthFooter" role="contentinfo">
{authFooterLinks}
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">
{_t("powered_by_matrix")}
</a>
</footer>
);
};
export default VectorAuthFooter;

View file

@ -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 (
<aside className="mx_AuthHeaderLogo">
<img src={logoUrl} alt="Element" />
</aside>
);
}
}

View file

@ -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<React.PropsWithChildren> {
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 (
<div className="mx_AuthPage" style={pageStyle}>
<div className="mx_AuthPage_modal" style={modalStyle}>
<div className="mx_AuthPage_modalBlur" style={blurStyle} />
<div className="mx_AuthPage_modalContent" style={modalContentStyle}>
{this.props.children}
</div>
</div>
<VectorAuthFooter />
</div>
);
}
}

View file

@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { MutableRefObject, ReactNode, StrictMode } from "react"; 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 { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { TooltipProvider } from "@vector-im/compound-web"; 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 // 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. // CSS stacking context, and thus be able to control their z-indexes relative to each other.
function getOrCreateMasterContainer(): HTMLDivElement { function getOrCreateMasterContainer(): HTMLDivElement {
let container = getContainer("mx_PersistedElement_container"); let container = document.getElementById("mx_PersistedElement_container") as HTMLDivElement;
if (!container) { if (!container) {
container = document.createElement("div"); container = document.createElement("div");
container.id = "mx_PersistedElement_container"; container.id = "mx_PersistedElement_container";
@ -34,18 +34,10 @@ function getOrCreateMasterContainer(): HTMLDivElement {
return container; return container;
} }
function getContainer(containerId: string): HTMLDivElement {
return document.getElementById(containerId) as HTMLDivElement;
}
function getOrCreateContainer(containerId: string): HTMLDivElement { function getOrCreateContainer(containerId: string): HTMLDivElement {
let container = getContainer(containerId); const container = document.createElement("div");
container.id = containerId;
if (!container) { getOrCreateMasterContainer().appendChild(container);
container = document.createElement("div");
container.id = containerId;
getOrCreateMasterContainer().appendChild(container);
}
return container; return container;
} }
@ -83,6 +75,8 @@ export default class PersistedElement extends React.Component<IProps> {
private childContainer?: HTMLDivElement; private childContainer?: HTMLDivElement;
private child?: HTMLDivElement; private child?: HTMLDivElement;
private static rootMap: Record<string, [root: Root, container: Element]> = {};
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
@ -99,14 +93,16 @@ export default class PersistedElement extends React.Component<IProps> {
* @param {string} persistKey Key used to uniquely identify this PersistedElement * @param {string} persistKey Key used to uniquely identify this PersistedElement
*/ */
public static destroyElement(persistKey: string): void { public static destroyElement(persistKey: string): void {
const container = getContainer("mx_persistedElement_" + persistKey); const pair = PersistedElement.rootMap[persistKey];
if (container) { if (pair) {
container.remove(); pair[0].unmount();
pair[1].remove();
} }
delete PersistedElement.rootMap[persistKey];
} }
public static isMounted(persistKey: string): boolean { public static isMounted(persistKey: string): boolean {
return Boolean(getContainer("mx_persistedElement_" + persistKey)); return Boolean(PersistedElement.rootMap[persistKey]);
} }
private collectChildContainer = (ref: HTMLDivElement): void => { private collectChildContainer = (ref: HTMLDivElement): void => {
@ -179,7 +175,14 @@ export default class PersistedElement extends React.Component<IProps> {
</StrictMode> </StrictMode>
); );
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 { private updateChildVisibility(child?: HTMLDivElement, visible = false): void {

View file

@ -13,8 +13,8 @@ import classNames from "classnames";
import * as HtmlUtils from "../../../HtmlUtils"; import * as HtmlUtils from "../../../HtmlUtils";
import { editBodyDiffToHtml } from "../../../utils/MessageDiffUtils"; import { editBodyDiffToHtml } from "../../../utils/MessageDiffUtils";
import { formatTime } from "../../../DateUtils"; import { formatTime } from "../../../DateUtils";
import { pillifyLinks, unmountPills } from "../../../utils/pillify"; import { pillifyLinks } from "../../../utils/pillify";
import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify"; import { tooltipifyLinks } from "../../../utils/tooltipify";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import RedactedBody from "./RedactedBody"; import RedactedBody from "./RedactedBody";
@ -23,6 +23,7 @@ import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog";
import ViewSource from "../../structures/ViewSource"; import ViewSource from "../../structures/ViewSource";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ReactRootManager } from "../../../utils/react";
function getReplacedContent(event: MatrixEvent): IContent { function getReplacedContent(event: MatrixEvent): IContent {
const originalContent = event.getOriginalContent(); const originalContent = event.getOriginalContent();
@ -47,8 +48,8 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
public declare context: React.ContextType<typeof MatrixClientContext>; public declare context: React.ContextType<typeof MatrixClientContext>;
private content = createRef<HTMLDivElement>(); private content = createRef<HTMLDivElement>();
private pills: Element[] = []; private pills = new ReactRootManager();
private tooltips: Element[] = []; private tooltips = new ReactRootManager();
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
@ -103,7 +104,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
private tooltipifyLinks(): void { private tooltipifyLinks(): void {
// not present for redacted events // not present for redacted events
if (this.content.current) { if (this.content.current) {
tooltipifyLinks(this.content.current.children, this.pills, this.tooltips); tooltipifyLinks(this.content.current.children, this.pills.elements, this.tooltips);
} }
} }
@ -113,8 +114,8 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
unmountPills(this.pills); this.pills.unmount();
unmountTooltips(this.tooltips); this.tooltips.unmount();
const event = this.props.mxEvent; const event = this.props.mxEvent;
event.localRedactionEvent()?.off(MatrixEventEvent.Status, this.onAssociatedStatusChanged); event.localRedactionEvent()?.off(MatrixEventEvent.Status, this.onAssociatedStatusChanged);
} }

View file

@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { createRef, SyntheticEvent, MouseEvent, StrictMode } from "react"; import React, { createRef, SyntheticEvent, MouseEvent, StrictMode } from "react";
import ReactDOM from "react-dom";
import { MsgType } from "matrix-js-sdk/src/matrix"; import { MsgType } from "matrix-js-sdk/src/matrix";
import { TooltipProvider } from "@vector-im/compound-web"; import { TooltipProvider } from "@vector-im/compound-web";
@ -17,8 +16,8 @@ import Modal from "../../../Modal";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { pillifyLinks, unmountPills } from "../../../utils/pillify"; import { pillifyLinks } from "../../../utils/pillify";
import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify"; import { tooltipifyLinks } from "../../../utils/tooltipify";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import { isPermalinkHost, tryTransformPermalinkToLocalHref } from "../../../utils/permalinks/Permalinks"; import { isPermalinkHost, tryTransformPermalinkToLocalHref } from "../../../utils/permalinks/Permalinks";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
@ -36,6 +35,7 @@ import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
import { IEventTileOps } from "../rooms/EventTile"; import { IEventTileOps } from "../rooms/EventTile";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import CodeBlock from "./CodeBlock"; import CodeBlock from "./CodeBlock";
import { ReactRootManager } from "../../../utils/react";
interface IState { interface IState {
// the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody. // the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody.
@ -48,9 +48,9 @@ interface IState {
export default class TextualBody extends React.Component<IBodyProps, IState> { export default class TextualBody extends React.Component<IBodyProps, IState> {
private readonly contentRef = createRef<HTMLDivElement>(); private readonly contentRef = createRef<HTMLDivElement>();
private pills: Element[] = []; private pills = new ReactRootManager();
private tooltips: Element[] = []; private tooltips = new ReactRootManager();
private reactRoots: Element[] = []; private reactRoots = new ReactRootManager();
private ref = createRef<HTMLDivElement>(); private ref = createRef<HTMLDivElement>();
@ -82,7 +82,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
// tooltipifyLinks AFTER calculateUrlPreview because the DOM inside the tooltip // tooltipifyLinks AFTER calculateUrlPreview because the DOM inside the tooltip
// container is empty before the internal component has mounted so calculateUrlPreview // container is empty before the internal component has mounted so calculateUrlPreview
// won't find any anchors // 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") { if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
// Handle expansion and add buttons // Handle expansion and add buttons
@ -113,12 +113,11 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
private wrapPreInReact(pre: HTMLPreElement): void { private wrapPreInReact(pre: HTMLPreElement): void {
const root = document.createElement("div"); const root = document.createElement("div");
root.className = "mx_EventTile_pre_container"; root.className = "mx_EventTile_pre_container";
this.reactRoots.push(root);
// Insert containing div in place of <pre> block // Insert containing div in place of <pre> block
pre.parentNode?.replaceChild(root, pre); pre.parentNode?.replaceChild(root, pre);
ReactDOM.render( this.reactRoots.render(
<StrictMode> <StrictMode>
<CodeBlock onHeightChanged={this.props.onHeightChanged}>{pre}</CodeBlock> <CodeBlock onHeightChanged={this.props.onHeightChanged}>{pre}</CodeBlock>
</StrictMode>, </StrictMode>,
@ -137,16 +136,9 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
unmountPills(this.pills); this.pills.unmount();
unmountTooltips(this.tooltips); this.tooltips.unmount();
this.reactRoots.unmount();
for (const root of this.reactRoots) {
ReactDOM.unmountComponentAtNode(root);
}
this.pills = [];
this.tooltips = [];
this.reactRoots = [];
} }
public shouldComponentUpdate(nextProps: Readonly<IBodyProps>, nextState: Readonly<IState>): boolean { public shouldComponentUpdate(nextProps: Readonly<IBodyProps>, nextState: Readonly<IState>): boolean {
@ -204,7 +196,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
</StrictMode> </StrictMode>
); );
ReactDOM.render(spoiler, spoilerContainer); this.reactRoots.render(spoiler, spoilerContainer);
node.parentNode?.replaceChild(spoilerContainer, node); node.parentNode?.replaceChild(spoilerContainer, node);
node = spoilerContainer; node = spoilerContainer;

View file

@ -51,7 +51,6 @@ export const DiscoverySettings: React.FC = () => {
const [emails, setEmails] = useState<ThirdPartyIdentifier[]>([]); const [emails, setEmails] = useState<ThirdPartyIdentifier[]>([]);
const [phoneNumbers, setPhoneNumbers] = useState<ThirdPartyIdentifier[]>([]); const [phoneNumbers, setPhoneNumbers] = useState<ThirdPartyIdentifier[]>([]);
const [idServerName, setIdServerName] = useState<string | undefined>(abbreviateUrl(client.getIdentityServerUrl())); const [idServerName, setIdServerName] = useState<string | undefined>(abbreviateUrl(client.getIdentityServerUrl()));
const [canMake3pidChanges, setCanMake3pidChanges] = useState<boolean>(false);
const [requiredPolicyInfo, setRequiredPolicyInfo] = useState<RequiredPolicyInfo>({ const [requiredPolicyInfo, setRequiredPolicyInfo] = useState<RequiredPolicyInfo>({
// This object is passed along to a component for handling // This object is passed along to a component for handling
@ -88,11 +87,6 @@ export const DiscoverySettings: React.FC = () => {
try { try {
await getThreepidState(); 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 // 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. // for free. So we might as well use that for our own purposes.
const idServerUrl = client.getIdentityServerUrl(); const idServerUrl = client.getIdentityServerUrl();
@ -166,7 +160,7 @@ export const DiscoverySettings: React.FC = () => {
medium={ThreepidMedium.Email} medium={ThreepidMedium.Email}
threepids={emails} threepids={emails}
onChange={getThreepidState} onChange={getThreepidState}
disabled={!canMake3pidChanges} disabled={!hasTerms}
isLoading={isLoadingThreepids} isLoading={isLoadingThreepids}
/> />
</SettingsSubsection> </SettingsSubsection>
@ -180,7 +174,7 @@ export const DiscoverySettings: React.FC = () => {
medium={ThreepidMedium.Phone} medium={ThreepidMedium.Phone}
threepids={phoneNumbers} threepids={phoneNumbers}
onChange={getThreepidState} onChange={getThreepidState}
disabled={!canMake3pidChanges} disabled={!hasTerms}
isLoading={isLoadingThreepids} isLoading={isLoadingThreepids}
/> />
</SettingsSubsection> </SettingsSubsection>

View file

@ -207,7 +207,6 @@
"failed_query_registration_methods": "Nepovedlo se načíst podporované způsoby přihlášení.", "failed_query_registration_methods": "Nepovedlo se načíst podporované způsoby přihlášení.",
"failed_soft_logout_auth": "Nepovedlo se autentifikovat", "failed_soft_logout_auth": "Nepovedlo se autentifikovat",
"failed_soft_logout_homeserver": "Kvůli problémům s domovským server se nepovedlo autentifikovat znovu", "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_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_email_required": "Musíte zadat e-mailovou adresu spojenou s vaším účtem.",
"forgot_password_prompt": "Zapomněli jste heslo?", "forgot_password_prompt": "Zapomněli jste heslo?",
@ -3525,7 +3524,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "A %(count)s dalších..." "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_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", "unsupported_server_title": "Váš server není podporován",
"update": { "update": {

View file

@ -203,7 +203,6 @@
"failed_query_registration_methods": "Konnte unterstützte Registrierungsmethoden nicht abrufen.", "failed_query_registration_methods": "Konnte unterstützte Registrierungsmethoden nicht abrufen.",
"failed_soft_logout_auth": "Erneute Authentifizierung fehlgeschlagen", "failed_soft_logout_auth": "Erneute Authentifizierung fehlgeschlagen",
"failed_soft_logout_homeserver": "Erneute Authentifizierung aufgrund eines Problems des Heim-Servers 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_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_email_required": "Es muss die mit dem Benutzerkonto verbundene E-Mail-Adresse eingegeben werden.",
"forgot_password_prompt": "Passwort vergessen?", "forgot_password_prompt": "Passwort vergessen?",
@ -3500,7 +3499,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Und %(count)s weitere …" "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_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", "unsupported_server_title": "Dein Server wird nicht unterstützt",
"update": { "update": {

View file

@ -182,7 +182,6 @@
"failed_query_registration_methods": "Αδυναμία λήψης των υποστηριζόμενων μεθόδων εγγραφής.", "failed_query_registration_methods": "Αδυναμία λήψης των υποστηριζόμενων μεθόδων εγγραφής.",
"failed_soft_logout_auth": "Απέτυχε ο εκ νέου έλεγχος ταυτότητας", "failed_soft_logout_auth": "Απέτυχε ο εκ νέου έλεγχος ταυτότητας",
"failed_soft_logout_homeserver": "Απέτυχε ο εκ νέου έλεγχος ταυτότητας λόγω προβλήματος με τον κεντρικό διακομιστή", "failed_soft_logout_homeserver": "Απέτυχε ο εκ νέου έλεγχος ταυτότητας λόγω προβλήματος με τον κεντρικό διακομιστή",
"footer_powered_by_matrix": "λειτουργεί με το Matrix",
"forgot_password_email_invalid": "Η διεύθυνση email δε φαίνεται να είναι έγκυρη.", "forgot_password_email_invalid": "Η διεύθυνση email δε φαίνεται να είναι έγκυρη.",
"forgot_password_email_required": "Πρέπει να εισηχθεί η διεύθυνση ηλ. αλληλογραφίας που είναι συνδεδεμένη με τον λογαριασμό σας.", "forgot_password_email_required": "Πρέπει να εισηχθεί η διεύθυνση ηλ. αλληλογραφίας που είναι συνδεδεμένη με τον λογαριασμό σας.",
"forgot_password_prompt": "Ξεχάσετε τον κωδικό σας;", "forgot_password_prompt": "Ξεχάσετε τον κωδικό σας;",
@ -2829,7 +2828,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Και %(count)s ακόμα..." "other": "Και %(count)s ακόμα..."
}, },
"unknown_device": "Άγνωστη συσκευή",
"update": { "update": {
"changelog": "Αλλαγές", "changelog": "Αλλαγές",
"check_action": "Έλεγχος για ενημέρωση", "check_action": "Έλεγχος για ενημέρωση",

View file

@ -209,7 +209,6 @@
"failed_query_registration_methods": "Unable to query for supported registration methods.", "failed_query_registration_methods": "Unable to query for supported registration methods.",
"failed_soft_logout_auth": "Failed to re-authenticate", "failed_soft_logout_auth": "Failed to re-authenticate",
"failed_soft_logout_homeserver": "Failed to re-authenticate due to a homeserver problem", "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_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_email_required": "The email address linked to your account must be entered.",
"forgot_password_prompt": "Forgotten your password?", "forgot_password_prompt": "Forgotten your password?",
@ -3706,7 +3705,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "And %(count)s more..." "other": "And %(count)s more..."
}, },
"unknown_device": "Unknown device",
"unsupported_browser": { "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.", "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" "title": "%(brand)s does not support this browser"

View file

@ -171,7 +171,6 @@
"failed_query_registration_methods": "Ne povas peti subtenatajn registrajn metodojn.", "failed_query_registration_methods": "Ne povas peti subtenatajn registrajn metodojn.",
"failed_soft_logout_auth": "Malsukcesis reaŭtentikigi", "failed_soft_logout_auth": "Malsukcesis reaŭtentikigi",
"failed_soft_logout_homeserver": "Malsukcesis reaŭtentikigi pro hejmservila problemo", "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_invalid": "La retpoŝtadreso ŝajnas ne valida.",
"forgot_password_email_required": "Vi devas enigi retpoŝtadreson ligitan al via konto.", "forgot_password_email_required": "Vi devas enigi retpoŝtadreson ligitan al via konto.",
"forgot_password_prompt": "Ĉu vi forgesis vian pasvorton?", "forgot_password_prompt": "Ĉu vi forgesis vian pasvorton?",
@ -2544,7 +2543,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Kaj %(count)s pliaj…" "other": "Kaj %(count)s pliaj…"
}, },
"unknown_device": "Nekonata aparato",
"update": { "update": {
"changelog": "Protokolo de ŝanĝoj", "changelog": "Protokolo de ŝanĝoj",
"check_action": "Kontroli ĝisdatigojn", "check_action": "Kontroli ĝisdatigojn",

View file

@ -193,7 +193,6 @@
"failed_query_registration_methods": "No se pueden consultar los métodos de registro admitidos.", "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_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", "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_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_email_required": "Debes ingresar la dirección de correo electrónico vinculada a tu cuenta.",
"forgot_password_prompt": "¿Olvidaste tu contraseña?", "forgot_password_prompt": "¿Olvidaste tu contraseña?",
@ -3224,7 +3223,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Y %(count)s más…" "other": "Y %(count)s más…"
}, },
"unknown_device": "Dispositivo desconocido",
"update": { "update": {
"changelog": "Registro de cambios", "changelog": "Registro de cambios",
"check_action": "Comprobar si hay actualizaciones", "check_action": "Comprobar si hay actualizaciones",

View file

@ -207,7 +207,6 @@
"failed_query_registration_methods": "Ei õnnestunud pärida toetatud registreerimismeetodite loendit.", "failed_query_registration_methods": "Ei õnnestunud pärida toetatud registreerimismeetodite loendit.",
"failed_soft_logout_auth": "Uuesti autentimine ei õnnestunud", "failed_soft_logout_auth": "Uuesti autentimine ei õnnestunud",
"failed_soft_logout_homeserver": "Uuesti autentimine ei õnnestunud koduserveri vea tõttu", "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_invalid": "See e-posti aadress ei tundu olema korrektne.",
"forgot_password_email_required": "Sa pead sisestama oma kontoga seotud e-posti aadressi.", "forgot_password_email_required": "Sa pead sisestama oma kontoga seotud e-posti aadressi.",
"forgot_password_prompt": "Kas sa unustasid oma salasõna?", "forgot_password_prompt": "Kas sa unustasid oma salasõna?",
@ -3465,7 +3464,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Ja %(count)s muud..." "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_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", "unsupported_server_title": "Sinu server ei ole toetatud",
"update": { "update": {

View file

@ -165,7 +165,6 @@
"failed_query_registration_methods": "درخواست از روش‌های پشتیبانی‌شده‌ی ثبت‌نام میسر نیست.", "failed_query_registration_methods": "درخواست از روش‌های پشتیبانی‌شده‌ی ثبت‌نام میسر نیست.",
"failed_soft_logout_auth": "احراز هویت مجدد موفیت‌آمیز نبود", "failed_soft_logout_auth": "احراز هویت مجدد موفیت‌آمیز نبود",
"failed_soft_logout_homeserver": "به دلیل مشکلی که در سرور وجود دارد ، احراز هویت مجدد انجام نشد", "failed_soft_logout_homeserver": "به دلیل مشکلی که در سرور وجود دارد ، احراز هویت مجدد انجام نشد",
"footer_powered_by_matrix": "قدرت‌یافته از ماتریکس",
"forgot_password_email_required": "آدرس ایمیلی که به حساب کاربری شما متصل است، باید وارد شود.", "forgot_password_email_required": "آدرس ایمیلی که به حساب کاربری شما متصل است، باید وارد شود.",
"forgot_password_prompt": "گذرواژه‌ی خود را فراموش کردید؟", "forgot_password_prompt": "گذرواژه‌ی خود را فراموش کردید؟",
"identifier_label": "نحوه ورود", "identifier_label": "نحوه ورود",
@ -2230,7 +2229,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "و %(count)s مورد بیشتر ..." "other": "و %(count)s مورد بیشتر ..."
}, },
"unknown_device": "دستگاه ناشناخته",
"update": { "update": {
"changelog": "تغییراتِ به‌وجودآمده", "changelog": "تغییراتِ به‌وجودآمده",
"check_action": "بررسی برای به‌روزرسانی جدید", "check_action": "بررسی برای به‌روزرسانی جدید",

View file

@ -194,7 +194,6 @@
"failed_query_registration_methods": "Tuettuja rekisteröitymistapoja ei voitu kysellä.", "failed_query_registration_methods": "Tuettuja rekisteröitymistapoja ei voitu kysellä.",
"failed_soft_logout_auth": "Uudelleenautentikointi epäonnistui", "failed_soft_logout_auth": "Uudelleenautentikointi epäonnistui",
"failed_soft_logout_homeserver": "Uudelleenautentikointi epäonnistui kotipalvelinongelmasta johtuen", "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_invalid": "Sähköpostiosoite ei vaikuta kelvolliselta.",
"forgot_password_email_required": "Sinun pitää syöttää tiliisi liitetty sähköpostiosoite.", "forgot_password_email_required": "Sinun pitää syöttää tiliisi liitetty sähköpostiosoite.",
"forgot_password_prompt": "Unohditko salasanasi?", "forgot_password_prompt": "Unohditko salasanasi?",
@ -3100,7 +3099,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Ja %(count)s muuta..." "other": "Ja %(count)s muuta..."
}, },
"unknown_device": "Tuntematon laite",
"update": { "update": {
"changelog": "Muutosloki", "changelog": "Muutosloki",
"check_action": "Tarkista päivitykset", "check_action": "Tarkista päivitykset",

View file

@ -209,7 +209,6 @@
"failed_query_registration_methods": "Impossible de demander les méthodes dinscription prises en charge.", "failed_query_registration_methods": "Impossible de demander les méthodes dinscription prises en charge.",
"failed_soft_logout_auth": "Échec de la ré-authentification", "failed_soft_logout_auth": "Échec de la ré-authentification",
"failed_soft_logout_homeserver": "Échec de la ré-authentification à cause dun problème du serveur daccueil", "failed_soft_logout_homeserver": "Échec de la ré-authentification à cause dun problème du serveur daccueil",
"footer_powered_by_matrix": "propulsé par Matrix",
"forgot_password_email_invalid": "Ladresse e-mail semble être invalide.", "forgot_password_email_invalid": "Ladresse e-mail semble être invalide.",
"forgot_password_email_required": "Ladresse e-mail liée à votre compte doit être renseignée.", "forgot_password_email_required": "Ladresse e-mail liée à votre compte doit être renseignée.",
"forgot_password_prompt": "Mot de passe oublié ?", "forgot_password_prompt": "Mot de passe oublié ?",
@ -3625,7 +3624,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Et %(count)s autres…" "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_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 nest pas pris en charge", "unsupported_server_title": "Votre serveur nest pas pris en charge",
"update": { "update": {

View file

@ -184,7 +184,6 @@
"failed_query_registration_methods": "Non se puido consultar os métodos de rexistro soportados.", "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_auth": "Fallo na reautenticación",
"failed_soft_logout_homeserver": "Fallo ó reautenticar debido a un problema no servidor", "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_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_email_required": "Debe introducir o correo electrónico ligado a súa conta.",
"forgot_password_prompt": "¿Esqueceches o contrasinal?", "forgot_password_prompt": "¿Esqueceches o contrasinal?",
@ -2993,7 +2992,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "E %(count)s máis..." "other": "E %(count)s máis..."
}, },
"unknown_device": "Dispositivo descoñecido",
"update": { "update": {
"changelog": "Rexistro de cambios", "changelog": "Rexistro de cambios",
"check_action": "Comprobar actualización", "check_action": "Comprobar actualización",

View file

@ -167,7 +167,6 @@
"failed_query_registration_methods": "לא ניתן לשאול לשיטות רישום נתמכות.", "failed_query_registration_methods": "לא ניתן לשאול לשיטות רישום נתמכות.",
"failed_soft_logout_auth": "האימות מחדש נכשל", "failed_soft_logout_auth": "האימות מחדש נכשל",
"failed_soft_logout_homeserver": "האימות מחדש נכשל עקב בעיית שרת בית", "failed_soft_logout_homeserver": "האימות מחדש נכשל עקב בעיית שרת בית",
"footer_powered_by_matrix": "מופעל ע\"י Matrix",
"forgot_password_email_required": "יש להזין את כתובת הדוא\"ל המקושרת לחשבונך.", "forgot_password_email_required": "יש להזין את כתובת הדוא\"ל המקושרת לחשבונך.",
"forgot_password_prompt": "שכחת את הסיסמה שלך?", "forgot_password_prompt": "שכחת את הסיסמה שלך?",
"forgot_password_send_email": "שלח אימייל", "forgot_password_send_email": "שלח אימייל",
@ -2396,7 +2395,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "ו%(count)s עוד..." "other": "ו%(count)s עוד..."
}, },
"unknown_device": "מכשיר לא ידוע",
"update": { "update": {
"changelog": "דו\"ח שינויים", "changelog": "דו\"ח שינויים",
"check_action": "בדוק עדכונים", "check_action": "בדוק עדכונים",

View file

@ -202,7 +202,6 @@
"failed_query_registration_methods": "A támogatott regisztrációs módokat nem lehet lekérdezni.", "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_auth": "Újra bejelentkezés sikertelen",
"failed_soft_logout_homeserver": "Az újbóli hitelesítés a Matrix-kiszolgáló hibájából 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_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_email_required": "A fiókodhoz kötött e-mail címet add meg.",
"forgot_password_prompt": "Elfelejtetted a jelszavad?", "forgot_password_prompt": "Elfelejtetted a jelszavad?",
@ -3436,7 +3435,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "És még %(count)s..." "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_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", "unsupported_server_title": "A kiszolgálója nem támogatott",
"update": { "update": {

View file

@ -202,7 +202,6 @@
"failed_query_registration_methods": "Tidak dapat menanyakan metode pendaftaran yang didukung.", "failed_query_registration_methods": "Tidak dapat menanyakan metode pendaftaran yang didukung.",
"failed_soft_logout_auth": "Gagal untuk mengautentikasi ulang", "failed_soft_logout_auth": "Gagal untuk mengautentikasi ulang",
"failed_soft_logout_homeserver": "Gagal untuk mengautentikasi ulang karena masalah homeserver", "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_invalid": "Alamat email ini tidak terlihat absah.",
"forgot_password_email_required": "Alamat email yang tertaut ke akun Anda harus dimasukkan.", "forgot_password_email_required": "Alamat email yang tertaut ke akun Anda harus dimasukkan.",
"forgot_password_prompt": "Lupa kata sandi Anda?", "forgot_password_prompt": "Lupa kata sandi Anda?",
@ -3469,7 +3468,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Dan %(count)s lagi..." "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_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", "unsupported_server_title": "Server Anda tidak didukung",
"update": { "update": {

View file

@ -184,7 +184,6 @@
"failed_connect_identity_server": "Næ ekki sambandi við auðkennisþjón", "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_auth": "Tókst ekki að endurauðkenna",
"failed_soft_logout_homeserver": "Tókst ekki að endurauðkenna vegna vandamála með heimaþjón", "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_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_email_required": "Það þarf að setja inn tölvupóstfangið sem tengt er notandaaðgangnum þínum.",
"forgot_password_prompt": "Gleymdirðu lykilorðinu þínu?", "forgot_password_prompt": "Gleymdirðu lykilorðinu þínu?",
@ -2902,7 +2901,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Og %(count)s til viðbótar..." "other": "Og %(count)s til viðbótar..."
}, },
"unknown_device": "Óþekkt tæki",
"update": { "update": {
"changelog": "Breytingaskrá", "changelog": "Breytingaskrá",
"check_action": "Athuga með uppfærslu", "check_action": "Athuga með uppfærslu",

View file

@ -207,7 +207,6 @@
"failed_query_registration_methods": "Impossibile richiedere i metodi di registrazione supportati.", "failed_query_registration_methods": "Impossibile richiedere i metodi di registrazione supportati.",
"failed_soft_logout_auth": "Riautenticazione fallita", "failed_soft_logout_auth": "Riautenticazione fallita",
"failed_soft_logout_homeserver": "Riautenticazione fallita per un problema dell'homeserver", "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_invalid": "L'indirizzo email non sembra essere valido.",
"forgot_password_email_required": "Deve essere inserito l'indirizzo email collegato al tuo account.", "forgot_password_email_required": "Deve essere inserito l'indirizzo email collegato al tuo account.",
"forgot_password_prompt": "Hai dimenticato la password?", "forgot_password_prompt": "Hai dimenticato la password?",
@ -3518,7 +3517,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "E altri %(count)s ..." "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_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", "unsupported_server_title": "Il tuo server non è supportato",
"update": { "update": {

View file

@ -3231,7 +3231,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "他%(count)s人以上…" "other": "他%(count)s人以上…"
}, },
"unknown_device": "不明な端末",
"update": { "update": {
"changelog": "更新履歴", "changelog": "更新履歴",
"check_action": "更新を確認", "check_action": "更新を確認",

View file

@ -181,7 +181,6 @@
"failed_query_registration_methods": "ບໍ່ສາມາດສອບຖາມວິທີການລົງທະບຽນໄດ້.", "failed_query_registration_methods": "ບໍ່ສາມາດສອບຖາມວິທີການລົງທະບຽນໄດ້.",
"failed_soft_logout_auth": "ການພິສູດຢືນຢັນຄືນໃໝ່ບໍ່ສຳເລັດ", "failed_soft_logout_auth": "ການພິສູດຢືນຢັນຄືນໃໝ່ບໍ່ສຳເລັດ",
"failed_soft_logout_homeserver": "ການພິສູດຢືນຢັນຄືນໃໝ່ເນື່ອງຈາກບັນຫາ homeserver ບໍ່ສຳເລັດ", "failed_soft_logout_homeserver": "ການພິສູດຢືນຢັນຄືນໃໝ່ເນື່ອງຈາກບັນຫາ homeserver ບໍ່ສຳເລັດ",
"footer_powered_by_matrix": "ຂັບເຄື່ອນໂດຍ Matrix",
"forgot_password_email_invalid": "ທີ່ຢູ່ອີເມວບໍ່ຖືກຕ້ອງ.", "forgot_password_email_invalid": "ທີ່ຢູ່ອີເມວບໍ່ຖືກຕ້ອງ.",
"forgot_password_email_required": "ຕ້ອງໃສ່ທີ່ຢູ່ອີເມວທີ່ເຊື່ອມຕໍ່ກັບບັນຊີຂອງທ່ານ.", "forgot_password_email_required": "ຕ້ອງໃສ່ທີ່ຢູ່ອີເມວທີ່ເຊື່ອມຕໍ່ກັບບັນຊີຂອງທ່ານ.",
"forgot_password_prompt": "ລືມລະຫັດຜ່ານຂອງທ່ານບໍ?", "forgot_password_prompt": "ລືມລະຫັດຜ່ານຂອງທ່ານບໍ?",
@ -2845,7 +2844,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "ແລະ %(count)sອີກ..." "other": "ແລະ %(count)sອີກ..."
}, },
"unknown_device": "ທີ່ບໍ່ຮູ້ຈັກອຸປະກອນນີ້",
"update": { "update": {
"changelog": "ບັນທຶກການປ່ຽນແປງ", "changelog": "ບັນທຶກການປ່ຽນແປງ",
"check_action": "ກວດເບິ່ງເພຶ່ອອັບເດດ", "check_action": "ກວດເບິ່ງເພຶ່ອອັບເດດ",

View file

@ -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_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_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", "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_email_required": "Privalo būti įvestas su jūsų paskyra susietas el. pašto adresas.",
"forgot_password_prompt": "Pamiršote savo slaptažodį?", "forgot_password_prompt": "Pamiršote savo slaptažodį?",
"identifier_label": "Prisijungti naudojant", "identifier_label": "Prisijungti naudojant",
@ -2285,7 +2284,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Ir dar %(count)s..." "other": "Ir dar %(count)s..."
}, },
"unknown_device": "Nežinomas įrenginys",
"update": { "update": {
"changelog": "Keitinių žurnalas", "changelog": "Keitinių žurnalas",
"check_action": "Tikrinti, ar yra atnaujinimų", "check_action": "Tikrinti, ar yra atnaujinimų",

View file

@ -196,7 +196,6 @@
"failed_query_registration_methods": "Kan ondersteunde registratiemethoden niet opvragen.", "failed_query_registration_methods": "Kan ondersteunde registratiemethoden niet opvragen.",
"failed_soft_logout_auth": "Opnieuw inloggen is mislukt", "failed_soft_logout_auth": "Opnieuw inloggen is mislukt",
"failed_soft_logout_homeserver": "Opnieuw inloggen is mislukt wegens een probleem met de homeserver", "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_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_email_required": "Het aan jouw account gekoppelde e-mailadres dient ingevoerd worden.",
"forgot_password_prompt": "Wachtwoord vergeten?", "forgot_password_prompt": "Wachtwoord vergeten?",
@ -3046,7 +3045,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "En %(count)s meer…" "other": "En %(count)s meer…"
}, },
"unknown_device": "Onbekend apparaat",
"update": { "update": {
"changelog": "Wijzigingslogboek", "changelog": "Wijzigingslogboek",
"check_action": "Controleren op updates", "check_action": "Controleren op updates",

View file

@ -209,7 +209,6 @@
"failed_query_registration_methods": "Nie można uzyskać wspieranych metod rejestracji.", "failed_query_registration_methods": "Nie można uzyskać wspieranych metod rejestracji.",
"failed_soft_logout_auth": "Nie udało się uwierzytelnić ponownie", "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", "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_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_email_required": "Musisz wpisać adres e-mail połączony z twoim kontem.",
"forgot_password_prompt": "Nie pamiętasz hasła?", "forgot_password_prompt": "Nie pamiętasz hasła?",
@ -2049,14 +2048,6 @@
"button_view_all": "Pokaż wszystkie", "button_view_all": "Pokaż wszystkie",
"description": "Ten pokój ma przypięte wiadomości. Kliknij, aby je wyświetlić.", "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.", "go_to_message": "Wyświetl przypiętą wiadomość na osi czasu.",
"prefix": {
"audio": "Audio",
"file": "Plik",
"image": "Obraz",
"poll": "Ankieta",
"video": "Wideo"
},
"preview": "<bold>%(prefix)s:</bold> %(preview)s",
"title": "<bold>%(index)s z %(length)s</bold> przypiętych wiadomości" "title": "<bold>%(index)s z %(length)s</bold> przypiętych wiadomości"
}, },
"read_topic": "Kliknij, aby przeczytać temat", "read_topic": "Kliknij, aby przeczytać temat",
@ -3720,7 +3711,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "I %(count)s więcej…" "other": "I %(count)s więcej…"
}, },
"unknown_device": "Nieznane urządzenie",
"unsupported_browser": { "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.", "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" "title": "%(brand)s nie wspiera tej przeglądarki"

View file

@ -169,7 +169,6 @@
"failed_query_registration_methods": "Não foi possível consultar as opções de registro suportadas.", "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_auth": "Falha em autenticar novamente",
"failed_soft_logout_homeserver": "Falha em autenticar novamente devido à um problema no servidor local", "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_email_required": "O e-mail vinculado à sua conta precisa ser informado.",
"forgot_password_prompt": "Esqueceu sua senha?", "forgot_password_prompt": "Esqueceu sua senha?",
"identifier_label": "Entrar com", "identifier_label": "Entrar com",
@ -2446,7 +2445,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "E %(count)s mais..." "other": "E %(count)s mais..."
}, },
"unknown_device": "Dispositivo desconhecido",
"update": { "update": {
"changelog": "Registro de alterações", "changelog": "Registro de alterações",
"check_action": "Verificar atualizações", "check_action": "Verificar atualizações",

View file

@ -202,7 +202,6 @@
"failed_query_registration_methods": "Невозможно запросить поддерживаемые методы регистрации.", "failed_query_registration_methods": "Невозможно запросить поддерживаемые методы регистрации.",
"failed_soft_logout_auth": "Ошибка повторной аутентификации", "failed_soft_logout_auth": "Ошибка повторной аутентификации",
"failed_soft_logout_homeserver": "Ошибка повторной аутентификации из-за проблем на сервере", "failed_soft_logout_homeserver": "Ошибка повторной аутентификации из-за проблем на сервере",
"footer_powered_by_matrix": "основано на Matrix",
"forgot_password_email_invalid": "Адрес электронной почты не является действительным.", "forgot_password_email_invalid": "Адрес электронной почты не является действительным.",
"forgot_password_email_required": "Введите адрес электронной почты, связанный с вашей учётной записью.", "forgot_password_email_required": "Введите адрес электронной почты, связанный с вашей учётной записью.",
"forgot_password_prompt": "Забыли Ваш пароль?", "forgot_password_prompt": "Забыли Ваш пароль?",
@ -3503,7 +3502,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Еще %(count)s…" "other": "Еще %(count)s…"
}, },
"unknown_device": "Неизвестное устройство",
"unsupported_server_description": "На этом сервере используется старая версия Matrix. Перейдите на Matrix%(version)s, чтобы использовать %(brand)s ее без ошибок.", "unsupported_server_description": "На этом сервере используется старая версия Matrix. Перейдите на Matrix%(version)s, чтобы использовать %(brand)s ее без ошибок.",
"unsupported_server_title": "Ваш сервер не поддерживается", "unsupported_server_title": "Ваш сервер не поддерживается",
"update": { "update": {

View file

@ -202,7 +202,6 @@
"failed_query_registration_methods": "Nie je možné požiadať o podporované metódy registrácie.", "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_auth": "Nepodarilo sa opätovne overiť",
"failed_soft_logout_homeserver": "Opätovná autentifikácia zlyhala kvôli problému domovského servera", "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_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_email_required": "Musíte zadať emailovú adresu prepojenú s vašim účtom.",
"forgot_password_prompt": "Zabudli ste heslo?", "forgot_password_prompt": "Zabudli ste heslo?",
@ -3532,7 +3531,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "A %(count)s ďalších…" "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_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ý", "unsupported_server_title": "Váš server nie je podporovaný",
"update": { "update": {

View file

@ -195,7 +195,6 @@
"failed_query_registration_methods": "Sarrihet të kërkohet për metoda regjistrimi që mbulohen.", "failed_query_registration_methods": "Sarrihet të kërkohet për metoda regjistrimi që mbulohen.",
"failed_soft_logout_auth": "Su arrit të ribëhej mirëfilltësimi", "failed_soft_logout_auth": "Su arrit të ribëhej mirëfilltësimi",
"failed_soft_logout_homeserver": "Su arrit të ribëhej mirëfilltësimi, për shkak të një problemi me shërbyesin Home", "failed_soft_logout_homeserver": "Su 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 sduket të jetë e vlefshme.", "forgot_password_email_invalid": "Adresa email sduket të jetë e vlefshme.",
"forgot_password_email_required": "Duhet dhënë adresa email e lidhur me llogarinë tuaj.", "forgot_password_email_required": "Duhet dhënë adresa email e lidhur me llogarinë tuaj.",
"forgot_password_prompt": "Harruat fjalëkalimin tuaj?", "forgot_password_prompt": "Harruat fjalëkalimin tuaj?",
@ -3298,7 +3297,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Dhe %(count)s të tjerë…" "other": "Dhe %(count)s të tjerë…"
}, },
"unknown_device": "Pajisje e panjohur",
"update": { "update": {
"changelog": "Regjistër ndryshimesh", "changelog": "Regjistër ndryshimesh",
"check_action": "Kontrollo për përditësime", "check_action": "Kontrollo për përditësime",

View file

@ -207,7 +207,6 @@
"failed_query_registration_methods": "Kunde inte fråga efter stödda registreringsmetoder.", "failed_query_registration_methods": "Kunde inte fråga efter stödda registreringsmetoder.",
"failed_soft_logout_auth": "Misslyckades att återautentisera", "failed_soft_logout_auth": "Misslyckades att återautentisera",
"failed_soft_logout_homeserver": "Misslyckades att återautentisera p.g.a. ett hemserverproblem", "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_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_email_required": "E-postadressen som är kopplad till ditt konto måste anges.",
"forgot_password_prompt": "Glömt ditt lösenord?", "forgot_password_prompt": "Glömt ditt lösenord?",
@ -3518,7 +3517,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Och %(count)s till…" "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_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", "unsupported_server_title": "Din server stöds inte",
"update": { "update": {

View file

@ -201,7 +201,6 @@
"failed_query_registration_methods": "Не вдалося запитати підтримувані способи реєстрації.", "failed_query_registration_methods": "Не вдалося запитати підтримувані способи реєстрації.",
"failed_soft_logout_auth": "Не вдалося перезайти", "failed_soft_logout_auth": "Не вдалося перезайти",
"failed_soft_logout_homeserver": "Не вдалося перезайти через проблему з домашнім сервером", "failed_soft_logout_homeserver": "Не вдалося перезайти через проблему з домашнім сервером",
"footer_powered_by_matrix": "працює на Matrix",
"forgot_password_email_invalid": "Хибна адреса е-пошти.", "forgot_password_email_invalid": "Хибна адреса е-пошти.",
"forgot_password_email_required": "Введіть е-пошту, прив'язану до вашого облікового запису.", "forgot_password_email_required": "Введіть е-пошту, прив'язану до вашого облікового запису.",
"forgot_password_prompt": "Забули свій пароль?", "forgot_password_prompt": "Забули свій пароль?",
@ -3430,7 +3429,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "І ще %(count)s..." "other": "І ще %(count)s..."
}, },
"unknown_device": "Невідомий пристрій",
"unsupported_server_description": "Цей сервер використовує стару версію Matrix. Оновіть Matrix до %(version)s, щоб використовувати %(brand)s без помилок.", "unsupported_server_description": "Цей сервер використовує стару версію Matrix. Оновіть Matrix до %(version)s, щоб використовувати %(brand)s без помилок.",
"unsupported_server_title": "Ваш сервер не підтримується", "unsupported_server_title": "Ваш сервер не підтримується",
"update": { "update": {

View file

@ -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_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_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ủ", "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_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_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?", "forgot_password_prompt": "Quên mật khẩu của bạn?",
@ -3173,7 +3172,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "Và %(count)s thêm…" "other": "Và %(count)s thêm…"
}, },
"unknown_device": "Thiết bị không xác định",
"update": { "update": {
"changelog": "Lịch sử thay đổi", "changelog": "Lịch sử thay đổi",
"check_action": "Kiểm tra cập nhật", "check_action": "Kiểm tra cập nhật",

View file

@ -202,7 +202,6 @@
"failed_query_registration_methods": "无法查询支持的注册方法。", "failed_query_registration_methods": "无法查询支持的注册方法。",
"failed_soft_logout_auth": "重新认证失败", "failed_soft_logout_auth": "重新认证失败",
"failed_soft_logout_homeserver": "由于家服务器的问题,重新认证失败", "failed_soft_logout_homeserver": "由于家服务器的问题,重新认证失败",
"footer_powered_by_matrix": "由 Matrix 驱动",
"forgot_password_email_invalid": "电子邮件地址似乎无效。", "forgot_password_email_invalid": "电子邮件地址似乎无效。",
"forgot_password_email_required": "必须输入和你账户关联的邮箱地址。", "forgot_password_email_required": "必须输入和你账户关联的邮箱地址。",
"forgot_password_prompt": "忘记你的密码了吗?", "forgot_password_prompt": "忘记你的密码了吗?",
@ -3153,7 +3152,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "和 %(count)s 个其他…" "other": "和 %(count)s 个其他…"
}, },
"unknown_device": "未知设备",
"update": { "update": {
"changelog": "更改日志", "changelog": "更改日志",
"check_action": "检查更新", "check_action": "检查更新",

View file

@ -201,7 +201,6 @@
"failed_query_registration_methods": "無法查詢支援的註冊方法。", "failed_query_registration_methods": "無法查詢支援的註冊方法。",
"failed_soft_logout_auth": "無法重新驗證", "failed_soft_logout_auth": "無法重新驗證",
"failed_soft_logout_homeserver": "因為家伺服器的問題,所以無法重新驗證", "failed_soft_logout_homeserver": "因為家伺服器的問題,所以無法重新驗證",
"footer_powered_by_matrix": "由 Matrix 提供",
"forgot_password_email_invalid": "電子郵件地址似乎無效。", "forgot_password_email_invalid": "電子郵件地址似乎無效。",
"forgot_password_email_required": "必須輸入和您帳號綁定的電子郵件地址。", "forgot_password_email_required": "必須輸入和您帳號綁定的電子郵件地址。",
"forgot_password_prompt": "忘記您的密碼了?", "forgot_password_prompt": "忘記您的密碼了?",
@ -3421,7 +3420,6 @@
"truncated_list_n_more": { "truncated_list_n_more": {
"other": "與更多 %(count)s 個…" "other": "與更多 %(count)s 個…"
}, },
"unknown_device": "未知裝置",
"unsupported_server_description": "此伺服器正在使用較舊版本的 Matrix。升級至 Matrix %(version)s 以在沒有錯誤的情況下使用 %(brand)s。", "unsupported_server_description": "此伺服器正在使用較舊版本的 Matrix。升級至 Matrix %(version)s 以在沒有錯誤的情況下使用 %(brand)s。",
"unsupported_server_title": "您的伺服器不支援", "unsupported_server_title": "您的伺服器不支援",
"update": { "update": {

View file

@ -7,12 +7,13 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; 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 { Room, MatrixEvent, EventType, MsgType } from "matrix-js-sdk/src/matrix";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import escapeHtml from "escape-html"; import escapeHtml from "escape-html";
import { TooltipProvider } from "@vector-im/compound-web"; import { TooltipProvider } from "@vector-im/compound-web";
import { defer } from "matrix-js-sdk/src/utils";
import Exporter from "./Exporter"; import Exporter from "./Exporter";
import { mediaFromMxc } from "../../customisations/Media"; import { mediaFromMxc } from "../../customisations/Media";
@ -263,7 +264,7 @@ export default class HTMLExporter extends Exporter {
return wantsDateSeparator(prevEvent.getDate() || undefined, event.getDate() || undefined); 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 ( return (
<div className="mx_Export_EventWrapper" id={mxEv.getId()}> <div className="mx_Export_EventWrapper" id={mxEv.getId()}>
<MatrixClientContext.Provider value={this.room.client}> <MatrixClientContext.Provider value={this.room.client}>
@ -287,6 +288,7 @@ export default class HTMLExporter extends Exporter {
layout={Layout.Group} layout={Layout.Group}
showReadReceipts={false} showReadReceipts={false}
getRelationsForEvent={this.getRelationsForEvent} getRelationsForEvent={this.getRelationsForEvent}
ref={ref}
/> />
</TooltipProvider> </TooltipProvider>
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
@ -298,7 +300,10 @@ export default class HTMLExporter extends Exporter {
const avatarUrl = this.getAvatarURL(mxEv); const avatarUrl = this.getAvatarURL(mxEv);
const hasAvatar = !!avatarUrl; const hasAvatar = !!avatarUrl;
if (hasAvatar) await this.saveAvatarIfNeeded(mxEv); 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<void>();
const EventTile = this.getEventTile(mxEv, continuation, deferred.resolve);
let eventTileMarkup: string; let eventTileMarkup: string;
if ( 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 // 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 // So, we'll have to render the component into a temporary root element
const tempRoot = document.createElement("div"); const tempElement = document.createElement("div");
ReactDOM.render(EventTile, tempRoot); const tempRoot = createRoot(tempElement);
eventTileMarkup = tempRoot.innerHTML; tempRoot.render(EventTile);
await deferred.promise;
eventTileMarkup = tempElement.innerHTML;
tempRoot.unmount();
} else { } else {
eventTileMarkup = renderToStaticMarkup(EventTile); eventTileMarkup = renderToStaticMarkup(EventTile);
} }

View file

@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { StrictMode } from "react"; import React, { StrictMode } from "react";
import ReactDOM from "react-dom";
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
import { MatrixClient, MatrixEvent, RuleId } from "matrix-js-sdk/src/matrix"; import { MatrixClient, MatrixEvent, RuleId } from "matrix-js-sdk/src/matrix";
import { TooltipProvider } from "@vector-im/compound-web"; 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 { Pill, pillRoomNotifLen, pillRoomNotifPos, PillType } from "../components/views/elements/Pill";
import { parsePermalink } from "./permalinks/Permalinks"; import { parsePermalink } from "./permalinks/Permalinks";
import { PermalinkParts } from "./permalinks/PermalinkConstructor"; import { PermalinkParts } from "./permalinks/PermalinkConstructor";
import { ReactRootManager } from "./react";
/** /**
* A node here is an A element with a href attribute tag. * 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. * to turn into pills.
* @param {MatrixEvent} mxEvent - the matrix event which the DOM nodes are * @param {MatrixEvent} mxEvent - the matrix event which the DOM nodes are
* part of representing. * 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. * React components which have been mounted as part of this.
* The initial caller should pass in an empty array to seed the accumulator. * The initial caller should pass in an empty array to seed the accumulator.
*/ */
@ -56,7 +56,7 @@ export function pillifyLinks(
matrixClient: MatrixClient, matrixClient: MatrixClient,
nodes: ArrayLike<Element>, nodes: ArrayLike<Element>,
mxEvent: MatrixEvent, mxEvent: MatrixEvent,
pills: Element[], pills: ReactRootManager,
): void { ): void {
const room = matrixClient.getRoom(mxEvent.getRoomId()) ?? undefined; const room = matrixClient.getRoom(mxEvent.getRoomId()) ?? undefined;
const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar");
@ -64,7 +64,7 @@ export function pillifyLinks(
while (node) { while (node) {
let pillified = false; 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 // Skip code blocks and existing pills
node = node.nextSibling as Element; node = node.nextSibling as Element;
continue; continue;
@ -83,9 +83,9 @@ export function pillifyLinks(
</StrictMode> </StrictMode>
); );
ReactDOM.render(pill, pillContainer); pills.render(pill, pillContainer);
node.parentNode?.replaceChild(pillContainer, node); node.parentNode?.replaceChild(pillContainer, node);
pills.push(pillContainer);
// Pills within pills aren't going to go well, so move on // Pills within pills aren't going to go well, so move on
pillified = true; pillified = true;
@ -147,9 +147,8 @@ export function pillifyLinks(
</StrictMode> </StrictMode>
); );
ReactDOM.render(pill, pillContainer); pills.render(pill, pillContainer);
roomNotifTextNode.parentNode?.replaceChild(pillContainer, roomNotifTextNode); roomNotifTextNode.parentNode?.replaceChild(pillContainer, roomNotifTextNode);
pills.push(pillContainer);
} }
// Nothing else to do for a text node (and we don't need to advance // Nothing else to do for a text node (and we don't need to advance
// the loop pointer because we did it above) // the loop pointer because we did it above)
@ -165,20 +164,3 @@ export function pillifyLinks(
node = node.nextSibling as Element; 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);
}
}

37
src/utils/react.tsx Normal file
View file

@ -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();
}
}
}

View file

@ -7,11 +7,11 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React, { StrictMode } from "react"; import React, { StrictMode } from "react";
import ReactDOM from "react-dom";
import { TooltipProvider } from "@vector-im/compound-web"; import { TooltipProvider } from "@vector-im/compound-web";
import PlatformPeg from "../PlatformPeg"; import PlatformPeg from "../PlatformPeg";
import LinkWithTooltip from "../components/views/elements/LinkWithTooltip"; 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 * 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 * @param {Element[]} rootNodes - a list of sibling DOM nodes to traverse to try
* to add tooltips. * to add tooltips.
* @param {Element[]} ignoredNodes: a list of nodes to not recurse into. * @param {Element[]} ignoredNodes - a list of nodes to not recurse into.
* @param {Element[]} containers: an accumulator of the DOM nodes which contain * @param {ReactRootManager} tooltips - an accumulator of the DOM nodes which contain
* React components that have been mounted by this function. The initial caller * React components that have been mounted by this function. The initial caller
* should pass in an empty array to seed the accumulator. * should pass in an empty array to seed the accumulator.
*/ */
export function tooltipifyLinks(rootNodes: ArrayLike<Element>, ignoredNodes: Element[], containers: Element[]): void { export function tooltipifyLinks(
rootNodes: ArrayLike<Element>,
ignoredNodes: Element[],
tooltips: ReactRootManager,
): void {
if (!PlatformPeg.get()?.needsUrlTooltips()) { if (!PlatformPeg.get()?.needsUrlTooltips()) {
return; return;
} }
@ -32,7 +36,7 @@ export function tooltipifyLinks(rootNodes: ArrayLike<Element>, ignoredNodes: Ele
let node = rootNodes[0]; let node = rootNodes[0];
while (node) { while (node) {
if (ignoredNodes.includes(node) || containers.includes(node)) { if (ignoredNodes.includes(node) || tooltips.elements.includes(node)) {
node = node.nextSibling as Element; node = node.nextSibling as Element;
continue; continue;
} }
@ -62,26 +66,11 @@ export function tooltipifyLinks(rootNodes: ArrayLike<Element>, ignoredNodes: Ele
</StrictMode> </StrictMode>
); );
ReactDOM.render(tooltip, node); tooltips.render(tooltip, node);
containers.push(node);
} else if (node.childNodes?.length) { } else if (node.childNodes?.length) {
tooltipifyLinks(node.childNodes as NodeListOf<Element>, ignoredNodes, containers); tooltipifyLinks(node.childNodes as NodeListOf<Element>, ignoredNodes, tooltips);
} }
node = node.nextSibling as Element; 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);
}
}

View file

@ -27,7 +27,6 @@ import MatrixChat from "../components/structures/MatrixChat";
import { ValidatedServerConfig } from "../utils/ValidatedServerConfig"; import { ValidatedServerConfig } from "../utils/ValidatedServerConfig";
import { ModuleRunner } from "../modules/ModuleRunner"; import { ModuleRunner } from "../modules/ModuleRunner";
import { parseQs } from "./url_utils"; import { parseQs } from "./url_utils";
import VectorBasePlatform from "./platform/VectorBasePlatform";
import { getInitialScreenAfterLogin, getScreenFromLocation, init as initRouting, onNewScreen } from "./routing"; import { getInitialScreenAfterLogin, getScreenFromLocation, init as initRouting, onNewScreen } from "./routing";
import { UserFriendlyError } from "../languageHandler"; import { UserFriendlyError } from "../languageHandler";
@ -64,7 +63,7 @@ export async function loadApp(fragParams: {}, matrixChatRef: React.Ref<MatrixCha
const urlWithoutQuery = window.location.protocol + "//" + window.location.host + window.location.pathname; const urlWithoutQuery = window.location.protocol + "//" + window.location.host + window.location.pathname;
logger.log("Vector starting at " + urlWithoutQuery); logger.log("Vector starting at " + urlWithoutQuery);
(platform as VectorBasePlatform).startUpdater(); platform?.startUpdater();
// Don't bother loading the app until the config is verified // Don't bother loading the app until the config is verified
const config = await verifyServerConfig(); const config = await verifyServerConfig();

View file

@ -15,7 +15,7 @@ import React from "react";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { randomString } from "matrix-js-sdk/src/randomstring";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform"; import BasePlatform, { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform";
import BaseEventIndexManager from "../../indexing/BaseEventIndexManager"; import BaseEventIndexManager from "../../indexing/BaseEventIndexManager";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
@ -35,7 +35,6 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
import { avatarUrlForRoom, getInitialLetter } from "../../Avatar"; import { avatarUrlForRoom, getInitialLetter } from "../../Avatar";
import DesktopCapturerSourcePicker from "../../components/views/elements/DesktopCapturerSourcePicker"; import DesktopCapturerSourcePicker from "../../components/views/elements/DesktopCapturerSourcePicker";
import { MatrixClientPeg } from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
import VectorBasePlatform from "./VectorBasePlatform";
import { SeshatIndexManager } from "./SeshatIndexManager"; import { SeshatIndexManager } from "./SeshatIndexManager";
import { IPCManager } from "./IPCManager"; import { IPCManager } from "./IPCManager";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
@ -90,7 +89,7 @@ function getUpdateCheckStatus(status: boolean | string): UpdateStatus {
} }
} }
export default class ElectronPlatform extends VectorBasePlatform { export default class ElectronPlatform extends BasePlatform {
private readonly ipc = new IPCManager("ipcCall", "ipcReply"); private readonly ipc = new IPCManager("ipcCall", "ipcReply");
private readonly eventIndexManager: BaseEventIndexManager = new SeshatIndexManager(); private readonly eventIndexManager: BaseEventIndexManager = new SeshatIndexManager();
// this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile // this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile

View file

@ -1,80 +0,0 @@
/*
Copyright 2018-2024 New Vector Ltd.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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<IConfigOptions | undefined> {
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");
}
}

View file

@ -11,12 +11,11 @@ import UAParser from "ua-parser-js";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
import { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform"; import BasePlatform, { UpdateCheckStatus, UpdateStatus } from "../../BasePlatform";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { hideToast as hideUpdateToast, showToast as showUpdateToast } from "../../toasts/UpdateToast"; import { hideToast as hideUpdateToast, showToast as showUpdateToast } from "../../toasts/UpdateToast";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { CheckUpdatesPayload } from "../../dispatcher/payloads/CheckUpdatesPayload"; import { CheckUpdatesPayload } from "../../dispatcher/payloads/CheckUpdatesPayload";
import VectorBasePlatform from "./VectorBasePlatform";
import { parseQs } from "../url_utils"; import { parseQs } from "../url_utils";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
@ -31,7 +30,7 @@ function getNormalizedAppVersion(version: string): string {
return version; 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 private static readonly VERSION = process.env.VERSION!; // baked in by Webpack
public constructor() { public constructor() {

View file

@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import EventEmitter from "events"; import EventEmitter from "events";
import { act } from "jest-matrix-react";
import { ActionPayload } from "../../src/dispatcher/payloads"; import { ActionPayload } from "../../src/dispatcher/payloads";
import defaultDispatcher from "../../src/dispatcher/dispatcher"; import defaultDispatcher from "../../src/dispatcher/dispatcher";
@ -119,7 +120,7 @@ export function untilEmission(
}); });
} }
export const flushPromises = async () => await new Promise<void>((resolve) => window.setTimeout(resolve)); export const flushPromises = () => act(async () => await new Promise<void>((resolve) => window.setTimeout(resolve)));
// with jest's modern fake timers process.nextTick is also mocked, // with jest's modern fake timers process.nextTick is also mocked,
// flushing promises in the normal way then waits for some advancement // flushing promises in the normal way then waits for some advancement

View file

@ -953,7 +953,7 @@ describe("<MatrixChat />", () => {
const getComponentAndWaitForReady = async (): Promise<RenderResult> => { const getComponentAndWaitForReady = async (): Promise<RenderResult> => {
const renderResult = getComponent(); const renderResult = getComponent();
// wait for welcome page chrome render // wait for welcome page chrome render
await screen.findByText("powered by Matrix"); await screen.findByText("Powered by Matrix");
// go to login page // go to login page
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
@ -1480,7 +1480,7 @@ describe("<MatrixChat />", () => {
const getComponentAndWaitForReady = async (): Promise<RenderResult> => { const getComponentAndWaitForReady = async (): Promise<RenderResult> => {
const renderResult = getComponent(); const renderResult = getComponent();
// wait for welcome page chrome render // wait for welcome page chrome render
await screen.findByText("powered by Matrix"); await screen.findByText("Powered by Matrix");
// go to mobile_register page // go to mobile_register page
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
@ -1500,7 +1500,7 @@ describe("<MatrixChat />", () => {
it("should render welcome screen if mobile registration is not enabled in settings", async () => { it("should render welcome screen if mobile registration is not enabled in settings", async () => {
await getComponentAndWaitForReady(); await getComponentAndWaitForReady();
await screen.findByText("powered by Matrix"); await screen.findByText("Powered by Matrix");
}); });
it("should render mobile registration", async () => { it("should render mobile registration", async () => {

View file

@ -114,46 +114,56 @@ exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during sta
> >
<div <div
class="mx_AuthPage_modal" class="mx_AuthPage_modal"
style="position: relative;"
> >
<div <div
class="mx_Welcome" class="mx_AuthPage_modalBlur"
data-testid="mx_welcome_screen" style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; filter: blur(40px);"
/>
<div
class="mx_AuthPage_modalContent"
style="display: flex; z-index: 1; background: rgba(255, 255, 255, 0.59); border-radius: 8px;"
> >
<div <div
class="mx_WelcomePage mx_WelcomePage_loggedIn" class="mx_Welcome"
data-testid="mx_welcome_screen"
> >
<div <div
class="mx_WelcomePage_body" class="mx_WelcomePage mx_WelcomePage_loggedIn"
>
<h1>
Hello
</h1>
</div>
</div>
<div
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
>
<div
aria-describedby="mx_LanguageDropdown_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Language Dropdown"
aria-owns="mx_LanguageDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
> >
<div <div
class="mx_Dropdown_option" class="mx_WelcomePage_body"
id="mx_LanguageDropdown_value"
> >
<div> <h1>
English Hello
</div> </h1>
</div>
</div>
<div
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
>
<div
aria-describedby="mx_LanguageDropdown_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Language Dropdown"
aria-owns="mx_LanguageDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="mx_LanguageDropdown_value"
>
<div>
English
</div>
</div>
<span
class="mx_Dropdown_arrow"
/>
</div> </div>
<span
class="mx_Dropdown_arrow"
/>
</div> </div>
</div> </div>
</div> </div>
@ -162,12 +172,33 @@ exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during sta
class="mx_AuthFooter" class="mx_AuthFooter"
role="contentinfo" role="contentinfo"
> >
<a
href="https://element.io/blog"
rel="noreferrer noopener"
target="_blank"
>
Blog
</a>
<a
href="https://twitter.com/element_hq"
rel="noreferrer noopener"
target="_blank"
>
Twitter
</a>
<a
href="https://github.com/element-hq/element-web"
rel="noreferrer noopener"
target="_blank"
>
GitHub
</a>
<a <a
href="https://matrix.org" href="https://matrix.org"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >
powered by Matrix Powered by Matrix
</a> </a>
</footer> </footer>
</div> </div>
@ -201,116 +232,150 @@ exports[`<MatrixChat /> with a soft-logged-out session should show the soft-logo
> >
<div <div
class="mx_AuthPage_modal" class="mx_AuthPage_modal"
style="position: relative;"
> >
<div <div
class="mx_AuthHeader" class="mx_AuthPage_modalBlur"
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; filter: blur(40px);"
/>
<div
class="mx_AuthPage_modalContent"
style="display: flex; z-index: 1; background: rgba(255, 255, 255, 0.59); border-radius: 8px;"
> >
<aside
class="mx_AuthHeaderLogo"
>
Matrix
</aside>
<div <div
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language" class="mx_AuthHeader"
> >
<div <aside
aria-describedby="mx_LanguageDropdown_value" class="mx_AuthHeaderLogo"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Language Dropdown"
aria-owns="mx_LanguageDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
> >
<div <img
class="mx_Dropdown_option" alt="Element"
id="mx_LanguageDropdown_value" src="themes/element/img/logos/element-logo.svg"
>
<div>
English
</div>
</div>
<span
class="mx_Dropdown_arrow"
/> />
</div> </aside>
</div>
</div>
<main
class="mx_AuthBody"
>
<h1>
You're signed out
</h1>
<h2>
Sign in
</h2>
<div>
<form>
<p>
Enter your password to sign in and regain access to your account.
</p>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_Field_1"
label="Password"
placeholder="Password"
type="password"
value=""
/>
<label
for="mx_Field_1"
>
Password
</label>
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
type="submit"
>
Sign in
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
role="button"
tabindex="0"
>
Forgotten your password?
</div>
</form>
</div>
<h2>
Clear personal data
</h2>
<p>
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.
</p>
<div>
<div <div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger" class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
role="button"
tabindex="0"
> >
Clear all data <div
aria-describedby="mx_LanguageDropdown_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Language Dropdown"
aria-owns="mx_LanguageDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="mx_LanguageDropdown_value"
>
<div>
English
</div>
</div>
<span
class="mx_Dropdown_arrow"
/>
</div>
</div> </div>
</div> </div>
</main> <main
class="mx_AuthBody"
>
<h1>
You're signed out
</h1>
<h2>
Sign in
</h2>
<div>
<form>
<p>
Enter your password to sign in and regain access to your account.
</p>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_Field_1"
label="Password"
placeholder="Password"
type="password"
value=""
/>
<label
for="mx_Field_1"
>
Password
</label>
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
type="submit"
>
Sign in
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
role="button"
tabindex="0"
>
Forgotten your password?
</div>
</form>
</div>
<h2>
Clear personal data
</h2>
<p>
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.
</p>
<div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
role="button"
tabindex="0"
>
Clear all data
</div>
</div>
</main>
</div>
</div> </div>
<footer <footer
class="mx_AuthFooter" class="mx_AuthFooter"
role="contentinfo" role="contentinfo"
> >
<a
href="https://element.io/blog"
rel="noreferrer noopener"
target="_blank"
>
Blog
</a>
<a
href="https://twitter.com/element_hq"
rel="noreferrer noopener"
target="_blank"
>
Twitter
</a>
<a
href="https://github.com/element-hq/element-web"
rel="noreferrer noopener"
target="_blank"
>
GitHub
</a>
<a <a
href="https://matrix.org" href="https://matrix.org"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >
powered by Matrix Powered by Matrix
</a> </a>
</footer> </footer>
</div> </div>

View file

@ -9,16 +9,16 @@ Please see LICENSE files in the repository root for full details.
import * as React from "react"; import * as React from "react";
import { render } from "jest-matrix-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"; import { setupLanguageMock } from "../../../../setup/setupLanguage";
describe("<VectorAuthPage />", () => { describe("<AuthFooter />", () => {
beforeEach(() => { beforeEach(() => {
setupLanguageMock(); setupLanguageMock();
}); });
it("should match snapshot", () => { it("should match snapshot", () => {
const { asFragment } = render(<VectorAuthPage />); const { asFragment } = render(<AuthFooter />);
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
}); });

View file

@ -9,11 +9,11 @@ Please see LICENSE files in the repository root for full details.
import * as React from "react"; import * as React from "react";
import { render } from "jest-matrix-react"; import { render } from "jest-matrix-react";
import VectorAuthHeaderLogo from "../../../../../src/components/views/auth/VectorAuthHeaderLogo"; import AuthHeaderLogo from "../../../../../src/components/views/auth/AuthHeaderLogo";
describe("<VectorAuthHeaderLogo />", () => { describe("<AuthHeaderLogo />", () => {
it("should match snapshot", () => { it("should match snapshot", () => {
const { asFragment } = render(<VectorAuthHeaderLogo />); const { asFragment } = render(<AuthHeaderLogo />);
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
}); });

View file

@ -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("<AuthPage />", () => {
beforeEach(() => {
setupLanguageMock();
SdkConfig.reset();
// @ts-ignore private access
AuthPage.welcomeBackgroundUrl = undefined;
});
it("should match snapshot", () => {
const { asFragment } = render(<AuthPage />);
expect(asFragment()).toMatchSnapshot();
});
it("should use configured background url", () => {
SdkConfig.add({ branding: { welcome_background_url: ["https://example.com/image.png"] } });
const { container } = render(<AuthPage />);
expect(container.querySelector(".mx_AuthPage")).toHaveStyle({
background: "center/cover fixed url(https://example.com/image.png)",
});
});
});

View file

@ -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("<VectorAuthFooter />", () => {
beforeEach(() => {
setupLanguageMock();
});
it("should match snapshot", () => {
const { asFragment } = render(<VectorAuthFooter />);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<VectorAuthFooter /> should match snapshot 1`] = ` exports[`<AuthFooter /> should match snapshot 1`] = `
<DocumentFragment> <DocumentFragment>
<footer <footer
class="mx_AuthFooter" class="mx_AuthFooter"

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<VectorAuthHeaderLogo /> should match snapshot 1`] = ` exports[`<AuthHeaderLogo /> should match snapshot 1`] = `
<DocumentFragment> <DocumentFragment>
<aside <aside
class="mx_AuthHeaderLogo" class="mx_AuthHeaderLogo"

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<VectorAuthPage /> should match snapshot 1`] = ` exports[`<AuthPage /> should match snapshot 1`] = `
<DocumentFragment> <DocumentFragment>
<div <div
class="mx_AuthPage" class="mx_AuthPage"

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; import React from "react";
import { fireEvent, render, RenderResult } from "jest-matrix-react"; import { fireEvent, render, RenderResult, waitFor } from "jest-matrix-react";
import { import {
MatrixEvent, MatrixEvent,
Relations, Relations,
@ -83,7 +83,7 @@ describe("MPollBody", () => {
expect(votesCount(renderResult, "poutine")).toBe(""); expect(votesCount(renderResult, "poutine")).toBe("");
expect(votesCount(renderResult, "italian")).toBe(""); expect(votesCount(renderResult, "italian")).toBe("");
expect(votesCount(renderResult, "wings")).toBe(""); expect(votesCount(renderResult, "wings")).toBe("");
expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("No votes cast"); await waitFor(() => expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("No votes cast"));
expect(renderResult.getByText("What should we order for the party?")).toBeTruthy(); expect(renderResult.getByText("What should we order for the party?")).toBeTruthy();
}); });

View file

@ -59,7 +59,7 @@ describe("<JoinRuleSettings />", () => {
onError: jest.fn(), onError: jest.fn(),
}; };
const getComponent = (props: Partial<JoinRuleSettingsProps> = {}) => const getComponent = (props: Partial<JoinRuleSettingsProps> = {}) =>
render(<JoinRuleSettings {...defaultProps} {...props} />); render(<JoinRuleSettings {...defaultProps} {...props} />, { legacyRoot: false });
const setRoomStateEvents = ( const setRoomStateEvents = (
room: Room, room: Room,

View file

@ -130,10 +130,8 @@ describe("<SecureBackupPanel />", () => {
}) })
.mockResolvedValue(null); .mockResolvedValue(null);
getComponent(); getComponent();
// flush checkKeyBackup promise
await flushPromises();
fireEvent.click(screen.getByText("Delete Backup")); fireEvent.click(await screen.findByText("Delete Backup"));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");

View file

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; import React from "react";
import { render } from "jest-matrix-react"; import { act, render } from "jest-matrix-react";
import { MatrixEvent, ConditionKind, EventType, PushRuleActionName, Room, TweakName } from "matrix-js-sdk/src/matrix"; import { MatrixEvent, ConditionKind, EventType, PushRuleActionName, Room, TweakName } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
@ -15,6 +15,7 @@ import { pillifyLinks } from "../../../src/utils/pillify";
import { stubClient } from "../../test-utils"; import { stubClient } from "../../test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import DMRoomMap from "../../../src/utils/DMRoomMap"; import DMRoomMap from "../../../src/utils/DMRoomMap";
import { ReactRootManager } from "../../../src/utils/react.tsx";
describe("pillify", () => { describe("pillify", () => {
const roomId = "!room:id"; const roomId = "!room:id";
@ -84,51 +85,55 @@ describe("pillify", () => {
it("should do nothing for empty element", () => { it("should do nothing for empty element", () => {
const { container } = render(<div />); const { container } = render(<div />);
const originalHtml = container.outerHTML; const originalHtml = container.outerHTML;
const containers: Element[] = []; const containers = new ReactRootManager();
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers); pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
expect(containers).toHaveLength(0); expect(containers.elements).toHaveLength(0);
expect(container.outerHTML).toEqual(originalHtml); expect(container.outerHTML).toEqual(originalHtml);
}); });
it("should pillify @room", () => { it("should pillify @room", () => {
const { container } = render(<div>@room</div>); const { container } = render(<div>@room</div>);
const containers: Element[] = []; const containers = new ReactRootManager();
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers); act(() => pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers));
expect(containers).toHaveLength(1); expect(containers.elements).toHaveLength(1);
expect(container.querySelector(".mx_Pill.mx_AtRoomPill")?.textContent).toBe("!@room"); expect(container.querySelector(".mx_Pill.mx_AtRoomPill")?.textContent).toBe("!@room");
}); });
it("should pillify @room in an intentional mentions world", () => { it("should pillify @room in an intentional mentions world", () => {
mocked(MatrixClientPeg.safeGet().supportsIntentionalMentions).mockReturnValue(true); mocked(MatrixClientPeg.safeGet().supportsIntentionalMentions).mockReturnValue(true);
const { container } = render(<div>@room</div>); const { container } = render(<div>@room</div>);
const containers: Element[] = []; const containers = new ReactRootManager();
pillifyLinks( act(() =>
MatrixClientPeg.safeGet(), pillifyLinks(
[container], MatrixClientPeg.safeGet(),
new MatrixEvent({ [container],
room_id: roomId, new MatrixEvent({
type: EventType.RoomMessage, room_id: roomId,
content: { type: EventType.RoomMessage,
"body": "@room", content: {
"m.mentions": { "body": "@room",
room: true, "m.mentions": {
room: true,
},
}, },
}, }),
}), containers,
containers, ),
); );
expect(containers).toHaveLength(1); expect(containers.elements).toHaveLength(1);
expect(container.querySelector(".mx_Pill.mx_AtRoomPill")?.textContent).toBe("!@room"); expect(container.querySelector(".mx_Pill.mx_AtRoomPill")?.textContent).toBe("!@room");
}); });
it("should not double up pillification on repeated calls", () => { it("should not double up pillification on repeated calls", () => {
const { container } = render(<div>@room</div>); const { container } = render(<div>@room</div>);
const containers: Element[] = []; const containers = new ReactRootManager();
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers); act(() => {
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers); pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers); pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers); pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
expect(containers).toHaveLength(1); pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
});
expect(containers.elements).toHaveLength(1);
expect(container.querySelector(".mx_Pill.mx_AtRoomPill")?.textContent).toBe("!@room"); expect(container.querySelector(".mx_Pill.mx_AtRoomPill")?.textContent).toBe("!@room");
}); });
}); });

View file

@ -12,6 +12,7 @@ import { act, render } from "jest-matrix-react";
import { tooltipifyLinks } from "../../../src/utils/tooltipify"; import { tooltipifyLinks } from "../../../src/utils/tooltipify";
import PlatformPeg from "../../../src/PlatformPeg"; import PlatformPeg from "../../../src/PlatformPeg";
import BasePlatform from "../../../src/BasePlatform"; import BasePlatform from "../../../src/BasePlatform";
import { ReactRootManager } from "../../../src/utils/react.tsx";
describe("tooltipify", () => { describe("tooltipify", () => {
jest.spyOn(PlatformPeg, "get").mockReturnValue({ needsUrlTooltips: () => true } as unknown as BasePlatform); jest.spyOn(PlatformPeg, "get").mockReturnValue({ needsUrlTooltips: () => true } as unknown as BasePlatform);
@ -19,9 +20,9 @@ describe("tooltipify", () => {
it("does nothing for empty element", () => { it("does nothing for empty element", () => {
const { container: root } = render(<div />); const { container: root } = render(<div />);
const originalHtml = root.outerHTML; const originalHtml = root.outerHTML;
const containers: Element[] = []; const containers = new ReactRootManager();
tooltipifyLinks([root], [], containers); tooltipifyLinks([root], [], containers);
expect(containers).toHaveLength(0); expect(containers.elements).toHaveLength(0);
expect(root.outerHTML).toEqual(originalHtml); expect(root.outerHTML).toEqual(originalHtml);
}); });
@ -31,9 +32,9 @@ describe("tooltipify", () => {
<a href="/foo">click</a> <a href="/foo">click</a>
</div>, </div>,
); );
const containers: Element[] = []; const containers = new ReactRootManager();
tooltipifyLinks([root], [], containers); tooltipifyLinks([root], [], containers);
expect(containers).toHaveLength(1); expect(containers.elements).toHaveLength(1);
const anchor = root.querySelector("a"); const anchor = root.querySelector("a");
expect(anchor?.getAttribute("href")).toEqual("/foo"); expect(anchor?.getAttribute("href")).toEqual("/foo");
const tooltip = anchor!.querySelector(".mx_TextWithTooltip_target"); const tooltip = anchor!.querySelector(".mx_TextWithTooltip_target");
@ -47,9 +48,9 @@ describe("tooltipify", () => {
</div>, </div>,
); );
const originalHtml = root.outerHTML; const originalHtml = root.outerHTML;
const containers: Element[] = []; const containers = new ReactRootManager();
tooltipifyLinks([root], [root.children[0]], containers); tooltipifyLinks([root], [root.children[0]], containers);
expect(containers).toHaveLength(0); expect(containers.elements).toHaveLength(0);
expect(root.outerHTML).toEqual(originalHtml); expect(root.outerHTML).toEqual(originalHtml);
}); });
@ -59,12 +60,12 @@ describe("tooltipify", () => {
<a href="/foo">click</a> <a href="/foo">click</a>
</div>, </div>,
); );
const containers: Element[] = []; const containers = new ReactRootManager();
tooltipifyLinks([root], [], containers); tooltipifyLinks([root], [], containers);
tooltipifyLinks([root], [], containers); tooltipifyLinks([root], [], containers);
tooltipifyLinks([root], [], containers); tooltipifyLinks([root], [], containers);
tooltipifyLinks([root], [], containers); tooltipifyLinks([root], [], containers);
expect(containers).toHaveLength(1); expect(containers.elements).toHaveLength(1);
const anchor = root.querySelector("a"); const anchor = root.querySelector("a");
expect(anchor?.getAttribute("href")).toEqual("/foo"); expect(anchor?.getAttribute("href")).toEqual("/foo");
const tooltip = anchor!.querySelector(".mx_TextWithTooltip_target"); const tooltip = anchor!.querySelector(".mx_TextWithTooltip_target");

View file

@ -229,4 +229,18 @@ describe("WebPlatform", () => {
}); });
}); });
}); });
it("should return config from config.json", async () => {
window.location.hostname = "domain.com";
fetchMock.get(/config\.json.*/, { brand: "test" });
const platform = new WebPlatform();
await expect(platform.getConfig()).resolves.toEqual(expect.objectContaining({ brand: "test" }));
});
it("should re-render favicon when setting error status", () => {
const platform = new WebPlatform();
const spy = jest.spyOn(platform.favicon, "badge");
platform.setErrorStatus(true);
expect(spy).toHaveBeenCalledWith(expect.anything(), { bgColor: "#f00" });
});
}); });