Merge remote-tracking branch 'upstream/develop' into last-admin-leave-room-warning

This commit is contained in:
Arnei 2022-10-21 14:18:18 +02:00
commit b3e535a7aa
223 changed files with 7406 additions and 2005 deletions

View file

@ -155,17 +155,6 @@ jobs:
cypress/videos
cypress/synapselogs
- run: mv cypress/performance/*.json cypress/performance/measurements-${{ strategy.job-index }}.json
continue-on-error: true
- name: Upload Benchmark
uses: actions/upload-artifact@v2
with:
name: cypress-benchmark
path: cypress/performance/*
if-no-files-found: ignore
retention-days: 1
report:
name: Report results
needs: tests
@ -181,36 +170,3 @@ jobs:
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
store-benchmark:
needs: tests
runs-on: ubuntu-latest
if: |
github.event.workflow_run.event != 'pull_request' &&
github.event.workflow_run.head_branch == 'develop' &&
github.event.workflow_run.head_repository.full_name == github.repository
permissions:
contents: write
steps:
- uses: actions/checkout@v2
- name: Download benchmark result
uses: actions/download-artifact@v3
with:
name: cypress-benchmark
- name: Merge measurements
run: jq -s add measurements-*.json > measurements.json
- name: Store benchmark result
uses: matrix-org/github-action-benchmark@jsperfentry-6
with:
name: Cypress measurements
tool: 'jsperformanceentry'
output-file-path: measurements.json
# The dashboard is available at https://matrix-org.github.io/matrix-react-sdk/cypress/bench/
benchmark-data-dir-path: cypress/bench
fail-on-alert: false
comment-on-alert: false
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: ${{ github.event.workflow_run.event != 'pull_request' }}

View file

@ -1 +1 @@
14
16

View file

@ -0,0 +1,140 @@
/*
Copyright 2022 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.
*/
/// <reference types="cypress" />
import { SynapseInstance } from "../../plugins/synapsedocker";
import { SettingLevel } from "../../../src/settings/SettingLevel";
describe("Composer", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
synapse = data;
});
});
afterEach(() => {
cy.stopSynapse(synapse);
});
describe("CIDER", () => {
beforeEach(() => {
cy.initTestUser(synapse, "Janet").then(() => {
cy.createRoom({ name: "Composing Room" });
});
cy.viewRoomByName("Composing Room");
});
it("sends a message when you click send or press Enter", () => {
// Type a message
cy.get('div[contenteditable=true]').type('my message 0');
// It has not been sent yet
cy.contains('.mx_EventTile_body', 'my message 0').should('not.exist');
// Click send
cy.get('div[aria-label="Send message"]').click();
// It has been sent
cy.contains('.mx_EventTile_body', 'my message 0');
// Type another and press Enter afterwards
cy.get('div[contenteditable=true]').type('my message 1{enter}');
// It was sent
cy.contains('.mx_EventTile_body', 'my message 1');
});
it("can write formatted text", () => {
cy.get('div[contenteditable=true]').type('my bold{ctrl+b} message');
cy.get('div[aria-label="Send message"]').click();
// Note: both "bold" and "message" are bold, which is probably surprising
cy.contains('.mx_EventTile_body strong', 'bold message');
});
describe("when Ctrl+Enter is required to send", () => {
beforeEach(() => {
cy.setSettingValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);
});
it("only sends when you press Ctrl+Enter", () => {
// Type a message and press Enter
cy.get('div[contenteditable=true]').type('my message 3{enter}');
// It has not been sent yet
cy.contains('.mx_EventTile_body', 'my message 3').should('not.exist');
// Press Ctrl+Enter
cy.get('div[contenteditable=true]').type('{ctrl+enter}');
// It was sent
cy.contains('.mx_EventTile_body', 'my message 3');
});
});
});
describe("WYSIWYG", () => {
beforeEach(() => {
cy.enableLabsFeature("feature_wysiwyg_composer");
cy.initTestUser(synapse, "Janet").then(() => {
cy.createRoom({ name: "Composing Room" });
});
cy.viewRoomByName("Composing Room");
});
it("sends a message when you click send or press Enter", () => {
// Type a message
cy.get('div[contenteditable=true]').type('my message 0');
// It has not been sent yet
cy.contains('.mx_EventTile_body', 'my message 0').should('not.exist');
// Click send
cy.get('div[aria-label="Send message"]').click();
// It has been sent
cy.contains('.mx_EventTile_body', 'my message 0');
// Type another
cy.get('div[contenteditable=true]').type('my message 1');
// Press enter. Would be nice to just use {enter} but we can't because Cypress
// does not trigger an insertParagraph when you do that.
cy.get('div[contenteditable=true]').trigger('input', { inputType: "insertParagraph" });
// It was sent
cy.contains('.mx_EventTile_body', 'my message 1');
});
it("can write formatted text", () => {
cy.get('div[contenteditable=true]').type('my {ctrl+b}bold{ctrl+b} message');
cy.get('div[aria-label="Send message"]').click();
cy.contains('.mx_EventTile_body strong', 'bold');
});
describe("when Ctrl+Enter is required to send", () => {
beforeEach(() => {
cy.setSettingValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);
});
it("only sends when you press Ctrl+Enter", () => {
// Type a message and press Enter
cy.get('div[contenteditable=true]').type('my message 3');
cy.get('div[contenteditable=true]').trigger('input', { inputType: "insertParagraph" });
// It has not been sent yet
cy.contains('.mx_EventTile_body', 'my message 3').should('not.exist');
// Press Ctrl+Enter
cy.get('div[contenteditable=true]').type('{ctrl+enter}');
// It was sent
cy.contains('.mx_EventTile_body', 'my message 3');
});
});
});
});

View file

@ -54,13 +54,11 @@ describe("Create Room", () => {
// Fill room address
cy.get('[label="Room address"]').type("test-room-1");
// Submit
cy.startMeasuring("from-submit-to-room");
cy.get(".mx_Dialog_primary").click();
});
cy.url().should("contain", "/#/room/#test-room-1:localhost");
cy.stopMeasuring("from-submit-to-room");
cy.get(".mx_RoomHeader_nametext").contains(name);
cy.get(".mx_RoomHeader_topic").contains(topic);
cy.contains(".mx_RoomHeader_nametext", name);
cy.contains(".mx_RoomHeader_topic", topic);
});
});

View file

@ -62,7 +62,7 @@ describe("Editing", () => {
cy.get(".mx_BasicMessageComposer_input").type("Foo{backspace}{backspace}{backspace}{enter}");
cy.checkA11y();
});
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", "Message");
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Message");
// Assert that the edit composer has gone away
cy.get(".mx_EditMessageComposer").should("not.exist");

View file

@ -46,7 +46,7 @@ describe("Consent", () => {
// Accept terms & conditions
cy.get(".mx_QuestionDialog").within(() => {
cy.get("#mx_BaseDialog_title").contains("Terms and Conditions");
cy.contains("#mx_BaseDialog_title", "Terms and Conditions");
cy.get(".mx_Dialog_primary").click();
});
@ -58,7 +58,7 @@ describe("Consent", () => {
cy.visit(url);
cy.get('[type="submit"]').click();
cy.get("p").contains("Danke schon");
cy.contains("p", "Danke schon");
});
});

View file

@ -21,13 +21,6 @@ import { SynapseInstance } from "../../plugins/synapsedocker";
describe("Login", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.visit("/#/login");
cy.startSynapse("consent").then(data => {
synapse = data;
});
});
afterEach(() => {
cy.stopSynapse(synapse);
});
@ -37,7 +30,11 @@ describe("Login", () => {
const password = "p4s5W0rD";
beforeEach(() => {
cy.registerUser(synapse, username, password);
cy.startSynapse("consent").then(data => {
synapse = data;
cy.registerUser(synapse, username, password);
cy.visit("/#/login");
});
});
it("logs in with an existing account and lands on the home screen", () => {
@ -55,24 +52,25 @@ describe("Login", () => {
cy.get("#mx_LoginForm_username").type(username);
cy.get("#mx_LoginForm_password").type(password);
cy.startMeasuring("from-submit-to-home");
cy.get(".mx_Login_submit").click();
cy.url().should('contain', '/#/home', { timeout: 30000 });
cy.stopMeasuring("from-submit-to-home");
});
});
describe("logout", () => {
beforeEach(() => {
cy.initTestUser(synapse, "Erin");
cy.startSynapse("consent").then(data => {
synapse = data;
cy.initTestUser(synapse, "Erin");
});
});
it("should go to login page on logout", () => {
cy.get('[aria-label="User menu"]').click();
// give a change for the outstanding requests queue to settle before logging out
cy.wait(500);
cy.wait(2000);
cy.get(".mx_UserMenu_contextMenu").within(() => {
cy.get(".mx_UserMenu_iconSignOut").click();
@ -94,7 +92,7 @@ describe("Login", () => {
cy.get('[aria-label="User menu"]').click();
// give a change for the outstanding requests queue to settle before logging out
cy.wait(500);
cy.wait(2000);
cy.get(".mx_UserMenu_contextMenu").within(() => {
cy.get(".mx_UserMenu_iconSignOut").click();

View file

@ -94,7 +94,7 @@ describe("Polls", () => {
cy.stopSynapse(synapse);
});
it("Open polls can be created and voted in", () => {
it("should be creatable and votable", () => {
let bot: MatrixClient;
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
bot = _bot;
@ -122,7 +122,7 @@ describe("Polls", () => {
createPoll(pollParams);
// Wait for message to send, get its ID and save as @pollId
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", pollParams.title)
.invoke("attr", "data-scroll-tokens").as("pollId");
cy.get<string>("@pollId").then(pollId => {
@ -159,7 +159,95 @@ describe("Polls", () => {
});
});
it("displays polls correctly in thread panel", () => {
it("should be editable from context menu if no votes have been cast", () => {
let bot: MatrixClient;
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
bot = _bot;
});
let roomId: string;
cy.createRoom({}).then(_roomId => {
roomId = _roomId;
cy.inviteUser(roomId, bot.getUserId());
cy.visit('/#/room/' + roomId);
});
cy.openMessageComposerOptions().within(() => {
cy.get('[aria-label="Poll"]').click();
});
const pollParams = {
title: 'Does the polls feature work?',
options: ['Yes', 'No', 'Maybe'],
};
createPoll(pollParams);
// Wait for message to send, get its ID and save as @pollId
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
.invoke("attr", "data-scroll-tokens").as("pollId");
cy.get<string>("@pollId").then(pollId => {
// Open context menu
getPollTile(pollId).rightclick();
// Select edit item
cy.get('.mx_ContextualMenu').within(() => {
cy.get('[aria-label="Edit"]').click();
});
// Expect poll editing dialog
cy.get('.mx_PollCreateDialog');
});
});
it("should not be editable from context menu if votes have been cast", () => {
let bot: MatrixClient;
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
bot = _bot;
});
let roomId: string;
cy.createRoom({}).then(_roomId => {
roomId = _roomId;
cy.inviteUser(roomId, bot.getUserId());
cy.visit('/#/room/' + roomId);
});
cy.openMessageComposerOptions().within(() => {
cy.get('[aria-label="Poll"]').click();
});
const pollParams = {
title: 'Does the polls feature work?',
options: ['Yes', 'No', 'Maybe'],
};
createPoll(pollParams);
// Wait for message to send, get its ID and save as @pollId
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
.invoke("attr", "data-scroll-tokens").as("pollId");
cy.get<string>("@pollId").then(pollId => {
// Bot votes 'Maybe' in the poll
botVoteForOption(bot, roomId, pollId, pollParams.options[2]);
// wait for bot's vote to arrive
cy.get('.mx_MPollBody_totalVotes').should('contain', '1 vote cast');
// Open context menu
getPollTile(pollId).rightclick();
// Select edit item
cy.get('.mx_ContextualMenu').within(() => {
cy.get('[aria-label="Edit"]').click();
});
// Expect error dialog
cy.get('.mx_ErrorDialog');
});
});
it("should be displayed correctly in thread panel", () => {
let botBob: MatrixClient;
let botCharlie: MatrixClient;
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
@ -190,7 +278,7 @@ describe("Polls", () => {
createPoll(pollParams);
// Wait for message to send, get its ID and save as @pollId
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", pollParams.title)
.invoke("attr", "data-scroll-tokens").as("pollId");
cy.get<string>("@pollId").then(pollId => {

View file

@ -55,7 +55,6 @@ describe("Registration", () => {
cy.get("#mx_RegistrationForm_username").type("alice");
cy.get("#mx_RegistrationForm_password").type("totally a great password");
cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password");
cy.startMeasuring("create-account");
cy.get(".mx_Login_submit").click();
cy.get(".mx_RegistrationEmailPromptDialog").should("be.visible");
@ -63,13 +62,11 @@ describe("Registration", () => {
cy.checkA11y();
cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click();
cy.stopMeasuring("create-account");
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy").should("be.visible");
cy.percySnapshot("Registration terms prompt", { percyCSS });
cy.checkA11y();
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click();
cy.startMeasuring("from-submit-to-home");
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
cy.get(".mx_UseCaseSelection_skip", { timeout: 30000 }).should("exist");
@ -78,7 +75,6 @@ describe("Registration", () => {
cy.get(".mx_UseCaseSelection_skip .mx_AccessibleButton").click();
cy.url().should('contain', '/#/home');
cy.stopMeasuring("from-submit-to-home");
cy.get('[aria-label="User menu"]').click();
cy.get('[aria-label="Security & Privacy"]').click();

View file

@ -93,7 +93,7 @@ describe("Room Directory", () => {
cy.get(".mx_RoomDirectory_dialogWrapper").percySnapshotElement("Room Directory - filtered no results");
cy.get('.mx_RoomDirectory_dialogWrapper [name="dirsearch"]').type("{selectAll}{backspace}test1234");
cy.get(".mx_RoomDirectory_dialogWrapper").contains(".mx_RoomDirectory_listItem", name)
cy.contains(".mx_RoomDirectory_dialogWrapper .mx_RoomDirectory_listItem", name)
.should("exist").as("resultRow");
cy.get(".mx_RoomDirectory_dialogWrapper").percySnapshotElement("Room Directory - filtered one result");
cy.get("@resultRow").find(".mx_AccessibleButton").contains("Join").click();

View file

@ -293,7 +293,7 @@ describe("Sliding Sync", () => {
]);
cy.contains(".mx_RoomTile", "Reject").click();
cy.get(".mx_RoomView").contains(".mx_AccessibleButton", "Reject").click();
cy.contains(".mx_RoomView .mx_AccessibleButton", "Reject").click();
// wait for the rejected room to disappear
cy.get(".mx_RoomTile").should('have.length', 3);
@ -328,8 +328,8 @@ describe("Sliding Sync", () => {
cy.getClient().then(cli => cli.setRoomTag(roomId, "m.favourite", { order: 0.5 }));
});
cy.get('.mx_RoomSublist[aria-label="Favourites"]').contains(".mx_RoomTile", "Favourite DM").should("exist");
cy.get('.mx_RoomSublist[aria-label="People"]').contains(".mx_RoomTile", "Favourite DM").should("not.exist");
cy.contains('.mx_RoomSublist[aria-label="Favourites"] .mx_RoomTile', "Favourite DM").should("exist");
cy.contains('.mx_RoomSublist[aria-label="People"] .mx_RoomTile', "Favourite DM").should("not.exist");
});
// Regression test for a bug in SS mode, but would be useful to have in non-SS mode too.

View file

@ -83,26 +83,26 @@ describe("Spaces", () => {
cy.get('input[label="Name"]').type("Let's have a Riot");
cy.get('input[label="Address"]').should("have.value", "lets-have-a-riot");
cy.get('textarea[label="Description"]').type("This is a space to reminisce Riot.im!");
cy.get(".mx_AccessibleButton").contains("Create").click();
cy.contains(".mx_AccessibleButton", "Create").click();
});
// Create the default General & Random rooms, as well as a custom "Jokes" room
cy.get('input[label="Room name"][value="General"]').should("exist");
cy.get('input[label="Room name"][value="Random"]').should("exist");
cy.get('input[placeholder="Support"]').type("Jokes");
cy.get(".mx_AccessibleButton").contains("Continue").click();
cy.contains(".mx_AccessibleButton", "Continue").click();
// Copy matrix.to link
cy.get(".mx_SpacePublicShare_shareButton").focus().realClick();
cy.getClipboardText().should("eq", "https://matrix.to/#/#lets-have-a-riot:localhost");
// Go to space home
cy.get(".mx_AccessibleButton").contains("Go to my first room").click();
cy.contains(".mx_AccessibleButton", "Go to my first room").click();
// Assert rooms exist in the room list
cy.get(".mx_RoomList").contains(".mx_RoomTile", "General").should("exist");
cy.get(".mx_RoomList").contains(".mx_RoomTile", "Random").should("exist");
cy.get(".mx_RoomList").contains(".mx_RoomTile", "Jokes").should("exist");
cy.contains(".mx_RoomList .mx_RoomTile", "General").should("exist");
cy.contains(".mx_RoomList .mx_RoomTile", "Random").should("exist");
cy.contains(".mx_RoomList .mx_RoomTile", "Jokes").should("exist");
});
it("should allow user to create private space", () => {
@ -113,7 +113,7 @@ describe("Spaces", () => {
cy.get('input[label="Name"]').type("This is not a Riot");
cy.get('input[label="Address"]').should("not.exist");
cy.get('textarea[label="Description"]').type("This is a private space of mourning Riot.im...");
cy.get(".mx_AccessibleButton").contains("Create").click();
cy.contains(".mx_AccessibleButton", "Create").click();
});
cy.get(".mx_SpaceRoomView_privateScope_meAndMyTeammatesButton").click();
@ -122,20 +122,20 @@ describe("Spaces", () => {
cy.get('input[label="Room name"][value="General"]').should("exist");
cy.get('input[label="Room name"][value="Random"]').should("exist");
cy.get('input[placeholder="Support"]').type("Projects");
cy.get(".mx_AccessibleButton").contains("Continue").click();
cy.contains(".mx_AccessibleButton", "Continue").click();
cy.get(".mx_SpaceRoomView").should("contain", "Invite your teammates");
cy.get(".mx_AccessibleButton").contains("Skip for now").click();
cy.contains(".mx_AccessibleButton", "Skip for now").click();
// Assert rooms exist in the room list
cy.get(".mx_RoomList").contains(".mx_RoomTile", "General").should("exist");
cy.get(".mx_RoomList").contains(".mx_RoomTile", "Random").should("exist");
cy.get(".mx_RoomList").contains(".mx_RoomTile", "Projects").should("exist");
cy.contains(".mx_RoomList .mx_RoomTile", "General").should("exist");
cy.contains(".mx_RoomList .mx_RoomTile", "Random").should("exist");
cy.contains(".mx_RoomList .mx_RoomTile", "Projects").should("exist");
// Assert rooms exist in the space explorer
cy.get(".mx_SpaceHierarchy_list").contains(".mx_SpaceHierarchy_roomTile", "General").should("exist");
cy.get(".mx_SpaceHierarchy_list").contains(".mx_SpaceHierarchy_roomTile", "Random").should("exist");
cy.get(".mx_SpaceHierarchy_list").contains(".mx_SpaceHierarchy_roomTile", "Projects").should("exist");
cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "General").should("exist");
cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "Random").should("exist");
cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "Projects").should("exist");
});
it("should allow user to create just-me space", () => {
@ -155,10 +155,10 @@ describe("Spaces", () => {
cy.get(".mx_SpaceRoomView_privateScope_justMeButton").click();
cy.get(".mx_AddExistingToSpace_entry").click();
cy.get(".mx_AccessibleButton").contains("Add").click();
cy.contains(".mx_AccessibleButton", "Add").click();
cy.get(".mx_RoomList").contains(".mx_RoomTile", "Sample Room").should("exist");
cy.get(".mx_SpaceHierarchy_list").contains(".mx_SpaceHierarchy_roomTile", "Sample Room").should("exist");
cy.contains(".mx_RoomList .mx_RoomTile", "Sample Room").should("exist");
cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "Sample Room").should("exist");
});
it("should allow user to invite another to a space", () => {
@ -186,7 +186,7 @@ describe("Spaces", () => {
cy.get(".mx_InviteDialog_other").within(() => {
cy.get('input[type="text"]').type(bot.getUserId());
cy.get(".mx_AccessibleButton").contains("Invite").click();
cy.contains(".mx_AccessibleButton", "Invite").click();
});
cy.get(".mx_InviteDialog_other").should("not.exist");

View file

@ -53,6 +53,7 @@ describe("Threads", () => {
cy.window().should("have.prop", "beforeReload", true);
cy.leaveBeta("Threads");
cy.wait(1000);
// after reload the property should be gone
cy.window().should("not.have.prop", "beforeReload");
});
@ -66,6 +67,7 @@ describe("Threads", () => {
cy.window().should("have.prop", "beforeReload", true);
cy.joinBeta("Threads");
cy.wait(1000);
// after reload the property should be gone
cy.window().should("not.have.prop", "beforeReload");
});
@ -92,7 +94,7 @@ describe("Threads", () => {
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
// Wait for message to send, get its ID and save as @threadId
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.invoke("attr", "data-scroll-tokens").as("threadId");
// Bot starts thread
@ -116,21 +118,21 @@ describe("Threads", () => {
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Test");
// User reacts to message instead
cy.get(".mx_ThreadView .mx_EventTile").contains(".mx_EventTile_line", "Hello there")
cy.contains(".mx_ThreadView .mx_EventTile .mx_EventTile_line", "Hello there")
.find('[aria-label="React"]').click({ force: true }); // Cypress has no ability to hover
cy.get(".mx_EmojiPicker").within(() => {
cy.get('input[type="text"]').type("wave");
cy.get('[role="menuitem"]').contains("👋").click();
cy.contains('[role="menuitem"]', "👋").click();
});
// User redacts their prior response
cy.get(".mx_ThreadView .mx_EventTile").contains(".mx_EventTile_line", "Test")
cy.contains(".mx_ThreadView .mx_EventTile .mx_EventTile_line", "Test")
.find('[aria-label="Options"]').click({ force: true }); // Cypress has no ability to hover
cy.get(".mx_IconizedContextMenu").within(() => {
cy.get('[role="menuitem"]').contains("Remove").click();
cy.contains('[role="menuitem"]', "Remove").click();
});
cy.get(".mx_TextInputDialog").within(() => {
cy.get(".mx_Dialog_primary").contains("Remove").click();
cy.contains(".mx_Dialog_primary", "Remove").click();
});
// User asserts summary was updated correctly
@ -171,7 +173,7 @@ describe("Threads", () => {
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Great!");
// User edits & asserts
cy.get(".mx_ThreadView .mx_EventTile_last").contains(".mx_EventTile_line", "Great!").within(() => {
cy.contains(".mx_ThreadView .mx_EventTile_last .mx_EventTile_line", "Great!").within(() => {
cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover
cy.get(".mx_BasicMessageComposer_input").type(" How about yourself?{enter}");
});
@ -234,7 +236,7 @@ describe("Threads", () => {
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
// Create thread
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.realHover().find(".mx_MessageActionBar_threadButton").click();
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);
@ -256,7 +258,7 @@ describe("Threads", () => {
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
// Create thread
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.realHover().find(".mx_MessageActionBar_threadButton").click();
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);
@ -268,7 +270,7 @@ describe("Threads", () => {
cy.get(".mx_BaseCard_close").click();
// Open existing thread
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.realHover().find(".mx_MessageActionBar_threadButton").click();
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);
cy.get(".mx_BaseCard .mx_EventTile").should("contain", "Hello Mr. Bot");

View file

@ -329,7 +329,7 @@ describe("Timeline", () => {
cy.getComposer().type(`${MESSAGE}{enter}`);
// Reply to the message
cy.get(".mx_RoomView_body").contains(".mx_EventTile_line", "Hello world").within(() => {
cy.contains(".mx_RoomView_body .mx_EventTile_line", "Hello world").within(() => {
cy.get('[aria-label="Reply"]').click({ force: true }); // Cypress has no ability to hover
});
};

View file

@ -24,7 +24,7 @@ function assertNoToasts(): void {
}
function getToast(expectedTitle: string): Chainable<JQuery> {
return cy.get(".mx_Toast_toast").contains("h2", expectedTitle).should("exist").closest(".mx_Toast_toast");
return cy.contains(".mx_Toast_toast h2", expectedTitle).should("exist").closest(".mx_Toast_toast");
}
function acceptToast(expectedTitle: string): void {

View file

@ -18,7 +18,6 @@ limitations under the License.
import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
import { performance } from "./performance";
import { synapseDocker } from "./synapsedocker";
import { slidingSyncProxyDocker } from "./sliding-sync";
import { webserver } from "./webserver";
@ -30,7 +29,6 @@ import { log } from "./log";
*/
export default function(on: PluginEvents, config: PluginConfigOptions) {
docker(on, config);
performance(on, config);
synapseDocker(on, config);
slidingSyncProxyDocker(on, config);
webserver(on, config);

View file

@ -1,47 +0,0 @@
/*
Copyright 2022 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.
*/
/// <reference types="cypress" />
import * as path from "path";
import * as fse from "fs-extra";
import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
// This holds all the performance measurements throughout the run
let bufferedMeasurements: PerformanceEntry[] = [];
function addMeasurements(measurements: PerformanceEntry[]): void {
bufferedMeasurements = bufferedMeasurements.concat(measurements);
return null;
}
async function writeMeasurementsFile() {
try {
const measurementsPath = path.join("cypress", "performance", "measurements.json");
await fse.outputJSON(measurementsPath, bufferedMeasurements, {
spaces: 4,
});
} finally {
bufferedMeasurements = [];
}
}
export function performance(on: PluginEvents, config: PluginConfigOptions) {
on("task", { addMeasurements });
on("after:run", writeMeasurementsFile);
}

View file

@ -19,7 +19,6 @@ limitations under the License.
import "@percy/cypress";
import "cypress-real-events";
import "./performance";
import "./synapse";
import "./login";
import "./labs";

View file

@ -91,7 +91,7 @@ Cypress.Commands.add("loginUser", (synapse: SynapseInstance, username: string, p
Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string, prelaunchFn?: () => void): Chainable<UserCredentials> => {
// XXX: work around Cypress not clearing IDB between tests
cy.window({ log: false }).then(win => {
win.indexedDB.databases().then(databases => {
win.indexedDB.databases()?.then(databases => {
databases.forEach(database => {
win.indexedDB.deleteDatabase(database.name);
});

View file

@ -1,74 +0,0 @@
/*
Copyright 2022 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.
*/
/// <reference types="cypress" />
import Chainable = Cypress.Chainable;
import AUTWindow = Cypress.AUTWindow;
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Start measuring the duration of some task.
* @param task The task name.
*/
startMeasuring(task: string): Chainable<AUTWindow>;
/**
* Stop measuring the duration of some task.
* The duration is reported in the Cypress log.
* @param task The task name.
*/
stopMeasuring(task: string): Chainable<AUTWindow>;
}
}
}
function getPrefix(task: string): string {
return `cy:${Cypress.spec.name.split(".")[0]}:${task}`;
}
function startMeasuring(task: string): Chainable<AUTWindow> {
return cy.window({ log: false }).then((win) => {
win.mxPerformanceMonitor.start(getPrefix(task));
});
}
function stopMeasuring(task: string): Chainable<AUTWindow> {
return cy.window({ log: false }).then((win) => {
const measure = win.mxPerformanceMonitor.stop(getPrefix(task));
cy.log(`**${task}** ${measure.duration} ms`);
});
}
Cypress.Commands.add("startMeasuring", startMeasuring);
Cypress.Commands.add("stopMeasuring", stopMeasuring);
Cypress.on("window:before:unload", (event: BeforeUnloadEvent) => {
const doc = event.target as Document;
if (doc.location.href === "about:blank") return;
const win = doc.defaultView as AUTWindow;
if (!win.mxPerformanceMonitor) return;
const entries = win.mxPerformanceMonitor.getEntries().filter(entry => {
return entry.name.startsWith("cy:");
});
if (!entries || entries.length === 0) return;
cy.task("addMeasurements", entries);
});
// Needed to make this file a module
export { };

View file

@ -153,7 +153,7 @@ Cypress.Commands.add("openRoomSettings", (tab?: string): Chainable<JQuery<HTMLEl
Cypress.Commands.add("switchTab", (tab: string): Chainable<JQuery<HTMLElement>> => {
return cy.get(".mx_TabbedView_tabLabels").within(() => {
cy.get(".mx_TabbedView_tabLabel").contains(tab).click();
cy.contains(".mx_TabbedView_tabLabel", tab).click();
});
});
@ -162,13 +162,13 @@ Cypress.Commands.add("closeDialog", (): Chainable<JQuery<HTMLElement>> => {
});
Cypress.Commands.add("joinBeta", (name: string): Chainable<JQuery<HTMLElement>> => {
return cy.get(".mx_BetaCard_title").contains(name).closest(".mx_BetaCard").within(() => {
return cy.contains(".mx_BetaCard_title", name).closest(".mx_BetaCard").within(() => {
return cy.get(".mx_BetaCard_buttons").contains("Join the beta").click();
});
});
Cypress.Commands.add("leaveBeta", (name: string): Chainable<JQuery<HTMLElement>> => {
return cy.get(".mx_BetaCard_title").contains(name).closest(".mx_BetaCard").within(() => {
return cy.contains(".mx_BetaCard_title", name).closest(".mx_BetaCard").within(() => {
return cy.get(".mx_BetaCard_buttons").contains("Leave the beta").click();
});
});

View file

@ -57,7 +57,7 @@
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/analytics-events": "^0.2.0",
"@matrix-org/matrix-wysiwyg": "^0.2.0",
"@matrix-org/matrix-wysiwyg": "^0.3.0",
"@matrix-org/react-sdk-module-api": "^0.0.3",
"@sentry/browser": "^6.11.0",
"@sentry/tracing": "^6.11.0",
@ -111,6 +111,7 @@
"react-focus-lock": "^2.5.1",
"react-transition-group": "^4.4.1",
"rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3",
"sanitize-html": "^2.3.2",
"tar-js": "^0.3.0",
"ua-parser-js": "^1.0.2",

View file

@ -4,7 +4,7 @@
@import "./_font-sizes.pcss";
@import "./_font-weights.pcss";
@import "./_spacing.pcss";
@import "./components/atoms/_Icon.pcss";
@import "./compound/_Icon.pcss";
@import "./components/views/beacon/_BeaconListItem.pcss";
@import "./components/views/beacon/_BeaconStatus.pcss";
@import "./components/views/beacon/_BeaconStatusTooltip.pcss";
@ -96,6 +96,7 @@
@import "./views/auth/_CountryDropdown.pcss";
@import "./views/auth/_InteractiveAuthEntryComponents.pcss";
@import "./views/auth/_LanguageSelector.pcss";
@import "./views/auth/_LoginWithQR.pcss";
@import "./views/auth/_PassphraseField.pcss";
@import "./views/auth/_Welcome.pcss";
@import "./views/avatars/_BaseAvatar.pcss";
@ -281,6 +282,7 @@
@import "./views/rooms/_ReplyPreview.pcss";
@import "./views/rooms/_ReplyTile.pcss";
@import "./views/rooms/_RoomBreadcrumbs.pcss";
@import "./views/rooms/_RoomCallBanner.pcss";
@import "./views/rooms/_RoomHeader.pcss";
@import "./views/rooms/_RoomInfoLine.pcss";
@import "./views/rooms/_RoomList.pcss";
@ -367,6 +369,7 @@
@import "./views/voip/_VideoFeed.pcss";
@import "./voice-broadcast/atoms/_LiveBadge.pcss";
@import "./voice-broadcast/atoms/_PlaybackControlButton.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss";
@import "./voice-broadcast/molecules/_VoiceBroadcastPlaybackBody.pcss";
@import "./voice-broadcast/molecules/_VoiceBroadcastRecordingBody.pcss";

View file

@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* Compound icon
* {@link https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed}
*/
.mx_Icon {
box-sizing: border-box;
display: inline-block;
mask-origin: content-box;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
padding: 1px;
}
@ -28,15 +29,3 @@ limitations under the License.
height: 16px;
width: 16px;
}
.mx_Icon_accent {
background-color: $accent;
}
.mx_Icon_live-badge {
background-color: #fff;
}
.mx_Icon_compound-secondary-content {
background-color: $secondary-content;
}

View file

@ -0,0 +1,171 @@
/*
Copyright 2022 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_LoginWithQRSection .mx_AccessibleButton {
margin-right: $spacing-12;
}
.mx_AuthPage .mx_LoginWithQR {
.mx_AccessibleButton {
display: block !important;
}
.mx_AccessibleButton + .mx_AccessibleButton {
margin-top: $spacing-8;
}
.mx_LoginWithQR_separator {
display: flex;
align-items: center;
text-align: center;
&::before, &::after {
content: '';
flex: 1;
border-bottom: 1px solid $quinary-content;
}
&:not(:empty) {
&::before {
margin-right: 1em;
}
&::after {
margin-left: 1em;
}
}
}
font-size: $font-15px;
}
.mx_UserSettingsDialog .mx_LoginWithQR {
.mx_AccessibleButton + .mx_AccessibleButton {
margin-left: $spacing-12;
}
font-size: $font-14px;
h1 {
font-size: $font-24px;
margin-bottom: 0;
}
li {
line-height: 1.8;
}
.mx_QRCode {
padding: $spacing-12 $spacing-40;
margin: $spacing-28 0;
}
.mx_LoginWithQR_buttons {
text-align: center;
}
.mx_LoginWithQR_qrWrapper {
display: flex;
}
}
.mx_LoginWithQR {
min-height: 350px;
display: flex;
flex-direction: column;
.mx_LoginWithQR_centreTitle {
h1 {
text-align: centre;
}
}
h1 > svg {
&.normal {
color: $secondary-content;
}
&.error {
color: $alert;
}
&.success {
color: $accent;
}
height: 1.3em;
margin-right: $spacing-8;
vertical-align: middle;
}
.mx_LoginWithQR_confirmationDigits {
text-align: center;
margin: $spacing-48 auto;
font-weight: 600;
font-size: $font-24px;
color: $primary-content;
}
.mx_LoginWithQR_confirmationAlert {
border: 1px solid $quaternary-content;
border-radius: $spacing-8;
padding: $spacing-8;
line-height: 1.5em;
display: flex;
svg {
height: 30px;
}
}
.mx_LoginWithQR_separator {
margin: 1em 0;
}
ol {
list-style-position: inside;
padding-inline-start: 0;
li::marker {
color: $accent;
}
}
.mx_LoginWithQR_BackButton {
height: $spacing-12;
margin-bottom: $spacing-24;
svg {
height: 100%;
}
}
.mx_LoginWithQR_main {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.mx_QRCode {
border: 1px solid $quinary-content;
border-radius: $spacing-8;
display: flex;
justify-content: center;
}
.mx_LoginWithQR_spinner {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
}

View file

@ -0,0 +1,54 @@
/*
Copyright 2022 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_RoomCallBanner {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
padding: $spacing-12 $spacing-16;
color: $primary-content;
background-color: $system;
cursor: pointer;
}
.mx_RoomCallBanner_text {
display: flex;
flex: 1;
align-items: center;
}
.mx_RoomCallBanner_label {
color: $primary-content;
font-weight: 600;
padding-right: $spacing-8;
&::before {
display: inline-block;
vertical-align: middle;
content: "";
background-color: $secondary-content;
mask-size: 16px;
mask-position-y: center;
width: 16px;
height: 1.2em; /* to match line height */
margin-right: 8px;
mask-image: url("$(res)/img/element-icons/call/video-call.svg");
}
}

View file

@ -0,0 +1,31 @@
/*
Copyright 2022 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_VoiceBroadcastControl {
align-items: center;
background-color: $background;
border-radius: 50%;
color: $secondary-content;
display: flex;
height: 32px;
justify-content: center;
margin-bottom: $spacing-8;
width: 32px;
}
.mx_VoiceBroadcastControl-recording {
color: $alert;
}

View file

@ -20,6 +20,11 @@ limitations under the License.
width: 266px;
}
.mx_VoiceBroadcastHeader_content {
flex-grow: 1;
min-width: 0;
}
.mx_VoiceBroadcastHeader_room {
font-size: $font-12px;
font-weight: $font-semi-bold;
@ -34,4 +39,14 @@ limitations under the License.
font-size: $font-12px;
display: flex;
gap: $spacing-4;
i {
flex-shrink: 0;
}
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View file

@ -31,5 +31,5 @@ limitations under the License.
.mx_VoiceBroadcastRecordingPip_controls {
display: flex;
justify-content: center;
justify-content: space-around;
}

View file

@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="5" cy="5" r="5" fill="#FF5B55"/>
</svg>

After

Width:  |  Height:  |  Size: 148 B

View file

@ -1,3 +1,3 @@
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.973633" y="2" width="12" height="12" rx="1" fill="#737D8C"/>
<rect x="0.973633" y="2" width="12" height="12" rx="1" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 180 B

View file

@ -0,0 +1,3 @@
<svg width="18" height="14" viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 7H1M1 7L7 13M1 7L7 1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

View file

@ -0,0 +1,11 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_721_35674)">
<path d="M5.33333 9.33313C5.33333 8.5998 5.93333 7.9998 6.66667 7.9998H28C28.7333 7.9998 29.3333 7.3998 29.3333 6.66646C29.3333 5.93313 28.7333 5.33313 28 5.33313H5.33333C3.86667 5.33313 2.66667 6.53313 2.66667 7.9998V22.6665H2C0.893333 22.6665 0 23.5598 0 24.6665C0 25.7731 0.893333 26.6665 2 26.6665H18.6667V22.6665H5.33333V9.33313ZM30.6667 10.6665H22.6667C21.9333 10.6665 21.3333 11.2665 21.3333 11.9998V25.3331C21.3333 26.0665 21.9333 26.6665 22.6667 26.6665H30.6667C31.4 26.6665 32 26.0665 32 25.3331V11.9998C32 11.2665 31.4 10.6665 30.6667 10.6665ZM29.3333 22.6665H24V13.3331H29.3333V22.6665Z" fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_721_35674">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 891 B

View file

@ -5,54 +5,23 @@
viewBox="0 0 21.799 21.799"
fill="none"
version="1.1"
id="svg12"
sodipodi:docname="live.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs16" />
<sodipodi:namedview
id="namedview14"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="35.416667"
inkscape:cx="8.7670588"
inkscape:cy="8.1882353"
inkscape:window-width="1920"
inkscape:window-height="1053"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg12" />
<path
d="m 19.188575,3.0855891 c -0.3391,-0.43594 -0.9674,-0.51448 -1.4033,-0.17541 -0.4355,0.3387 -0.5143,0.96596 -0.1766,1.40185 l 8e-4,9.8e-4 0.001,0.00129 0.0136,0.01816 c 0.0133,0.0178 0.0346,0.04686 0.0629,0.0867 0.0566,0.07972 0.1408,0.20227 0.2435,0.36368 0.2058,0.32333 0.4838,0.79947 0.7625,1.39672 0.5603,1.20054 1.1062,2.85339 1.1062,4.7199899 0,1.8666 -0.5459,3.5194 -1.1062,4.72 -0.2787,0.5972 -0.5567,1.0733 -0.7625,1.3967 -0.1027,0.1614 -0.1869,0.2839 -0.2435,0.3637 -0.0283,0.0398 -0.0496,0.0689 -0.0629,0.0867 l -0.0136,0.0181 -0.001,0.0013 -9e-4,0.0012 c -0.3376,0.4358 -0.2587,1.063 0.1767,1.4016 0.4359,0.3391 1.0642,0.2606 1.4033,-0.1754 l -0.7453,-0.5796 c 0.7453,0.5796 0.7453,0.5796 0.7453,0.5796 l 0.002,-0.0025 0.0028,-0.0038 0.0083,-0.0108 0.0265,-0.0352 c 0.0219,-0.0294 0.0521,-0.0707 0.0895,-0.1232 0.0746,-0.1051 0.1779,-0.2558 0.3002,-0.448 0.2442,-0.3838 0.5662,-0.9362 0.8875,-1.6247 0.6397,-1.3709 1.2938,-3.318 1.2938,-5.5657 0,-2.2477199 -0.6541,-4.1948699 -1.2938,-5.5657599 -0.3213,-0.68846 -0.6433,-1.2409 -0.8875,-1.6247 -0.1223,-0.19217 -0.2256,-0.34283 -0.3002,-0.44793 -0.0374,-0.05258 -0.0676,-0.09382 -0.0895,-0.12323 l -0.0265,-0.03521 -0.0083,-0.01085 -0.0028,-0.00371 -0.0012,-0.00143 c 0,0 -8e-4,-0.00114 -0.7902,0.6128 z"
fill="#c1c6cd"
id="path2" />
fill="currentColor" />
<path
d="m 16.589375,6.6858091 c -0.339,-0.43595 -0.9673,-0.51448 -1.4033,-0.17541 -0.4348,0.33819 -0.514,0.96407 -0.178,1.39987 l 0.0034,0.00458 c 0.0045,0.00599 0.0129,0.01748 0.0248,0.03422 0.0238,0.03351 0.0612,0.08776 0.1076,0.16077 0.0933,0.14655 0.2213,0.36554 0.35,0.64137 0.2603,0.55764 0.5062,1.3105399 0.5062,2.1485399 0,0.838 -0.2459,1.5909 -0.5062,2.1485 -0.1287,0.2759 -0.2567,0.4949 -0.35,0.6414 -0.0464,0.073 -0.0838,0.1273 -0.1076,0.1608 -0.0119,0.0167 -0.0203,0.0282 -0.0248,0.0342 l -0.0034,0.0046 c -0.336,0.4358 -0.2568,1.0617 0.178,1.3999 0.436,0.339 1.0643,0.2605 1.4033,-0.1755 l -0.7893,-0.6139 c 0.7893,0.6139 0.7893,0.6139 0.7893,0.6139 l 0.0018,-0.0022 0.0022,-0.0029 0.0058,-0.0075 0.0164,-0.0219 c 0.0131,-0.0176 0.0305,-0.0412 0.0514,-0.0707 0.0418,-0.0589 0.0982,-0.1413 0.1643,-0.245 0.1317,-0.2071 0.3037,-0.5024 0.475,-0.8694 0.3397,-0.728 0.6938,-1.7752 0.6938,-2.9943 0,-1.2190999 -0.3541,-2.2662799 -0.6938,-2.9943099 -0.1713,-0.36703 -0.3433,-0.66233 -0.475,-0.86935 -0.0661,-0.10377 -0.1225,-0.18613 -0.1643,-0.24503 -0.0209,-0.02947 -0.0383,-0.05314 -0.0514,-0.07074 l -0.0164,-0.02187 -0.0058,-0.00748 -0.0022,-0.00287 -9e-4,-0.00122 c 0,0 -9e-4,-0.00107 -0.7902,0.61287 z"
fill="#c1c6cd"
id="path4" />
fill="currentColor" />
<path
d="m 2.6104749,18.713449 c 0.33907,0.4359 0.96734,0.5144 1.40329,0.1754 0.43547,-0.3387 0.5143,-0.966 0.17653,-1.4019 l -7.6e-4,-10e-4 -9.7e-4,-0.0013 -0.01365,-0.0181 c -0.01325,-0.0178 -0.0346,-0.0469 -0.06289,-0.0867 -0.05661,-0.0797 -0.14082,-0.2023 -0.24354,-0.3637 -0.20576,-0.3233 -0.48376,-0.7995 -0.76248,-1.3967 -0.56025,-1.2006 -1.10618,-2.8534 -1.10618,-4.72 0,-1.8665999 0.54593,-3.5194099 1.10618,-4.7199499 0.27872,-0.59726 0.55672,-1.07339 0.76248,-1.39673 0.10272,-0.1614 0.18693,-0.28396 0.24354,-0.36368 0.02829,-0.03983 0.04964,-0.0689 0.06289,-0.0867 l 0.01365,-0.01815 9.7e-4,-0.00129 9.1e-4,-0.00117 c 0.33761,-0.43588 0.25873,-1.06302 -0.17668,-1.40166 -0.43595,-0.33907 -1.06422,-0.26054 -1.40329,0.17541 l 0.74526,0.57964 c -0.74527,-0.57963 -0.74526,-0.57964 -0.74526,-0.57964 l -0.00199,0.00256 -0.00287,0.00372 -0.0083,0.01085 -0.02649,0.0352 c -0.0219,0.02941 -0.05212,0.07066 -0.08945,0.12323 -0.07464,0.10511 -0.17792,0.25577 -0.30021,0.44793 -0.24424,0.38381 -0.56624,0.93625 -0.88752,1.62471 -0.63975001,1.37088 -1.29382000681,3.31804 -1.29382000681,5.5657199 0,2.2477 0.65406999681,4.1949 1.29382000681,5.5658 0.32128,0.6884 0.64328,1.2409 0.88752,1.6247 0.12229,0.1921 0.22557,0.3428 0.30021,0.4479 0.03733,0.0526 0.06755,0.0938 0.08945,0.1232 l 0.02649,0.0352 0.0083,0.0109 0.00287,0.0037 0.0011,0.0014 c 0,0 8.9e-4,0.0012 0.79024,-0.6128 z"
fill="#c1c6cd"
id="path6" />
fill="currentColor" />
<path
d="m 5.2095949,15.113149 c 0.33907,0.436 0.96735,0.5145 1.40329,0.1754 0.43481,-0.3381 0.51407,-0.964 0.17806,-1.3998 l -0.00343,-0.0046 c -0.00447,-0.006 -0.01292,-0.0175 -0.0248,-0.0342 -0.0238,-0.0335 -0.06114,-0.0878 -0.10761,-0.1608 -0.09326,-0.1465 -0.22126,-0.3655 -0.34997,-0.6414 -0.26026,-0.5576 -0.50619,-1.3105 -0.50619,-2.1485 0,-0.838 0.24593,-1.5908999 0.50619,-2.1485499 0.12871,-0.27582 0.25671,-0.49481 0.34997,-0.64136 0.04647,-0.07302 0.08381,-0.12727 0.10761,-0.16078 0.01188,-0.01673 0.02033,-0.02822 0.0248,-0.03422 l 0.00344,-0.00458 c 0.336,-0.43581 0.25674,-1.06168 -0.17807,-1.39986 -0.43594,-0.33907 -1.06422,-0.26054 -1.40329,0.17541 l 0.78935,0.61394 c -0.78935,-0.61394 -0.78935,-0.61394 -0.78935,-0.61394 l -0.00177,0.00228 -0.00222,0.00287 -0.00572,0.00749 -0.01646,0.02186 c -0.01311,0.01761 -0.03044,0.04128 -0.05137,0.07075 -0.04182,0.0589 -0.09823,0.14125 -0.16427,0.24503 -0.13174,0.20702 -0.30374,0.50231 -0.47502,0.86934 -0.33975,0.72803 -0.69382,1.77522 -0.69382,2.9943199 0,1.2191 0.35407,2.2663 0.69382,2.9943 0.17128,0.367 0.34328,0.6623 0.47502,0.8694 0.06604,0.1037 0.12245,0.1861 0.16427,0.245 0.02093,0.0295 0.03826,0.0531 0.05137,0.0707 l 0.01646,0.0219 0.00572,0.0075 0.00222,0.0029 9.4e-4,0.0012 c 0,0 8.3e-4,10e-4 0.79018,-0.6129 z"
fill="#c1c6cd"
id="path8" />
fill="currentColor" />
<circle
cx="10.999774"
cy="10.898949"
r="2"
fill="#c1c6cd"
id="circle10" />
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -1,4 +1,4 @@
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 1C0 0.447715 0.447715 0 1 0H2C2.55228 0 3 0.447715 3 1V11C3 11.5523 2.55228 12 2 12H1C0.447715 12 0 11.5523 0 11V1Z" fill="#737D8C"/>
<path d="M7 1C7 0.447715 7.44772 0 8 0H9C9.55228 0 10 0.447715 10 1V11C10 11.5523 9.55228 12 9 12H8C7.44772 12 7 11.5523 7 11V1Z" fill="#737D8C"/>
<path d="M0 1C0 0.447715 0.447715 0 1 0H2C2.55228 0 3 0.447715 3 1V11C3 11.5523 2.55228 12 2 12H1C0.447715 12 0 11.5523 0 11V1Z" fill="currentColor"/>
<path d="M7 1C7 0.447715 7.44772 0 8 0H9C9.55228 0 10 0.447715 10 1V11C10 11.5523 9.55228 12 9 12H8C7.44772 12 7 11.5523 7 11V1Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 396 B

After

Width:  |  Height:  |  Size: 406 B

View file

@ -1,3 +1,3 @@
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14.2104V1.78956C0 1.00724 0.857827 0.527894 1.5241 0.937906L11.6161 7.14834C12.2506 7.53883 12.2506 8.46117 11.6161 8.85166L1.5241 15.0621C0.857828 15.4721 0 14.9928 0 14.2104Z" fill="#737D8C"/>
<path d="M0 14.2104V1.78956C0 1.00724 0.857827 0.527894 1.5241 0.937906L11.6161 7.14834C12.2506 7.53883 12.2506 8.46117 11.6161 8.85166L1.5241 15.0621C0.857828 15.4721 0 14.9928 0 14.2104Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 310 B

After

Width:  |  Height:  |  Size: 315 B

View file

@ -0,0 +1,4 @@
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.16667 12V10.6667H8.5V12H7.16667ZM5.83333 10.6667V7.33333H7.16667V10.6667H5.83333ZM11.1667 8.66667V6H12.5V8.66667H11.1667ZM9.83333 6V4.66667H11.1667V6H9.83333ZM1.83333 7.33333V6H3.16667V7.33333H1.83333ZM0.5 6V4.66667H1.83333V6H0.5ZM6.5 1.33333V0H7.83333V1.33333H6.5ZM1.33333 3.16667H3.66667V0.833333H1.33333V3.16667ZM1 4C0.855556 4 0.736111 3.95278 0.641667 3.85833C0.547222 3.76389 0.5 3.64444 0.5 3.5V0.5C0.5 0.355556 0.547222 0.236111 0.641667 0.141667C0.736111 0.0472223 0.855556 0 1 0H4C4.14444 0 4.26389 0.0472223 4.35833 0.141667C4.45278 0.236111 4.5 0.355556 4.5 0.5V3.5C4.5 3.64444 4.45278 3.76389 4.35833 3.85833C4.26389 3.95278 4.14444 4 4 4H1ZM1.33333 11.1667H3.66667V8.83333H1.33333V11.1667ZM1 12C0.855556 12 0.736111 11.9528 0.641667 11.8583C0.547222 11.7639 0.5 11.6444 0.5 11.5V8.5C0.5 8.35556 0.547222 8.23611 0.641667 8.14167C0.736111 8.04722 0.855556 8 1 8H4C4.14444 8 4.26389 8.04722 4.35833 8.14167C4.45278 8.23611 4.5 8.35556 4.5 8.5V11.5C4.5 11.6444 4.45278 11.7639 4.35833 11.8583C4.26389 11.9528 4.14444 12 4 12H1ZM9.33333 3.16667H11.6667V0.833333H9.33333V3.16667ZM9 4C8.85556 4 8.73611 3.95278 8.64167 3.85833C8.54722 3.76389 8.5 3.64444 8.5 3.5V0.5C8.5 0.355556 8.54722 0.236111 8.64167 0.141667C8.73611 0.0472223 8.85556 0 9 0H12C12.1444 0 12.2639 0.0472223 12.3583 0.141667C12.4528 0.236111 12.5 0.355556 12.5 0.5V3.5C12.5 3.64444 12.4528 3.76389 12.3583 3.85833C12.2639 3.95278 12.1444 4 12 4H9ZM9.83333 12V10H8.5V8.66667H11.1667V10.6667H12.5V12H9.83333ZM7.16667 7.33333V6H9.83333V7.33333H7.16667ZM4.5 7.33333V6H3.16667V4.66667H7.16667V6H5.83333V7.33333H4.5ZM5.16667 4V1.33333H6.5V2.66667H7.83333V4H5.16667ZM2 2.5V1.5H3V2.5H2ZM2 10.5V9.5H3V10.5H2ZM10 2.5V1.5H11V2.5H10Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.09999 6.15C8.09999 3.99609 9.84608 2.25 12 2.25C14.1539 2.25 15.9 3.99609 15.9 6.15V11.9825C15.9 14.1365 14.1539 15.8825 12 15.8825C9.84608 15.8825 8.09999 14.1365 8.09999 11.9825V6.15ZM5.1748 10.7375C5.86516 10.7375 6.4248 11.2972 6.4248 11.9875C6.4248 15.0574 8.91483 17.5493 11.9915 17.5538C11.9943 17.5538 11.9972 17.5538 12 17.5538C12.0028 17.5538 12.0056 17.5538 12.0084 17.5538C15.085 17.5492 17.5748 15.0573 17.5748 11.9875C17.5748 11.2972 18.1344 10.7375 18.8248 10.7375C19.5152 10.7375 20.0748 11.2972 20.0748 11.9875C20.0748 16.0189 17.115 19.3576 13.25 19.9577V20.7513C13.25 21.4416 12.6904 22.0013 12 22.0013C11.3096 22.0013 10.75 21.4416 10.75 20.7513V19.9578C6.88482 19.3578 3.9248 16.0191 3.9248 11.9875C3.9248 11.2972 4.48445 10.7375 5.1748 10.7375Z" fill="#737D8C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.09999 6.15C8.09999 3.99609 9.84608 2.25 12 2.25C14.1539 2.25 15.9 3.99609 15.9 6.15V11.9825C15.9 14.1365 14.1539 15.8825 12 15.8825C9.84608 15.8825 8.09999 14.1365 8.09999 11.9825V6.15ZM5.1748 10.7375C5.86516 10.7375 6.4248 11.2972 6.4248 11.9875C6.4248 15.0574 8.91483 17.5493 11.9915 17.5538C11.9943 17.5538 11.9972 17.5538 12 17.5538C12.0028 17.5538 12.0056 17.5538 12.0084 17.5538C15.085 17.5492 17.5748 15.0573 17.5748 11.9875C17.5748 11.2972 18.1344 10.7375 18.8248 10.7375C19.5152 10.7375 20.0748 11.2972 20.0748 11.9875C20.0748 16.0189 17.115 19.3576 13.25 19.9577V20.7513C13.25 21.4416 12.6904 22.0013 12 22.0013C11.3096 22.0013 10.75 21.4416 10.75 20.7513V19.9578C6.88482 19.3578 3.9248 16.0191 3.9248 11.9875C3.9248 11.2972 4.48445 10.7375 5.1748 10.7375Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 940 B

After

Width:  |  Height:  |  Size: 945 B

View file

@ -43,7 +43,6 @@ import { RoomUpload } from "./models/RoomUpload";
import SettingsStore from "./settings/SettingsStore";
import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics";
import { TimelineRenderingType } from "./contexts/RoomContext";
import { RoomViewStore } from "./stores/RoomViewStore";
import { addReplyToMessageContent } from "./utils/Reply";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import UploadFailureDialog from "./components/views/dialogs/UploadFailureDialog";
@ -51,6 +50,7 @@ import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog"
import { createThumbnail } from "./utils/image-media";
import { attachRelation } from "./components/views/rooms/SendMessageComposer";
import { doMaybeLocalRoomAction } from "./utils/local-room";
import { SdkContextClass } from "./contexts/SDKContext";
// scraped out of a macOS hidpi (5660ppm) screenshot png
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
@ -361,7 +361,7 @@ export default class ContentMessages {
return;
}
const replyToEvent = RoomViewStore.instance.getQuotingEvent();
const replyToEvent = SdkContextClass.instance.roomViewStore.getQuotingEvent();
if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
await this.ensureMediaConfigFetched(matrixClient);

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2022 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.
@ -177,6 +178,16 @@ export function formatFullDateNoDay(date: Date) {
});
}
/**
* Returns an ISO date string without textual description of the date (ie: no "Wednesday" or
* similar)
* @param date The date to format.
* @returns The date string in ISO format.
*/
export function formatFullDateNoDayISO(date: Date): string {
return date.toISOString();
}
export function formatFullDateNoDayNoTime(date: Date) {
return (
date.getFullYear() +

View file

@ -39,7 +39,6 @@ import PlatformPeg from "./PlatformPeg";
import { sendLoginRequest } from "./Login";
import * as StorageManager from './utils/StorageManager';
import SettingsStore from "./settings/SettingsStore";
import TypingStore from "./stores/TypingStore";
import ToastStore from "./stores/ToastStore";
import { IntegrationManagers } from "./integrations/IntegrationManagers";
import { Mjolnir } from "./mjolnir/Mjolnir";
@ -62,6 +61,7 @@ import { DialogOpener } from "./utils/DialogOpener";
import { Action } from "./dispatcher/actions";
import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler";
import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload";
import { SdkContextClass } from './contexts/SDKContext';
const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url";
@ -426,7 +426,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
const { hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest } = await getStoredSessionVars();
if (hasAccessToken && !accessToken) {
abortLogin();
await abortLogin();
}
if (accessToken && userId && hsUrl) {
@ -797,7 +797,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
dis.dispatch({ action: 'will_start_client' }, true);
// reset things first just in case
TypingStore.sharedInstance().reset();
SdkContextClass.instance.typingStore.reset();
ToastStore.sharedInstance().reset();
DialogOpener.instance.prepare();
@ -927,7 +927,7 @@ export function stopMatrixClient(unsetClient = true): void {
Notifier.stop();
LegacyCallHandler.instance.stop();
UserActivity.sharedInstance().stop();
TypingStore.sharedInstance().reset();
SdkContextClass.instance.typingStore.reset();
Presence.stop();
ActiveWidgetStore.instance.stop();
IntegrationManagers.sharedInstance().stopWatching();

View file

@ -41,12 +41,12 @@ import SettingsStore from "./settings/SettingsStore";
import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast";
import { SettingLevel } from "./settings/SettingLevel";
import { isPushNotifyDisabled } from "./settings/controllers/NotificationControllers";
import { RoomViewStore } from "./stores/RoomViewStore";
import UserActivity from "./UserActivity";
import { mediaFromMxc } from "./customisations/Media";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import LegacyCallHandler from "./LegacyCallHandler";
import VoipUserMapper from "./VoipUserMapper";
import { SdkContextClass } from "./contexts/SDKContext";
import { localNotificationsAreSilenced } from "./utils/notifications";
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
import ToastStore from "./stores/ToastStore";
@ -435,7 +435,7 @@ export const Notifier = {
if (actions?.notify) {
this._performCustomEventHandling(ev);
if (RoomViewStore.instance.getRoomId() === room.roomId &&
if (SdkContextClass.instance.roomViewStore.getRoomId() === room.roomId &&
UserActivity.sharedInstance().userActiveRecently() &&
!Modal.hasDialogs()
) {

View file

@ -272,12 +272,12 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from './MatrixClientPeg';
import dis from './dispatcher/dispatcher';
import WidgetUtils from './utils/WidgetUtils';
import { RoomViewStore } from './stores/RoomViewStore';
import { _t } from './languageHandler';
import { IntegrationManagers } from "./integrations/IntegrationManagers";
import { WidgetType } from "./widgets/WidgetType";
import { objectClone } from "./utils/objects";
import { EffectiveMembership, getEffectiveMembership } from './utils/membership';
import { SdkContextClass } from './contexts/SDKContext';
enum Action {
CloseScalar = "close_scalar",
@ -721,7 +721,7 @@ const onMessage = function(event: MessageEvent<any>): void {
}
}
if (roomId !== RoomViewStore.instance.getRoomId()) {
if (roomId !== SdkContextClass.instance.roomViewStore.getRoomId()) {
sendError(event, _t('Room %(roomId)s not visible', { roomId: roomId }));
return;
}

View file

@ -47,7 +47,7 @@ export const DEFAULTS: IConfigOptions = {
url: "https://element.io/get-started",
},
voice_broadcast: {
chunk_length: 60, // one minute
chunk_length: 120, // two minutes
},
};

View file

@ -62,7 +62,6 @@ import InfoDialog from "./components/views/dialogs/InfoDialog";
import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog";
import { shouldShowComponent } from "./customisations/helpers/UIComponents";
import { TimelineRenderingType } from './contexts/RoomContext';
import { RoomViewStore } from "./stores/RoomViewStore";
import { XOR } from "./@types/common";
import { PosthogAnalytics } from "./PosthogAnalytics";
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
@ -70,6 +69,7 @@ import VoipUserMapper from './VoipUserMapper';
import { htmlSerializeFromMdIfNeeded } from './editor/serialize';
import { leaveRoomBehaviour } from "./utils/leave-behaviour";
import { isLocalRoom } from './utils/localRoom/isLocalRoom';
import { SdkContextClass } from './contexts/SDKContext';
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
interface HTMLInputEvent extends Event {
@ -209,7 +209,7 @@ function successSync(value: any) {
const isCurrentLocalRoom = (): boolean => {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(RoomViewStore.instance.getRoomId());
const room = cli.getRoom(SdkContextClass.instance.roomViewStore.getRoomId());
return isLocalRoom(room);
};
@ -868,7 +868,7 @@ export const Commands = [
description: _td('Define the power level of a user'),
isEnabled(): boolean {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(RoomViewStore.instance.getRoomId());
const room = cli.getRoom(SdkContextClass.instance.roomViewStore.getRoomId());
return room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId())
&& !isLocalRoom(room);
},
@ -909,7 +909,7 @@ export const Commands = [
description: _td('Deops user with given id'),
isEnabled(): boolean {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(RoomViewStore.instance.getRoomId());
const room = cli.getRoom(SdkContextClass.instance.roomViewStore.getRoomId());
return room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId())
&& !isLocalRoom(room);
},

View file

@ -25,7 +25,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg";
import { arrayFastClone } from "../utils/arrays";
import { PlaybackManager } from "./PlaybackManager";
import { isVoiceMessage } from "../utils/EventUtils";
import { RoomViewStore } from "../stores/RoomViewStore";
import { SdkContextClass } from "../contexts/SDKContext";
/**
* Audio playback queue management for a given room. This keeps track of where the user
@ -51,7 +51,7 @@ export class PlaybackQueue {
constructor(private room: Room) {
this.loadClocks();
RoomViewStore.instance.addRoomListener(this.room.roomId, (isActive) => {
SdkContextClass.instance.roomViewStore.addRoomListener(this.room.roomId, (isActive) => {
if (!isActive) return;
// Reset the state of the playbacks before they start mounting and enqueuing updates.

View file

@ -1,83 +0,0 @@
/*
Copyright 2022 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.
*/
import React from "react";
import liveIcon from "../../../res/img/element-icons/live.svg";
import microphoneIcon from "../../../res/img/voip/call-view/mic-on.svg";
import pauseIcon from "../../../res/img/element-icons/pause.svg";
import playIcon from "../../../res/img/element-icons/play.svg";
import stopIcon from "../../../res/img/element-icons/Stop.svg";
export enum IconType {
Live,
Microphone,
Pause,
Play,
Stop,
}
const iconTypeMap = new Map([
[IconType.Live, liveIcon],
[IconType.Microphone, microphoneIcon],
[IconType.Pause, pauseIcon],
[IconType.Play, playIcon],
[IconType.Stop, stopIcon],
]);
export enum IconColour {
Accent = "accent",
LiveBadge = "live-badge",
CompoundSecondaryContent = "compound-secondary-content",
}
export enum IconSize {
S16 = "16",
}
interface IconProps {
colour?: IconColour;
size?: IconSize;
type: IconType;
}
export const Icon: React.FC<IconProps> = ({
size = IconSize.S16,
colour = IconColour.Accent,
type,
...rest
}) => {
const classes = [
"mx_Icon",
`mx_Icon_${size}`,
`mx_Icon_${colour}`,
];
const styles: React.CSSProperties = {
maskImage: `url("${iconTypeMap.get(type)}")`,
WebkitMaskImage: `url("${iconTypeMap.get(type)}")`,
};
return (
<i
aria-hidden
className={classes.join(" ")}
role="presentation"
style={styles}
{...rest}
/>
);
};

View file

@ -565,8 +565,13 @@ type ContextMenuTuple<T> = [
(val: boolean) => void,
];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<T> => {
const button = useRef<T>(null);
export const useContextMenu = <T extends any = HTMLElement>(inputRef?: RefObject<T>): ContextMenuTuple<T> => {
let button = useRef<T>(null);
if (inputRef) {
// if we are given a ref, use it instead of ours
button = inputRef;
}
const [isOpen, setIsOpen] = useState(false);
const open = (ev?: SyntheticEvent) => {
ev?.preventDefault();
@ -579,7 +584,7 @@ export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<
setIsOpen(false);
};
return [isOpen, button, open, close, setIsOpen];
return [button.current ? isOpen : false, button, open, close, setIsOpen];
};
// XXX: Deprecated, used only for dynamic Tooltips. Avoid using at all costs.

View file

@ -19,7 +19,7 @@ import React, { useEffect, useState } from "react";
import { _t } from "../../languageHandler";
interface IProps {
parent: HTMLElement;
parent: HTMLElement | null;
onFileDrop(dataTransfer: DataTransfer): void;
}
@ -90,20 +90,20 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
}));
};
parent.addEventListener("drop", onDrop);
parent.addEventListener("dragover", onDragOver);
parent.addEventListener("dragenter", onDragEnter);
parent.addEventListener("dragleave", onDragLeave);
parent?.addEventListener("drop", onDrop);
parent?.addEventListener("dragover", onDragOver);
parent?.addEventListener("dragenter", onDragEnter);
parent?.addEventListener("dragleave", onDragLeave);
return () => {
// disconnect the D&D event listeners from the room view. This
// is really just for hygiene - we're going to be
// deleted anyway, so it doesn't matter if the event listeners
// don't get cleaned up.
parent.removeEventListener("drop", onDrop);
parent.removeEventListener("dragover", onDragOver);
parent.removeEventListener("dragenter", onDragEnter);
parent.removeEventListener("dragleave", onDragLeave);
parent?.removeEventListener("drop", onDrop);
parent?.removeEventListener("dragover", onDragOver);
parent?.removeEventListener("dragenter", onDragEnter);
parent?.removeEventListener("dragleave", onDragLeave);
};
}, [parent, onFileDrop]);

View file

@ -22,6 +22,7 @@ import classNames from 'classnames';
import { ISyncStateData, SyncState } from 'matrix-js-sdk/src/sync';
import { IUsageLimit } from 'matrix-js-sdk/src/@types/partials';
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { MatrixError } from 'matrix-js-sdk/src/matrix';
import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../Keyboard';
import PageTypes from '../../PageTypes';
@ -288,8 +289,8 @@ class LoggedInView extends React.Component<IProps, IState> {
};
private onSync = (syncState: SyncState, oldSyncState?: SyncState, data?: ISyncStateData): void => {
const oldErrCode = this.state.syncErrorData?.error?.errcode;
const newErrCode = data && data.error && data.error.errcode;
const oldErrCode = (this.state.syncErrorData?.error as MatrixError)?.errcode;
const newErrCode = (data?.error as MatrixError)?.errcode;
if (syncState === oldSyncState && oldErrCode === newErrCode) return;
this.setState({
@ -317,9 +318,9 @@ class LoggedInView extends React.Component<IProps, IState> {
};
private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
const error = (syncError?.error as MatrixError)?.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
if (error) {
usageLimitEventContent = syncError.error.data as IUsageLimit;
usageLimitEventContent = (syncError?.error as MatrixError).data as IUsageLimit;
}
// usageLimitDismissed is true when the user has explicitly hidden the toast

View file

@ -24,7 +24,6 @@ import {
MatrixEventEvent,
} from 'matrix-js-sdk/src/matrix';
import { ISyncStateData, SyncState } from 'matrix-js-sdk/src/sync';
import { MatrixError } from 'matrix-js-sdk/src/http-api';
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
@ -137,8 +136,10 @@ import { TimelineRenderingType } from "../../contexts/RoomContext";
import { UseCaseSelection } from '../views/elements/UseCaseSelection';
import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig';
import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
import { SdkContextClass, SDKContext } from '../../contexts/SDKContext';
import { viewUserDeviceSettings } from '../../actions/handlers/viewUserDeviceSettings';
import { isNumberArray } from '../../utils/TypeUtils';
import { VoiceBroadcastResumer } from '../../voice-broadcast';
// legacy export
export { default as Views } from "../../Views";
@ -202,7 +203,7 @@ interface IState {
// When showing Modal dialogs we need to set aria-hidden on the root app element
// and disable it when there are no dialogs
hideToSRUsers: boolean;
syncError?: MatrixError;
syncError?: Error;
resizeNotifier: ResizeNotifier;
serverConfig?: ValidatedServerConfig;
ready: boolean;
@ -234,14 +235,18 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private focusComposer: boolean;
private subTitleStatus: string;
private prevWindowWidth: number;
private voiceBroadcastResumer: VoiceBroadcastResumer;
private readonly loggedInView: React.RefObject<LoggedInViewType>;
private readonly dispatcherRef: string;
private readonly themeWatcher: ThemeWatcher;
private readonly fontWatcher: FontWatcher;
private readonly stores: SdkContextClass;
constructor(props: IProps) {
super(props);
this.stores = SdkContextClass.instance;
this.stores.constructEagerStores();
this.state = {
view: Views.LOADING,
@ -430,6 +435,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
window.removeEventListener("resize", this.onWindowResized);
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
if (this.voiceBroadcastResumer) this.voiceBroadcastResumer.destroy();
}
private onWindowResized = (): void => {
@ -763,6 +769,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Modal.createDialog(DialPadModal, {}, "mx_Dialog_dialPadWrapper");
break;
case Action.OnLoggedIn:
this.stores.client = MatrixClientPeg.get();
if (
// Skip this handling for token login as that always calls onLoggedIn itself
!this.tokenLogin &&
@ -1473,7 +1480,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (data.error instanceof InvalidStoreError) {
Lifecycle.handleInvalidStoreError(data.error);
}
this.setState({ syncError: data.error || {} as MatrixError });
this.setState({ syncError: data.error });
} else if (this.state.syncError) {
this.setState({ syncError: null });
}
@ -1637,6 +1644,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
});
this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli);
}
/**
@ -2111,7 +2120,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
return <ErrorBoundary>
{ view }
<SDKContext.Provider value={this.stores}>
{ view }
</SDKContext.Provider>
</ErrorBoundary>;
}
}

View file

@ -44,21 +44,18 @@ import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import ResizeNotifier from '../../utils/ResizeNotifier';
import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal';
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../LegacyCallHandler';
import { LegacyCallHandlerEvent } from '../../LegacyCallHandler';
import dis, { defaultDispatcher } from '../../dispatcher/dispatcher';
import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching';
import MainSplit from './MainSplit';
import RightPanel from './RightPanel';
import { RoomViewStore } from '../../stores/RoomViewStore';
import RoomScrollStateStore, { ScrollState } from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/enums/Layout";
import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import MatrixClientContext, { MatrixClientProps, withMatrixClientHOC } from "../../contexts/MatrixClientContext";
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
import { Action } from "../../dispatcher/actions";
import { IMatrixClientCreds } from "../../MatrixClientPeg";
@ -76,12 +73,10 @@ import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
import EffectsOverlay from "../views/elements/EffectsOverlay";
import { containsEmoji } from '../../effects/utils';
import { CHAT_EFFECTS } from '../../effects';
import WidgetStore from "../../stores/WidgetStore";
import { CallView } from "../views/voip/CallView";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import Notifier from "../../Notifier";
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import { getKeyBindingsManager } from '../../KeyBindingsManager';
import { objectHasDiff } from "../../utils/objects";
@ -118,8 +113,8 @@ import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
import { RoomStatusBarUnsentMessages } from './RoomStatusBarUnsentMessages';
import { LargeLoader } from './LargeLoader';
import { VoiceBroadcastInfoEventType } from '../../voice-broadcast';
import { isVideoRoom } from '../../utils/video-rooms';
import { SDKContext } from '../../contexts/SDKContext';
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
import { Call } from "../../models/Call";
@ -133,7 +128,7 @@ if (DEBUG) {
debuglog = logger.log.bind(console);
}
interface IRoomProps extends MatrixClientProps {
interface IRoomProps {
threepidInvite: IThreepidInvite;
oobData?: IOOBData;
@ -203,7 +198,6 @@ export interface IRoomState {
upgradeRecommendation?: IRecommendedVersion;
canReact: boolean;
canSendMessages: boolean;
canSendVoiceBroadcasts: boolean;
tombstone?: MatrixEvent;
resizing: boolean;
layout: Layout;
@ -381,13 +375,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private messagePanel: TimelinePanel;
private roomViewBody = createRef<HTMLDivElement>();
static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
static contextType = SDKContext;
public context!: React.ContextType<typeof SDKContext>;
constructor(props: IRoomProps, context: React.ContextType<typeof MatrixClientContext>) {
constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
super(props, context);
const llMembers = context.hasLazyLoadMembersEnabled();
const llMembers = context.client.hasLazyLoadMembersEnabled();
this.state = {
roomId: null,
roomLoading: true,
@ -408,7 +402,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
statusBarVisible: false,
canReact: false,
canSendMessages: false,
canSendVoiceBroadcasts: false,
resizing: false,
layout: SettingsStore.getValue("layout"),
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
@ -422,7 +415,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: context?.isInitialSyncComplete(),
matrixClientIsReady: context.client?.isInitialSyncComplete(),
mainSplitContentType: MainSplitContentType.Timeline,
timelineRenderingType: TimelineRenderingType.Room,
liveTimeline: undefined,
@ -430,25 +423,25 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};
this.dispatcherRef = dis.register(this.onAction);
context.on(ClientEvent.Room, this.onRoom);
context.on(RoomEvent.Timeline, this.onRoomTimeline);
context.on(RoomEvent.TimelineReset, this.onRoomTimelineReset);
context.on(RoomEvent.Name, this.onRoomName);
context.on(RoomStateEvent.Events, this.onRoomStateEvents);
context.on(RoomStateEvent.Update, this.onRoomStateUpdate);
context.on(RoomEvent.MyMembership, this.onMyMembership);
context.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
context.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
context.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
context.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
context.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
context.client.on(ClientEvent.Room, this.onRoom);
context.client.on(RoomEvent.Timeline, this.onRoomTimeline);
context.client.on(RoomEvent.TimelineReset, this.onRoomTimelineReset);
context.client.on(RoomEvent.Name, this.onRoomName);
context.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
context.client.on(RoomStateEvent.Update, this.onRoomStateUpdate);
context.client.on(RoomEvent.MyMembership, this.onMyMembership);
context.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
context.client.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
context.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
context.client.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
// Start listening for RoomViewStore updates
RoomViewStore.instance.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
context.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
CallStore.instance.on(CallStoreEvent.ActiveCalls, this.onActiveCalls);
@ -501,16 +494,16 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
action: "appsDrawer",
show: true,
});
if (WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room)) {
if (this.context.widgetLayoutStore.hasMaximisedWidget(this.state.room)) {
// Show chat in right panel when a widget is maximised
RightPanelStore.instance.setCard({ phase: RightPanelPhases.Timeline });
this.context.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline });
}
this.checkWidgets(this.state.room);
};
private checkWidgets = (room: Room): void => {
this.setState({
hasPinnedWidgets: WidgetLayoutStore.instance.hasPinnedWidgets(room),
hasPinnedWidgets: this.context.widgetLayoutStore.hasPinnedWidgets(room),
mainSplitContentType: this.getMainSplitContentType(room),
showApps: this.shouldShowApps(room),
});
@ -518,12 +511,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private getMainSplitContentType = (room: Room) => {
if (
(SettingsStore.getValue("feature_group_calls") && RoomViewStore.instance.isViewingCall())
(SettingsStore.getValue("feature_group_calls") && this.context.roomViewStore.isViewingCall())
|| isVideoRoom(room)
) {
return MainSplitContentType.Call;
}
if (WidgetLayoutStore.instance.hasMaximisedWidget(room)) {
if (this.context.widgetLayoutStore.hasMaximisedWidget(room)) {
return MainSplitContentType.MaximisedWidget;
}
return MainSplitContentType.Timeline;
@ -534,7 +527,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return;
}
if (!initial && this.state.roomId !== RoomViewStore.instance.getRoomId()) {
if (!initial && this.state.roomId !== this.context.roomViewStore.getRoomId()) {
// RoomView explicitly does not support changing what room
// is being viewed: instead it should just be re-mounted when
// switching rooms. Therefore, if the room ID changes, we
@ -549,45 +542,45 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return;
}
const roomId = RoomViewStore.instance.getRoomId();
const room = this.context.getRoom(roomId);
const roomId = this.context.roomViewStore.getRoomId();
const room = this.context.client.getRoom(roomId);
// This convoluted type signature ensures we get IntelliSense *and* correct typing
const newState: Partial<IRoomState> & Pick<IRoomState, any> = {
roomId,
roomAlias: RoomViewStore.instance.getRoomAlias(),
roomLoading: RoomViewStore.instance.isRoomLoading(),
roomLoadError: RoomViewStore.instance.getRoomLoadError(),
joining: RoomViewStore.instance.isJoining(),
replyToEvent: RoomViewStore.instance.getQuotingEvent(),
roomAlias: this.context.roomViewStore.getRoomAlias(),
roomLoading: this.context.roomViewStore.isRoomLoading(),
roomLoadError: this.context.roomViewStore.getRoomLoadError(),
joining: this.context.roomViewStore.isJoining(),
replyToEvent: this.context.roomViewStore.getQuotingEvent(),
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.instance.shouldPeek(),
shouldPeek: this.state.matrixClientIsReady && this.context.roomViewStore.shouldPeek(),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
showRedactions: SettingsStore.getValue("showRedactions", roomId),
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
wasContextSwitch: RoomViewStore.instance.getWasContextSwitch(),
wasContextSwitch: this.context.roomViewStore.getWasContextSwitch(),
mainSplitContentType: room === null ? undefined : this.getMainSplitContentType(room),
initialEventId: null, // default to clearing this, will get set later in the method if needed
showRightPanel: RightPanelStore.instance.isOpenForRoom(roomId),
showRightPanel: this.context.rightPanelStore.isOpenForRoom(roomId),
activeCall: CallStore.instance.getActiveCall(roomId),
};
if (
this.state.mainSplitContentType !== MainSplitContentType.Timeline
&& newState.mainSplitContentType === MainSplitContentType.Timeline
&& RightPanelStore.instance.isOpen
&& RightPanelStore.instance.currentCard.phase === RightPanelPhases.Timeline
&& RightPanelStore.instance.roomPhaseHistory.some(card => (card.phase === RightPanelPhases.Timeline))
&& this.context.rightPanelStore.isOpen
&& this.context.rightPanelStore.currentCard.phase === RightPanelPhases.Timeline
&& this.context.rightPanelStore.roomPhaseHistory.some(card => (card.phase === RightPanelPhases.Timeline))
) {
// We're returning to the main timeline, so hide the right panel timeline
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary });
RightPanelStore.instance.togglePanel(this.state.roomId ?? null);
this.context.rightPanelStore.setCard({ phase: RightPanelPhases.RoomSummary });
this.context.rightPanelStore.togglePanel(this.state.roomId ?? null);
newState.showRightPanel = false;
}
const initialEventId = RoomViewStore.instance.getInitialEventId();
const initialEventId = this.context.roomViewStore.getInitialEventId();
if (initialEventId) {
let initialEvent = room?.findEventById(initialEventId);
// The event does not exist in the current sync data
@ -600,7 +593,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// becomes available to fetch a whole thread
if (!initialEvent) {
initialEvent = await fetchInitialEvent(
this.context,
this.context.client,
roomId,
initialEventId,
);
@ -616,21 +609,21 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
action: Action.ShowThread,
rootEvent: thread.rootEvent,
initialEvent,
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
highlighted: this.context.roomViewStore.isInitialEventHighlighted(),
scroll_into_view: this.context.roomViewStore.initialEventScrollIntoView(),
});
} else {
newState.initialEventId = initialEventId;
newState.isInitialEventHighlighted = RoomViewStore.instance.isInitialEventHighlighted();
newState.initialEventScrollIntoView = RoomViewStore.instance.initialEventScrollIntoView();
newState.isInitialEventHighlighted = this.context.roomViewStore.isInitialEventHighlighted();
newState.initialEventScrollIntoView = this.context.roomViewStore.initialEventScrollIntoView();
if (thread && initialEvent?.isThreadRoot) {
dis.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: thread.rootEvent,
initialEvent,
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
highlighted: this.context.roomViewStore.isInitialEventHighlighted(),
scroll_into_view: this.context.roomViewStore.initialEventScrollIntoView(),
});
}
}
@ -657,7 +650,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
// Stop peeking because we have joined this room now
this.context.stopPeeking();
this.context.client.stopPeeking();
}
// Temporary logging to diagnose https://github.com/vector-im/element-web/issues/4307
@ -674,7 +667,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// NB: This does assume that the roomID will not change for the lifetime of
// the RoomView instance
if (initial) {
newState.room = this.context.getRoom(newState.roomId);
newState.room = this.context.client.getRoom(newState.roomId);
if (newState.room) {
newState.showApps = this.shouldShowApps(newState.room);
this.onRoomLoaded(newState.room);
@ -784,7 +777,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
peekLoading: true,
isPeeking: true, // this will change to false if peeking fails
});
this.context.peekInRoom(roomId).then((room) => {
this.context.client.peekInRoom(roomId).then((room) => {
if (this.unmounted) {
return;
}
@ -817,7 +810,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
});
} else if (room) {
// Stop peeking because we have joined this room previously
this.context.stopPeeking();
this.context.client.stopPeeking();
this.setState({ isPeeking: false });
}
}
@ -835,7 +828,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// Otherwise (in case the user set hideWidgetDrawer by clicking the button) follow the parameter.
const isManuallyShown = hideWidgetDrawer ? hideWidgetDrawer === "false": true;
const widgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top);
const widgets = this.context.widgetLayoutStore.getContainerWidgets(room, Container.Top);
return isManuallyShown && widgets.length > 0;
}
@ -848,7 +841,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
callState: callState,
});
LegacyCallHandler.instance.on(LegacyCallHandlerEvent.CallState, this.onCallState);
this.context.legacyCallHandler.on(LegacyCallHandlerEvent.CallState, this.onCallState);
window.addEventListener('beforeunload', this.onPageUnload);
}
@ -885,7 +878,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true;
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState);
this.context.legacyCallHandler.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState);
// update the scroll map before we get unmounted
if (this.state.roomId) {
@ -893,47 +886,47 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
if (this.state.shouldPeek) {
this.context.stopPeeking();
this.context.client.stopPeeking();
}
// stop tracking room changes to format permalinks
this.stopAllPermalinkCreators();
dis.unregister(this.dispatcherRef);
if (this.context) {
this.context.removeListener(ClientEvent.Room, this.onRoom);
this.context.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
this.context.removeListener(RoomEvent.TimelineReset, this.onRoomTimelineReset);
this.context.removeListener(RoomEvent.Name, this.onRoomName);
this.context.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
this.context.removeListener(RoomEvent.MyMembership, this.onMyMembership);
this.context.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
this.context.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
this.context.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
this.context.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
this.context.removeListener(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
this.context.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
if (this.context.client) {
this.context.client.removeListener(ClientEvent.Room, this.onRoom);
this.context.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
this.context.client.removeListener(RoomEvent.TimelineReset, this.onRoomTimelineReset);
this.context.client.removeListener(RoomEvent.Name, this.onRoomName);
this.context.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
this.context.client.removeListener(RoomEvent.MyMembership, this.onMyMembership);
this.context.client.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
this.context.client.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
this.context.client.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
this.context.client.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
this.context.client.removeListener(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
this.context.client.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
}
window.removeEventListener('beforeunload', this.onPageUnload);
RoomViewStore.instance.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.context.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
this.context.rightPanelStore.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
WidgetStore.instance.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate);
this.context.widgetStore.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate);
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
if (this.state.room) {
WidgetLayoutStore.instance.off(
this.context.widgetLayoutStore.off(
WidgetLayoutStore.emissionForRoom(this.state.room),
this.onWidgetLayoutChange,
);
}
CallStore.instance.off(CallStoreEvent.ActiveCalls, this.onActiveCalls);
LegacyCallHandler.instance.off(LegacyCallHandlerEvent.CallState, this.onCallState);
this.context.legacyCallHandler.off(LegacyCallHandlerEvent.CallState, this.onCallState);
// cancel any pending calls to the throttled updated
this.updateRoomMembers.cancel();
@ -944,13 +937,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (this.viewsLocalRoom) {
// clean up if this was a local room
this.props.mxClient.store.removeRoom(this.state.room.roomId);
this.context.client.store.removeRoom(this.state.room.roomId);
}
}
private onRightPanelStoreUpdate = () => {
this.setState({
showRightPanel: RightPanelStore.instance.isOpenForRoom(this.state.roomId),
showRightPanel: this.context.rightPanelStore.isOpenForRoom(this.state.roomId),
});
};
@ -1017,7 +1010,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
break;
case 'picture_snapshot':
ContentMessages.sharedInstance().sendContentListToRoom(
[payload.file], this.state.room.roomId, null, this.context);
[payload.file], this.state.room.roomId, null, this.context.client);
break;
case 'notifier_enabled':
case Action.UploadStarted:
@ -1043,7 +1036,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
case 'MatrixActions.sync':
if (!this.state.matrixClientIsReady) {
this.setState({
matrixClientIsReady: this.context?.isInitialSyncComplete(),
matrixClientIsReady: this.context.client?.isInitialSyncComplete(),
}, () => {
// send another "initial" RVS update to trigger peeking if needed
this.onRoomViewStoreUpdate(true);
@ -1112,7 +1105,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private onLocalRoomEvent(roomId: string) {
if (roomId !== this.state.room.roomId) return;
createRoomFromLocalRoom(this.props.mxClient, this.state.room as LocalRoom);
createRoomFromLocalRoom(this.context.client, this.state.room as LocalRoom);
}
private onRoomTimeline = (ev: MatrixEvent, room: Room | null, toStartOfTimeline: boolean, removed, data) => {
@ -1145,7 +1138,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.handleEffects(ev);
}
if (ev.getSender() !== this.context.credentials.userId) {
if (ev.getSender() !== this.context.client.credentials.userId) {
// update unread count when scrolled up
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
// no change
@ -1165,7 +1158,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};
private handleEffects = (ev: MatrixEvent) => {
const notifState = RoomNotificationStateStore.instance.getRoomState(this.state.room);
const notifState = this.context.roomNotificationStateStore.getRoomState(this.state.room);
if (!notifState.isUnread) return;
CHAT_EFFECTS.forEach(effect => {
@ -1202,7 +1195,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private onRoomLoaded = (room: Room) => {
if (this.unmounted) return;
// Attach a widget store listener only when we get a room
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
this.calculatePeekRules(room);
this.updatePreviewUrlVisibility(room);
@ -1214,10 +1207,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (
this.getMainSplitContentType(room) !== MainSplitContentType.Timeline
&& RoomNotificationStateStore.instance.getRoomState(room).isUnread
&& this.context.roomNotificationStateStore.getRoomState(room).isUnread
) {
// Automatically open the chat panel to make unread messages easier to discover
RightPanelStore.instance.setCard({ phase: RightPanelPhases.Timeline }, true, room.roomId);
this.context.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline }, true, room.roomId);
}
this.setState({
@ -1244,7 +1237,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private async loadMembersIfJoined(room: Room) {
// lazy load members if enabled
if (this.context.hasLazyLoadMembersEnabled()) {
if (this.context.client.hasLazyLoadMembersEnabled()) {
if (room && room.getMyMembership() === 'join') {
try {
await room.loadMembersIfNeeded();
@ -1270,7 +1263,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private updatePreviewUrlVisibility({ roomId }: Room) {
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
const key = this.context.isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
const key = this.context.client.isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
this.setState({
showUrlPreview: SettingsStore.getValue(key, roomId),
});
@ -1283,7 +1276,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// Detach the listener if the room is changing for some reason
if (this.state.room) {
WidgetLayoutStore.instance.off(
this.context.widgetLayoutStore.off(
WidgetLayoutStore.emissionForRoom(this.state.room),
this.onWidgetLayoutChange,
);
@ -1320,15 +1313,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};
private async updateE2EStatus(room: Room) {
if (!this.context.isRoomEncrypted(room.roomId)) return;
if (!this.context.client.isRoomEncrypted(room.roomId)) return;
// If crypto is not currently enabled, we aren't tracking devices at all,
// so we don't know what the answer is. Let's error on the safe side and show
// a warning for this case.
let e2eStatus = E2EStatus.Warning;
if (this.context.isCryptoEnabled()) {
if (this.context.client.isCryptoEnabled()) {
/* At this point, the user has encryption on and cross-signing on */
e2eStatus = await shieldStatusForRoom(this.context, room);
e2eStatus = await shieldStatusForRoom(this.context.client, room);
}
if (this.unmounted) return;
@ -1374,19 +1367,17 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private updatePermissions(room: Room) {
if (room) {
const me = this.context.getUserId();
const me = this.context.client.getUserId();
const canReact = (
room.getMyMembership() === "join" &&
room.currentState.maySendEvent(EventType.Reaction, me)
);
const canSendMessages = room.maySendMessage();
const canSelfRedact = room.currentState.maySendEvent(EventType.RoomRedaction, me);
const canSendVoiceBroadcasts = room.currentState.maySendEvent(VoiceBroadcastInfoEventType, me);
this.setState({
canReact,
canSendMessages,
canSendVoiceBroadcasts,
canSelfRedact,
});
}
@ -1442,7 +1433,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private onJoinButtonClicked = () => {
// If the user is a ROU, allow them to transition to a PWLU
if (this.context?.isGuest()) {
if (this.context.client?.isGuest()) {
// Join this room once the user has registered and logged in
// (If we failed to peek, we may not have a valid room object.)
dis.dispatch<DoAfterSyncPreparedPayload<ViewRoomPayload>>({
@ -1499,13 +1490,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};
private injectSticker(url: string, info: object, text: string, threadId: string | null) {
if (this.context.isGuest()) {
if (this.context.client.isGuest()) {
dis.dispatch({ action: 'require_registration' });
return;
}
ContentMessages.sharedInstance()
.sendStickerContentToRoom(url, this.state.room.roomId, threadId, info, text, this.context)
.sendStickerContentToRoom(url, this.state.room.roomId, threadId, info, text, this.context.client)
.then(undefined, (error) => {
if (error.name === "UnknownDeviceError") {
// Let the staus bar handle this
@ -1578,7 +1569,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return b.length - a.length;
});
if (this.context.supportsExperimentalThreads()) {
if (this.context.client.supportsExperimentalThreads()) {
// Process all thread roots returned in this batch of search results
// XXX: This won't work for results coming from Seshat which won't include the bundled relationship
for (const result of results.results) {
@ -1586,7 +1577,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const bundledRelationship = event
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
if (!bundledRelationship || event.getThread()) continue;
const room = this.context.getRoom(event.getRoomId());
const room = this.context.client.getRoom(event.getRoomId());
const thread = room.findThreadForEvent(event);
if (thread) {
event.setThread(thread);
@ -1658,7 +1649,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const mxEv = result.context.getEvent();
const roomId = mxEv.getRoomId();
const room = this.context.getRoom(roomId);
const room = this.context.client.getRoom(roomId);
if (!room) {
// if we do not have the room in js-sdk stores then hide it as we cannot easily show it
// As per the spec, an all rooms search can create this condition,
@ -1715,7 +1706,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.setState({
rejecting: true,
});
this.context.leave(this.state.roomId).then(() => {
this.context.client.leave(this.state.roomId).then(() => {
dis.dispatch({ action: Action.ViewHomePage });
this.setState({
rejecting: false,
@ -1742,13 +1733,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
});
try {
const myMember = this.state.room.getMember(this.context.getUserId());
const myMember = this.state.room.getMember(this.context.client.getUserId());
const inviteEvent = myMember.events.member;
const ignoredUsers = this.context.getIgnoredUsers();
const ignoredUsers = this.context.client.getIgnoredUsers();
ignoredUsers.push(inviteEvent.getSender()); // de-duped internally in the js-sdk
await this.context.setIgnoredUsers(ignoredUsers);
await this.context.client.setIgnoredUsers(ignoredUsers);
await this.context.leave(this.state.roomId);
await this.context.client.leave(this.state.roomId);
dis.dispatch({ action: Action.ViewHomePage });
this.setState({
rejecting: false,
@ -1911,7 +1902,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (!this.state.room) {
return null;
}
return LegacyCallHandler.instance.getCallForRoom(this.state.room.roomId);
return this.context.legacyCallHandler.getCallForRoom(this.state.room.roomId);
}
// this has to be a proper method rather than an unnamed function,
@ -1924,7 +1915,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const createEvent = this.state.room.currentState.getStateEvents(EventType.RoomCreate, "");
if (!createEvent || !createEvent.getContent()['predecessor']) return null;
return this.context.getRoom(createEvent.getContent()['predecessor']['room_id']);
return this.context.client.getRoom(createEvent.getContent()['predecessor']['room_id']);
}
getHiddenHighlightCount() {
@ -1953,7 +1944,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
Array.from(dataTransfer.files),
this.state.room?.roomId ?? this.state.roomId,
null,
this.context,
this.context.client,
TimelineRenderingType.Room,
);
@ -1970,7 +1961,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
private renderLocalRoomCreateLoader(): ReactElement {
const names = this.state.room.getDefaultRoomName(this.props.mxClient.getUserId());
const names = this.state.room.getDefaultRoomName(this.context.client.getUserId());
return <RoomContext.Provider value={this.state}>
<LocalRoomCreateLoader
names={names}
@ -2081,7 +2072,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
</ErrorBoundary>
);
} else {
const myUserId = this.context.credentials.userId;
const myUserId = this.context.client.credentials.userId;
const myMember = this.state.room.getMember(myUserId);
const inviteEvent = myMember ? myMember.events.member : null;
let inviterName = _t("Unknown");
@ -2162,7 +2153,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const showRoomUpgradeBar = (
roomVersionRecommendation &&
roomVersionRecommendation.needsUpgrade &&
this.state.room.userMayUpgradeRoom(this.context.credentials.userId)
this.state.room.userMayUpgradeRoom(this.context.client.credentials.userId)
);
const hiddenHighlightCount = this.getHiddenHighlightCount();
@ -2174,7 +2165,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
searchInProgress={this.state.searchInProgress}
onCancelClick={this.onCancelSearchClick}
onSearch={this.onSearch}
isRoomEncrypted={this.context.isRoomEncrypted(this.state.room.roomId)}
isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)}
/>;
} else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} />;
@ -2236,7 +2227,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const auxPanel = (
<AuxPanel
room={this.state.room}
userId={this.context.credentials.userId}
userId={this.context.client.credentials.userId}
showApps={this.state.showApps}
resizeNotifier={this.props.resizeNotifier}
>
@ -2257,7 +2248,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
resizeNotifier={this.props.resizeNotifier}
replyToEvent={this.state.replyToEvent}
permalinkCreator={this.permalinkCreator}
showVoiceBroadcastButton={this.state.canSendVoiceBroadcasts}
/>;
}
@ -2397,7 +2387,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
mainSplitBody = <>
<AppsDrawer
room={this.state.room}
userId={this.context.credentials.userId}
userId={this.context.client.credentials.userId}
resizeNotifier={this.props.resizeNotifier}
showApps={true}
/>
@ -2451,7 +2441,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
onAppsClick = null;
onForgetClick = null;
onSearchClick = null;
if (this.state.room.canInvite(this.context.credentials.userId)) {
if (this.state.room.canInvite(this.context.client.credentials.userId)) {
onInviteClick = this.onInviteClick;
}
viewingCall = true;
@ -2493,5 +2483,4 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
}
const RoomViewWithMatrixClient = withMatrixClientHOC(RoomView);
export default RoomViewWithMatrixClient;
export default RoomView;

View file

@ -60,13 +60,13 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import { useTypedEventEmitterState } from "../../hooks/useEventEmitter";
import { IOOBData } from "../../stores/ThreepidInviteStore";
import { awaitRoomDownSync } from "../../utils/RoomUpgrade";
import { RoomViewStore } from "../../stores/RoomViewStore";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { Alignment } from "../views/elements/Tooltip";
import { getTopic } from "../../hooks/room/useTopic";
import { SdkContextClass } from "../../contexts/SDKContext";
interface IProps {
space: Room;
@ -378,7 +378,7 @@ export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
metricsTrigger: "SpaceHierarchy",
});
}, err => {
RoomViewStore.instance.showJoinRoomError(err, roomId);
SdkContextClass.instance.roomViewStore.showJoinRoomError(err, roomId);
});
return prom;

View file

@ -16,7 +16,7 @@ limitations under the License.
import React, { createRef, KeyboardEvent } from 'react';
import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
import { Room } from 'matrix-js-sdk/src/models/room';
import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room';
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
import { Direction } from 'matrix-js-sdk/src/models/event-timeline';
@ -51,10 +51,10 @@ import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import Measured from '../views/elements/Measured';
import PosthogTrackers from "../../PosthogTrackers";
import { ButtonEvent } from "../views/elements/AccessibleButton";
import { RoomViewStore } from '../../stores/RoomViewStore';
import Spinner from "../views/elements/Spinner";
import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
import Heading from '../views/typography/Heading';
import { SdkContextClass } from '../../contexts/SDKContext';
interface IProps {
room: Room;
@ -70,6 +70,7 @@ interface IProps {
interface IState {
thread?: Thread;
lastReply?: MatrixEvent | null;
layout: Layout;
editState?: EditorStateTransfer;
replyToEvent?: MatrixEvent;
@ -88,9 +89,16 @@ export default class ThreadView extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
const thread = this.props.room.getThread(this.props.mxEvent.getId());
this.setupThreadListeners(thread);
this.state = {
layout: SettingsStore.getValue("layout"),
narrow: false,
thread,
lastReply: thread?.lastReply((ev: MatrixEvent) => {
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
}),
};
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
@ -99,6 +107,9 @@ export default class ThreadView extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
if (this.state.thread) {
this.postThreadUpdate(this.state.thread);
}
this.setupThread(this.props.mxEvent);
this.dispatcherRef = dis.register(this.onAction);
@ -113,7 +124,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
room.removeListener(ThreadEvent.New, this.onNewThread);
SettingsStore.unwatchSetting(this.layoutWatcherRef);
const hasRoomChanged = RoomViewStore.instance.getRoomId() !== roomId;
const hasRoomChanged = SdkContextClass.instance.roomViewStore.getRoomId() !== roomId;
if (this.props.isInitialEventHighlighted && !hasRoomChanged) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
@ -189,19 +200,49 @@ export default class ThreadView extends React.Component<IProps, IState> {
}
};
private updateThreadRelation = (): void => {
this.setState({
lastReply: this.threadLastReply,
});
};
private get threadLastReply(): MatrixEvent | undefined {
return this.state.thread?.lastReply((ev: MatrixEvent) => {
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
});
}
private updateThread = (thread?: Thread) => {
if (thread && this.state.thread !== thread) {
if (this.state.thread === thread) return;
this.setupThreadListeners(thread, this.state.thread);
if (thread) {
this.setState({
thread,
}, async () => {
thread.emit(ThreadEvent.ViewThread);
await thread.fetchInitialEvents();
this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
this.timelinePanel.current?.refreshTimeline();
});
lastReply: this.threadLastReply,
}, async () => this.postThreadUpdate(thread));
}
};
private async postThreadUpdate(thread: Thread): Promise<void> {
thread.emit(ThreadEvent.ViewThread);
await thread.fetchInitialEvents();
this.updateThreadRelation();
this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
this.timelinePanel.current?.refreshTimeline();
}
private setupThreadListeners(thread?: Thread | undefined, oldThread?: Thread | undefined): void {
if (oldThread) {
this.state.thread.off(ThreadEvent.NewReply, this.updateThreadRelation);
this.props.room.off(RoomEvent.LocalEchoUpdated, this.updateThreadRelation);
}
if (thread) {
thread.on(ThreadEvent.NewReply, this.updateThreadRelation);
this.props.room.on(RoomEvent.LocalEchoUpdated, this.updateThreadRelation);
}
}
private resetJumpToEvent = (event?: string): void => {
if (this.props.initialEvent && this.props.initialEventScrollIntoView &&
event === this.props.initialEvent?.getId()) {
@ -242,14 +283,14 @@ export default class ThreadView extends React.Component<IProps, IState> {
}
};
private nextBatch: string;
private nextBatch: string | undefined | null = null;
private onPaginationRequest = async (
timelineWindow: TimelineWindow | null,
direction = Direction.Backward,
limit = 20,
): Promise<boolean> => {
if (!Thread.hasServerSideSupport) {
if (!Thread.hasServerSideSupport && timelineWindow) {
timelineWindow.extend(direction, limit);
return true;
}
@ -262,40 +303,50 @@ export default class ThreadView extends React.Component<IProps, IState> {
opts.from = this.nextBatch;
}
const { nextBatch } = await this.state.thread.fetchEvents(opts);
this.nextBatch = nextBatch;
let nextBatch: string | null | undefined = null;
if (this.state.thread) {
const response = await this.state.thread.fetchEvents(opts);
nextBatch = response.nextBatch;
this.nextBatch = nextBatch;
}
// Advances the marker on the TimelineWindow to define the correct
// window of events to display on screen
timelineWindow.extend(direction, limit);
timelineWindow?.extend(direction, limit);
return !!nextBatch;
};
private onFileDrop = (dataTransfer: DataTransfer) => {
ContentMessages.sharedInstance().sendContentListToRoom(
Array.from(dataTransfer.files),
this.props.mxEvent.getRoomId(),
this.threadRelation,
MatrixClientPeg.get(),
TimelineRenderingType.Thread,
);
const roomId = this.props.mxEvent.getRoomId();
if (roomId) {
ContentMessages.sharedInstance().sendContentListToRoom(
Array.from(dataTransfer.files),
roomId,
this.threadRelation,
MatrixClientPeg.get(),
TimelineRenderingType.Thread,
);
} else {
console.warn("Unknwon roomId for event", this.props.mxEvent);
}
};
private get threadRelation(): IEventRelation {
const lastThreadReply = this.state.thread?.lastReply((ev: MatrixEvent) => {
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
});
return {
const relation = {
"rel_type": THREAD_RELATION_TYPE.name,
"event_id": this.state.thread?.id,
"is_falling_back": true,
"m.in_reply_to": {
"event_id": lastThreadReply?.getId() ?? this.state.thread?.id,
},
};
const fallbackEventId = this.state.lastReply?.getId() ?? this.state.thread?.id;
if (fallbackEventId) {
relation["m.in_reply_to"] = {
"event_id": fallbackEventId,
};
}
return relation;
}
private renderThreadViewHeader = (): JSX.Element => {
@ -314,7 +365,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
const threadRelation = this.threadRelation;
let timeline: JSX.Element;
let timeline: JSX.Element | null;
if (this.state.thread) {
if (this.props.initialEvent && this.props.initialEvent.getRoomId() !== this.state.thread.roomId) {
logger.warn("ThreadView attempting to render TimelinePanel with mismatched initialEvent",

View file

@ -0,0 +1,396 @@
/*
Copyright 2022 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.
*/
import React from 'react';
import { MSC3906Rendezvous, MSC3906RendezvousPayload, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous';
import { MSC3886SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvous/transports';
import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from 'matrix-js-sdk/src/rendezvous/channels';
import { logger } from 'matrix-js-sdk/src/logger';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { _t } from "../../../languageHandler";
import AccessibleButton from '../elements/AccessibleButton';
import QRCode from '../elements/QRCode';
import Spinner from '../elements/Spinner';
import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.svg";
import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg";
import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg";
import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg";
import { wrapRequestWithDialog } from '../../../utils/UserInteractiveAuth';
/**
* The intention of this enum is to have a mode that scans a QR code instead of generating one.
*/
export enum Mode {
/**
* A QR code with be generated and shown
*/
Show = "show",
}
enum Phase {
Loading,
ShowingQR,
Connecting,
Connected,
WaitingForDevice,
Verifying,
Error,
}
interface IProps {
client: MatrixClient;
mode: Mode;
onFinished(...args: any): void;
}
interface IState {
phase: Phase;
rendezvous?: MSC3906Rendezvous;
confirmationDigits?: string;
failureReason?: RendezvousFailureReason;
mediaPermissionError?: boolean;
}
/**
* A component that allows sign in and E2EE set up with a QR code.
*
* It implements both `login.start` and `login-reciprocate` capabilities as well as both scanning and showing QR codes.
*
* This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
*/
export default class LoginWithQR extends React.Component<IProps, IState> {
public constructor(props) {
super(props);
this.state = {
phase: Phase.Loading,
};
}
public componentDidMount(): void {
this.updateMode(this.props.mode).then(() => {});
}
public componentDidUpdate(prevProps: Readonly<IProps>): void {
if (prevProps.mode !== this.props.mode) {
this.updateMode(this.props.mode).then(() => {});
}
}
private async updateMode(mode: Mode) {
this.setState({ phase: Phase.Loading });
if (this.state.rendezvous) {
this.state.rendezvous.onFailure = undefined;
await this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled);
this.setState({ rendezvous: undefined });
}
if (mode === Mode.Show) {
await this.generateCode();
}
}
public componentWillUnmount(): void {
if (this.state.rendezvous) {
// eslint-disable-next-line react/no-direct-mutation-state
this.state.rendezvous.onFailure = undefined;
// calling cancel will call close() as well to clean up the resources
this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled).then(() => {});
}
}
private approveLogin = async (): Promise<void> => {
if (!this.state.rendezvous) {
throw new Error('Rendezvous not found');
}
this.setState({ phase: Phase.Loading });
try {
logger.info("Requesting login token");
const { login_token: loginToken } = await wrapRequestWithDialog(this.props.client.requestLoginToken, {
matrixClient: this.props.client,
title: _t("Sign in new device"),
})();
this.setState({ phase: Phase.WaitingForDevice });
const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken);
if (!newDeviceId) {
// user denied
return;
}
if (!this.props.client.crypto) {
// no E2EE to set up
this.props.onFinished(true);
return;
}
await this.state.rendezvous.verifyNewDeviceOnExistingDevice();
this.props.onFinished(true);
} catch (e) {
logger.error('Error whilst approving sign in', e);
this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown });
}
};
private generateCode = async () => {
let rendezvous: MSC3906Rendezvous;
try {
const transport = new MSC3886SimpleHttpRendezvousTransport<MSC3903ECDHPayload>({
onFailure: this.onFailure,
client: this.props.client,
});
const channel = new MSC3903ECDHv1RendezvousChannel<MSC3906RendezvousPayload>(
transport, undefined, this.onFailure,
);
rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure);
await rendezvous.generateCode();
this.setState({
phase: Phase.ShowingQR,
rendezvous,
failureReason: undefined,
});
} catch (e) {
logger.error('Error whilst generating QR code', e);
this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.HomeserverLacksSupport });
return;
}
try {
const confirmationDigits = await rendezvous.startAfterShowingCode();
this.setState({ phase: Phase.Connected, confirmationDigits });
} catch (e) {
logger.error('Error whilst doing QR login', e);
// only set to error phase if it hasn't already been set by onFailure or similar
if (this.state.phase !== Phase.Error) {
this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown });
}
}
};
private onFailure = (reason: RendezvousFailureReason) => {
logger.info(`Rendezvous failed: ${reason}`);
this.setState({ phase: Phase.Error, failureReason: reason });
};
public reset() {
this.setState({
rendezvous: undefined,
confirmationDigits: undefined,
failureReason: undefined,
});
}
private cancelClicked = async (e: React.FormEvent) => {
e.preventDefault();
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
this.reset();
this.props.onFinished(false);
};
private declineClicked = async (e: React.FormEvent) => {
e.preventDefault();
await this.state.rendezvous?.declineLoginOnExistingDevice();
this.reset();
this.props.onFinished(false);
};
private tryAgainClicked = async (e: React.FormEvent) => {
e.preventDefault();
this.reset();
await this.updateMode(this.props.mode);
};
private onBackClick = async () => {
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
this.props.onFinished(false);
};
private cancelButton = () => <AccessibleButton
kind="primary_outline"
onClick={this.cancelClicked}
>
{ _t("Cancel") }
</AccessibleButton>;
private simpleSpinner = (description?: string): JSX.Element => {
return <div className="mx_LoginWithQR_spinner">
<div>
<Spinner />
{ description && <p>{ description }</p> }
</div>
</div>;
};
public render() {
let title: string;
let titleIcon: JSX.Element | undefined;
let main: JSX.Element | undefined;
let buttons: JSX.Element | undefined;
let backButton = true;
let cancellationMessage: string | undefined;
let centreTitle = false;
switch (this.state.phase) {
case Phase.Error:
switch (this.state.failureReason) {
case RendezvousFailureReason.Expired:
cancellationMessage = _t("The linking wasn't completed in the required time.");
break;
case RendezvousFailureReason.InvalidCode:
cancellationMessage = _t("The scanned code is invalid.");
break;
case RendezvousFailureReason.UnsupportedAlgorithm:
cancellationMessage = _t("Linking with this device is not supported.");
break;
case RendezvousFailureReason.UserDeclined:
cancellationMessage = _t("The request was declined on the other device.");
break;
case RendezvousFailureReason.OtherDeviceAlreadySignedIn:
cancellationMessage = _t("The other device is already signed in.");
break;
case RendezvousFailureReason.OtherDeviceNotSignedIn:
cancellationMessage = _t("The other device isn't signed in.");
break;
case RendezvousFailureReason.UserCancelled:
cancellationMessage = _t("The request was cancelled.");
break;
case RendezvousFailureReason.Unknown:
cancellationMessage = _t("An unexpected error occurred.");
break;
case RendezvousFailureReason.HomeserverLacksSupport:
cancellationMessage = _t("The homeserver doesn't support signing in another device.");
break;
default:
cancellationMessage = _t("The request was cancelled.");
break;
}
title = _t("Connection failed");
centreTitle = true;
titleIcon = <WarningBadge className="error" />;
backButton = false;
main = <p data-testid="cancellation-message">{ cancellationMessage }</p>;
buttons = <>
<AccessibleButton
kind="primary"
onClick={this.tryAgainClicked}
>
{ _t("Try again") }
</AccessibleButton>
{ this.cancelButton() }
</>;
break;
case Phase.Connected:
title = _t("Devices connected");
titleIcon = <DevicesIcon className="normal" />;
backButton = false;
main = <>
<p>{ _t("Check that the code below matches with your other device:") }</p>
<div className="mx_LoginWithQR_confirmationDigits">
{ this.state.confirmationDigits }
</div>
<div className="mx_LoginWithQR_confirmationAlert">
<div>
<InfoIcon />
</div>
<div>{ _t("By approving access for this device, it will have full access to your account.") }</div>
</div>
</>;
buttons = <>
<AccessibleButton
data-testid="decline-login-button"
kind="primary_outline"
onClick={this.declineClicked}
>
{ _t("Cancel") }
</AccessibleButton>
<AccessibleButton
data-testid="approve-login-button"
kind="primary"
onClick={this.approveLogin}
>
{ _t("Approve") }
</AccessibleButton>
</>;
break;
case Phase.ShowingQR:
title =_t("Sign in with QR code");
if (this.state.rendezvous) {
const code = <div className="mx_LoginWithQR_qrWrapper">
<QRCode data={[{ data: Buffer.from(this.state.rendezvous.code), mode: 'byte' }]} className="mx_QRCode" />
</div>;
main = <>
<p>{ _t("Scan the QR code below with your device that's signed out.") }</p>
<ol>
<li>{ _t("Start at the sign in screen") }</li>
<li>{ _t("Select 'Scan QR code'") }</li>
<li>{ _t("Review and approve the sign in") }</li>
</ol>
{ code }
</>;
} else {
main = this.simpleSpinner();
buttons = this.cancelButton();
}
break;
case Phase.Loading:
main = this.simpleSpinner();
break;
case Phase.Connecting:
main = this.simpleSpinner(_t("Connecting..."));
buttons = this.cancelButton();
break;
case Phase.WaitingForDevice:
main = this.simpleSpinner(_t("Waiting for device to sign in"));
buttons = this.cancelButton();
break;
case Phase.Verifying:
title = _t("Success");
centreTitle = true;
main = this.simpleSpinner(_t("Completing set up of your new device"));
break;
}
return (
<div data-testid="login-with-qr" className="mx_LoginWithQR">
<div className={centreTitle ? "mx_LoginWithQR_centreTitle" : ""}>
{ backButton ?
<AccessibleButton
data-testid="back-button"
className="mx_LoginWithQR_BackButton"
onClick={this.onBackClick}
title="Back"
>
<BackButtonIcon />
</AccessibleButton>
: null }
<h1>{ titleIcon }{ title }</h1>
</div>
<div className="mx_LoginWithQR_main">
{ main }
</div>
<div className="mx_LoginWithQR_buttons">
{ buttons }
</div>
</div>
);
}
}

View file

@ -0,0 +1,131 @@
/*
Copyright 2022 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.
*/
import React, { useCallback } from "react";
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../languageHandler";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import dispatcher, { defaultDispatcher } from "../../../dispatcher/dispatcher";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions";
import { Call, ConnectionState, ElementCall } from "../../../models/Call";
import { useCall } from "../../../hooks/useCall";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import {
OwnBeaconStore,
OwnBeaconStoreEvent,
} from "../../../stores/OwnBeaconStore";
import { CallDurationFromEvent } from "../voip/CallDuration";
import { SdkContextClass } from "../../../contexts/SDKContext";
interface RoomCallBannerProps {
roomId: Room["roomId"];
call: Call;
}
const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({
roomId,
call,
}) => {
const callEvent: MatrixEvent | null = (call as ElementCall)?.groupCall;
const connect = useCallback(
(ev: ButtonEvent) => {
ev.preventDefault();
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
view_call: true,
metricsTrigger: undefined,
});
},
[roomId],
);
const onClick = useCallback(() => {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
metricsTrigger: undefined,
event_id: callEvent.getId(),
scroll_into_view: true,
highlighted: true,
});
}, [callEvent, roomId]);
return (
<div
className="mx_RoomCallBanner"
onClick={onClick}
>
<div className="mx_RoomCallBanner_text">
<span className="mx_RoomCallBanner_label">{ _t("Video call") }</span>
<CallDurationFromEvent mxEvent={callEvent} />
</div>
<AccessibleButton
onClick={connect}
kind="primary"
element="button"
disabled={false}
>
{ _t("Join") }
</AccessibleButton>
</div>
);
};
interface Props {
roomId: Room["roomId"];
}
const RoomCallBanner: React.FC<Props> = ({ roomId }) => {
const call = useCall(roomId);
// this section is to check if we have a live location share. If so, we dont show the call banner
const isMonitoringLiveLocation = useEventEmitterState(
OwnBeaconStore.instance,
OwnBeaconStoreEvent.MonitoringLivePosition,
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
);
const liveBeaconIds = useEventEmitterState(
OwnBeaconStore.instance,
OwnBeaconStoreEvent.LivenessChange,
() => OwnBeaconStore.instance.getLiveBeaconIds(roomId),
);
if (isMonitoringLiveLocation && liveBeaconIds.length) {
return null;
}
// Check if the call is already showing. No banner is needed in this case.
if (SdkContextClass.instance.roomViewStore.isViewingCall()) {
return null;
}
// Split into outer/inner to avoid watching various parts if there is no call
if (call) {
// No banner if the call is connected (or connecting/disconnecting)
if (call.connectionState !== ConnectionState.Disconnected) return null;
return <RoomCallBannerInner call={call} roomId={roomId} />;
}
return null;
};
export default RoomCallBanner;

View file

@ -141,6 +141,7 @@ const RoomLiveShareWarning: React.FC<Props> = ({ roomId }) => {
);
if (!isMonitoringLiveLocation || !liveBeaconIds.length) {
// This logic is entangled with the RoomCallBanner-test's. The tests need updating if this logic changes.
return null;
}

View file

@ -37,7 +37,6 @@ import Modal from "../../../Modal";
import ExportDialog from "../dialogs/ExportDialog";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import { usePinnedEvents } from "../right_panel/PinnedMessagesCard";
import { RoomViewStore } from "../../../stores/RoomViewStore";
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import { ROOM_NOTIFICATIONS_TAB } from "../dialogs/RoomSettingsDialog";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
@ -50,6 +49,7 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import SettingsStore from "../../../settings/SettingsStore";
import DevtoolsDialog from "../dialogs/DevtoolsDialog";
import { SdkContextClass } from "../../../contexts/SDKContext";
interface IProps extends IContextMenuProps {
room: Room;
@ -332,7 +332,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
};
const ensureViewingRoom = (ev: ButtonEvent) => {
if (RoomViewStore.instance.getRoomId() === room.roomId) return;
if (SdkContextClass.instance.roomViewStore.getRoomId() === room.roomId) return;
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
@ -377,7 +377,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
ev.stopPropagation();
Modal.createDialog(DevtoolsDialog, {
roomId: RoomViewStore.instance.getRoomId(),
roomId: SdkContextClass.instance.roomViewStore.getRoomId(),
}, "mx_DevtoolsDialog_wrapper");
onFinished();
}}

View file

@ -29,9 +29,9 @@ import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
interface IProps {
export interface ThreadListContextMenuProps {
mxEvent: MatrixEvent;
permalinkCreator: RoomPermalinkCreator;
permalinkCreator?: RoomPermalinkCreator;
onMenuToggle?: (open: boolean) => void;
}
@ -43,7 +43,7 @@ const contextMenuBelow = (elementRect: DOMRect) => {
return { left, top, chevronFace };
};
const ThreadListContextMenu: React.FC<IProps> = ({
const ThreadListContextMenu: React.FC<ThreadListContextMenuProps> = ({
mxEvent,
permalinkCreator,
onMenuToggle,
@ -64,12 +64,14 @@ const ThreadListContextMenu: React.FC<IProps> = ({
closeThreadOptions();
}, [mxEvent, closeThreadOptions]);
const copyLinkToThread = useCallback(async (evt: ButtonEvent) => {
evt.preventDefault();
evt.stopPropagation();
const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId());
await copyPlaintext(matrixToUrl);
closeThreadOptions();
const copyLinkToThread = useCallback(async (evt: ButtonEvent | undefined) => {
if (permalinkCreator) {
evt?.preventDefault();
evt?.stopPropagation();
const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId());
await copyPlaintext(matrixToUrl);
closeThreadOptions();
}
}, [mxEvent, closeThreadOptions, permalinkCreator]);
useEffect(() => {
@ -87,6 +89,7 @@ const ThreadListContextMenu: React.FC<IProps> = ({
title={_t("Thread options")}
isExpanded={menuDisplayed}
inputRef={button}
data-testid="threadlist-dropdown-button"
/>
{ menuDisplayed && (<IconizedContextMenu
onFinished={closeThreadOptions}
@ -102,11 +105,14 @@ const ThreadListContextMenu: React.FC<IProps> = ({
label={_t("View in room")}
iconClassName="mx_ThreadPanel_viewInRoom"
/> }
<IconizedContextMenuOption
onClick={(e) => copyLinkToThread(e)}
label={_t("Copy link to thread")}
iconClassName="mx_ThreadPanel_copyLinkToThread"
/>
{ permalinkCreator &&
<IconizedContextMenuOption
data-testid="copy-thread-link"
onClick={(e) => copyLinkToThread(e)}
label={_t("Copy link to thread")}
iconClassName="mx_ThreadPanel_copyLinkToThread"
/>
}
</IconizedContextMenuOptionList>
</IconizedContextMenu>) }
</React.Fragment>;

View file

@ -38,7 +38,7 @@ interface IDialogAesthetics {
};
}
interface IProps extends IDialogProps {
export interface InteractiveAuthDialogProps extends IDialogProps {
// matrix client to use for UI auth requests
matrixClient: MatrixClient;
@ -82,8 +82,8 @@ interface IState {
uiaStagePhase: number | string;
}
export default class InteractiveAuthDialog extends React.Component<IProps, IState> {
constructor(props: IProps) {
export default class InteractiveAuthDialog extends React.Component<InteractiveAuthDialogProps, IState> {
constructor(props: InteractiveAuthDialogProps) {
super(props);
this.state = {

View file

@ -21,10 +21,11 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { OIDCState, WidgetPermissionStore } from "../../../stores/widgets/WidgetPermissionStore";
import { OIDCState } from "../../../stores/widgets/WidgetPermissionStore";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { SdkContextClass } from '../../../contexts/SDKContext';
interface IProps extends IDialogProps {
widget: Widget;
@ -57,7 +58,7 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
if (this.state.rememberSelection) {
logger.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`);
WidgetPermissionStore.instance.setOIDCState(
SdkContextClass.instance.widgetPermissionStore.setOIDCState(
this.props.widget, this.props.widgetKind, this.props.inRoomId,
allowed ? OIDCState.Allowed : OIDCState.Denied,
);

View file

@ -66,7 +66,7 @@ import { BreadcrumbsStore } from "../../../../stores/BreadcrumbsStore";
import { RoomNotificationState } from "../../../../stores/notifications/RoomNotificationState";
import { RoomNotificationStateStore } from "../../../../stores/notifications/RoomNotificationStateStore";
import { RecentAlgorithm } from "../../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import { RoomViewStore } from "../../../../stores/RoomViewStore";
import { SdkContextClass } from "../../../../contexts/SDKContext";
import { getMetaSpaceName } from "../../../../stores/spaces";
import SpaceStore from "../../../../stores/spaces/SpaceStore";
import { DirectoryMember, Member, startDmOnFirstMessage } from "../../../../utils/direct-messages";
@ -1060,7 +1060,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
</h4>
<div>
{ BreadcrumbsStore.instance.rooms
.filter(r => r.roomId !== RoomViewStore.instance.getRoomId())
.filter(r => r.roomId !== SdkContextClass.instance.roomViewStore.getRoomId())
.map(room => (
<TooltipOption
id={`mx_SpotlightDialog_button_recentlyViewed_${room.roomId}`}

View file

@ -43,13 +43,13 @@ import { IApp } from "../../../stores/WidgetStore";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { OwnProfileStore } from '../../../stores/OwnProfileStore';
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
import { RoomViewStore } from '../../../stores/RoomViewStore';
import WidgetUtils from '../../../utils/WidgetUtils';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ActionPayload } from "../../../dispatcher/payloads";
import { Action } from '../../../dispatcher/actions';
import { ElementWidgetCapabilities } from '../../../stores/widgets/ElementWidgetCapabilities';
import { WidgetMessagingStore } from '../../../stores/widgets/WidgetMessagingStore';
import { SdkContextClass } from '../../../contexts/SDKContext';
interface IProps {
app: IApp;
@ -175,7 +175,7 @@ export default class AppTile extends React.Component<IProps, IState> {
);
if (isActiveWidget) {
// We just left the room that the active widget was from.
if (this.props.room && RoomViewStore.instance.getRoomId() !== this.props.room.roomId) {
if (this.props.room && SdkContextClass.instance.roomViewStore.getRoomId() !== this.props.room.roomId) {
// If we are not actively looking at the room then destroy this widget entirely.
this.endWidgetActions();
} else if (WidgetType.JITSI.matches(this.props.app.type)) {

View file

@ -40,6 +40,7 @@ export default class Spinner extends React.PureComponent<IProps> {
style={{ width: w, height: h }}
aria-label={_t("Loading...")}
role="progressbar"
data-testid="spinner"
/>
</div>
);

View file

@ -57,7 +57,7 @@ type State = Partial<Pick<CSSProperties, "display" | "right" | "top" | "transfor
export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
private static container: HTMLElement;
private parent: Element;
private parent: Element | null = null;
// XXX: This is because some components (Field) are unable to `import` the Tooltip class,
// so we expose the Alignment options off of us statically.
@ -87,7 +87,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
capture: true,
});
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
this.parent = ReactDOM.findDOMNode(this)?.parentNode as Element ?? null;
this.updatePosition();
}
@ -109,7 +109,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
// positioned, also taking into account any window zoom
private updatePosition = (): void => {
// When the tooltip is hidden, no need to thrash the DOM with `style` attribute updates (performance)
if (!this.props.visible) return;
if (!this.props.visible || !this.parent) return;
const parentBox = this.parent.getBoundingClientRect();
const width = UIStore.instance.windowWidth;

View file

@ -69,6 +69,7 @@ export const LocationButton: React.FC<IProps> = ({ roomId, sender, menuPosition,
iconClassName="mx_MessageComposer_location"
onClick={openMenu}
title={_t("Location")}
inputRef={button}
/>
{ contextMenu }

View file

@ -83,7 +83,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
getRelationsForEvent,
}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
const [onFocus, isActive, ref] = useRovingTabIndex(button);
const [onFocus, isActive] = useRovingTabIndex(button);
useEffect(() => {
onFocusChange(menuDisplayed);
}, [onFocusChange, menuDisplayed]);
@ -123,7 +123,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
onClick={onOptionsClick}
onContextMenu={onOptionsClick}
isExpanded={menuDisplayed}
inputRef={ref}
inputRef={button}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
>
@ -141,7 +141,7 @@ interface IReactButtonProps {
const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusChange }) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
const [onFocus, isActive, ref] = useRovingTabIndex(button);
const [onFocus, isActive] = useRovingTabIndex(button);
useEffect(() => {
onFocusChange(menuDisplayed);
}, [onFocusChange, menuDisplayed]);
@ -173,7 +173,7 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
onClick={onClick}
onContextMenu={onClick}
isExpanded={menuDisplayed}
inputRef={ref}
inputRef={button}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
>

View file

@ -43,8 +43,6 @@ import MjolnirBody from "./MjolnirBody";
import MBeaconBody from "./MBeaconBody";
import { IEventTileOps } from "../rooms/EventTile";
import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from '../../../voice-broadcast';
import { Features } from '../../../settings/Settings';
import { SettingLevel } from '../../../settings/SettingLevel';
// onMessageAllowed is handled internally
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
@ -58,18 +56,10 @@ interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper
isSeeingThroughMessageHiddenForModeration?: boolean;
}
interface State {
voiceBroadcastEnabled: boolean;
}
export interface IOperableEventTile {
getEventTileOps(): IEventTileOps;
}
interface State {
voiceBroadcastEnabled: boolean;
}
const baseBodyTypes = new Map<string, typeof React.Component>([
[MsgType.Text, TextualBody],
[MsgType.Notice, TextualBody],
@ -87,7 +77,7 @@ const baseEvTypes = new Map<string, React.ComponentType<Partial<IBodyProps>>>([
[M_BEACON_INFO.altName, MBeaconBody],
]);
export default class MessageEvent extends React.Component<IProps, State> implements IMediaBody, IOperableEventTile {
export default class MessageEvent extends React.Component<IProps> implements IMediaBody, IOperableEventTile {
private body: React.RefObject<React.Component | IOperableEventTile> = createRef();
private mediaHelper: MediaEventHelper;
private bodyTypes = new Map<string, typeof React.Component>(baseBodyTypes.entries());
@ -95,7 +85,6 @@ export default class MessageEvent extends React.Component<IProps, State> impleme
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
private voiceBroadcastSettingWatcherRef: string;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context);
@ -105,29 +94,15 @@ export default class MessageEvent extends React.Component<IProps, State> impleme
}
this.updateComponentMaps();
this.state = {
// only check voice broadcast settings for a voice broadcast event
voiceBroadcastEnabled: this.props.mxEvent.getType() === VoiceBroadcastInfoEventType
&& SettingsStore.getValue(Features.VoiceBroadcast),
};
}
public componentDidMount(): void {
this.props.mxEvent.addListener(MatrixEventEvent.Decrypted, this.onDecrypted);
if (this.props.mxEvent.getType() === VoiceBroadcastInfoEventType) {
this.watchVoiceBroadcastFeatureSetting();
}
}
public componentWillUnmount() {
this.props.mxEvent.removeListener(MatrixEventEvent.Decrypted, this.onDecrypted);
this.mediaHelper?.destroy();
if (this.voiceBroadcastSettingWatcherRef) {
SettingsStore.unwatchSetting(this.voiceBroadcastSettingWatcherRef);
}
}
public componentDidUpdate(prevProps: Readonly<IProps>) {
@ -171,16 +146,6 @@ export default class MessageEvent extends React.Component<IProps, State> impleme
this.forceUpdate();
};
private watchVoiceBroadcastFeatureSetting(): void {
this.voiceBroadcastSettingWatcherRef = SettingsStore.watchSetting(
Features.VoiceBroadcast,
null,
(settingName: string, roomId: string, atLevel: SettingLevel, newValAtLevel, newValue: boolean) => {
this.setState({ voiceBroadcastEnabled: newValue });
},
);
}
public render() {
const content = this.props.mxEvent.getContent();
const type = this.props.mxEvent.getType();
@ -209,8 +174,7 @@ export default class MessageEvent extends React.Component<IProps, State> impleme
}
if (
this.state.voiceBroadcastEnabled
&& type === VoiceBroadcastInfoEventType
type === VoiceBroadcastInfoEventType
&& content?.state === VoiceBroadcastInfoState.Started
) {
BodyType = VoiceBroadcastBody;

View file

@ -33,7 +33,6 @@ import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import { ActionPayload } from '../../../dispatcher/payloads';
import { Action } from '../../../dispatcher/actions';
import { RoomViewStore } from '../../../stores/RoomViewStore';
import ContentMessages from '../../../ContentMessages';
import UploadBar from '../../structures/UploadBar';
import SettingsStore from '../../../settings/SettingsStore';
@ -42,6 +41,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import Measured from '../elements/Measured';
import Heading from '../typography/Heading';
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
import { SdkContextClass } from '../../../contexts/SDKContext';
interface IProps {
room: Room;
@ -91,7 +91,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
RoomViewStore.instance.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.dispatcherRef = dis.register(this.onAction);
this.readReceiptsSettingWatcher = SettingsStore.watchSetting("showReadReceipts", null, (...[,,, value]) =>
this.setState({ showReadReceipts: value as boolean }),
@ -102,7 +102,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
}
public componentWillUnmount(): void {
RoomViewStore.instance.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SdkContextClass.instance.roomViewStore.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
if (this.readReceiptsSettingWatcher) {
SettingsStore.unwatchSetting(this.readReceiptsSettingWatcher);
@ -116,12 +116,9 @@ export default class TimelineCard extends React.Component<IProps, IState> {
private onRoomViewStoreUpdate = async (initial?: boolean): Promise<void> => {
const newState: Pick<IState, any> = {
// roomLoading: RoomViewStore.instance.isRoomLoading(),
// roomLoadError: RoomViewStore.instance.getRoomLoadError(),
initialEventId: RoomViewStore.instance.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.instance.isInitialEventHighlighted(),
replyToEvent: RoomViewStore.instance.getQuotingEvent(),
initialEventId: SdkContextClass.instance.roomViewStore.getInitialEventId(),
isInitialEventHighlighted: SdkContextClass.instance.roomViewStore.isInitialEventHighlighted(),
replyToEvent: SdkContextClass.instance.roomViewStore.getQuotingEvent(),
};
this.setState(newState);

View file

@ -36,7 +36,6 @@ import { _t } from '../../../languageHandler';
import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import SdkConfig from '../../../SdkConfig';
import { RoomViewStore } from "../../../stores/RoomViewStore";
import MultiInviter from "../../../utils/MultiInviter";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon";
@ -77,6 +76,7 @@ import UserIdentifierCustomisations from '../../../customisations/UserIdentifier
import PosthogTrackers from "../../../PosthogTrackers";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { DirectoryMember, startDmOnFirstMessage } from '../../../utils/direct-messages';
import { SdkContextClass } from '../../../contexts/SDKContext';
export interface IDevice {
deviceId: string;
@ -412,7 +412,7 @@ const UserOptionsSection: React.FC<{
}
if (canInvite && (member?.membership ?? 'leave') === 'leave' && shouldShowComponent(UIComponent.InviteUsers)) {
const roomId = member && member.roomId ? member.roomId : RoomViewStore.instance.getRoomId();
const roomId = member && member.roomId ? member.roomId : SdkContextClass.instance.roomViewStore.getRoomId();
const onInviteUserButton = async (ev: ButtonEvent) => {
try {
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.

View file

@ -31,7 +31,6 @@ import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
import { getAutoCompleteCreator, Part, Type } from '../../../editor/parts';
import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
import { renderModel } from '../../../editor/render';
import TypingStore from "../../../stores/TypingStore";
import SettingsStore from "../../../settings/SettingsStore";
import { IS_MAC, Key } from "../../../Keyboard";
import { EMOTICON_TO_EMOJI } from "../../../emoji";
@ -47,6 +46,7 @@ import { getKeyBindingsManager } from '../../../KeyBindingsManager';
import { ALTERNATE_KEY_NAME, KeyBindingAction } from '../../../accessibility/KeyboardShortcuts';
import { _t } from "../../../languageHandler";
import { linkify } from '../../../linkify-matrix';
import { SdkContextClass } from '../../../contexts/SDKContext';
// matches emoticons which follow the start of a line or whitespace
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
@ -246,7 +246,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
isTyping = false;
}
}
TypingStore.sharedInstance().setSelfTyping(
SdkContextClass.instance.typingStore.setSelfTyping(
this.props.room.roomId,
this.props.threadId,
isTyping,
@ -789,6 +789,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
aria-activedescendant={activeDescendant}
dir="auto"
aria-disabled={this.props.disabled}
data-testid="basicmessagecomposer"
/>
</div>);
}

View file

@ -932,6 +932,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
rightClick={true}
reactions={this.state.reactions}
link={this.state.contextMenu.link}
getRelationsForEvent={this.props.getRelationsForEvent}
/>
);
}

View file

@ -73,6 +73,7 @@ function SendButton(props: ISendButtonProps) {
className="mx_MessageComposer_sendMessage"
onClick={props.onClick}
title={props.title ?? _t('Send message')}
data-testid="sendmessagebtn"
/>
);
}
@ -85,7 +86,6 @@ interface IProps {
relation?: IEventRelation;
e2eStatus?: E2EStatus;
compact?: boolean;
showVoiceBroadcastButton?: boolean;
}
interface IState {
@ -384,10 +384,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
return this.state.showStickersButton && !isLocalRoom(this.props.room);
}
private get showVoiceBroadcastButton(): boolean {
return this.props.showVoiceBroadcastButton && this.state.showVoiceBroadcastButton;
}
public render() {
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
const controls = [
@ -532,10 +528,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
showPollsButton={this.state.showPollsButton}
showStickersButton={this.showStickersButton}
toggleButtonMenu={this.toggleButtonMenu}
showVoiceBroadcastButton={this.showVoiceBroadcastButton}
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
onStartVoiceBroadcastClick={() => {
startNewVoiceBroadcastRecording(
this.props.room.roomId,
this.props.room,
MatrixClientPeg.get(),
VoiceBroadcastRecordingsStore.instance(),
);

View file

@ -179,6 +179,7 @@ const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition }) =>
iconClassName="mx_MessageComposer_emoji"
onClick={openMenu}
title={_t("Emoji")}
inputRef={button}
/>
{ contextMenu }

View file

@ -209,7 +209,8 @@ export function ReadReceiptGroup(
onMouseOver={showTooltip}
onMouseLeave={hideTooltip}
onFocus={showTooltip}
onBlur={hideTooltip}>
onBlur={hideTooltip}
>
{ remText }
<span
className="mx_ReadReceiptGroup_container"

View file

@ -67,6 +67,7 @@ import IconizedContextMenu, {
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { CallDurationFromEvent } from "../voip/CallDuration";
import { Alignment } from "../elements/Tooltip";
import RoomCallBanner from '../beacon/RoomCallBanner';
class DisabledWithReason {
constructor(public readonly reason: string) { }
@ -733,6 +734,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
{ betaPill }
{ buttons }
</div>
{ !isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} /> }
<RoomLiveShareWarning roomId={this.props.room.roomId} />
</header>
);

View file

@ -38,7 +38,6 @@ import { ITagMap } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import { RoomViewStore } from "../../../stores/RoomViewStore";
import {
isMetaSpace,
ISuggestedRoom,
@ -62,6 +61,7 @@ import IconizedContextMenu, {
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import ExtraTile from "./ExtraTile";
import RoomSublist, { IAuxButtonProps } from "./RoomSublist";
import { SdkContextClass } from "../../../contexts/SDKContext";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
@ -421,7 +421,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
public componentDidMount(): void {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
RoomViewStore.instance.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
this.favouriteMessageWatcher =
@ -436,19 +436,19 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
SettingsStore.unwatchSetting(this.favouriteMessageWatcher);
defaultDispatcher.unregister(this.dispatcherRef);
RoomViewStore.instance.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
}
private onRoomViewStoreUpdate = () => {
this.setState({
currentRoomId: RoomViewStore.instance.getRoomId(),
currentRoomId: SdkContextClass.instance.roomViewStore.getRoomId(),
});
};
private onAction = (payload: ActionPayload) => {
if (payload.action === Action.ViewRoomDelta) {
const viewRoomDeltaPayload = payload as ViewRoomDeltaPayload;
const currentRoomId = RoomViewStore.instance.getRoomId();
const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
const room = this.getRoomDelta(currentRoomId, viewRoomDeltaPayload.delta, viewRoomDeltaPayload.unread);
if (room) {
defaultDispatcher.dispatch<ViewRoomPayload>({

View file

@ -379,7 +379,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
isExpanded={mainMenuDisplayed}
className="mx_RoomListHeader_contextMenuButton"
title={activeSpace
? _t("%(spaceName)s menu", { spaceName })
? _t("%(spaceName)s menu", { spaceName: spaceName ?? activeSpace.name })
: _t("Home options")}
>
{ title }

View file

@ -44,10 +44,10 @@ import PosthogTrackers from "../../../PosthogTrackers";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { RoomViewStore } from "../../../stores/RoomViewStore";
import { RoomTileCallSummary } from "./RoomTileCallSummary";
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
import { CallStore, CallStoreEvent } from "../../../stores/CallStore";
import { SdkContextClass } from "../../../contexts/SDKContext";
interface IProps {
room: Room;
@ -86,7 +86,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
super(props);
this.state = {
selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId,
selected: SdkContextClass.instance.roomViewStore.getRoomId() === this.props.room.roomId,
notificationsMenuPosition: null,
generalMenuPosition: null,
call: CallStore.instance.getCall(this.props.room.roomId),
@ -146,7 +146,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.scrollIntoView();
}
RoomViewStore.instance.addRoomListener(this.props.room.roomId, this.onActiveRoomUpdate);
SdkContextClass.instance.roomViewStore.addRoomListener(this.props.room.roomId, this.onActiveRoomUpdate);
this.dispatcherRef = defaultDispatcher.register(this.onAction);
MessagePreviewStore.instance.on(
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
@ -163,7 +163,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
}
public componentWillUnmount() {
RoomViewStore.instance.removeRoomListener(this.props.room.roomId, this.onActiveRoomUpdate);
SdkContextClass.instance.roomViewStore.removeRoomListener(this.props.room.roomId, this.onActiveRoomUpdate);
MessagePreviewStore.instance.off(
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
this.onRoomPreviewChanged,

View file

@ -16,7 +16,7 @@ limitations under the License.
import React, { useCallback, useEffect } from 'react';
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
import { useWysiwyg, Wysiwyg, WysiwygInputEvent } from "@matrix-org/matrix-wysiwyg";
import { Editor } from './Editor';
import { FormattingButtons } from './FormattingButtons';
@ -25,6 +25,7 @@ import { sendMessage } from './message';
import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
import { useRoomContext } from '../../../../contexts/RoomContext';
import { useWysiwygActionHandler } from './useWysiwygActionHandler';
import { useSettingValue } from '../../../../hooks/useSettings';
interface WysiwygProps {
disabled?: boolean;
@ -41,8 +42,27 @@ export function WysiwygComposer(
) {
const roomContext = useRoomContext();
const mxClient = useMatrixClientContext();
const ctrlEnterToSend = useSettingValue("MessageComposerInput.ctrlEnterToSend");
const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg();
function inputEventProcessor(event: WysiwygInputEvent, wysiwyg: Wysiwyg): WysiwygInputEvent | null {
if (event instanceof ClipboardEvent) {
return event;
}
if (
(event.inputType === 'insertParagraph' && !ctrlEnterToSend) ||
event.inputType === 'sendMessage'
) {
sendMessage(content, { mxClient, roomContext, ...props });
wysiwyg.actions.clear();
ref.current?.focus();
return null;
}
return event;
}
const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg({ inputEventProcessor });
useEffect(() => {
if (!disabled && content !== null) {

View file

@ -19,13 +19,14 @@ import classNames from 'classnames';
import { IMyDevice } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
import { CryptoEvent } from 'matrix-js-sdk/src/crypto';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import DevicesPanelEntry from "./DevicesPanelEntry";
import Spinner from "../elements/Spinner";
import AccessibleButton from "../elements/AccessibleButton";
import { deleteDevicesWithInteractiveAuth } from './devices/deleteDevices';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
interface IProps {
className?: string;
@ -40,6 +41,8 @@ interface IState {
}
export default class DevicesPanel extends React.Component<IProps, IState> {
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
private unmounted = false;
constructor(props: IProps) {
@ -52,15 +55,22 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
this.context.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
this.loadDevices();
}
public componentWillUnmount(): void {
this.context.off(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
this.unmounted = true;
}
private onDevicesUpdated = (users: string[]) => {
if (!users.includes(this.context.getUserId())) return;
this.loadDevices();
};
private loadDevices(): void {
const cli = MatrixClientPeg.get();
const cli = this.context;
cli.getDevices().then(
(resp) => {
if (this.unmounted) { return; }
@ -111,7 +121,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
private isDeviceVerified(device: IMyDevice): boolean | null {
try {
const cli = MatrixClientPeg.get();
const cli = this.context;
const deviceInfo = cli.getStoredDevice(cli.getUserId(), device.device_id);
return this.state.crossSigningInfo.checkDeviceTrust(
this.state.crossSigningInfo,
@ -184,7 +194,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
try {
await deleteDevicesWithInteractiveAuth(
MatrixClientPeg.get(),
this.context,
this.state.selectedDevices,
(success) => {
if (success) {
@ -208,7 +218,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
};
private renderDevice = (device: IMyDevice): JSX.Element => {
const myDeviceId = MatrixClientPeg.get().getDeviceId();
const myDeviceId = this.context.getDeviceId();
const myDevice = this.state.devices.find((device) => (device.device_id === myDeviceId));
const isOwnDevice = device.device_id === myDeviceId;
@ -246,7 +256,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
return <Spinner />;
}
const myDeviceId = MatrixClientPeg.get().getDeviceId();
const myDeviceId = this.context.getDeviceId();
const myDevice = devices.find((device) => (device.device_id === myDeviceId));
if (!myDevice) {

View file

@ -62,7 +62,6 @@ const DeviceDetails: React.FC<Props> = ({
id: 'session',
values: [
{ label: _t('Session ID'), value: device.device_id },
{ label: _t('Client'), value: device.client },
{
label: _t('Last activity'),
value: device.last_seen_ts && formatDate(new Date(device.last_seen_ts)),
@ -84,6 +83,7 @@ const DeviceDetails: React.FC<Props> = ({
values: [
{ label: _t('Model'), value: device.deviceModel },
{ label: _t('Operating system'), value: device.deviceOperatingSystem },
{ label: _t('Browser'), value: device.client },
{ label: _t('IP address'), value: device.last_seen_ip },
],
},

View file

@ -0,0 +1,63 @@
/*
Copyright 2022 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.
*/
import React from 'react';
import type { IServerVersions } from 'matrix-js-sdk/src/matrix';
import { _t } from '../../../../languageHandler';
import AccessibleButton from '../../elements/AccessibleButton';
import SettingsSubsection from '../shared/SettingsSubsection';
import SettingsStore from '../../../../settings/SettingsStore';
interface IProps {
onShowQr: () => void;
versions: IServerVersions;
}
export default class LoginWithQRSection extends React.Component<IProps> {
public constructor(props: IProps) {
super(props);
}
public render(): JSX.Element {
const msc3882Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3882'];
const msc3886Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3886'];
// Needs to be enabled as a feature + server support MSC3886 or have a default rendezvous server configured:
const offerShowQr = SettingsStore.getValue("feature_qr_signin_reciprocate_show") &&
msc3882Supported && msc3886Supported; // We don't support configuration of a fallback at the moment so we just check the MSCs
// don't show anything if no method is available
if (!offerShowQr) {
return null;
}
return <SettingsSubsection
heading={_t('Sign in with QR code')}
>
<div className="mx_LoginWithQRSection">
<p className="mx_SettingsTab_subsectionText">{
_t("You can use this device to sign in a new device with a QR code. You will need to " +
"scan the QR code shown on this device with your device that's signed out.")
}</p>
<AccessibleButton
onClick={this.props.onShowQr}
kind="primary"
>{ _t("Show QR code") }</AccessibleButton>
</div>
</SettingsSubsection>;
}
}

View file

@ -31,6 +31,7 @@ import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/reque
import { MatrixError } from "matrix-js-sdk/src/http-api";
import { logger } from "matrix-js-sdk/src/logger";
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
import { _t } from "../../../../languageHandler";
@ -179,6 +180,12 @@ export const useOwnDevices = (): DevicesState => {
refreshDevices();
}, [refreshDevices]);
useEventEmitter(matrixClient, CryptoEvent.DevicesUpdated, (users: string[]): void => {
if (users.includes(userId)) {
refreshDevices();
}
});
useEventEmitter(matrixClient, ClientEvent.AccountData, (event: MatrixEvent): void => {
const type = event.getType();
if (type.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {

View file

@ -38,6 +38,9 @@ import InlineSpinner from "../../../elements/InlineSpinner";
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog";
import { privateShouldBeEncrypted } from "../../../../../utils/rooms";
import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
import LoginWithQRSection from '../../devices/LoginWithQRSection';
import type { IServerVersions } from 'matrix-js-sdk/src/matrix';
interface IIgnoredUserProps {
userId: string;
@ -72,6 +75,8 @@ interface IState {
waitingUnignored: string[];
managingInvites: boolean;
invitedRoomIds: Set<string>;
showLoginWithQR: Mode | null;
versions?: IServerVersions;
}
export default class SecurityUserSettingsTab extends React.Component<IProps, IState> {
@ -88,6 +93,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
waitingUnignored: [],
managingInvites: false,
invitedRoomIds,
showLoginWithQR: null,
};
}
@ -102,6 +108,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
public componentDidMount(): void {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on(RoomEvent.MyMembership, this.onMyMembership);
MatrixClientPeg.get().getVersions().then(versions => this.setState({ versions }));
}
public componentWillUnmount(): void {
@ -251,6 +258,14 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
);
}
private onShowQRClicked = (): void => {
this.setState({ showLoginWithQR: Mode.Show });
};
private onLoginWithQRFinished = (): void => {
this.setState({ showLoginWithQR: null });
};
public render(): JSX.Element {
const secureBackup = (
<div className='mx_SettingsTab_section'>
@ -347,6 +362,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
}
const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager");
const showQrCodeEnabled = SettingsStore.getValue("feature_qr_signin_reciprocate_show");
const devicesSection = useNewSessionManager
? null
: <>
@ -363,8 +379,20 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
</span>
<DevicesPanel />
</div>
{ showQrCodeEnabled ?
<LoginWithQRSection onShowQr={this.onShowQRClicked} versions={this.state.versions} />
: null
}
</>;
const client = MatrixClientPeg.get();
if (showQrCodeEnabled && this.state.showLoginWithQR) {
return <div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<LoginWithQR onFinished={this.onLoginWithQRFinished} mode={this.state.showLoginWithQR} client={client} />
</div>;
}
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
{ warning }

View file

@ -32,6 +32,10 @@ import SecurityRecommendations from '../../devices/SecurityRecommendations';
import { DeviceSecurityVariation, ExtendedDevice } from '../../devices/types';
import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices';
import SettingsTab from '../SettingsTab';
import LoginWithQRSection from '../../devices/LoginWithQRSection';
import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
import SettingsStore from '../../../../../settings/SettingsStore';
import { useAsyncMemo } from '../../../../../hooks/useAsyncMemo';
const useSignOut = (
matrixClient: MatrixClient,
@ -104,6 +108,7 @@ const SessionManagerTab: React.FC = () => {
const matrixClient = useContext(MatrixClientContext);
const userId = matrixClient.getUserId();
const currentUserMember = userId && matrixClient.getUser(userId) || undefined;
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
const onDeviceExpandToggle = (deviceId: ExtendedDevice['device_id']): void => {
if (expandedDeviceIds.includes(deviceId)) {
@ -175,6 +180,26 @@ const SessionManagerTab: React.FC = () => {
onSignOutOtherDevices(Object.keys(otherDevices));
}: undefined;
const [signInWithQrMode, setSignInWithQrMode] = useState<Mode | null>();
const showQrCodeEnabled = SettingsStore.getValue("feature_qr_signin_reciprocate_show");
const onQrFinish = useCallback(() => {
setSignInWithQrMode(null);
}, [setSignInWithQrMode]);
const onShowQrClicked = useCallback(() => {
setSignInWithQrMode(Mode.Show);
}, [setSignInWithQrMode]);
if (showQrCodeEnabled && signInWithQrMode) {
return <LoginWithQR
mode={signInWithQrMode}
onFinished={onQrFinish}
client={matrixClient}
/>;
}
return <SettingsTab heading={_t('Sessions')}>
<SecurityRecommendations
devices={devices}
@ -222,6 +247,10 @@ const SessionManagerTab: React.FC = () => {
/>
</SettingsSubsection>
}
{ showQrCodeEnabled ?
<LoginWithQRSection onShowQr={onShowQrClicked} versions={clientVersions} />
: null
}
</SettingsTab>;
};

View file

@ -36,7 +36,7 @@ import { Icon as FavoriteIcon } from '../../../../res/img/element-icons/roomlist
import SettingsStore from "../../../settings/SettingsStore";
import Modal from "../../../Modal";
import DevtoolsDialog from "../dialogs/DevtoolsDialog";
import { RoomViewStore } from "../../../stores/RoomViewStore";
import { SdkContextClass } from "../../../contexts/SDKContext";
const QuickSettingsButton = ({ isPanelCollapsed = false }) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
@ -72,7 +72,7 @@ const QuickSettingsButton = ({ isPanelCollapsed = false }) => {
onClick={() => {
closeMenu();
Modal.createDialog(DevtoolsDialog, {
roomId: RoomViewStore.instance.getRoomId(),
roomId: SdkContextClass.instance.roomViewStore.getRoomId(),
}, "mx_DevtoolsDialog_wrapper");
}}
kind="danger_outline"

View file

@ -204,9 +204,7 @@ const CreateSpaceButton = ({
isPanelCollapsed,
setPanelCollapsed,
}: Pick<IInnerSpacePanelProps, "isPanelCollapsed" | "setPanelCollapsed">) => {
// We don't need the handle as we position the menu in a constant location
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [menuDisplayed, _handle, openMenu, closeMenu] = useContextMenu<void>();
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>();
useEffect(() => {
if (!isPanelCollapsed && menuDisplayed) {
@ -231,13 +229,14 @@ const CreateSpaceButton = ({
role="treeitem"
>
<SpaceButton
data-test-id='create-space-button'
data-testid='create-space-button'
className={classNames("mx_SpaceButton_new", {
mx_SpaceButton_newCancel: menuDisplayed,
})}
label={menuDisplayed ? _t("Cancel") : _t("Create a space")}
onClick={onNewClick}
isNarrow={isPanelCollapsed}
ref={handle}
/>
{ contextMenu }

View file

@ -14,7 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { MouseEvent, ComponentProps, ComponentType, createRef, InputHTMLAttributes, LegacyRef } from "react";
import React, {
MouseEvent,
ComponentProps,
ComponentType,
createRef,
InputHTMLAttributes,
LegacyRef,
forwardRef,
RefObject,
} from "react";
import classNames from "classnames";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
@ -54,7 +63,7 @@ interface IButtonProps extends Omit<ComponentProps<typeof AccessibleTooltipButto
onClick?(ev?: ButtonEvent): void;
}
export const SpaceButton: React.FC<IButtonProps> = ({
export const SpaceButton = forwardRef<HTMLElement, IButtonProps>(({
space,
spaceKey,
className,
@ -67,9 +76,9 @@ export const SpaceButton: React.FC<IButtonProps> = ({
children,
ContextMenuComponent,
...props
}) => {
const [menuDisplayed, ref, openMenu, closeMenu] = useContextMenu<HTMLElement>();
const [onFocus, isActive, handle] = useRovingTabIndex(ref);
}, ref: RefObject<HTMLElement>) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>(ref);
const [onFocus, isActive] = useRovingTabIndex(handle);
const tabIndex = isActive ? 0 : -1;
let avatar = <div className="mx_SpaceButton_avatarPlaceholder"><div className="mx_SpaceButton_icon" /></div>;
@ -150,7 +159,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
</div>
</AccessibleTooltipButton>
);
};
});
interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
space: Room;

View file

@ -21,7 +21,6 @@ import classNames from 'classnames';
import { Room } from "matrix-js-sdk/src/models/room";
import LegacyCallView from "./LegacyCallView";
import { RoomViewStore } from '../../../stores/RoomViewStore';
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../../LegacyCallHandler';
import PersistentApp from "../elements/PersistentApp";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
@ -34,6 +33,7 @@ import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/Activ
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
import { SdkContextClass } from '../../../contexts/SDKContext';
import { CallStore } from "../../../stores/CallStore";
import {
VoiceBroadcastRecording,
@ -129,7 +129,7 @@ class PipView extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
const roomId = RoomViewStore.instance.getRoomId();
const roomId = SdkContextClass.instance.roomViewStore.getRoomId();
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(roomId);
@ -147,7 +147,7 @@ class PipView extends React.Component<IProps, IState> {
public componentDidMount() {
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls);
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
RoomViewStore.instance.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
const room = MatrixClientPeg.get()?.getRoom(this.state.viewedRoomId);
if (room) {
@ -164,7 +164,7 @@ class PipView extends React.Component<IProps, IState> {
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
const cli = MatrixClientPeg.get();
cli?.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
RoomViewStore.instance.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
SdkContextClass.instance.roomViewStore.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
const room = cli?.getRoom(this.state.viewedRoomId);
if (room) {
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
@ -186,7 +186,7 @@ class PipView extends React.Component<IProps, IState> {
private onMove = () => this.movePersistedElement.current?.();
private onRoomViewStoreUpdate = () => {
const newRoomId = RoomViewStore.instance.getRoomId();
const newRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
const oldRoomId = this.state.viewedRoomId;
if (newRoomId === oldRoomId) return;
// The WidgetLayoutStore observer always tracks the currently viewed Room,

View file

@ -45,7 +45,6 @@ const RoomContext = createContext<IRoomState>({
canReact: false,
canSelfRedact: false,
canSendMessages: false,
canSendVoiceBroadcasts: false,
resizing: false,
layout: Layout.Group,
lowBandwidth: false,

144
src/contexts/SDKContext.ts Normal file
View file

@ -0,0 +1,144 @@
/*
Copyright 2022 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.
*/
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { createContext } from "react";
import defaultDispatcher from "../dispatcher/dispatcher";
import LegacyCallHandler from "../LegacyCallHandler";
import { PosthogAnalytics } from "../PosthogAnalytics";
import { SlidingSyncManager } from "../SlidingSyncManager";
import { RoomNotificationStateStore } from "../stores/notifications/RoomNotificationStateStore";
import RightPanelStore from "../stores/right-panel/RightPanelStore";
import { RoomViewStore } from "../stores/RoomViewStore";
import SpaceStore, { SpaceStoreClass } from "../stores/spaces/SpaceStore";
import TypingStore from "../stores/TypingStore";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import { WidgetPermissionStore } from "../stores/widgets/WidgetPermissionStore";
import WidgetStore from "../stores/WidgetStore";
export const SDKContext = createContext<SdkContextClass>(undefined);
SDKContext.displayName = "SDKContext";
/**
* A class which lazily initialises stores as and when they are requested, ensuring they remain
* as singletons scoped to this object.
*/
export class SdkContextClass {
/**
* The global SdkContextClass instance. This is a temporary measure whilst so many stores remain global
* as well. Over time, these stores should accept a `SdkContextClass` instance in their constructor.
* When all stores do this, this static variable can be deleted.
*/
public static readonly instance = new SdkContextClass();
// Optional as we don't have a client on initial load if unregistered. This should be set
// when the MatrixClient is first acquired in the dispatcher event Action.OnLoggedIn.
// It is only safe to set this once, as updating this value will NOT notify components using
// this Context.
public client?: MatrixClient;
// All protected fields to make it easier to derive test stores
protected _WidgetPermissionStore?: WidgetPermissionStore;
protected _RightPanelStore?: RightPanelStore;
protected _RoomNotificationStateStore?: RoomNotificationStateStore;
protected _RoomViewStore?: RoomViewStore;
protected _WidgetLayoutStore?: WidgetLayoutStore;
protected _WidgetStore?: WidgetStore;
protected _PosthogAnalytics?: PosthogAnalytics;
protected _SlidingSyncManager?: SlidingSyncManager;
protected _SpaceStore?: SpaceStoreClass;
protected _LegacyCallHandler?: LegacyCallHandler;
protected _TypingStore?: TypingStore;
/**
* Automatically construct stores which need to be created eagerly so they can register with
* the dispatcher.
*/
public constructEagerStores() {
this._RoomViewStore = this.roomViewStore;
}
public get legacyCallHandler(): LegacyCallHandler {
if (!this._LegacyCallHandler) {
this._LegacyCallHandler = LegacyCallHandler.instance;
}
return this._LegacyCallHandler;
}
public get rightPanelStore(): RightPanelStore {
if (!this._RightPanelStore) {
this._RightPanelStore = RightPanelStore.instance;
}
return this._RightPanelStore;
}
public get roomNotificationStateStore(): RoomNotificationStateStore {
if (!this._RoomNotificationStateStore) {
this._RoomNotificationStateStore = RoomNotificationStateStore.instance;
}
return this._RoomNotificationStateStore;
}
public get roomViewStore(): RoomViewStore {
if (!this._RoomViewStore) {
this._RoomViewStore = new RoomViewStore(
defaultDispatcher, this,
);
}
return this._RoomViewStore;
}
public get widgetLayoutStore(): WidgetLayoutStore {
if (!this._WidgetLayoutStore) {
this._WidgetLayoutStore = WidgetLayoutStore.instance;
}
return this._WidgetLayoutStore;
}
public get widgetPermissionStore(): WidgetPermissionStore {
if (!this._WidgetPermissionStore) {
this._WidgetPermissionStore = new WidgetPermissionStore(this);
}
return this._WidgetPermissionStore;
}
public get widgetStore(): WidgetStore {
if (!this._WidgetStore) {
this._WidgetStore = WidgetStore.instance;
}
return this._WidgetStore;
}
public get posthogAnalytics(): PosthogAnalytics {
if (!this._PosthogAnalytics) {
this._PosthogAnalytics = PosthogAnalytics.instance;
}
return this._PosthogAnalytics;
}
public get slidingSyncManager(): SlidingSyncManager {
if (!this._SlidingSyncManager) {
this._SlidingSyncManager = SlidingSyncManager.instance;
}
return this._SlidingSyncManager;
}
public get spaceStore(): SpaceStoreClass {
if (!this._SpaceStore) {
this._SpaceStore = SpaceStore.instance;
}
return this._SpaceStore;
}
public get typingStore(): TypingStore {
if (!this._TypingStore) {
this._TypingStore = new TypingStore(this);
window.mxTypingStore = this._TypingStore;
}
return this._TypingStore;
}
}

View file

@ -2163,5 +2163,15 @@
"You cannot place calls in this browser.": "Не можете да провеждате обаждания в този браузър.",
"Calls are unsupported": "Обажданията не се поддържат",
"The user you called is busy.": "Потребителят, когото потърсихте, е зает.",
"User Busy": "Потребителят е зает"
"User Busy": "Потребителят е зает",
"Some invites couldn't be sent": "Някои покани не можаха да бъдат изпратени",
"We sent the others, but the below people couldn't be invited to <RoomName/>": "Изпратихме останалите покани, но следните хора не можаха да бъдат поканени в <RoomName/>",
"Empty room (was %(oldName)s)": "Празна стая (беше %(oldName)s)",
"Inviting %(user)s and %(count)s others|one": "Канене на %(user)s и още 1 друг",
"Inviting %(user)s and %(count)s others|other": "Канене на %(user)s и %(count)s други",
"Inviting %(user1)s and %(user2)s": "Канене на %(user1)s и %(user2)s",
"%(user)s and %(count)s others|one": "%(user)s и още 1",
"%(user)s and %(count)s others|other": "%(user)s и %(count)s други",
"%(user1)s and %(user2)s": "%(user1)s и %(user2)s",
"Empty room": "Празна стая"
}

View file

@ -538,7 +538,7 @@
"Upgrade this room to version %(version)s": "Aktualizace místnosti na verzi %(version)s",
"Security & Privacy": "Zabezpečení a soukromí",
"Encryption": "Šifrování",
"Once enabled, encryption cannot be disabled.": "Jakmile je šifrování povoleno, nelze jej zakázat.",
"Once enabled, encryption cannot be disabled.": "Po zapnutí šifrování ho není možné vypnout.",
"Encrypted": "Šifrováno",
"General": "Obecné",
"General failure": "Nějaká chyba",
@ -941,7 +941,7 @@
"You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Můžete si změnit heslo, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se toto varování zobrazuje i nadále, zkontrolujte svojí konfiguraci nebo kontaktujte správce serveru.",
"You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Můžete se přihlásit, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se vám toto varování zobrazuje i nadále, zkontrolujte svojí konfiguraci nebo kontaktujte správce serveru.",
"Call failed due to misconfigured server": "Volání selhalo, protože je rozbitá konfigurace serveru",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Požádejte správce svého homeserveru (<code>%(homeserverDomain)s</code>) jestli by nemohl nakonfigurovat TURN server, aby volání fungovala spolehlivě.",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Požádejte správce svého domovského serveru (<code>%(homeserverDomain)s</code>) jestli by nemohl nakonfigurovat TURN server, aby volání fungovala spolehlivě.",
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Můžete také zkusit použít veřejný server na adrese <code>turn.matrix.org</code>, ale ten nebude tak spolehlivý a bude sdílet vaši IP adresu s tímto serverem. To můžete spravovat také v Nastavení.",
"Try using turn.matrix.org": "Zkuste použít turn.matrix.org",
"Messages": "Zprávy",
@ -1262,7 +1262,7 @@
"about a day from now": "asi za den",
"%(num)s days from now": "za %(num)s dní",
"Show info about bridges in room settings": "Zobrazovat v nastavení místnosti informace o propojeních",
"Never send encrypted messages to unverified sessions from this session": "Nikdy neposílat šifrované zprávy neověřených zařízením",
"Never send encrypted messages to unverified sessions from this session": "Nikdy neposílat šifrované zprávy do neověřených relací z této relace",
"Never send encrypted messages to unverified sessions in this room from this session": "Nikdy v této místnosti neposílat šifrované zprávy neověřeným relacím",
"Enable message search in encrypted rooms": "Povolit vyhledávání v šifrovaných místnostech",
"How fast should messages be downloaded.": "Jak rychle se mají zprávy stahovat.",
@ -1441,7 +1441,7 @@
"Manually Verify by Text": "Manuální textové ověření",
"Interactively verify by Emoji": "Interaktivní ověření s emotikonami",
"Support adding custom themes": "Umožnit přidání vlastního vzhledu",
"Manually verify all remote sessions": "Manuálně ověřit všechny relace",
"Manually verify all remote sessions": "Ručně ověřit všechny relace",
"cached locally": "uložen lokálně",
"not found locally": "nenalezen lolálně",
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individuálně ověřit každou uživatelovu relaci a označit jí za důvěryhodnou, bez důvěry v křížový podpis.",
@ -3582,12 +3582,87 @@
"Failed to set pusher state": "Nepodařilo se nastavit stav push oznámení",
"%(selectedDeviceCount)s sessions selected": "%(selectedDeviceCount)s vybraných relací",
"Receive push notifications on this session.": "Přijímat push oznámení v této relaci.",
"Toggle push notifications on this session.": "Přepnout push notifikace v této relaci.",
"Push notifications": "Push notifikace",
"Toggle push notifications on this session.": "Přepnout push oznámení v této relaci.",
"Push notifications": "Push oznámení",
"Enable notifications for this device": "Povolit oznámení pro toto zařízení",
"Turn off to disable notifications on all your devices and sessions": "Vypnutím zakážete oznámení na všech zařízeních a relacích",
"Enable notifications for this account": "Povolit oznámení pro tento účet",
"Video call ended": "Videohovor ukončen",
"%(name)s started a video call": "%(name)s zahájil(a) videohovor",
"Record the client name, version, and url to recognise sessions more easily in session manager": "Zaznamenat název, verzi a url pro snadnější rozpoznání relací ve správci relací"
"Record the client name, version, and url to recognise sessions more easily in session manager": "Zaznamenat název, verzi a url pro snadnější rozpoznání relací ve správci relací",
"URL": "URL",
"Version": "Verze",
"Application": "Aplikace",
"Room info": "Informace o místnosti",
"View chat timeline": "Zobrazit časovou osu konverzace",
"Close call": "Zavřít hovor",
"Freedom": "Svoboda",
"Layout type": "Typ rozložení",
"Spotlight": "Reflektor",
"Unknown session type": "Neznámý typ relace",
"Web session": "Relace na webu",
"Mobile session": "Relace mobilního zařízení",
"Desktop session": "Relace stolního počítače",
"Fill screen": "Vyplnit obrazovku",
"Video call started": "Videohovor byl zahájen",
"Unknown room": "Neznámá místnost",
"Video call started in %(roomName)s. (not supported by this browser)": "Videohovor byl zahájen v %(roomName)s. (není podporováno tímto prohlížečem)",
"Video call started in %(roomName)s.": "Videohovor byl zahájen v %(roomName)s.",
"Operating system": "Operační systém",
"Model": "Model",
"Client": "Klient",
"Video call (%(brand)s)": "Videohovor (%(brand)s)",
"Call type": "Typ volání",
"You do not have sufficient permissions to change this.": "Ke změně nemáte dostatečná oprávnění.",
"%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.": "%(brand)s je koncově šifrovaný, ale v současné době je omezen na menší počet uživatelů.",
"Enable %(brand)s as an additional calling option in this room": "Povolit %(brand)s jako další možnost volání v této místnosti",
"Join %(brand)s calls": "Připojit se k %(brand)s volání",
"Start %(brand)s calls": "Zahájit %(brand)s volání",
"Sorry — this call is currently full": "Omlouváme se — tento hovor je v současné době plný",
"Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Náš nový správce relací poskytuje lepší přehled o všech relacích a lepší kontrolu nad nimi, včetně možnosti vzdáleně přepínat push oznámení.",
"Have greater visibility and control over all your sessions.": "Získejte větší přehled a kontrolu nad všemi relacemi.",
"New session manager": "Nový správce relací",
"Use new session manager": "Použít nový správce relací",
"Wysiwyg composer (plain text mode coming soon) (under active development)": "Wysiwyg editor (textový režim již brzy) (v aktivním vývoji)",
"Sign out all other sessions": "Odhlásit všechny ostatní relace",
"resume voice broadcast": "obnovit hlasové vysílání",
"pause voice broadcast": "pozastavit hlasové vysílání",
"Underline": "Podtržení",
"Italic": "Kurzíva",
"Try out the rich text editor (plain text mode coming soon)": "Vyzkoušejte nový editor (textový režim již brzy)",
"You have already joined this call from another device": "K tomuto hovoru jste se již připojili z jiného zařízení",
"stop voice broadcast": "zastavit hlasové vysílání",
"Notifications silenced": "Oznámení ztlumena",
"Yes, stop broadcast": "Ano, zastavit vysílání",
"Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Opravdu chcete ukončit živé vysílání? Tím se vysílání ukončí a v místnosti bude k dispozici celý záznam.",
"Stop live broadcasting?": "Ukončit živé vysílání?",
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Hlasové vysílání už nahrává někdo jiný. Počkejte, až jeho hlasové vysílání skončí, a spusťte nové.",
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Nemáte potřebná oprávnění ke spuštění hlasového vysílání v této místnosti. Obraťte se na správce místnosti, aby vám zvýšil oprávnění.",
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Již nahráváte hlasové vysílání. Ukončete prosím aktuální hlasové vysílání a spusťte nové.",
"Can't start a new voice broadcast": "Nelze spustit nové hlasové vysílání",
"Completing set up of your new device": "Dokončování nastavení nového zařízení",
"Waiting for device to sign in": "Čekání na přihlášení zařízení",
"Connecting...": "Připojování...",
"Review and approve the sign in": "Zkontrolovat a schválit přihlášení",
"Select 'Scan QR code'": "Vyberte \"Naskenovat QR kód\"",
"Start at the sign in screen": "Začněte na přihlašovací obrazovce",
"Scan the QR code below with your device that's signed out.": "Níže uvedený QR kód naskenujte pomocí přihlašovaného zařízení.",
"By approving access for this device, it will have full access to your account.": "Schválením přístupu tohoto zařízení získá zařízení plný přístup k vašemu účtu.",
"Check that the code below matches with your other device:": "Zkontrolujte, zda se níže uvedený kód shoduje s vaším dalším zařízením:",
"Devices connected": "Zařízení byla propojena",
"The homeserver doesn't support signing in another device.": "Domovský server nepodporuje přihlášení pomocí jiného zařízení.",
"An unexpected error occurred.": "Došlo k neočekávané chybě.",
"The request was cancelled.": "Požadavek byl zrušen.",
"The other device isn't signed in.": "Druhé zařízení není přihlášeno.",
"The other device is already signed in.": "Druhé zařízení je již přihlášeno.",
"The request was declined on the other device.": "Požadavek byl na druhém zařízení odmítnut.",
"Linking with this device is not supported.": "Propojení s tímto zařízením není podporováno.",
"The scanned code is invalid.": "Naskenovaný kód je neplatný.",
"The linking wasn't completed in the required time.": "Propojení nebylo dokončeno v požadovaném čase.",
"Sign in new device": "Přihlásit nové zařízení",
"Show QR code": "Zobrazit QR kód",
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Toto zařízení můžete použít k přihlášení nového zařízení pomocí QR kódu. QR kód zobrazený na tomto zařízení musíte naskenovat pomocí odhlášeného zařízení.",
"Sign in with QR code": "Přihlásit se pomocí QR kódu",
"Browser": "Prohlížeč",
"Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Povolit zobrazení QR kódu ve správci relací pro přihlášení do jiného zařízení (vyžaduje kompatibilní domovský server)"
}

File diff suppressed because it is too large Load diff

View file

@ -637,10 +637,17 @@
"Send <b>%(msgtype)s</b> messages as you in your active room": "Send <b>%(msgtype)s</b> messages as you in your active room",
"See <b>%(msgtype)s</b> messages posted to this room": "See <b>%(msgtype)s</b> messages posted to this room",
"See <b>%(msgtype)s</b> messages posted to your active room": "See <b>%(msgtype)s</b> messages posted to your active room",
"Live": "Live",
"pause voice broadcast": "pause voice broadcast",
"Can't start a new voice broadcast": "Can't start a new voice broadcast",
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.",
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.",
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.",
"Stop live broadcasting?": "Stop live broadcasting?",
"Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.",
"Yes, stop broadcast": "Yes, stop broadcast",
"play voice broadcast": "play voice broadcast",
"resume voice broadcast": "resume voice broadcast",
"stop voice broadcast": "stop voice broadcast",
"pause voice broadcast": "pause voice broadcast",
"Live": "Live",
"Voice broadcast": "Voice broadcast",
"Cannot reach homeserver": "Cannot reach homeserver",
"Ensure you have a stable internet connection, or get in touch with the server admin": "Ensure you have a stable internet connection, or get in touch with the server admin",
@ -756,6 +763,7 @@
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Are you sure you want to exit during this export?": "Are you sure you want to exit during this export?",
"Unnamed Room": "Unnamed Room",
"Generating a ZIP": "Generating a ZIP",
"Fetched %(count)s events out of %(total)s|other": "Fetched %(count)s events out of %(total)s",
"Fetched %(count)s events out of %(total)s|one": "Fetched %(count)s event out of %(total)s",
@ -931,6 +939,7 @@
"New session manager": "New session manager",
"Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.",
"Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.",
"Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)",
"Font size": "Font size",
"Use custom size": "Use custom size",
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
@ -1738,7 +1747,6 @@
"Rename session": "Rename session",
"Please be aware that session names are also visible to people you communicate with": "Please be aware that session names are also visible to people you communicate with",
"Session ID": "Session ID",
"Client": "Client",
"Last activity": "Last activity",
"Application": "Application",
"Version": "Version",
@ -1746,6 +1754,7 @@
"Device": "Device",
"Model": "Model",
"Operating system": "Operating system",
"Browser": "Browser",
"IP address": "IP address",
"Session details": "Session details",
"Toggle push notifications on this session.": "Toggle push notifications on this session.",
@ -1784,6 +1793,9 @@
"Filter devices": "Filter devices",
"Show": "Show",
"%(selectedDeviceCount)s sessions selected": "%(selectedDeviceCount)s sessions selected",
"Sign in with QR code": "Sign in with QR code",
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.",
"Show QR code": "Show QR code",
"Security recommendations": "Security recommendations",
"Improve your account security by following these recommendations": "Improve your account security by following these recommendations",
"View all": "View all",
@ -2768,7 +2780,6 @@
"Or send invite link": "Or send invite link",
"Unnamed Space": "Unnamed Space",
"Invite to %(roomName)s": "Invite to %(roomName)s",
"Unnamed Room": "Unnamed Room",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
"Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.",
@ -3178,6 +3189,26 @@
"Submit": "Submit",
"Something went wrong in confirming your identity. Cancel and try again.": "Something went wrong in confirming your identity. Cancel and try again.",
"Start authentication": "Start authentication",
"Sign in new device": "Sign in new device",
"The linking wasn't completed in the required time.": "The linking wasn't completed in the required time.",
"The scanned code is invalid.": "The scanned code is invalid.",
"Linking with this device is not supported.": "Linking with this device is not supported.",
"The request was declined on the other device.": "The request was declined on the other device.",
"The other device is already signed in.": "The other device is already signed in.",
"The other device isn't signed in.": "The other device isn't signed in.",
"The request was cancelled.": "The request was cancelled.",
"An unexpected error occurred.": "An unexpected error occurred.",
"The homeserver doesn't support signing in another device.": "The homeserver doesn't support signing in another device.",
"Devices connected": "Devices connected",
"Check that the code below matches with your other device:": "Check that the code below matches with your other device:",
"By approving access for this device, it will have full access to your account.": "By approving access for this device, it will have full access to your account.",
"Scan the QR code below with your device that's signed out.": "Scan the QR code below with your device that's signed out.",
"Start at the sign in screen": "Start at the sign in screen",
"Select 'Scan QR code'": "Select 'Scan QR code'",
"Review and approve the sign in": "Review and approve the sign in",
"Connecting...": "Connecting...",
"Waiting for device to sign in": "Waiting for device to sign in",
"Completing set up of your new device": "Completing set up of your new device",
"Enter password": "Enter password",
"Nice, strong password!": "Nice, strong password!",
"Password is allowed, but unsafe": "Password is allowed, but unsafe",

View file

@ -3549,5 +3549,83 @@
"Empty room (was %(oldName)s)": "Sala vacía (antes era %(oldName)s)",
"%(user)s and %(count)s others|one": "%(user)s y 1 más",
"%(user)s and %(count)s others|other": "%(user)s y %(count)s más",
"%(user1)s and %(user2)s": "%(user1)s y %(user2)s"
"%(user1)s and %(user2)s": "%(user1)s y %(user2)s",
"Spotlight": "Spotlight",
"Your server lacks native support, you must specify a proxy": "Tu servidor no es compatible, debes configurar un intermediario (proxy)",
"View chat timeline": "Ver historial del chat",
"You do not have permission to start voice calls": "No tienes permiso para iniciar llamadas de voz",
"Failed to set pusher state": "Fallo al establecer el estado push",
"Sign out of this session": "Cerrar esta sesión",
"Receive push notifications on this session.": "Recibir notificaciones push en esta sesión.",
"Please be aware that session names are also visible to people you communicate with": "Ten en cuenta que cualquiera con quien te comuniques puede ver los nombres de las sesiones",
"Sign out all other sessions": "Cerrar el resto de sesiones",
"You do not have sufficient permissions to change this.": "No tienes suficientes permisos para cambiar esto.",
"%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.": "%(brand)s está cifrado de extremo a extremo, pero actualmente está limitado a unos pocos participantes.",
"Enable %(brand)s as an additional calling option in this room": "Activar %(brand)s como una opción para las llamadas de esta sala",
"Enable notifications for this device": "Activar notificaciones en este dispositivo",
"Turn off to disable notifications on all your devices and sessions": "Desactiva para no recibir notificaciones en todos tus dispositivos y sesiones",
"You need to be able to kick users to do that.": "Debes poder sacar usuarios para hacer eso.",
"Video call started in %(roomName)s. (not supported by this browser)": "Videollamada empezada en %(roomName)s. (no compatible con este navegador)",
"Video call started in %(roomName)s.": "Videollamada empezada en %(roomName)s.",
"Layout type": "Tipo de disposición",
"%(downloadButton)s or %(copyButton)s": "%(downloadButton)s o %(copyButton)s",
"%(securityKey)s or %(recoveryFile)s": "%(securityKey)s o %(recoveryFile)s",
"Proxy URL": "URL de servidor proxy",
"Proxy URL (optional)": "URL de servidor proxy (opcional)",
"To disable you will need to log out and back in, use with caution!": "Para desactivarlo, tendrás que cerrar sesión y volverla a iniciar. ¡Ten cuidado!",
"Sliding Sync configuration": "Configuración de la sincronización progresiva",
"Your server lacks native support": "Tu servidor no es compatible",
"Your server has native support": "Tu servidor es compatible",
"Checking...": "Comprobando…",
"%(qrCode)s or %(appLinks)s": "%(qrCode)s o %(appLinks)s",
"Video call ended": "Videollamada terminada",
"%(name)s started a video call": "%(name)s comenzó una videollamada",
"%(qrCode)s or %(emojiCompare)s": "%(qrCode)s o %(emojiCompare)s",
"Room info": "Info. de la sala",
"Underline": "Subrayado",
"Italic": "Cursiva",
"Close call": "Terminar llamada",
"Freedom": "Libertad",
"There's no one here to call": "No hay nadie a quien llamar aquí",
"You do not have permission to start video calls": "No tienes permiso para empezar videollamadas",
"Ongoing call": "Llamada en curso",
"Video call (%(brand)s)": "Videollamada (%(brand)s)",
"Video call (Jitsi)": "Videollamada (Jitsi)",
"%(selectedDeviceCount)s sessions selected": "%(selectedDeviceCount)s sesiones seleccionadas",
"Unknown session type": "Sesión de tipo desconocido",
"Web session": "Sesión web",
"Mobile session": "Sesión móvil",
"Desktop session": "Sesión de escritorio",
"Toggle push notifications on this session.": "Activar/desactivar notificaciones push en esta sesión.",
"Push notifications": "Notificaciones push",
"Operating system": "Sistema operativo",
"Model": "Modelo",
"URL": "URL",
"Version": "Versión",
"Application": "Aplicación",
"Client": "Cliente",
"Rename session": "Renombrar sesión",
"Call type": "Tipo de llamada",
"Join %(brand)s calls": "Unirte a llamadas de %(brand)s",
"Start %(brand)s calls": "Empezar llamadas de %(brand)s",
"Voice broadcasts": "Retransmisiones de voz",
"Enable notifications for this account": "Activar notificaciones para esta cuenta",
"Fill screen": "Llenar la pantalla",
"You have already joined this call from another device": "Ya te has unido a la llamada desde otro dispositivo",
"Sorry — this call is currently full": "Lo sentimos — la llamada está llena",
"Use new session manager": "Usar el nuevo gestor de sesiones",
"New session manager": "Nuevo gestor de sesiones",
"Voice broadcast (under active development)": "Retransmisión de voz (en desarrollo)",
"New group call experience": "Nueva experiencia de llamadas grupales",
"Element Call video rooms": "Salas de vídeo Element Call",
"Sliding Sync mode (under active development, cannot be disabled)": "Modo de sincronización progresiva (en pleno desarrollo, no puede desactivarse)",
"Try out the rich text editor (plain text mode coming soon)": "Prueba el nuevo editor de texto con formato (un modo sin formato estará disponible próximamente)",
"Notifications silenced": "Notificaciones silenciadas",
"Video call started": "Videollamada iniciada",
"Unknown room": "Sala desconocida",
"Voice broadcast": "Retransmisión de voz",
"stop voice broadcast": "parar retransmisión de voz",
"resume voice broadcast": "reanudar retransmisión de voz",
"pause voice broadcast": "pausar retransmisión de voz",
"Live": "En directo"
}

View file

@ -3587,5 +3587,81 @@
"Push notifications": "Tõuketeavitused",
"Toggle push notifications on this session.": "Lülita tõuketeavitused selles sessioonis sisse/välja.",
"Enable notifications for this device": "Võta teavitused selles seadmes kasutusele",
"Turn off to disable notifications on all your devices and sessions": "Välja lülitades keelad teavitused kõikides oma seadmetes ja sessioonides"
"Turn off to disable notifications on all your devices and sessions": "Välja lülitades keelad teavitused kõikides oma seadmetes ja sessioonides",
"Room info": "Jututoa teave",
"View chat timeline": "Vaata vestluse ajajoont",
"Close call": "Lõpeta kõne",
"Layout type": "Kujunduse tüüp",
"Spotlight": "Rambivalgus",
"Freedom": "Vabadus",
"Unknown session type": "Tundmatu sessioonitüüp",
"Web session": "Veebirakendus",
"Mobile session": "Nutirakendus",
"Desktop session": "Töölauarakendus",
"URL": "URL",
"Version": "Versioon",
"Application": "Rakendus",
"Fill screen": "Täida ekraan",
"Record the client name, version, and url to recognise sessions more easily in session manager": "Sessioonide paremaks tuvastamiseks saad nüüd sessioonihalduris salvestada klientrakenduse nime, versiooni ja aadressi",
"Video call started": "Videokõne algas",
"Unknown room": "Teadmata jututuba",
"Live": "Otseeeter",
"Video call started in %(roomName)s. (not supported by this browser)": "Videokõne algas %(roomName)s jututoas. (ei ole selles brauseris toetatud)",
"Video call started in %(roomName)s.": "Videokõne algas %(roomName)s jututoas.",
"Video call (%(brand)s)": "Videokõne (%(brand)s)",
"Operating system": "Operatsioonisüsteem",
"Model": "Mudel",
"Client": "Klient",
"Call type": "Kõne tüüp",
"You do not have sufficient permissions to change this.": "Sul pole piisavalt õigusi selle muutmiseks.",
"%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.": "%(brand)s kasutab läbivat krüptimist, kuid on hetkel piiratud väikese osalejate arvuga ühes kõnes.",
"Enable %(brand)s as an additional calling option in this room": "Võta kasutusele %(brand)s kui lisavõimalus kõnedeks selles jututoas",
"Join %(brand)s calls": "Liitu %(brand)s kõnedega",
"Start %(brand)s calls": "Alusta helistamist %(brand)s abil",
"Sorry — this call is currently full": "Vabandust, selles kõnes ei saa rohkem osalejaid olla",
"stop voice broadcast": "lõpeta ringhäälingukõne",
"resume voice broadcast": "jätka ringhäälingukõnet",
"pause voice broadcast": "peata ringhäälingukõne",
"Underline": "Allajoonitud tekst",
"Italic": "Kaldkiri",
"Sign out all other sessions": "Logi välja kõikidest oma muudest sessioonidest",
"You have already joined this call from another device": "Sa oled selle kõnega juba ühest teisest seadmest liitunud",
"Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Uues sessioonihalduris saad parema ülevaate kõikidest oma sessioonidest ning rohkem võimalusi neid hallata, sealhulgas tõuketeavituste sisse- ja väljalülitamine.",
"Have greater visibility and control over all your sessions.": "Sellega saad parema ülevaate oma sessioonidest ja võimaluse neid mugavasti hallata.",
"New session manager": "Uus sessioonihaldur",
"Use new session manager": "Kasuta uut sessioonihaldurit",
"Try out the rich text editor (plain text mode coming soon)": "Proovi vormindatud teksti alusel töötavat tekstitoimetit (varsti lisandub ka vormindamata teksti režiim)",
"Notifications silenced": "Teavitused on summutatud",
"Completing set up of your new device": "Lõpetame uue seadme seadistamise",
"Waiting for device to sign in": "Ootame, et teine seade logiks võrku",
"Connecting...": "Ühendamisel…",
"Review and approve the sign in": "Vaata üle ja kinnita sisselogimine Matrixi'i võrku",
"Select 'Scan QR code'": "Vali „Loe QR-koodi“",
"Start at the sign in screen": "Alusta sisselogimisvaatest",
"Scan the QR code below with your device that's signed out.": "Loe QR-koodi seadmega, kus sa oled Matrix'i võrgust välja loginud.",
"By approving access for this device, it will have full access to your account.": "Lubades ligipääsu sellele seadmele, annad talle ka täismahulise ligipääsu oma kasutajakontole.",
"Check that the code below matches with your other device:": "Kontrolli, et järgnev kood klapib teises seadmes kuvatava koodiga:",
"Devices connected": "Seadmed on ühendatud",
"The homeserver doesn't support signing in another device.": "Koduserver ei toeta muude seadmete võrku logimise võimalust.",
"An unexpected error occurred.": "Tekkis teadmata viga.",
"The request was cancelled.": "Päring katkestati.",
"The other device isn't signed in.": "Teine seade ei ole võrku loginud.",
"The other device is already signed in.": "Teine seade on juba võrku loginud.",
"The request was declined on the other device.": "Teine seade lükkas päringu tagasi.",
"Linking with this device is not supported.": "Sidumine selle seadmega ei ole toetatud.",
"The scanned code is invalid.": "Skaneeritud QR-kood on vigane.",
"The linking wasn't completed in the required time.": "Sidumine ei lõppenud etteantud aja jooksul.",
"Sign in new device": "Logi sisse uus seade",
"Show QR code": "Näita QR-koodi",
"Sign in with QR code": "Logi sisse QR-koodi abil",
"Browser": "Brauser",
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Sa saad kasutada seda seadet mõne muu seadme logimiseks Matrix'i võrku QR-koodi alusel. Selleks skaneeri võrgust väljalogitud seadmega seda QR-koodi.",
"Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Teise seadme sisselogimiseks luba QR-koodi kuvamine sessioonihalduris (eeldab, et koduserver sellist võimalust toetab)",
"Yes, stop broadcast": "Jah, lõpeta",
"Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Kas sa oled kindel, et soovid otseeetri lõpetada? Sellega ringhäälingukõne salvestamine lõppeb ja salvestis on kättesaadav kõigile jututoas.",
"Stop live broadcasting?": "Kas lõpetame otseeetri?",
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Keegi juba salvestab ringhäälingukõnet. Uue ringhäälingukõne salvestamiseks palun oota, kuni see teine ringhäälingukõne on lõppenud.",
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Sul pole piisavalt õigusi selles jututoas ringhäälingukõne algatamiseks. Õiguste lisamiseks palun võta ühendust jututoa haldajaga.",
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Sa juba salvestad ringhäälingukõnet. Uue alustamiseks palun lõpeta eelmine salvestus.",
"Can't start a new voice broadcast": "Uue ringhäälingukõne alustamine pole võimalik"
}

View file

@ -2502,5 +2502,12 @@
"Jump to the given date in the timeline": "پرش به تاریخ تعیین شده در جدول زمانی",
"Failed to invite users to %(roomName)s": "افزودن کاربران به %(roomName)s با شکست روبرو شد",
"You're trying to access a community link (%(groupId)s).<br/>Communities are no longer supported and have been replaced by spaces.<br2/><a>Learn more about spaces here.</a>": "شما قصد دسترسی به لینک انجمن%(groupId)s را دارید. <br/> انجمن ها دیگر پشتیبانی نمی شوند و با فضاها جایگزین شده اند.<br2/><a> در مورد فضا بیشتر بدانید</a>",
"That link is no longer supported": "لینک موردنظر دیگر پشتیبانی نمی شود"
"That link is no longer supported": "لینک موردنظر دیگر پشتیبانی نمی شود",
"Inviting %(user)s and %(count)s others|other": "دعوت کردن %(user)s و %(count)s دیگر",
"Video call started in %(roomName)s. (not supported by this browser)": "تماس ویدئویی در %(roomName)s شروع شد. (توسط این مرورگر پشتیبانی نمی‌شود.)",
"Video call started in %(roomName)s.": "تماس ویدئویی در %(roomName)s شروع شد.",
"No virtual room for this room": "اتاق مجازی برای این اتاق وجود ندارد",
"Switches to this room's virtual room, if it has one": "جابجایی به اتاق مجازی این اتاق، اگر یکی وجود داشت",
"You need to be able to kick users to do that.": "برای انجام این کار نیاز دارید که بتوانید کاربران را حذف کنید.",
"Empty room (was %(oldName)s)": "اتاق خالی (نام قبلی: %(oldName)s)"
}

View file

@ -3589,5 +3589,47 @@
"Enable notifications for this account": "Activer les notifications pour ce compte",
"Video call ended": "Appel vidéo terminé",
"%(name)s started a video call": "%(name)s a démarré un appel vidéo",
"Record the client name, version, and url to recognise sessions more easily in session manager": "Enregistrez le nom, la version et l'URL du client afin de reconnaitre les sessions plus facilement dans le gestionnaire de sessions"
"Record the client name, version, and url to recognise sessions more easily in session manager": "Enregistrez le nom, la version et l'URL du client afin de reconnaitre les sessions plus facilement dans le gestionnaire de sessions",
"Version": "Version",
"Application": "Application",
"URL": "URL",
"Unknown session type": "Type de session inconnu",
"Web session": "session internet",
"Mobile session": "Session de téléphone portable",
"Desktop session": "Session de bureau",
"Video call started": "Appel vidéo commencé",
"Unknown room": "Salon inconnu",
"Video call started in %(roomName)s. (not supported by this browser)": "Appel vidéo commencé dans %(roomName)s. (non supporté par ce navigateur)",
"Video call started in %(roomName)s.": "Appel vidéo commencé dans %(roomName)s.",
"Close call": "Terminer lappel",
"Layout type": "Type de mise en page",
"Spotlight": "Projecteur",
"Freedom": "Liberté",
"Fill screen": "Remplir lécran",
"Room info": "Information du salon",
"View chat timeline": "Afficher la chronologie du chat",
"Video call (%(brand)s)": "Appel vidéo (%(brand)s)",
"Operating system": "Système dexploitation",
"Model": "Modèle",
"Client": "Client",
"Call type": "Type dappel",
"You do not have sufficient permissions to change this.": "Vous navez pas assez de permissions pour changer ceci.",
"%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.": "%(brand)s est chiffré de bout en bout, mais nest actuellement utilisable quavec un petit nombre dutilisateurs.",
"Enable %(brand)s as an additional calling option in this room": "Activer %(brand)s comme une option supplémentaire dappel dans ce salon",
"Join %(brand)s calls": "Rejoindre des appels %(brand)s",
"Start %(brand)s calls": "Démarrer des appels %(brand)s",
"Sorry — this call is currently full": "Désolé — Cet appel est actuellement complet",
"Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Notre nouveau gestionnaire de sessions fournit une meilleure visibilité sur toutes vos sessions, et un plus grand contrôle sur ces dernières avec la possibilité de désactiver à distance les notifications push.",
"Have greater visibility and control over all your sessions.": "Ayez une meilleur visibilité et plus de contrôle sur toutes vos sessions.",
"New session manager": "Nouveau gestionnaire de sessions",
"Use new session manager": "Utiliser le nouveau gestionnaire de session",
"Wysiwyg composer (plain text mode coming soon) (under active development)": "Compositeur Wysiwyg (le mode texte brut arrive prochainement) (en cours de développement)",
"Sign out all other sessions": "Déconnecter toutes les autres sessions",
"resume voice broadcast": "continuer la diffusion audio",
"pause voice broadcast": "mettre en pause la diffusion audio",
"Underline": "Souligné",
"Italic": "Italique",
"You have already joined this call from another device": "Vous avez déjà rejoint cet appel depuis un autre appareil",
"Try out the rich text editor (plain text mode coming soon)": "Essayer léditeur de texte formaté (le mode texte brut arrive bientôt)",
"stop voice broadcast": "arrêter la diffusion audio"
}

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