feat: show call participants in room list (Discord-style)
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled

This commit is contained in:
sorB
2026-05-10 14:25:35 +02:00
parent b797925316
commit 3da363517f
4610 changed files with 827237 additions and 1 deletions
@@ -0,0 +1,78 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { render, screen, act } from "jest-matrix-react";
import { waitFor } from "@testing-library/dom";
import userEvent from "@testing-library/user-event";
import NewRecoveryMethodDialog from "../../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog";
import { createTestClient } from "../../../../test-utils";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext.tsx";
import dis from "../../../../../src/dispatcher/dispatcher.ts";
import { Action } from "../../../../../src/dispatcher/actions.ts";
import Modal from "../../../../../src/Modal.tsx";
describe("<NewRecoveryMethodDialog />", () => {
let matrixClient: MatrixClient;
beforeEach(() => {
matrixClient = createTestClient();
jest.spyOn(dis, "fire");
jest.spyOn(Modal, "createDialog");
});
afterEach(() => {
jest.restoreAllMocks();
});
function renderComponent(onFinished: () => void = jest.fn()) {
return render(
<MatrixClientContext.Provider value={matrixClient}>
<NewRecoveryMethodDialog onFinished={onFinished} />
</MatrixClientContext.Provider>,
);
}
test("when cancel is clicked", async () => {
const onFinished = jest.fn();
act(() => {
renderComponent(onFinished);
});
await userEvent.click(screen.getByRole("button", { name: "Go to Settings" }));
expect(onFinished).toHaveBeenCalled();
expect(dis.fire).toHaveBeenCalledWith(Action.ViewUserSettings);
});
test("when key backup is enabled", async () => {
jest.spyOn(matrixClient.getCrypto()!, "getActiveSessionBackupVersion").mockResolvedValue("version");
const onFinished = jest.fn();
const { asFragment } = renderComponent(onFinished);
await waitFor(() =>
expect(
screen.getByText("This session is encrypting history using the new recovery method."),
).toBeInTheDocument(),
);
expect(asFragment()).toMatchSnapshot();
await userEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" }));
expect(onFinished).toHaveBeenCalled();
});
test("when key backup is disabled", async () => {
const onFinished = jest.fn();
const { asFragment } = renderComponent(onFinished);
expect(asFragment()).toMatchSnapshot();
await userEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" }));
await waitFor(() => expect(Modal.createDialog).toHaveBeenCalled());
});
});
@@ -0,0 +1,34 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen, fireEvent, waitFor } from "jest-matrix-react";
import RecoveryMethodRemovedDialog from "../../../../../src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog";
import dispatch from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
import { UserTab } from "../../../../../src/components/views/dialogs/UserTab";
describe("<RecoveryMethodRemovedDialog />", () => {
afterEach(() => {
jest.restoreAllMocks();
});
it("should open CreateKeyBackupDialog on primary action click", async () => {
const onFinished = jest.fn();
jest.spyOn(dispatch, "dispatch");
render(<RecoveryMethodRemovedDialog onFinished={onFinished} />);
fireEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" }));
await waitFor(() =>
expect(dispatch.dispatch).toHaveBeenCalledWith({
action: Action.ViewUserSettings,
initialTabId: UserTab.Encryption,
}),
);
});
});
@@ -0,0 +1,194 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<NewRecoveryMethodDialog /> when key backup is disabled 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_KeyBackupFailedDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
<span
class="mx_KeyBackupFailedDialog_title"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20"
/>
</svg>
New Recovery Method
</span>
</h1>
</div>
<p>
A new Security Phrase and key for Secure Messages have been detected.
</p>
<strong
class="warning"
>
If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.
</strong>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
data-testid="dialog-cancel-button"
type="button"
>
Go to Settings
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Set up Secure Messages
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
/>
</svg>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`<NewRecoveryMethodDialog /> when key backup is enabled 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_KeyBackupFailedDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
<span
class="mx_KeyBackupFailedDialog_title"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20"
/>
</svg>
New Recovery Method
</span>
</h1>
</div>
<p>
A new Security Phrase and key for Secure Messages have been detected.
</p>
<p>
This session is encrypting history using the new recovery method.
</p>
<strong
class="warning"
>
If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.
</strong>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
data-testid="dialog-cancel-button"
type="button"
>
Go to Settings
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Set up Secure Messages
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
/>
</svg>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
@@ -0,0 +1,31 @@
/*
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 OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import SdkConfig from "../../../../src/SdkConfig";
import { ErrorView, UnsupportedBrowserView } from "../../../../src/async-components/structures/ErrorView";
describe("<ErrorView />", () => {
it("should match snapshot", () => {
const { asFragment } = render(<ErrorView title="TITLE" messages={["MSG1", "MSG2"]} />);
expect(asFragment()).toMatchSnapshot();
});
});
describe("<UnsupportedBrowserView />", () => {
beforeEach(() => {
SdkConfig.put({});
});
it("should match snapshot", () => {
const { asFragment } = render(<UnsupportedBrowserView />);
expect(asFragment()).toMatchSnapshot();
});
});
@@ -0,0 +1,300 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<ErrorView /> should match snapshot 1`] = `
<DocumentFragment>
<div
class="mx_ErrorView cpd-theme-light"
>
<img
alt="Element"
class="mx_ErrorView_logo"
height="160"
src="themes/element/img/logos/element-app-logo.png"
/>
<div
class="mx_ErrorView_container"
>
<h1
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
>
TITLE
</h1>
<p
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69"
>
MSG1
</p>
<p
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69"
>
MSG2
</p>
</div>
</div>
</DocumentFragment>
`;
exports[`<UnsupportedBrowserView /> should match snapshot 1`] = `
<DocumentFragment>
<div
class="mx_ErrorView cpd-theme-light"
>
<img
alt="Element"
class="mx_ErrorView_logo"
height="160"
src="themes/element/img/logos/element-app-logo.png"
/>
<div
class="mx_ErrorView_container"
>
<h1
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
>
Element does not support this browser
</h1>
<p
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69"
>
Element uses some browser features which are not available in your current browser. Try updating this browser if you're not using the latest version and try again.
</p>
<p
class="_typography_6v6n8_153 _font-body-lg-regular_6v6n8_69"
>
<span>
For the best experience, use
<a
href="https://google.com/chrome"
rel="noreferrer noopener"
target="_blank"
>
Chrome
</a>
,
<a
href="https://firefox.com"
rel="noreferrer noopener"
target="_blank"
>
Firefox
</a>
,
<a
href="https://microsoft.com/edge"
rel="noreferrer noopener"
target="_blank"
>
Edge
</a>
, or
<a
href="https://apple.com/safari"
rel="noreferrer noopener"
target="_blank"
>
Safari
</a>
.
</span>
</p>
<div
class="_flex_4dswl_9 mx_ErrorView_flexContainer mx_ErrorView_buttons"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
>
<button
class="_button_1nw83_8 _has-icon_1nw83_60"
data-kind="secondary"
data-size="md"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
/>
<path
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
/>
</svg>
Learn more
</button>
</div>
</div>
<div
class="_separator_13qwf_8"
data-kind="primary"
data-orientation="horizontal"
role="separator"
/>
<h2
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
>
Use Element Desktop instead
</h2>
<div
class="_flex_4dswl_9 mx_ErrorView_flexContainer"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
>
<a
class="_button_1nw83_8 _has-icon_1nw83_60"
data-kind="secondary"
data-size="lg"
href="https://packages.element.io/desktop/install/macos/Element.dmg"
role="link"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M16.099 2.4a4.1 4.1 0 0 1-1.057 3.073c-.747.863-1.878 1.36-3.07 1.348-.075-1.081.315-2.146 1.085-2.96.78-.825 1.866-1.346 3.042-1.461m3.767 6.54c-1.37.783-2.214 2.163-2.234 3.657.002 1.69 1.092 3.215 2.768 3.873a9.4 9.4 0 0 1-1.44 2.723c-.848 1.178-1.737 2.329-3.149 2.35-.671.015-1.124-.165-1.596-.351-.493-.195-1.006-.398-1.809-.398-.852 0-1.388.21-1.905.412-.447.174-.88.343-1.49.367-1.343.046-2.37-1.258-3.25-2.425-1.756-2.383-3.124-6.716-1.29-9.664.86-1.437 2.471-2.349 4.241-2.402.763-.015 1.494.258 2.135.497.49.183.929.347 1.287.347.315 0 .74-.157 1.237-.34.78-.288 1.737-.64 2.71-.545 1.514.044 2.917.748 3.785 1.9"
fill-rule="evenodd"
/>
</svg>
Mac
</a>
<a
class="_button_1nw83_8 _has-icon_1nw83_60"
data-kind="secondary"
data-size="lg"
href="https://packages.element.io/desktop/install/win32/x64/Element%20Setup.exe"
role="link"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.589 2.4H21.6v9.011h-9.011zM2.4 12.588h9.011v9.011H2.4zM2.4 2.4h9.011v9.011H2.4zm10.189 10.188H21.6v9.011h-9.011z"
/>
</svg>
Windows (64-bit)
</a>
<a
class="_button_1nw83_8 _has-icon_1nw83_60"
data-kind="secondary"
data-size="lg"
href="https://packages.element.io/desktop/install/win32/arm64/Element%20Setup.exe"
role="link"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.589 2.4H21.6v9.011h-9.011zM2.4 12.588h9.011v9.011H2.4zM2.4 2.4h9.011v9.011H2.4zm10.189 10.188H21.6v9.011h-9.011z"
/>
</svg>
Windows (ARM 64-bit)
</a>
<a
class="_button_1nw83_8 _has-icon_1nw83_60"
data-kind="secondary"
data-size="lg"
href="https://element.io/download#linux"
role="link"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#cpd_LinuxIcon_a)"
>
<path
d="M13.206 20.644a2.7 2.7 0 0 1-1.097.248 2.8 2.8 0 0 1-1.567-.495c-.153.38-.392.723-.7 1a2.8 2.8 0 0 1-1.078.604h6.141a2.8 2.8 0 0 1-.99-.51 2.8 2.8 0 0 1-.709-.847M6.71 16.673a.2.2 0 0 0 .067-.03q.056-.49.13-.94c-.01-.169.017-.338.08-.495.348-1.904.834-3.24 1.504-4.052a.13.13 0 0 1 .18-.018.123.123 0 0 1 .018.174 4 4 0 0 0-.203.272q-.764 1.113-1.199 3.389h.03a.6.6 0 0 1 .166 0c.198.042.508.203.985.67.061-2.72 1.661-4.913 3.642-4.913 1.748 0 3.208 1.715 3.556 3.997a1.7 1.7 0 0 1 .762-.742.8.8 0 0 1 .254-.047 7.5 7.5 0 0 0-.948-2.342 4 4 0 0 0-.2-.272.12.12 0 0 1-.03-.09.12.12 0 0 1 .048-.083.13.13 0 0 1 .092-.028.13.13 0 0 1 .085.045c.508.624.93 1.557 1.245 2.822.678.247.696 1.361.71 2.267 0 .43 0 .877.105.99.104.114.325.09.635-.04.036-.393.053-.742.063-1.036v-1.082c0-2.475-3.266-6.755-3.266-6.755l-.378-3.218c0-2.953-2.733-2.926-2.733-2.926s-2.743-.027-2.743 2.914l-.365 3.23s-3.267 4.277-3.267 6.755v.31s-.012.299 0 .767v.227c.414.154.839.315.976.28m5.388-10.41c.761.068 1.975.247 2.031.613.04.282-.429.822-.523.926-.195.215-.876.92-1.49.92-.615 0-1.296-.705-1.49-.92-.093-.104-.563-.644-.525-.926.046-.374 1.273-.545 1.996-.614"
/>
<path
d="M12.111 8.475c.333 0 .843-.33 1.298-.837.348-.384.475-.666.465-.73-.064-.127-.894-.317-1.778-.401-.856.084-1.687.274-1.75.406.091.275.252.523.467.722.457.51.965.84 1.298.84m.684 10.664c-.292-2.607-.023-3.146.206-3.324a.42.42 0 0 1 .4-.057c.23.11.431.271.587.47.287.304.488.495.674.391.131-.077.32-.495.507-.879.097-.205.196-.425.305-.636-.254-2.33-1.65-4.126-3.355-4.126-1.872 0-3.393 2.166-3.393 4.827v.096c.31.317.68.743 1.133 1.3q.266.328.487.685a2.7 2.7 0 0 1 .29 2.258c.426.312.943.485 1.476.495.341-.002.679-.073.99-.21a2.6 2.6 0 0 1-.16-.46 7 7 0 0 1-.147-.83"
/>
<path
d="M9.651 17.35c-.355-.44-.657-.789-.916-1.071l-.13-.139-.132-.138c-.587-.594-.897-.758-1.06-.785a.2.2 0 0 0-.068 0 .15.15 0 0 0-.114.082v.017a.9.9 0 0 0-.058.406v.327c.034.25-.019.505-.15.723a.4.4 0 0 1-.087.071l-.053.032a.6.6 0 0 1-.107.043H6.75c-.211.037-.554-.084-.988-.248l-.127-.05c-.544-.21-1.179-.452-1.644-.452a.63.63 0 0 0-.553.232c-.34.495.421 1.273 1.036 1.901.378.389.675.693.708.921.059.42-.393.582-.792.725a1.5 1.5 0 0 0-.564.278c-.06.069-.058.116-.045.15.055.191.507.61 2.897 1.285q.398.112.806.178c.49.073.992.01 1.448-.182a2.64 2.64 0 0 0 1.13-.902q.165-.245.271-.522l.05-.121c0-.042.024-.09.037-.131a2.48 2.48 0 0 0-.293-1.98 7 7 0 0 0-.475-.65m9.618-.17c-.212.02-.418.084-.604.186l-.277.117c-.305.113-.61.168-.807-.095a.6.6 0 0 1-.094-.227 3.6 3.6 0 0 1-.056-.592v-.322c0-.72-.026-1.586-.373-1.925a.5.5 0 0 0-.255-.14h-.038a.5.5 0 0 0-.254.035c-.3.12-.56.496-.79.936l-.109.215-.117.248-.104.225c-.223.495-.398.874-.601.99-.376.216-.704-.13-.991-.438a1.5 1.5 0 0 0-.48-.4.2.2 0 0 0-.076-.016q-.048 0-.084.03c-.158.124-.384.69-.112 3.104q.044.4.137.792.052.207.137.404.027.056.056.111c.02.037.033.074.054.111.208.377.51.696.879.928a2.66 2.66 0 0 0 2.504.171c.398-.18.743-.454 1.003-.8a7 7 0 0 0 .45-.675c1.218-2.116 1.241-2.722 1.147-2.896a.15.15 0 0 0-.144-.077"
/>
</g>
<defs>
<clippath
id="cpd_LinuxIcon_a"
>
<path
d="M3.167 2.19H20.5V22H3.165z"
/>
</clippath>
</defs>
</svg>
Linux
</a>
</div>
<h2
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
>
Or use our mobile app
</h2>
<div
class="_flex_4dswl_9 mx_ErrorView_flexContainer"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-6x); --mx-flex-wrap: nowrap;"
>
<a
href="https://apps.apple.com/app/vector/id1083446067"
rel="noreferrer noopener"
target="_blank"
>
<img
alt="Apple App Store"
height="64"
src="themes/element/img/download/apple.svg"
/>
</a>
<a
href="https://play.google.com/store/apps/details?id=im.vector.app"
rel="noreferrer noopener"
target="_blank"
>
<img
alt="Google Play Store"
height="64"
src="themes/element/img/download/google.svg"
/>
</a>
<a
href="https://f-droid.org/repository/browse/?fdid=im.vector.app"
rel="noreferrer noopener"
target="_blank"
>
<img
alt="F-Droid"
height="64"
src="themes/element/img/download/fdroid.svg"
/>
</a>
</div>
</div>
</DocumentFragment>
`;