Merge branch 'develop' into staging

# Conflicts:
#	package.json
#	src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx
This commit is contained in:
ElementRobot 2023-06-01 16:50:29 +01:00
commit c494e3c049
441 changed files with 11057 additions and 7362 deletions

View file

@ -1,13 +1,33 @@
# Triggers after the layered build has finished, taking the artifact and running cypress on it # Triggers after the layered build has finished, taking the artifact and running cypress on it
#
# Also called by a workflow in matrix-js-sdk.
#
name: Cypress End to End Tests name: Cypress End to End Tests
on: on:
workflow_run: workflow_run:
workflows: ["Element Web - Build"] workflows: ["Element Web - Build"]
types: types:
- completed - completed
# support calls from other workflows
workflow_call:
inputs:
react-sdk-repository:
type: string
required: true
description: "The name of the github repository to check out and build."
rust-crypto:
type: boolean
required: false
description: "Enable Rust cryptography for the cypress run."
secrets:
CYPRESS_RECORD_KEY:
required: true
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }} group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }} cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
jobs: jobs:
prepare: prepare:
name: Prepare name: Prepare
@ -100,16 +120,10 @@ jobs:
- uses: browser-actions/setup-chrome@c485fa3bab6be59dce18dbc18ef6ab7cbc8ff5f1 - uses: browser-actions/setup-chrome@c485fa3bab6be59dce18dbc18ef6ab7cbc8ff5f1
- run: echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV - run: echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV
- uses: tecolicom/actions-use-apt-tools@ceaf289fdbc6169fd2406a0f0365a584ffba003b # v1
with:
# Our test suite includes some screenshot tests with unusual diacritics, which are
# supposed to be covered by STIXGeneral.
tools: fonts-stix
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action # There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess: # (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact - name: 📥 Download artifact
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2 uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
with: with:
run_id: ${{ github.event.workflow_run.id }} run_id: ${{ github.event.workflow_run.id }}
name: previewbuild name: previewbuild
@ -129,14 +143,23 @@ jobs:
# XXX: We're checking out untrusted code in a secure context # XXX: We're checking out untrusted code in a secure context
# We need to be careful to not trust anything this code outputs/may do # We need to be careful to not trust anything this code outputs/may do
# #
# Note that we check out from the default repository, which is (for this workflow) the # Note that (in the absence of a `react-sdk-repository` input),
# we check out from the default repository, which is (for this workflow) the
# *target* repository for the pull request. # *target* repository for the pull request.
#
ref: ${{ steps.sha.outputs.sha }} ref: ${{ steps.sha.outputs.sha }}
persist-credentials: false persist-credentials: false
path: matrix-react-sdk path: matrix-react-sdk
repository: ${{ inputs.react-sdk-repository || github.repository }}
# Enable rust crypto if the calling workflow requests it
- name: Enable rust crypto
if: inputs.rust-crypto
run: |
echo "CYPRESS_RUST_CRYPTO=1" >> "$GITHUB_ENV"
- name: Run Cypress tests - name: Run Cypress tests
uses: cypress-io/github-action@59c3b9b4a1a6e623c29806797d849845443487d1 uses: cypress-io/github-action@40a1a26c08d0e549e8516612ecebbd1ab5eeec8f
with: with:
working-directory: matrix-react-sdk working-directory: matrix-react-sdk
# The built-in Electron runner seems to grind to a halt trying # The built-in Electron runner seems to grind to a halt trying

View file

@ -12,19 +12,37 @@ on:
branches: [develop, master] branches: [develop, master]
repository_dispatch: repository_dispatch:
types: [upstream-sdk-notify] types: [upstream-sdk-notify]
# support triggering from other workflows
workflow_call:
inputs:
react-sdk-repository:
type: string
required: true
description: "The name of the github repository to check out and build."
matrix-js-sdk-sha:
type: string
required: false
description: "The Git SHA of matrix-js-sdk to build against. By default, will use a matching branch name if it exists, or develop."
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true cancel-in-progress: true
env: env:
# These must be set for fetchdep.sh to get the right branch # fetchdep.sh needs to know our PR number
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
jobs: jobs:
build: build:
name: "Build Element-Web" name: "Build Element-Web"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - name: Checkout code
uses: actions/checkout@v3
with:
repository: ${{ inputs.react-sdk-repository || github.repository }}
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
@ -32,6 +50,9 @@ jobs:
- name: Fetch layered build - name: Fetch layered build
id: layered_build id: layered_build
env:
# tell layered.sh to check out the right sha of the JS-SDK, if we were given one
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
run: | run: |
scripts/ci/layered.sh scripts/ci/layered.sh
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD) JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
@ -43,7 +64,6 @@ jobs:
run: cp element.io/develop/config.json config.json run: cp element.io/develop/config.json config.json
working-directory: ./element-web working-directory: ./element-web
# After building we write the version file and the react-sdk sha so our cypress tests are from the same sha
- name: Build - name: Build
env: env:
CI_PACKAGE: true CI_PACKAGE: true
@ -51,9 +71,13 @@ jobs:
run: | run: |
yarn build yarn build
echo $VERSION > webapp/version echo $VERSION > webapp/version
echo $GITHUB_SHA > webapp/sha
working-directory: ./element-web working-directory: ./element-web
# Record the react-sdk sha so our cypress tests are from the same sha
- name: Record react-sdk SHA
run: |
git rev-parse HEAD > element-web/webapp/sha
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:

View file

@ -33,7 +33,7 @@ jobs:
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action # There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess: # (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact - name: 📥 Download artifact
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2 uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
with: with:
run_id: ${{ github.event.workflow_run.id }} run_id: ${{ github.event.workflow_run.id }}
name: previewbuild name: previewbuild

View file

@ -10,10 +10,11 @@ on:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true cancel-in-progress: true
env: env:
# These must be set for fetchdep.sh to get the right branch # fetchdep.sh needs to know our PR number
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
jobs: jobs:
ts_lint: ts_lint:
name: "Typescript Syntax Check" name: "Typescript Syntax Check"
@ -59,7 +60,7 @@ jobs:
- name: Get diff lines - name: Get diff lines
id: diff id: diff
uses: Equip-Collaboration/diff-line-numbers@df70b4b83e05105c15f20dc6cc61f1463411b2a6 # v1.0.0 uses: Equip-Collaboration/diff-line-numbers@e752977e2cb4207d671bb9e4dad18c07c1b73d52 # v1.1.0
with: with:
include: '["\\.tsx?$"]' include: '["\\.tsx?$"]'

View file

@ -20,11 +20,12 @@ on:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true cancel-in-progress: true
env: env:
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' && inputs.disable_coverage != 'true' }} ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' && inputs.disable_coverage != 'true' }}
# These must be set for fetchdep.sh to get the right branch # fetchdep.sh needs to know our PR number
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
jobs: jobs:
jest: jest:
name: Jest name: Jest

View file

@ -33,6 +33,11 @@ module.exports = {
"import-notation": null, "import-notation": null,
"value-keyword-case": null, "value-keyword-case": null,
"declaration-block-no-redundant-longhand-properties": null, "declaration-block-no-redundant-longhand-properties": null,
"declaration-block-no-duplicate-properties": [
true,
// useful for fallbacks
{ ignore: ["consecutive-duplicates-with-different-values"] },
],
"shorthand-property-no-redundant-values": null, "shorthand-property-no-redundant-values": null,
"property-no-vendor-prefix": null, "property-no-vendor-prefix": null,
"value-no-vendor-prefix": null, "value-no-vendor-prefix": null,

View file

@ -176,7 +176,7 @@ describe("Audio player", () => {
// Enable high contrast manually // Enable high contrast manually
cy.openUserSettings("Appearance") cy.openUserSettings("Appearance")
.get(".mx_ThemeChoicePanel") .findByTestId("mx_ThemeChoicePanel")
.findByLabelText("Use high contrast") .findByLabelText("Use high contrast")
.click({ force: true }); // force click because the size of the checkbox is zero .click({ force: true }); // force click because the size of the checkbox is zero
@ -333,30 +333,33 @@ describe("Audio player", () => {
// On a thread // On a thread
cy.get(".mx_ThreadView").within(() => { cy.get(".mx_ThreadView").within(() => {
cy.get(".mx_EventTile_last") cy.get(".mx_EventTile_last").within(() => {
.within(() => { // Assert that the player is correctly rendered on a thread
// Assert that the player is correctly rendered on a thread cy.get(".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container").within(() => {
cy.get(".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container").within(() => { // Assert that the counter is zero before clicking the play button
// Assert that the counter is zero before clicking the play button cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
// Find and click "Play" button, the wait is to make the test less flaky // Find and click "Play" button, the wait is to make the test less flaky
cy.findByRole("button", { name: "Play" }).should("exist"); cy.findByRole("button", { name: "Play" }).should("exist");
cy.wait(500).findByRole("button", { name: "Play" }).click(); cy.wait(500).findByRole("button", { name: "Play" }).click();
// Assert that "Pause" button can be found // Assert that "Pause" button can be found
cy.findByRole("button", { name: "Pause" }).should("exist"); cy.findByRole("button", { name: "Pause" }).should("exist");
// Assert that the timer is reset when the audio file finished playing // Assert that the timer is reset when the audio file finished playing
cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist"); cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
// Assert that "Play" button can be found // Assert that "Play" button can be found
cy.findByRole("button", { name: "Play" }).should("exist").should("not.have.attr", "disabled"); cy.findByRole("button", { name: "Play" }).should("exist").should("not.have.attr", "disabled");
}); });
}) });
.realHover()
.findByRole("button", { name: "Reply" }) // Find and click "Reply" button
.click(); // Find and click "Reply" button //
// Calling cy.get(".mx_EventTile_last") again here is a workaround for
// https://github.com/matrix-org/matrix-js-sdk/issues/3394: the event tile may have been re-mounted while
// the audio was playing.
cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Reply" }).click();
cy.get(".mx_MessageComposer--compact").within(() => { cy.get(".mx_MessageComposer--compact").within(() => {
// Assert that the reply preview is rendered on the message composer // Assert that the reply preview is rendered on the message composer

View file

@ -225,9 +225,10 @@ describe("Composer", () => {
}); });
// ...inserts the username into the composer // ...inserts the username into the composer
cy.findByRole("textbox").within(() => { cy.findByRole("textbox").within(() => {
// TODO update this test when the mentions are inserted as pills, instead cy.findByText(otherUserName, { exact: false })
// of as text .should("exist")
cy.findByText(otherUserName, { exact: false }).should("exist"); .should("have.attr", "contenteditable", "false")
.should("have.attr", "data-mention-type", "user");
}); });
// Send the message to clear the composer // Send the message to clear the composer
@ -250,9 +251,10 @@ describe("Composer", () => {
// Selecting the autocomplete option using Enter inserts it into the composer // Selecting the autocomplete option using Enter inserts it into the composer
cy.findByRole("textbox").type(`{Enter}`); cy.findByRole("textbox").type(`{Enter}`);
cy.findByRole("textbox").within(() => { cy.findByRole("textbox").within(() => {
// TODO update this test when the mentions are inserted as pills, instead cy.findByText(otherUserName, { exact: false })
// of as text .should("exist")
cy.findByText(otherUserName, { exact: false }).should("exist"); .should("have.attr", "contenteditable", "false")
.should("have.attr", "data-mention-type", "user");
}); });
}); });
}); });

View file

@ -16,8 +16,9 @@ limitations under the License.
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { handleVerificationRequest, waitForVerificationRequest } from "./utils"; import { handleVerificationRequest, logIntoElement, waitForVerificationRequest } from "./utils";
import { CypressBot } from "../../support/bot"; import { CypressBot } from "../../support/bot";
import { skipIfRustCrypto } from "../../support/util";
describe("Complete security", () => { describe("Complete security", () => {
let homeserver: HomeserverInstance; let homeserver: HomeserverInstance;
@ -46,6 +47,8 @@ describe("Complete security", () => {
}); });
it("should walk through device verification if we have a signed device", () => { it("should walk through device verification if we have a signed device", () => {
skipIfRustCrypto();
// create a new user, and have it bootstrap cross-signing // create a new user, and have it bootstrap cross-signing
let botClient: CypressBot; let botClient: CypressBot;
cy.getBot(homeserver, { displayName: "Jeff" }) cy.getBot(homeserver, { displayName: "Jeff" })
@ -66,7 +69,6 @@ describe("Complete security", () => {
// accept the verification request on the "bot" side // accept the verification request on the "bot" side
cy.wrap(botVerificationRequestPromise).then(async (verificationRequest: VerificationRequest) => { cy.wrap(botVerificationRequestPromise).then(async (verificationRequest: VerificationRequest) => {
await verificationRequest.accept();
await handleVerificationRequest(verificationRequest); await handleVerificationRequest(verificationRequest);
}); });
@ -80,22 +82,3 @@ describe("Complete security", () => {
}); });
}); });
}); });
/**
* Fill in the login form in element with the given creds
*/
function logIntoElement(homeserverUrl: string, username: string, password: string) {
cy.visit("/#/login");
// select homeserver
cy.findByRole("button", { name: "Edit" }).click();
cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserverUrl);
cy.findByRole("button", { name: "Continue" }).click();
// wait for the dialog to go away
cy.get(".mx_ServerPickerDialog").should("not.exist");
cy.findByRole("textbox", { name: "Username" }).type(username);
cy.findByPlaceholderText("Password").type(password);
cy.findByRole("button", { name: "Sign in" }).click();
}

View file

@ -19,7 +19,14 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/
import type { CypressBot } from "../../support/bot"; import type { CypressBot } from "../../support/bot";
import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { UserCredentials } from "../../support/login"; import { UserCredentials } from "../../support/login";
import { EmojiMapping, handleVerificationRequest, waitForVerificationRequest } from "./utils"; import {
checkDeviceIsCrossSigned,
EmojiMapping,
handleVerificationRequest,
logIntoElement,
waitForVerificationRequest,
} from "./utils";
import { skipIfRustCrypto } from "../../support/util";
interface CryptoTestContext extends Mocha.Context { interface CryptoTestContext extends Mocha.Context {
homeserver: HomeserverInstance; homeserver: HomeserverInstance;
@ -103,6 +110,27 @@ function autoJoin(client: MatrixClient) {
}); });
} }
/**
* Given a VerificationRequest in a bot client, add cypress commands to:
* - wait for the bot to receive a 'verify by emoji' notification
* - check that the bot sees the same emoji as the application
*
* @param botVerificationRequest - a verification request in a bot client
*/
function doTwoWaySasVerification(botVerificationRequest: VerificationRequest): void {
// on the bot side, wait for the emojis, confirm they match, and return them
const emojiPromise = handleVerificationRequest(botVerificationRequest);
// then, check that our application shows an emoji panel with the same emojis.
cy.wrap(emojiPromise).then((emojis: EmojiMapping[]) => {
cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
emojis.forEach((emoji: EmojiMapping, index: number) => {
expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
});
});
});
}
const verify = function (this: CryptoTestContext) { const verify = function (this: CryptoTestContext) {
const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob); const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob);
@ -111,21 +139,9 @@ const verify = function (this: CryptoTestContext) {
cy.findByText("Bob").click(); cy.findByText("Bob").click();
cy.findByRole("button", { name: "Verify" }).click(); cy.findByRole("button", { name: "Verify" }).click();
cy.findByRole("button", { name: "Start Verification" }).click(); cy.findByRole("button", { name: "Start Verification" }).click();
cy.wrap(bobsVerificationRequestPromise)
.then((verificationRequest: VerificationRequest) => {
verificationRequest.accept();
return verificationRequest;
})
.as("bobsVerificationRequest");
cy.findByRole("button", { name: "Verify by emoji" }).click(); cy.findByRole("button", { name: "Verify by emoji" }).click();
cy.get<VerificationRequest>("@bobsVerificationRequest").then((request: VerificationRequest) => { cy.wrap(bobsVerificationRequestPromise).then((request: VerificationRequest) => {
return cy.wrap(handleVerificationRequest(request)).then((emojis: EmojiMapping[]) => { doTwoWaySasVerification(request);
cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
emojis.forEach((emoji: EmojiMapping, index: number) => {
expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
});
});
});
}); });
cy.findByRole("button", { name: "They match" }).click(); cy.findByRole("button", { name: "They match" }).click();
cy.findByText("You've successfully verified Bob!").should("exist"); cy.findByText("You've successfully verified Bob!").should("exist");
@ -143,7 +159,11 @@ describe("Cryptography", function () {
cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => { cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => {
aliceCredentials = credentials; aliceCredentials = credentials;
}); });
cy.getBot(homeserver, { displayName: "Bob", autoAcceptInvites: false, userIdPrefix: "bob_" }).as("bob"); cy.getBot(homeserver, {
displayName: "Bob",
autoAcceptInvites: false,
userIdPrefix: "bob_",
}).as("bob");
}); });
}); });
@ -152,6 +172,7 @@ describe("Cryptography", function () {
}); });
it("setting up secure key backup should work", () => { it("setting up secure key backup should work", () => {
skipIfRustCrypto();
cy.openUserSettings("Security & Privacy"); cy.openUserSettings("Security & Privacy");
cy.findByRole("button", { name: "Set up Secure Backup" }).click(); cy.findByRole("button", { name: "Set up Secure Backup" }).click();
cy.get(".mx_Dialog").within(() => { cy.get(".mx_Dialog").within(() => {
@ -175,6 +196,7 @@ describe("Cryptography", function () {
}); });
it("creating a DM should work, being e2e-encrypted / user verification", function (this: CryptoTestContext) { it("creating a DM should work, being e2e-encrypted / user verification", function (this: CryptoTestContext) {
skipIfRustCrypto();
cy.bootstrapCrossSigning(aliceCredentials); cy.bootstrapCrossSigning(aliceCredentials);
startDMWithBob.call(this); startDMWithBob.call(this);
// send first message // send first message
@ -196,6 +218,7 @@ describe("Cryptography", function () {
}); });
it("should allow verification when there is no existing DM", function (this: CryptoTestContext) { it("should allow verification when there is no existing DM", function (this: CryptoTestContext) {
skipIfRustCrypto();
cy.bootstrapCrossSigning(aliceCredentials); cy.bootstrapCrossSigning(aliceCredentials);
autoJoin(this.bob); autoJoin(this.bob);
@ -214,6 +237,7 @@ describe("Cryptography", function () {
}); });
it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) { it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) {
skipIfRustCrypto();
cy.bootstrapCrossSigning(aliceCredentials); cy.bootstrapCrossSigning(aliceCredentials);
// bob has a second, not cross-signed, device // bob has a second, not cross-signed, device
@ -300,3 +324,67 @@ describe("Cryptography", function () {
}); });
}); });
}); });
describe("Verify own device", () => {
let aliceBotClient: CypressBot;
let homeserver: HomeserverInstance;
beforeEach(() => {
cy.startHomeserver("default").then((data: HomeserverInstance) => {
homeserver = data;
// Visit the login page of the app, to load the matrix sdk
cy.visit("/#/login");
// wait for the page to load
cy.window({ log: false }).should("have.property", "matrixcs");
// Create a new device for alice
cy.getBot(homeserver, { bootstrapCrossSigning: true }).then((bot) => {
aliceBotClient = bot;
});
});
});
afterEach(() => {
cy.stopHomeserver(homeserver);
});
/* Click the "Verify with another device" button, and have the bot client auto-accept it.
*
* Stores the incoming `VerificationRequest` on the bot client as `@verificationRequest`.
*/
function initiateAliceVerificationRequest() {
// alice bot waits for verification request
const promiseVerificationRequest = waitForVerificationRequest(aliceBotClient);
// Click on "Verify with another device"
cy.get(".mx_AuthPage").within(() => {
cy.findByRole("button", { name: "Verify with another device" }).click();
});
// alice bot responds yes to verification request from alice
cy.wrap(promiseVerificationRequest).as("verificationRequest");
}
it("with SAS", function (this: CryptoTestContext) {
logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password);
// Launch the verification request between alice and the bot
initiateAliceVerificationRequest();
// Handle emoji SAS verification
cy.get(".mx_InfoDialog").within(() => {
cy.get<VerificationRequest>("@verificationRequest").then((request: VerificationRequest) => {
// Handle emoji request and check that emojis are matching
doTwoWaySasVerification(request);
});
cy.findByRole("button", { name: "They match" }).click();
cy.findByRole("button", { name: "Got it" }).click();
});
// Check that our device is now cross-signed
checkDeviceIsCrossSigned();
});
});

View file

@ -15,11 +15,11 @@ limitations under the License.
*/ */
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
import type { MatrixClient } from "matrix-js-sdk/src/matrix"; import type { MatrixClient } from "matrix-js-sdk/src/matrix";
import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { UserCredentials } from "../../support/login"; import { UserCredentials } from "../../support/login";
import Chainable = Cypress.Chainable; import { handleVerificationRequest } from "./utils";
import { skipIfRustCrypto } from "../../support/util";
const ROOM_NAME = "Test room"; const ROOM_NAME = "Test room";
const TEST_USER = "Alia"; const TEST_USER = "Alia";
@ -39,24 +39,6 @@ const waitForVerificationRequest = (cli: MatrixClient): Promise<VerificationRequ
}); });
}; };
const handleVerificationRequest = (request: VerificationRequest): Chainable<EmojiMapping[]> => {
return cy.wrap(
new Promise<EmojiMapping[]>((resolve) => {
const onShowSas = (event: ISasEvent) => {
verifier.off("show_sas", onShowSas);
event.confirm();
resolve(event.sas.emoji);
};
const verifier = request.beginKeyVerification("m.sas.v1");
verifier.on("show_sas", onShowSas);
verifier.verify();
}),
// extra timeout, as this sometimes takes a while
{ timeout: 30_000 },
);
};
const checkTimelineNarrow = (button = true) => { const checkTimelineNarrow = (button = true) => {
cy.viewport(800, 600); // SVGA cy.viewport(800, 600); // SVGA
cy.get(".mx_LeftPanel_minimized").should("exist"); // Wait until the left panel is minimized cy.get(".mx_LeftPanel_minimized").should("exist"); // Wait until the left panel is minimized
@ -86,6 +68,7 @@ describe("Decryption Failure Bar", () => {
let roomId: string; let roomId: string;
beforeEach(function () { beforeEach(function () {
skipIfRustCrypto();
cy.startHomeserver("default").then((hs: HomeserverInstance) => { cy.startHomeserver("default").then((hs: HomeserverInstance) => {
homeserver = hs; homeserver = hs;
cy.initTestUser(homeserver, TEST_USER) cy.initTestUser(homeserver, TEST_USER)
@ -161,7 +144,11 @@ describe("Decryption Failure Bar", () => {
); );
cy.wrap(verificationRequestPromise).then((verificationRequest: VerificationRequest) => { cy.wrap(verificationRequestPromise).then((verificationRequest: VerificationRequest) => {
cy.wrap(verificationRequest.accept()); cy.wrap(verificationRequest.accept());
handleVerificationRequest(verificationRequest).then((emojis) => { cy.wrap(
handleVerificationRequest(verificationRequest),
// extra timeout, as this sometimes takes a while
{ timeout: 30_000 },
).then((emojis: EmojiMapping[]) => {
cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => { cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
emojis.forEach((emoji: EmojiMapping, index: number) => { emojis.forEach((emoji: EmojiMapping, index: number) => {
expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]); expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);

View file

@ -21,15 +21,16 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/
export type EmojiMapping = [emoji: string, name: string]; export type EmojiMapping = [emoji: string, name: string];
/** /**
* wait for the given client to receive an incoming verification request * wait for the given client to receive an incoming verification request, and automatically accept it
* *
* @param cli - matrix client we expect to receive a request * @param cli - matrix client we expect to receive a request
*/ */
export function waitForVerificationRequest(cli: MatrixClient): Promise<VerificationRequest> { export function waitForVerificationRequest(cli: MatrixClient): Promise<VerificationRequest> {
return new Promise<VerificationRequest>((resolve) => { return new Promise<VerificationRequest>((resolve) => {
const onVerificationRequestEvent = (request: VerificationRequest) => { const onVerificationRequestEvent = async (request: VerificationRequest) => {
// @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here // @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here
cli.off("crypto.verification.request", onVerificationRequestEvent); cli.off("crypto.verification.request", onVerificationRequestEvent);
await request.accept();
resolve(request); resolve(request);
}; };
// @ts-ignore // @ts-ignore
@ -38,26 +39,83 @@ export function waitForVerificationRequest(cli: MatrixClient): Promise<Verificat
} }
/** /**
* Handle an incoming verification request * Automatically handle an incoming verification request
* *
* Starts the key verification process, and, once it is accepted on the other side, confirms that the * Starts the key verification process, and, once it is accepted on the other side, confirms that the
* emojis match. * emojis match.
* *
* Returns a promise that resolves, with the emoji list, once we confirm the emojis
*
* @param request - incoming verification request * @param request - incoming verification request
* @returns A promise that resolves, with the emoji list, once we confirm the emojis
*/ */
export function handleVerificationRequest(request: VerificationRequest) { export function handleVerificationRequest(request: VerificationRequest): Promise<EmojiMapping[]> {
return new Promise<EmojiMapping[]>((resolve) => { return new Promise<EmojiMapping[]>((resolve) => {
const onShowSas = (event: ISasEvent) => { const onShowSas = (event: ISasEvent) => {
// @ts-ignore VerifierEvent is a pain to get at here as we don't have a reference to matrixcs;
// using the string value here
verifier.off("show_sas", onShowSas); verifier.off("show_sas", onShowSas);
event.confirm(); event.confirm();
verifier.done();
resolve(event.sas.emoji); resolve(event.sas.emoji);
}; };
const verifier = request.beginKeyVerification("m.sas.v1"); const verifier = request.beginKeyVerification("m.sas.v1");
// @ts-ignore as above, avoiding reference to VerifierEvent
verifier.on("show_sas", onShowSas); verifier.on("show_sas", onShowSas);
verifier.verify(); verifier.verify();
}); });
} }
/**
* Check that the user has published cross-signing keys, and that the user's device has been cross-signed.
*/
export function checkDeviceIsCrossSigned(): void {
let userId: string;
let myDeviceId: string;
cy.window({ log: false })
.then((win) => {
// Get the userId and deviceId of the current user
const cli = win.mxMatrixClientPeg.get();
const accessToken = cli.getAccessToken()!;
const homeserverUrl = cli.getHomeserverUrl();
myDeviceId = cli.getDeviceId();
userId = cli.getUserId();
return cy.request({
method: "POST",
url: `${homeserverUrl}/_matrix/client/v3/keys/query`,
headers: { Authorization: `Bearer ${accessToken}` },
body: { device_keys: { [userId]: [] } },
});
})
.then((res) => {
// there should be three cross-signing keys
expect(res.body.master_keys[userId]).to.have.property("keys");
expect(res.body.self_signing_keys[userId]).to.have.property("keys");
expect(res.body.user_signing_keys[userId]).to.have.property("keys");
// and the device should be signed by the self-signing key
const selfSigningKeyId = Object.keys(res.body.self_signing_keys[userId].keys)[0];
expect(res.body.device_keys[userId][myDeviceId]).to.exist;
const myDeviceSignatures = res.body.device_keys[userId][myDeviceId].signatures[userId];
expect(myDeviceSignatures[selfSigningKeyId]).to.exist;
});
}
/**
* Fill in the login form in element with the given creds
*/
export function logIntoElement(homeserverUrl: string, username: string, password: string) {
cy.visit("/#/login");
// select homeserver
cy.findByRole("button", { name: "Edit" }).click();
cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserverUrl);
cy.findByRole("button", { name: "Continue" }).click();
// wait for the dialog to go away
cy.get(".mx_ServerPickerDialog").should("not.exist");
cy.findByRole("textbox", { name: "Username" }).type(username);
cy.findByPlaceholderText("Password").type(password);
cy.findByRole("button", { name: "Sign in" }).click();
}

View file

@ -164,6 +164,14 @@ describe("Invite dialog", function () {
// Assert that the invite dialog disappears // Assert that the invite dialog disappears
cy.get(".mx_InviteDialog_other").should("not.exist"); cy.get(".mx_InviteDialog_other").should("not.exist");
// Assert that the hovered user name on invitation UI does not have background color
// TODO: implement the test on room-header.spec.ts
cy.get(".mx_RoomHeader").within(() => {
cy.get(".mx_RoomHeader_name--textonly")
.realHover()
.should("have.css", "background-color", "rgba(0, 0, 0, 0)");
});
// Send a message to invite the bots // Send a message to invite the bots
cy.getComposer().type("Hello{enter}"); cy.getComposer().type("Hello{enter}");

View file

@ -21,10 +21,6 @@ import { HomeserverInstance } from "../../plugins/utils/homeserver";
describe("Login", () => { describe("Login", () => {
let homeserver: HomeserverInstance; let homeserver: HomeserverInstance;
beforeEach(() => {
cy.stubDefaultServer();
});
afterEach(() => { afterEach(() => {
cy.stopHomeserver(homeserver); cy.stopHomeserver(homeserver);
}); });
@ -44,17 +40,18 @@ describe("Login", () => {
it("logs in with an existing account and lands on the home screen", () => { it("logs in with an existing account and lands on the home screen", () => {
cy.injectAxe(); cy.injectAxe();
cy.findByRole("textbox", { name: "Username", timeout: 15000 }).should("be.visible"); // first pick the homeserver, as otherwise the user picker won't be visible
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
//cy.percySnapshot("Login");
cy.checkA11y();
cy.findByRole("button", { name: "Edit" }).click(); cy.findByRole("button", { name: "Edit" }).click();
cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserver.baseUrl); cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserver.baseUrl);
cy.findByRole("button", { name: "Continue" }).click(); cy.findByRole("button", { name: "Continue" }).click();
// wait for the dialog to go away // wait for the dialog to go away
cy.get(".mx_ServerPickerDialog").should("not.exist"); cy.get(".mx_ServerPickerDialog").should("not.exist");
cy.findByRole("textbox", { name: "Username", timeout: 15000 }).should("be.visible");
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
//cy.percySnapshot("Login");
cy.checkA11y();
cy.findByRole("textbox", { name: "Username" }).type(username); cy.findByRole("textbox", { name: "Username" }).type(username);
cy.findByPlaceholderText("Password").type(password); cy.findByPlaceholderText("Password").type(password);
cy.findByRole("button", { name: "Sign in" }).click(); cy.findByRole("button", { name: "Sign in" }).click();

View file

@ -126,13 +126,14 @@ describe("permalinks", () => {
getPill(danielle.getSafeUserId()); getPill(danielle.getSafeUserId());
}); });
// clean up before taking the snapshot // Exclude various components from the snapshot, for consistency
cy.get(".mx_cryptoEvent").invoke("remove"); const percyCSS =
cy.get(".mx_NewRoomIntro").invoke("remove"); ".mx_cryptoEvent, " +
cy.get(".mx_GenericEventListSummary").invoke("remove"); ".mx_NewRoomIntro, " +
".mx_MessageTimestamp, " +
".mx_RoomView_myReadMarker, " +
".mx_GenericEventListSummary { visibility: hidden !important; }";
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/25283 cy.get(".mx_RoomView_timeline").percySnapshotElement("Permalink rendering", { percyCSS });
//const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
//cy.get(".mx_RoomView_timeline").percySnapshotElement("Permalink rendering", { percyCSS });
}); });
}); });

View file

@ -17,12 +17,12 @@ limitations under the License.
/// <reference types="cypress" /> /// <reference types="cypress" />
import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { checkDeviceIsCrossSigned } from "../crypto/utils";
describe("Registration", () => { describe("Registration", () => {
let homeserver: HomeserverInstance; let homeserver: HomeserverInstance;
beforeEach(() => { beforeEach(() => {
cy.stubDefaultServer();
cy.visit("/#/register"); cy.visit("/#/register");
cy.startHomeserver("consent").then((data) => { cy.startHomeserver("consent").then((data) => {
homeserver = data; homeserver = data;
@ -89,39 +89,14 @@ describe("Registration", () => {
// check that the device considers itself verified // check that the device considers itself verified
cy.findByRole("button", { name: "User menu" }).click(); cy.findByRole("button", { name: "User menu" }).click();
cy.findByRole("menuitem", { name: "Security & Privacy" }).click(); cy.findByRole("menuitem", { name: "All settings" }).click();
cy.get(".mx_DevicesPanel_myDevice .mx_DevicesPanel_deviceTrust .mx_E2EIcon").should( cy.findByRole("tab", { name: "Sessions" }).click();
"have.class", cy.findByTestId("current-session-section").within(() => {
"mx_E2EIcon_verified", cy.findByTestId("device-metadata-isVerified").should("have.text", "Verified");
); });
// check that cross-signing keys have been uploaded. // check that cross-signing keys have been uploaded.
const myUserId = "@alice:localhost"; checkDeviceIsCrossSigned();
let myDeviceId: string;
cy.window({ log: false })
.then((win) => {
const cli = win.mxMatrixClientPeg.get();
const accessToken = cli.getAccessToken()!;
myDeviceId = cli.getDeviceId();
return cy.request({
method: "POST",
url: `${homeserver.baseUrl}/_matrix/client/v3/keys/query`,
headers: { Authorization: `Bearer ${accessToken}` },
body: { device_keys: { [myUserId]: [] } },
});
})
.then((res) => {
// there should be three cross-signing keys
expect(res.body.master_keys[myUserId]).to.have.property("keys");
expect(res.body.self_signing_keys[myUserId]).to.have.property("keys");
expect(res.body.user_signing_keys[myUserId]).to.have.property("keys");
// and the device should be signed by the self-signing key
const selfSigningKeyId = Object.keys(res.body.self_signing_keys[myUserId].keys)[0];
expect(res.body.device_keys[myUserId][myDeviceId]).to.exist;
const myDeviceSignatures = res.body.device_keys[myUserId][myDeviceId].signatures[myUserId];
expect(myDeviceSignatures[selfSigningKeyId]).to.exist;
});
}); });
it("should require username to fulfil requirements and be available", () => { it("should require username to fulfil requirements and be available", () => {

View file

@ -16,6 +16,8 @@ limitations under the License.
/// <reference types="cypress" /> /// <reference types="cypress" />
import { IWidget } from "matrix-widget-api";
import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { SettingLevel } from "../../../src/settings/SettingLevel"; import { SettingLevel } from "../../../src/settings/SettingLevel";
@ -94,14 +96,8 @@ describe("Room Header", () => {
// Assert the size of buttons on RoomHeader are specified and the buttons are not compressed // Assert the size of buttons on RoomHeader are specified and the buttons are not compressed
// Note these assertions do not check the size of mx_RoomHeader_name button // Note these assertions do not check the size of mx_RoomHeader_name button
// TODO: merge the assertions by using the same class name
cy.get(".mx_RoomHeader_button") cy.get(".mx_RoomHeader_button")
.should("have.length", 3) .should("have.length", 6)
.should("be.visible")
.should("have.css", "height", "32px")
.should("have.css", "width", "32px");
cy.get(".mx_RightPanel_headerButton")
.should("have.length", 3)
.should("be.visible") .should("be.visible")
.should("have.css", "height", "32px") .should("have.css", "height", "32px")
.should("have.css", "width", "32px"); .should("have.css", "width", "32px");
@ -196,4 +192,101 @@ describe("Room Header", () => {
}); });
}); });
}); });
describe("with a widget", () => {
const ROOM_NAME = "Test Room with a widget";
const WIDGET_ID = "fake-widget";
const WIDGET_HTML = `
<html lang="en">
<head>
<title>Fake Widget</title>
</head>
<body>
Hello World
</body>
</html>
`;
let widgetUrl: string;
let roomId: string;
beforeEach(() => {
cy.serveHtmlFile(WIDGET_HTML).then((url) => {
widgetUrl = url;
});
cy.createRoom({ name: ROOM_NAME }).then((id) => {
roomId = id;
// setup widget via state event
cy.getClient()
.then(async (matrixClient) => {
const content: IWidget = {
id: WIDGET_ID,
creatorUserId: "somebody",
type: "widget",
name: "widget",
url: widgetUrl,
};
await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, WIDGET_ID);
})
.as("widgetEventSent");
// set initial layout
cy.getClient()
.then(async (matrixClient) => {
const content = {
widgets: {
[WIDGET_ID]: {
container: "top",
index: 1,
width: 100,
height: 0,
},
},
};
await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, "");
})
.as("layoutEventSent");
});
cy.all([cy.get<string>("@widgetEventSent"), cy.get<string>("@layoutEventSent")]).then(() => {
// open the room
cy.viewRoomByName(ROOM_NAME);
});
});
it("should highlight the apps button", () => {
// Assert that AppsDrawer is rendered
cy.get(".mx_AppsDrawer").should("exist");
cy.get(".mx_RoomHeader").within(() => {
// Assert that "Hide Widgets" button is rendered and aria-checked is set to true
cy.findByRole("button", { name: "Hide Widgets" })
.should("exist")
.should("have.attr", "aria-checked", "true");
});
cy.get(".mx_RoomHeader").percySnapshotElement("Room header - with apps button (highlighted)");
});
it("should support hiding a widget", () => {
cy.get(".mx_AppsDrawer").should("exist");
cy.get(".mx_RoomHeader").within(() => {
// Click the apps button to hide AppsDrawer
cy.findByRole("button", { name: "Hide Widgets" }).should("exist").click();
// Assert that "Show widgets" button is rendered and aria-checked is set to false
cy.findByRole("button", { name: "Show Widgets" })
.should("exist")
.should("have.attr", "aria-checked", "false");
});
// Assert that AppsDrawer is not rendered
cy.get(".mx_AppsDrawer").should("not.exist");
cy.get(".mx_RoomHeader").percySnapshotElement("Room header - with apps button (not highlighted)");
});
});
}); });

View file

@ -36,12 +36,11 @@ describe("Appearance user settings tab", () => {
it("should be rendered properly", () => { it("should be rendered properly", () => {
cy.openUserSettings("Appearance"); cy.openUserSettings("Appearance");
cy.get(".mx_SettingsTab.mx_AppearanceUserSettingsTab").within(() => { cy.findByTestId("mx_AppearanceUserSettingsTab").within(() => {
// Assert that the top heading is rendered cy.get("h2").should("have.text", "Customise your appearance").should("be.visible");
cy.findByTestId("appearance").should("have.text", "Customise your appearance").should("be.visible");
}); });
cy.get(".mx_SettingsTab.mx_AppearanceUserSettingsTab").percySnapshotElement( cy.findByTestId("mx_AppearanceUserSettingsTab").percySnapshotElement(
"User settings tab - Appearance (advanced options collapsed)", "User settings tab - Appearance (advanced options collapsed)",
{ {
// Emulate TabbedView's actual min and max widths // Emulate TabbedView's actual min and max widths
@ -57,7 +56,7 @@ describe("Appearance user settings tab", () => {
// Assert that "Hide advanced" link button is rendered // Assert that "Hide advanced" link button is rendered
cy.findByRole("button", { name: "Hide advanced" }).should("exist"); cy.findByRole("button", { name: "Hide advanced" }).should("exist");
cy.get(".mx_SettingsTab.mx_AppearanceUserSettingsTab").percySnapshotElement( cy.findByTestId("mx_AppearanceUserSettingsTab").percySnapshotElement(
"User settings tab - Appearance (advanced options expanded)", "User settings tab - Appearance (advanced options expanded)",
{ {
// Emulate TabbedView's actual min and max widths // Emulate TabbedView's actual min and max widths
@ -74,7 +73,7 @@ describe("Appearance user settings tab", () => {
cy.openUserSettings("Appearance"); cy.openUserSettings("Appearance");
cy.get(".mx_AppearanceUserSettingsTab .mx_LayoutSwitcher_RadioButtons").within(() => { cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
// Assert that the layout selected by default is "Modern" // Assert that the layout selected by default is "Modern"
cy.get(".mx_LayoutSwitcher_RadioButton_selected .mx_StyledRadioButton_enabled").within(() => { cy.get(".mx_LayoutSwitcher_RadioButton_selected .mx_StyledRadioButton_enabled").within(() => {
cy.findByLabelText("Modern").should("exist"); cy.findByLabelText("Modern").should("exist");
@ -84,7 +83,7 @@ describe("Appearance user settings tab", () => {
// Assert that the room layout is set to group (modern) layout // Assert that the room layout is set to group (modern) layout
cy.get(".mx_RoomView_body[data-layout='group']").should("exist"); cy.get(".mx_RoomView_body[data-layout='group']").should("exist");
cy.get(".mx_AppearanceUserSettingsTab .mx_LayoutSwitcher_RadioButtons").within(() => { cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
// Select the first layout // Select the first layout
cy.get(".mx_LayoutSwitcher_RadioButton").first().click(); cy.get(".mx_LayoutSwitcher_RadioButton").first().click();
@ -97,7 +96,7 @@ describe("Appearance user settings tab", () => {
// Assert that the room layout is set to IRC layout // Assert that the room layout is set to IRC layout
cy.get(".mx_RoomView_body[data-layout='irc']").should("exist"); cy.get(".mx_RoomView_body[data-layout='irc']").should("exist");
cy.get(".mx_AppearanceUserSettingsTab .mx_LayoutSwitcher_RadioButtons").within(() => { cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
// Select the last layout // Select the last layout
cy.get(".mx_LayoutSwitcher_RadioButton").last().click(); cy.get(".mx_LayoutSwitcher_RadioButton").last().click();
@ -114,7 +113,7 @@ describe("Appearance user settings tab", () => {
it("should support changing font size by clicking the font slider", () => { it("should support changing font size by clicking the font slider", () => {
cy.openUserSettings("Appearance"); cy.openUserSettings("Appearance");
cy.get(".mx_SettingsTab.mx_AppearanceUserSettingsTab").within(() => { cy.findByTestId("mx_AppearanceUserSettingsTab").within(() => {
cy.get(".mx_FontScalingPanel_fontSlider").within(() => { cy.get(".mx_FontScalingPanel_fontSlider").within(() => {
cy.findByLabelText("Font size").should("exist"); cy.findByLabelText("Font size").should("exist");
}); });
@ -150,7 +149,7 @@ describe("Appearance user settings tab", () => {
it("should disable font size slider when custom font size is used", () => { it("should disable font size slider when custom font size is used", () => {
cy.openUserSettings("Appearance"); cy.openUserSettings("Appearance");
cy.get(".mx_FontScalingPanel").within(() => { cy.findByTestId("mx_FontScalingPanel").within(() => {
cy.findByLabelText("Use custom size").click({ force: true }); // force click as checkbox size is zero cy.findByLabelText("Use custom size").click({ force: true }); // force click as checkbox size is zero
// Assert that the font slider is disabled // Assert that the font slider is disabled
@ -167,10 +166,8 @@ describe("Appearance user settings tab", () => {
// Click "Show advanced" link button // Click "Show advanced" link button
cy.findByRole("button", { name: "Show advanced" }).click(); cy.findByRole("button", { name: "Show advanced" }).click();
cy.get(".mx_AppearanceUserSettingsTab_Advanced").within(() => { // force click as checkbox size is zero
// force click as checkbox size is zero cy.findByLabelText("Use a more compact 'Modern' layout").click({ force: true });
cy.findByLabelText("Use a more compact 'Modern' layout").click({ force: true });
});
// Assert that the room layout is set to compact group (modern) layout // Assert that the room layout is set to compact group (modern) layout
cy.get("#matrixchat .mx_MatrixChat_wrapper.mx_MatrixChat_useCompactLayout").should("exist"); cy.get("#matrixchat .mx_MatrixChat_wrapper.mx_MatrixChat_useCompactLayout").should("exist");
@ -178,13 +175,7 @@ describe("Appearance user settings tab", () => {
it("should disable compact group (modern) layout option on IRC layout and bubble layout", () => { it("should disable compact group (modern) layout option on IRC layout and bubble layout", () => {
const checkDisabled = () => { const checkDisabled = () => {
cy.get(".mx_AppearanceUserSettingsTab_Advanced").within(() => { cy.findByLabelText("Use a more compact 'Modern' layout").should("be.disabled");
cy.get(".mx_Checkbox")
.first()
.within(() => {
cy.get("input[type='checkbox'][disabled]").should("exist");
});
});
}; };
cy.openUserSettings("Appearance"); cy.openUserSettings("Appearance");
@ -193,7 +184,7 @@ describe("Appearance user settings tab", () => {
cy.findByRole("button", { name: "Show advanced" }).click(); cy.findByRole("button", { name: "Show advanced" }).click();
// Enable IRC layout // Enable IRC layout
cy.get(".mx_AppearanceUserSettingsTab .mx_LayoutSwitcher_RadioButtons").within(() => { cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
// Select the first layout // Select the first layout
cy.get(".mx_LayoutSwitcher_RadioButton").first().click(); cy.get(".mx_LayoutSwitcher_RadioButton").first().click();
@ -206,7 +197,7 @@ describe("Appearance user settings tab", () => {
checkDisabled(); checkDisabled();
// Enable bubble layout // Enable bubble layout
cy.get(".mx_AppearanceUserSettingsTab .mx_LayoutSwitcher_RadioButtons").within(() => { cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
// Select the first layout // Select the first layout
cy.get(".mx_LayoutSwitcher_RadioButton").last().click(); cy.get(".mx_LayoutSwitcher_RadioButton").last().click();
@ -225,10 +216,8 @@ describe("Appearance user settings tab", () => {
// Click "Show advanced" link button // Click "Show advanced" link button
cy.findByRole("button", { name: "Show advanced" }).click(); cy.findByRole("button", { name: "Show advanced" }).click();
cy.get(".mx_AppearanceUserSettingsTab_Advanced").within(() => { // force click as checkbox size is zero
// force click as checkbox size is zero cy.findByLabelText("Use a system font").click({ force: true });
cy.findByLabelText("Use a system font").click({ force: true });
});
// Assert that the font-family value was removed // Assert that the font-family value was removed
cy.get("body").should("have.css", "font-family", '""'); cy.get("body").should("have.css", "font-family", '""');
@ -242,7 +231,7 @@ describe("Appearance user settings tab", () => {
it("should be rendered with the light theme selected", () => { it("should be rendered with the light theme selected", () => {
cy.openUserSettings("Appearance") cy.openUserSettings("Appearance")
.get(".mx_ThemeChoicePanel") .findByTestId("mx_ThemeChoicePanel")
.within(() => { .within(() => {
cy.findByTestId("checkbox-use-system-theme").within(() => { cy.findByTestId("checkbox-use-system-theme").within(() => {
cy.findByText("Match system theme").should("be.visible"); cy.findByText("Match system theme").should("be.visible");
@ -252,7 +241,7 @@ describe("Appearance user settings tab", () => {
cy.get(".mx_Checkbox_checkmark").should("not.be.visible"); cy.get(".mx_Checkbox_checkmark").should("not.be.visible");
}); });
cy.get(".mx_ThemeSelectors").within(() => { cy.findByTestId("theme-choice-panel-selectors").within(() => {
cy.get(".mx_ThemeSelector_light").should("exist"); cy.get(".mx_ThemeSelector_light").should("exist");
cy.get(".mx_ThemeSelector_dark").should("exist"); cy.get(".mx_ThemeSelector_dark").should("exist");
@ -274,11 +263,11 @@ describe("Appearance user settings tab", () => {
"the system theme is clicked", "the system theme is clicked",
() => { () => {
cy.openUserSettings("Appearance") cy.openUserSettings("Appearance")
.get(".mx_ThemeChoicePanel") .findByTestId("mx_ThemeChoicePanel")
.findByLabelText("Match system theme") .findByLabelText("Match system theme")
.click({ force: true }); // force click because the size of the checkbox is zero .click({ force: true }); // force click because the size of the checkbox is zero
cy.get(".mx_ThemeChoicePanel").within(() => { cy.findByTestId("mx_ThemeChoicePanel").within(() => {
// Assert that the labels for the light theme and dark theme are disabled // Assert that the labels for the light theme and dark theme are disabled
cy.get(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled").should("exist"); cy.get(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled").should("exist");
cy.get(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled").should("exist"); cy.get(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled").should("exist");
@ -321,7 +310,7 @@ describe("Appearance user settings tab", () => {
}); });
cy.openUserSettings("Appearance") cy.openUserSettings("Appearance")
.get(".mx_ThemeChoicePanel") .findByTestId("mx_ThemeChoicePanel")
.findByLabelText("Use high contrast") .findByLabelText("Use high contrast")
.click({ force: true }); // force click because the size of the checkbox is zero .click({ force: true }); // force click because the size of the checkbox is zero

View file

@ -24,7 +24,6 @@ describe("Device manager", () => {
let user: UserCredentials | undefined; let user: UserCredentials | undefined;
beforeEach(() => { beforeEach(() => {
cy.enableLabsFeature("feature_new_device_manager");
cy.startHomeserver("default").then((data) => { cy.startHomeserver("default").then((data) => {
homeserver = data; homeserver = data;

View file

@ -53,7 +53,7 @@ describe("General user settings tab", () => {
cy.findByTestId("mx_GeneralUserSettingsTab").within(() => { cy.findByTestId("mx_GeneralUserSettingsTab").within(() => {
// Assert that the top heading is rendered // Assert that the top heading is rendered
cy.findByTestId("general").should("have.text", "General").should("be.visible"); cy.findByText("General").should("be.visible");
cy.get(".mx_ProfileSettings_profile") cy.get(".mx_ProfileSettings_profile")
.scrollIntoView() .scrollIntoView()
@ -83,10 +83,14 @@ describe("General user settings tab", () => {
}); });
// Wait until spinners disappear // Wait until spinners disappear
cy.get(".mx_GeneralUserSettingsTab_section--account .mx_Spinner").should("not.exist"); cy.findByTestId("accountSection").within(() => {
cy.get(".mx_GeneralUserSettingsTab_section--discovery .mx_Spinner").should("not.exist"); cy.get(".mx_Spinner").should("not.exist");
});
cy.findByTestId("discoverySection").within(() => {
cy.get(".mx_Spinner").should("not.exist");
});
cy.get(".mx_GeneralUserSettingsTab_section--account").within(() => { cy.findByTestId("accountSection").within(() => {
// Assert that input areas for changing a password exists // Assert that input areas for changing a password exists
cy.get("form.mx_GeneralUserSettingsTab_section--account_changePassword") cy.get("form.mx_GeneralUserSettingsTab_section--account_changePassword")
.scrollIntoView() .scrollIntoView()
@ -95,29 +99,28 @@ describe("General user settings tab", () => {
cy.findByLabelText("New Password").should("be.visible"); cy.findByLabelText("New Password").should("be.visible");
cy.findByLabelText("Confirm password").should("be.visible"); cy.findByLabelText("Confirm password").should("be.visible");
}); });
// Check email addresses area
cy.get(".mx_EmailAddresses")
.scrollIntoView()
.within(() => {
// Assert that an input area for a new email address is rendered
cy.findByRole("textbox", { name: "Email Address" }).should("be.visible");
// Assert the add button is visible
cy.findByRole("button", { name: "Add" }).should("be.visible");
});
// Check phone numbers area
cy.get(".mx_PhoneNumbers")
.scrollIntoView()
.within(() => {
// Assert that an input area for a new phone number is rendered
cy.findByRole("textbox", { name: "Phone Number" }).should("be.visible");
// Assert that the add button is rendered
cy.findByRole("button", { name: "Add" }).should("be.visible");
});
}); });
// Check email addresses area
cy.findByTestId("mx_AccountEmailAddresses")
.scrollIntoView()
.within(() => {
// Assert that an input area for a new email address is rendered
cy.findByRole("textbox", { name: "Email Address" }).should("be.visible");
// Assert the add button is visible
cy.findByRole("button", { name: "Add" }).should("be.visible");
});
// Check phone numbers area
cy.findByTestId("mx_AccountPhoneNumbers")
.scrollIntoView()
.within(() => {
// Assert that an input area for a new phone number is rendered
cy.findByRole("textbox", { name: "Phone Number" }).should("be.visible");
// Assert that the add button is rendered
cy.findByRole("button", { name: "Add" }).should("be.visible");
});
// Check language and region setting dropdown // Check language and region setting dropdown
cy.get(".mx_GeneralUserSettingsTab_section_languageInput") cy.get(".mx_GeneralUserSettingsTab_section_languageInput")
@ -188,7 +191,7 @@ describe("General user settings tab", () => {
it("should set a country calling code based on default_country_code", () => { it("should set a country calling code based on default_country_code", () => {
// Check phone numbers area // Check phone numbers area
cy.get(".mx_PhoneNumbers") cy.findByTestId("mx_AccountPhoneNumbers")
.scrollIntoView() .scrollIntoView()
.within(() => { .within(() => {
// Assert that an input area for a new phone number is rendered // Assert that an input area for a new phone number is rendered

View file

@ -0,0 +1,72 @@
/*
Copyright 2023 Suguru Hirahara
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import { HomeserverInstance } from "../../plugins/utils/homeserver";
describe("Security user settings tab", () => {
let homeserver: HomeserverInstance;
afterEach(() => {
cy.stopHomeserver(homeserver);
});
describe("with posthog enabled", () => {
beforeEach(() => {
// Enable posthog
cy.intercept("/config.json?cachebuster=*", (req) => {
req.continue((res) => {
res.send(200, {
...res.body,
posthog: {
project_api_key: "foo",
api_host: "bar",
},
privacy_policy_url: "example.tld", // Set privacy policy URL to enable privacyPolicyLink
});
});
});
cy.startHomeserver("default").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, "Hanako");
});
// Hide "Notification" toast on Cypress Cloud
cy.contains(".mx_Toast_toast h2", "Notifications")
.should("exist")
.closest(".mx_Toast_toast")
.within(() => {
cy.findByRole("button", { name: "Dismiss" }).click();
});
cy.get(".mx_Toast_buttons").within(() => {
cy.findByRole("button", { name: "Yes" }).should("exist").click(); // Allow analytics
});
cy.openUserSettings("Security");
});
describe("AnalyticsLearnMoreDialog", () => {
it("should be rendered properly", () => {
cy.findByRole("button", { name: "Learn more" }).click();
cy.get(".mx_AnalyticsLearnMoreDialog_wrapper").percySnapshotElement("AnalyticsLearnMoreDialog");
});
});
});
});

View file

@ -140,6 +140,8 @@ describe("Spaces", () => {
cy.findByPlaceholderText("Support").type("Projects"); cy.findByPlaceholderText("Support").type("Projects");
cy.findByRole("button", { name: "Continue" }).click(); cy.findByRole("button", { name: "Continue" }).click();
cy.get(".mx_SpaceRoomView").percySnapshotElement("Space - 'Invite your teammates' dialog");
cy.get(".mx_SpaceRoomView").within(() => { cy.get(".mx_SpaceRoomView").within(() => {
cy.get("h1").findByText("Invite your teammates"); cy.get("h1").findByText("Invite your teammates");
cy.findByRole("button", { name: "Skip for now" }).click(); cy.findByRole("button", { name: "Skip for now" }).click();

View file

@ -203,6 +203,10 @@ describe("Spotlight", () => {
cy.get(".mx_RoomSublist_skeletonUI").should("not.exist"); cy.get(".mx_RoomSublist_skeletonUI").should("not.exist");
}); });
}); });
// wait for the room to have the right name
cy.get(".mx_RoomHeader").within(() => {
cy.findByText(room1Name);
});
}); });
afterEach(() => { afterEach(() => {
@ -212,8 +216,12 @@ describe("Spotlight", () => {
it("should be able to add and remove filters via keyboard", () => { it("should be able to add and remove filters via keyboard", () => {
cy.openSpotlightDialog().within(() => { cy.openSpotlightDialog().within(() => {
cy.spotlightSearch().type("{downArrow}"); cy.wait(1000); // wait for the dialog to settle, otherwise our keypresses might race with an update
// initially, publicrooms should be highlighted (because there are no other suggestions)
cy.get("#mx_SpotlightDialog_button_explorePublicRooms").should("have.attr", "aria-selected", "true"); cy.get("#mx_SpotlightDialog_button_explorePublicRooms").should("have.attr", "aria-selected", "true");
// hitting enter should enable the publicrooms filter
cy.spotlightSearch().type("{enter}"); cy.spotlightSearch().type("{enter}");
cy.get(".mx_SpotlightDialog_filter").should("contain", "Public rooms"); cy.get(".mx_SpotlightDialog_filter").should("contain", "Public rooms");
cy.spotlightSearch().type("{backspace}"); cy.spotlightSearch().type("{backspace}");
@ -233,7 +241,6 @@ describe("Spotlight", () => {
cy.openSpotlightDialog() cy.openSpotlightDialog()
.within(() => { .within(() => {
cy.spotlightSearch().clear().type(room1Name); cy.spotlightSearch().clear().type(room1Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room1Name); cy.spotlightResults().eq(0).should("contain", room1Name);
cy.spotlightResults().eq(0).click(); cy.spotlightResults().eq(0).click();
@ -249,7 +256,6 @@ describe("Spotlight", () => {
.within(() => { .within(() => {
cy.spotlightFilter(Filter.PublicRooms); cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room1Name); cy.spotlightSearch().clear().type(room1Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room1Name); cy.spotlightResults().eq(0).should("contain", room1Name);
cy.spotlightResults().eq(0).should("contain", "View"); cy.spotlightResults().eq(0).should("contain", "View");
@ -266,7 +272,6 @@ describe("Spotlight", () => {
.within(() => { .within(() => {
cy.spotlightFilter(Filter.PublicRooms); cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room2Name); cy.spotlightSearch().clear().type(room2Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room2Name); cy.spotlightResults().eq(0).should("contain", room2Name);
cy.spotlightResults().eq(0).should("contain", "Join"); cy.spotlightResults().eq(0).should("contain", "Join");
@ -284,7 +289,6 @@ describe("Spotlight", () => {
.within(() => { .within(() => {
cy.spotlightFilter(Filter.PublicRooms); cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room3Name); cy.spotlightSearch().clear().type(room3Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room3Name); cy.spotlightResults().eq(0).should("contain", room3Name);
cy.spotlightResults().eq(0).should("contain", "View"); cy.spotlightResults().eq(0).should("contain", "View");
@ -326,7 +330,6 @@ describe("Spotlight", () => {
.within(() => { .within(() => {
cy.spotlightFilter(Filter.People); cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot1Name); cy.spotlightSearch().clear().type(bot1Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot1Name); cy.spotlightResults().eq(0).should("contain", bot1Name);
cy.spotlightResults().eq(0).click(); cy.spotlightResults().eq(0).click();
@ -341,7 +344,6 @@ describe("Spotlight", () => {
.within(() => { .within(() => {
cy.spotlightFilter(Filter.People); cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot2Name); cy.spotlightSearch().clear().type(bot2Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot2Name); cy.spotlightResults().eq(0).should("contain", bot2Name);
cy.spotlightResults().eq(0).click(); cy.spotlightResults().eq(0).click();
@ -359,7 +361,6 @@ describe("Spotlight", () => {
cy.openSpotlightDialog().within(() => { cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.People); cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot2Name); cy.spotlightSearch().clear().type(bot2Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot2Name); cy.spotlightResults().eq(0).should("contain", bot2Name);
cy.spotlightResults().eq(0).click(); cy.spotlightResults().eq(0).click();

View file

@ -296,7 +296,7 @@ describe("Threads", () => {
}); });
cy.findByRole("button", { name: "Threads" }) cy.findByRole("button", { name: "Threads" })
.should("have.class", "mx_RightPanel_headerButton_unread") // User asserts thread list unread indicator .should("have.class", "mx_RoomHeader_button--unread") // User asserts thread list unread indicator
.click(); // User opens thread list .click(); // User opens thread list
// User asserts thread with correct root & latest events & unread dot // User asserts thread with correct root & latest events & unread dot

View file

@ -95,6 +95,10 @@ describe("Widget Layout", () => {
cy.stopWebServers(); cy.stopWebServers();
}); });
it("should be set properly", () => {
cy.get(".mx_AppsDrawer").percySnapshotElement("Widgets drawer on the timeline (AppsDrawer)");
});
it("manually resize the height of the top container layout", () => { it("manually resize the height of the top container layout", () => {
cy.get('iframe[title="widget"]').invoke("height").should("be.lessThan", 250); cy.get('iframe[title="widget"]').invoke("height").should("be.lessThan", 250);

View file

@ -1,48 +0,0 @@
{
"flows": [
{
"type": "m.login.sso",
"identity_providers": [
{
"id": "oidc-github",
"name": "GitHub",
"icon": "mxc://matrix.org/sVesTtrFDTpXRbYfpahuJsKP",
"brand": "github"
},
{
"id": "oidc-google",
"name": "Google",
"icon": "mxc://matrix.org/ZlnaaZNPxtUuQemvgQzlOlkz",
"brand": "google"
},
{
"id": "oidc-gitlab",
"name": "GitLab",
"icon": "mxc://matrix.org/MCVOEmFgVieKFshPxmnejWOq",
"brand": "gitlab"
},
{
"id": "oidc-facebook",
"name": "Facebook",
"icon": "mxc://matrix.org/nsyeLIgzxazZmJadflMAsAWG",
"brand": "facebook"
},
{
"id": "oidc-apple",
"name": "Apple",
"icon": "mxc://matrix.org/QQKNSOdLiMHtJhzeAObmkFiU",
"brand": "apple"
}
]
},
{
"type": "m.login.token"
},
{
"type": "m.login.password"
},
{
"type": "m.login.application_service"
}
]
}

View file

@ -1,8 +0,0 @@
{
"m.homeserver": {
"base_url": "https://matrix-client.matrix.org"
},
"m.identity_server": {
"base_url": "https://vector.im"
}
}

View file

@ -1 +0,0 @@
{}

View file

@ -59,6 +59,10 @@ Cypress.Commands.overwrite(
"color-contrast": { "color-contrast": {
enabled: false, enabled: false,
}, },
// link-in-text-block also complains due to known contrast issues
"link-in-text-block": {
enabled: false,
},
...options.rules, ...options.rules,
}, },
}, },

View file

@ -0,0 +1,51 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* Intercept requests to `config.json`, so that we can test against a known configuration.
*
* If we don't do this, we end up testing against the Element config for develop.element.io, which then means
* we make requests to the live `matrix.org`, which makes our tests dependent on matrix.org being up and responsive.
*/
import { isRustCryptoEnabled } from "./util";
const CONFIG_JSON = {
// This is deliberately quite a minimal config.json, so that we can test that the default settings
// actually work.
//
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
// We point that to a guaranteed-invalid domain.
default_server_config: {
"m.homeserver": {
base_url: "https://server.invalid",
},
},
// the location tests want a map style url.
map_style_url: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx",
};
beforeEach(() => {
const configJson = CONFIG_JSON;
// configure element to use rust crypto if the env var tells us so
if (isRustCryptoEnabled()) {
configJson["features"] = {
feature_rust_crypto: true,
};
}
cy.intercept({ method: "GET", pathname: "/config.json" }, { body: configJson });
});

View file

@ -20,6 +20,7 @@ import "@percy/cypress";
import "cypress-real-events"; import "cypress-real-events";
import "@testing-library/cypress/add-commands"; import "@testing-library/cypress/add-commands";
import "./config.json";
import "./homeserver"; import "./homeserver";
import "./login"; import "./login";
import "./labs"; import "./labs";

View file

@ -68,29 +68,5 @@ Cypress.Commands.add("goOnline", (): void => {
}); });
}); });
Cypress.Commands.add("stubDefaultServer", (): void => {
cy.log("Stubbing vector.im and matrix.org network calls");
// We intercept vector.im & matrix.org calls so that tests don't fail when it has issues
cy.intercept("GET", "https://vector.im/_matrix/identity/v2", {
fixture: "vector-im-identity-v2.json",
});
cy.intercept("GET", "https://matrix.org/.well-known/matrix/client", {
fixture: "matrix-org-client-well-known.json",
});
cy.intercept("GET", "https://matrix-client.matrix.org/_matrix/client/versions", {
fixture: "matrix-org-client-versions.json",
});
cy.intercept("GET", "https://matrix-client.matrix.org/_matrix/client/r0/login", {
fixture: "matrix-org-client-login.json",
});
cy.intercept("POST", "https://matrix-client.matrix.org/_matrix/client/r0/register?kind=guest", {
statusCode: 403,
body: {
errcode: "M_FORBIDDEN",
error: "Registration is not enabled on this homeserver.",
},
});
});
// Needed to make this file a module // Needed to make this file a module
export {}; export {};

View file

@ -56,5 +56,20 @@ cy.all = function all(commands): Cypress.Chainable {
return cy.wrap(resultArray, { log: false }); return cy.wrap(resultArray, { log: false });
}; };
// Needed to make this file a module /**
export {}; * Check if Cypress has been configured to enable rust crypto, and bail out if so.
*/
export function skipIfRustCrypto() {
if (isRustCryptoEnabled()) {
cy.log("Skipping due to rust crypto");
//@ts-ignore: 'state' is a secret internal command
cy.state("runnable").skip();
}
}
/**
* Determine if Cypress has been configured to enable rust crypto (by checking the environment variable)
*/
export function isRustCryptoEnabled(): boolean {
return !!Cypress.env("RUST_CRYPTO");
}

View file

@ -45,6 +45,16 @@ To launch it:
yarn run test:cypress:open yarn run test:cypress:open
``` ```
### Running with Rust cryptography
`matrix-js-sdk` is currently in the
[process](https://github.com/vector-im/element-web/issues/21972) of being
updated to replace its end-to-end encryption implementation to use the [Matrix
Rust SDK](https://github.com/matrix-org/matrix-rust-sdk). This is not currently
enabled by default, but it is possible to have Cypress configure Element to use
the Rust crypto implementation by setting the environment variable
`CYPRESS_RUST_CRYPTO=1`.
## How the Tests Work ## How the Tests Work
Everything Cypress-related lives in the `cypress/` subdirectory of react-sdk Everything Cypress-related lives in the `cypress/` subdirectory of react-sdk

View file

@ -68,7 +68,6 @@
"@testing-library/react-hooks": "^8.0.1", "@testing-library/react-hooks": "^8.0.1",
"await-lock": "^2.1.0", "await-lock": "^2.1.0",
"blurhash": "^1.1.3", "blurhash": "^1.1.3",
"cheerio": "^1.0.0-rc.9",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"commonmark": "^0.30.0", "commonmark": "^0.30.0",
"counterpart": "^0.18.6", "counterpart": "^0.18.6",
@ -83,6 +82,7 @@
"focus-visible": "^5.2.0", "focus-visible": "^5.2.0",
"gfm.css": "^1.1.2", "gfm.css": "^1.1.2",
"glob-to-regexp": "^0.4.1", "glob-to-regexp": "^0.4.1",
"grapheme-splitter": "^1.0.4",
"highlight.js": "^11.3.1", "highlight.js": "^11.3.1",
"html-entities": "^2.0.0", "html-entities": "^2.0.0",
"is-ip": "^3.1.0", "is-ip": "^3.1.0",
@ -96,16 +96,16 @@
"maplibre-gl": "^2.0.0", "maplibre-gl": "^2.0.0",
"matrix-encrypt-attachment": "^1.0.3", "matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1", "matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "25.2.0-rc.5", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.3.1", "matrix-widget-api": "^1.4.0",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"opus-recorder": "^8.0.3", "opus-recorder": "^8.0.3",
"pako": "^2.0.3", "pako": "^2.0.3",
"png-chunks-extract": "^1.0.0", "png-chunks-extract": "^1.0.0",
"posthog-js": "1.53.2", "posthog-js": "1.57.2",
"proposal-temporal": "^0.9.0", "proposal-temporal": "^0.9.0",
"qrcode": "1.5.1", "qrcode": "1.5.3",
"re-resizable": "^6.9.0", "re-resizable": "^6.9.0",
"react": "17.0.2", "react": "17.0.2",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
@ -118,7 +118,6 @@
"sanitize-html": "2.10.0", "sanitize-html": "2.10.0",
"tar-js": "^0.3.0", "tar-js": "^0.3.0",
"ua-parser-js": "^1.0.2", "ua-parser-js": "^1.0.2",
"url": "^0.11.0",
"what-input": "^5.2.10", "what-input": "^5.2.10",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },
@ -175,7 +174,7 @@
"@typescript-eslint/eslint-plugin": "^5.35.1", "@typescript-eslint/eslint-plugin": "^5.35.1",
"@typescript-eslint/parser": "^5.6.0", "@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.1.0", "allchange": "^1.1.0",
"axe-core": "4.7.0", "axe-core": "4.7.1",
"babel-jest": "^29.0.0", "babel-jest": "^29.0.0",
"blob-polyfill": "^7.0.0", "blob-polyfill": "^7.0.0",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
@ -183,7 +182,7 @@
"cypress-axe": "^1.0.0", "cypress-axe": "^1.0.0",
"cypress-multi-reporters": "^1.6.1", "cypress-multi-reporters": "^1.6.1",
"cypress-real-events": "^1.7.1", "cypress-real-events": "^1.7.1",
"eslint": "8.38.0", "eslint": "8.40.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-deprecate": "^0.7.0", "eslint-plugin-deprecate": "^0.7.0",
@ -193,7 +192,7 @@
"eslint-plugin-matrix-org": "1.1.0", "eslint-plugin-matrix-org": "1.1.0",
"eslint-plugin-react": "^7.28.0", "eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unicorn": "^46.0.0", "eslint-plugin-unicorn": "^47.0.0",
"fetch-mock-jest": "^1.5.1", "fetch-mock-jest": "^1.5.1",
"fs-extra": "^11.0.0", "fs-extra": "^11.0.0",
"jest": "29.3.1", "jest": "29.3.1",
@ -206,11 +205,11 @@
"mocha-junit-reporter": "^2.2.0", "mocha-junit-reporter": "^2.2.0",
"node-fetch": "2", "node-fetch": "2",
"postcss-scss": "^4.0.4", "postcss-scss": "^4.0.4",
"prettier": "2.8.7", "prettier": "2.8.8",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"rimraf": "^5.0.0", "rimraf": "^5.0.0",
"stylelint": "^15.0.0", "stylelint": "^15.0.0",
"stylelint-config-standard": "^32.0.0", "stylelint-config-standard": "^33.0.0",
"stylelint-scss": "^5.0.0", "stylelint-scss": "^5.0.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "5.0.4", "typescript": "5.0.4",

View file

@ -34,6 +34,10 @@ limitations under the License.
transition: opacity 300ms ease; transition: opacity 300ms ease;
} }
:root {
--hover-transition: 0.08s cubic-bezier(0.46, 0.03, 0.52, 0.96); /* quadratic */
}
@keyframes mx--anim-pulse { @keyframes mx--anim-pulse {
0% { 0% {
opacity: 1; opacity: 1;

View file

@ -23,10 +23,6 @@ limitations under the License.
@import "./_spacing.pcss"; @import "./_spacing.pcss";
@import url("maplibre-gl/dist/maplibre-gl.css"); @import url("maplibre-gl/dist/maplibre-gl.css");
$hover-transition: 0.08s cubic-bezier(0.46, 0.03, 0.52, 0.96); /* quadratic */
$selected-message-border-width: 4px;
:root { :root {
font-size: 10px; font-size: 10px;
@ -37,6 +33,22 @@ $selected-message-border-width: 4px;
--buttons-dialog-gap-row: $spacing-8; --buttons-dialog-gap-row: $spacing-8;
--buttons-dialog-gap-column: $spacing-8; --buttons-dialog-gap-column: $spacing-8;
--MBody-border-radius: 8px; --MBody-border-radius: 8px;
/* Expected z-indexes for dialogs:
4000 - Default wrapper index
4009 - Static dialog background
4010 - Static dialog itself
4011 - Standard dialog background
4012 - Standard dialog itself
These are set up such that the static dialog always appears
underneath the standard dialogs.
*/
--dialog-zIndex-wrapper-default: 4000;
--dialog-zIndex-static-background: 4009;
--dialog-zIndex-static: calc(var(--dialog-zIndex-static-background) + 1); /* 4010 */
--dialog-zIndex-standard-background: calc(var(--dialog-zIndex-static) + 1); /* 4011 */
--dialog-zIndex-standard: calc(var(--dialog-zIndex-standard-background) + 1); /* 4012 */
} }
@media only percy { @media only percy {
@ -281,20 +293,9 @@ legend {
color: $secondary-accent-color; color: $secondary-accent-color;
} }
/* Expected z-indexes for dialogs:
4000 - Default wrapper index
4009 - Static dialog background
4010 - Static dialog itself
4011 - Standard dialog background
4012 - Standard dialog itself
These are set up such that the static dialog always appears
underneath the standard dialogs.
*/
.mx_Dialog_wrapper { .mx_Dialog_wrapper {
position: fixed; position: fixed;
z-index: 4000; z-index: var(--dialog-zIndex-wrapper-default);
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
@ -308,7 +309,7 @@ legend {
.mx_Dialog { .mx_Dialog {
background-color: $background; background-color: $background;
color: $light-fg-color; color: $light-fg-color;
z-index: 4012; z-index: var(--dialog-zIndex-standard);
font-size: $font-15px; font-size: $font-15px;
position: relative; position: relative;
padding: 24px; padding: 24px;
@ -316,73 +317,89 @@ legend {
box-shadow: 2px 15px 30px 0 $dialog-shadow-color; box-shadow: 2px 15px 30px 0 $dialog-shadow-color;
border-radius: 8px; border-radius: 8px;
overflow-y: auto; overflow-y: auto;
}
/* Styles copied/inspired by GroupLayout, ReplyTile, and EventTile variants. */ .mx_Dialog_staticWrapper & {
.mx_Dialog .markdown-body { z-index: var(--dialog-zIndex-static);
font-family: inherit !important; contain: content;
white-space: normal !important;
line-height: inherit !important;
color: inherit; /* inherit the colour from the dark or light theme by default (but not for code blocks) */
font-size: $font-14px;
pre,
code {
font-family: $monospace-font-family !important;
background-color: $codeblock-background-color;
} }
/* this selector wrongly applies to code blocks too but we will unset it in the next one */ .mx_Dialog_lightbox & {
code { border-radius: 0px;
white-space: pre-wrap; /* don't collapse spaces in inline code blocks */ background-color: transparent;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
pointer-events: none;
padding: 0;
} }
pre code { /* Styles copied/inspired by GroupLayout, ReplyTile, and EventTile variants. */
white-space: pre; /* we want code blocks to be scrollable and not wrap */ .markdown-body {
font-family: inherit !important;
white-space: normal !important;
line-height: inherit !important;
color: inherit; /* inherit the colour from the dark or light theme by default (but not for code blocks) */
font-size: $font-14px;
> * { pre,
display: inline; code {
font-family: $monospace-font-family !important;
background-color: $codeblock-background-color;
}
/* this selector wrongly applies to code blocks too but we will unset it in the next one */
code {
white-space: pre-wrap; /* don't collapse spaces in inline code blocks */
}
pre {
/* have to use overlay rather than auto otherwise Linux and Windows */
/* Chrome gets very confused about vertical spacing: */
/* https://github.com/vector-im/vector-web/issues/754 */
overflow-x: overlay;
overflow-y: visible;
&::-webkit-scrollbar-corner {
background: transparent;
}
code {
white-space: pre; /* we want code blocks to be scrollable and not wrap */
> * {
display: inline;
}
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: inherit !important;
color: inherit;
}
/* Make h1 and h2 the same size as h3. */
h1,
h2 {
font-size: 1.5em;
border-bottom: none !important; /* override GFM */
}
a {
color: $accent-alt;
}
blockquote {
border-left: 2px solid $blockquote-bar-color;
border-radius: 2px;
padding: 0 10px;
} }
} }
pre {
/* have to use overlay rather than auto otherwise Linux and Windows */
/* Chrome gets very confused about vertical spacing: */
/* https://github.com/vector-im/vector-web/issues/754 */
overflow-x: overlay;
overflow-y: visible;
&::-webkit-scrollbar-corner {
background: transparent;
}
}
}
.mx_Dialog .markdown-body h1,
.mx_Dialog .markdown-body h2,
.mx_Dialog .markdown-body h3,
.mx_Dialog .markdown-body h4,
.mx_Dialog .markdown-body h5,
.mx_Dialog .markdown-body h6 {
font-family: inherit !important;
color: inherit;
}
/* Make h1 and h2 the same size as h3. */
.mx_Dialog .markdown-body h1,
.mx_Dialog .markdown-body h2 {
font-size: 1.5em;
border-bottom: none !important; /* override GFM */
}
.mx_Dialog .markdown-body a {
color: $accent-alt;
}
.mx_Dialog .markdown-body blockquote {
border-left: 2px solid $blockquote-bar-color;
border-radius: 2px;
padding: 0 10px;
} }
.mx_Dialog_fixedWidth { .mx_Dialog_fixedWidth {
@ -390,11 +407,6 @@ legend {
max-width: 704px; max-width: 704px;
} }
.mx_Dialog_staticWrapper .mx_Dialog {
z-index: 4010;
contain: content;
}
.mx_Dialog_background { .mx_Dialog_background {
position: fixed; position: fixed;
top: 0; top: 0;
@ -403,41 +415,24 @@ legend {
height: 100%; height: 100%;
background-color: $dialog-backdrop-color; background-color: $dialog-backdrop-color;
opacity: 0.8; opacity: 0.8;
z-index: 4011; z-index: var(--dialog-zIndex-standard-background);
}
.mx_Dialog_background.mx_Dialog_staticBackground { &.mx_Dialog_staticBackground {
z-index: 4009; z-index: var(--dialog-zIndex-static-background);
} }
.mx_Dialog_wrapperWithStaticUnder .mx_Dialog_background { .mx_Dialog_wrapperWithStaticUnder & {
/* Roughly half of what it would normally be - we don't want to black out */ /* Roughly half of what it would normally be - we don't want to black out */
/* the app, just make it clear that the dialogs are stacked. */ /* the app, just make it clear that the dialogs are stacked. */
opacity: 0.4; opacity: 0.4;
} }
.mx_Dialog_lightbox .mx_Dialog_background { .mx_Dialog_lightbox & {
opacity: $lightbox-background-bg-opacity; opacity: $lightbox-background-bg-opacity;
background-color: $lightbox-background-bg-color; background-color: $lightbox-background-bg-color;
animation-name: mx_Dialog_lightbox_background_keyframes; animation-name: mx_Dialog_lightbox_background_keyframes;
animation-duration: 300ms; animation-duration: 300ms;
} }
.mx_Dialog_lightbox .mx_Dialog {
border-radius: 0px;
background-color: transparent;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
pointer-events: none;
padding: 0;
}
.mx_Dialog_header {
position: relative;
padding: 3px 0;
margin-bottom: 10px;
} }
.mx_Dialog_titleImage { .mx_Dialog_titleImage {
@ -454,22 +449,29 @@ legend {
display: inline-block; display: inline-block;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
&.danger {
color: $alert;
}
} }
.mx_Dialog_header.mx_Dialog_headerWithButton > .mx_Dialog_title { .mx_Dialog_header {
text-align: center; position: relative;
} padding: 3px 0;
.mx_Dialog_header.mx_Dialog_headerWithCancel { margin-bottom: 10px;
padding-right: 20px; /* leave space for the 'X' cancel button */
}
.mx_Dialog_header.mx_Dialog_headerWithCancelOnly { &.mx_Dialog_headerWithButton > .mx_Dialog_title {
padding: 0 20px 0 0; text-align: center;
margin: 0; }
}
.mx_Dialog_title.danger { &.mx_Dialog_headerWithCancel {
color: $alert; padding-right: 20px; /* leave space for the 'X' cancel button */
}
&.mx_Dialog_headerWithCancelOnly {
padding: 0 20px 0 0;
margin: 0;
}
} }
@define-mixin customisedCancelButton { @define-mixin customisedCancelButton {
@ -509,21 +511,21 @@ legend {
/* The consumer is responsible for positioning their elements. */ /* The consumer is responsible for positioning their elements. */
float: left; float: left;
} }
}
.mx_Dialog_buttons_row { .mx_Dialog_buttons_row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end; justify-content: flex-end;
text-align: initial; text-align: initial;
margin-inline-start: auto; margin-inline-start: auto;
/* default gap among elements */ /* default gap among elements */
column-gap: var(--buttons-dialog-gap-column); column-gap: var(--buttons-dialog-gap-column);
row-gap: var(--buttons-dialog-gap-row); row-gap: var(--buttons-dialog-gap-row);
button { button {
margin: 0 !important; /* override the margin settings */ margin: 0 !important; /* override the margin settings */
}
} }
} }

View file

@ -124,7 +124,6 @@
@import "./views/dialogs/_BugReportDialog.pcss"; @import "./views/dialogs/_BugReportDialog.pcss";
@import "./views/dialogs/_BulkRedactDialog.pcss"; @import "./views/dialogs/_BulkRedactDialog.pcss";
@import "./views/dialogs/_ChangelogDialog.pcss"; @import "./views/dialogs/_ChangelogDialog.pcss";
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.pcss";
@import "./views/dialogs/_CompoundDialog.pcss"; @import "./views/dialogs/_CompoundDialog.pcss";
@import "./views/dialogs/_ConfirmSpaceUserActionDialog.pcss"; @import "./views/dialogs/_ConfirmSpaceUserActionDialog.pcss";
@import "./views/dialogs/_ConfirmUserActionDialog.pcss"; @import "./views/dialogs/_ConfirmUserActionDialog.pcss";
@ -179,7 +178,6 @@
@import "./views/elements/_Dropdown.pcss"; @import "./views/elements/_Dropdown.pcss";
@import "./views/elements/_EditableItemList.pcss"; @import "./views/elements/_EditableItemList.pcss";
@import "./views/elements/_ErrorBoundary.pcss"; @import "./views/elements/_ErrorBoundary.pcss";
@import "./views/elements/_EventTilePreview.pcss";
@import "./views/elements/_ExternalLink.pcss"; @import "./views/elements/_ExternalLink.pcss";
@import "./views/elements/_FacePile.pcss"; @import "./views/elements/_FacePile.pcss";
@import "./views/elements/_Field.pcss"; @import "./views/elements/_Field.pcss";
@ -198,7 +196,6 @@
@import "./views/elements/_ReplyChain.pcss"; @import "./views/elements/_ReplyChain.pcss";
@import "./views/elements/_ResizeHandle.pcss"; @import "./views/elements/_ResizeHandle.pcss";
@import "./views/elements/_RichText.pcss"; @import "./views/elements/_RichText.pcss";
@import "./views/elements/_RoleButton.pcss";
@import "./views/elements/_RoomAliasField.pcss"; @import "./views/elements/_RoomAliasField.pcss";
@import "./views/elements/_SSOButtons.pcss"; @import "./views/elements/_SSOButtons.pcss";
@import "./views/elements/_SearchWarning.pcss"; @import "./views/elements/_SearchWarning.pcss";
@ -319,7 +316,6 @@
@import "./views/settings/_AvatarSetting.pcss"; @import "./views/settings/_AvatarSetting.pcss";
@import "./views/settings/_CrossSigningPanel.pcss"; @import "./views/settings/_CrossSigningPanel.pcss";
@import "./views/settings/_CryptographyPanel.pcss"; @import "./views/settings/_CryptographyPanel.pcss";
@import "./views/settings/_DevicesPanel.pcss";
@import "./views/settings/_FontScalingPanel.pcss"; @import "./views/settings/_FontScalingPanel.pcss";
@import "./views/settings/_ImageSizePanel.pcss"; @import "./views/settings/_ImageSizePanel.pcss";
@import "./views/settings/_IntegrationManager.pcss"; @import "./views/settings/_IntegrationManager.pcss";
@ -348,7 +344,6 @@
@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.pcss"; @import "./views/settings/tabs/user/_MjolnirUserSettingsTab.pcss";
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.pcss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.pcss";
@import "./views/settings/tabs/user/_SidebarUserSettingsTab.pcss"; @import "./views/settings/tabs/user/_SidebarUserSettingsTab.pcss";
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.pcss";
@import "./views/spaces/_SpaceBasicSettings.pcss"; @import "./views/spaces/_SpaceBasicSettings.pcss";
@import "./views/spaces/_SpaceChildrenPicker.pcss"; @import "./views/spaces/_SpaceChildrenPicker.pcss";
@import "./views/spaces/_SpaceCreateMenu.pcss"; @import "./views/spaces/_SpaceCreateMenu.pcss";

View file

@ -53,25 +53,25 @@ limitations under the License.
mask-image: url("$(res)/img/feather-customised/help-circle.svg"); mask-image: url("$(res)/img/feather-customised/help-circle.svg");
} }
} }
}
.mx_AppPermission_tooltip { .mx_Tooltip.mx_Tooltip--appPermission {
box-shadow: none; box-shadow: none;
background-color: $tooltip-timeline-bg-color; background-color: $tooltip-timeline-bg-color;
color: $tooltip-timeline-fg-color; color: $tooltip-timeline-fg-color;
border: none; border: none;
border-radius: 3px; border-radius: 3px;
padding: 6px 8px; padding: 6px 8px;
&.mx_AppPermission_tooltip--dark { &.mx_Tooltip--appPermission--dark {
.mx_Tooltip_chevron::after { .mx_Tooltip_chevron::after {
border-right-color: $tooltip-timeline-bg-color; border-right-color: $tooltip-timeline-bg-color;
}
}
ul {
list-style-position: inside;
padding-left: 2px;
margin-left: 0;
} }
} }
ul {
list-style-position: inside;
padding-left: 2px;
margin-left: 0;
}
} }

View file

@ -34,7 +34,9 @@ limitations under the License.
width: 100%; width: 100%;
display: grid; display: grid;
grid-gap: $spacing-8; grid-gap: $spacing-8;
grid-template-columns: 1fr; // setting minwidth 0 makes columns definitely sized
// fixing horizontal overflow
grid-template-columns: minmax(0, 1fr);
justify-items: flex-start; justify-items: flex-start;
margin-top: $spacing-24; margin-top: $spacing-24;
@ -50,4 +52,8 @@ limitations under the License.
&.mx_SettingsSubsection_contentStretch { &.mx_SettingsSubsection_contentStretch {
justify-items: stretch; justify-items: stretch;
} }
&.mx_SettingsSubsection_noHeading {
margin-top: 0;
}
} }

View file

@ -113,9 +113,11 @@ limitations under the License.
} }
.mx_QuickSettingsButton_icon { .mx_QuickSettingsButton_icon {
// TODO remove when all icons have fill=currentColor
* { * {
fill: $secondary-content; fill: $secondary-content;
} }
color: $secondary-content;
width: 16px; width: 16px;
height: 16px; height: 16px;
position: absolute; position: absolute;

View file

@ -34,40 +34,6 @@ limitations under the License.
/** Fixme - factor this out with the main header **/ /** Fixme - factor this out with the main header **/
/* See: mx_RoomHeader_button, of which this is a copy.
* TODO: factor out a common component to avoid this duplication.
*/
.mx_RightPanel_headerButton {
cursor: pointer;
flex: 0 0 auto;
margin-left: 1px;
margin-right: 1px;
height: 32px;
width: 32px;
position: relative;
border-radius: 100%;
&::before {
content: "";
position: absolute;
top: 4px; /* center with parent of 32px */
left: 4px; /* center with parent of 32px */
height: 24px;
width: 24px;
background-color: $icon-button-color;
mask-repeat: no-repeat;
mask-size: contain;
}
&:hover {
background: rgba($accent, 0.1);
&::before {
background-color: $accent;
}
}
}
.mx_RightPanel_threadsButton::before { .mx_RightPanel_threadsButton::before {
mask-image: url("$(res)/img/element-icons/room/thread.svg"); mask-image: url("$(res)/img/element-icons/room/thread.svg");
} }
@ -89,41 +55,6 @@ limitations under the License.
} }
} }
.mx_RightPanel_headerButton_unreadIndicator_bg {
position: absolute;
right: var(--RoomHeader-indicator-dot-offset);
top: var(--RoomHeader-indicator-dot-offset);
margin: 4px;
width: var(--RoomHeader-indicator-dot-size);
height: var(--RoomHeader-indicator-dot-size);
border-radius: 50%;
transform: scale(1.6);
transform-origin: center center;
background: rgba($background, 1);
}
.mx_RightPanel_headerButton_unreadIndicator {
position: absolute;
right: var(--RoomHeader-indicator-dot-offset);
top: var(--RoomHeader-indicator-dot-offset);
margin: 4px;
&.mx_Indicator_red {
background: rgba($alert, 1);
box-shadow: rgba($alert, 1);
}
&.mx_Indicator_gray {
background: rgba($room-icon-unread-color, 1);
box-shadow: rgba($room-icon-unread-color, 1);
}
&.mx_Indicator_bold {
background: rgba($primary-content, 1);
box-shadow: rgba($primary-content, 1);
}
}
.mx_RightPanel_timelineCardButton { .mx_RightPanel_timelineCardButton {
&::before { &::before {
mask-image: url("$(res)/img/element-icons/feedback.svg"); mask-image: url("$(res)/img/element-icons/feedback.svg");
@ -131,19 +62,6 @@ limitations under the License.
} }
} }
.mx_RightPanel_headerButton_unread {
&::before {
background-color: $room-icon-unread-color !important;
}
}
.mx_RightPanel_headerButton_highlight,
.mx_RightPanel_headerButton:hover {
&::before {
background-color: $accent !important;
}
}
.mx_RightPanel .mx_MemberList, .mx_RightPanel .mx_MemberList,
.mx_RightPanel .mx_MemberInfo { .mx_RightPanel .mx_MemberInfo {
order: 2; order: 2;

View file

@ -160,7 +160,7 @@ limitations under the License.
} }
} }
.mx_RoomStatusBar_connectionLostBar img { .mx_RoomStatusBar_connectionLostBar svg {
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
vertical-align: middle; vertical-align: middle;

View file

@ -46,14 +46,6 @@ limitations under the License.
} }
} }
.mx_RoomView_auxPanel {
min-width: 0px;
width: 100%;
margin: 0px auto;
overflow: auto;
}
.mx_RoomView_auxPanel_hiddenHighlights { .mx_RoomView_auxPanel_hiddenHighlights {
border-bottom: 1px solid $primary-hairline-color; border-bottom: 1px solid $primary-hairline-color;
padding: 10px 26px; padding: 10px 26px;

View file

@ -271,23 +271,6 @@ $SpaceRoomViewInnerWidth: 428px;
} }
.mx_SpaceRoomView_inviteTeammates { .mx_SpaceRoomView_inviteTeammates {
/* XXX remove this when spaces leaves Beta */
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
padding: 16px;
position: relative;
border-radius: 8px;
background-color: $header-panel-bg-color;
max-width: $SpaceRoomViewInnerWidth;
margin: 20px 0 30px;
box-sizing: border-box;
.mx_BetaCard_betaPill {
position: absolute;
left: 16px;
top: 16px;
}
}
.mx_SpaceRoomView_inviteTeammates_buttons { .mx_SpaceRoomView_inviteTeammates_buttons {
color: $secondary-content; color: $secondary-content;
margin-top: 28px; margin-top: 28px;

View file

@ -1,41 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ChatCreateOrReuseDialog .mx_ChatCreateOrReuseDialog_tiles {
margin-top: 24px;
}
.mx_ChatCreateOrReuseDialog .mx_Dialog_content {
margin-bottom: 24px;
/*
To stop spinner that mx_ChatCreateOrReuseDialog_profile replaces from causing a
height change
*/
min-height: 100px;
}
.mx_ChatCreateOrReuseDialog .mx_RoomTile_badge {
display: none;
}
.mx_ChatCreateOrReuseDialog_profile {
display: flex;
}
.mx_ChatCreateOrReuseDialog_profile_name {
padding: 14px;
}

View file

@ -31,16 +31,6 @@ limitations under the License.
.mx_SettingsTab { .mx_SettingsTab {
min-width: unset; min-width: unset;
.mx_SettingsTab_section {
font-size: $font-15px;
line-height: $font-24px;
.mx_Checkbox + p {
color: $secondary-content;
margin: 0 20px 0 24px;
}
}
} }
} }
} }

View file

@ -37,12 +37,6 @@ limitations under the License.
margin-bottom: 20px; margin-bottom: 20px;
} }
& + .mx_SettingsTab_subheading {
border-top: 1px solid $menu-border-color;
margin-top: 0;
padding-top: 24px;
}
.mx_StyledRadioButton { .mx_StyledRadioButton {
margin-top: 8px; margin-top: 8px;
margin-bottom: 4px; margin-bottom: 4px;

View file

@ -58,8 +58,8 @@ limitations under the License.
.mx_Dropdown_option { .mx_Dropdown_option {
height: 35px; height: 35px;
line-height: $font-35px; line-height: $font-35px;
padding-left: 8px; // Overwrites the default padding for any li elements
padding-right: 8px; padding: 0 8px;
} }
.mx_Dropdown_input > .mx_Dropdown_option { .mx_Dropdown_input > .mx_Dropdown_option {
@ -121,6 +121,10 @@ input.mx_Dropdown_option:focus {
min-height: 35px; min-height: 35px;
} }
ul.mx_Dropdown_menu li.mx_Dropdown_option {
list-style: none;
}
.mx_Dropdown_menu .mx_Dropdown_option_highlight { .mx_Dropdown_menu .mx_Dropdown_option_highlight {
background-color: $focus-bg-color; background-color: $focus-bg-color;
} }

View file

@ -1,21 +0,0 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_FontScalingPanel {
.mx_FontScalingPanel_preview.mx_EventTilePreview_loader {
padding: var(--FontScalingPanel_preview-padding-block) 0;
}
}

View file

@ -1,33 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoleButton {
margin-left: 4px;
margin-right: 4px;
cursor: pointer;
display: inline-block;
}
.mx_RoleButton object {
pointer-events: none;
}
.mx_RoleButton_tooltip {
display: inline-block;
position: relative;
top: -25px;
left: 6px;
}

View file

@ -17,16 +17,12 @@ limitations under the License.
.mx_TagComposer { .mx_TagComposer {
.mx_TagComposer_input { .mx_TagComposer_input {
display: flex; display: flex;
flex-direction: row;
.mx_Field {
flex: 1;
margin: 0; /* override from field styles */
}
.mx_AccessibleButton { .mx_AccessibleButton {
min-width: 70px; min-width: 70px;
padding: 0 8px; /* override from button styles */ padding: 0 8px; /* override from button styles */
margin-left: 16px; /* distance from <Field> */ align-self: stretch; /* override default settingstab style */
} }
.mx_Field, .mx_Field,

View file

@ -222,60 +222,34 @@ limitations under the License.
} }
.mx_AppTileMenuBar_widgets { .mx_AppTileMenuBar_widgets {
float: right;
display: flex; display: flex;
flex-direction: row;
align-items: center; align-items: center;
}
.mx_AppTileMenuBar_iconButton { .mx_AppTileMenuBar_widgets_button {
--size: 24px; /* Size of the button. Its height and width values should be same */ --size: 24px; /* Size of the button. Its height and width values should be same */
margin: 0 4px; margin: 0 4px;
position: relative; position: relative;
height: var(--size);
width: var(--size);
&::before,
&:hover::after {
content: "";
position: absolute;
height: var(--size); height: var(--size);
width: var(--size); width: var(--size);
} display: flex;
align-items: center;
justify-content: center;
&::before { &:hover::after {
background-color: $muted-fg-color; content: "";
mask-position: center; position: absolute;
mask-repeat: no-repeat; height: var(--size);
mask-size: 12px; width: var(--size);
} background-color: $panel-actions;
border-radius: 50%;
left: 0;
top: 0;
}
&:hover::after { .mx_Icon {
background-color: $panel-actions; color: $muted-fg-color;
border-radius: 50%; }
left: 0;
top: 0;
}
&.mx_AppTileMenuBar_iconButton--collapse::before {
mask-image: url("$(res)/img/element-icons/minimise-collapse.svg");
}
&.mx_AppTileMenuBar_iconButton--maximise::before {
mask-image: url("$(res)/img/element-icons/maximise-expand.svg");
}
&.mx_AppTileMenuBar_iconButton--minimise::before {
mask-image: url("$(res)/img/element-icons/minus-button.svg");
}
&.mx_AppTileMenuBar_iconButton--popout::before {
mask-image: url("$(res)/img/feather-customised/widget/external-link.svg");
}
&.mx_AppTileMenuBar_iconButton--menu::before {
mask-image: url("$(res)/img/element-icons/room/ellipsis.svg");
} }
} }
} }

View file

@ -14,37 +14,47 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.m_RoomView_auxPanel_stateViews { .mx_AuxPanel {
padding: 5px; min-width: 0px;
padding-left: 19px; width: 100%;
border-bottom: 1px solid $primary-hairline-color; margin: 0px auto;
}
.m_RoomView_auxPanel_stateViews_span a { overflow: auto;
text-decoration: none;
color: inherit;
}
.m_RoomView_auxPanel_stateViews_span[data-severity="warning"] { .mx_AuxPanel_stateViews {
font-weight: bold; padding: 5px;
color: orange; padding-left: 19px;
} border-bottom: 1px solid $primary-hairline-color;
}
.m_RoomView_auxPanel_stateViews_span[data-severity="alert"] { .mx_AuxPanel_stateViews_span {
font-weight: bold; &[data-severity="warning"] {
color: red; font-weight: bold;
} color: orange;
}
.m_RoomView_auxPanel_stateViews_span[data-severity="normal"] { &[data-severity="alert"] {
font-weight: normal; font-weight: bold;
} color: red;
}
.m_RoomView_auxPanel_stateViews_span[data-severity="notice"] { &[data-severity="normal"] {
font-weight: normal; font-weight: normal;
color: $settings-grey-fg-color; }
}
.m_RoomView_auxPanel_stateViews_delim { &[data-severity="notice"] {
padding: 0 5px; font-weight: normal;
color: $settings-grey-fg-color; color: $settings-grey-fg-color;
}
a {
text-decoration: none;
color: inherit;
}
}
.mx_AuxPanel_stateViews_delim {
padding: 0 5px;
color: $settings-grey-fg-color;
}
} }

View file

@ -170,8 +170,10 @@ $left-gutter: 64px;
&[data-layout="irc"], &[data-layout="irc"],
&[data-layout="group"] { &[data-layout="group"] {
--selected-message-border-width: 4px;
/* TODO: adjust the values for IRC layout */ /* TODO: adjust the values for IRC layout */
--EventTile-box-shadow-offset-x: calc(50px + $selected-message-border-width); --EventTile-box-shadow-offset-x: calc(50px + var(--selected-message-border-width));
--EventTile-box-shadow-spread-radius: -50px; --EventTile-box-shadow-spread-radius: -50px;
.mx_EventTile_e2eIcon { .mx_EventTile_e2eIcon {
position: absolute; position: absolute;
@ -447,7 +449,9 @@ $left-gutter: 64px;
&.mx_EventTile_isEditing > .mx_EventTile_line { &.mx_EventTile_isEditing > .mx_EventTile_line {
.mx_EditMessageComposer { .mx_EditMessageComposer {
/* add space for the stroke on box-shadow */ /* add space for the stroke on box-shadow */
padding-inline-start: calc($selected-message-border-width + var(--EditMessageComposer-padding-inline)); padding-inline-start: calc(
var(--selected-message-border-width) + var(--EditMessageComposer-padding-inline)
);
} }
} }
@ -592,7 +596,7 @@ $left-gutter: 64px;
&.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, &.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
&.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, &.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
&.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { &.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
padding-inline-start: calc($left-gutter + 18px + $selected-message-border-width); padding-inline-start: calc($left-gutter + 18px + var(--selected-message-border-width));
} }
} }
} }
@ -1120,29 +1124,6 @@ $left-gutter: 64px;
box-sizing: border-box; box-sizing: border-box;
padding-bottom: 0; padding-bottom: 0;
padding-inline-start: var(--leftOffset); padding-inline-start: var(--leftOffset);
.mx_ThreadPanel_replies {
margin-top: $spacing-8;
display: flex;
align-items: center;
position: relative;
&::before {
@mixin ThreadSummaryIcon;
}
.mx_ThreadPanel_replies_amount {
@mixin ThreadRepliesAmount;
line-height: var(--EventTile_ThreadSummary-line-height);
font-size: $font-12px; /* Same font size as the counter on the main panel */
}
.mx_ThreadSummary_content {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
} }
.mx_MessageTimestamp { .mx_MessageTimestamp {
@ -1167,6 +1148,31 @@ $left-gutter: 64px;
} }
} }
.mx_EventTile[data-shape="ThreadsList"] {
.mx_ThreadPanel_replies {
margin-top: $spacing-8;
display: flex;
align-items: center;
position: relative;
&::before {
@mixin ThreadSummaryIcon;
}
.mx_ThreadPanel_replies_amount {
@mixin ThreadRepliesAmount;
line-height: var(--EventTile_ThreadSummary-line-height);
font-size: $font-12px; /* Same font size as the counter on the main panel */
}
.mx_ThreadSummary_content {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}
/* For style rules of ThreadView, see _ThreadPanel.pcss */ /* For style rules of ThreadView, see _ThreadPanel.pcss */
.mx_ThreadView { .mx_ThreadView {
--ThreadView_group_spacing-start: 56px; /* 56px: 64px - 8px (padding) */ --ThreadView_group_spacing-start: 56px; /* 56px: 64px - 8px (padding) */
@ -1449,12 +1455,14 @@ $left-gutter: 64px;
margin-bottom: $spacing-4; /* 1/4 of the non-compact margin-bottom */ margin-bottom: $spacing-4; /* 1/4 of the non-compact margin-bottom */
} }
} }
}
&[data-shape="ThreadsList"][data-notification]::before, &[data-shape="ThreadsList"][data-notification]::before,
.mx_NotificationBadge { .mx_NotificationBadge {
/* stylelint-disable-next-line declaration-colon-space-after */ /* stylelint-disable-next-line declaration-colon-space-after */
inset-block-start: calc($notification-inset-block-start - var(--MatrixChat_useCompactLayout_group-padding-top)); inset-block-start: calc(
$notification-inset-block-start - var(--MatrixChat_useCompactLayout_group-padding-top)
);
}
} }
} }

View file

@ -76,6 +76,12 @@ limitations under the License.
.mx_MemberInfo_container { .mx_MemberInfo_container {
margin: 0 16px 16px 16px; margin: 0 16px 16px 16px;
&.mx_MemberInfo_container--profile {
margin-bottom: 16px;
font-size: $font-15px;
position: relative;
}
} }
.mx_MemberInfo_avatar { .mx_MemberInfo_avatar {
@ -95,23 +101,11 @@ limitations under the License.
} }
} }
.mx_MemberInfo_profile {
margin-bottom: 16px;
}
.mx_MemberInfo_profileField {
font-size: $font-15px;
position: relative;
}
.mx_MemberInfo_buttons {
margin-bottom: 16px;
}
.mx_MemberInfo_field { .mx_MemberInfo_field {
cursor: pointer; cursor: pointer;
font-size: $font-15px; font-size: $font-15px;
color: $primary-content; color: $primary-content;
margin-left: 8px; margin-left: 8px;
margin-bottom: 16px;
line-height: $font-23px; line-height: $font-23px;
} }

View file

@ -69,3 +69,10 @@ limitations under the License.
} }
} }
} }
.mx_NotificationBadge_tooltip {
display: inline-block;
position: relative;
top: -25px;
left: 6px;
}

View file

@ -80,6 +80,7 @@ limitations under the License.
padding: 1px 4px; padding: 1px 4px;
display: flex; display: flex;
user-select: none; user-select: none;
cursor: pointer;
&:hover { &:hover {
background-color: $quinary-content; background-color: $quinary-content;
@ -102,6 +103,14 @@ limitations under the License.
background-color: $tertiary-content; background-color: $tertiary-content;
} }
&.mx_RoomHeader_name--textonly {
cursor: unset;
&:hover {
background-color: unset;
}
}
&[aria-expanded="true"] { &[aria-expanded="true"] {
background-color: $quinary-content; background-color: $quinary-content;
@ -120,11 +129,6 @@ limitations under the License.
opacity: 0.6; opacity: 0.6;
} }
.mx_RoomHeader_name:not(.mx_RoomHeader_name--textonly),
.mx_RoomHeader_avatar {
cursor: pointer;
}
.mx_RoomTopic { .mx_RoomTopic {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
@ -157,6 +161,7 @@ limitations under the License.
flex: 0; flex: 0;
margin: 0 7px; margin: 0 7px;
position: relative; position: relative;
cursor: pointer;
} }
.mx_RoomHeader_avatar .mx_BaseAvatar_image { .mx_RoomHeader_avatar .mx_BaseAvatar_image {
@ -194,6 +199,54 @@ limitations under the License.
} }
} }
.mx_RoomHeader_button_unreadIndicator_bg {
position: absolute;
right: var(--RoomHeader-indicator-dot-offset);
top: var(--RoomHeader-indicator-dot-offset);
margin: 4px;
width: var(--RoomHeader-indicator-dot-size);
height: var(--RoomHeader-indicator-dot-size);
border-radius: 50%;
transform: scale(1.6);
transform-origin: center center;
background: rgba($background, 1);
}
.mx_RoomHeader_button_unreadIndicator {
position: absolute;
right: var(--RoomHeader-indicator-dot-offset);
top: var(--RoomHeader-indicator-dot-offset);
margin: 4px;
&.mx_Indicator_red {
background: rgba($alert, 1);
box-shadow: rgba($alert, 1);
}
&.mx_Indicator_gray {
background: rgba($room-icon-unread-color, 1);
box-shadow: rgba($room-icon-unread-color, 1);
}
&.mx_Indicator_bold {
background: rgba($primary-content, 1);
box-shadow: rgba($primary-content, 1);
}
}
.mx_RoomHeader_button--unread {
&::before {
background-color: $room-icon-unread-color !important;
}
}
.mx_RoomHeader_button--highlight,
.mx_RoomHeader_button:hover {
&::before {
background-color: $accent !important;
}
}
.mx_RoomHeader_forgetButton::before { .mx_RoomHeader_forgetButton::before {
mask-image: url("$(res)/img/element-icons/leave.svg"); mask-image: url("$(res)/img/element-icons/leave.svg");
width: 26px; width: 26px;

View file

@ -55,13 +55,18 @@ limitations under the License.
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
.mx_RoomTile_title,
.mx_RoomTile_subtitle { .mx_RoomTile_subtitle {
width: 100%; align-items: center;
color: $secondary-content;
display: flex;
gap: $spacing-4;
line-height: $font-18px;
}
/* Ellipsize any text overflow */ .mx_RoomTile_title,
text-overflow: ellipsis; .mx_RoomTile_subtitle_text {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
@ -74,11 +79,6 @@ limitations under the License.
} }
} }
.mx_RoomTile_subtitle {
line-height: $font-18px;
color: $secondary-content;
}
.mx_RoomTile_titleWithSubtitle { .mx_RoomTile_titleWithSubtitle {
margin-top: -3px; /* shift the title up a bit more */ margin-top: -3px; /* shift the title up a bit more */
} }

View file

@ -22,7 +22,7 @@ limitations under the License.
position: relative; position: relative;
.mx_AvatarSetting_hover { .mx_AvatarSetting_hover {
transition: opacity $hover-transition; transition: opacity var(--hover-transition);
/* position to place the hover bg over the entire thing */ /* position to place the hover bg over the entire thing */
position: absolute; position: absolute;

View file

@ -32,13 +32,9 @@ limitations under the License.
} }
} }
.mx_CryptographyPanel_importExportButtons .mx_AccessibleButton {
margin-right: 10px;
}
.mx_CryptographyPanel_importExportButtons { .mx_CryptographyPanel_importExportButtons {
margin-bottom: 15px;
display: inline-flex; display: inline-flex;
flex-flow: wrap; flex-flow: wrap;
row-gap: 10px; row-gap: $spacing-8;
column-gap: $spacing-8;
} }

View file

@ -1,102 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_DevicesPanel {
width: auto;
max-width: 880px;
hr {
border: none;
border-bottom: 1px solid $quinary-content;
}
}
.mx_DevicesPanel_header {
display: flex;
align-items: center;
margin-block: 10px;
.mx_DevicesPanel_header_title {
font-size: $font-18px;
font-weight: var(--font-semi-bold);
color: $primary-content;
}
.mx_DevicesPanel_selectButton {
padding-top: 9px;
}
.mx_E2EIcon {
width: 24px;
height: 24px;
margin-left: 0;
margin-right: 5px;
}
}
.mx_DevicesPanel_deleteButton {
margin-top: 10px;
}
.mx_DevicesPanel_device {
display: flex;
align-items: flex-start;
margin-block: 10px;
min-height: 35px;
padding: 0 $spacing-8;
.mx_DeviceTypeIcon {
/* hide the new device type in legacy device list
for backwards compat reasons */
display: none;
}
}
.mx_DevicesPanel_icon {
margin-left: 0px;
margin-right: $spacing-16;
margin-top: 2px;
}
.mx_DevicesPanel_deviceInfo {
flex-grow: 1;
}
.mx_DevicesPanel_deviceName {
color: $primary-content;
}
.mx_DevicesPanel_lastSeen {
font-size: $font-12px;
}
.mx_DevicesPanel_deviceButtons {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 9px;
}
.mx_DevicesPanel_renameForm {
display: flex;
align-items: center;
gap: 5px;
.mx_Field_input {
width: 240px;
margin: 0;
}
}

View file

@ -14,63 +14,48 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_FontScalingPanel { .mx_FontScalingPanel_preview {
color: $primary-content; --FontScalingPanel_preview-padding-block: 9px;
.mx_FontScalingPanel_preview, border: 1px solid $quinary-content;
.mx_FontScalingPanel_fontSlider { border-radius: 10px;
margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end); padding: 0 $spacing-16 var(--FontScalingPanel_preview-padding-block) $spacing-16;
pointer-events: none;
display: flow-root;
&.mx_IRCLayout {
padding-top: 9px; /* TODO: Use a spacing variable */
} }
.mx_FontScalingPanel_preview { .mx_EventTile[data-layout="bubble"] {
--FontScalingPanel_preview-padding-block: 9px; margin-top: 30px; /* TODO: Use a spacing variable */
border: 1px solid $quinary-content;
border-radius: 10px;
padding: 0 $spacing-16 var(--FontScalingPanel_preview-padding-block) $spacing-16;
pointer-events: none;
display: flow-root;
&.mx_IRCLayout {
padding-top: 9px; /* TODO: Use a spacing variable */
}
.mx_EventTile[data-layout="bubble"] {
margin-top: 30px; /* TODO: Use a spacing variable */
}
.mx_EventTile_msgOption {
display: none;
}
} }
.mx_FontScalingPanel_fontSlider { .mx_EventTile_msgOption {
display: flex; display: none;
align-items: center; }
padding: 15px $spacing-20 35px; /* TODO: Use spacing variables */ }
background: rgba($quinary-content, 0.2);
border-radius: 10px; .mx_FontScalingPanel_fontSlider {
font-size: $font-10px; display: flex;
margin-top: $spacing-24; align-items: center;
margin-bottom: $spacing-24; padding: 15px $spacing-20 35px; /* TODO: Use spacing variables */
background: rgba($quinary-content, 0.2);
.mx_FontScalingPanel_fontSlider_smallText, border-radius: 10px;
.mx_FontScalingPanel_fontSlider_largeText { font-size: $font-10px;
font-weight: 500;
} .mx_FontScalingPanel_fontSlider_smallText,
.mx_FontScalingPanel_fontSlider_largeText {
.mx_FontScalingPanel_fontSlider_smallText { font-weight: 500;
font-size: $font-15px; }
padding-inline-end: $spacing-20;
} .mx_FontScalingPanel_fontSlider_smallText {
font-size: $font-15px;
.mx_FontScalingPanel_fontSlider_largeText { padding-inline-end: $spacing-20;
font-size: $font-18px; }
padding-inline-start: $spacing-20;
} .mx_FontScalingPanel_fontSlider_largeText {
} font-size: $font-18px;
padding-inline-start: $spacing-20;
.mx_FontScalingPanel_customFontSizeField {
margin-inline-start: var(--AppearanceUserSettingsTab_Field-margin-inline-start);
} }
} }

View file

@ -14,34 +14,31 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_ImageSizePanel { .mx_ImageSizePanel_radios {
color: $primary-content; display: flex;
flex-direction: row;
gap: $spacing-16;
.mx_ImageSizePanel_radios { > label {
display: flex; margin-right: 68px; /* keep the boxes separate */
margin-top: 16px; /* move away from header a bit */ cursor: pointer;
}
> label { .mx_ImageSizePanel_size {
margin-right: 68px; /* keep the boxes separate */ background-color: $quinary-content;
cursor: pointer; mask-repeat: no-repeat;
mask-size: 221px;
mask-position: center;
width: 221px;
height: 148px;
margin-bottom: 14px; /* move radio button away from bottom edge a bit */
&.mx_ImageSizePanel_sizeDefault {
mask: url("$(res)/img/element-icons/settings/img-size-normal.svg");
} }
.mx_ImageSizePanel_size { &.mx_ImageSizePanel_sizeLarge {
background-color: $quinary-content; mask: url("$(res)/img/element-icons/settings/img-size-large.svg");
mask-repeat: no-repeat;
mask-size: 221px;
mask-position: center;
width: 221px;
height: 148px;
margin-bottom: 14px; /* move radio button away from bottom edge a bit */
&.mx_ImageSizePanel_sizeDefault {
mask: url("$(res)/img/element-icons/settings/img-size-normal.svg");
}
&.mx_ImageSizePanel_sizeLarge {
mask: url("$(res)/img/element-icons/settings/img-size-large.svg");
}
} }
} }
} }

View file

@ -57,7 +57,6 @@ limitations under the License.
.mx_JoinRuleSettings_radioButton { .mx_JoinRuleSettings_radioButton {
padding-top: 16px; padding-top: 16px;
margin-bottom: 8px;
.mx_StyledRadioButton_content { .mx_StyledRadioButton_content {
margin-left: 14px; margin-left: 14px;

View file

@ -15,79 +15,78 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_LayoutSwitcher { .mx_LayoutSwitcher_RadioButtons {
.mx_LayoutSwitcher_RadioButtons { display: flex;
flex-direction: row;
gap: 24px;
width: 100%;
color: $primary-content;
> .mx_LayoutSwitcher_RadioButton {
flex-grow: 0;
flex-shrink: 1;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
gap: 24px;
color: $primary-content; flex-basis: 33%;
min-width: 0;
> .mx_LayoutSwitcher_RadioButton { border: 1px solid $quinary-content;
flex-grow: 0; border-radius: 10px;
flex-shrink: 1;
.mx_EventTile_msgOption,
.mx_MessageActionBar {
display: none;
}
.mx_LayoutSwitcher_RadioButton_preview {
flex-grow: 1;
display: flex; display: flex;
flex-direction: column; align-items: center;
padding: 10px;
pointer-events: none;
width: 300px; .mx_EventTile[data-layout="bubble"] .mx_EventTile_line {
min-width: 0; padding-right: 11px;
border: 1px solid $quinary-content;
border-radius: 10px;
.mx_EventTile_msgOption,
.mx_MessageActionBar {
display: none;
}
.mx_LayoutSwitcher_RadioButton_preview {
flex-grow: 1;
display: flex;
align-items: center;
padding: 10px;
pointer-events: none;
.mx_EventTile[data-layout="bubble"] .mx_EventTile_line {
padding-right: 11px;
}
}
.mx_StyledRadioButton {
flex-grow: 0;
padding: 10px;
}
.mx_EventTile_content {
margin-right: 0;
}
&.mx_LayoutSwitcher_RadioButton_selected {
border-color: $accent;
} }
} }
.mx_StyledRadioButton { .mx_StyledRadioButton {
border-top: 1px solid $quinary-content; flex-grow: 0;
padding: 10px;
} }
.mx_StyledRadioButton_checked { .mx_EventTile_content {
background-color: rgba($accent, 0.08); margin-right: 0;
} }
.mx_EventTile { &.mx_LayoutSwitcher_RadioButton_selected {
margin: 0; border-color: $accent;
&[data-layout="bubble"] { }
margin-right: 40px; }
flex-shrink: 1;
} .mx_StyledRadioButton {
&[data-layout="irc"] { border-top: 1px solid $quinary-content;
> a { }
display: none;
} .mx_StyledRadioButton_checked {
} background-color: rgba($accent, 0.08);
.mx_EventTile_line { }
max-width: 90%;
.mx_EventTile {
margin: 0;
&[data-layout="bubble"] {
margin-right: 40px;
flex-shrink: 1;
}
&[data-layout="irc"] {
> a {
display: none;
} }
} }
.mx_EventTile_line {
max-width: 90%;
}
} }
} }

View file

@ -20,7 +20,6 @@ limitations under the License.
grid-template-columns: auto repeat(3, 62px); grid-template-columns: auto repeat(3, 62px);
place-items: center center; place-items: center center;
grid-gap: 8px; grid-gap: 8px;
margin-top: $spacing-40;
/* Override StyledRadioButton default styles */ /* Override StyledRadioButton default styles */
.mx_StyledRadioButton { .mx_StyledRadioButton {
@ -34,6 +33,11 @@ limitations under the License.
display: none; display: none;
} }
} }
// left align section heading
.mx_SettingsSubsectionHeading {
justify-self: start;
}
} }
.mx_UserNotifSettings_gridRowContainer { .mx_UserNotifSettings_gridRowContainer {
@ -51,10 +55,6 @@ limitations under the License.
/* force it inline using float */ /* force it inline using float */
float: left; float: left;
} }
.mx_UserNotifSettings_gridRowHeading {
font-size: $font-18px;
font-weight: var(--font-semi-bold);
}
.mx_UserNotifSettings_gridColumnLabel { .mx_UserNotifSettings_gridColumnLabel {
color: $secondary-content; color: $secondary-content;
@ -70,39 +70,35 @@ limitations under the License.
margin-top: -$spacing-4; margin-top: -$spacing-4;
} }
.mx_UserNotifSettings { .mx_UserNotifSettings_floatingSection {
color: $primary-content; /* override from default settings page styles */ margin-top: 40px;
.mx_UserNotifSettings_floatingSection { & > div:first-child {
margin-top: 40px; /* section header */
font-size: $font-18px;
& > div:first-child { font-weight: var(--font-semi-bold);
/* section header */
font-size: $font-18px;
font-weight: var(--font-semi-bold);
}
> table {
border-collapse: collapse;
border-spacing: 0;
margin-top: 8px;
tr > td:first-child {
/* Just for a bit of spacing */
padding-right: 8px;
}
}
} }
.mx_UserNotifSettings_clearNotifsButton { > table {
border-collapse: collapse;
border-spacing: 0;
margin-top: 8px; margin-top: 8px;
}
.mx_TagComposer { tr > td:first-child {
margin-top: 35px; /* lots of distance from the last line of the table */ /* Just for a bit of spacing */
padding-right: 8px;
}
} }
} }
.mx_UserNotifSettings_clearNotifsButton {
margin-top: 8px;
}
.mx_TagComposer {
margin-top: 35px; /* lots of distance from the last line of the table */
}
.mx_AccessibleButton.mx_NotificationSound_browse { .mx_AccessibleButton.mx_NotificationSound_browse {
margin-right: 10px; margin-right: 10px;
} }

View file

@ -15,7 +15,6 @@ limitations under the License.
*/ */
.mx_ProfileSettings { .mx_ProfileSettings {
margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end);
border-bottom: 1px solid $quinary-content; border-bottom: 1px solid $quinary-content;
.mx_ProfileSettings_avatarUpload { .mx_ProfileSettings_avatarUpload {
@ -29,11 +28,13 @@ limitations under the License.
flex-grow: 1; flex-grow: 1;
margin-inline-end: 54px; margin-inline-end: 54px;
.mx_Field:first-child { .mx_Field {
margin-top: 0; margin-top: $spacing-8;
} }
.mx_ProfileSettings_profile_controls_topic { .mx_ProfileSettings_profile_controls_topic {
margin-top: $spacing-8;
& > textarea { & > textarea {
font-family: inherit; font-family: inherit;
resize: vertical; resize: vertical;

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,8 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_SetIdServer .mx_Field_input { .mx_SetIdServer {
margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end); display: flex;
flex-direction: column;
align-items: flex-start;
gap: $spacing-8;
.mx_Field {
width: 100%;
margin: 0;
}
} }
.mx_SetIdServer_tooltip { .mx_SetIdServer_tooltip {

View file

@ -15,7 +15,6 @@ limitations under the License.
*/ */
.mx_SettingsFieldset { .mx_SettingsFieldset {
margin: 10px 80px 10px 0;
box-sizing: content-box; box-sizing: content-box;
} }
@ -31,8 +30,6 @@ limitations under the License.
} }
.mx_SettingsFieldset_description { .mx_SettingsFieldset_description {
color: $settings-subsection-fg-color;
font-size: $font-14px;
display: block; display: block;
margin-top: 0; margin-top: 0;
margin-bottom: 10px; margin-bottom: 10px;
@ -46,3 +43,9 @@ limitations under the License.
} }
} }
} }
.mx_SettingsFieldset_content {
display: flex;
flex-direction: column;
gap: $spacing-8;
}

View file

@ -17,7 +17,6 @@ limitations under the License.
.mx_ExistingSpellCheckLanguage { .mx_ExistingSpellCheckLanguage {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 5px;
} }
.mx_ExistingSpellCheckLanguage_language { .mx_ExistingSpellCheckLanguage_language {
@ -26,10 +25,5 @@ limitations under the License.
} }
.mx_GeneralUserSettingsTab_spellCheckLanguageInput { .mx_GeneralUserSettingsTab_spellCheckLanguageInput {
margin-top: 1em; margin-bottom: $spacing-8;
margin-bottom: 1em;
}
.mx_SpellCheckLanguages {
margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end);
} }

View file

@ -14,53 +14,47 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_ThemeChoicePanel { .mx_ThemeChoicePanel_themeSelectors {
color: $primary-content; color: $primary-content;
display: flex;
flex-direction: row;
flex-wrap: wrap;
> .mx_ThemeSelectors { > .mx_StyledRadioButton {
display: flex; padding: $font-16px;
flex-direction: row; box-sizing: border-box;
flex-wrap: wrap; border-radius: 10px;
width: 180px;
margin-top: 4px; background: $quinary-content;
margin-bottom: 30px; opacity: 0.4;
> .mx_StyledRadioButton { flex-shrink: 1;
padding: $font-16px; flex-grow: 0;
box-sizing: border-box;
border-radius: 10px;
width: 180px;
background: $quinary-content; margin-right: 15px;
opacity: 0.4; margin-top: 10px;
flex-shrink: 1; font-weight: var(--font-semi-bold);
flex-grow: 0;
margin-right: 15px; > span {
margin-top: 10px; justify-content: center;
}
}
font-weight: var(--font-semi-bold); > .mx_StyledRadioButton_enabled {
opacity: 1;
> span { /* These colors need to be hardcoded because they don't change with the theme */
justify-content: center; &.mx_ThemeSelector_light {
} background-color: #f3f8fd;
color: #2e2f32;
} }
> .mx_StyledRadioButton_enabled { &.mx_ThemeSelector_dark {
opacity: 1; /* 5% lightened version of 181b21 */
background-color: #25282e;
/* These colors need to be hardcoded because they don't change with the theme */ color: #f3f8fd;
&.mx_ThemeSelector_light {
background-color: #f3f8fd;
color: #2e2f32;
}
&.mx_ThemeSelector_dark {
/* 5% lightened version of 181b21 */
background-color: #25282e;
color: #f3f8fd;
}
} }
} }
} }

View file

@ -17,7 +17,6 @@ limitations under the License.
.mx_SettingsSection { .mx_SettingsSection {
--SettingsTab_section-margin-bottom-preferences-labs: 30px; --SettingsTab_section-margin-bottom-preferences-labs: 30px;
--SettingsTab_heading_nth_child-margin-top: 30px; /* TODO: Use a spacing variable */ --SettingsTab_heading_nth_child-margin-top: 30px; /* TODO: Use a spacing variable */
--SettingsTab_fullWidthField-margin-inline-end: 100px;
--SettingsTab_tooltip-max-width: 120px; /* So it fits in the space provided by the page */ --SettingsTab_tooltip-max-width: 120px; /* So it fits in the space provided by the page */
color: $primary-content; color: $primary-content;

View file

@ -15,9 +15,6 @@ limitations under the License.
*/ */
.mx_SettingsTab { .mx_SettingsTab {
--SettingsTab_section-margin-bottom-preferences-labs: 30px;
--SettingsTab_heading_nth_child-margin-top: 30px; /* TODO: Use a spacing variable */
--SettingsTab_fullWidthField-margin-inline-end: 100px;
--SettingsTab_tooltip-max-width: 120px; /* So it fits in the space provided by the page */ --SettingsTab_tooltip-max-width: 120px; /* So it fits in the space provided by the page */
color: $primary-content; color: $primary-content;
@ -25,35 +22,30 @@ limitations under the License.
a { a {
color: $links; color: $links;
} }
form {
display: flex;
flex-direction: column;
gap: $spacing-8;
flex-grow: 1;
}
// never want full width buttons
// event when other content is 100% width
.mx_AccessibleButton {
align-self: flex-start;
justify-self: flex-start;
}
.mx_Field {
margin: 0;
flex: 1;
}
} }
.mx_SettingsTab_warningText { .mx_SettingsTab_warningText {
color: $alert; color: $alert;
} }
.mx_SettingsTab_heading {
font-size: $font-20px;
font-weight: var(--font-semi-bold);
color: $primary-content;
margin-top: 10px; /* TODO: Use a spacing variable */
margin-bottom: 10px; /* TODO: Use a spacing variable */
margin-right: 100px; /* TODO: Use a spacing variable */
&:nth-child(n + 2) {
margin-top: var(--SettingsTab_heading_nth_child-margin-top);
}
}
.mx_SettingsTab_subheading {
font-size: $font-16px;
display: block;
font-weight: var(--font-semi-bold);
color: $primary-content;
margin-top: $spacing-12;
margin-bottom: 10px; /* TODO: Use a spacing variable */
margin-right: 100px; /* TODO: Use a spacing variable */
}
.mx_SettingsTab_subsectionText { .mx_SettingsTab_subsectionText {
color: $secondary-content; color: $secondary-content;
font-size: $font-14px; font-size: $font-14px;

View file

@ -14,25 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_AppearanceUserSettingsTab { .mx_Field.mx_AppearanceUserSettingsTab_checkboxControlledField {
--AppearanceUserSettingsTab_Field-margin-inline-start: calc($font-16px + 10px); width: 256px;
// matches checkbox box + padding
.mx_SettingsTab_subsectionText { // to align with checkbox label
margin-block: $spacing-12 $spacing-32; margin-inline-start: calc($font-16px + 10px);
color: $primary-content; /* Same as mx_SettingsTab */
}
.mx_Field {
width: 256px;
}
.mx_AppearanceUserSettingsTab_Advanced {
.mx_Checkbox {
margin-block: $spacing-16;
}
.mx_AppearanceUserSettingsTab_systemFont {
margin-inline-start: var(--AppearanceUserSettingsTab_Field-margin-inline-start);
}
}
} }

View file

@ -14,42 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_GeneralUserSettingsTab_section--account_changePassword {
.mx_Field {
margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end);
&:first-child {
margin-top: 0;
}
}
}
/* TODO: Make this selector less painful */
.mx_GeneralUserSettingsTab_section--account .mx_SettingsTab_subheading:nth-child(n + 1),
.mx_GeneralUserSettingsTab_section--discovery .mx_SettingsTab_subheading:nth-child(n + 2),
.mx_SetIdServer .mx_SettingsTab_subheading {
margin-top: 24px;
}
.mx_GeneralUserSettingsTab_section--account,
.mx_GeneralUserSettingsTab_section--discovery {
.mx_Spinner {
/* Move the spinner to the left side of the container (default center) */
justify-content: flex-start;
}
}
.mx_GeneralUserSettingsTab_section--account .mx_EmailAddresses,
.mx_GeneralUserSettingsTab_section--account .mx_PhoneNumbers,
.mx_GeneralUserSettingsTab_section--discovery .mx_GeneralUserSettingsTab_section--discovery_existing,
.mx_GeneralUserSettingsTab_section_languageInput {
margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end);
}
.mx_GeneralUserSettingsTab_section--discovery_existing { .mx_GeneralUserSettingsTab_section--discovery_existing {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 5px;
} }
.mx_GeneralUserSettingsTab_section--discovery_existing_address, .mx_GeneralUserSettingsTab_section--discovery_existing_address,
@ -62,10 +29,8 @@ limitations under the License.
margin-left: 5px; margin-left: 5px;
} }
.mx_GeneralUserSettingsTab_section--spellcheck .mx_ToggleSwitch { .mx_GeneralUserSettingsTab_warningIcon {
float: right;
}
.mx_GeneralUserSettingsTab_heading_warningIcon {
vertical-align: middle; vertical-align: middle;
margin-right: $spacing-8;
margin-bottom: 2px;
} }

View file

@ -15,31 +15,26 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_KeyboardUserSettingsTab .mx_SettingsTab_section { .mx_KeyboardShortcut_shortcutList {
ul { margin: 0;
margin: 0; padding: 0;
padding: 0; width: 100%;
} display: grid;
grid-gap: $spacing-4;
.mx_KeyboardShortcut_shortcutRow, }
.mx_KeyboardShortcut {
display: flex; .mx_KeyboardShortcut_shortcutRow,
justify-content: space-between; .mx_KeyboardShortcut {
align-items: center; display: flex;
} justify-content: space-between;
align-items: center;
.mx_KeyboardShortcut_shortcutRow { }
column-gap: $spacing-8;
margin-bottom: $spacing-4; .mx_KeyboardShortcut_shortcutRow {
column-gap: $spacing-8;
/* TODO: Use flexbox */ }
&:last-of-type {
margin-bottom: 0; .mx_KeyboardShortcut {
} flex-wrap: nowrap;
column-gap: $spacing-4;
.mx_KeyboardShortcut {
flex-wrap: nowrap;
column-gap: 5px; /* TODO: Use a spacing variable */
}
}
} }

View file

@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_MjolnirUserSettingsTab .mx_Field {
margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end);
}
.mx_MjolnirUserSettingsTab_listItem { .mx_MjolnirUserSettingsTab_listItem {
margin-bottom: 2px; margin-bottom: 2px;
} }

View file

@ -14,43 +14,36 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_SecurityUserSettingsTab_bulkOptions .mx_AccessibleButton { .mx_SecurityUserSettingsTab_bulkOptions {
margin-right: 10px; display: flex;
flex-direction: row;
column-gap: $spacing-8;
} }
.mx_SecurityUserSettingsTab_ignoredUser { .mx_SecurityUserSettingsTab_ignoredUser {
margin-bottom: 5px; margin-bottom: $spacing-4;
} }
.mx_SecurityUserSettingsTab_ignoredUser .mx_AccessibleButton { .mx_SecurityUserSettingsTab_ignoredUser .mx_AccessibleButton {
margin-right: 10px; margin-right: $spacing-8;
} }
.mx_SecurityUserSettingsTab { .mx_SecurityUserSettingsTab_warning {
.mx_SettingsTab_section { color: $alert;
.mx_AccessibleButton_kind_link { position: relative;
font-size: inherit; padding-left: 40px;
}
}
.mx_SecurityUserSettingsTab_warning { &::before {
color: $alert; mask-repeat: no-repeat;
position: relative; mask-position: 0 center;
padding-left: 40px; mask-size: $font-24px;
margin-top: 30px; position: absolute;
width: $font-24px;
&::before { height: $font-24px;
mask-repeat: no-repeat; content: "";
mask-position: 0 center; top: 0;
mask-size: $font-24px; left: 0;
position: absolute; background-color: $alert;
width: $font-24px; mask-image: url("$(res)/img/feather-customised/alert-triangle.svg");
height: $font-24px;
content: "";
top: 0;
left: 0;
background-color: $alert;
mask-image: url("$(res)/img/feather-customised/alert-triangle.svg");
}
} }
} }

View file

@ -14,67 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_SidebarUserSettingsTab { .mx_SidebarUserSettingsTab_homeAllRoomsCheckbox {
.mx_Checkbox { margin-left: 24px;
margin-top: 12px;
font-size: $font-15px;
line-height: $font-24px;
color: $secondary-content;
}
.mx_SidebarUserSettingsTab_checkboxMicrocopy { & + div {
margin-bottom: 12px; margin-left: 48px;
margin-left: 24px; }
font-size: $font-15px; }
line-height: $font-24px;
color: $secondary-content; .mx_SidebarUserSettingsTab_checkbox {
} margin-bottom: $spacing-8;
// override checkbox styles˚
.mx_SidebarUserSettingsTab_homeAllRoomsCheckbox { label {
margin-left: 24px; align-items: flex-start !important;
}
& + div {
margin-left: 48px; svg {
} height: 16px;
} width: 16px;
margin-right: $spacing-8;
.mx_SidebarUserSettingsTab_homeCheckbox, margin-bottom: -1px;
.mx_SidebarUserSettingsTab_favouritesCheckbox,
.mx_SidebarUserSettingsTab_peopleCheckbox,
.mx_SidebarUserSettingsTab_orphansCheckbox {
.mx_Checkbox_background + div {
padding-left: 20px;
position: relative;
&::before {
background-color: $secondary-content;
content: "";
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
width: 16px;
height: 16px;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
}
}
}
.mx_SidebarUserSettingsTab_homeCheckbox .mx_Checkbox_background + div::before {
mask-image: url("$(res)/img/element-icons/home.svg");
}
.mx_SidebarUserSettingsTab_favouritesCheckbox .mx_Checkbox_background + div::before {
mask-image: url("$(res)/img/element-icons/roomlist/favorite.svg");
}
.mx_SidebarUserSettingsTab_peopleCheckbox .mx_Checkbox_background + div::before {
mask-image: url("$(res)/img/element-icons/room/members.svg");
}
.mx_SidebarUserSettingsTab_orphansCheckbox .mx_Checkbox_background + div::before {
mask-image: url("$(res)/img/element-icons/roomlist/hash-circle.svg");
} }
} }

View file

@ -1,23 +0,0 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_VoiceUserSettingsTab .mx_Field {
margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end);
}
.mx_VoiceUserSettingsTab_missingMediaPermissions {
margin-bottom: 15px;
}

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.88867 0.138672C1.36989 0.138672 0.138672 1.36989 0.138672 2.88867V15.1109C0.138672 15.395 0.299174 15.6547 0.553262 15.7817C0.80735 15.9088 1.11141 15.8813 1.33867 15.7109L4.36089 13.4442C4.57726 13.2819 4.84043 13.1942 5.11089 13.1942H13.1109C14.6297 13.1942 15.8609 11.963 15.8609 10.4442V2.88822C15.8609 1.36922 14.6295 0.138672 13.1109 0.138672H2.88867ZM3.69421 5.33301C3.69421 4.91879 4.03 4.58301 4.44421 4.58301H11.5553C11.9695 4.58301 12.3053 4.91879 12.3053 5.33301C12.3053 5.74722 11.9695 6.08301 11.5553 6.08301H4.44421C4.03 6.08301 3.69421 5.74722 3.69421 5.33301ZM4.44421 7.24976C4.03 7.24976 3.69421 7.58554 3.69421 7.99976C3.69421 8.41397 4.03 8.74976 4.44421 8.74976H7.99977C8.41398 8.74976 8.74977 8.41397 8.74977 7.99976C8.74977 7.58554 8.41398 7.24976 7.99977 7.24976H4.44421Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 974 B

View file

@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.2804 7.90031L13.2804 2.06697C12.5387 1.4489 11.4613 1.4489 10.7196 2.06698L3.71963 7.90031C3.26365 8.28029 3 8.84319 3 9.43675V20.5C3 21.6046 3.89543 22.5 5 22.5H7C8.10457 22.5 9 21.6046 9 20.5V16C9 14.8954 9.89543 14 11 14H13C14.1046 14 15 14.8954 15 16V20.5C15 21.6046 15.8954 22.5 17 22.5H19C20.1046 22.5 21 21.6046 21 20.5V9.43675C21 8.84319 20.7364 8.28029 20.2804 7.90031Z" fill="#737D8C"/> <path d="M20.2804 7.90031L13.2804 2.06697C12.5387 1.4489 11.4613 1.4489 10.7196 2.06698L3.71963 7.90031C3.26365 8.28029 3 8.84319 3 9.43675V20.5C3 21.6046 3.89543 22.5 5 22.5H7C8.10457 22.5 9 21.6046 9 20.5V16C9 14.8954 9.89543 14 11 14H13C14.1046 14 15 14.8954 15 16V20.5C15 21.6046 15.8954 22.5 17 22.5H19C20.1046 22.5 21 21.6046 21 20.5V9.43675C21 8.84319 20.7364 8.28029 20.2804 7.90031Z" fill="currentColor"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 517 B

After

Width:  |  Height:  |  Size: 522 B

View file

@ -1,3 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 4.34778V0.777778C14 0.35 13.65 0 13.2222 0H9.65222C8.96 0 8.61 0.84 9.1 1.33L10.3367 2.56667L2.55889 10.3444L1.32222 9.10778C0.84 8.61778 0 8.96 0 9.65222V13.2222C0 13.65 0.35 14 0.777778 14H4.34778C5.04 14 5.39 13.16 4.9 12.67L3.66333 11.4333L11.4411 3.65556L12.6778 4.89222C13.16 5.38222 14 5.04 14 4.34778Z" fill="#737D8C"/> <path d="M14 4.34778V0.777778C14 0.35 13.65 0 13.2222 0H9.65222C8.96 0 8.61 0.84 9.1 1.33L10.3367 2.56667L2.55889 10.3444L1.32222 9.10778C0.84 8.61778 0 8.96 0 9.65222V13.2222C0 13.65 0.35 14 0.777778 14H4.34778C5.04 14 5.39 13.16 4.9 12.67L3.66333 11.4333L11.4411 3.65556L12.6778 4.89222C13.16 5.38222 14 5.04 14 4.34778Z" fill="currentColor"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 444 B

After

Width:  |  Height:  |  Size: 449 B

View file

@ -1,3 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.7855 1.25024L10.4345 4.60126L11.5953 5.76207C12.0553 6.22202 11.7267 7.01049 11.077 7.01049H7.73326C7.33172 7.01049 7.00319 6.68196 7.00319 6.28042V2.9221C7.00319 2.27234 7.79166 1.94381 8.25161 2.40375L9.41242 3.56456L12.7634 0.213545C13.0482 -0.0711818 13.5081 -0.0711818 13.7928 0.213545C14.0703 0.505573 14.0703 0.965517 13.7855 1.25024ZM1.25024 13.7855L4.60126 10.4345L5.76207 11.5953C6.22202 12.0553 7.01049 11.7267 7.01049 11.077V7.73326C7.01049 7.33172 6.68196 7.00319 6.28042 7.00319H2.9221C2.27234 7.00319 1.94381 7.79166 2.40375 8.25161L3.56456 9.41242L0.213545 12.7634C-0.0711818 13.0482 -0.0711818 13.5081 0.213545 13.7928C0.505573 14.0703 0.965517 14.0703 1.25024 13.7855Z" fill="#737D8C"/> <path d="M13.7855 1.25024L10.4345 4.60126L11.5953 5.76207C12.0553 6.22202 11.7267 7.01049 11.077 7.01049H7.73326C7.33172 7.01049 7.00319 6.68196 7.00319 6.28042V2.9221C7.00319 2.27234 7.79166 1.94381 8.25161 2.40375L9.41242 3.56456L12.7634 0.213545C13.0482 -0.0711818 13.5081 -0.0711818 13.7928 0.213545C14.0703 0.505573 14.0703 0.965517 13.7855 1.25024ZM1.25024 13.7855L4.60126 10.4345L5.76207 11.5953C6.22202 12.0553 7.01049 11.7267 7.01049 11.077V7.73326C7.01049 7.33172 6.68196 7.00319 6.28042 7.00319H2.9221C2.27234 7.00319 1.94381 7.79166 2.40375 8.25161L3.56456 9.41242L0.213545 12.7634C-0.0711818 13.0482 -0.0711818 13.5081 0.213545 13.7928C0.505573 14.0703 0.965517 14.0703 1.25024 13.7855Z" fill="currentColor"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 821 B

After

Width:  |  Height:  |  Size: 826 B

View file

@ -1,5 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="15.5" cy="10" r="1.5" transform="rotate(180 15.5 10)" fill="#15191E"/> <circle cx="15.5" cy="10" r="1.5" transform="rotate(180 15.5 10)" fill="currentColor"/>
<circle cx="10" cy="10" r="1.5" transform="rotate(180 10 10)" fill="#15191E"/> <circle cx="10" cy="10" r="1.5" transform="rotate(180 10 10)" fill="currentColor"/>
<circle cx="4.5" cy="10" r="1.5" transform="rotate(180 4.5 10)" fill="#15191E"/> <circle cx="4.5" cy="10" r="1.5" transform="rotate(180 4.5 10)" fill="currentColor"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 373 B

View file

@ -2,6 +2,6 @@
<mask id="path-1-inside-1" fill="white"> <mask id="path-1-inside-1" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5911 20.2922C15.9951 21.3704 14.0711 22 12 22C9.74879 22 7.67132 21.2561 6 20.0007C3.5711 18.1763 2 15.2716 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 15.4518 20.2511 18.4951 17.5911 20.2922ZM12 12.5C13.6569 12.5 15 11.0449 15 9.25C15 7.45507 13.6569 6 12 6C10.3431 6 9 7.45507 9 9.25C9 11.0449 10.3431 12.5 12 12.5ZM12 20C14.162 20 16.1236 19.1424 17.5634 17.7488C16.673 15.5506 14.5176 14 12 14C9.48242 14 7.32699 15.5506 6.43662 17.7488C7.87635 19.1424 9.83802 20 12 20Z"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M17.5911 20.2922C15.9951 21.3704 14.0711 22 12 22C9.74879 22 7.67132 21.2561 6 20.0007C3.5711 18.1763 2 15.2716 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 15.4518 20.2511 18.4951 17.5911 20.2922ZM12 12.5C13.6569 12.5 15 11.0449 15 9.25C15 7.45507 13.6569 6 12 6C10.3431 6 9 7.45507 9 9.25C9 11.0449 10.3431 12.5 12 12.5ZM12 20C14.162 20 16.1236 19.1424 17.5634 17.7488C16.673 15.5506 14.5176 14 12 14C9.48242 14 7.32699 15.5506 6.43662 17.7488C7.87635 19.1424 9.83802 20 12 20Z"/>
</mask> </mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5911 20.2922C15.9951 21.3704 14.0711 22 12 22C9.74879 22 7.67132 21.2561 6 20.0007C3.5711 18.1763 2 15.2716 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 15.4518 20.2511 18.4951 17.5911 20.2922ZM12 12.5C13.6569 12.5 15 11.0449 15 9.25C15 7.45507 13.6569 6 12 6C10.3431 6 9 7.45507 9 9.25C9 11.0449 10.3431 12.5 12 12.5ZM12 20C14.162 20 16.1236 19.1424 17.5634 17.7488C16.673 15.5506 14.5176 14 12 14C9.48242 14 7.32699 15.5506 6.43662 17.7488C7.87635 19.1424 9.83802 20 12 20Z" fill="black"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M17.5911 20.2922C15.9951 21.3704 14.0711 22 12 22C9.74879 22 7.67132 21.2561 6 20.0007C3.5711 18.1763 2 15.2716 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 15.4518 20.2511 18.4951 17.5911 20.2922ZM12 12.5C13.6569 12.5 15 11.0449 15 9.25C15 7.45507 13.6569 6 12 6C10.3431 6 9 7.45507 9 9.25C9 11.0449 10.3431 12.5 12 12.5ZM12 20C14.162 20 16.1236 19.1424 17.5634 17.7488C16.673 15.5506 14.5176 14 12 14C9.48242 14 7.32699 15.5506 6.43662 17.7488C7.87635 19.1424 9.83802 20 12 20Z" fill="currentColor"/>
<path d="M17.5911 20.2922L16.4715 18.6349L17.5911 20.2922ZM6 20.0007L4.79885 21.5999L4.79885 21.5999L6 20.0007ZM17.5634 17.7488L18.9544 19.1859L19.9234 18.2479L19.4171 16.998L17.5634 17.7488ZM6.43662 17.7488L4.5829 16.998L4.07662 18.2479L5.04563 19.1859L6.43662 17.7488ZM12 24C14.4825 24 16.7945 23.244 18.7107 21.9494L16.4715 18.6349C15.1957 19.4968 13.6596 20 12 20V24ZM4.79885 21.5999C6.80462 23.1065 9.30085 24 12 24V20C10.1967 20 8.53802 19.4058 7.20115 18.4016L4.79885 21.5999ZM0 12C0 15.9273 1.88868 19.414 4.79885 21.5999L7.20115 18.4016C5.25353 16.9387 4 14.616 4 12H0ZM12 0C5.37258 0 0 5.37258 0 12H4C4 7.58172 7.58172 4 12 4V0ZM24 12C24 5.37258 18.6274 0 12 0V4C16.4183 4 20 7.58172 20 12H24ZM18.7107 21.9494C21.8977 19.7963 24 16.144 24 12H20C20 14.7596 18.6045 17.1939 16.4715 18.6349L18.7107 21.9494ZM13 9.25C13 10.0941 12.4046 10.5 12 10.5V14.5C14.9091 14.5 17 11.9958 17 9.25H13ZM12 8C12.4046 8 13 8.4059 13 9.25H17C17 6.50425 14.9091 4 12 4V8ZM11 9.25C11 8.4059 11.5954 8 12 8V4C9.09086 4 7 6.50425 7 9.25H11ZM12 10.5C11.5954 10.5 11 10.0941 11 9.25H7C7 11.9958 9.09086 14.5 12 14.5V10.5ZM16.1724 16.3118C15.0906 17.3588 13.6223 18 12 18V22C14.7017 22 17.1567 20.926 18.9544 19.1859L16.1724 16.3118ZM12 16C13.6752 16 15.1146 17.0305 15.7097 18.4996L19.4171 16.998C18.2314 14.0707 15.3599 12 12 12V16ZM8.29033 18.4996C8.88541 17.0305 10.3248 16 12 16V12C8.64008 12 5.76858 14.0707 4.5829 16.998L8.29033 18.4996ZM12 18C10.3777 18 8.90936 17.3588 7.82761 16.3118L5.04563 19.1859C6.84334 20.926 9.2983 22 12 22V18Z" fill="black" mask="url(#path-1-inside-1)"/> <path d="M17.5911 20.2922L16.4715 18.6349L17.5911 20.2922ZM6 20.0007L4.79885 21.5999L4.79885 21.5999L6 20.0007ZM17.5634 17.7488L18.9544 19.1859L19.9234 18.2479L19.4171 16.998L17.5634 17.7488ZM6.43662 17.7488L4.5829 16.998L4.07662 18.2479L5.04563 19.1859L6.43662 17.7488ZM12 24C14.4825 24 16.7945 23.244 18.7107 21.9494L16.4715 18.6349C15.1957 19.4968 13.6596 20 12 20V24ZM4.79885 21.5999C6.80462 23.1065 9.30085 24 12 24V20C10.1967 20 8.53802 19.4058 7.20115 18.4016L4.79885 21.5999ZM0 12C0 15.9273 1.88868 19.414 4.79885 21.5999L7.20115 18.4016C5.25353 16.9387 4 14.616 4 12H0ZM12 0C5.37258 0 0 5.37258 0 12H4C4 7.58172 7.58172 4 12 4V0ZM24 12C24 5.37258 18.6274 0 12 0V4C16.4183 4 20 7.58172 20 12H24ZM18.7107 21.9494C21.8977 19.7963 24 16.144 24 12H20C20 14.7596 18.6045 17.1939 16.4715 18.6349L18.7107 21.9494ZM13 9.25C13 10.0941 12.4046 10.5 12 10.5V14.5C14.9091 14.5 17 11.9958 17 9.25H13ZM12 8C12.4046 8 13 8.4059 13 9.25H17C17 6.50425 14.9091 4 12 4V8ZM11 9.25C11 8.4059 11.5954 8 12 8V4C9.09086 4 7 6.50425 7 9.25H11ZM12 10.5C11.5954 10.5 11 10.0941 11 9.25H7C7 11.9958 9.09086 14.5 12 14.5V10.5ZM16.1724 16.3118C15.0906 17.3588 13.6223 18 12 18V22C14.7017 22 17.1567 20.926 18.9544 19.1859L16.1724 16.3118ZM12 16C13.6752 16 15.1146 17.0305 15.7097 18.4996L19.4171 16.998C18.2314 14.0707 15.3599 12 12 12V16ZM8.29033 18.4996C8.88541 17.0305 10.3248 16 12 16V12C8.64008 12 5.76858 14.0707 4.5829 16.998L8.29033 18.4996ZM12 18C10.3777 18 8.90936 17.3588 7.82761 16.3118L5.04563 19.1859C6.84334 20.926 9.2983 22 12 22V18Z" fill="currentColor" mask="url(#path-1-inside-1)"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -1,3 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.41411 1.43218C8.59217 0.855939 9.40783 0.855941 9.58589 1.43218L11.1715 6.56319H16.3856C16.9721 6.56319 17.224 7.30764 16.7578 7.66373L12.5135 10.9061L14.1185 16.1001C14.2948 16.6705 13.6348 17.1309 13.1604 16.7684L9 13.5902L4.83965 16.7684C4.3652 17.1309 3.70521 16.6705 3.88148 16.1001L5.4865 10.9061L1.24216 7.66373C0.776033 7.30764 1.02785 6.56319 1.61443 6.56319H6.82854L8.41411 1.43218Z" fill="black"/> <path d="M8.41411 1.43218C8.59217 0.855939 9.40783 0.855941 9.58589 1.43218L11.1715 6.56319H16.3856C16.9721 6.56319 17.224 7.30764 16.7578 7.66373L12.5135 10.9061L14.1185 16.1001C14.2948 16.6705 13.6348 17.1309 13.1604 16.7684L9 13.5902L4.83965 16.7684C4.3652 17.1309 3.70521 16.6705 3.88148 16.1001L5.4865 10.9061L1.24216 7.66373C0.776033 7.30764 1.02785 6.56319 1.61443 6.56319H6.82854L8.41411 1.43218Z" fill="currentColor"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 524 B

After

Width:  |  Height:  |  Size: 531 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

@ -3,7 +3,7 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 14 12" style="enable-background:new 0 0 14 12;" xml:space="preserve"> viewBox="0 0 14 12" style="enable-background:new 0 0 14 12;" xml:space="preserve">
<style type="text/css"> <style type="text/css">
.st0{fill:none;stroke:#454545;stroke-linecap:round;stroke-linejoin:round;} .st0{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;}
</style> </style>
<g> <g>
<path class="st0" d="M6,1.5L1.2,9.3c-0.2,0.3-0.2,0.8,0,1.1c0.2,0.3,0.6,0.6,1,0.6h9.7c0.4,0,0.8-0.2,1-0.6c0.2-0.3,0.2-0.8,0-1.1 <path class="st0" d="M6,1.5L1.2,9.3c-0.2,0.3-0.2,0.8,0,1.1c0.2,0.3,0.6,0.6,1,0.6h9.7c0.4,0,0.8-0.2,1-0.6c0.2-0.3,0.2-0.8,0-1.1

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 709 B

View file

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 11 11"> <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 11 11">
<g fill="none" fill-rule="evenodd" stroke="#212121" stroke-linecap="round" stroke-linejoin="round"> <g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M8.5 6v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h3M7 1h3v3M4.5 6.5L10 1"/> <path d="M8.5 6v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h3M7 1h3v3M4.5 6.5L10 1"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 304 B

After

Width:  |  Height:  |  Size: 309 B

View file

@ -1,23 +1,17 @@
/* Nunito lacks combining diacritics, so these will fall through /* Nunito lacks combining diacritics, so these will fall through
to the next font. Helevetica's diacritics sometimes do not combine to the next font. Helevetica's diacritics sometimes do not combine
nicely (on OSX, at least) and result in a huge horizontal mess. nicely (on OSX, at least) and result in a huge horizontal mess.
Arial empirically gets it right, hence prioritising Arial here. Arial empirically gets it right, hence prioritising Arial here. */
We also include STIXGeneral explicitly to support a wider range
of combining diacritics (Chrome fails without it, as per
https://bugs.chromium.org/p/chromium/issues/detail?id=1328898).
We should never actively *prefer* STIXGeneral over the default font though,
since it looks pretty rough and implements some non-LGC scripts only
partially, making, for example, Japanese text look patchy and sad. */
/* We fall through to Twemoji for emoji rather than falling through /* We fall through to Twemoji for emoji rather than falling through
to native Emoji fonts (if any) to ensure cross-browser consistency */ to native Emoji fonts (if any) to ensure cross-browser consistency */
/* Noto Color Emoji contains digits, in fixed-width, therefore causing /* Noto Color Emoji contains digits, in fixed-width, therefore causing
digits in flowed text to stand out. digits in flowed text to stand out.
TODO: Consider putting all emoji fonts to the end rather than the front. */ TODO: Consider putting all emoji fonts to the end rather than the front. */
$font-family: "Nunito", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif, $font-family: "Nunito", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif,
"STIXGeneral", "Noto Color Emoji"; "Noto Color Emoji";
$monospace-font-family: "Inconsolata", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace, $monospace-font-family: "Inconsolata", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace,
"STIXGeneral", "Noto Color Emoji"; "Noto Color Emoji";
/* unified palette */ /* unified palette */
/* try to use these colors when possible */ /* try to use these colors when possible */

View file

@ -82,11 +82,11 @@ $roomtopic-color: $secondary-content;
background-color: $panel-actions !important; background-color: $panel-actions !important;
} }
.mx_ThemeChoicePanel > .mx_ThemeSelectors > .mx_StyledRadioButton input[type="radio"]:disabled + div { .mx_ThemeChoicePanel_themeSelectors > .mx_StyledRadioButton input[type="radio"]:disabled + div {
border-color: $primary-content; border-color: $primary-content;
} }
.mx_ThemeChoicePanel > .mx_ThemeSelectors > .mx_StyledRadioButton.mx_StyledRadioButton_disabled { .mx_ThemeChoicePanel_themeSelectors > .mx_StyledRadioButton.mx_StyledRadioButton_disabled {
color: $primary-content; color: $primary-content;
} }

View file

@ -1,23 +1,17 @@
/* Nunito and Inter lacks combining diacritics, so these will fall through /* Nunito and Inter lacks combining diacritics, so these will fall through
to the next font. Helevetica's diacritics sometimes do not combine to the next font. Helevetica's diacritics sometimes do not combine
nicely (on OSX, at least) and result in a huge horizontal mess. nicely (on OSX, at least) and result in a huge horizontal mess.
Arial empirically gets it right, hence prioritising Arial here. Arial empirically gets it right, hence prioritising Arial here. */
We also include STIXGeneral explicitly to support a wider range
of combining diacritics (Chrome fails without it, as per
https://bugs.chromium.org/p/chromium/issues/detail?id=1328898).
We should never actively *prefer* STIXGeneral over the default font though,
since it looks pretty rough and implements some non-LGC scripts only
partially, making, for example, Japanese text look patchy and sad. */
/* We fall through to Twemoji for emoji rather than falling through /* We fall through to Twemoji for emoji rather than falling through
to native Emoji fonts (if any) to ensure cross-browser consistency */ to native Emoji fonts (if any) to ensure cross-browser consistency */
/* Noto Color Emoji contains digits, in fixed-width, therefore causing /* Noto Color Emoji contains digits, in fixed-width, therefore causing
digits in flowed text to stand out. digits in flowed text to stand out.
TODO: Consider putting all emoji fonts to the end rather than the front. */ TODO: Consider putting all emoji fonts to the end rather than the front. */
$font-family: "Inter", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif, "STIXGeneral", $font-family: "Inter", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif,
"Noto Color Emoji"; "Noto Color Emoji";
$monospace-font-family: "Inconsolata", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace, $monospace-font-family: "Inconsolata", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace,
"STIXGeneral", "Noto Color Emoji"; "Noto Color Emoji";
/* Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A120 */ /* Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=559%3A120 */
/* ******************** */ /* ******************** */

View file

@ -10,8 +10,15 @@ defbranch="$3"
rm -r "$defrepo" || true rm -r "$defrepo" || true
PR_ORG=${PR_ORG:-"matrix-org"} # figure out where to look for pull requests:
PR_REPO=${PR_REPO:-"matrix-react-sdk"} # - We may have been told an explicit repo via the PR_ORG/PR_REPO/PR_NUMBER env vars
# - otherwise, check the $GITHUB_ env vars which are set by Github Actions
# - failing that, fall back to the matrix-org/matrix-react-sdk repo.
#
# in ether case, the PR_NUMBER variable must be set explicitly.
default_org_repo=${GITHUB_REPOSITORY:-"matrix-org/matrix-react-sdk"}
PR_ORG=${PR_ORG:-${default_org_repo%%/*}}
PR_REPO=${PR_REPO:-${default_org_repo##*/}}
# A function that clones a branch of a repo based on the org, repo and branch # A function that clones a branch of a repo based on the org, repo and branch
clone() { clone() {

View file

@ -16,18 +16,17 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { IAuthData, IRequestMsisdnTokenResponse, IRequestTokenResponse } from "matrix-js-sdk/src/matrix"; import { IAuthData, IRequestMsisdnTokenResponse, IRequestTokenResponse, MatrixClient } from "matrix-js-sdk/src/matrix";
import { MatrixError, HTTPError } from "matrix-js-sdk/src/matrix"; import { MatrixError, HTTPError } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "./MatrixClientPeg";
import Modal from "./Modal"; import Modal from "./Modal";
import { _t, UserFriendlyError } from "./languageHandler"; import { _t, UserFriendlyError } from "./languageHandler";
import IdentityAuthClient from "./IdentityAuthClient"; import IdentityAuthClient from "./IdentityAuthClient";
import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents"; import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents";
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog"; import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
function getIdServerDomain(): string { function getIdServerDomain(matrixClient: MatrixClient): string {
const idBaseUrl = MatrixClientPeg.get().getIdentityServerUrl(true); const idBaseUrl = matrixClient.getIdentityServerUrl(true);
if (!idBaseUrl) { if (!idBaseUrl) {
throw new UserFriendlyError("Identity server not set"); throw new UserFriendlyError("Identity server not set");
} }
@ -55,11 +54,11 @@ export type Binding = {
export default class AddThreepid { export default class AddThreepid {
private sessionId: string; private sessionId: string;
private submitUrl?: string; private submitUrl?: string;
private clientSecret: string; private bind = false;
private bind: boolean; private readonly clientSecret: string;
public constructor() { public constructor(private readonly matrixClient: MatrixClient) {
this.clientSecret = MatrixClientPeg.get().generateClientSecret(); this.clientSecret = matrixClient.generateClientSecret();
} }
/** /**
@ -70,7 +69,7 @@ export default class AddThreepid {
*/ */
public async addEmailAddress(emailAddress: string): Promise<IRequestTokenResponse> { public async addEmailAddress(emailAddress: string): Promise<IRequestTokenResponse> {
try { try {
const res = await MatrixClientPeg.get().requestAdd3pidEmailToken(emailAddress, this.clientSecret, 1); const res = await this.matrixClient.requestAdd3pidEmailToken(emailAddress, this.clientSecret, 1);
this.sessionId = res.sid; this.sessionId = res.sid;
return res; return res;
} catch (err) { } catch (err) {
@ -90,12 +89,12 @@ export default class AddThreepid {
*/ */
public async bindEmailAddress(emailAddress: string): Promise<IRequestTokenResponse> { public async bindEmailAddress(emailAddress: string): Promise<IRequestTokenResponse> {
this.bind = true; this.bind = true;
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { if (await this.matrixClient.doesServerSupportSeparateAddAndBind()) {
// For separate bind, request a token directly from the IS. // For separate bind, request a token directly from the IS.
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
const identityAccessToken = (await authClient.getAccessToken()) ?? undefined; const identityAccessToken = (await authClient.getAccessToken()) ?? undefined;
try { try {
const res = await MatrixClientPeg.get().requestEmailToken( const res = await this.matrixClient.requestEmailToken(
emailAddress, emailAddress,
this.clientSecret, this.clientSecret,
1, 1,
@ -126,7 +125,7 @@ export default class AddThreepid {
*/ */
public async addMsisdn(phoneCountry: string, phoneNumber: string): Promise<IRequestMsisdnTokenResponse> { public async addMsisdn(phoneCountry: string, phoneNumber: string): Promise<IRequestMsisdnTokenResponse> {
try { try {
const res = await MatrixClientPeg.get().requestAdd3pidMsisdnToken( const res = await this.matrixClient.requestAdd3pidMsisdnToken(
phoneCountry, phoneCountry,
phoneNumber, phoneNumber,
this.clientSecret, this.clientSecret,
@ -153,12 +152,12 @@ export default class AddThreepid {
*/ */
public async bindMsisdn(phoneCountry: string, phoneNumber: string): Promise<IRequestMsisdnTokenResponse> { public async bindMsisdn(phoneCountry: string, phoneNumber: string): Promise<IRequestMsisdnTokenResponse> {
this.bind = true; this.bind = true;
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { if (await this.matrixClient.doesServerSupportSeparateAddAndBind()) {
// For separate bind, request a token directly from the IS. // For separate bind, request a token directly from the IS.
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
const identityAccessToken = (await authClient.getAccessToken()) ?? undefined; const identityAccessToken = (await authClient.getAccessToken()) ?? undefined;
try { try {
const res = await MatrixClientPeg.get().requestMsisdnToken( const res = await this.matrixClient.requestMsisdnToken(
phoneCountry, phoneCountry,
phoneNumber, phoneNumber,
this.clientSecret, this.clientSecret,
@ -189,17 +188,17 @@ export default class AddThreepid {
*/ */
public async checkEmailLinkClicked(): Promise<[success?: boolean, result?: IAuthData | Error | null]> { public async checkEmailLinkClicked(): Promise<[success?: boolean, result?: IAuthData | Error | null]> {
try { try {
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { if (await this.matrixClient.doesServerSupportSeparateAddAndBind()) {
if (this.bind) { if (this.bind) {
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken(); const identityAccessToken = await authClient.getAccessToken();
if (!identityAccessToken) { if (!identityAccessToken) {
throw new UserFriendlyError("No identity access token found"); throw new UserFriendlyError("No identity access token found");
} }
await MatrixClientPeg.get().bindThreePid({ await this.matrixClient.bindThreePid({
sid: this.sessionId, sid: this.sessionId,
client_secret: this.clientSecret, client_secret: this.clientSecret,
id_server: getIdServerDomain(), id_server: getIdServerDomain(this.matrixClient),
id_access_token: identityAccessToken, id_access_token: identityAccessToken,
}); });
} else { } else {
@ -233,7 +232,7 @@ export default class AddThreepid {
}; };
const { finished } = Modal.createDialog(InteractiveAuthDialog, { const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("Add Email Address"), title: _t("Add Email Address"),
matrixClient: MatrixClientPeg.get(), matrixClient: this.matrixClient,
authData: err.data, authData: err.data,
makeRequest: this.makeAddThreepidOnlyRequest, makeRequest: this.makeAddThreepidOnlyRequest,
aestheticsForStagePhases: { aestheticsForStagePhases: {
@ -245,11 +244,11 @@ export default class AddThreepid {
} }
} }
} else { } else {
await MatrixClientPeg.get().addThreePid( await this.matrixClient.addThreePid(
{ {
sid: this.sessionId, sid: this.sessionId,
client_secret: this.clientSecret, client_secret: this.clientSecret,
id_server: getIdServerDomain(), id_server: getIdServerDomain(this.matrixClient),
}, },
this.bind, this.bind,
); );
@ -272,7 +271,7 @@ export default class AddThreepid {
* @return {Promise<Object>} Response from /3pid/add call (in current spec, an empty object) * @return {Promise<Object>} Response from /3pid/add call (in current spec, an empty object)
*/ */
private makeAddThreepidOnlyRequest = (auth?: { type: string; session?: string }): Promise<{}> => { private makeAddThreepidOnlyRequest = (auth?: { type: string; session?: string }): Promise<{}> => {
return MatrixClientPeg.get().addThreePidOnly({ return this.matrixClient.addThreePidOnly({
sid: this.sessionId, sid: this.sessionId,
client_secret: this.clientSecret, client_secret: this.clientSecret,
auth, auth,
@ -291,18 +290,18 @@ export default class AddThreepid {
msisdnToken: string, msisdnToken: string,
): Promise<[success?: boolean, result?: IAuthData | Error | null] | undefined> { ): Promise<[success?: boolean, result?: IAuthData | Error | null] | undefined> {
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
const supportsSeparateAddAndBind = await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind(); const supportsSeparateAddAndBind = await this.matrixClient.doesServerSupportSeparateAddAndBind();
let result: { success: boolean } | MatrixError; let result: { success: boolean } | MatrixError;
if (this.submitUrl) { if (this.submitUrl) {
result = await MatrixClientPeg.get().submitMsisdnTokenOtherUrl( result = await this.matrixClient.submitMsisdnTokenOtherUrl(
this.submitUrl, this.submitUrl,
this.sessionId, this.sessionId,
this.clientSecret, this.clientSecret,
msisdnToken, msisdnToken,
); );
} else if (this.bind || !supportsSeparateAddAndBind) { } else if (this.bind || !supportsSeparateAddAndBind) {
result = await MatrixClientPeg.get().submitMsisdnToken( result = await this.matrixClient.submitMsisdnToken(
this.sessionId, this.sessionId,
this.clientSecret, this.clientSecret,
msisdnToken, msisdnToken,
@ -317,10 +316,10 @@ export default class AddThreepid {
if (supportsSeparateAddAndBind) { if (supportsSeparateAddAndBind) {
if (this.bind) { if (this.bind) {
await MatrixClientPeg.get().bindThreePid({ await this.matrixClient.bindThreePid({
sid: this.sessionId, sid: this.sessionId,
client_secret: this.clientSecret, client_secret: this.clientSecret,
id_server: getIdServerDomain(), id_server: getIdServerDomain(this.matrixClient),
id_access_token: await authClient.getAccessToken(), id_access_token: await authClient.getAccessToken(),
}); });
} else { } else {
@ -354,7 +353,7 @@ export default class AddThreepid {
}; };
const { finished } = Modal.createDialog(InteractiveAuthDialog, { const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("Add Phone Number"), title: _t("Add Phone Number"),
matrixClient: MatrixClientPeg.get(), matrixClient: this.matrixClient,
authData: err.data, authData: err.data,
makeRequest: this.makeAddThreepidOnlyRequest, makeRequest: this.makeAddThreepidOnlyRequest,
aestheticsForStagePhases: { aestheticsForStagePhases: {
@ -366,11 +365,11 @@ export default class AddThreepid {
} }
} }
} else { } else {
await MatrixClientPeg.get().addThreePid( await this.matrixClient.addThreePid(
{ {
sid: this.sessionId, sid: this.sessionId,
client_secret: this.clientSecret, client_secret: this.clientSecret,
id_server: getIdServerDomain(), id_server: getIdServerDomain(this.matrixClient),
}, },
this.bind, this.bind,
); );

View file

@ -18,11 +18,11 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { User } from "matrix-js-sdk/src/models/user"; import { User } from "matrix-js-sdk/src/models/user";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
import { split } from "lodash";
import DMRoomMap from "./utils/DMRoomMap"; import DMRoomMap from "./utils/DMRoomMap";
import { mediaFromMxc } from "./customisations/Media"; import { mediaFromMxc } from "./customisations/Media";
import { isLocalRoom } from "./utils/localRoom/isLocalRoom"; import { isLocalRoom } from "./utils/localRoom/isLocalRoom";
import { getFirstGrapheme } from "./utils/strings";
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already // Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember( export function avatarUrlForMember(
@ -133,8 +133,7 @@ export function getInitialLetter(name: string): string | undefined {
name = name.substring(1); name = name.substring(1);
} }
// rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis return getFirstGrapheme(name).toUpperCase();
return split(name, "", 1)[0].toUpperCase();
} }
export function avatarUrlForRoom( export function avatarUrlForRoom(

View file

@ -436,15 +436,18 @@ export default class ContentMessages {
} }
} }
promBefore = doMaybeLocalRoomAction(roomId, (actualRoomId) => promBefore = doMaybeLocalRoomAction(
this.sendContentToRoom( roomId,
file, (actualRoomId) =>
actualRoomId, this.sendContentToRoom(
relation, file,
matrixClient, actualRoomId,
replyToEvent ?? undefined, relation,
loopPromiseBefore, matrixClient,
), replyToEvent ?? undefined,
loopPromiseBefore,
),
matrixClient,
); );
} }
@ -580,13 +583,13 @@ export default class ContentMessages {
} catch (error) { } catch (error) {
// 413: File was too big or upset the server in some way: // 413: File was too big or upset the server in some way:
// clear the media size limit so we fetch it again next time we try to upload // clear the media size limit so we fetch it again next time we try to upload
if (error?.httpStatus === 413) { if (error instanceof HTTPError && error.httpStatus === 413) {
this.mediaConfig = null; this.mediaConfig = null;
} }
if (!upload.cancelled) { if (!upload.cancelled) {
let desc = _t("The file '%(fileName)s' failed to upload.", { fileName: upload.fileName }); let desc = _t("The file '%(fileName)s' failed to upload.", { fileName: upload.fileName });
if (error.httpStatus === 413) { if (error instanceof HTTPError && error.httpStatus === 413) {
desc = _t("The file '%(fileName)s' exceeds this homeserver's size limit for uploads", { desc = _t("The file '%(fileName)s' exceeds this homeserver's size limit for uploads", {
fileName: upload.fileName, fileName: upload.fileName,
}); });

View file

@ -17,11 +17,10 @@ limitations under the License.
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { ClientEvent, EventType, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { ClientEvent, EventType, MatrixClient, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { SyncState } from "matrix-js-sdk/src/sync"; import { SyncState } from "matrix-js-sdk/src/sync";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { MatrixClientPeg } from "./MatrixClientPeg";
import dis from "./dispatcher/dispatcher"; import dis from "./dispatcher/dispatcher";
import { import {
hideToast as hideBulkUnverifiedSessionsToast, hideToast as hideBulkUnverifiedSessionsToast,
@ -67,6 +66,8 @@ export default class DeviceListener {
// The set of device IDs we're currently displaying toasts for // The set of device IDs we're currently displaying toasts for
private displayingToastsForDeviceIds = new Set<string>(); private displayingToastsForDeviceIds = new Set<string>();
private running = false; private running = false;
// The client with which the instance is running. Only set if `running` is true, otherwise undefined.
private client?: MatrixClient;
private shouldRecordClientInformation = false; private shouldRecordClientInformation = false;
private enableBulkUnverifiedSessionsReminder = true; private enableBulkUnverifiedSessionsReminder = true;
private deviceClientInformationSettingWatcherRef: string | undefined; private deviceClientInformationSettingWatcherRef: string | undefined;
@ -76,16 +77,17 @@ export default class DeviceListener {
return window.mxDeviceListener; return window.mxDeviceListener;
} }
public start(): void { public start(matrixClient: MatrixClient): void {
this.running = true; this.running = true;
MatrixClientPeg.get().on(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices); this.client = matrixClient;
MatrixClientPeg.get().on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); this.client.on(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices);
MatrixClientPeg.get().on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged); this.client.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
MatrixClientPeg.get().on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged); this.client.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
MatrixClientPeg.get().on(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged); this.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
MatrixClientPeg.get().on(ClientEvent.AccountData, this.onAccountData); this.client.on(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged);
MatrixClientPeg.get().on(ClientEvent.Sync, this.onSync); this.client.on(ClientEvent.AccountData, this.onAccountData);
MatrixClientPeg.get().on(RoomStateEvent.Events, this.onRoomStateEvents); this.client.on(ClientEvent.Sync, this.onSync);
this.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
this.shouldRecordClientInformation = SettingsStore.getValue("deviceClientInformationOptIn"); this.shouldRecordClientInformation = SettingsStore.getValue("deviceClientInformationOptIn");
// only configurable in config, so we don't need to watch the value // only configurable in config, so we don't need to watch the value
this.enableBulkUnverifiedSessionsReminder = SettingsStore.getValue(UIFeature.BulkUnverifiedSessionsReminder); this.enableBulkUnverifiedSessionsReminder = SettingsStore.getValue(UIFeature.BulkUnverifiedSessionsReminder);
@ -101,18 +103,15 @@ export default class DeviceListener {
public stop(): void { public stop(): void {
this.running = false; this.running = false;
if (MatrixClientPeg.get()) { if (this.client) {
MatrixClientPeg.get().removeListener(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices); this.client.removeListener(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices);
MatrixClientPeg.get().removeListener(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); this.client.removeListener(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
MatrixClientPeg.get().removeListener( this.client.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
CryptoEvent.DeviceVerificationChanged, this.client.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
this.onDeviceVerificationChanged, this.client.removeListener(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged);
); this.client.removeListener(ClientEvent.AccountData, this.onAccountData);
MatrixClientPeg.get().removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged); this.client.removeListener(ClientEvent.Sync, this.onSync);
MatrixClientPeg.get().removeListener(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged); this.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
MatrixClientPeg.get().removeListener(ClientEvent.AccountData, this.onAccountData);
MatrixClientPeg.get().removeListener(ClientEvent.Sync, this.onSync);
MatrixClientPeg.get().removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
} }
if (this.deviceClientInformationSettingWatcherRef) { if (this.deviceClientInformationSettingWatcherRef) {
SettingsStore.unwatchSetting(this.deviceClientInformationSettingWatcherRef); SettingsStore.unwatchSetting(this.deviceClientInformationSettingWatcherRef);
@ -128,6 +127,7 @@ export default class DeviceListener {
this.keyBackupStatusChecked = false; this.keyBackupStatusChecked = false;
this.ourDeviceIdsAtStart = null; this.ourDeviceIdsAtStart = null;
this.displayingToastsForDeviceIds = new Set(); this.displayingToastsForDeviceIds = new Set();
this.client = undefined;
} }
/** /**
@ -160,22 +160,23 @@ export default class DeviceListener {
* @returns the set of device IDs * @returns the set of device IDs
*/ */
private async getDeviceIds(): Promise<Set<string>> { private async getDeviceIds(): Promise<Set<string>> {
const cli = MatrixClientPeg.get(); const cli = this.client;
const crypto = cli.getCrypto(); const crypto = cli?.getCrypto();
if (crypto === undefined) return new Set(); if (crypto === undefined) return new Set();
const userId = cli.getSafeUserId(); const userId = cli!.getSafeUserId();
const devices = await crypto.getUserDeviceInfo([userId]); const devices = await crypto.getUserDeviceInfo([userId]);
return new Set(devices.get(userId)?.keys() ?? []); return new Set(devices.get(userId)?.keys() ?? []);
} }
private onWillUpdateDevices = async (users: string[], initialFetch?: boolean): Promise<void> => { private onWillUpdateDevices = async (users: string[], initialFetch?: boolean): Promise<void> => {
if (!this.client) return;
// If we didn't know about *any* devices before (ie. it's fresh login), // If we didn't know about *any* devices before (ie. it's fresh login),
// then they are all pre-existing devices, so ignore this and set the // then they are all pre-existing devices, so ignore this and set the
// devicesAtStart list to the devices that we see after the fetch. // devicesAtStart list to the devices that we see after the fetch.
if (initialFetch) return; if (initialFetch) return;
const myUserId = MatrixClientPeg.get().getUserId()!; const myUserId = this.client.getSafeUserId();
if (users.includes(myUserId)) await this.ensureDeviceIdsAtStartPopulated(); if (users.includes(myUserId)) await this.ensureDeviceIdsAtStartPopulated();
// No need to do a recheck here: we just need to get a snapshot of our devices // No need to do a recheck here: we just need to get a snapshot of our devices
@ -183,17 +184,20 @@ export default class DeviceListener {
}; };
private onDevicesUpdated = (users: string[]): void => { private onDevicesUpdated = (users: string[]): void => {
if (!users.includes(MatrixClientPeg.get().getUserId()!)) return; if (!this.client) return;
if (!users.includes(this.client.getSafeUserId())) return;
this.recheck(); this.recheck();
}; };
private onDeviceVerificationChanged = (userId: string): void => { private onDeviceVerificationChanged = (userId: string): void => {
if (userId !== MatrixClientPeg.get().getUserId()) return; if (!this.client) return;
if (userId !== this.client.getUserId()) return;
this.recheck(); this.recheck();
}; };
private onUserTrustStatusChanged = (userId: string): void => { private onUserTrustStatusChanged = (userId: string): void => {
if (userId !== MatrixClientPeg.get().getUserId()) return; if (!this.client) return;
if (userId !== this.client.getUserId()) return;
this.recheck(); this.recheck();
}; };
@ -239,13 +243,14 @@ export default class DeviceListener {
// The server doesn't tell us when key backup is set up, so we poll // The server doesn't tell us when key backup is set up, so we poll
// & cache the result // & cache the result
private async getKeyBackupInfo(): Promise<IKeyBackupInfo | null> { private async getKeyBackupInfo(): Promise<IKeyBackupInfo | null> {
if (!this.client) return null;
const now = new Date().getTime(); const now = new Date().getTime();
if ( if (
!this.keyBackupInfo || !this.keyBackupInfo ||
!this.keyBackupFetchedAt || !this.keyBackupFetchedAt ||
this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL
) { ) {
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); this.keyBackupInfo = await this.client.getKeyBackupVersion();
this.keyBackupFetchedAt = now; this.keyBackupFetchedAt = now;
} }
return this.keyBackupInfo; return this.keyBackupInfo;
@ -256,13 +261,13 @@ export default class DeviceListener {
// modifying the state involved here, so don't add new toasts to setup. // modifying the state involved here, so don't add new toasts to setup.
if (isSecretStorageBeingAccessed()) return false; if (isSecretStorageBeingAccessed()) return false;
// Show setup toasts once the user is in at least one encrypted room. // Show setup toasts once the user is in at least one encrypted room.
const cli = MatrixClientPeg.get(); const cli = this.client;
return cli && cli.getRooms().some((r) => cli.isRoomEncrypted(r.roomId)); return cli?.getRooms().some((r) => cli.isRoomEncrypted(r.roomId)) ?? false;
} }
private async recheck(): Promise<void> { private async recheck(): Promise<void> {
if (!this.running) return; // we have been stopped if (!this.running || !this.client) return; // we have been stopped
const cli = MatrixClientPeg.get(); const cli = this.client;
// cross-signing support was added to Matrix in MSC1756, which landed in spec v1.1 // cross-signing support was added to Matrix in MSC1756, which landed in spec v1.1
if (!(await cli.isVersionSupported("v1.1"))) return; if (!(await cli.isVersionSupported("v1.1"))) return;
@ -285,11 +290,11 @@ export default class DeviceListener {
this.checkKeyBackupStatus(); this.checkKeyBackupStatus();
} else if (this.shouldShowSetupEncryptionToast()) { } else if (this.shouldShowSetupEncryptionToast()) {
// make sure our keys are finished downloading // make sure our keys are finished downloading
await crypto.getUserDeviceInfo([cli.getUserId()!]); await crypto.getUserDeviceInfo([cli.getSafeUserId()]);
// cross signing isn't enabled - nag to enable it // cross signing isn't enabled - nag to enable it
// There are 3 different toasts for: // There are 3 different toasts for:
if (!(await crypto.getCrossSigningKeyId()) && cli.getStoredCrossSigningForUser(cli.getUserId()!)) { if (!(await crypto.getCrossSigningKeyId()) && cli.getStoredCrossSigningForUser(cli.getSafeUserId())) {
// Cross-signing on account but this device doesn't trust the master key (verify this session) // Cross-signing on account but this device doesn't trust the master key (verify this session)
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
this.checkKeyBackupStatus(); this.checkKeyBackupStatus();
@ -301,7 +306,7 @@ export default class DeviceListener {
} else { } else {
// No cross-signing or key backup on account (set up encryption) // No cross-signing or key backup on account (set up encryption)
await cli.waitForClientWellKnown(); await cli.waitForClientWellKnown();
if (isSecureBackupRequired() && isLoggedIn()) { if (isSecureBackupRequired(cli) && isLoggedIn()) {
// If we're meant to set up, and Secure Backup is required, // If we're meant to set up, and Secure Backup is required,
// trigger the flow directly without a toast once logged in. // trigger the flow directly without a toast once logged in.
hideSetupEncryptionToast(); hideSetupEncryptionToast();
@ -327,7 +332,9 @@ export default class DeviceListener {
const isCurrentDeviceTrusted = const isCurrentDeviceTrusted =
crossSigningReady && crossSigningReady &&
Boolean((await crypto.getDeviceVerificationStatus(cli.getUserId()!, cli.deviceId!))?.crossSigningVerified); Boolean(
(await crypto.getDeviceVerificationStatus(cli.getSafeUserId(), cli.deviceId!))?.crossSigningVerified,
);
// as long as cross-signing isn't ready, // as long as cross-signing isn't ready,
// you can't see or dismiss any device toasts // you can't see or dismiss any device toasts
@ -336,7 +343,7 @@ export default class DeviceListener {
for (const deviceId of devices) { for (const deviceId of devices) {
if (deviceId === cli.deviceId) continue; if (deviceId === cli.deviceId) continue;
const deviceTrust = await crypto.getDeviceVerificationStatus(cli.getUserId()!, deviceId); const deviceTrust = await crypto.getDeviceVerificationStatus(cli.getSafeUserId(), deviceId);
if (!deviceTrust?.crossSigningVerified && !this.dismissed.has(deviceId)) { if (!deviceTrust?.crossSigningVerified && !this.dismissed.has(deviceId)) {
if (this.ourDeviceIdsAtStart?.has(deviceId)) { if (this.ourDeviceIdsAtStart?.has(deviceId)) {
oldUnverifiedDeviceIds.add(deviceId); oldUnverifiedDeviceIds.add(deviceId);
@ -383,11 +390,11 @@ export default class DeviceListener {
} }
private checkKeyBackupStatus = async (): Promise<void> => { private checkKeyBackupStatus = async (): Promise<void> => {
if (this.keyBackupStatusChecked) { if (this.keyBackupStatusChecked || !this.client) {
return; return;
} }
// returns null when key backup status hasn't finished being checked // returns null when key backup status hasn't finished being checked
const isKeyBackupEnabled = MatrixClientPeg.get().getKeyBackupEnabled(); const isKeyBackupEnabled = this.client.getKeyBackupEnabled();
this.keyBackupStatusChecked = isKeyBackupEnabled !== null; this.keyBackupStatusChecked = isKeyBackupEnabled !== null;
if (isKeyBackupEnabled === false) { if (isKeyBackupEnabled === false) {
@ -412,11 +419,12 @@ export default class DeviceListener {
}; };
private updateClientInformation = async (): Promise<void> => { private updateClientInformation = async (): Promise<void> => {
if (!this.client) return;
try { try {
if (this.shouldRecordClientInformation) { if (this.shouldRecordClientInformation) {
await recordClientInformation(MatrixClientPeg.get(), SdkConfig.get(), PlatformPeg.get() ?? undefined); await recordClientInformation(this.client, SdkConfig.get(), PlatformPeg.get() ?? undefined);
} else { } else {
await removeClientInformation(MatrixClientPeg.get()); await removeClientInformation(this.client);
} }
} catch (error) { } catch (error) {
// this is a best effort operation // this is a best effort operation

View file

@ -19,16 +19,16 @@ limitations under the License.
import React, { LegacyRef, ReactElement, ReactNode } from "react"; import React, { LegacyRef, ReactElement, ReactNode } from "react";
import sanitizeHtml from "sanitize-html"; import sanitizeHtml from "sanitize-html";
import { load as cheerio } from "cheerio";
import classNames from "classnames"; import classNames from "classnames";
import EMOJIBASE_REGEX from "emojibase-regex"; import EMOJIBASE_REGEX from "emojibase-regex";
import { merge, split } from "lodash"; import { merge } from "lodash";
import katex from "katex"; import katex from "katex";
import { decode } from "html-entities"; import { decode } from "html-entities";
import { IContent } from "matrix-js-sdk/src/models/event"; import { IContent } from "matrix-js-sdk/src/models/event";
import { Optional } from "matrix-events-sdk"; import { Optional } from "matrix-events-sdk";
import _Linkify from "linkify-react"; import _Linkify from "linkify-react";
import escapeHtml from "escape-html"; import escapeHtml from "escape-html";
import GraphemeSplitter from "grapheme-splitter";
import { import {
_linkifyElement, _linkifyElement,
@ -464,14 +464,18 @@ const emojiToJsxSpan = (emoji: string, key: number): JSX.Element => (
* @returns if isHtmlMessage is true, returns an array of strings, otherwise return an array of React Elements for emojis * @returns if isHtmlMessage is true, returns an array of strings, otherwise return an array of React Elements for emojis
* and plain text for everything else * and plain text for everything else
*/ */
function formatEmojis(message: string | undefined, isHtmlMessage: boolean): (JSX.Element | string)[] { export function formatEmojis(message: string | undefined, isHtmlMessage?: false): JSX.Element[];
export function formatEmojis(message: string | undefined, isHtmlMessage: true): string[];
export function formatEmojis(message: string | undefined, isHtmlMessage: boolean): (JSX.Element | string)[] {
const emojiToSpan = isHtmlMessage ? emojiToHtmlSpan : emojiToJsxSpan; const emojiToSpan = isHtmlMessage ? emojiToHtmlSpan : emojiToJsxSpan;
const result: (JSX.Element | string)[] = []; const result: (JSX.Element | string)[] = [];
if (!message) return result;
let text = ""; let text = "";
let key = 0; let key = 0;
// We use lodash's grapheme splitter to avoid breaking apart compound emojis const splitter = new GraphemeSplitter();
for (const char of split(message, "")) { for (const char of splitter.iterateGraphemes(message)) {
if (EMOJIBASE_REGEX.test(char)) { if (EMOJIBASE_REGEX.test(char)) {
if (text) { if (text) {
result.push(text); result.push(text);
@ -549,30 +553,19 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
} }
safeBody = sanitizeHtml(formattedBody!, sanitizeParams); safeBody = sanitizeHtml(formattedBody!, sanitizeParams);
const phtml = cheerio(safeBody, { const phtml = new DOMParser().parseFromString(safeBody, "text/html");
// @ts-ignore: The `_useHtmlParser2` internal option is the const isPlainText = phtml.body.innerHTML === phtml.body.textContent;
// simplest way to both parse and render using `htmlparser2`.
_useHtmlParser2: true,
decodeEntities: false,
});
const isPlainText = phtml.html() === phtml.root().text();
isHtmlMessage = !isPlainText; isHtmlMessage = !isPlainText;
if (isHtmlMessage && SettingsStore.getValue("feature_latex_maths")) { if (isHtmlMessage && SettingsStore.getValue("feature_latex_maths")) {
// @ts-ignore - The types for `replaceWith` wrongly expect [...phtml.querySelectorAll<HTMLElement>("div, span[data-mx-maths]")].forEach((e) => {
// Cheerio instance to be returned. e.outerHTML = katex.renderToString(decode(e.getAttribute("data-mx-maths")), {
phtml('div, span[data-mx-maths!=""]').replaceWith(function (i, e) {
return katex.renderToString(decode(phtml(e).attr("data-mx-maths")), {
throwOnError: false, throwOnError: false,
// @ts-ignore - `e` can be an Element, not just a Node displayMode: e.tagName == "DIV",
displayMode: e.name == "div",
output: "htmlAndMathml", output: "htmlAndMathml",
}); });
}); });
safeBody = phtml.html(); safeBody = phtml.body.innerHTML;
}
if (bodyHasEmoji) {
safeBody = formatEmojis(safeBody, true).join("");
} }
} else if (highlighter) { } else if (highlighter) {
safeBody = highlighter.applyHighlights(escapeHtml(plainBody), safeHighlights!).join(""); safeBody = highlighter.applyHighlights(escapeHtml(plainBody), safeHighlights!).join("");
@ -581,13 +574,9 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
delete sanitizeParams.textFilter; delete sanitizeParams.textFilter;
} }
const contentBody = safeBody ?? strippedBody;
if (opts.returnString) {
return contentBody;
}
let emojiBody = false; let emojiBody = false;
if (!opts.disableBigEmoji && bodyHasEmoji) { if (!opts.disableBigEmoji && bodyHasEmoji) {
const contentBody = safeBody ?? strippedBody;
let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : ""; let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : "";
// Remove zero width joiner, zero width spaces and other spaces in body // Remove zero width joiner, zero width spaces and other spaces in body
@ -607,6 +596,15 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
(!content.formatted_body.includes("http:") && !content.formatted_body.includes("https:"))); (!content.formatted_body.includes("http:") && !content.formatted_body.includes("https:")));
} }
if (isFormattedBody && bodyHasEmoji && safeBody) {
// This has to be done after the emojiBody check above as to not break big emoji on replies
safeBody = formatEmojis(safeBody, true).join("");
}
if (opts.returnString) {
return safeBody ?? strippedBody;
}
const className = classNames({ const className = classNames({
"mx_EventTile_body": true, "mx_EventTile_body": true,
"mx_EventTile_bigEmoji": emojiBody, "mx_EventTile_bigEmoji": emojiBody,
@ -668,7 +666,7 @@ export function topicToHtml(
isFormattedTopic = false; // Fall back to plain-text topic isFormattedTopic = false; // Fall back to plain-text topic
} }
let emojiBodyElements: ReturnType<typeof formatEmojis> | undefined; let emojiBodyElements: JSX.Element[] | undefined;
if (!isFormattedTopic && topicHasEmoji) { if (!isFormattedTopic && topicHasEmoji) {
emojiBodyElements = formatEmojis(topic, false); emojiBodyElements = formatEmojis(topic, false);
} }

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