Merge branch 'develop' into staging
# Conflicts: # package.json # src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx
41
.github/workflows/cypress.yaml
vendored
|
@ -1,13 +1,33 @@
|
|||
# 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
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Element Web - Build"]
|
||||
types:
|
||||
- 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:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare
|
||||
|
@ -100,16 +120,10 @@ jobs:
|
|||
- uses: browser-actions/setup-chrome@c485fa3bab6be59dce18dbc18ef6ab7cbc8ff5f1
|
||||
- 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
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
|
||||
with:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: previewbuild
|
||||
|
@ -129,14 +143,23 @@ jobs:
|
|||
# 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
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
ref: ${{ steps.sha.outputs.sha }}
|
||||
persist-credentials: false
|
||||
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
|
||||
uses: cypress-io/github-action@59c3b9b4a1a6e623c29806797d849845443487d1
|
||||
uses: cypress-io/github-action@40a1a26c08d0e549e8516612ecebbd1ab5eeec8f
|
||||
with:
|
||||
working-directory: matrix-react-sdk
|
||||
# The built-in Electron runner seems to grind to a halt trying
|
||||
|
|
34
.github/workflows/element-web.yaml
vendored
|
@ -12,19 +12,37 @@ on:
|
|||
branches: [develop, master]
|
||||
repository_dispatch:
|
||||
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:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
# fetchdep.sh needs to know our PR number
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build Element-Web"
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
with:
|
||||
|
@ -32,6 +50,9 @@ jobs:
|
|||
|
||||
- name: Fetch 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: |
|
||||
scripts/ci/layered.sh
|
||||
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
|
||||
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
|
||||
env:
|
||||
CI_PACKAGE: true
|
||||
|
@ -51,9 +71,13 @@ jobs:
|
|||
run: |
|
||||
yarn build
|
||||
echo $VERSION > webapp/version
|
||||
echo $GITHUB_SHA > webapp/sha
|
||||
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
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
|
2
.github/workflows/netlify.yaml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
|||
# 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:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
|
||||
with:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: previewbuild
|
||||
|
|
7
.github/workflows/static_analysis.yaml
vendored
|
@ -10,10 +10,11 @@ on:
|
|||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
# fetchdep.sh needs to know our PR number
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
jobs:
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
|
@ -59,7 +60,7 @@ jobs:
|
|||
|
||||
- name: Get diff lines
|
||||
id: diff
|
||||
uses: Equip-Collaboration/diff-line-numbers@df70b4b83e05105c15f20dc6cc61f1463411b2a6 # v1.0.0
|
||||
uses: Equip-Collaboration/diff-line-numbers@e752977e2cb4207d671bb9e4dad18c07c1b73d52 # v1.1.0
|
||||
with:
|
||||
include: '["\\.tsx?$"]'
|
||||
|
||||
|
|
5
.github/workflows/tests.yml
vendored
|
@ -20,11 +20,12 @@ on:
|
|||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' && inputs.disable_coverage != 'true' }}
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
# fetchdep.sh needs to know our PR number
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
jobs:
|
||||
jest:
|
||||
name: Jest
|
||||
|
|
|
@ -33,6 +33,11 @@ module.exports = {
|
|||
"import-notation": null,
|
||||
"value-keyword-case": 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,
|
||||
"property-no-vendor-prefix": null,
|
||||
"value-no-vendor-prefix": null,
|
||||
|
|
|
@ -176,7 +176,7 @@ describe("Audio player", () => {
|
|||
|
||||
// Enable high contrast manually
|
||||
cy.openUserSettings("Appearance")
|
||||
.get(".mx_ThemeChoicePanel")
|
||||
.findByTestId("mx_ThemeChoicePanel")
|
||||
.findByLabelText("Use high contrast")
|
||||
.click({ force: true }); // force click because the size of the checkbox is zero
|
||||
|
||||
|
@ -333,30 +333,33 @@ describe("Audio player", () => {
|
|||
|
||||
// On a thread
|
||||
cy.get(".mx_ThreadView").within(() => {
|
||||
cy.get(".mx_EventTile_last")
|
||||
.within(() => {
|
||||
// Assert that the player is correctly rendered on a thread
|
||||
cy.get(".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container").within(() => {
|
||||
// Assert that the counter is zero before clicking the play button
|
||||
cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
|
||||
cy.get(".mx_EventTile_last").within(() => {
|
||||
// Assert that the player is correctly rendered on a thread
|
||||
cy.get(".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container").within(() => {
|
||||
// Assert that the counter is zero before clicking the play button
|
||||
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
|
||||
cy.findByRole("button", { name: "Play" }).should("exist");
|
||||
cy.wait(500).findByRole("button", { name: "Play" }).click();
|
||||
// Find and click "Play" button, the wait is to make the test less flaky
|
||||
cy.findByRole("button", { name: "Play" }).should("exist");
|
||||
cy.wait(500).findByRole("button", { name: "Play" }).click();
|
||||
|
||||
// Assert that "Pause" button can be found
|
||||
cy.findByRole("button", { name: "Pause" }).should("exist");
|
||||
// Assert that "Pause" button can be found
|
||||
cy.findByRole("button", { name: "Pause" }).should("exist");
|
||||
|
||||
// Assert that the timer is reset when the audio file finished playing
|
||||
cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
|
||||
// Assert that the timer is reset when the audio file finished playing
|
||||
cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
|
||||
|
||||
// Assert that "Play" button can be found
|
||||
cy.findByRole("button", { name: "Play" }).should("exist").should("not.have.attr", "disabled");
|
||||
});
|
||||
})
|
||||
.realHover()
|
||||
.findByRole("button", { name: "Reply" })
|
||||
.click(); // Find and click "Reply" button
|
||||
// Assert that "Play" button can be found
|
||||
cy.findByRole("button", { name: "Play" }).should("exist").should("not.have.attr", "disabled");
|
||||
});
|
||||
});
|
||||
|
||||
// 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(() => {
|
||||
// Assert that the reply preview is rendered on the message composer
|
||||
|
|
|
@ -225,9 +225,10 @@ describe("Composer", () => {
|
|||
});
|
||||
// ...inserts the username into the composer
|
||||
cy.findByRole("textbox").within(() => {
|
||||
// TODO update this test when the mentions are inserted as pills, instead
|
||||
// of as text
|
||||
cy.findByText(otherUserName, { exact: false }).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
|
||||
|
@ -250,9 +251,10 @@ describe("Composer", () => {
|
|||
// Selecting the autocomplete option using Enter inserts it into the composer
|
||||
cy.findByRole("textbox").type(`{Enter}`);
|
||||
cy.findByRole("textbox").within(() => {
|
||||
// TODO update this test when the mentions are inserted as pills, instead
|
||||
// of as text
|
||||
cy.findByText(otherUserName, { exact: false }).should("exist");
|
||||
cy.findByText(otherUserName, { exact: false })
|
||||
.should("exist")
|
||||
.should("have.attr", "contenteditable", "false")
|
||||
.should("have.attr", "data-mention-type", "user");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,8 +16,9 @@ limitations under the License.
|
|||
|
||||
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
import { handleVerificationRequest, waitForVerificationRequest } from "./utils";
|
||||
import { handleVerificationRequest, logIntoElement, waitForVerificationRequest } from "./utils";
|
||||
import { CypressBot } from "../../support/bot";
|
||||
import { skipIfRustCrypto } from "../../support/util";
|
||||
|
||||
describe("Complete security", () => {
|
||||
let homeserver: HomeserverInstance;
|
||||
|
@ -46,6 +47,8 @@ describe("Complete security", () => {
|
|||
});
|
||||
|
||||
it("should walk through device verification if we have a signed device", () => {
|
||||
skipIfRustCrypto();
|
||||
|
||||
// create a new user, and have it bootstrap cross-signing
|
||||
let botClient: CypressBot;
|
||||
cy.getBot(homeserver, { displayName: "Jeff" })
|
||||
|
@ -66,7 +69,6 @@ describe("Complete security", () => {
|
|||
|
||||
// accept the verification request on the "bot" side
|
||||
cy.wrap(botVerificationRequestPromise).then(async (verificationRequest: VerificationRequest) => {
|
||||
await verificationRequest.accept();
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -19,7 +19,14 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/
|
|||
import type { CypressBot } from "../../support/bot";
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
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 {
|
||||
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 bobsVerificationRequestPromise = waitForVerificationRequest(this.bob);
|
||||
|
||||
|
@ -111,21 +139,9 @@ const verify = function (this: CryptoTestContext) {
|
|||
cy.findByText("Bob").click();
|
||||
cy.findByRole("button", { name: "Verify" }).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.get<VerificationRequest>("@bobsVerificationRequest").then((request: VerificationRequest) => {
|
||||
return cy.wrap(handleVerificationRequest(request)).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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
cy.wrap(bobsVerificationRequestPromise).then((request: VerificationRequest) => {
|
||||
doTwoWaySasVerification(request);
|
||||
});
|
||||
cy.findByRole("button", { name: "They match" }).click();
|
||||
cy.findByText("You've successfully verified Bob!").should("exist");
|
||||
|
@ -143,7 +159,11 @@ describe("Cryptography", function () {
|
|||
cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((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", () => {
|
||||
skipIfRustCrypto();
|
||||
cy.openUserSettings("Security & Privacy");
|
||||
cy.findByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
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) {
|
||||
skipIfRustCrypto();
|
||||
cy.bootstrapCrossSigning(aliceCredentials);
|
||||
startDMWithBob.call(this);
|
||||
// send first message
|
||||
|
@ -196,6 +218,7 @@ describe("Cryptography", function () {
|
|||
});
|
||||
|
||||
it("should allow verification when there is no existing DM", function (this: CryptoTestContext) {
|
||||
skipIfRustCrypto();
|
||||
cy.bootstrapCrossSigning(aliceCredentials);
|
||||
autoJoin(this.bob);
|
||||
|
||||
|
@ -214,6 +237,7 @@ describe("Cryptography", function () {
|
|||
});
|
||||
|
||||
it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) {
|
||||
skipIfRustCrypto();
|
||||
cy.bootstrapCrossSigning(aliceCredentials);
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,11 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
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 { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
import { UserCredentials } from "../../support/login";
|
||||
import Chainable = Cypress.Chainable;
|
||||
import { handleVerificationRequest } from "./utils";
|
||||
import { skipIfRustCrypto } from "../../support/util";
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
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) => {
|
||||
cy.viewport(800, 600); // SVGA
|
||||
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;
|
||||
|
||||
beforeEach(function () {
|
||||
skipIfRustCrypto();
|
||||
cy.startHomeserver("default").then((hs: HomeserverInstance) => {
|
||||
homeserver = hs;
|
||||
cy.initTestUser(homeserver, TEST_USER)
|
||||
|
@ -161,7 +144,11 @@ describe("Decryption Failure Bar", () => {
|
|||
);
|
||||
cy.wrap(verificationRequestPromise).then((verificationRequest: VerificationRequest) => {
|
||||
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) => {
|
||||
emojis.forEach((emoji: EmojiMapping, index: number) => {
|
||||
expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
|
||||
|
|
|
@ -21,15 +21,16 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/
|
|||
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
|
||||
*/
|
||||
export function waitForVerificationRequest(cli: MatrixClient): Promise<VerificationRequest> {
|
||||
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
|
||||
cli.off("crypto.verification.request", onVerificationRequestEvent);
|
||||
await request.accept();
|
||||
resolve(request);
|
||||
};
|
||||
// @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
|
||||
* emojis match.
|
||||
*
|
||||
* Returns a promise that resolves, with the emoji list, once we confirm the emojis
|
||||
*
|
||||
* @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) => {
|
||||
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);
|
||||
event.confirm();
|
||||
verifier.done();
|
||||
resolve(event.sas.emoji);
|
||||
};
|
||||
|
||||
const verifier = request.beginKeyVerification("m.sas.v1");
|
||||
// @ts-ignore as above, avoiding reference to VerifierEvent
|
||||
verifier.on("show_sas", onShowSas);
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -164,6 +164,14 @@ describe("Invite dialog", function () {
|
|||
// Assert that the invite dialog disappears
|
||||
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
|
||||
cy.getComposer().type("Hello{enter}");
|
||||
|
||||
|
|
|
@ -21,10 +21,6 @@ import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
|||
describe("Login", () => {
|
||||
let homeserver: HomeserverInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.stubDefaultServer();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopHomeserver(homeserver);
|
||||
});
|
||||
|
@ -44,17 +40,18 @@ describe("Login", () => {
|
|||
it("logs in with an existing account and lands on the home screen", () => {
|
||||
cy.injectAxe();
|
||||
|
||||
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();
|
||||
|
||||
// first pick the homeserver, as otherwise the user picker won't be visible
|
||||
cy.findByRole("button", { name: "Edit" }).click();
|
||||
cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserver.baseUrl);
|
||||
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", 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.findByPlaceholderText("Password").type(password);
|
||||
cy.findByRole("button", { name: "Sign in" }).click();
|
||||
|
|
|
@ -126,13 +126,14 @@ describe("permalinks", () => {
|
|||
getPill(danielle.getSafeUserId());
|
||||
});
|
||||
|
||||
// clean up before taking the snapshot
|
||||
cy.get(".mx_cryptoEvent").invoke("remove");
|
||||
cy.get(".mx_NewRoomIntro").invoke("remove");
|
||||
cy.get(".mx_GenericEventListSummary").invoke("remove");
|
||||
// Exclude various components from the snapshot, for consistency
|
||||
const percyCSS =
|
||||
".mx_cryptoEvent, " +
|
||||
".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
|
||||
//const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
|
||||
//cy.get(".mx_RoomView_timeline").percySnapshotElement("Permalink rendering", { percyCSS });
|
||||
cy.get(".mx_RoomView_timeline").percySnapshotElement("Permalink rendering", { percyCSS });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,12 +17,12 @@ limitations under the License.
|
|||
/// <reference types="cypress" />
|
||||
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
import { checkDeviceIsCrossSigned } from "../crypto/utils";
|
||||
|
||||
describe("Registration", () => {
|
||||
let homeserver: HomeserverInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.stubDefaultServer();
|
||||
cy.visit("/#/register");
|
||||
cy.startHomeserver("consent").then((data) => {
|
||||
homeserver = data;
|
||||
|
@ -89,39 +89,14 @@ describe("Registration", () => {
|
|||
|
||||
// check that the device considers itself verified
|
||||
cy.findByRole("button", { name: "User menu" }).click();
|
||||
cy.findByRole("menuitem", { name: "Security & Privacy" }).click();
|
||||
cy.get(".mx_DevicesPanel_myDevice .mx_DevicesPanel_deviceTrust .mx_E2EIcon").should(
|
||||
"have.class",
|
||||
"mx_E2EIcon_verified",
|
||||
);
|
||||
cy.findByRole("menuitem", { name: "All settings" }).click();
|
||||
cy.findByRole("tab", { name: "Sessions" }).click();
|
||||
cy.findByTestId("current-session-section").within(() => {
|
||||
cy.findByTestId("device-metadata-isVerified").should("have.text", "Verified");
|
||||
});
|
||||
|
||||
// check that cross-signing keys have been uploaded.
|
||||
const myUserId = "@alice:localhost";
|
||||
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;
|
||||
});
|
||||
checkDeviceIsCrossSigned();
|
||||
});
|
||||
|
||||
it("should require username to fulfil requirements and be available", () => {
|
||||
|
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { IWidget } from "matrix-widget-api";
|
||||
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
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
|
||||
// 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")
|
||||
.should("have.length", 3)
|
||||
.should("be.visible")
|
||||
.should("have.css", "height", "32px")
|
||||
.should("have.css", "width", "32px");
|
||||
cy.get(".mx_RightPanel_headerButton")
|
||||
.should("have.length", 3)
|
||||
.should("have.length", 6)
|
||||
.should("be.visible")
|
||||
.should("have.css", "height", "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)");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,12 +36,11 @@ describe("Appearance user settings tab", () => {
|
|||
it("should be rendered properly", () => {
|
||||
cy.openUserSettings("Appearance");
|
||||
|
||||
cy.get(".mx_SettingsTab.mx_AppearanceUserSettingsTab").within(() => {
|
||||
// Assert that the top heading is rendered
|
||||
cy.findByTestId("appearance").should("have.text", "Customise your appearance").should("be.visible");
|
||||
cy.findByTestId("mx_AppearanceUserSettingsTab").within(() => {
|
||||
cy.get("h2").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)",
|
||||
{
|
||||
// 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
|
||||
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)",
|
||||
{
|
||||
// Emulate TabbedView's actual min and max widths
|
||||
|
@ -74,7 +73,7 @@ describe("Appearance user settings tab", () => {
|
|||
|
||||
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"
|
||||
cy.get(".mx_LayoutSwitcher_RadioButton_selected .mx_StyledRadioButton_enabled").within(() => {
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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", () => {
|
||||
cy.openUserSettings("Appearance");
|
||||
|
||||
cy.get(".mx_SettingsTab.mx_AppearanceUserSettingsTab").within(() => {
|
||||
cy.findByTestId("mx_AppearanceUserSettingsTab").within(() => {
|
||||
cy.get(".mx_FontScalingPanel_fontSlider").within(() => {
|
||||
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", () => {
|
||||
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
|
||||
|
||||
// Assert that the font slider is disabled
|
||||
|
@ -167,10 +166,8 @@ describe("Appearance user settings tab", () => {
|
|||
// Click "Show advanced" link button
|
||||
cy.findByRole("button", { name: "Show advanced" }).click();
|
||||
|
||||
cy.get(".mx_AppearanceUserSettingsTab_Advanced").within(() => {
|
||||
// force click as checkbox size is zero
|
||||
cy.findByLabelText("Use a more compact 'Modern' layout").click({ force: true });
|
||||
});
|
||||
// force click as checkbox size is zero
|
||||
cy.findByLabelText("Use a more compact 'Modern' layout").click({ force: true });
|
||||
|
||||
// Assert that the room layout is set to compact group (modern) layout
|
||||
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", () => {
|
||||
const checkDisabled = () => {
|
||||
cy.get(".mx_AppearanceUserSettingsTab_Advanced").within(() => {
|
||||
cy.get(".mx_Checkbox")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get("input[type='checkbox'][disabled]").should("exist");
|
||||
});
|
||||
});
|
||||
cy.findByLabelText("Use a more compact 'Modern' layout").should("be.disabled");
|
||||
};
|
||||
|
||||
cy.openUserSettings("Appearance");
|
||||
|
@ -193,7 +184,7 @@ describe("Appearance user settings tab", () => {
|
|||
cy.findByRole("button", { name: "Show advanced" }).click();
|
||||
|
||||
// Enable IRC layout
|
||||
cy.get(".mx_AppearanceUserSettingsTab .mx_LayoutSwitcher_RadioButtons").within(() => {
|
||||
cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
|
||||
// Select the first layout
|
||||
cy.get(".mx_LayoutSwitcher_RadioButton").first().click();
|
||||
|
||||
|
@ -206,7 +197,7 @@ describe("Appearance user settings tab", () => {
|
|||
checkDisabled();
|
||||
|
||||
// Enable bubble layout
|
||||
cy.get(".mx_AppearanceUserSettingsTab .mx_LayoutSwitcher_RadioButtons").within(() => {
|
||||
cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
|
||||
// Select the first layout
|
||||
cy.get(".mx_LayoutSwitcher_RadioButton").last().click();
|
||||
|
||||
|
@ -225,10 +216,8 @@ describe("Appearance user settings tab", () => {
|
|||
// Click "Show advanced" link button
|
||||
cy.findByRole("button", { name: "Show advanced" }).click();
|
||||
|
||||
cy.get(".mx_AppearanceUserSettingsTab_Advanced").within(() => {
|
||||
// force click as checkbox size is zero
|
||||
cy.findByLabelText("Use a system font").click({ force: true });
|
||||
});
|
||||
// force click as checkbox size is zero
|
||||
cy.findByLabelText("Use a system font").click({ force: true });
|
||||
|
||||
// Assert that the font-family value was removed
|
||||
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", () => {
|
||||
cy.openUserSettings("Appearance")
|
||||
.get(".mx_ThemeChoicePanel")
|
||||
.findByTestId("mx_ThemeChoicePanel")
|
||||
.within(() => {
|
||||
cy.findByTestId("checkbox-use-system-theme").within(() => {
|
||||
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_ThemeSelectors").within(() => {
|
||||
cy.findByTestId("theme-choice-panel-selectors").within(() => {
|
||||
cy.get(".mx_ThemeSelector_light").should("exist");
|
||||
cy.get(".mx_ThemeSelector_dark").should("exist");
|
||||
|
||||
|
@ -274,11 +263,11 @@ describe("Appearance user settings tab", () => {
|
|||
"the system theme is clicked",
|
||||
() => {
|
||||
cy.openUserSettings("Appearance")
|
||||
.get(".mx_ThemeChoicePanel")
|
||||
.findByTestId("mx_ThemeChoicePanel")
|
||||
.findByLabelText("Match system theme")
|
||||
.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
|
||||
cy.get(".mx_ThemeSelector_light.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")
|
||||
.get(".mx_ThemeChoicePanel")
|
||||
.findByTestId("mx_ThemeChoicePanel")
|
||||
.findByLabelText("Use high contrast")
|
||||
.click({ force: true }); // force click because the size of the checkbox is zero
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ describe("Device manager", () => {
|
|||
let user: UserCredentials | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.enableLabsFeature("feature_new_device_manager");
|
||||
cy.startHomeserver("default").then((data) => {
|
||||
homeserver = data;
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ describe("General user settings tab", () => {
|
|||
|
||||
cy.findByTestId("mx_GeneralUserSettingsTab").within(() => {
|
||||
// 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")
|
||||
.scrollIntoView()
|
||||
|
@ -83,10 +83,14 @@ describe("General user settings tab", () => {
|
|||
});
|
||||
|
||||
// Wait until spinners disappear
|
||||
cy.get(".mx_GeneralUserSettingsTab_section--account .mx_Spinner").should("not.exist");
|
||||
cy.get(".mx_GeneralUserSettingsTab_section--discovery .mx_Spinner").should("not.exist");
|
||||
cy.findByTestId("accountSection").within(() => {
|
||||
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
|
||||
cy.get("form.mx_GeneralUserSettingsTab_section--account_changePassword")
|
||||
.scrollIntoView()
|
||||
|
@ -95,29 +99,28 @@ describe("General user settings tab", () => {
|
|||
cy.findByLabelText("New 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
|
||||
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", () => {
|
||||
// Check phone numbers area
|
||||
cy.get(".mx_PhoneNumbers")
|
||||
cy.findByTestId("mx_AccountPhoneNumbers")
|
||||
.scrollIntoView()
|
||||
.within(() => {
|
||||
// Assert that an input area for a new phone number is rendered
|
||||
|
|
72
cypress/e2e/settings/security-user-settings-tab.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -140,6 +140,8 @@ describe("Spaces", () => {
|
|||
cy.findByPlaceholderText("Support").type("Projects");
|
||||
cy.findByRole("button", { name: "Continue" }).click();
|
||||
|
||||
cy.get(".mx_SpaceRoomView").percySnapshotElement("Space - 'Invite your teammates' dialog");
|
||||
|
||||
cy.get(".mx_SpaceRoomView").within(() => {
|
||||
cy.get("h1").findByText("Invite your teammates");
|
||||
cy.findByRole("button", { name: "Skip for now" }).click();
|
||||
|
|
|
@ -203,6 +203,10 @@ describe("Spotlight", () => {
|
|||
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(() => {
|
||||
|
@ -212,8 +216,12 @@ describe("Spotlight", () => {
|
|||
|
||||
it("should be able to add and remove filters via keyboard", () => {
|
||||
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");
|
||||
|
||||
// hitting enter should enable the publicrooms filter
|
||||
cy.spotlightSearch().type("{enter}");
|
||||
cy.get(".mx_SpotlightDialog_filter").should("contain", "Public rooms");
|
||||
cy.spotlightSearch().type("{backspace}");
|
||||
|
@ -233,7 +241,6 @@ describe("Spotlight", () => {
|
|||
cy.openSpotlightDialog()
|
||||
.within(() => {
|
||||
cy.spotlightSearch().clear().type(room1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room1Name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
|
@ -249,7 +256,6 @@ describe("Spotlight", () => {
|
|||
.within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room1Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "View");
|
||||
|
@ -266,7 +272,6 @@ describe("Spotlight", () => {
|
|||
.within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room2Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "Join");
|
||||
|
@ -284,7 +289,6 @@ describe("Spotlight", () => {
|
|||
.within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room3Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room3Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "View");
|
||||
|
@ -326,7 +330,6 @@ describe("Spotlight", () => {
|
|||
.within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", bot1Name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
|
@ -341,7 +344,6 @@ describe("Spotlight", () => {
|
|||
.within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", bot2Name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
|
@ -359,7 +361,6 @@ describe("Spotlight", () => {
|
|||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", bot2Name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
|
|
|
@ -296,7 +296,7 @@ describe("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
|
||||
|
||||
// User asserts thread with correct root & latest events & unread dot
|
||||
|
|
|
@ -95,6 +95,10 @@ describe("Widget Layout", () => {
|
|||
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", () => {
|
||||
cy.get('iframe[title="widget"]').invoke("height").should("be.lessThan", 250);
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"m.homeserver": {
|
||||
"base_url": "https://matrix-client.matrix.org"
|
||||
},
|
||||
"m.identity_server": {
|
||||
"base_url": "https://vector.im"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{}
|
|
@ -59,6 +59,10 @@ Cypress.Commands.overwrite(
|
|||
"color-contrast": {
|
||||
enabled: false,
|
||||
},
|
||||
// link-in-text-block also complains due to known contrast issues
|
||||
"link-in-text-block": {
|
||||
enabled: false,
|
||||
},
|
||||
...options.rules,
|
||||
},
|
||||
},
|
||||
|
|
51
cypress/support/config.json.ts
Normal 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 });
|
||||
});
|
|
@ -20,6 +20,7 @@ import "@percy/cypress";
|
|||
import "cypress-real-events";
|
||||
import "@testing-library/cypress/add-commands";
|
||||
|
||||
import "./config.json";
|
||||
import "./homeserver";
|
||||
import "./login";
|
||||
import "./labs";
|
||||
|
|
|
@ -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
|
||||
export {};
|
||||
|
|
|
@ -56,5 +56,20 @@ cy.all = function all(commands): Cypress.Chainable {
|
|||
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");
|
||||
}
|
||||
|
|
|
@ -45,6 +45,16 @@ To launch it:
|
|||
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
|
||||
|
||||
Everything Cypress-related lives in the `cypress/` subdirectory of react-sdk
|
||||
|
|
21
package.json
|
@ -68,7 +68,6 @@
|
|||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"await-lock": "^2.1.0",
|
||||
"blurhash": "^1.1.3",
|
||||
"cheerio": "^1.0.0-rc.9",
|
||||
"classnames": "^2.2.6",
|
||||
"commonmark": "^0.30.0",
|
||||
"counterpart": "^0.18.6",
|
||||
|
@ -83,6 +82,7 @@
|
|||
"focus-visible": "^5.2.0",
|
||||
"gfm.css": "^1.1.2",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
"highlight.js": "^11.3.1",
|
||||
"html-entities": "^2.0.0",
|
||||
"is-ip": "^3.1.0",
|
||||
|
@ -96,16 +96,16 @@
|
|||
"maplibre-gl": "^2.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-js-sdk": "25.2.0-rc.5",
|
||||
"matrix-widget-api": "^1.3.1",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^1.4.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"minimist": "^1.2.5",
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.53.2",
|
||||
"posthog-js": "1.57.2",
|
||||
"proposal-temporal": "^0.9.0",
|
||||
"qrcode": "1.5.1",
|
||||
"qrcode": "1.5.3",
|
||||
"re-resizable": "^6.9.0",
|
||||
"react": "17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
|
@ -118,7 +118,6 @@
|
|||
"sanitize-html": "2.10.0",
|
||||
"tar-js": "^0.3.0",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"url": "^0.11.0",
|
||||
"what-input": "^5.2.10",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
|
@ -175,7 +174,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "^5.35.1",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.1.0",
|
||||
"axe-core": "4.7.0",
|
||||
"axe-core": "4.7.1",
|
||||
"babel-jest": "^29.0.0",
|
||||
"blob-polyfill": "^7.0.0",
|
||||
"chokidar": "^3.5.1",
|
||||
|
@ -183,7 +182,7 @@
|
|||
"cypress-axe": "^1.0.0",
|
||||
"cypress-multi-reporters": "^1.6.1",
|
||||
"cypress-real-events": "^1.7.1",
|
||||
"eslint": "8.38.0",
|
||||
"eslint": "8.40.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-deprecate": "^0.7.0",
|
||||
|
@ -193,7 +192,7 @@
|
|||
"eslint-plugin-matrix-org": "1.1.0",
|
||||
"eslint-plugin-react": "^7.28.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",
|
||||
"fs-extra": "^11.0.0",
|
||||
"jest": "29.3.1",
|
||||
|
@ -206,11 +205,11 @@
|
|||
"mocha-junit-reporter": "^2.2.0",
|
||||
"node-fetch": "2",
|
||||
"postcss-scss": "^4.0.4",
|
||||
"prettier": "2.8.7",
|
||||
"prettier": "2.8.8",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^5.0.0",
|
||||
"stylelint": "^15.0.0",
|
||||
"stylelint-config-standard": "^32.0.0",
|
||||
"stylelint-config-standard": "^33.0.0",
|
||||
"stylelint-scss": "^5.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.0.4",
|
||||
|
|
|
@ -34,6 +34,10 @@ limitations under the License.
|
|||
transition: opacity 300ms ease;
|
||||
}
|
||||
|
||||
:root {
|
||||
--hover-transition: 0.08s cubic-bezier(0.46, 0.03, 0.52, 0.96); /* quadratic */
|
||||
}
|
||||
|
||||
@keyframes mx--anim-pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
|
|
|
@ -23,10 +23,6 @@ limitations under the License.
|
|||
@import "./_spacing.pcss";
|
||||
@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 {
|
||||
font-size: 10px;
|
||||
|
||||
|
@ -37,6 +33,22 @@ $selected-message-border-width: 4px;
|
|||
--buttons-dialog-gap-row: $spacing-8;
|
||||
--buttons-dialog-gap-column: $spacing-8;
|
||||
--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 {
|
||||
|
@ -281,20 +293,9 @@ legend {
|
|||
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 {
|
||||
position: fixed;
|
||||
z-index: 4000;
|
||||
z-index: var(--dialog-zIndex-wrapper-default);
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
@ -308,7 +309,7 @@ legend {
|
|||
.mx_Dialog {
|
||||
background-color: $background;
|
||||
color: $light-fg-color;
|
||||
z-index: 4012;
|
||||
z-index: var(--dialog-zIndex-standard);
|
||||
font-size: $font-15px;
|
||||
position: relative;
|
||||
padding: 24px;
|
||||
|
@ -316,73 +317,89 @@ legend {
|
|||
box-shadow: 2px 15px 30px 0 $dialog-shadow-color;
|
||||
border-radius: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Styles copied/inspired by GroupLayout, ReplyTile, and EventTile variants. */
|
||||
.mx_Dialog .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,
|
||||
code {
|
||||
font-family: $monospace-font-family !important;
|
||||
background-color: $codeblock-background-color;
|
||||
.mx_Dialog_staticWrapper & {
|
||||
z-index: var(--dialog-zIndex-static);
|
||||
contain: content;
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
.mx_Dialog_lightbox & {
|
||||
border-radius: 0px;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
pointer-events: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
pre code {
|
||||
white-space: pre; /* we want code blocks to be scrollable and not wrap */
|
||||
/* Styles copied/inspired by GroupLayout, ReplyTile, and EventTile variants. */
|
||||
.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;
|
||||
|
||||
> * {
|
||||
display: inline;
|
||||
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 */
|
||||
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 {
|
||||
|
@ -390,11 +407,6 @@ legend {
|
|||
max-width: 704px;
|
||||
}
|
||||
|
||||
.mx_Dialog_staticWrapper .mx_Dialog {
|
||||
z-index: 4010;
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.mx_Dialog_background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
@ -403,41 +415,24 @@ legend {
|
|||
height: 100%;
|
||||
background-color: $dialog-backdrop-color;
|
||||
opacity: 0.8;
|
||||
z-index: 4011;
|
||||
}
|
||||
z-index: var(--dialog-zIndex-standard-background);
|
||||
|
||||
.mx_Dialog_background.mx_Dialog_staticBackground {
|
||||
z-index: 4009;
|
||||
}
|
||||
&.mx_Dialog_staticBackground {
|
||||
z-index: var(--dialog-zIndex-static-background);
|
||||
}
|
||||
|
||||
.mx_Dialog_wrapperWithStaticUnder .mx_Dialog_background {
|
||||
/* 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. */
|
||||
opacity: 0.4;
|
||||
}
|
||||
.mx_Dialog_wrapperWithStaticUnder & {
|
||||
/* 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. */
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.mx_Dialog_lightbox .mx_Dialog_background {
|
||||
opacity: $lightbox-background-bg-opacity;
|
||||
background-color: $lightbox-background-bg-color;
|
||||
animation-name: mx_Dialog_lightbox_background_keyframes;
|
||||
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_lightbox & {
|
||||
opacity: $lightbox-background-bg-opacity;
|
||||
background-color: $lightbox-background-bg-color;
|
||||
animation-name: mx_Dialog_lightbox_background_keyframes;
|
||||
animation-duration: 300ms;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Dialog_titleImage {
|
||||
|
@ -454,22 +449,29 @@ legend {
|
|||
display: inline-block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.danger {
|
||||
color: $alert;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Dialog_header.mx_Dialog_headerWithButton > .mx_Dialog_title {
|
||||
text-align: center;
|
||||
}
|
||||
.mx_Dialog_header.mx_Dialog_headerWithCancel {
|
||||
padding-right: 20px; /* leave space for the 'X' cancel button */
|
||||
}
|
||||
.mx_Dialog_header {
|
||||
position: relative;
|
||||
padding: 3px 0;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.mx_Dialog_header.mx_Dialog_headerWithCancelOnly {
|
||||
padding: 0 20px 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
&.mx_Dialog_headerWithButton > .mx_Dialog_title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mx_Dialog_title.danger {
|
||||
color: $alert;
|
||||
&.mx_Dialog_headerWithCancel {
|
||||
padding-right: 20px; /* leave space for the 'X' cancel button */
|
||||
}
|
||||
|
||||
&.mx_Dialog_headerWithCancelOnly {
|
||||
padding: 0 20px 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@define-mixin customisedCancelButton {
|
||||
|
@ -509,21 +511,21 @@ legend {
|
|||
/* The consumer is responsible for positioning their elements. */
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Dialog_buttons_row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
text-align: initial;
|
||||
margin-inline-start: auto;
|
||||
.mx_Dialog_buttons_row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
text-align: initial;
|
||||
margin-inline-start: auto;
|
||||
|
||||
/* default gap among elements */
|
||||
column-gap: var(--buttons-dialog-gap-column);
|
||||
row-gap: var(--buttons-dialog-gap-row);
|
||||
/* default gap among elements */
|
||||
column-gap: var(--buttons-dialog-gap-column);
|
||||
row-gap: var(--buttons-dialog-gap-row);
|
||||
|
||||
button {
|
||||
margin: 0 !important; /* override the margin settings */
|
||||
button {
|
||||
margin: 0 !important; /* override the margin settings */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -124,7 +124,6 @@
|
|||
@import "./views/dialogs/_BugReportDialog.pcss";
|
||||
@import "./views/dialogs/_BulkRedactDialog.pcss";
|
||||
@import "./views/dialogs/_ChangelogDialog.pcss";
|
||||
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.pcss";
|
||||
@import "./views/dialogs/_CompoundDialog.pcss";
|
||||
@import "./views/dialogs/_ConfirmSpaceUserActionDialog.pcss";
|
||||
@import "./views/dialogs/_ConfirmUserActionDialog.pcss";
|
||||
|
@ -179,7 +178,6 @@
|
|||
@import "./views/elements/_Dropdown.pcss";
|
||||
@import "./views/elements/_EditableItemList.pcss";
|
||||
@import "./views/elements/_ErrorBoundary.pcss";
|
||||
@import "./views/elements/_EventTilePreview.pcss";
|
||||
@import "./views/elements/_ExternalLink.pcss";
|
||||
@import "./views/elements/_FacePile.pcss";
|
||||
@import "./views/elements/_Field.pcss";
|
||||
|
@ -198,7 +196,6 @@
|
|||
@import "./views/elements/_ReplyChain.pcss";
|
||||
@import "./views/elements/_ResizeHandle.pcss";
|
||||
@import "./views/elements/_RichText.pcss";
|
||||
@import "./views/elements/_RoleButton.pcss";
|
||||
@import "./views/elements/_RoomAliasField.pcss";
|
||||
@import "./views/elements/_SSOButtons.pcss";
|
||||
@import "./views/elements/_SearchWarning.pcss";
|
||||
|
@ -319,7 +316,6 @@
|
|||
@import "./views/settings/_AvatarSetting.pcss";
|
||||
@import "./views/settings/_CrossSigningPanel.pcss";
|
||||
@import "./views/settings/_CryptographyPanel.pcss";
|
||||
@import "./views/settings/_DevicesPanel.pcss";
|
||||
@import "./views/settings/_FontScalingPanel.pcss";
|
||||
@import "./views/settings/_ImageSizePanel.pcss";
|
||||
@import "./views/settings/_IntegrationManager.pcss";
|
||||
|
@ -348,7 +344,6 @@
|
|||
@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.pcss";
|
||||
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.pcss";
|
||||
@import "./views/settings/tabs/user/_SidebarUserSettingsTab.pcss";
|
||||
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.pcss";
|
||||
@import "./views/spaces/_SpaceBasicSettings.pcss";
|
||||
@import "./views/spaces/_SpaceChildrenPicker.pcss";
|
||||
@import "./views/spaces/_SpaceCreateMenu.pcss";
|
||||
|
|
|
@ -53,25 +53,25 @@ limitations under the License.
|
|||
mask-image: url("$(res)/img/feather-customised/help-circle.svg");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AppPermission_tooltip {
|
||||
box-shadow: none;
|
||||
background-color: $tooltip-timeline-bg-color;
|
||||
color: $tooltip-timeline-fg-color;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
padding: 6px 8px;
|
||||
.mx_Tooltip.mx_Tooltip--appPermission {
|
||||
box-shadow: none;
|
||||
background-color: $tooltip-timeline-bg-color;
|
||||
color: $tooltip-timeline-fg-color;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
padding: 6px 8px;
|
||||
|
||||
&.mx_AppPermission_tooltip--dark {
|
||||
.mx_Tooltip_chevron::after {
|
||||
border-right-color: $tooltip-timeline-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
padding-left: 2px;
|
||||
margin-left: 0;
|
||||
&.mx_Tooltip--appPermission--dark {
|
||||
.mx_Tooltip_chevron::after {
|
||||
border-right-color: $tooltip-timeline-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
padding-left: 2px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,9 @@ limitations under the License.
|
|||
width: 100%;
|
||||
display: grid;
|
||||
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;
|
||||
margin-top: $spacing-24;
|
||||
|
||||
|
@ -50,4 +52,8 @@ limitations under the License.
|
|||
&.mx_SettingsSubsection_contentStretch {
|
||||
justify-items: stretch;
|
||||
}
|
||||
|
||||
&.mx_SettingsSubsection_noHeading {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,9 +113,11 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_QuickSettingsButton_icon {
|
||||
// TODO remove when all icons have fill=currentColor
|
||||
* {
|
||||
fill: $secondary-content;
|
||||
}
|
||||
color: $secondary-content;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
|
|
|
@ -34,40 +34,6 @@ limitations under the License.
|
|||
|
||||
/** 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 {
|
||||
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 {
|
||||
&::before {
|
||||
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_MemberInfo {
|
||||
order: 2;
|
||||
|
|
|
@ -160,7 +160,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_connectionLostBar img {
|
||||
.mx_RoomStatusBar_connectionLostBar svg {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -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 {
|
||||
border-bottom: 1px solid $primary-hairline-color;
|
||||
padding: 10px 26px;
|
||||
|
|
|
@ -271,23 +271,6 @@ $SpaceRoomViewInnerWidth: 428px;
|
|||
}
|
||||
|
||||
.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 {
|
||||
color: $secondary-content;
|
||||
margin-top: 28px;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -31,16 +31,6 @@ limitations under the License.
|
|||
|
||||
.mx_SettingsTab {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,12 +37,6 @@ limitations under the License.
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
& + .mx_SettingsTab_subheading {
|
||||
border-top: 1px solid $menu-border-color;
|
||||
margin-top: 0;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.mx_StyledRadioButton {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
|
|
|
@ -58,8 +58,8 @@ limitations under the License.
|
|||
.mx_Dropdown_option {
|
||||
height: 35px;
|
||||
line-height: $font-35px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
// Overwrites the default padding for any li elements
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.mx_Dropdown_input > .mx_Dropdown_option {
|
||||
|
@ -121,6 +121,10 @@ input.mx_Dropdown_option:focus {
|
|||
min-height: 35px;
|
||||
}
|
||||
|
||||
ul.mx_Dropdown_menu li.mx_Dropdown_option {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.mx_Dropdown_menu .mx_Dropdown_option_highlight {
|
||||
background-color: $focus-bg-color;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -17,16 +17,12 @@ limitations under the License.
|
|||
.mx_TagComposer {
|
||||
.mx_TagComposer_input {
|
||||
display: flex;
|
||||
|
||||
.mx_Field {
|
||||
flex: 1;
|
||||
margin: 0; /* override from field styles */
|
||||
}
|
||||
flex-direction: row;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
min-width: 70px;
|
||||
padding: 0 8px; /* override from button styles */
|
||||
margin-left: 16px; /* distance from <Field> */
|
||||
align-self: stretch; /* override default settingstab style */
|
||||
}
|
||||
|
||||
.mx_Field,
|
||||
|
|
|
@ -222,60 +222,34 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_AppTileMenuBar_widgets {
|
||||
float: right;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBar_iconButton {
|
||||
--size: 24px; /* Size of the button. Its height and width values should be same */
|
||||
.mx_AppTileMenuBar_widgets_button {
|
||||
--size: 24px; /* Size of the button. Its height and width values should be same */
|
||||
|
||||
margin: 0 4px;
|
||||
position: relative;
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
|
||||
&::before,
|
||||
&:hover::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
margin: 0 4px;
|
||||
position: relative;
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::before {
|
||||
background-color: $muted-fg-color;
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 12px;
|
||||
}
|
||||
&:hover::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
background-color: $panel-actions;
|
||||
border-radius: 50%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background-color: $panel-actions;
|
||||
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");
|
||||
.mx_Icon {
|
||||
color: $muted-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,37 +14,47 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.m_RoomView_auxPanel_stateViews {
|
||||
padding: 5px;
|
||||
padding-left: 19px;
|
||||
border-bottom: 1px solid $primary-hairline-color;
|
||||
}
|
||||
.mx_AuxPanel {
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
margin: 0px auto;
|
||||
|
||||
.m_RoomView_auxPanel_stateViews_span a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
overflow: auto;
|
||||
|
||||
.m_RoomView_auxPanel_stateViews_span[data-severity="warning"] {
|
||||
font-weight: bold;
|
||||
color: orange;
|
||||
}
|
||||
.mx_AuxPanel_stateViews {
|
||||
padding: 5px;
|
||||
padding-left: 19px;
|
||||
border-bottom: 1px solid $primary-hairline-color;
|
||||
}
|
||||
|
||||
.m_RoomView_auxPanel_stateViews_span[data-severity="alert"] {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
}
|
||||
.mx_AuxPanel_stateViews_span {
|
||||
&[data-severity="warning"] {
|
||||
font-weight: bold;
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.m_RoomView_auxPanel_stateViews_span[data-severity="normal"] {
|
||||
font-weight: normal;
|
||||
}
|
||||
&[data-severity="alert"] {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.m_RoomView_auxPanel_stateViews_span[data-severity="notice"] {
|
||||
font-weight: normal;
|
||||
color: $settings-grey-fg-color;
|
||||
}
|
||||
&[data-severity="normal"] {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.m_RoomView_auxPanel_stateViews_delim {
|
||||
padding: 0 5px;
|
||||
color: $settings-grey-fg-color;
|
||||
&[data-severity="notice"] {
|
||||
font-weight: normal;
|
||||
color: $settings-grey-fg-color;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AuxPanel_stateViews_delim {
|
||||
padding: 0 5px;
|
||||
color: $settings-grey-fg-color;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -170,8 +170,10 @@ $left-gutter: 64px;
|
|||
|
||||
&[data-layout="irc"],
|
||||
&[data-layout="group"] {
|
||||
--selected-message-border-width: 4px;
|
||||
|
||||
/* 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;
|
||||
.mx_EventTile_e2eIcon {
|
||||
position: absolute;
|
||||
|
@ -447,7 +449,9 @@ $left-gutter: 64px;
|
|||
&.mx_EventTile_isEditing > .mx_EventTile_line {
|
||||
.mx_EditMessageComposer {
|
||||
/* 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_unverified.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;
|
||||
padding-bottom: 0;
|
||||
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 {
|
||||
|
@ -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 */
|
||||
.mx_ThreadView {
|
||||
--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 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-shape="ThreadsList"][data-notification]::before,
|
||||
.mx_NotificationBadge {
|
||||
/* stylelint-disable-next-line declaration-colon-space-after */
|
||||
inset-block-start: calc($notification-inset-block-start - var(--MatrixChat_useCompactLayout_group-padding-top));
|
||||
&[data-shape="ThreadsList"][data-notification]::before,
|
||||
.mx_NotificationBadge {
|
||||
/* stylelint-disable-next-line declaration-colon-space-after */
|
||||
inset-block-start: calc(
|
||||
$notification-inset-block-start - var(--MatrixChat_useCompactLayout_group-padding-top)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -76,6 +76,12 @@ limitations under the License.
|
|||
|
||||
.mx_MemberInfo_container {
|
||||
margin: 0 16px 16px 16px;
|
||||
|
||||
&.mx_MemberInfo_container--profile {
|
||||
margin-bottom: 16px;
|
||||
font-size: $font-15px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
cursor: pointer;
|
||||
font-size: $font-15px;
|
||||
color: $primary-content;
|
||||
margin-left: 8px;
|
||||
margin-bottom: 16px;
|
||||
line-height: $font-23px;
|
||||
}
|
||||
|
|
|
@ -69,3 +69,10 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_NotificationBadge_tooltip {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: -25px;
|
||||
left: 6px;
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ limitations under the License.
|
|||
padding: 1px 4px;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: $quinary-content;
|
||||
|
@ -102,6 +103,14 @@ limitations under the License.
|
|||
background-color: $tertiary-content;
|
||||
}
|
||||
|
||||
&.mx_RoomHeader_name--textonly {
|
||||
cursor: unset;
|
||||
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&[aria-expanded="true"] {
|
||||
background-color: $quinary-content;
|
||||
|
||||
|
@ -120,11 +129,6 @@ limitations under the License.
|
|||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.mx_RoomHeader_name:not(.mx_RoomHeader_name--textonly),
|
||||
.mx_RoomHeader_avatar {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_RoomTopic {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
@ -157,6 +161,7 @@ limitations under the License.
|
|||
flex: 0;
|
||||
margin: 0 7px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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 {
|
||||
mask-image: url("$(res)/img/element-icons/leave.svg");
|
||||
width: 26px;
|
||||
|
|
|
@ -55,13 +55,18 @@ limitations under the License.
|
|||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.mx_RoomTile_title,
|
||||
.mx_RoomTile_subtitle {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
color: $secondary-content;
|
||||
display: flex;
|
||||
gap: $spacing-4;
|
||||
line-height: $font-18px;
|
||||
}
|
||||
|
||||
/* Ellipsize any text overflow */
|
||||
text-overflow: ellipsis;
|
||||
.mx_RoomTile_title,
|
||||
.mx_RoomTile_subtitle_text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
@ -74,11 +79,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_RoomTile_subtitle {
|
||||
line-height: $font-18px;
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
.mx_RoomTile_titleWithSubtitle {
|
||||
margin-top: -3px; /* shift the title up a bit more */
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ limitations under the License.
|
|||
position: relative;
|
||||
|
||||
.mx_AvatarSetting_hover {
|
||||
transition: opacity $hover-transition;
|
||||
transition: opacity var(--hover-transition);
|
||||
|
||||
/* position to place the hover bg over the entire thing */
|
||||
position: absolute;
|
||||
|
|
|
@ -32,13 +32,9 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CryptographyPanel_importExportButtons .mx_AccessibleButton {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mx_CryptographyPanel_importExportButtons {
|
||||
margin-bottom: 15px;
|
||||
display: inline-flex;
|
||||
flex-flow: wrap;
|
||||
row-gap: 10px;
|
||||
row-gap: $spacing-8;
|
||||
column-gap: $spacing-8;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -14,63 +14,48 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_FontScalingPanel {
|
||||
color: $primary-content;
|
||||
.mx_FontScalingPanel_preview {
|
||||
--FontScalingPanel_preview-padding-block: 9px;
|
||||
|
||||
.mx_FontScalingPanel_preview,
|
||||
.mx_FontScalingPanel_fontSlider {
|
||||
margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end);
|
||||
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_FontScalingPanel_preview {
|
||||
--FontScalingPanel_preview-padding-block: 9px;
|
||||
|
||||
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_EventTile[data-layout="bubble"] {
|
||||
margin-top: 30px; /* TODO: Use a spacing variable */
|
||||
}
|
||||
|
||||
.mx_FontScalingPanel_fontSlider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px $spacing-20 35px; /* TODO: Use spacing variables */
|
||||
background: rgba($quinary-content, 0.2);
|
||||
border-radius: 10px;
|
||||
font-size: $font-10px;
|
||||
margin-top: $spacing-24;
|
||||
margin-bottom: $spacing-24;
|
||||
|
||||
.mx_FontScalingPanel_fontSlider_smallText,
|
||||
.mx_FontScalingPanel_fontSlider_largeText {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mx_FontScalingPanel_fontSlider_smallText {
|
||||
font-size: $font-15px;
|
||||
padding-inline-end: $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);
|
||||
.mx_EventTile_msgOption {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_FontScalingPanel_fontSlider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px $spacing-20 35px; /* TODO: Use spacing variables */
|
||||
background: rgba($quinary-content, 0.2);
|
||||
border-radius: 10px;
|
||||
font-size: $font-10px;
|
||||
|
||||
.mx_FontScalingPanel_fontSlider_smallText,
|
||||
.mx_FontScalingPanel_fontSlider_largeText {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mx_FontScalingPanel_fontSlider_smallText {
|
||||
font-size: $font-15px;
|
||||
padding-inline-end: $spacing-20;
|
||||
}
|
||||
|
||||
.mx_FontScalingPanel_fontSlider_largeText {
|
||||
font-size: $font-18px;
|
||||
padding-inline-start: $spacing-20;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,34 +14,31 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_ImageSizePanel {
|
||||
color: $primary-content;
|
||||
.mx_ImageSizePanel_radios {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $spacing-16;
|
||||
|
||||
.mx_ImageSizePanel_radios {
|
||||
display: flex;
|
||||
margin-top: 16px; /* move away from header a bit */
|
||||
> label {
|
||||
margin-right: 68px; /* keep the boxes separate */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> label {
|
||||
margin-right: 68px; /* keep the boxes separate */
|
||||
cursor: pointer;
|
||||
.mx_ImageSizePanel_size {
|
||||
background-color: $quinary-content;
|
||||
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 {
|
||||
background-color: $quinary-content;
|
||||
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");
|
||||
}
|
||||
&.mx_ImageSizePanel_sizeLarge {
|
||||
mask: url("$(res)/img/element-icons/settings/img-size-large.svg");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,6 @@ limitations under the License.
|
|||
|
||||
.mx_JoinRuleSettings_radioButton {
|
||||
padding-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.mx_StyledRadioButton_content {
|
||||
margin-left: 14px;
|
||||
|
|
|
@ -15,79 +15,78 @@ See the License for the specific language governing permissions and
|
|||
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;
|
||||
flex-direction: row;
|
||||
gap: 24px;
|
||||
flex-direction: column;
|
||||
|
||||
color: $primary-content;
|
||||
flex-basis: 33%;
|
||||
min-width: 0;
|
||||
|
||||
> .mx_LayoutSwitcher_RadioButton {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
border: 1px solid $quinary-content;
|
||||
border-radius: 10px;
|
||||
|
||||
.mx_EventTile_msgOption,
|
||||
.mx_MessageActionBar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_LayoutSwitcher_RadioButton_preview {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
pointer-events: none;
|
||||
|
||||
width: 300px;
|
||||
min-width: 0;
|
||||
|
||||
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_EventTile[data-layout="bubble"] .mx_EventTile_line {
|
||||
padding-right: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_StyledRadioButton {
|
||||
border-top: 1px solid $quinary-content;
|
||||
flex-grow: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mx_StyledRadioButton_checked {
|
||||
background-color: rgba($accent, 0.08);
|
||||
.mx_EventTile_content {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.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%;
|
||||
&.mx_LayoutSwitcher_RadioButton_selected {
|
||||
border-color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_StyledRadioButton {
|
||||
border-top: 1px solid $quinary-content;
|
||||
}
|
||||
|
||||
.mx_StyledRadioButton_checked {
|
||||
background-color: rgba($accent, 0.08);
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ limitations under the License.
|
|||
grid-template-columns: auto repeat(3, 62px);
|
||||
place-items: center center;
|
||||
grid-gap: 8px;
|
||||
margin-top: $spacing-40;
|
||||
|
||||
/* Override StyledRadioButton default styles */
|
||||
.mx_StyledRadioButton {
|
||||
|
@ -34,6 +33,11 @@ limitations under the License.
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// left align section heading
|
||||
.mx_SettingsSubsectionHeading {
|
||||
justify-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserNotifSettings_gridRowContainer {
|
||||
|
@ -51,10 +55,6 @@ limitations under the License.
|
|||
/* force it inline using float */
|
||||
float: left;
|
||||
}
|
||||
.mx_UserNotifSettings_gridRowHeading {
|
||||
font-size: $font-18px;
|
||||
font-weight: var(--font-semi-bold);
|
||||
}
|
||||
|
||||
.mx_UserNotifSettings_gridColumnLabel {
|
||||
color: $secondary-content;
|
||||
|
@ -70,39 +70,35 @@ limitations under the License.
|
|||
margin-top: -$spacing-4;
|
||||
}
|
||||
|
||||
.mx_UserNotifSettings {
|
||||
color: $primary-content; /* override from default settings page styles */
|
||||
.mx_UserNotifSettings_floatingSection {
|
||||
margin-top: 40px;
|
||||
|
||||
.mx_UserNotifSettings_floatingSection {
|
||||
margin-top: 40px;
|
||||
|
||||
& > div:first-child {
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
& > div:first-child {
|
||||
/* section header */
|
||||
font-size: $font-18px;
|
||||
font-weight: var(--font-semi-bold);
|
||||
}
|
||||
|
||||
.mx_UserNotifSettings_clearNotifsButton {
|
||||
> table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mx_TagComposer {
|
||||
margin-top: 35px; /* lots of distance from the last line of the table */
|
||||
tr > td:first-child {
|
||||
/* 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 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_ProfileSettings {
|
||||
margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end);
|
||||
border-bottom: 1px solid $quinary-content;
|
||||
|
||||
.mx_ProfileSettings_avatarUpload {
|
||||
|
@ -29,11 +28,13 @@ limitations under the License.
|
|||
flex-grow: 1;
|
||||
margin-inline-end: 54px;
|
||||
|
||||
.mx_Field:first-child {
|
||||
margin-top: 0;
|
||||
.mx_Field {
|
||||
margin-top: $spacing-8;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_profile_controls_topic {
|
||||
margin-top: $spacing-8;
|
||||
|
||||
& > textarea {
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
|
|
|
@ -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");
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_SetIdServer .mx_Field_input {
|
||||
margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end);
|
||||
.mx_SetIdServer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $spacing-8;
|
||||
|
||||
.mx_Field {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SetIdServer_tooltip {
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_SettingsFieldset {
|
||||
margin: 10px 80px 10px 0;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
|
@ -31,8 +30,6 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_SettingsFieldset_description {
|
||||
color: $settings-subsection-fg-color;
|
||||
font-size: $font-14px;
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
|
@ -46,3 +43,9 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SettingsFieldset_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-8;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
.mx_ExistingSpellCheckLanguage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.mx_ExistingSpellCheckLanguage_language {
|
||||
|
@ -26,10 +25,5 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_GeneralUserSettingsTab_spellCheckLanguageInput {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.mx_SpellCheckLanguages {
|
||||
margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end);
|
||||
margin-bottom: $spacing-8;
|
||||
}
|
||||
|
|
|
@ -14,53 +14,47 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_ThemeChoicePanel {
|
||||
.mx_ThemeChoicePanel_themeSelectors {
|
||||
color: $primary-content;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> .mx_ThemeSelectors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
> .mx_StyledRadioButton {
|
||||
padding: $font-16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
width: 180px;
|
||||
|
||||
margin-top: 4px;
|
||||
margin-bottom: 30px;
|
||||
background: $quinary-content;
|
||||
opacity: 0.4;
|
||||
|
||||
> .mx_StyledRadioButton {
|
||||
padding: $font-16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
width: 180px;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
|
||||
background: $quinary-content;
|
||||
opacity: 0.4;
|
||||
margin-right: 15px;
|
||||
margin-top: 10px;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
font-weight: var(--font-semi-bold);
|
||||
|
||||
margin-right: 15px;
|
||||
margin-top: 10px;
|
||||
> span {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
font-weight: var(--font-semi-bold);
|
||||
> .mx_StyledRadioButton_enabled {
|
||||
opacity: 1;
|
||||
|
||||
> span {
|
||||
justify-content: center;
|
||||
}
|
||||
/* These colors need to be hardcoded because they don't change with the theme */
|
||||
&.mx_ThemeSelector_light {
|
||||
background-color: #f3f8fd;
|
||||
color: #2e2f32;
|
||||
}
|
||||
|
||||
> .mx_StyledRadioButton_enabled {
|
||||
opacity: 1;
|
||||
|
||||
/* These colors need to be hardcoded because they don't change with the theme */
|
||||
&.mx_ThemeSelector_light {
|
||||
background-color: #f3f8fd;
|
||||
color: #2e2f32;
|
||||
}
|
||||
|
||||
&.mx_ThemeSelector_dark {
|
||||
/* 5% lightened version of 181b21 */
|
||||
background-color: #25282e;
|
||||
color: #f3f8fd;
|
||||
}
|
||||
&.mx_ThemeSelector_dark {
|
||||
/* 5% lightened version of 181b21 */
|
||||
background-color: #25282e;
|
||||
color: #f3f8fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
.mx_SettingsSection {
|
||||
--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 */
|
||||
|
||||
color: $primary-content;
|
||||
|
|
|
@ -15,9 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.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 */
|
||||
|
||||
color: $primary-content;
|
||||
|
@ -25,35 +22,30 @@ limitations under the License.
|
|||
a {
|
||||
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 {
|
||||
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 {
|
||||
color: $secondary-content;
|
||||
font-size: $font-14px;
|
||||
|
|
|
@ -14,25 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_AppearanceUserSettingsTab {
|
||||
--AppearanceUserSettingsTab_Field-margin-inline-start: calc($font-16px + 10px);
|
||||
|
||||
.mx_SettingsTab_subsectionText {
|
||||
margin-block: $spacing-12 $spacing-32;
|
||||
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);
|
||||
}
|
||||
}
|
||||
.mx_Field.mx_AppearanceUserSettingsTab_checkboxControlledField {
|
||||
width: 256px;
|
||||
// matches checkbox box + padding
|
||||
// to align with checkbox label
|
||||
margin-inline-start: calc($font-16px + 10px);
|
||||
}
|
||||
|
|
|
@ -14,42 +14,9 @@ See the License for the specific language governing permissions and
|
|||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.mx_GeneralUserSettingsTab_section--discovery_existing_address,
|
||||
|
@ -62,10 +29,8 @@ limitations under the License.
|
|||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.mx_GeneralUserSettingsTab_section--spellcheck .mx_ToggleSwitch {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.mx_GeneralUserSettingsTab_heading_warningIcon {
|
||||
.mx_GeneralUserSettingsTab_warningIcon {
|
||||
vertical-align: middle;
|
||||
margin-right: $spacing-8;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
|
|
@ -15,31 +15,26 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_KeyboardUserSettingsTab .mx_SettingsTab_section {
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mx_KeyboardShortcut_shortcutRow,
|
||||
.mx_KeyboardShortcut {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_KeyboardShortcut_shortcutRow {
|
||||
column-gap: $spacing-8;
|
||||
margin-bottom: $spacing-4;
|
||||
|
||||
/* TODO: Use flexbox */
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mx_KeyboardShortcut {
|
||||
flex-wrap: nowrap;
|
||||
column-gap: 5px; /* TODO: Use a spacing variable */
|
||||
}
|
||||
}
|
||||
.mx_KeyboardShortcut_shortcutList {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-gap: $spacing-4;
|
||||
}
|
||||
|
||||
.mx_KeyboardShortcut_shortcutRow,
|
||||
.mx_KeyboardShortcut {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_KeyboardShortcut_shortcutRow {
|
||||
column-gap: $spacing-8;
|
||||
}
|
||||
|
||||
.mx_KeyboardShortcut {
|
||||
flex-wrap: nowrap;
|
||||
column-gap: $spacing-4;
|
||||
}
|
||||
|
|
|
@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_MjolnirUserSettingsTab .mx_Field {
|
||||
margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end);
|
||||
}
|
||||
|
||||
.mx_MjolnirUserSettingsTab_listItem {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
|
|
@ -14,43 +14,36 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_SecurityUserSettingsTab_bulkOptions .mx_AccessibleButton {
|
||||
margin-right: 10px;
|
||||
.mx_SecurityUserSettingsTab_bulkOptions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
column-gap: $spacing-8;
|
||||
}
|
||||
|
||||
.mx_SecurityUserSettingsTab_ignoredUser {
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: $spacing-4;
|
||||
}
|
||||
|
||||
.mx_SecurityUserSettingsTab_ignoredUser .mx_AccessibleButton {
|
||||
margin-right: 10px;
|
||||
margin-right: $spacing-8;
|
||||
}
|
||||
|
||||
.mx_SecurityUserSettingsTab {
|
||||
.mx_SettingsTab_section {
|
||||
.mx_AccessibleButton_kind_link {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
.mx_SecurityUserSettingsTab_warning {
|
||||
color: $alert;
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
|
||||
.mx_SecurityUserSettingsTab_warning {
|
||||
color: $alert;
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
margin-top: 30px;
|
||||
|
||||
&::before {
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 0 center;
|
||||
mask-size: $font-24px;
|
||||
position: absolute;
|
||||
width: $font-24px;
|
||||
height: $font-24px;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: $alert;
|
||||
mask-image: url("$(res)/img/feather-customised/alert-triangle.svg");
|
||||
}
|
||||
&::before {
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 0 center;
|
||||
mask-size: $font-24px;
|
||||
position: absolute;
|
||||
width: $font-24px;
|
||||
height: $font-24px;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: $alert;
|
||||
mask-image: url("$(res)/img/feather-customised/alert-triangle.svg");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,67 +14,25 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_SidebarUserSettingsTab {
|
||||
.mx_Checkbox {
|
||||
margin-top: 12px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $secondary-content;
|
||||
}
|
||||
.mx_SidebarUserSettingsTab_homeAllRoomsCheckbox {
|
||||
margin-left: 24px;
|
||||
|
||||
.mx_SidebarUserSettingsTab_checkboxMicrocopy {
|
||||
margin-bottom: 12px;
|
||||
margin-left: 24px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
.mx_SidebarUserSettingsTab_homeAllRoomsCheckbox {
|
||||
margin-left: 24px;
|
||||
|
||||
& + div {
|
||||
margin-left: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SidebarUserSettingsTab_homeCheckbox,
|
||||
.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");
|
||||
& + div {
|
||||
margin-left: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SidebarUserSettingsTab_checkbox {
|
||||
margin-bottom: $spacing-8;
|
||||
// override checkbox styles˚
|
||||
label {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-right: $spacing-8;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
3
res/img/compound/thread-16px.svg
Normal 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 |
|
@ -1,3 +1,3 @@
|
|||
<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>
|
||||
|
|
Before Width: | Height: | Size: 517 B After Width: | Height: | Size: 522 B |
|
@ -1,3 +1,3 @@
|
|||
<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>
|
||||
|
|
Before Width: | Height: | Size: 444 B After Width: | Height: | Size: 449 B |
|
@ -1,3 +1,3 @@
|
|||
<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>
|
||||
|
|
Before Width: | Height: | Size: 821 B After Width: | Height: | Size: 826 B |
|
@ -1,5 +1,5 @@
|
|||
<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="10" cy="10" r="1.5" transform="rotate(180 10 10)" fill="#15191E"/>
|
||||
<circle cx="4.5" cy="10" r="1.5" transform="rotate(180 4.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="currentColor"/>
|
||||
<circle cx="4.5" cy="10" r="1.5" transform="rotate(180 4.5 10)" fill="currentColor"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 358 B After Width: | Height: | Size: 373 B |
|
@ -2,6 +2,6 @@
|
|||
<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"/>
|
||||
</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 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 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="currentColor" mask="url(#path-1-inside-1)"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
@ -1,3 +1,3 @@
|
|||
<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>
|
||||
|
|
Before Width: | Height: | Size: 524 B After Width: | Height: | Size: 531 B |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
@ -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"
|
||||
viewBox="0 0 14 12" style="enable-background:new 0 0 14 12;" xml:space="preserve">
|
||||
<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>
|
||||
<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
|
||||
|
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 709 B |
|
@ -1,5 +1,5 @@
|
|||
<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"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 304 B After Width: | Height: | Size: 309 B |
|
@ -1,23 +1,17 @@
|
|||
/* Nunito lacks combining diacritics, so these will fall through
|
||||
to the next font. Helevetica's diacritics sometimes do not combine
|
||||
nicely (on OSX, at least) and result in a huge horizontal mess.
|
||||
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. */
|
||||
Arial empirically gets it right, hence prioritising Arial here. */
|
||||
/* We fall through to Twemoji for emoji rather than falling through
|
||||
to native Emoji fonts (if any) to ensure cross-browser consistency */
|
||||
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
||||
digits in flowed text to stand out.
|
||||
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,
|
||||
"STIXGeneral", "Noto Color Emoji";
|
||||
"Noto Color Emoji";
|
||||
|
||||
$monospace-font-family: "Inconsolata", "Twemoji", "Apple Color Emoji", "Segoe UI Emoji", "Courier", monospace,
|
||||
"STIXGeneral", "Noto Color Emoji";
|
||||
"Noto Color Emoji";
|
||||
|
||||
/* unified palette */
|
||||
/* try to use these colors when possible */
|
||||
|
|
|
@ -82,11 +82,11 @@ $roomtopic-color: $secondary-content;
|
|||
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;
|
||||
}
|
||||
|
||||
.mx_ThemeChoicePanel > .mx_ThemeSelectors > .mx_StyledRadioButton.mx_StyledRadioButton_disabled {
|
||||
.mx_ThemeChoicePanel_themeSelectors > .mx_StyledRadioButton.mx_StyledRadioButton_disabled {
|
||||
color: $primary-content;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,17 @@
|
|||
/* Nunito and Inter lacks combining diacritics, so these will fall through
|
||||
to the next font. Helevetica's diacritics sometimes do not combine
|
||||
nicely (on OSX, at least) and result in a huge horizontal mess.
|
||||
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. */
|
||||
Arial empirically gets it right, hence prioritising Arial here. */
|
||||
/* We fall through to Twemoji for emoji rather than falling through
|
||||
to native Emoji fonts (if any) to ensure cross-browser consistency */
|
||||
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
||||
digits in flowed text to stand out.
|
||||
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";
|
||||
|
||||
$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 */
|
||||
/* ******************** */
|
||||
|
|
|
@ -10,8 +10,15 @@ defbranch="$3"
|
|||
|
||||
rm -r "$defrepo" || true
|
||||
|
||||
PR_ORG=${PR_ORG:-"matrix-org"}
|
||||
PR_REPO=${PR_REPO:-"matrix-react-sdk"}
|
||||
# figure out where to look for pull requests:
|
||||
# - 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
|
||||
clone() {
|
||||
|
|
|
@ -16,18 +16,17 @@ See the License for the specific language governing permissions and
|
|||
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 { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import Modal from "./Modal";
|
||||
import { _t, UserFriendlyError } from "./languageHandler";
|
||||
import IdentityAuthClient from "./IdentityAuthClient";
|
||||
import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents";
|
||||
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
|
||||
|
||||
function getIdServerDomain(): string {
|
||||
const idBaseUrl = MatrixClientPeg.get().getIdentityServerUrl(true);
|
||||
function getIdServerDomain(matrixClient: MatrixClient): string {
|
||||
const idBaseUrl = matrixClient.getIdentityServerUrl(true);
|
||||
if (!idBaseUrl) {
|
||||
throw new UserFriendlyError("Identity server not set");
|
||||
}
|
||||
|
@ -55,11 +54,11 @@ export type Binding = {
|
|||
export default class AddThreepid {
|
||||
private sessionId: string;
|
||||
private submitUrl?: string;
|
||||
private clientSecret: string;
|
||||
private bind: boolean;
|
||||
private bind = false;
|
||||
private readonly clientSecret: string;
|
||||
|
||||
public constructor() {
|
||||
this.clientSecret = MatrixClientPeg.get().generateClientSecret();
|
||||
public constructor(private readonly matrixClient: MatrixClient) {
|
||||
this.clientSecret = matrixClient.generateClientSecret();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -70,7 +69,7 @@ export default class AddThreepid {
|
|||
*/
|
||||
public async addEmailAddress(emailAddress: string): Promise<IRequestTokenResponse> {
|
||||
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;
|
||||
return res;
|
||||
} catch (err) {
|
||||
|
@ -90,12 +89,12 @@ export default class AddThreepid {
|
|||
*/
|
||||
public async bindEmailAddress(emailAddress: string): Promise<IRequestTokenResponse> {
|
||||
this.bind = true;
|
||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
if (await this.matrixClient.doesServerSupportSeparateAddAndBind()) {
|
||||
// For separate bind, request a token directly from the IS.
|
||||
const authClient = new IdentityAuthClient();
|
||||
const identityAccessToken = (await authClient.getAccessToken()) ?? undefined;
|
||||
try {
|
||||
const res = await MatrixClientPeg.get().requestEmailToken(
|
||||
const res = await this.matrixClient.requestEmailToken(
|
||||
emailAddress,
|
||||
this.clientSecret,
|
||||
1,
|
||||
|
@ -126,7 +125,7 @@ export default class AddThreepid {
|
|||
*/
|
||||
public async addMsisdn(phoneCountry: string, phoneNumber: string): Promise<IRequestMsisdnTokenResponse> {
|
||||
try {
|
||||
const res = await MatrixClientPeg.get().requestAdd3pidMsisdnToken(
|
||||
const res = await this.matrixClient.requestAdd3pidMsisdnToken(
|
||||
phoneCountry,
|
||||
phoneNumber,
|
||||
this.clientSecret,
|
||||
|
@ -153,12 +152,12 @@ export default class AddThreepid {
|
|||
*/
|
||||
public async bindMsisdn(phoneCountry: string, phoneNumber: string): Promise<IRequestMsisdnTokenResponse> {
|
||||
this.bind = true;
|
||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
if (await this.matrixClient.doesServerSupportSeparateAddAndBind()) {
|
||||
// For separate bind, request a token directly from the IS.
|
||||
const authClient = new IdentityAuthClient();
|
||||
const identityAccessToken = (await authClient.getAccessToken()) ?? undefined;
|
||||
try {
|
||||
const res = await MatrixClientPeg.get().requestMsisdnToken(
|
||||
const res = await this.matrixClient.requestMsisdnToken(
|
||||
phoneCountry,
|
||||
phoneNumber,
|
||||
this.clientSecret,
|
||||
|
@ -189,17 +188,17 @@ export default class AddThreepid {
|
|||
*/
|
||||
public async checkEmailLinkClicked(): Promise<[success?: boolean, result?: IAuthData | Error | null]> {
|
||||
try {
|
||||
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||
if (await this.matrixClient.doesServerSupportSeparateAddAndBind()) {
|
||||
if (this.bind) {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const identityAccessToken = await authClient.getAccessToken();
|
||||
if (!identityAccessToken) {
|
||||
throw new UserFriendlyError("No identity access token found");
|
||||
}
|
||||
await MatrixClientPeg.get().bindThreePid({
|
||||
await this.matrixClient.bindThreePid({
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
id_server: getIdServerDomain(),
|
||||
id_server: getIdServerDomain(this.matrixClient),
|
||||
id_access_token: identityAccessToken,
|
||||
});
|
||||
} else {
|
||||
|
@ -233,7 +232,7 @@ export default class AddThreepid {
|
|||
};
|
||||
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
||||
title: _t("Add Email Address"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
matrixClient: this.matrixClient,
|
||||
authData: err.data,
|
||||
makeRequest: this.makeAddThreepidOnlyRequest,
|
||||
aestheticsForStagePhases: {
|
||||
|
@ -245,11 +244,11 @@ export default class AddThreepid {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
await MatrixClientPeg.get().addThreePid(
|
||||
await this.matrixClient.addThreePid(
|
||||
{
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
id_server: getIdServerDomain(),
|
||||
id_server: getIdServerDomain(this.matrixClient),
|
||||
},
|
||||
this.bind,
|
||||
);
|
||||
|
@ -272,7 +271,7 @@ export default class AddThreepid {
|
|||
* @return {Promise<Object>} Response from /3pid/add call (in current spec, an empty object)
|
||||
*/
|
||||
private makeAddThreepidOnlyRequest = (auth?: { type: string; session?: string }): Promise<{}> => {
|
||||
return MatrixClientPeg.get().addThreePidOnly({
|
||||
return this.matrixClient.addThreePidOnly({
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
auth,
|
||||
|
@ -291,18 +290,18 @@ export default class AddThreepid {
|
|||
msisdnToken: string,
|
||||
): Promise<[success?: boolean, result?: IAuthData | Error | null] | undefined> {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const supportsSeparateAddAndBind = await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind();
|
||||
const supportsSeparateAddAndBind = await this.matrixClient.doesServerSupportSeparateAddAndBind();
|
||||
|
||||
let result: { success: boolean } | MatrixError;
|
||||
if (this.submitUrl) {
|
||||
result = await MatrixClientPeg.get().submitMsisdnTokenOtherUrl(
|
||||
result = await this.matrixClient.submitMsisdnTokenOtherUrl(
|
||||
this.submitUrl,
|
||||
this.sessionId,
|
||||
this.clientSecret,
|
||||
msisdnToken,
|
||||
);
|
||||
} else if (this.bind || !supportsSeparateAddAndBind) {
|
||||
result = await MatrixClientPeg.get().submitMsisdnToken(
|
||||
result = await this.matrixClient.submitMsisdnToken(
|
||||
this.sessionId,
|
||||
this.clientSecret,
|
||||
msisdnToken,
|
||||
|
@ -317,10 +316,10 @@ export default class AddThreepid {
|
|||
|
||||
if (supportsSeparateAddAndBind) {
|
||||
if (this.bind) {
|
||||
await MatrixClientPeg.get().bindThreePid({
|
||||
await this.matrixClient.bindThreePid({
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
id_server: getIdServerDomain(),
|
||||
id_server: getIdServerDomain(this.matrixClient),
|
||||
id_access_token: await authClient.getAccessToken(),
|
||||
});
|
||||
} else {
|
||||
|
@ -354,7 +353,7 @@ export default class AddThreepid {
|
|||
};
|
||||
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
||||
title: _t("Add Phone Number"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
matrixClient: this.matrixClient,
|
||||
authData: err.data,
|
||||
makeRequest: this.makeAddThreepidOnlyRequest,
|
||||
aestheticsForStagePhases: {
|
||||
|
@ -366,11 +365,11 @@ export default class AddThreepid {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
await MatrixClientPeg.get().addThreePid(
|
||||
await this.matrixClient.addThreePid(
|
||||
{
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
id_server: getIdServerDomain(),
|
||||
id_server: getIdServerDomain(this.matrixClient),
|
||||
},
|
||||
this.bind,
|
||||
);
|
||||
|
|
|
@ -18,11 +18,11 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
|||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
|
||||
import { split } from "lodash";
|
||||
|
||||
import DMRoomMap from "./utils/DMRoomMap";
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
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
|
||||
export function avatarUrlForMember(
|
||||
|
@ -133,8 +133,7 @@ export function getInitialLetter(name: string): string | undefined {
|
|||
name = name.substring(1);
|
||||
}
|
||||
|
||||
// rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis
|
||||
return split(name, "", 1)[0].toUpperCase();
|
||||
return getFirstGrapheme(name).toUpperCase();
|
||||
}
|
||||
|
||||
export function avatarUrlForRoom(
|
||||
|
|
|
@ -436,15 +436,18 @@ export default class ContentMessages {
|
|||
}
|
||||
}
|
||||
|
||||
promBefore = doMaybeLocalRoomAction(roomId, (actualRoomId) =>
|
||||
this.sendContentToRoom(
|
||||
file,
|
||||
actualRoomId,
|
||||
relation,
|
||||
matrixClient,
|
||||
replyToEvent ?? undefined,
|
||||
loopPromiseBefore,
|
||||
),
|
||||
promBefore = doMaybeLocalRoomAction(
|
||||
roomId,
|
||||
(actualRoomId) =>
|
||||
this.sendContentToRoom(
|
||||
file,
|
||||
actualRoomId,
|
||||
relation,
|
||||
matrixClient,
|
||||
replyToEvent ?? undefined,
|
||||
loopPromiseBefore,
|
||||
),
|
||||
matrixClient,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -580,13 +583,13 @@ export default class ContentMessages {
|
|||
} catch (error) {
|
||||
// 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
|
||||
if (error?.httpStatus === 413) {
|
||||
if (error instanceof HTTPError && error.httpStatus === 413) {
|
||||
this.mediaConfig = null;
|
||||
}
|
||||
|
||||
if (!upload.cancelled) {
|
||||
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", {
|
||||
fileName: upload.fileName,
|
||||
});
|
||||
|
|
|
@ -17,11 +17,10 @@ limitations under the License.
|
|||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
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 { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
import {
|
||||
hideToast as hideBulkUnverifiedSessionsToast,
|
||||
|
@ -67,6 +66,8 @@ export default class DeviceListener {
|
|||
// The set of device IDs we're currently displaying toasts for
|
||||
private displayingToastsForDeviceIds = new Set<string>();
|
||||
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 enableBulkUnverifiedSessionsReminder = true;
|
||||
private deviceClientInformationSettingWatcherRef: string | undefined;
|
||||
|
@ -76,16 +77,17 @@ export default class DeviceListener {
|
|||
return window.mxDeviceListener;
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
public start(matrixClient: MatrixClient): void {
|
||||
this.running = true;
|
||||
MatrixClientPeg.get().on(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices);
|
||||
MatrixClientPeg.get().on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
|
||||
MatrixClientPeg.get().on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
|
||||
MatrixClientPeg.get().on(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged);
|
||||
MatrixClientPeg.get().on(ClientEvent.AccountData, this.onAccountData);
|
||||
MatrixClientPeg.get().on(ClientEvent.Sync, this.onSync);
|
||||
MatrixClientPeg.get().on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.client = matrixClient;
|
||||
this.client.on(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices);
|
||||
this.client.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
|
||||
this.client.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
|
||||
this.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
|
||||
this.client.on(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged);
|
||||
this.client.on(ClientEvent.AccountData, this.onAccountData);
|
||||
this.client.on(ClientEvent.Sync, this.onSync);
|
||||
this.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.shouldRecordClientInformation = SettingsStore.getValue("deviceClientInformationOptIn");
|
||||
// only configurable in config, so we don't need to watch the value
|
||||
this.enableBulkUnverifiedSessionsReminder = SettingsStore.getValue(UIFeature.BulkUnverifiedSessionsReminder);
|
||||
|
@ -101,18 +103,15 @@ export default class DeviceListener {
|
|||
|
||||
public stop(): void {
|
||||
this.running = false;
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices);
|
||||
MatrixClientPeg.get().removeListener(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
|
||||
MatrixClientPeg.get().removeListener(
|
||||
CryptoEvent.DeviceVerificationChanged,
|
||||
this.onDeviceVerificationChanged,
|
||||
);
|
||||
MatrixClientPeg.get().removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
|
||||
MatrixClientPeg.get().removeListener(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged);
|
||||
MatrixClientPeg.get().removeListener(ClientEvent.AccountData, this.onAccountData);
|
||||
MatrixClientPeg.get().removeListener(ClientEvent.Sync, this.onSync);
|
||||
MatrixClientPeg.get().removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
if (this.client) {
|
||||
this.client.removeListener(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices);
|
||||
this.client.removeListener(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
|
||||
this.client.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
|
||||
this.client.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
|
||||
this.client.removeListener(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged);
|
||||
this.client.removeListener(ClientEvent.AccountData, this.onAccountData);
|
||||
this.client.removeListener(ClientEvent.Sync, this.onSync);
|
||||
this.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
}
|
||||
if (this.deviceClientInformationSettingWatcherRef) {
|
||||
SettingsStore.unwatchSetting(this.deviceClientInformationSettingWatcherRef);
|
||||
|
@ -128,6 +127,7 @@ export default class DeviceListener {
|
|||
this.keyBackupStatusChecked = false;
|
||||
this.ourDeviceIdsAtStart = null;
|
||||
this.displayingToastsForDeviceIds = new Set();
|
||||
this.client = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -160,22 +160,23 @@ export default class DeviceListener {
|
|||
* @returns the set of device IDs
|
||||
*/
|
||||
private async getDeviceIds(): Promise<Set<string>> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const crypto = cli.getCrypto();
|
||||
const cli = this.client;
|
||||
const crypto = cli?.getCrypto();
|
||||
if (crypto === undefined) return new Set();
|
||||
|
||||
const userId = cli.getSafeUserId();
|
||||
const userId = cli!.getSafeUserId();
|
||||
const devices = await crypto.getUserDeviceInfo([userId]);
|
||||
return new Set(devices.get(userId)?.keys() ?? []);
|
||||
}
|
||||
|
||||
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),
|
||||
// then they are all pre-existing devices, so ignore this and set the
|
||||
// devicesAtStart list to the devices that we see after the fetch.
|
||||
if (initialFetch) return;
|
||||
|
||||
const myUserId = MatrixClientPeg.get().getUserId()!;
|
||||
const myUserId = this.client.getSafeUserId();
|
||||
if (users.includes(myUserId)) await this.ensureDeviceIdsAtStartPopulated();
|
||||
|
||||
// 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 => {
|
||||
if (!users.includes(MatrixClientPeg.get().getUserId()!)) return;
|
||||
if (!this.client) return;
|
||||
if (!users.includes(this.client.getSafeUserId())) return;
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
private onDeviceVerificationChanged = (userId: string): void => {
|
||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
if (!this.client) return;
|
||||
if (userId !== this.client.getUserId()) return;
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
private onUserTrustStatusChanged = (userId: string): void => {
|
||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
if (!this.client) return;
|
||||
if (userId !== this.client.getUserId()) return;
|
||||
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
|
||||
// & cache the result
|
||||
private async getKeyBackupInfo(): Promise<IKeyBackupInfo | null> {
|
||||
if (!this.client) return null;
|
||||
const now = new Date().getTime();
|
||||
if (
|
||||
!this.keyBackupInfo ||
|
||||
!this.keyBackupFetchedAt ||
|
||||
this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL
|
||||
) {
|
||||
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
this.keyBackupInfo = await this.client.getKeyBackupVersion();
|
||||
this.keyBackupFetchedAt = now;
|
||||
}
|
||||
return this.keyBackupInfo;
|
||||
|
@ -256,13 +261,13 @@ export default class DeviceListener {
|
|||
// modifying the state involved here, so don't add new toasts to setup.
|
||||
if (isSecretStorageBeingAccessed()) return false;
|
||||
// Show setup toasts once the user is in at least one encrypted room.
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli && cli.getRooms().some((r) => cli.isRoomEncrypted(r.roomId));
|
||||
const cli = this.client;
|
||||
return cli?.getRooms().some((r) => cli.isRoomEncrypted(r.roomId)) ?? false;
|
||||
}
|
||||
|
||||
private async recheck(): Promise<void> {
|
||||
if (!this.running) return; // we have been stopped
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!this.running || !this.client) return; // we have been stopped
|
||||
const cli = this.client;
|
||||
|
||||
// cross-signing support was added to Matrix in MSC1756, which landed in spec v1.1
|
||||
if (!(await cli.isVersionSupported("v1.1"))) return;
|
||||
|
@ -285,11 +290,11 @@ export default class DeviceListener {
|
|||
this.checkKeyBackupStatus();
|
||||
} else if (this.shouldShowSetupEncryptionToast()) {
|
||||
// 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
|
||||
// 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)
|
||||
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
|
||||
this.checkKeyBackupStatus();
|
||||
|
@ -301,7 +306,7 @@ export default class DeviceListener {
|
|||
} else {
|
||||
// No cross-signing or key backup on account (set up encryption)
|
||||
await cli.waitForClientWellKnown();
|
||||
if (isSecureBackupRequired() && isLoggedIn()) {
|
||||
if (isSecureBackupRequired(cli) && isLoggedIn()) {
|
||||
// If we're meant to set up, and Secure Backup is required,
|
||||
// trigger the flow directly without a toast once logged in.
|
||||
hideSetupEncryptionToast();
|
||||
|
@ -327,7 +332,9 @@ export default class DeviceListener {
|
|||
|
||||
const isCurrentDeviceTrusted =
|
||||
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,
|
||||
// you can't see or dismiss any device toasts
|
||||
|
@ -336,7 +343,7 @@ export default class DeviceListener {
|
|||
for (const deviceId of devices) {
|
||||
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 (this.ourDeviceIdsAtStart?.has(deviceId)) {
|
||||
oldUnverifiedDeviceIds.add(deviceId);
|
||||
|
@ -383,11 +390,11 @@ export default class DeviceListener {
|
|||
}
|
||||
|
||||
private checkKeyBackupStatus = async (): Promise<void> => {
|
||||
if (this.keyBackupStatusChecked) {
|
||||
if (this.keyBackupStatusChecked || !this.client) {
|
||||
return;
|
||||
}
|
||||
// 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;
|
||||
|
||||
if (isKeyBackupEnabled === false) {
|
||||
|
@ -412,11 +419,12 @@ export default class DeviceListener {
|
|||
};
|
||||
|
||||
private updateClientInformation = async (): Promise<void> => {
|
||||
if (!this.client) return;
|
||||
try {
|
||||
if (this.shouldRecordClientInformation) {
|
||||
await recordClientInformation(MatrixClientPeg.get(), SdkConfig.get(), PlatformPeg.get() ?? undefined);
|
||||
await recordClientInformation(this.client, SdkConfig.get(), PlatformPeg.get() ?? undefined);
|
||||
} else {
|
||||
await removeClientInformation(MatrixClientPeg.get());
|
||||
await removeClientInformation(this.client);
|
||||
}
|
||||
} catch (error) {
|
||||
// this is a best effort operation
|
||||
|
|
|
@ -19,16 +19,16 @@ limitations under the License.
|
|||
|
||||
import React, { LegacyRef, ReactElement, ReactNode } from "react";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { load as cheerio } from "cheerio";
|
||||
import classNames from "classnames";
|
||||
import EMOJIBASE_REGEX from "emojibase-regex";
|
||||
import { merge, split } from "lodash";
|
||||
import { merge } from "lodash";
|
||||
import katex from "katex";
|
||||
import { decode } from "html-entities";
|
||||
import { IContent } from "matrix-js-sdk/src/models/event";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
import _Linkify from "linkify-react";
|
||||
import escapeHtml from "escape-html";
|
||||
import GraphemeSplitter from "grapheme-splitter";
|
||||
|
||||
import {
|
||||
_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
|
||||
* 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 result: (JSX.Element | string)[] = [];
|
||||
if (!message) return result;
|
||||
|
||||
let text = "";
|
||||
let key = 0;
|
||||
|
||||
// We use lodash's grapheme splitter to avoid breaking apart compound emojis
|
||||
for (const char of split(message, "")) {
|
||||
const splitter = new GraphemeSplitter();
|
||||
for (const char of splitter.iterateGraphemes(message)) {
|
||||
if (EMOJIBASE_REGEX.test(char)) {
|
||||
if (text) {
|
||||
result.push(text);
|
||||
|
@ -549,30 +553,19 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
|
|||
}
|
||||
|
||||
safeBody = sanitizeHtml(formattedBody!, sanitizeParams);
|
||||
const phtml = cheerio(safeBody, {
|
||||
// @ts-ignore: The `_useHtmlParser2` internal option is the
|
||||
// simplest way to both parse and render using `htmlparser2`.
|
||||
_useHtmlParser2: true,
|
||||
decodeEntities: false,
|
||||
});
|
||||
const isPlainText = phtml.html() === phtml.root().text();
|
||||
const phtml = new DOMParser().parseFromString(safeBody, "text/html");
|
||||
const isPlainText = phtml.body.innerHTML === phtml.body.textContent;
|
||||
isHtmlMessage = !isPlainText;
|
||||
|
||||
if (isHtmlMessage && SettingsStore.getValue("feature_latex_maths")) {
|
||||
// @ts-ignore - The types for `replaceWith` wrongly expect
|
||||
// Cheerio instance to be returned.
|
||||
phtml('div, span[data-mx-maths!=""]').replaceWith(function (i, e) {
|
||||
return katex.renderToString(decode(phtml(e).attr("data-mx-maths")), {
|
||||
[...phtml.querySelectorAll<HTMLElement>("div, span[data-mx-maths]")].forEach((e) => {
|
||||
e.outerHTML = katex.renderToString(decode(e.getAttribute("data-mx-maths")), {
|
||||
throwOnError: false,
|
||||
// @ts-ignore - `e` can be an Element, not just a Node
|
||||
displayMode: e.name == "div",
|
||||
displayMode: e.tagName == "DIV",
|
||||
output: "htmlAndMathml",
|
||||
});
|
||||
});
|
||||
safeBody = phtml.html();
|
||||
}
|
||||
if (bodyHasEmoji) {
|
||||
safeBody = formatEmojis(safeBody, true).join("");
|
||||
safeBody = phtml.body.innerHTML;
|
||||
}
|
||||
} else if (highlighter) {
|
||||
safeBody = highlighter.applyHighlights(escapeHtml(plainBody), safeHighlights!).join("");
|
||||
|
@ -581,13 +574,9 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
|
|||
delete sanitizeParams.textFilter;
|
||||
}
|
||||
|
||||
const contentBody = safeBody ?? strippedBody;
|
||||
if (opts.returnString) {
|
||||
return contentBody;
|
||||
}
|
||||
|
||||
let emojiBody = false;
|
||||
if (!opts.disableBigEmoji && bodyHasEmoji) {
|
||||
const contentBody = safeBody ?? strippedBody;
|
||||
let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : "";
|
||||
|
||||
// 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:")));
|
||||
}
|
||||
|
||||
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({
|
||||
"mx_EventTile_body": true,
|
||||
"mx_EventTile_bigEmoji": emojiBody,
|
||||
|
@ -668,7 +666,7 @@ export function topicToHtml(
|
|||
isFormattedTopic = false; // Fall back to plain-text topic
|
||||
}
|
||||
|
||||
let emojiBodyElements: ReturnType<typeof formatEmojis> | undefined;
|
||||
let emojiBodyElements: JSX.Element[] | undefined;
|
||||
if (!isFormattedTopic && topicHasEmoji) {
|
||||
emojiBodyElements = formatEmojis(topic, false);
|
||||
}
|
||||
|
|