Merge remote-tracking branch 'origin/develop' into feat/emoji-picker-rich-text-mode
This commit is contained in:
commit
54e12d265b
139 changed files with 2830 additions and 3202 deletions
7
.github/workflows/cypress.yaml
vendored
7
.github/workflows/cypress.yaml
vendored
|
@ -43,7 +43,7 @@ jobs:
|
||||||
- name: Get commit details
|
- name: Get commit details
|
||||||
id: commit
|
id: commit
|
||||||
if: github.event.workflow_run.event == 'pull_request'
|
if: github.event.workflow_run.event == 'pull_request'
|
||||||
uses: actions/github-script@v5
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const response = await github.rest.git.getCommit({
|
const response = await github.rest.git.getCommit({
|
||||||
|
@ -82,7 +82,7 @@ jobs:
|
||||||
# Run 4 instances in Parallel
|
# Run 4 instances in Parallel
|
||||||
runner: [1, 2, 3, 4]
|
runner: [1, 2, 3, 4]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
# XXX: We're checking out untrusted code in a secure context
|
# XXX: We're checking out untrusted code in a secure context
|
||||||
# We need to be careful to not trust anything this code outputs/may do
|
# We need to be careful to not trust anything this code outputs/may do
|
||||||
|
@ -96,7 +96,6 @@ jobs:
|
||||||
- name: 📥 Download artifact
|
- name: 📥 Download artifact
|
||||||
uses: dawidd6/action-download-artifact@v2
|
uses: dawidd6/action-download-artifact@v2
|
||||||
with:
|
with:
|
||||||
workflow: element-build-and-test.yaml
|
|
||||||
run_id: ${{ github.event.workflow_run.id }}
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
name: previewbuild
|
name: previewbuild
|
||||||
path: webapp
|
path: webapp
|
||||||
|
@ -147,7 +146,7 @@ jobs:
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: cypress-results
|
name: cypress-results
|
||||||
path: |
|
path: |
|
||||||
|
|
4
.github/workflows/element-web.yaml
vendored
4
.github/workflows/element-web.yaml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
||||||
name: "Build Element-Web"
|
name: "Build Element-Web"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -46,7 +46,7 @@ jobs:
|
||||||
working-directory: ./element-web
|
working-directory: ./element-web
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: previewbuild
|
name: previewbuild
|
||||||
path: element-web/webapp
|
path: element-web/webapp
|
||||||
|
|
2
.github/workflows/i18n_check.yml
vendored
2
.github/workflows/i18n_check.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
||||||
- name: "Get modified files"
|
- name: "Get modified files"
|
||||||
id: changed_files
|
id: changed_files
|
||||||
if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot'
|
if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot'
|
||||||
uses: tj-actions/changed-files@v19
|
uses: tj-actions/changed-files@v34
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
src/i18n/strings/*
|
src/i18n/strings/*
|
||||||
|
|
2
.github/workflows/notify-element-web.yml
vendored
2
.github/workflows/notify-element-web.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
||||||
if: github.repository == 'matrix-org/matrix-react-sdk'
|
if: github.repository == 'matrix-org/matrix-react-sdk'
|
||||||
steps:
|
steps:
|
||||||
- name: Notify element-web repo that a new SDK build is on develop
|
- name: Notify element-web repo that a new SDK build is on develop
|
||||||
uses: peter-evans/repository-dispatch@v1
|
uses: peter-evans/repository-dispatch@v2
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
repository: vector-im/element-web
|
repository: vector-im/element-web
|
||||||
|
|
10
.github/workflows/static_analysis.yaml
vendored
10
.github/workflows/static_analysis.yaml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
||||||
name: "Typescript Syntax Check"
|
name: "Typescript Syntax Check"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -89,7 +89,7 @@ jobs:
|
||||||
name: "Rethemendex Check"
|
name: "Rethemendex Check"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- run: ./res/css/rethemendex.sh
|
- run: ./res/css/rethemendex.sh
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ jobs:
|
||||||
name: "ESLint"
|
name: "ESLint"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -116,7 +116,7 @@ jobs:
|
||||||
name: "Style Lint"
|
name: "Style Lint"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -133,7 +133,7 @@ jobs:
|
||||||
name: "Analyse Dead Code"
|
name: "Analyse Dead Code"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
|
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Yarn cache
|
- name: Yarn cache
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
@ -38,7 +38,7 @@ jobs:
|
||||||
run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}"
|
run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}"
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: coverage
|
name: coverage
|
||||||
path: |
|
path: |
|
||||||
|
@ -49,7 +49,7 @@ jobs:
|
||||||
name: Element Web Integration Tests
|
name: Element Web Integration Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -91,6 +91,17 @@ const bobJoin = function(this: CryptoTestContext) {
|
||||||
cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist");
|
cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** configure the given MatrixClient to auto-accept any invites */
|
||||||
|
function autoJoin(client: MatrixClient) {
|
||||||
|
cy.window({ log: false }).then(async win => {
|
||||||
|
client.on(win.matrixcs.RoomMemberEvent.Membership, (event, member) => {
|
||||||
|
if (member.membership === "invite" && member.userId === client.getUserId()) {
|
||||||
|
client.joinRoom(member.roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const handleVerificationRequest = (request: VerificationRequest): Chainable<EmojiMapping[]> => {
|
const handleVerificationRequest = (request: VerificationRequest): Chainable<EmojiMapping[]> => {
|
||||||
return cy.wrap(new Promise<EmojiMapping[]>((resolve) => {
|
return cy.wrap(new Promise<EmojiMapping[]>((resolve) => {
|
||||||
const onShowSas = (event: ISasEvent) => {
|
const onShowSas = (event: ISasEvent) => {
|
||||||
|
@ -174,4 +185,22 @@ describe("Cryptography", function() {
|
||||||
testMessages.call(this);
|
testMessages.call(this);
|
||||||
verify.call(this);
|
verify.call(this);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should allow verification when there is no existing DM", function(this: CryptoTestContext) {
|
||||||
|
cy.bootstrapCrossSigning();
|
||||||
|
autoJoin(this.bob);
|
||||||
|
|
||||||
|
/* we need to have a room with the other user present, so we can open the verification panel */
|
||||||
|
let roomId: string;
|
||||||
|
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }).then(_room1Id => {
|
||||||
|
roomId = _room1Id;
|
||||||
|
cy.log(`Created test room ${roomId}`);
|
||||||
|
cy.visit(`/#/room/${roomId}`);
|
||||||
|
// wait for Bob to join the room, otherwise our attempt to open his user details may race
|
||||||
|
// with his join.
|
||||||
|
cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
verify.call(this);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,6 +21,10 @@ import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||||
describe("Login", () => {
|
describe("Login", () => {
|
||||||
let synapse: SynapseInstance;
|
let synapse: SynapseInstance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.stubDefaultServer();
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopSynapse(synapse);
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,6 +22,7 @@ describe("Registration", () => {
|
||||||
let synapse: SynapseInstance;
|
let synapse: SynapseInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
cy.stubDefaultServer();
|
||||||
cy.visit("/#/register");
|
cy.visit("/#/register");
|
||||||
cy.startSynapse("consent").then(data => {
|
cy.startSynapse("consent").then(data => {
|
||||||
synapse = data;
|
synapse = data;
|
||||||
|
|
48
cypress/fixtures/matrix-org-client-login.json
Normal file
48
cypress/fixtures/matrix-org-client-login.json
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"flows": [
|
||||||
|
{
|
||||||
|
"type": "m.login.sso",
|
||||||
|
"identity_providers": [
|
||||||
|
{
|
||||||
|
"id": "oidc-github",
|
||||||
|
"name": "GitHub",
|
||||||
|
"icon": "mxc://matrix.org/sVesTtrFDTpXRbYfpahuJsKP",
|
||||||
|
"brand": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "oidc-google",
|
||||||
|
"name": "Google",
|
||||||
|
"icon": "mxc://matrix.org/ZlnaaZNPxtUuQemvgQzlOlkz",
|
||||||
|
"brand": "google"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "oidc-gitlab",
|
||||||
|
"name": "GitLab",
|
||||||
|
"icon": "mxc://matrix.org/MCVOEmFgVieKFshPxmnejWOq",
|
||||||
|
"brand": "gitlab"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "oidc-facebook",
|
||||||
|
"name": "Facebook",
|
||||||
|
"icon": "mxc://matrix.org/nsyeLIgzxazZmJadflMAsAWG",
|
||||||
|
"brand": "facebook"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "oidc-apple",
|
||||||
|
"name": "Apple",
|
||||||
|
"icon": "mxc://matrix.org/QQKNSOdLiMHtJhzeAObmkFiU",
|
||||||
|
"brand": "apple"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "m.login.token"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "m.login.password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "m.login.application_service"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
39
cypress/fixtures/matrix-org-client-versions.json
Normal file
39
cypress/fixtures/matrix-org-client-versions.json
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"versions": [
|
||||||
|
"r0.0.1",
|
||||||
|
"r0.1.0",
|
||||||
|
"r0.2.0",
|
||||||
|
"r0.3.0",
|
||||||
|
"r0.4.0",
|
||||||
|
"r0.5.0",
|
||||||
|
"r0.6.0",
|
||||||
|
"r0.6.1",
|
||||||
|
"v1.1",
|
||||||
|
"v1.2",
|
||||||
|
"v1.3",
|
||||||
|
"v1.4"
|
||||||
|
],
|
||||||
|
"unstable_features": {
|
||||||
|
"org.matrix.label_based_filtering": true,
|
||||||
|
"org.matrix.e2e_cross_signing": true,
|
||||||
|
"org.matrix.msc2432": true,
|
||||||
|
"uk.half-shot.msc2666.mutual_rooms": true,
|
||||||
|
"io.element.e2ee_forced.public": false,
|
||||||
|
"io.element.e2ee_forced.private": false,
|
||||||
|
"io.element.e2ee_forced.trusted_private": false,
|
||||||
|
"org.matrix.msc3026.busy_presence": false,
|
||||||
|
"org.matrix.msc2285.stable": true,
|
||||||
|
"org.matrix.msc3827.stable": true,
|
||||||
|
"org.matrix.msc2716": false,
|
||||||
|
"org.matrix.msc3030": false,
|
||||||
|
"org.matrix.msc3440.stable": true,
|
||||||
|
"org.matrix.msc3771": true,
|
||||||
|
"org.matrix.msc3773": false,
|
||||||
|
"fi.mau.msc2815": false,
|
||||||
|
"org.matrix.msc3882": false,
|
||||||
|
"org.matrix.msc3881": false,
|
||||||
|
"org.matrix.msc3874": false,
|
||||||
|
"org.matrix.msc3886": false,
|
||||||
|
"org.matrix.msc3912": false
|
||||||
|
}
|
||||||
|
}
|
8
cypress/fixtures/matrix-org-client-well-known.json
Normal file
8
cypress/fixtures/matrix-org-client-well-known.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"m.homeserver": {
|
||||||
|
"base_url": "https://matrix-client.matrix.org"
|
||||||
|
},
|
||||||
|
"m.identity_server": {
|
||||||
|
"base_url": "https://vector.im"
|
||||||
|
}
|
||||||
|
}
|
1
cypress/fixtures/vector-im-identity-v1.json
Normal file
1
cypress/fixtures/vector-im-identity-v1.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -78,6 +78,7 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
|
||||||
const username = Cypress._.uniqueId("userId_");
|
const username = Cypress._.uniqueId("userId_");
|
||||||
const password = Cypress._.uniqueId("password_");
|
const password = Cypress._.uniqueId("password_");
|
||||||
return cy.registerUser(synapse, username, password, opts.displayName).then(credentials => {
|
return cy.registerUser(synapse, username, password, opts.displayName).then(credentials => {
|
||||||
|
cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`);
|
||||||
return cy.window({ log: false }).then(win => {
|
return cy.window({ log: false }).then(win => {
|
||||||
const cli = new win.matrixcs.MatrixClient({
|
const cli = new win.matrixcs.MatrixClient({
|
||||||
baseUrl: synapse.baseUrl,
|
baseUrl: synapse.baseUrl,
|
||||||
|
|
|
@ -103,6 +103,7 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
|
||||||
return cy.registerUser(synapse, username, password, displayName).then(() => {
|
return cy.registerUser(synapse, username, password, displayName).then(() => {
|
||||||
return cy.loginUser(synapse, username, password);
|
return cy.loginUser(synapse, username, password);
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
|
cy.log(`Registered test user ${username} with displayname ${displayName}`);
|
||||||
cy.window({ log: false }).then(win => {
|
cy.window({ log: false }).then(win => {
|
||||||
// Seed the localStorage with the required credentials
|
// Seed the localStorage with the required credentials
|
||||||
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
|
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
|
||||||
|
|
|
@ -20,10 +20,12 @@ declare global {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
interface Chainable {
|
interface Chainable {
|
||||||
// Intercept all /_matrix/ networking requests for the logged in user and fail them
|
// Intercept all /_matrix/ networking requests for the logged-in user and fail them
|
||||||
goOffline(): void;
|
goOffline(): void;
|
||||||
// Remove intercept on all /_matrix/ networking requests
|
// Remove intercept on all /_matrix/ networking requests
|
||||||
goOnline(): void;
|
goOnline(): void;
|
||||||
|
// Intercept calls to vector.im/matrix.org so a login page can be shown offline
|
||||||
|
stubDefaultServer(): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,5 +60,29 @@ Cypress.Commands.add("goOnline", (): void => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("stubDefaultServer", (): void => {
|
||||||
|
cy.log("Stubbing vector.im and matrix.org network calls");
|
||||||
|
// We intercept vector.im & matrix.org calls so that tests don't fail when it has issues
|
||||||
|
cy.intercept("GET", "https://vector.im/_matrix/identity/api/v1", {
|
||||||
|
fixture: "vector-im-identity-v1.json",
|
||||||
|
});
|
||||||
|
cy.intercept("GET", "https://matrix.org/.well-known/matrix/client", {
|
||||||
|
fixture: "matrix-org-client-well-known.json",
|
||||||
|
});
|
||||||
|
cy.intercept("GET", "https://matrix-client.matrix.org/_matrix/client/versions", {
|
||||||
|
fixture: "matrix-org-client-versions.json",
|
||||||
|
});
|
||||||
|
cy.intercept("GET", "https://matrix-client.matrix.org/_matrix/client/r0/login", {
|
||||||
|
fixture: "matrix-org-client-login.json",
|
||||||
|
});
|
||||||
|
cy.intercept("POST", "https://matrix-client.matrix.org/_matrix/client/r0/register?kind=guest", {
|
||||||
|
statusCode: 403,
|
||||||
|
body: {
|
||||||
|
errcode: "M_FORBIDDEN",
|
||||||
|
error: "Registration is not enabled on this homeserver.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Needed to make this file a module
|
// Needed to make this file a module
|
||||||
export { };
|
export { };
|
||||||
|
|
45
package.json
45
package.json
|
@ -57,10 +57,10 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/analytics-events": "^0.3.0",
|
"@matrix-org/analytics-events": "^0.3.0",
|
||||||
"@matrix-org/matrix-wysiwyg": "^0.6.0",
|
"@matrix-org/matrix-wysiwyg": "^0.8.0",
|
||||||
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
||||||
"@sentry/browser": "^6.11.0",
|
"@sentry/browser": "^7.0.0",
|
||||||
"@sentry/tracing": "^6.11.0",
|
"@sentry/tracing": "^7.0.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
"@types/geojson": "^7946.0.8",
|
"@types/geojson": "^7946.0.8",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
|
@ -72,18 +72,18 @@
|
||||||
"counterpart": "^0.18.6",
|
"counterpart": "^0.18.6",
|
||||||
"diff-dom": "^4.2.2",
|
"diff-dom": "^4.2.2",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
"emojibase": "6.0.2",
|
"emojibase": "6.1.0",
|
||||||
"emojibase-data": "7.0.0",
|
"emojibase-data": "7.0.1",
|
||||||
"emojibase-regex": "6.0.0",
|
"emojibase-regex": "6.0.1",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"filesize": "6.1.0",
|
"filesize": "10.0.5",
|
||||||
"flux": "2.1.1",
|
"flux": "4.0.3",
|
||||||
"focus-visible": "^5.2.0",
|
"focus-visible": "^5.2.0",
|
||||||
"gfm.css": "^1.1.2",
|
"gfm.css": "^1.1.2",
|
||||||
"glob-to-regexp": "^0.4.1",
|
"glob-to-regexp": "^0.4.1",
|
||||||
"highlight.js": "^11.3.1",
|
"highlight.js": "^11.3.1",
|
||||||
"html-entities": "^1.4.0",
|
"html-entities": "^2.0.0",
|
||||||
"is-ip": "^3.1.0",
|
"is-ip": "^3.1.0",
|
||||||
"jszip": "^3.7.0",
|
"jszip": "^3.7.0",
|
||||||
"katex": "^0.12.0",
|
"katex": "^0.12.0",
|
||||||
|
@ -102,7 +102,6 @@
|
||||||
"parse5": "^6.0.1",
|
"parse5": "^6.0.1",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
"posthog-js": "1.12.2",
|
"posthog-js": "1.12.2",
|
||||||
"prop-types": "^15.7.2",
|
|
||||||
"qrcode": "1.4.4",
|
"qrcode": "1.4.4",
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
|
@ -141,7 +140,7 @@
|
||||||
"@peculiar/webcrypto": "^1.4.1",
|
"@peculiar/webcrypto": "^1.4.1",
|
||||||
"@percy/cli": "^1.11.0",
|
"@percy/cli": "^1.11.0",
|
||||||
"@percy/cypress": "^3.1.2",
|
"@percy/cypress": "^3.1.2",
|
||||||
"@sentry/types": "^6.10.0",
|
"@sentry/types": "^7.0.0",
|
||||||
"@sinonjs/fake-timers": "^9.1.2",
|
"@sinonjs/fake-timers": "^9.1.2",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^12.1.5",
|
"@testing-library/react": "^12.1.5",
|
||||||
|
@ -149,7 +148,7 @@
|
||||||
"@types/classnames": "^2.2.11",
|
"@types/classnames": "^2.2.11",
|
||||||
"@types/commonmark": "^0.27.4",
|
"@types/commonmark": "^0.27.4",
|
||||||
"@types/counterpart": "^0.18.1",
|
"@types/counterpart": "^0.18.1",
|
||||||
"@types/css-font-loading-module": "^0.0.6",
|
"@types/css-font-loading-module": "^0.0.7",
|
||||||
"@types/diff-match-patch": "^1.0.32",
|
"@types/diff-match-patch": "^1.0.32",
|
||||||
"@types/enzyme": "^3.10.9",
|
"@types/enzyme": "^3.10.9",
|
||||||
"@types/escape-html": "^1.0.1",
|
"@types/escape-html": "^1.0.1",
|
||||||
|
@ -160,8 +159,8 @@
|
||||||
"@types/katex": "^0.14.0",
|
"@types/katex": "^0.14.0",
|
||||||
"@types/lodash": "^4.14.168",
|
"@types/lodash": "^4.14.168",
|
||||||
"@types/modernizr": "^3.5.3",
|
"@types/modernizr": "^3.5.3",
|
||||||
"@types/node": "^14.18.28",
|
"@types/node": "^16",
|
||||||
"@types/pako": "^1.0.1",
|
"@types/pako": "^2.0.0",
|
||||||
"@types/parse5": "^6.0.0",
|
"@types/parse5": "^6.0.0",
|
||||||
"@types/qrcode": "^1.3.5",
|
"@types/qrcode": "^1.3.5",
|
||||||
"@types/react": "17.0.49",
|
"@types/react": "17.0.49",
|
||||||
|
@ -175,11 +174,11 @@
|
||||||
"@typescript-eslint/parser": "^5.6.0",
|
"@typescript-eslint/parser": "^5.6.0",
|
||||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
||||||
"allchange": "^1.1.0",
|
"allchange": "^1.1.0",
|
||||||
"axe-core": "^4.4.3",
|
"axe-core": "4.4.3",
|
||||||
"babel-jest": "^26.6.3",
|
"babel-jest": "^29.0.0",
|
||||||
"blob-polyfill": "^6.0.20211015",
|
"blob-polyfill": "^7.0.0",
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
"cypress": "^10.3.0",
|
"cypress": "^11.0.0",
|
||||||
"cypress-axe": "^1.0.0",
|
"cypress-axe": "^1.0.0",
|
||||||
"cypress-real-events": "^1.7.1",
|
"cypress-real-events": "^1.7.1",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
|
@ -192,10 +191,10 @@
|
||||||
"eslint-plugin-matrix-org": "^0.7.0",
|
"eslint-plugin-matrix-org": "^0.7.0",
|
||||||
"eslint-plugin-react": "^7.28.0",
|
"eslint-plugin-react": "^7.28.0",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
"eslint-plugin-unicorn": "^44.0.2",
|
"eslint-plugin-unicorn": "^45.0.0",
|
||||||
"fetch-mock-jest": "^1.5.1",
|
"fetch-mock-jest": "^1.5.1",
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^11.0.0",
|
||||||
"glob": "^7.1.6",
|
"glob": "^8.0.0",
|
||||||
"jest": "^29.2.2",
|
"jest": "^29.2.2",
|
||||||
"jest-canvas-mock": "^2.3.0",
|
"jest-canvas-mock": "^2.3.0",
|
||||||
"jest-environment-jsdom": "^29.2.2",
|
"jest-environment-jsdom": "^29.2.2",
|
||||||
|
@ -210,9 +209,9 @@
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rrweb-snapshot": "1.1.7",
|
"rrweb-snapshot": "1.1.7",
|
||||||
"stylelint": "^14.9.1",
|
"stylelint": "^14.9.1",
|
||||||
"stylelint-config-standard": "^26.0.0",
|
"stylelint-config-standard": "^29.0.0",
|
||||||
"stylelint-scss": "^4.2.0",
|
"stylelint-scss": "^4.2.0",
|
||||||
"typescript": "4.8.4",
|
"typescript": "4.9.3",
|
||||||
"walk": "^2.3.14"
|
"walk": "^2.3.14"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
|
|
@ -114,6 +114,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_BetaCard_betaPill {
|
.mx_BetaCard_betaPill {
|
||||||
|
|
|
@ -60,4 +60,8 @@ limitations under the License.
|
||||||
font-family: $monospace-font-family !important;
|
font-family: $monospace-font-family !important;
|
||||||
background-color: $rte-code-bg-color;
|
background-color: $rte-code-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SettingsTab_microcopy_warning::before {
|
||||||
|
content: "⚠️ ";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,9 @@ limitations under the License.
|
||||||
--EventTile_bubble_gap-inline: 5px;
|
--EventTile_bubble_gap-inline: 5px;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: var(--gutterSize);
|
/* Other half of the gutter is provided by margin-bottom on the last tile
|
||||||
|
of the section */
|
||||||
|
margin-top: calc(var(--gutterSize) / 2);
|
||||||
margin-left: var(--EventTile_bubble-margin-inline-start);
|
margin-left: var(--EventTile_bubble-margin-inline-start);
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
|
|
||||||
|
|
|
@ -462,6 +462,11 @@ $left-gutter: 64px;
|
||||||
&.mx_EventTile_continuation {
|
&.mx_EventTile_continuation {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
&.mx_EventTile_lastInSection {
|
||||||
|
/* Other half of the gutter is provided by margin-top on the first
|
||||||
|
tile of the section */
|
||||||
|
margin-bottom: calc(var(--gutterSize) / 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,11 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
> a {
|
> a {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template:
|
||||||
|
"sender" auto
|
||||||
|
"message" auto
|
||||||
|
/ auto;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
transition: color ease 0.15s;
|
transition: color ease 0.15s;
|
||||||
|
@ -58,6 +61,7 @@ limitations under the License.
|
||||||
|
|
||||||
/* We do reply size limiting with CSS to avoid duplicating the TextualBody component. */
|
/* We do reply size limiting with CSS to avoid duplicating the TextualBody component. */
|
||||||
.mx_EventTile_content {
|
.mx_EventTile_content {
|
||||||
|
grid-area: message;
|
||||||
$reply-lines: 2;
|
$reply-lines: 2;
|
||||||
$line-height: $font-18px;
|
$line-height: $font-18px;
|
||||||
|
|
||||||
|
@ -102,7 +106,16 @@ limitations under the License.
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_ReplyTile_inline > a {
|
||||||
|
/* Render replies to emotes inline with the sender avatar */
|
||||||
|
grid-template:
|
||||||
|
"sender message" auto
|
||||||
|
/ max-content auto;
|
||||||
|
gap: 4px; // increase spacing
|
||||||
|
}
|
||||||
|
|
||||||
.mx_ReplyTile_sender {
|
.mx_ReplyTile_sender {
|
||||||
|
grid-area: sender;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
|
@ -40,8 +40,9 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $spacing-4;
|
gap: $spacing-4;
|
||||||
|
|
||||||
i {
|
.mx_Spinner {
|
||||||
flex-shrink: 0;
|
flex: 0 0 14px;
|
||||||
|
padding: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
|
|
4
src/@types/global.d.ts
vendored
4
src/@types/global.d.ts
vendored
|
@ -149,14 +149,10 @@ declare global {
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
|
// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
|
||||||
interface OffscreenCanvas {
|
interface OffscreenCanvas {
|
||||||
height: number;
|
|
||||||
width: number;
|
|
||||||
getContext: HTMLCanvasElement["getContext"];
|
|
||||||
convertToBlob(opts?: {
|
convertToBlob(opts?: {
|
||||||
type?: string;
|
type?: string;
|
||||||
quality?: number;
|
quality?: number;
|
||||||
}): Promise<Blob>;
|
}): Promise<Blob>;
|
||||||
transferToImageBitmap(): ImageBitmap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HTMLAudioElement {
|
interface HTMLAudioElement {
|
||||||
|
|
|
@ -174,12 +174,12 @@ export class DecryptionFailureTracker {
|
||||||
* Start checking for and tracking failures.
|
* Start checking for and tracking failures.
|
||||||
*/
|
*/
|
||||||
public start(): void {
|
public start(): void {
|
||||||
this.checkInterval = setInterval(
|
this.checkInterval = window.setInterval(
|
||||||
() => this.checkFailures(Date.now()),
|
() => this.checkFailures(Date.now()),
|
||||||
DecryptionFailureTracker.CHECK_INTERVAL_MS,
|
DecryptionFailureTracker.CHECK_INTERVAL_MS,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.trackInterval = setInterval(
|
this.trackInterval = window.setInterval(
|
||||||
() => this.trackFailures(),
|
() => this.trackFailures(),
|
||||||
DecryptionFailureTracker.TRACK_INTERVAL_MS,
|
DecryptionFailureTracker.TRACK_INTERVAL_MS,
|
||||||
);
|
);
|
||||||
|
|
|
@ -47,6 +47,7 @@ import {
|
||||||
removeClientInformation,
|
removeClientInformation,
|
||||||
} from "./utils/device/clientInformation";
|
} from "./utils/device/clientInformation";
|
||||||
import SettingsStore, { CallbackFn } from "./settings/SettingsStore";
|
import SettingsStore, { CallbackFn } from "./settings/SettingsStore";
|
||||||
|
import { UIFeature } from "./settings/UIFeature";
|
||||||
|
|
||||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
@ -68,6 +69,7 @@ export default class DeviceListener {
|
||||||
private displayingToastsForDeviceIds = new Set<string>();
|
private displayingToastsForDeviceIds = new Set<string>();
|
||||||
private running = false;
|
private running = false;
|
||||||
private shouldRecordClientInformation = false;
|
private shouldRecordClientInformation = false;
|
||||||
|
private enableBulkUnverifiedSessionsReminder = true;
|
||||||
private deviceClientInformationSettingWatcherRef: string | undefined;
|
private deviceClientInformationSettingWatcherRef: string | undefined;
|
||||||
|
|
||||||
public static sharedInstance() {
|
public static sharedInstance() {
|
||||||
|
@ -86,6 +88,8 @@ export default class DeviceListener {
|
||||||
MatrixClientPeg.get().on(ClientEvent.Sync, this.onSync);
|
MatrixClientPeg.get().on(ClientEvent.Sync, this.onSync);
|
||||||
MatrixClientPeg.get().on(RoomStateEvent.Events, this.onRoomStateEvents);
|
MatrixClientPeg.get().on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||||
this.shouldRecordClientInformation = SettingsStore.getValue('deviceClientInformationOptIn');
|
this.shouldRecordClientInformation = SettingsStore.getValue('deviceClientInformationOptIn');
|
||||||
|
// only configurable in config, so we don't need to watch the value
|
||||||
|
this.enableBulkUnverifiedSessionsReminder = SettingsStore.getValue(UIFeature.BulkUnverifiedSessionsReminder);
|
||||||
this.deviceClientInformationSettingWatcherRef = SettingsStore.watchSetting(
|
this.deviceClientInformationSettingWatcherRef = SettingsStore.watchSetting(
|
||||||
'deviceClientInformationOptIn',
|
'deviceClientInformationOptIn',
|
||||||
null,
|
null,
|
||||||
|
@ -306,6 +310,9 @@ export default class DeviceListener {
|
||||||
// Unverified devices that have appeared since then
|
// Unverified devices that have appeared since then
|
||||||
const newUnverifiedDeviceIds = new Set<string>();
|
const newUnverifiedDeviceIds = new Set<string>();
|
||||||
|
|
||||||
|
const isCurrentDeviceTrusted = crossSigningReady &&
|
||||||
|
await (cli.checkDeviceTrust(cli.getUserId()!, cli.deviceId!)).isCrossSigningVerified();
|
||||||
|
|
||||||
// as long as cross-signing isn't ready,
|
// as long as cross-signing isn't ready,
|
||||||
// you can't see or dismiss any device toasts
|
// you can't see or dismiss any device toasts
|
||||||
if (crossSigningReady) {
|
if (crossSigningReady) {
|
||||||
|
@ -313,7 +320,7 @@ export default class DeviceListener {
|
||||||
for (const device of devices) {
|
for (const device of devices) {
|
||||||
if (device.deviceId === cli.deviceId) continue;
|
if (device.deviceId === cli.deviceId) continue;
|
||||||
|
|
||||||
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
|
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId()!, device.deviceId!);
|
||||||
if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) {
|
if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) {
|
||||||
if (this.ourDeviceIdsAtStart.has(device.deviceId)) {
|
if (this.ourDeviceIdsAtStart.has(device.deviceId)) {
|
||||||
oldUnverifiedDeviceIds.add(device.deviceId);
|
oldUnverifiedDeviceIds.add(device.deviceId);
|
||||||
|
@ -329,7 +336,12 @@ export default class DeviceListener {
|
||||||
logger.debug("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(','));
|
logger.debug("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(','));
|
||||||
|
|
||||||
// Display or hide the batch toast for old unverified sessions
|
// Display or hide the batch toast for old unverified sessions
|
||||||
if (oldUnverifiedDeviceIds.size > 0) {
|
// don't show the toast if the current device is unverified
|
||||||
|
if (
|
||||||
|
oldUnverifiedDeviceIds.size > 0
|
||||||
|
&& isCurrentDeviceTrusted
|
||||||
|
&& this.enableBulkUnverifiedSessionsReminder
|
||||||
|
) {
|
||||||
showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
|
showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
|
||||||
} else {
|
} else {
|
||||||
hideBulkUnverifiedSessionsToast();
|
hideBulkUnverifiedSessionsToast();
|
||||||
|
|
|
@ -24,7 +24,7 @@ import classNames from 'classnames';
|
||||||
import EMOJIBASE_REGEX from 'emojibase-regex';
|
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||||
import { split } from 'lodash';
|
import { split } from 'lodash';
|
||||||
import katex from 'katex';
|
import katex from 'katex';
|
||||||
import { AllHtmlEntities } from 'html-entities';
|
import { decode } from 'html-entities';
|
||||||
import { IContent } from 'matrix-js-sdk/src/models/event';
|
import { IContent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { Optional } from 'matrix-events-sdk';
|
import { Optional } from 'matrix-events-sdk';
|
||||||
|
|
||||||
|
@ -518,7 +518,7 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
|
||||||
// Cheerio instance to be returned.
|
// Cheerio instance to be returned.
|
||||||
phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
|
phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
|
||||||
return katex.renderToString(
|
return katex.renderToString(
|
||||||
AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),
|
decode(phtml(e).attr('data-mx-maths')),
|
||||||
{
|
{
|
||||||
throwOnError: false,
|
throwOnError: false,
|
||||||
// @ts-ignore - `e` can be an Element, not just a Node
|
// @ts-ignore - `e` can be an Element, not just a Node
|
||||||
|
|
|
@ -71,13 +71,52 @@ export const PROTOCOL_SIP_VIRTUAL = 'im.vector.protocol.sip_virtual';
|
||||||
|
|
||||||
const CHECK_PROTOCOLS_ATTEMPTS = 3;
|
const CHECK_PROTOCOLS_ATTEMPTS = 3;
|
||||||
|
|
||||||
enum AudioID {
|
type MediaEventType = keyof HTMLMediaElementEventMap;
|
||||||
|
const MEDIA_ERROR_EVENT_TYPES: MediaEventType[] = [
|
||||||
|
'error',
|
||||||
|
// The media has become empty; for example, this event is sent if the media has
|
||||||
|
// already been loaded (or partially loaded), and the HTMLMediaElement.load method
|
||||||
|
// is called to reload it.
|
||||||
|
'emptied',
|
||||||
|
// The user agent is trying to fetch media data, but data is unexpectedly not
|
||||||
|
// forthcoming.
|
||||||
|
'stalled',
|
||||||
|
// Media data loading has been suspended.
|
||||||
|
'suspend',
|
||||||
|
// Playback has stopped because of a temporary lack of data
|
||||||
|
'waiting',
|
||||||
|
];
|
||||||
|
const MEDIA_DEBUG_EVENT_TYPES: MediaEventType[] = [
|
||||||
|
'play',
|
||||||
|
'pause',
|
||||||
|
'playing',
|
||||||
|
'ended',
|
||||||
|
'loadeddata',
|
||||||
|
'loadedmetadata',
|
||||||
|
'canplay',
|
||||||
|
'canplaythrough',
|
||||||
|
'volumechange',
|
||||||
|
];
|
||||||
|
|
||||||
|
const MEDIA_EVENT_TYPES = [
|
||||||
|
...MEDIA_ERROR_EVENT_TYPES,
|
||||||
|
...MEDIA_DEBUG_EVENT_TYPES,
|
||||||
|
];
|
||||||
|
|
||||||
|
export enum AudioID {
|
||||||
Ring = 'ringAudio',
|
Ring = 'ringAudio',
|
||||||
Ringback = 'ringbackAudio',
|
Ringback = 'ringbackAudio',
|
||||||
CallEnd = 'callendAudio',
|
CallEnd = 'callendAudio',
|
||||||
Busy = 'busyAudio',
|
Busy = 'busyAudio',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
const debuglog = (...args: any[]): void => {
|
||||||
|
if (SettingsStore.getValue("debug_legacy_call_handler")) {
|
||||||
|
logger.log.call(console, "LegacyCallHandler debuglog:", ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
interface ThirdpartyLookupResponseFields {
|
interface ThirdpartyLookupResponseFields {
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
@ -119,6 +158,7 @@ export default class LegacyCallHandler extends EventEmitter {
|
||||||
// call with a different party to this one.
|
// call with a different party to this one.
|
||||||
private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee)
|
private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee)
|
||||||
private audioPromises = new Map<AudioID, Promise<void>>();
|
private audioPromises = new Map<AudioID, Promise<void>>();
|
||||||
|
private audioElementsWithListeners = new Map<HTMLMediaElement, boolean>();
|
||||||
private supportsPstnProtocol = null;
|
private supportsPstnProtocol = null;
|
||||||
private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol
|
private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol
|
||||||
private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native
|
private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native
|
||||||
|
@ -176,6 +216,16 @@ export default class LegacyCallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS);
|
this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS);
|
||||||
|
|
||||||
|
// Add event listeners for the <audio> elements
|
||||||
|
Object.values(AudioID).forEach((audioId) => {
|
||||||
|
const audioElement = document.getElementById(audioId) as HTMLMediaElement;
|
||||||
|
if (audioElement) {
|
||||||
|
this.addEventListenersForAudioElement(audioElement);
|
||||||
|
} else {
|
||||||
|
logger.warn(`LegacyCallHandler: missing <audio id="${audioId}"> from page`);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop(): void {
|
public stop(): void {
|
||||||
|
@ -183,6 +233,39 @@ export default class LegacyCallHandler extends EventEmitter {
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener(CallEventHandlerEvent.Incoming, this.onCallIncoming);
|
cli.removeListener(CallEventHandlerEvent.Incoming, this.onCallIncoming);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove event listeners for the <audio> elements
|
||||||
|
Array.from(this.audioElementsWithListeners.keys()).forEach((audioElement) => {
|
||||||
|
this.removeEventListenersForAudioElement(audioElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private addEventListenersForAudioElement(audioElement: HTMLMediaElement): void {
|
||||||
|
// Only need to setup the listeners once
|
||||||
|
if (!this.audioElementsWithListeners.get(audioElement)) {
|
||||||
|
MEDIA_EVENT_TYPES.forEach((errorEventType) => {
|
||||||
|
audioElement.addEventListener(errorEventType, this);
|
||||||
|
this.audioElementsWithListeners.set(audioElement, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeEventListenersForAudioElement(audioElement: HTMLMediaElement): void {
|
||||||
|
MEDIA_EVENT_TYPES.forEach((errorEventType) => {
|
||||||
|
audioElement.removeEventListener(errorEventType, this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next (remove if we start using this function for things other than debug logging) */
|
||||||
|
public handleEvent(e: Event): void {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const audioId = target?.id;
|
||||||
|
|
||||||
|
if (MEDIA_ERROR_EVENT_TYPES.includes(e.type as MediaEventType)) {
|
||||||
|
logger.error(`LegacyCallHandler: encountered "${e.type}" event with <audio id="${audioId}">`, e);
|
||||||
|
} else if (MEDIA_EVENT_TYPES.includes(e.type as MediaEventType)) {
|
||||||
|
debuglog(`encountered "${e.type}" event with <audio id="${audioId}">`, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isForcedSilent(): boolean {
|
public isForcedSilent(): boolean {
|
||||||
|
@ -254,7 +337,7 @@ export default class LegacyCallHandler extends EventEmitter {
|
||||||
logger.log("Failed to check for protocol support and no retries remain: assuming no support", e);
|
logger.log("Failed to check for protocol support and no retries remain: assuming no support", e);
|
||||||
} else {
|
} else {
|
||||||
logger.log("Failed to check for protocol support: will retry", e);
|
logger.log("Failed to check for protocol support: will retry", e);
|
||||||
setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.checkProtocols(maxTries - 1);
|
this.checkProtocols(maxTries - 1);
|
||||||
}, 10000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
|
@ -402,11 +485,21 @@ export default class LegacyCallHandler extends EventEmitter {
|
||||||
// which listens?
|
// which listens?
|
||||||
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
||||||
if (audio) {
|
if (audio) {
|
||||||
|
this.addEventListenersForAudioElement(audio);
|
||||||
const playAudio = async () => {
|
const playAudio = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (audio.muted) {
|
||||||
|
logger.error(
|
||||||
|
`${logPrefix} <audio> element was unexpectedly muted but we recovered ` +
|
||||||
|
`gracefully by unmuting it`,
|
||||||
|
);
|
||||||
|
// Recover gracefully
|
||||||
|
audio.muted = false;
|
||||||
|
}
|
||||||
|
|
||||||
// This still causes the chrome debugger to break on promise rejection if
|
// This still causes the chrome debugger to break on promise rejection if
|
||||||
// the promise is rejected, even though we're catching the exception.
|
// the promise is rejected, even though we're catching the exception.
|
||||||
logger.debug(`${logPrefix} attempting to play audio`);
|
logger.debug(`${logPrefix} attempting to play audio at volume=${audio.volume}`);
|
||||||
await audio.play();
|
await audio.play();
|
||||||
logger.debug(`${logPrefix} playing audio successfully`);
|
logger.debug(`${logPrefix} playing audio successfully`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -584,7 +584,7 @@ async function doSetLoggedIn(
|
||||||
// later than MatrixChat might assume.
|
// later than MatrixChat might assume.
|
||||||
//
|
//
|
||||||
// we fire it *synchronously* to make sure it fires before on_logged_in.
|
// we fire it *synchronously* to make sure it fires before on_logged_in.
|
||||||
// (dis.dispatch uses `setTimeout`, which does not guarantee ordering.)
|
// (dis.dispatch uses `window.setTimeout`, which does not guarantee ordering.)
|
||||||
dis.dispatch({ action: 'on_logging_in' }, true);
|
dis.dispatch({ action: 'on_logging_in' }, true);
|
||||||
|
|
||||||
if (clearStorageEnabled) {
|
if (clearStorageEnabled) {
|
||||||
|
@ -865,7 +865,7 @@ export async function onLoggedOut(): Promise<void> {
|
||||||
if (SdkConfig.get().logout_redirect_url) {
|
if (SdkConfig.get().logout_redirect_url) {
|
||||||
logger.log("Redirecting to external provider to finish logout");
|
logger.log("Redirecting to external provider to finish logout");
|
||||||
// XXX: Defer this so that it doesn't race with MatrixChat unmounting the world by going to /#/login
|
// XXX: Defer this so that it doesn't race with MatrixChat unmounting the world by going to /#/login
|
||||||
setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
window.location.href = SdkConfig.get().logout_redirect_url;
|
window.location.href = SdkConfig.get().logout_redirect_url;
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,7 +119,7 @@ export default class NodeAnimator extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// and then we animate to the resting state
|
// and then we animate to the resting state
|
||||||
setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.applyStyles(domNode as HTMLElement, restingStyle);
|
this.applyStyles(domNode as HTMLElement, restingStyle);
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {
|
||||||
PermissionChanged as PermissionChangedEvent,
|
PermissionChanged as PermissionChangedEvent,
|
||||||
} from "@matrix-org/analytics-events/types/typescript/PermissionChanged";
|
} from "@matrix-org/analytics-events/types/typescript/PermissionChanged";
|
||||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||||
|
import { IRoomTimelineData } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||||
|
@ -217,7 +218,7 @@ export const Notifier = {
|
||||||
this.boundOnRoomReceipt = this.boundOnRoomReceipt || this.onRoomReceipt.bind(this);
|
this.boundOnRoomReceipt = this.boundOnRoomReceipt || this.onRoomReceipt.bind(this);
|
||||||
this.boundOnEventDecrypted = this.boundOnEventDecrypted || this.onEventDecrypted.bind(this);
|
this.boundOnEventDecrypted = this.boundOnEventDecrypted || this.onEventDecrypted.bind(this);
|
||||||
|
|
||||||
MatrixClientPeg.get().on(ClientEvent.Event, this.boundOnEvent);
|
MatrixClientPeg.get().on(RoomEvent.Timeline, this.boundOnEvent);
|
||||||
MatrixClientPeg.get().on(RoomEvent.Receipt, this.boundOnRoomReceipt);
|
MatrixClientPeg.get().on(RoomEvent.Receipt, this.boundOnRoomReceipt);
|
||||||
MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted);
|
MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted);
|
||||||
MatrixClientPeg.get().on(ClientEvent.Sync, this.boundOnSyncStateChange);
|
MatrixClientPeg.get().on(ClientEvent.Sync, this.boundOnSyncStateChange);
|
||||||
|
@ -227,7 +228,7 @@ export const Notifier = {
|
||||||
|
|
||||||
stop: function(this: typeof Notifier) {
|
stop: function(this: typeof Notifier) {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener(ClientEvent.Event, this.boundOnEvent);
|
MatrixClientPeg.get().removeListener(RoomEvent.Timeline, this.boundOnEvent);
|
||||||
MatrixClientPeg.get().removeListener(RoomEvent.Receipt, this.boundOnRoomReceipt);
|
MatrixClientPeg.get().removeListener(RoomEvent.Receipt, this.boundOnRoomReceipt);
|
||||||
MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted);
|
MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted);
|
||||||
MatrixClientPeg.get().removeListener(ClientEvent.Sync, this.boundOnSyncStateChange);
|
MatrixClientPeg.get().removeListener(ClientEvent.Sync, this.boundOnSyncStateChange);
|
||||||
|
@ -368,7 +369,15 @@ export const Notifier = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onEvent: function(this: typeof Notifier, ev: MatrixEvent) {
|
onEvent: function(
|
||||||
|
this: typeof Notifier,
|
||||||
|
ev: MatrixEvent,
|
||||||
|
room: Room | undefined,
|
||||||
|
toStartOfTimeline: boolean | undefined,
|
||||||
|
removed: boolean,
|
||||||
|
data: IRoomTimelineData,
|
||||||
|
) {
|
||||||
|
if (!data.liveEvent) return; // only notify for new things, not old.
|
||||||
if (!this.isSyncing) return; // don't alert for any messages initially
|
if (!this.isSyncing) return; // don't alert for any messages initially
|
||||||
if (ev.getSender() === MatrixClientPeg.get().getUserId()) return;
|
if (ev.getSender() === MatrixClientPeg.get().getUserId()) return;
|
||||||
|
|
||||||
|
@ -428,6 +437,11 @@ export const Notifier = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
// e.g we are in the process of joining a room.
|
||||||
|
// Seen in the cypress lazy-loading test.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ export default class PasswordReset {
|
||||||
this.checkEmailLinkClicked()
|
this.checkEmailLinkClicked()
|
||||||
.then(() => resolve())
|
.then(() => resolve())
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setTimeout(
|
window.setTimeout(
|
||||||
() => this.tryCheckEmailLinkClicked(resolve),
|
() => this.tryCheckEmailLinkClicked(resolve),
|
||||||
CHECK_EMAIL_VERIFIED_POLL_INTERVAL,
|
CHECK_EMAIL_VERIFIED_POLL_INTERVAL,
|
||||||
);
|
);
|
||||||
|
|
|
@ -127,7 +127,7 @@ export class PlaybackClock implements IDestroyable {
|
||||||
// cast to number because the types are wrong
|
// cast to number because the types are wrong
|
||||||
// 100ms interval to make sure the time is as accurate as possible without
|
// 100ms interval to make sure the time is as accurate as possible without
|
||||||
// being overly insane
|
// being overly insane
|
||||||
this.timerId = <number><any>setInterval(this.checkTime, 100);
|
this.timerId = <number><any>window.setInterval(this.checkTime, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,4 +85,4 @@ class MxVoiceWorklet extends AudioWorkletProcessor {
|
||||||
|
|
||||||
registerProcessor(WORKLET_NAME, MxVoiceWorklet);
|
registerProcessor(WORKLET_NAME, MxVoiceWorklet);
|
||||||
|
|
||||||
export default null; // to appease module loaders (we never use the export)
|
export default ""; // to appease module loaders (we never use the export)
|
||||||
|
|
|
@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Recorder from 'opus-recorder';
|
// @ts-ignore
|
||||||
|
import Recorder from 'opus-recorder/dist/recorder.min.js';
|
||||||
import encoderPath from 'opus-recorder/dist/encoderWorker.min.js';
|
import encoderPath from 'opus-recorder/dist/encoderWorker.min.js';
|
||||||
import { SimpleObservable } from "matrix-widget-api";
|
import { SimpleObservable } from "matrix-widget-api";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
|
@ -32,12 +33,26 @@ import mxRecorderWorkletPath from "./RecorderWorklet";
|
||||||
|
|
||||||
const CHANNELS = 1; // stereo isn't important
|
const CHANNELS = 1; // stereo isn't important
|
||||||
export const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
|
export const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
|
||||||
const BITRATE = 24000; // 24kbps is pretty high quality for our use case in opus.
|
|
||||||
const TARGET_MAX_LENGTH = 900; // 15 minutes in seconds. Somewhat arbitrary, though longer == larger files.
|
const TARGET_MAX_LENGTH = 900; // 15 minutes in seconds. Somewhat arbitrary, though longer == larger files.
|
||||||
const TARGET_WARN_TIME_LEFT = 10; // 10 seconds, also somewhat arbitrary.
|
const TARGET_WARN_TIME_LEFT = 10; // 10 seconds, also somewhat arbitrary.
|
||||||
|
|
||||||
export const RECORDING_PLAYBACK_SAMPLES = 44;
|
export const RECORDING_PLAYBACK_SAMPLES = 44;
|
||||||
|
|
||||||
|
interface RecorderOptions {
|
||||||
|
bitrate: number;
|
||||||
|
encoderApplication: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const voiceRecorderOptions: RecorderOptions = {
|
||||||
|
bitrate: 24000, // recommended Opus bitrate for high-quality VoIP
|
||||||
|
encoderApplication: 2048, // voice
|
||||||
|
};
|
||||||
|
|
||||||
|
export const highQualityRecorderOptions: RecorderOptions = {
|
||||||
|
bitrate: 96000, // recommended Opus bitrate for high-quality music/audio streaming
|
||||||
|
encoderApplication: 2049, // full band audio
|
||||||
|
};
|
||||||
|
|
||||||
export interface IRecordingUpdate {
|
export interface IRecordingUpdate {
|
||||||
waveform: number[]; // floating points between 0 (low) and 1 (high).
|
waveform: number[]; // floating points between 0 (low) and 1 (high).
|
||||||
timeSeconds: number; // float
|
timeSeconds: number; // float
|
||||||
|
@ -88,13 +103,22 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
||||||
this.targetMaxLength = null;
|
this.targetMaxLength = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldRecordInHighQuality(): boolean {
|
||||||
|
// Non-voice use case is suspected when noise suppression is disabled by the user.
|
||||||
|
// When recording complex audio, higher quality is required to avoid audio artifacts.
|
||||||
|
// This is a really arbitrary decision, but it can be refined/replaced at any time.
|
||||||
|
return !MediaDeviceHandler.getAudioNoiseSuppression();
|
||||||
|
}
|
||||||
|
|
||||||
private async makeRecorder() {
|
private async makeRecorder() {
|
||||||
try {
|
try {
|
||||||
this.recorderStream = await navigator.mediaDevices.getUserMedia({
|
this.recorderStream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: {
|
audio: {
|
||||||
channelCount: CHANNELS,
|
channelCount: CHANNELS,
|
||||||
noiseSuppression: true, // browsers ignore constraints they can't honour
|
|
||||||
deviceId: MediaDeviceHandler.getAudioInput(),
|
deviceId: MediaDeviceHandler.getAudioInput(),
|
||||||
|
autoGainControl: { ideal: MediaDeviceHandler.getAudioAutoGainControl() },
|
||||||
|
echoCancellation: { ideal: MediaDeviceHandler.getAudioEchoCancellation() },
|
||||||
|
noiseSuppression: { ideal: MediaDeviceHandler.getAudioNoiseSuppression() },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.recorderContext = createAudioContext({
|
this.recorderContext = createAudioContext({
|
||||||
|
@ -135,15 +159,19 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
||||||
this.recorderProcessor.addEventListener("audioprocess", this.onAudioProcess);
|
this.recorderProcessor.addEventListener("audioprocess", this.onAudioProcess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recorderOptions = this.shouldRecordInHighQuality() ?
|
||||||
|
highQualityRecorderOptions : voiceRecorderOptions;
|
||||||
|
const { encoderApplication, bitrate } = recorderOptions;
|
||||||
|
|
||||||
this.recorder = new Recorder({
|
this.recorder = new Recorder({
|
||||||
encoderPath, // magic from webpack
|
encoderPath, // magic from webpack
|
||||||
encoderSampleRate: SAMPLE_RATE,
|
encoderSampleRate: SAMPLE_RATE,
|
||||||
encoderApplication: 2048, // voice (default is "audio")
|
encoderApplication: encoderApplication,
|
||||||
streamPages: true, // this speeds up the encoding process by using CPU over time
|
streamPages: true, // this speeds up the encoding process by using CPU over time
|
||||||
encoderFrameSize: 20, // ms, arbitrary frame size we send to the encoder
|
encoderFrameSize: 20, // ms, arbitrary frame size we send to the encoder
|
||||||
numberOfChannels: CHANNELS,
|
numberOfChannels: CHANNELS,
|
||||||
sourceNode: this.recorderSource,
|
sourceNode: this.recorderSource,
|
||||||
encoderBitRate: BITRATE,
|
encoderBitRate: bitrate,
|
||||||
|
|
||||||
// We use low values for the following to ease CPU usage - the resulting waveform
|
// We use low values for the following to ease CPU usage - the resulting waveform
|
||||||
// is indistinguishable for a voice message. Note that the underlying library will
|
// is indistinguishable for a voice message. Note that the underlying library will
|
||||||
|
|
|
@ -35,7 +35,7 @@ export interface ISelectionRange {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICompletion {
|
export interface ICompletion {
|
||||||
type: "at-room" | "command" | "community" | "room" | "user";
|
type?: "at-room" | "command" | "community" | "room" | "user";
|
||||||
completion: string;
|
completion: string;
|
||||||
completionId?: string;
|
completionId?: string;
|
||||||
component?: ReactElement;
|
component?: ReactElement;
|
||||||
|
|
|
@ -103,7 +103,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
return []; // don't give any suggestions if the user doesn't want them
|
return []; // don't give any suggestions if the user doesn't want them
|
||||||
}
|
}
|
||||||
|
|
||||||
let completions = [];
|
let completions: ISortedEmoji[] = [];
|
||||||
const { command, range } = this.getCurrentCommand(query, selection);
|
const { command, range } = this.getCurrentCommand(query, selection);
|
||||||
|
|
||||||
if (command && command[0].length > 2) {
|
if (command && command[0].length > 2) {
|
||||||
|
@ -132,7 +132,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
// Finally, sort by original ordering
|
// Finally, sort by original ordering
|
||||||
sorters.push(c => c._orderBy);
|
sorters.push(c => c._orderBy);
|
||||||
completions = sortBy(uniq(completions), sorters);
|
completions = sortBy<ISortedEmoji>(uniq(completions), sorters);
|
||||||
|
|
||||||
completions = completions.slice(0, LIMIT);
|
completions = completions.slice(0, LIMIT);
|
||||||
|
|
||||||
|
@ -141,9 +141,9 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
this.recentlyUsed.forEach(emoji => {
|
this.recentlyUsed.forEach(emoji => {
|
||||||
sorters.push(c => score(emoji.shortcodes[0], c.emoji.shortcodes[0]));
|
sorters.push(c => score(emoji.shortcodes[0], c.emoji.shortcodes[0]));
|
||||||
});
|
});
|
||||||
completions = sortBy(uniq(completions), sorters);
|
completions = sortBy<ISortedEmoji>(uniq(completions), sorters);
|
||||||
|
|
||||||
completions = completions.map(c => ({
|
return completions.map(c => ({
|
||||||
completion: c.emoji.unicode,
|
completion: c.emoji.unicode,
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion title={`:${c.emoji.shortcodes[0]}:`} aria-label={c.emoji.unicode}>
|
<PillCompletion title={`:${c.emoji.shortcodes[0]}:`} aria-label={c.emoji.unicode}>
|
||||||
|
@ -153,7 +153,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
range,
|
range,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return completions;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
|
|
|
@ -127,7 +127,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.props.poll) {
|
if (this.props.poll) {
|
||||||
this.intervalId = setInterval(() => {
|
this.intervalId = window.setInterval(() => {
|
||||||
this.authLogic.poll();
|
this.authLogic.poll();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1965,7 +1965,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
this.accountPassword = password;
|
this.accountPassword = password;
|
||||||
// self-destruct the password after 5mins
|
// self-destruct the password after 5mins
|
||||||
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
||||||
this.accountPasswordTimer = setTimeout(() => {
|
this.accountPasswordTimer = window.setTimeout(() => {
|
||||||
this.accountPassword = null;
|
this.accountPassword = null;
|
||||||
this.accountPasswordTimer = null;
|
this.accountPasswordTimer = null;
|
||||||
}, 60 * 5 * 1000);
|
}, 60 * 5 * 1000);
|
||||||
|
|
|
@ -459,7 +459,7 @@ export default class ScrollPanel extends React.Component<IProps> {
|
||||||
if (this.unfillDebouncer) {
|
if (this.unfillDebouncer) {
|
||||||
clearTimeout(this.unfillDebouncer);
|
clearTimeout(this.unfillDebouncer);
|
||||||
}
|
}
|
||||||
this.unfillDebouncer = setTimeout(() => {
|
this.unfillDebouncer = window.setTimeout(() => {
|
||||||
this.unfillDebouncer = null;
|
this.unfillDebouncer = null;
|
||||||
debuglog("unfilling now", { backwards, origExcessHeight });
|
debuglog("unfilling now", { backwards, origExcessHeight });
|
||||||
this.props.onUnfillRequest?.(backwards, markerScrollToken!);
|
this.props.onUnfillRequest?.(backwards, markerScrollToken!);
|
||||||
|
@ -485,7 +485,7 @@ export default class ScrollPanel extends React.Component<IProps> {
|
||||||
// this will block the scroll event handler for +700ms
|
// this will block the scroll event handler for +700ms
|
||||||
// if messages are already cached in memory,
|
// if messages are already cached in memory,
|
||||||
// This would cause jumping to happen on Chrome/macOS.
|
// This would cause jumping to happen on Chrome/macOS.
|
||||||
return new Promise(resolve => setTimeout(resolve, 1)).then(() => {
|
return new Promise(resolve => window.setTimeout(resolve, 1)).then(() => {
|
||||||
return this.props.onFillRequest(backwards);
|
return this.props.onFillRequest(backwards);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.pendingFillRequests[dir] = false;
|
this.pendingFillRequests[dir] = false;
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import filesize from "filesize";
|
import { filesize } from "filesize";
|
||||||
import { IEventRelation } from 'matrix-js-sdk/src/matrix';
|
import { IEventRelation } from 'matrix-js-sdk/src/matrix';
|
||||||
import { Optional } from "matrix-events-sdk";
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
|
|
|
@ -96,8 +96,9 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||||
private async updateMode(mode: Mode) {
|
private async updateMode(mode: Mode) {
|
||||||
this.setState({ phase: Phase.Loading });
|
this.setState({ phase: Phase.Loading });
|
||||||
if (this.state.rendezvous) {
|
if (this.state.rendezvous) {
|
||||||
this.state.rendezvous.onFailure = undefined;
|
const rendezvous = this.state.rendezvous;
|
||||||
await this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled);
|
rendezvous.onFailure = undefined;
|
||||||
|
await rendezvous.cancel(RendezvousFailureReason.UserCancelled);
|
||||||
this.setState({ rendezvous: undefined });
|
this.setState({ rendezvous: undefined });
|
||||||
}
|
}
|
||||||
if (mode === Mode.Show) {
|
if (mode === Mode.Show) {
|
||||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -27,11 +26,6 @@ interface IProps extends IContextMenuProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class LegacyCallContextMenu extends React.Component<IProps> {
|
export default class LegacyCallContextMenu extends React.Component<IProps> {
|
||||||
static propTypes = {
|
|
||||||
// js-sdk User object. Not required because it might not exist.
|
|
||||||
user: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
|
@ -697,7 +697,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||||
if (this.debounceTimer) {
|
if (this.debounceTimer) {
|
||||||
clearTimeout(this.debounceTimer);
|
clearTimeout(this.debounceTimer);
|
||||||
}
|
}
|
||||||
this.debounceTimer = setTimeout(() => {
|
this.debounceTimer = window.setTimeout(() => {
|
||||||
this.updateSuggestions(term);
|
this.updateSuggestions(term);
|
||||||
}, 150); // 150ms debounce (human reaction time + some)
|
}, 150); // 150ms debounce (human reaction time + some)
|
||||||
};
|
};
|
||||||
|
|
|
@ -48,7 +48,7 @@ async function syncHealthCheck(cli: MatrixClient): Promise<void> {
|
||||||
*/
|
*/
|
||||||
async function proxyHealthCheck(endpoint: string, hsUrl?: string): Promise<void> {
|
async function proxyHealthCheck(endpoint: string, hsUrl?: string): Promise<void> {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const id = setTimeout(() => controller.abort(), 10 * 1000); // 10s
|
const id = window.setTimeout(() => controller.abort(), 10 * 1000); // 10s
|
||||||
const res = await fetch(endpoint + "/client/server.json", {
|
const res = await fetch(endpoint + "/client/server.json", {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import filesize from "filesize";
|
import { filesize } from "filesize";
|
||||||
|
|
||||||
import { Icon as FileIcon } from '../../../../res/img/feather-customised/files.svg';
|
import { Icon as FileIcon } from '../../../../res/img/feather-customised/files.svg';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import filesize from 'filesize';
|
import { filesize } from 'filesize';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
|
@ -51,7 +51,7 @@ const VerificationRequestExplorer: React.FC<{
|
||||||
if (request.timeout == 0) return;
|
if (request.timeout == 0) return;
|
||||||
|
|
||||||
/* Note that request.timeout is a getter, so its value changes */
|
/* Note that request.timeout is a getter, so its value changes */
|
||||||
const id = setInterval(() => {
|
const id = window.setInterval(() => {
|
||||||
setRequestTimeout(request.timeout);
|
setRequestTimeout(request.timeout);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
|
|
@ -228,7 +228,7 @@ export const useWebSearchMetrics = (numResults: number, queryLength: number, via
|
||||||
if (!queryLength) return;
|
if (!queryLength) return;
|
||||||
|
|
||||||
// send metrics after a 1s debounce
|
// send metrics after a 1s debounce
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = window.setTimeout(() => {
|
||||||
PosthogAnalytics.instance.trackEvent<WebSearchEvent>({
|
PosthogAnalytics.instance.trackEvent<WebSearchEvent>({
|
||||||
eventName: "WebSearch",
|
eventName: "WebSearch",
|
||||||
viaSpotlight,
|
viaSpotlight,
|
||||||
|
|
|
@ -106,7 +106,7 @@ export default class DesktopCapturerSourcePicker extends React.Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
// setInterval() first waits and then executes, therefore
|
// window.setInterval() first waits and then executes, therefore
|
||||||
// we call getDesktopCapturerSources() here without any delay.
|
// we call getDesktopCapturerSources() here without any delay.
|
||||||
// Otherwise the dialog would be left empty for some time.
|
// Otherwise the dialog would be left empty for some time.
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -114,7 +114,7 @@ export default class DesktopCapturerSourcePicker extends React.Component<
|
||||||
});
|
});
|
||||||
|
|
||||||
// We update the sources every 500ms to get newer thumbnails
|
// We update the sources every 500ms to get newer thumbnails
|
||||||
this.interval = setInterval(async () => {
|
this.interval = window.setInterval(async () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
sources: await getDesktopCapturerSources(),
|
sources: await getDesktopCapturerSources(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -240,7 +240,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
|
||||||
{ _t("In reply to <a>this message</a>",
|
{ _t("In reply to <a>this message</a>",
|
||||||
{},
|
{},
|
||||||
{ a: (sub) => (
|
{ a: (sub) => (
|
||||||
<a className="mx_reply_anchor" href={`#${eventId}`} scroll-to={eventId}> { sub } </a>
|
<a className="mx_reply_anchor" href={`#${eventId}`} data-scroll-to={eventId}> { sub } </a>
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,12 +80,13 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
if (!canChange && this.props.hideIfCannotSet) return null;
|
if (!canChange && this.props.hideIfCannotSet) return null;
|
||||||
|
|
||||||
const label = this.props.label
|
const label = (this.props.label
|
||||||
? _t(this.props.label)
|
? _t(this.props.label)
|
||||||
: SettingsStore.getDisplayName(this.props.name, this.props.level);
|
: SettingsStore.getDisplayName(this.props.name, this.props.level)) ?? undefined;
|
||||||
const description = SettingsStore.getDescription(this.props.name);
|
const description = SettingsStore.getDescription(this.props.name);
|
||||||
|
const shouldWarn = SettingsStore.shouldHaveWarning(this.props.name);
|
||||||
|
|
||||||
let disabledDescription: JSX.Element;
|
let disabledDescription: JSX.Element | null = null;
|
||||||
if (this.props.disabled && this.props.disabledDescription) {
|
if (this.props.disabled && this.props.disabledDescription) {
|
||||||
disabledDescription = <div className="mx_SettingsFlag_microcopy">
|
disabledDescription = <div className="mx_SettingsFlag_microcopy">
|
||||||
{ this.props.disabledDescription }
|
{ this.props.disabledDescription }
|
||||||
|
@ -106,7 +107,20 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||||
<label className="mx_SettingsFlag_label">
|
<label className="mx_SettingsFlag_label">
|
||||||
<span className="mx_SettingsFlag_labelText">{ label }</span>
|
<span className="mx_SettingsFlag_labelText">{ label }</span>
|
||||||
{ description && <div className="mx_SettingsFlag_microcopy">
|
{ description && <div className="mx_SettingsFlag_microcopy">
|
||||||
{ description }
|
{ shouldWarn
|
||||||
|
? _t(
|
||||||
|
"<w>WARNING:</w> <description/>", {},
|
||||||
|
{
|
||||||
|
"w": (sub) => (
|
||||||
|
<span className="mx_SettingsTab_microcopy_warning">
|
||||||
|
{ sub }
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
"description": description,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: description
|
||||||
|
}
|
||||||
</div> }
|
</div> }
|
||||||
{ disabledDescription }
|
{ disabledDescription }
|
||||||
</label>
|
</label>
|
||||||
|
|
|
@ -35,7 +35,7 @@ export function UseCaseSelection({ onFinished }: Props) {
|
||||||
// Call onFinished 1.5s after `selection` becomes truthy, to give time for the animation to run
|
// Call onFinished 1.5s after `selection` becomes truthy, to give time for the animation to run
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selection) {
|
if (selection) {
|
||||||
let handler: number | null = setTimeout(() => {
|
let handler: number | null = window.setTimeout(() => {
|
||||||
handler = null;
|
handler = null;
|
||||||
onFinished(selection);
|
onFinished(selection);
|
||||||
}, TIMEOUT);
|
}, TIMEOUT);
|
||||||
|
|
|
@ -191,7 +191,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
||||||
this.setState({ filter });
|
this.setState({ filter });
|
||||||
// Header underlines need to be updated, but updating requires knowing
|
// Header underlines need to be updated, but updating requires knowing
|
||||||
// where the categories are, so we wait for a tick.
|
// where the categories are, so we wait for a tick.
|
||||||
setTimeout(this.updateVisibility, 0);
|
window.setTimeout(this.updateVisibility, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
private emojiMatchesFilter = (emoji: IEmoji, filter: string): boolean => {
|
private emojiMatchesFilter = (emoji: IEmoji, filter: string): boolean => {
|
||||||
|
|
|
@ -31,8 +31,8 @@ class Search extends React.PureComponent<IProps> {
|
||||||
private inputRef = React.createRef<HTMLInputElement>();
|
private inputRef = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// For some reason, neither the autoFocus nor just calling focus() here worked, so here's a setTimeout
|
// For some reason, neither the autoFocus nor just calling focus() here worked, so here's a window.setTimeout
|
||||||
setTimeout(() => this.inputRef.current.focus(), 0);
|
window.setTimeout(() => this.inputRef.current.focus(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import filesize from 'filesize';
|
import { filesize } from 'filesize';
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
|
@ -270,6 +270,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
|
|
||||||
// Set a placeholder image when we can't decrypt the image.
|
// Set a placeholder image when we can't decrypt the image.
|
||||||
this.setState({ error });
|
this.setState({ error });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
thumbUrl = this.getThumbUrl();
|
thumbUrl = this.getThumbUrl();
|
||||||
|
@ -291,17 +292,28 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
img.crossOrigin = "Anonymous"; // CORS allow canvas access
|
img.crossOrigin = "Anonymous"; // CORS allow canvas access
|
||||||
img.src = contentUrl;
|
img.src = contentUrl;
|
||||||
|
|
||||||
|
try {
|
||||||
await loadPromise;
|
await loadPromise;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Unable to download attachment: ", error);
|
||||||
|
this.setState({ error: error as Error });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const blob = await this.props.mediaEventHelper.sourceBlob.value;
|
const blob = await this.props.mediaEventHelper.sourceBlob.value;
|
||||||
if (!await blobIsAnimated(content.info.mimetype, blob)) {
|
if (!await blobIsAnimated(content.info?.mimetype, blob)) {
|
||||||
isAnimated = false;
|
isAnimated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAnimated) {
|
if (isAnimated) {
|
||||||
const thumb = await createThumbnail(img, img.width, img.height, content.info.mimetype, false);
|
const thumb = await createThumbnail(img, img.width, img.height, content.info!.mimetype, false);
|
||||||
thumbUrl = URL.createObjectURL(thumb.thumbnail);
|
thumbUrl = URL.createObjectURL(thumb.thumbnail);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// This is a non-critical failure, do not surface the error or bail the method here
|
||||||
|
logger.warn("Unable to generate thumbnail for animated image: ", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,7 +347,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
// Add a 150ms timer for blurhash to first appear.
|
// Add a 150ms timer for blurhash to first appear.
|
||||||
if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) {
|
if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) {
|
||||||
this.clearBlurhashTimeout();
|
this.clearBlurhashTimeout();
|
||||||
this.timeout = setTimeout(() => {
|
this.timeout = window.setTimeout(() => {
|
||||||
if (!this.state.imgLoaded || !this.state.imgError) {
|
if (!this.state.imgLoaded || !this.state.imgError) {
|
||||||
this.setState({
|
this.setState({
|
||||||
placeholder: Placeholder.Blurhash,
|
placeholder: Placeholder.Blurhash,
|
||||||
|
|
|
@ -130,7 +130,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||||
if (codes.length > 0) {
|
if (codes.length > 0) {
|
||||||
// Do this asynchronously: parsing code takes time and we don't
|
// Do this asynchronously: parsing code takes time and we don't
|
||||||
// need to block the DOM update on it.
|
// need to block the DOM update on it.
|
||||||
setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
for (let i = 0; i < codes.length; i++) {
|
for (let i = 0; i < codes.length; i++) {
|
||||||
this.highlightCode(codes[i]);
|
this.highlightCode(codes[i]);
|
||||||
|
|
|
@ -111,8 +111,21 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
|
||||||
const onStartVerification = useCallback(async () => {
|
const onStartVerification = useCallback(async () => {
|
||||||
setRequesting(true);
|
setRequesting(true);
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
let verificationRequest_: VerificationRequest;
|
||||||
|
try {
|
||||||
const roomId = await ensureDMExists(cli, member.userId);
|
const roomId = await ensureDMExists(cli, member.userId);
|
||||||
const verificationRequest_ = await cli.requestVerificationDM(member.userId, roomId);
|
verificationRequest_ = await cli.requestVerificationDM(member.userId, roomId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error starting verification", e);
|
||||||
|
setRequesting(false);
|
||||||
|
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
headerImage: require("../../../../res/img/e2e/warning.svg").default,
|
||||||
|
title: _t("Error starting verification"),
|
||||||
|
description: _t("We were unable to start a chat with the other user."),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
setRequest(verificationRequest_);
|
setRequest(verificationRequest_);
|
||||||
setPhase(verificationRequest_.phase);
|
setPhase(verificationRequest_.phase);
|
||||||
// Notify the RightPanelStore about this
|
// Notify the RightPanelStore about this
|
||||||
|
|
|
@ -127,7 +127,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.debounceCompletionsRequest = setTimeout(() => {
|
this.debounceCompletionsRequest = window.setTimeout(() => {
|
||||||
resolve(this.processQuery(query, selection));
|
resolve(this.processQuery(query, selection));
|
||||||
}, autocompleteDelay);
|
}, autocompleteDelay);
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ComponentProps, createRef } from 'react';
|
import React, { ComponentProps, createRef } from 'react';
|
||||||
import { AllHtmlEntities } from 'html-entities';
|
import { decode } from 'html-entities';
|
||||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
|
import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
||||||
|
|
||||||
// The description includes &-encoded HTML entities, we decode those as React treats the thing as an
|
// The description includes &-encoded HTML entities, we decode those as React treats the thing as an
|
||||||
// opaque string. This does not allow any HTML to be injected into the DOM.
|
// opaque string. This does not allow any HTML to be injected into the DOM.
|
||||||
const description = AllHtmlEntities.decode(p["og:description"] || "");
|
const description = decode(p["og:description"] || "");
|
||||||
|
|
||||||
const title = p["og:title"]?.trim() ?? "";
|
const title = p["og:title"]?.trim() ?? "";
|
||||||
const anchor = <a href={this.props.link} target="_blank" rel="noreferrer noopener">{ title }</a>;
|
const anchor = <a href={this.props.link} target="_blank" rel="noreferrer noopener">{ title }</a>;
|
||||||
|
|
|
@ -199,7 +199,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
// that the ScrollPanel listening to the resizeNotifier can
|
// that the ScrollPanel listening to the resizeNotifier can
|
||||||
// correctly measure it's new height and scroll down to keep
|
// correctly measure it's new height and scroll down to keep
|
||||||
// at the bottom if it already is
|
// at the bottom if it already is
|
||||||
setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.props.resizeNotifier.notifyTimelineHeightChanged();
|
this.props.resizeNotifier.notifyTimelineHeightChanged();
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
@ -395,7 +395,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onRecordingEndingSoon = ({ secondsLeft }) => {
|
private onRecordingEndingSoon = ({ secondsLeft }) => {
|
||||||
this.setState({ recordingTimeLeftSeconds: secondsLeft });
|
this.setState({ recordingTimeLeftSeconds: secondsLeft });
|
||||||
setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000);
|
window.setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000);
|
||||||
};
|
};
|
||||||
|
|
||||||
private setStickerPickerOpen = (isStickerPickerOpen: boolean) => {
|
private setStickerPickerOpen = (isStickerPickerOpen: boolean) => {
|
||||||
|
@ -584,6 +584,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
setUpVoiceBroadcastPreRecording(
|
setUpVoiceBroadcastPreRecording(
|
||||||
this.props.room,
|
this.props.room,
|
||||||
MatrixClientPeg.get(),
|
MatrixClientPeg.get(),
|
||||||
|
SdkContextClass.instance.voiceBroadcastPlaybacksStore,
|
||||||
VoiceBroadcastRecordingsStore.instance(),
|
VoiceBroadcastRecordingsStore.instance(),
|
||||||
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
|
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
|
||||||
);
|
);
|
||||||
|
|
|
@ -123,6 +123,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const classes = classNames("mx_ReplyTile", {
|
const classes = classNames("mx_ReplyTile", {
|
||||||
|
mx_ReplyTile_inline: msgType === MsgType.Emote,
|
||||||
mx_ReplyTile_info: isInfoMessage && !mxEvent.isRedacted(),
|
mx_ReplyTile_info: isInfoMessage && !mxEvent.isRedacted(),
|
||||||
mx_ReplyTile_audio: msgType === MsgType.Audio,
|
mx_ReplyTile_audio: msgType === MsgType.Audio,
|
||||||
mx_ReplyTile_video: msgType === MsgType.Video,
|
mx_ReplyTile_video: msgType === MsgType.Video,
|
||||||
|
|
|
@ -99,7 +99,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
||||||
// again and this time we want to show the newest breadcrumb because it'll be hidden
|
// again and this time we want to show the newest breadcrumb because it'll be hidden
|
||||||
// off screen for the animation.
|
// off screen for the animation.
|
||||||
this.setState({ doAnimation: false, skipFirst: true });
|
this.setState({ doAnimation: false, skipFirst: true });
|
||||||
setTimeout(() => this.setState({ doAnimation: true, skipFirst: false }), 0);
|
window.setTimeout(() => this.setState({ doAnimation: true, skipFirst: false }), 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
private viewRoom = (room: Room, index: number, viaKeyboard = false) => {
|
private viewRoom = (room: Room, index: number, viaKeyboard = false) => {
|
||||||
|
|
|
@ -385,7 +385,7 @@ const CallLayoutSelector: FC<CallLayoutSelectorProps> = ({ call }) => {
|
||||||
"mx_RoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
|
"mx_RoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
|
||||||
})}
|
})}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
title={_t("Layout type")}
|
title={_t("Change layout")}
|
||||||
alignment={Alignment.Bottom}
|
alignment={Alignment.Bottom}
|
||||||
key="layout"
|
key="layout"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -747,13 +747,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
public render(): React.ReactElement {
|
public render(): React.ReactElement {
|
||||||
const visibleTiles = this.renderVisibleTiles();
|
const visibleTiles = this.renderVisibleTiles();
|
||||||
|
const hidden = !this.state.rooms.length && !this.props.extraTiles?.length && this.props.alwaysVisible !== true;
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
'mx_RoomSublist': true,
|
'mx_RoomSublist': true,
|
||||||
'mx_RoomSublist_hasMenuOpen': !!this.state.contextMenuPosition,
|
'mx_RoomSublist_hasMenuOpen': !!this.state.contextMenuPosition,
|
||||||
'mx_RoomSublist_minimized': this.props.isMinimized,
|
'mx_RoomSublist_minimized': this.props.isMinimized,
|
||||||
'mx_RoomSublist_hidden': (
|
'mx_RoomSublist_hidden': hidden,
|
||||||
!this.state.rooms.length && !this.props.extraTiles?.length && this.props.alwaysVisible !== true
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let content = null;
|
let content = null;
|
||||||
|
@ -898,6 +897,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
ref={this.sublistRef}
|
ref={this.sublistRef}
|
||||||
className={classes}
|
className={classes}
|
||||||
role="group"
|
role="group"
|
||||||
|
aria-hidden={hidden}
|
||||||
aria-label={this.props.label}
|
aria-label={this.props.label}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
>
|
>
|
||||||
|
|
|
@ -28,7 +28,7 @@ export function useIsFocused() {
|
||||||
} else {
|
} else {
|
||||||
// To avoid a blink when we switch mode between plain text and rich text mode
|
// To avoid a blink when we switch mode between plain text and rich text mode
|
||||||
// We delay the unfocused action
|
// We delay the unfocused action
|
||||||
timeoutIDRef.current = setTimeout(() => setIsFocused(false), 100);
|
timeoutIDRef.current = window.setTimeout(() => setIsFocused(false), 100);
|
||||||
}
|
}
|
||||||
}, [setIsFocused, timeoutIDRef]);
|
}, [setIsFocused, timeoutIDRef]);
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ export function focusComposer(
|
||||||
if (timeoutId.current) {
|
if (timeoutId.current) {
|
||||||
clearTimeout(timeoutId.current);
|
clearTimeout(timeoutId.current);
|
||||||
}
|
}
|
||||||
timeoutId.current = setTimeout(
|
timeoutId.current = window.setTimeout(
|
||||||
() => composerElement.current?.focus(),
|
() => composerElement.current?.focus(),
|
||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
@ -66,11 +65,6 @@ interface IBridgeStateEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class BridgeTile extends React.PureComponent<IProps> {
|
export default class BridgeTile extends React.PureComponent<IProps> {
|
||||||
static propTypes = {
|
|
||||||
ev: PropTypes.object.isRequired,
|
|
||||||
room: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const content: IBridgeStateEvent = this.props.ev.getContent();
|
const content: IBridgeStateEvent = this.props.ev.getContent();
|
||||||
// Validate
|
// Validate
|
||||||
|
|
|
@ -150,7 +150,7 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
|
||||||
await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
|
await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
|
||||||
this.setState({ customThemeUrl: "", customThemeMessage: { text: _t("Theme added!"), isError: false } });
|
this.setState({ customThemeUrl: "", customThemeMessage: { text: _t("Theme added!"), isError: false } });
|
||||||
|
|
||||||
this.themeTimer = setTimeout(() => {
|
this.themeTimer = window.setTimeout(() => {
|
||||||
this.setState({ customThemeMessage: { text: "", isError: false } });
|
this.setState({ customThemeMessage: { text: "", isError: false } });
|
||||||
}, 3000);
|
}, 3000);
|
||||||
};
|
};
|
||||||
|
|
|
@ -64,12 +64,13 @@ const isDeviceSelected = (
|
||||||
) => selectedDeviceIds.includes(deviceId);
|
) => selectedDeviceIds.includes(deviceId);
|
||||||
|
|
||||||
// devices without timestamp metadata should be sorted last
|
// devices without timestamp metadata should be sorted last
|
||||||
const sortDevicesByLatestActivity = (left: ExtendedDevice, right: ExtendedDevice) =>
|
const sortDevicesByLatestActivityThenDisplayName = (left: ExtendedDevice, right: ExtendedDevice) =>
|
||||||
(right.last_seen_ts || 0) - (left.last_seen_ts || 0);
|
(right.last_seen_ts || 0) - (left.last_seen_ts || 0)
|
||||||
|
|| ((left.display_name || left.device_id).localeCompare(right.display_name || right.device_id));
|
||||||
|
|
||||||
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSecurityVariation) =>
|
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSecurityVariation) =>
|
||||||
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : [])
|
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : [])
|
||||||
.sort(sortDevicesByLatestActivity);
|
.sort(sortDevicesByLatestActivityThenDisplayName);
|
||||||
|
|
||||||
const ALL_FILTER_ID = 'ALL';
|
const ALL_FILTER_ID = 'ALL';
|
||||||
type DeviceFilterKey = DeviceSecurityVariation | typeof ALL_FILTER_ID;
|
type DeviceFilterKey = DeviceSecurityVariation | typeof ALL_FILTER_ID;
|
||||||
|
|
|
@ -324,12 +324,13 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||||
let privilegedUsersSection = <div>{ _t('No users have specific privileges in this room') }</div>;
|
let privilegedUsersSection = <div>{ _t('No users have specific privileges in this room') }</div>;
|
||||||
let mutedUsersSection;
|
let mutedUsersSection;
|
||||||
if (Object.keys(userLevels).length) {
|
if (Object.keys(userLevels).length) {
|
||||||
const privilegedUsers = [];
|
const privilegedUsers: JSX.Element[] = [];
|
||||||
const mutedUsers = [];
|
const mutedUsers: JSX.Element[] = [];
|
||||||
|
|
||||||
Object.keys(userLevels).forEach((user) => {
|
Object.keys(userLevels).forEach((user) => {
|
||||||
if (!Number.isInteger(userLevels[user])) { return; }
|
if (!Number.isInteger(userLevels[user])) return;
|
||||||
const canChange = userLevels[user] < currentUserLevel && canChangeLevels;
|
const isMe = user === client.getUserId();
|
||||||
|
const canChange = canChangeLevels && (userLevels[user] < currentUserLevel || isMe);
|
||||||
if (userLevels[user] > defaultUserLevel) { // privileged
|
if (userLevels[user] > defaultUserLevel) { // privileged
|
||||||
privilegedUsers.push(
|
privilegedUsers.push(
|
||||||
<PowerSelector
|
<PowerSelector
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { sortBy } from "lodash";
|
||||||
|
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import SdkConfig from "../../../../../SdkConfig";
|
import SdkConfig from "../../../../../SdkConfig";
|
||||||
import BetaCard from "../../../beta/BetaCard";
|
import BetaCard from "../../../beta/BetaCard";
|
||||||
|
@ -28,24 +27,6 @@ import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
|
||||||
import { LabGroup, labGroupNames } from "../../../../../settings/Settings";
|
import { LabGroup, labGroupNames } from "../../../../../settings/Settings";
|
||||||
import { EnhancedMap } from "../../../../../utils/maps";
|
import { EnhancedMap } from "../../../../../utils/maps";
|
||||||
|
|
||||||
interface ILabsSettingToggleProps {
|
|
||||||
featureId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LabsSettingToggle extends React.Component<ILabsSettingToggleProps> {
|
|
||||||
private onChange = async (checked: boolean): Promise<void> => {
|
|
||||||
await SettingsStore.setValue(this.props.featureId, null, SettingLevel.DEVICE, checked);
|
|
||||||
this.forceUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
const label = SettingsStore.getDisplayName(this.props.featureId);
|
|
||||||
const value = SettingsStore.getValue(this.props.featureId);
|
|
||||||
const canChange = SettingsStore.canSetValue(this.props.featureId, null, SettingLevel.DEVICE);
|
|
||||||
return <LabelledToggleSwitch value={value} label={label} onChange={this.onChange} disabled={!canChange} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
showJumpToDate: boolean;
|
showJumpToDate: boolean;
|
||||||
showExploringPublicSpaces: boolean;
|
showExploringPublicSpaces: boolean;
|
||||||
|
@ -93,7 +74,7 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
|
||||||
const groups = new EnhancedMap<LabGroup, JSX.Element[]>();
|
const groups = new EnhancedMap<LabGroup, JSX.Element[]>();
|
||||||
labs.forEach(f => {
|
labs.forEach(f => {
|
||||||
groups.getOrCreate(SettingsStore.getLabGroup(f), []).push(
|
groups.getOrCreate(SettingsStore.getLabGroup(f), []).push(
|
||||||
<LabsSettingToggle featureId={f} key={f} />,
|
<SettingsFlag level={SettingLevel.DEVICE} name={f} key={f} />,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -154,12 +135,30 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab mx_LabsUserSettingsTab">
|
<div className="mx_SettingsTab mx_LabsUserSettingsTab">
|
||||||
<div className="mx_SettingsTab_heading">{ _t("Labs") }</div>
|
<div className="mx_SettingsTab_heading">{ _t("Upcoming features") }</div>
|
||||||
<div className='mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_subsectionText'>
|
||||||
{
|
{
|
||||||
_t('Feeling experimental? Labs are the best way to get things early, ' +
|
_t(
|
||||||
'test out new features and help shape them before they actually launch. ' +
|
"What's next for %(brand)s? "
|
||||||
'<a>Learn more</a>.', {}, {
|
+ "Labs are the best way to get things early, "
|
||||||
|
+ "test out new features and help shape them before they actually launch.",
|
||||||
|
{ brand: SdkConfig.get("brand") },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{ betaSection }
|
||||||
|
{ labsSections && <>
|
||||||
|
<div className="mx_SettingsTab_heading">{ _t("Early previews") }</div>
|
||||||
|
<div className='mx_SettingsTab_subsectionText'>
|
||||||
|
{
|
||||||
|
_t(
|
||||||
|
"Feeling experimental? "
|
||||||
|
+ "Try out our latest ideas in development. "
|
||||||
|
+ "These features are not finalised; "
|
||||||
|
+ "they may be unstable, may change, or may be dropped altogether. "
|
||||||
|
+ "<a>Learn more</a>.",
|
||||||
|
{},
|
||||||
|
{
|
||||||
'a': (sub) => {
|
'a': (sub) => {
|
||||||
return <a
|
return <a
|
||||||
href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
|
href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
|
||||||
|
@ -170,8 +169,8 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{ betaSection }
|
|
||||||
{ labsSections }
|
{ labsSections }
|
||||||
|
</> }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,7 @@ const SessionManagerTab: React.FC = () => {
|
||||||
const [expandedDeviceIds, setExpandedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
const [expandedDeviceIds, setExpandedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
const [selectedDeviceIds, setSelectedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||||
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
|
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollIntoViewTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
const scrollIntoViewTimeoutRef = useRef<number>();
|
||||||
|
|
||||||
const matrixClient = useContext(MatrixClientContext);
|
const matrixClient = useContext(MatrixClientContext);
|
||||||
const userId = matrixClient.getUserId();
|
const userId = matrixClient.getUserId();
|
||||||
|
|
|
@ -57,7 +57,7 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const { request } = this.props;
|
const { request } = this.props;
|
||||||
if (request.timeout && request.timeout > 0) {
|
if (request.timeout && request.timeout > 0) {
|
||||||
this.intervalHandle = setInterval(() => {
|
this.intervalHandle = window.setInterval(() => {
|
||||||
let { counter } = this.state;
|
let { counter } = this.state;
|
||||||
counter = Math.max(0, counter - 1);
|
counter = Math.max(0, counter - 1);
|
||||||
this.setState({ counter });
|
this.setState({ counter });
|
||||||
|
|
|
@ -55,7 +55,7 @@ export function UserOnboardingPage({ justRegistered = false }: Props) {
|
||||||
const [showList, setShowList] = useState<boolean>(false);
|
const [showList, setShowList] = useState<boolean>(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialSyncComplete) {
|
if (initialSyncComplete) {
|
||||||
let handler: number | null = setTimeout(() => {
|
let handler: number | null = window.setTimeout(() => {
|
||||||
handler = null;
|
handler = null;
|
||||||
setShowList(true);
|
setShowList(true);
|
||||||
}, ANIMATION_DURATION);
|
}, ANIMATION_DURATION);
|
||||||
|
|
|
@ -43,7 +43,7 @@ interface GroupCallDurationProps {
|
||||||
export const GroupCallDuration: FC<GroupCallDurationProps> = ({ groupCall }) => {
|
export const GroupCallDuration: FC<GroupCallDurationProps> = ({ groupCall }) => {
|
||||||
const [now, setNow] = useState(() => Date.now());
|
const [now, setNow] = useState(() => Date.now());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => setNow(Date.now()), 1000);
|
const timer = window.setInterval(() => setNow(Date.now()), 1000);
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -367,14 +367,14 @@ class PipView extends React.Component<IProps, IState> {
|
||||||
const pipMode = true;
|
const pipMode = true;
|
||||||
let pipContent: CreatePipChildren | null = null;
|
let pipContent: CreatePipChildren | null = null;
|
||||||
|
|
||||||
if (this.props.voiceBroadcastPreRecording) {
|
|
||||||
pipContent = this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.voiceBroadcastPlayback) {
|
if (this.props.voiceBroadcastPlayback) {
|
||||||
pipContent = this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback);
|
pipContent = this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.voiceBroadcastPreRecording) {
|
||||||
|
pipContent = this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.voiceBroadcastRecording) {
|
if (this.props.voiceBroadcastRecording) {
|
||||||
pipContent = this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording);
|
pipContent = this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording);
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ export class MatrixDispatcher extends Dispatcher<ActionPayload> {
|
||||||
// if you dispatch from within a dispatch, so rather than action
|
// if you dispatch from within a dispatch, so rather than action
|
||||||
// handlers having to worry about not calling anything that might
|
// handlers having to worry about not calling anything that might
|
||||||
// then dispatch, we just do dispatches asynchronously.
|
// then dispatch, we just do dispatches asynchronously.
|
||||||
setTimeout(super.dispatch.bind(this, payload), 0);
|
window.setTimeout(super.dispatch.bind(this, payload), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AllHtmlEntities } from 'html-entities';
|
import { encode } from 'html-entities';
|
||||||
import cheerio from 'cheerio';
|
import cheerio from 'cheerio';
|
||||||
import escapeHtml from "escape-html";
|
import escapeHtml from "escape-html";
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ export function htmlSerializeFromMdIfNeeded(md: string, { forceHTML = false } =
|
||||||
patternDefaults[patternName][patternType];
|
patternDefaults[patternName][patternType];
|
||||||
|
|
||||||
md = md.replace(RegExp(pattern, "gms"), function(m, p1, p2) {
|
md = md.replace(RegExp(pattern, "gms"), function(m, p1, p2) {
|
||||||
const p2e = AllHtmlEntities.encode(p2);
|
const p2e = encode(p2);
|
||||||
switch (patternType) {
|
switch (patternType) {
|
||||||
case "display":
|
case "display":
|
||||||
return `${p1}<div data-mx-maths="${p2e}">\n\n</div>\n\n`;
|
return `${p1}<div data-mx-maths="${p2e}">\n\n</div>\n\n`;
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { M_POLL_START, Optional } from "matrix-events-sdk";
|
import { M_POLL_START, Optional } from "matrix-events-sdk";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { GroupCallIntent } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
|
||||||
import EditorStateTransfer from "../utils/EditorStateTransfer";
|
import EditorStateTransfer from "../utils/EditorStateTransfer";
|
||||||
import { RoomPermalinkCreator } from "../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../utils/permalinks/Permalinks";
|
||||||
|
@ -412,13 +413,9 @@ export function haveRendererForEvent(mxEvent: MatrixEvent, showHiddenEvents: boo
|
||||||
return Boolean(mxEvent.getContent()['predecessor']);
|
return Boolean(mxEvent.getContent()['predecessor']);
|
||||||
} else if (ElementCall.CALL_EVENT_TYPE.names.some(eventType => handler === STATE_EVENT_TILE_TYPES.get(eventType))) {
|
} else if (ElementCall.CALL_EVENT_TYPE.names.some(eventType => handler === STATE_EVENT_TILE_TYPES.get(eventType))) {
|
||||||
const intent = mxEvent.getContent()['m.intent'];
|
const intent = mxEvent.getContent()['m.intent'];
|
||||||
const prevContent = mxEvent.getPrevContent();
|
const newlyStarted = Object.keys(mxEvent.getPrevContent()).length === 0;
|
||||||
// If the call became unterminated or previously had invalid contents,
|
|
||||||
// then this event marks the start of the call
|
|
||||||
const newlyStarted = 'm.terminated' in prevContent
|
|
||||||
|| !('m.intent' in prevContent) || !('m.type' in prevContent);
|
|
||||||
// Only interested in events that mark the start of a non-room call
|
// Only interested in events that mark the start of a non-room call
|
||||||
return typeof intent === 'string' && intent !== 'm.room' && newlyStarted;
|
return newlyStarted && typeof intent === 'string' && intent !== GroupCallIntent.Room;
|
||||||
} else if (handler === JSONEventFactory) {
|
} else if (handler === JSONEventFactory) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -30,7 +30,7 @@ export function useDebouncedCallback<T extends any[]>(
|
||||||
callback(...params);
|
callback(...params);
|
||||||
};
|
};
|
||||||
if (enabled !== false) {
|
if (enabled !== false) {
|
||||||
handle = setTimeout(doSearch, DEBOUNCE_TIMEOUT);
|
handle = window.setTimeout(doSearch, DEBOUNCE_TIMEOUT);
|
||||||
return () => {
|
return () => {
|
||||||
if (handle) {
|
if (handle) {
|
||||||
clearTimeout(handle);
|
clearTimeout(handle);
|
||||||
|
|
|
@ -30,7 +30,7 @@ export const useTimeout = (handler: Handler, timeoutMs: number) => {
|
||||||
|
|
||||||
// Set up timer
|
// Set up timer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeoutID = setTimeout(() => {
|
const timeoutID = window.setTimeout(() => {
|
||||||
savedHandler.current();
|
savedHandler.current();
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
return () => clearTimeout(timeoutID);
|
return () => clearTimeout(timeoutID);
|
||||||
|
@ -49,7 +49,7 @@ export const useInterval = (handler: Handler, intervalMs: number) => {
|
||||||
|
|
||||||
// Set up timer
|
// Set up timer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const intervalID = setInterval(() => {
|
const intervalID = window.setInterval(() => {
|
||||||
savedHandler.current();
|
savedHandler.current();
|
||||||
}, intervalMs);
|
}, intervalMs);
|
||||||
return () => clearInterval(intervalID);
|
return () => clearInterval(intervalID);
|
||||||
|
|
|
@ -28,7 +28,7 @@ export const useTimeoutToggle = (defaultValue: boolean, timeoutMs: number) => {
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
setValue(!defaultValue);
|
setValue(!defaultValue);
|
||||||
timeoutId.current = setTimeout(() => setValue(defaultValue), timeoutMs);
|
timeoutId.current = window.setTimeout(() => setValue(defaultValue), timeoutMs);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -68,7 +68,7 @@ function useUserOnboardingContextValue<T>(defaultValue: T, callback: (cli: Matri
|
||||||
}
|
}
|
||||||
setValue(await handler(cli));
|
setValue(await handler(cli));
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
handle = setTimeout(repeater, USER_ONBOARDING_CONTEXT_INTERVAL);
|
handle = window.setTimeout(repeater, USER_ONBOARDING_CONTEXT_INTERVAL);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
repeater().catch(err => logger.warn("could not update user onboarding context", err));
|
repeater().catch(err => logger.warn("could not update user onboarding context", err));
|
||||||
|
|
|
@ -660,6 +660,7 @@
|
||||||
"Change input device": "Change input device",
|
"Change input device": "Change input device",
|
||||||
"Live": "Live",
|
"Live": "Live",
|
||||||
"Voice broadcast": "Voice broadcast",
|
"Voice broadcast": "Voice broadcast",
|
||||||
|
"Buffering…": "Buffering…",
|
||||||
"Cannot reach homeserver": "Cannot reach homeserver",
|
"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",
|
"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",
|
||||||
"Your %(brand)s is misconfigured": "Your %(brand)s is misconfigured",
|
"Your %(brand)s is misconfigured": "Your %(brand)s is misconfigured",
|
||||||
|
@ -808,7 +809,7 @@
|
||||||
"Yes": "Yes",
|
"Yes": "Yes",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"Help improve %(analyticsOwner)s": "Help improve %(analyticsOwner)s",
|
"Help improve %(analyticsOwner)s": "Help improve %(analyticsOwner)s",
|
||||||
"You have unverified logins": "You have unverified logins",
|
"You have unverified sessions": "You have unverified sessions",
|
||||||
"Review to ensure your account is safe": "Review to ensure your account is safe",
|
"Review to ensure your account is safe": "Review to ensure your account is safe",
|
||||||
"Review": "Review",
|
"Review": "Review",
|
||||||
"Later": "Later",
|
"Later": "Later",
|
||||||
|
@ -908,7 +909,8 @@
|
||||||
"Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.",
|
"Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.",
|
||||||
"Explore public spaces in the new search dialog": "Explore public spaces in the new search dialog",
|
"Explore public spaces in the new search dialog": "Explore public spaces in the new search dialog",
|
||||||
"Let moderators hide messages pending moderation.": "Let moderators hide messages pending moderation.",
|
"Let moderators hide messages pending moderation.": "Let moderators hide messages pending moderation.",
|
||||||
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
|
"Report to moderators": "Report to moderators",
|
||||||
|
"In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
|
||||||
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
|
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
|
||||||
"Message Pinning": "Message Pinning",
|
"Message Pinning": "Message Pinning",
|
||||||
"Threaded messaging": "Threaded messaging",
|
"Threaded messaging": "Threaded messaging",
|
||||||
|
@ -920,9 +922,11 @@
|
||||||
"How can I leave the beta?": "How can I leave the beta?",
|
"How can I leave the beta?": "How can I leave the beta?",
|
||||||
"To leave, return to this page and use the “%(leaveTheBeta)s” button.": "To leave, return to this page and use the “%(leaveTheBeta)s” button.",
|
"To leave, return to this page and use the “%(leaveTheBeta)s” button.": "To leave, return to this page and use the “%(leaveTheBeta)s” button.",
|
||||||
"Leave the beta": "Leave the beta",
|
"Leave the beta": "Leave the beta",
|
||||||
"Try out the rich text editor (plain text mode coming soon)": "Try out the rich text editor (plain text mode coming soon)",
|
"Rich text editor": "Rich text editor",
|
||||||
|
"Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.",
|
||||||
"Render simple counters in room header": "Render simple counters in room header",
|
"Render simple counters in room header": "Render simple counters in room header",
|
||||||
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
|
"New ways to ignore people": "New ways to ignore people",
|
||||||
|
"Currently experimental.": "Currently experimental.",
|
||||||
"Support adding custom themes": "Support adding custom themes",
|
"Support adding custom themes": "Support adding custom themes",
|
||||||
"Show message previews for reactions in DMs": "Show message previews for reactions in DMs",
|
"Show message previews for reactions in DMs": "Show message previews for reactions in DMs",
|
||||||
"Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms",
|
"Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms",
|
||||||
|
@ -932,15 +936,19 @@
|
||||||
"Show HTML representation of room topics": "Show HTML representation of room topics",
|
"Show HTML representation of room topics": "Show HTML representation of room topics",
|
||||||
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||||
"Use new room breadcrumbs": "Use new room breadcrumbs",
|
"Use new room breadcrumbs": "Use new room breadcrumbs",
|
||||||
"Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)",
|
"Right panel stays open": "Right panel stays open",
|
||||||
|
"Defaults to room member list.": "Defaults to room member list.",
|
||||||
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
||||||
"Send read receipts": "Send read receipts",
|
"Send read receipts": "Send read receipts",
|
||||||
"Sliding Sync mode (under active development, cannot be disabled)": "Sliding Sync mode (under active development, cannot be disabled)",
|
"Sliding Sync mode": "Sliding Sync mode",
|
||||||
|
"Under active development, cannot be disabled.": "Under active development, cannot be disabled.",
|
||||||
"Element Call video rooms": "Element Call video rooms",
|
"Element Call video rooms": "Element Call video rooms",
|
||||||
"New group call experience": "New group call experience",
|
"New group call experience": "New group call experience",
|
||||||
"Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)",
|
"Live Location Sharing": "Live Location Sharing",
|
||||||
"Favourite Messages (under active development)": "Favourite Messages (under active development)",
|
"Temporary implementation. Locations persist in room history.": "Temporary implementation. Locations persist in room history.",
|
||||||
"Voice broadcast (under active development)": "Voice broadcast (under active development)",
|
"Favourite Messages": "Favourite Messages",
|
||||||
|
"Under active development.": "Under active development.",
|
||||||
|
"Under active development": "Under active development",
|
||||||
"Use new session manager": "Use new session manager",
|
"Use new session manager": "Use new session manager",
|
||||||
"New session manager": "New session manager",
|
"New session manager": "New session manager",
|
||||||
"Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.",
|
"Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.",
|
||||||
|
@ -1001,7 +1009,8 @@
|
||||||
"Show shortcuts to recently viewed rooms above the room list": "Show shortcuts to recently viewed rooms above the room list",
|
"Show shortcuts to recently viewed rooms above the room list": "Show shortcuts to recently viewed rooms above the room list",
|
||||||
"Show shortcut to welcome checklist above the room list": "Show shortcut to welcome checklist above the room list",
|
"Show shortcut to welcome checklist above the room list": "Show shortcut to welcome checklist above the room list",
|
||||||
"Show hidden events in timeline": "Show hidden events in timeline",
|
"Show hidden events in timeline": "Show hidden events in timeline",
|
||||||
"Low bandwidth mode (requires compatible homeserver)": "Low bandwidth mode (requires compatible homeserver)",
|
"Low bandwidth mode": "Low bandwidth mode",
|
||||||
|
"Requires compatible homeserver.": "Requires compatible homeserver.",
|
||||||
"Allow fallback call assist server (turn.matrix.org)": "Allow fallback call assist server (turn.matrix.org)",
|
"Allow fallback call assist server (turn.matrix.org)": "Allow fallback call assist server (turn.matrix.org)",
|
||||||
"Only applies if your homeserver does not offer one. Your IP address would be shared during a call.": "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.",
|
"Only applies if your homeserver does not offer one. Your IP address would be shared during a call.": "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.",
|
||||||
"Show previews/thumbnails for images": "Show previews/thumbnails for images",
|
"Show previews/thumbnails for images": "Show previews/thumbnails for images",
|
||||||
|
@ -1539,8 +1548,10 @@
|
||||||
"Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.",
|
"Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.",
|
||||||
"Clear cache and reload": "Clear cache and reload",
|
"Clear cache and reload": "Clear cache and reload",
|
||||||
"Keyboard": "Keyboard",
|
"Keyboard": "Keyboard",
|
||||||
"Labs": "Labs",
|
"Upcoming features": "Upcoming features",
|
||||||
"Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.",
|
"What's next for %(brand)s? Labs are the best way to get things early, test out new features and help shape them before they actually launch.": "What's next for %(brand)s? Labs are the best way to get things early, test out new features and help shape them before they actually launch.",
|
||||||
|
"Early previews": "Early previews",
|
||||||
|
"Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. <a>Learn more</a>.": "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. <a>Learn more</a>.",
|
||||||
"Ignored/Blocked": "Ignored/Blocked",
|
"Ignored/Blocked": "Ignored/Blocked",
|
||||||
"Error adding ignored user/server": "Error adding ignored user/server",
|
"Error adding ignored user/server": "Error adding ignored user/server",
|
||||||
"Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.",
|
"Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.",
|
||||||
|
@ -1950,7 +1961,7 @@
|
||||||
"You do not have permission to start voice calls": "You do not have permission to start voice calls",
|
"You do not have permission to start voice calls": "You do not have permission to start voice calls",
|
||||||
"Freedom": "Freedom",
|
"Freedom": "Freedom",
|
||||||
"Spotlight": "Spotlight",
|
"Spotlight": "Spotlight",
|
||||||
"Layout type": "Layout type",
|
"Change layout": "Change layout",
|
||||||
"Forget room": "Forget room",
|
"Forget room": "Forget room",
|
||||||
"Hide Widgets": "Hide Widgets",
|
"Hide Widgets": "Hide Widgets",
|
||||||
"Show Widgets": "Show Widgets",
|
"Show Widgets": "Show Widgets",
|
||||||
|
@ -2151,6 +2162,8 @@
|
||||||
"The homeserver the user you're verifying is connected to": "The homeserver the user you're verifying is connected to",
|
"The homeserver the user you're verifying is connected to": "The homeserver the user you're verifying is connected to",
|
||||||
"Yours, or the other users' internet connection": "Yours, or the other users' internet connection",
|
"Yours, or the other users' internet connection": "Yours, or the other users' internet connection",
|
||||||
"Yours, or the other users' session": "Yours, or the other users' session",
|
"Yours, or the other users' session": "Yours, or the other users' session",
|
||||||
|
"Error starting verification": "Error starting verification",
|
||||||
|
"We were unable to start a chat with the other user.": "We were unable to start a chat with the other user.",
|
||||||
"Nothing pinned, yet": "Nothing pinned, yet",
|
"Nothing pinned, yet": "Nothing pinned, yet",
|
||||||
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
||||||
"Pinned messages": "Pinned messages",
|
"Pinned messages": "Pinned messages",
|
||||||
|
@ -2560,6 +2573,7 @@
|
||||||
"Join millions for free on the largest public server": "Join millions for free on the largest public server",
|
"Join millions for free on the largest public server": "Join millions for free on the largest public server",
|
||||||
"Homeserver": "Homeserver",
|
"Homeserver": "Homeserver",
|
||||||
"Help": "Help",
|
"Help": "Help",
|
||||||
|
"<w>WARNING:</w> <description/>": "<w>WARNING:</w> <description/>",
|
||||||
"Choose a locale": "Choose a locale",
|
"Choose a locale": "Choose a locale",
|
||||||
"Continue with %(provider)s": "Continue with %(provider)s",
|
"Continue with %(provider)s": "Continue with %(provider)s",
|
||||||
"Sign in with single sign-on": "Sign in with single sign-on",
|
"Sign in with single sign-on": "Sign in with single sign-on",
|
||||||
|
@ -2992,6 +3006,7 @@
|
||||||
"Upload %(count)s other files|one": "Upload %(count)s other file",
|
"Upload %(count)s other files|one": "Upload %(count)s other file",
|
||||||
"Cancel All": "Cancel All",
|
"Cancel All": "Cancel All",
|
||||||
"Upload Error": "Upload Error",
|
"Upload Error": "Upload Error",
|
||||||
|
"Labs": "Labs",
|
||||||
"Verify other device": "Verify other device",
|
"Verify other device": "Verify other device",
|
||||||
"Verification Request": "Verification Request",
|
"Verification Request": "Verification Request",
|
||||||
"Approve widget permissions": "Approve widget permissions",
|
"Approve widget permissions": "Approve widget permissions",
|
||||||
|
|
|
@ -377,7 +377,7 @@ export class JitsiCall extends Call {
|
||||||
|
|
||||||
this.participants = participants;
|
this.participants = participants;
|
||||||
if (allExpireAt < Infinity) {
|
if (allExpireAt < Infinity) {
|
||||||
this.participantsExpirationTimer = setTimeout(() => this.updateParticipants(), allExpireAt - now);
|
this.participantsExpirationTimer = window.setTimeout(() => this.updateParticipants(), allExpireAt - now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -553,7 +553,7 @@ export class JitsiCall extends Call {
|
||||||
// Tell others that we're connected, by adding our device to room state
|
// Tell others that we're connected, by adding our device to room state
|
||||||
await this.addOurDevice();
|
await this.addOurDevice();
|
||||||
// Re-add this device every so often so our video member event doesn't become stale
|
// Re-add this device every so often so our video member event doesn't become stale
|
||||||
this.resendDevicesTimer = setInterval(async () => {
|
this.resendDevicesTimer = window.setInterval(async () => {
|
||||||
logger.log(`Resending video member event for ${this.roomId}`);
|
logger.log(`Resending video member event for ${this.roomId}`);
|
||||||
await this.addOurDevice();
|
await this.addOurDevice();
|
||||||
}, (this.STUCK_DEVICE_TIMEOUT_MS * 3) / 4);
|
}, (this.STUCK_DEVICE_TIMEOUT_MS * 3) / 4);
|
||||||
|
@ -647,7 +647,6 @@ export class ElementCall extends Call {
|
||||||
client,
|
client,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.on(CallEvent.ConnectionState, this.onConnectionState);
|
|
||||||
this.on(CallEvent.Participants, this.onParticipants);
|
this.on(CallEvent.Participants, this.onParticipants);
|
||||||
groupCall.on(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants);
|
groupCall.on(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants);
|
||||||
groupCall.on(GroupCallEvent.GroupCallStateChanged, this.onGroupCallState);
|
groupCall.on(GroupCallEvent.GroupCallStateChanged, this.onGroupCallState);
|
||||||
|
@ -704,6 +703,7 @@ export class ElementCall extends Call {
|
||||||
throw new Error(`Failed to join call in room ${this.roomId}: ${e}`);
|
throw new Error(`Failed to join call in room ${this.roomId}: ${e}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.groupCall.enteredViaAnotherSession = true;
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||||
|
@ -724,11 +724,11 @@ export class ElementCall extends Call {
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.ScreenshareRequest}`, this.onScreenshareRequest);
|
this.messaging!.off(`action:${ElementWidgetActions.ScreenshareRequest}`, this.onScreenshareRequest);
|
||||||
super.setDisconnected();
|
super.setDisconnected();
|
||||||
|
this.groupCall.enteredViaAnotherSession = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
WidgetStore.instance.removeVirtualWidget(this.widget.id, this.groupCall.room.roomId);
|
WidgetStore.instance.removeVirtualWidget(this.widget.id, this.groupCall.room.roomId);
|
||||||
this.off(CallEvent.ConnectionState, this.onConnectionState);
|
|
||||||
this.off(CallEvent.Participants, this.onParticipants);
|
this.off(CallEvent.Participants, this.onParticipants);
|
||||||
this.groupCall.off(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants);
|
this.groupCall.off(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants);
|
||||||
this.groupCall.off(GroupCallEvent.GroupCallStateChanged, this.onGroupCallState);
|
this.groupCall.off(GroupCallEvent.GroupCallStateChanged, this.onGroupCallState);
|
||||||
|
@ -760,20 +760,6 @@ export class ElementCall extends Call {
|
||||||
participants.set(member, new Set(deviceMap.keys()));
|
participants.set(member, new Set(deviceMap.keys()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// We never enter group calls natively, so the GroupCall will think it's
|
|
||||||
// disconnected regardless of what our call member state says. Thus we
|
|
||||||
// have to insert our own device manually when connected via the widget.
|
|
||||||
if (this.connected) {
|
|
||||||
const localMember = this.room.getMember(this.client.getUserId()!)!;
|
|
||||||
let devices = participants.get(localMember);
|
|
||||||
if (devices === undefined) {
|
|
||||||
devices = new Set();
|
|
||||||
participants.set(localMember, devices);
|
|
||||||
}
|
|
||||||
|
|
||||||
devices.add(this.client.getDeviceId()!);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.participants = participants;
|
this.participants = participants;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -782,15 +768,6 @@ export class ElementCall extends Call {
|
||||||
&& this.room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, this.client);
|
&& this.room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, this.client);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onConnectionState = (state: ConnectionState, prevState: ConnectionState) => {
|
|
||||||
if (
|
|
||||||
(state === ConnectionState.Connected && !isConnected(prevState))
|
|
||||||
|| (state === ConnectionState.Disconnected && isConnected(prevState))
|
|
||||||
) {
|
|
||||||
this.updateParticipants(); // Local echo
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onParticipants = async (
|
private onParticipants = async (
|
||||||
participants: Map<RoomMember, Set<string>>,
|
participants: Map<RoomMember, Set<string>>,
|
||||||
prevParticipants: Map<RoomMember, Set<string>>,
|
prevParticipants: Map<RoomMember, Set<string>>,
|
||||||
|
@ -814,7 +791,7 @@ export class ElementCall extends Call {
|
||||||
// randomly between 2 and 8 seconds before terminating the call, to
|
// randomly between 2 and 8 seconds before terminating the call, to
|
||||||
// probabilistically reduce event spam. If someone else beats us to it,
|
// probabilistically reduce event spam. If someone else beats us to it,
|
||||||
// this timer will be automatically cleared upon the call's destruction.
|
// this timer will be automatically cleared upon the call's destruction.
|
||||||
this.terminationTimer = setTimeout(
|
this.terminationTimer = window.setTimeout(
|
||||||
() => this.groupCall.terminate(),
|
() => this.groupCall.terminate(),
|
||||||
Math.random() * 6000 + 2000,
|
Math.random() * 6000 + 2000,
|
||||||
);
|
);
|
||||||
|
|
|
@ -154,7 +154,7 @@ export class IndexedDBLogStore {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.db = event.target.result;
|
this.db = event.target.result;
|
||||||
// Periodically flush logs to local storage / indexeddb
|
// Periodically flush logs to local storage / indexeddb
|
||||||
setInterval(this.flush.bind(this), FLUSH_RATE_MS);
|
window.setInterval(this.flush.bind(this), FLUSH_RATE_MS);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -206,7 +206,7 @@ export async function initSentry(sentryConfig: IConfigOptions["sentry"]): Promis
|
||||||
new Sentry.Integrations.InboundFilters(),
|
new Sentry.Integrations.InboundFilters(),
|
||||||
new Sentry.Integrations.FunctionToString(),
|
new Sentry.Integrations.FunctionToString(),
|
||||||
new Sentry.Integrations.Breadcrumbs(),
|
new Sentry.Integrations.Breadcrumbs(),
|
||||||
new Sentry.Integrations.UserAgent(),
|
new Sentry.Integrations.HttpContext(),
|
||||||
new Sentry.Integrations.Dedupe(),
|
new Sentry.Integrations.Dedupe(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -122,13 +122,13 @@ export const labGroupNames: Record<LabGroup, string> = {
|
||||||
[LabGroup.Developer]: _td("Developer"),
|
[LabGroup.Developer]: _td("Developer"),
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SettingValueType = boolean |
|
export type SettingValueType = boolean
|
||||||
number |
|
| number
|
||||||
string |
|
| string
|
||||||
number[] |
|
| number[]
|
||||||
string[] |
|
| string[]
|
||||||
Record<string, unknown> |
|
| Record<string, unknown>
|
||||||
null;
|
| null;
|
||||||
|
|
||||||
export interface IBaseSetting<T extends SettingValueType = SettingValueType> {
|
export interface IBaseSetting<T extends SettingValueType = SettingValueType> {
|
||||||
isFeature?: false | undefined;
|
isFeature?: false | undefined;
|
||||||
|
@ -180,6 +180,9 @@ export interface IBaseSetting<T extends SettingValueType = SettingValueType> {
|
||||||
extraSettings?: string[];
|
extraSettings?: string[];
|
||||||
requiresRefresh?: boolean;
|
requiresRefresh?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Whether the setting should have a warning sign in the microcopy
|
||||||
|
shouldWarn?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFeature extends Omit<IBaseSetting<boolean>, "isFeature"> {
|
export interface IFeature extends Omit<IBaseSetting<boolean>, "isFeature"> {
|
||||||
|
@ -245,8 +248,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
"feature_report_to_moderators": {
|
"feature_report_to_moderators": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Moderation,
|
labsGroup: LabGroup.Moderation,
|
||||||
displayName: _td("Report to moderators prototype. " +
|
displayName: _td("Report to moderators"),
|
||||||
"In rooms that support moderation, the `report` button will let you report abuse to room moderators"),
|
description: _td(
|
||||||
|
"In rooms that support moderation, "
|
||||||
|
+"the “Report” button will let you report abuse to room moderators.",
|
||||||
|
),
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
@ -307,7 +313,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
"feature_wysiwyg_composer": {
|
"feature_wysiwyg_composer": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Messaging,
|
labsGroup: LabGroup.Messaging,
|
||||||
displayName: _td("Try out the rich text editor (plain text mode coming soon)"),
|
displayName: _td("Rich text editor"),
|
||||||
|
description: _td("Use rich text instead of Markdown in the message composer. Plain text mode coming soon."),
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
@ -321,7 +328,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
"feature_mjolnir": {
|
"feature_mjolnir": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Moderation,
|
labsGroup: LabGroup.Moderation,
|
||||||
displayName: _td("Try out new ways to ignore people (experimental)"),
|
displayName: _td("New ways to ignore people"),
|
||||||
|
description: _td("Currently experimental."),
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
@ -400,7 +408,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Rooms,
|
labsGroup: LabGroup.Rooms,
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
displayName: _td("Right panel stays open (defaults to room member list)"),
|
displayName: _td("Right panel stays open"),
|
||||||
|
description: _td("Defaults to room member list."),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
"feature_jump_to_date": {
|
"feature_jump_to_date": {
|
||||||
|
@ -425,7 +434,9 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Developer,
|
labsGroup: LabGroup.Developer,
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||||
displayName: _td('Sliding Sync mode (under active development, cannot be disabled)'),
|
displayName: _td('Sliding Sync mode'),
|
||||||
|
description: _td("Under active development, cannot be disabled."),
|
||||||
|
shouldWarn: true,
|
||||||
default: false,
|
default: false,
|
||||||
controller: new SlidingSyncController(),
|
controller: new SlidingSyncController(),
|
||||||
},
|
},
|
||||||
|
@ -453,23 +464,25 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Messaging,
|
labsGroup: LabGroup.Messaging,
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
displayName: _td(
|
displayName: _td("Live Location Sharing"),
|
||||||
"Live Location Sharing (temporary implementation: locations persist in room history)",
|
description: _td("Temporary implementation. Locations persist in room history."),
|
||||||
),
|
shouldWarn: true,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
"feature_favourite_messages": {
|
"feature_favourite_messages": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Messaging,
|
labsGroup: LabGroup.Messaging,
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
displayName: _td("Favourite Messages (under active development)"),
|
displayName: _td("Favourite Messages"),
|
||||||
|
description: _td("Under active development."),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
[Features.VoiceBroadcast]: {
|
[Features.VoiceBroadcast]: {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Messaging,
|
labsGroup: LabGroup.Messaging,
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
displayName: _td("Voice broadcast (under active development)"),
|
displayName: _td("Voice broadcast"),
|
||||||
|
description: _td("Under active development"),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
"feature_new_device_manager": {
|
"feature_new_device_manager": {
|
||||||
|
@ -910,9 +923,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
},
|
},
|
||||||
"lowBandwidth": {
|
"lowBandwidth": {
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||||
displayName: _td('Low bandwidth mode (requires compatible homeserver)'),
|
displayName: _td('Low bandwidth mode'),
|
||||||
|
description: _td("Requires compatible homeserver."),
|
||||||
default: false,
|
default: false,
|
||||||
controller: new ReloadOnChangeController(),
|
controller: new ReloadOnChangeController(),
|
||||||
|
shouldWarn: true,
|
||||||
},
|
},
|
||||||
"fallbackICEServerAllowed": {
|
"fallbackICEServerAllowed": {
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||||
|
@ -1056,6 +1071,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"debug_legacy_call_handler": {
|
||||||
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"audioInputMuted": {
|
"audioInputMuted": {
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -1130,6 +1149,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
supportedLevels: LEVELS_UI_FEATURE,
|
supportedLevels: LEVELS_UI_FEATURE,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
[UIFeature.BulkUnverifiedSessionsReminder]: {
|
||||||
|
supportedLevels: LEVELS_UI_FEATURE,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
// Electron-specific settings, they are stored by Electron and set/read over an IPC.
|
// Electron-specific settings, they are stored by Electron and set/read over an IPC.
|
||||||
// We store them over there are they are necessary to know before the renderer process launches.
|
// We store them over there are they are necessary to know before the renderer process launches.
|
||||||
|
|
|
@ -295,6 +295,16 @@ export default class SettingsStore {
|
||||||
return SETTINGS[settingName].isFeature;
|
return SETTINGS[settingName].isFeature;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a setting should have a warning sign in the microcopy
|
||||||
|
* @param {string} settingName The setting to look up.
|
||||||
|
* @return {boolean} True if the setting should have a warning sign.
|
||||||
|
*/
|
||||||
|
public static shouldHaveWarning(settingName: string): boolean {
|
||||||
|
if (!SETTINGS[settingName]) return false;
|
||||||
|
return SETTINGS[settingName].shouldWarn ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
public static getBetaInfo(settingName: string): ISetting["betaInfo"] {
|
public static getBetaInfo(settingName: string): ISetting["betaInfo"] {
|
||||||
// consider a beta disabled if the config is explicitly set to false, in which case treat as normal Labs flag
|
// consider a beta disabled if the config is explicitly set to false, in which case treat as normal Labs flag
|
||||||
if (SettingsStore.isFeature(settingName)
|
if (SettingsStore.isFeature(settingName)
|
||||||
|
@ -355,7 +365,7 @@ export default class SettingsStore {
|
||||||
public static getValueAt(
|
public static getValueAt(
|
||||||
level: SettingLevel,
|
level: SettingLevel,
|
||||||
settingName: string,
|
settingName: string,
|
||||||
roomId: string = null,
|
roomId: string | null = null,
|
||||||
explicit = false,
|
explicit = false,
|
||||||
excludeDefault = false,
|
excludeDefault = false,
|
||||||
): any {
|
): any {
|
||||||
|
@ -420,7 +430,7 @@ export default class SettingsStore {
|
||||||
private static getFinalValue(
|
private static getFinalValue(
|
||||||
setting: ISetting,
|
setting: ISetting,
|
||||||
level: SettingLevel,
|
level: SettingLevel,
|
||||||
roomId: string,
|
roomId: string | null,
|
||||||
calculatedValue: any,
|
calculatedValue: any,
|
||||||
calculatedAtLevel: SettingLevel,
|
calculatedAtLevel: SettingLevel,
|
||||||
): any {
|
): any {
|
||||||
|
|
|
@ -31,6 +31,7 @@ export enum UIFeature {
|
||||||
AdvancedSettings = "UIFeature.advancedSettings",
|
AdvancedSettings = "UIFeature.advancedSettings",
|
||||||
RoomHistorySettings = "UIFeature.roomHistorySettings",
|
RoomHistorySettings = "UIFeature.roomHistorySettings",
|
||||||
TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates",
|
TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates",
|
||||||
|
BulkUnverifiedSessionsReminder = "UIFeature.BulkUnverifiedSessionsReminder",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UIComponent {
|
export enum UIComponent {
|
||||||
|
|
|
@ -39,7 +39,7 @@ export default abstract class SettingController {
|
||||||
*/
|
*/
|
||||||
public getValueOverride(
|
public getValueOverride(
|
||||||
level: SettingLevel,
|
level: SettingLevel,
|
||||||
roomId: string,
|
roomId: string | null,
|
||||||
calculatedValue: any,
|
calculatedValue: any,
|
||||||
calculatedAtLevel: SettingLevel,
|
calculatedAtLevel: SettingLevel,
|
||||||
): any {
|
): any {
|
||||||
|
|
|
@ -437,7 +437,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.locationInterval = setInterval(() => {
|
this.locationInterval = window.setInterval(() => {
|
||||||
if (!this.lastPublishedPositionTimestamp) {
|
if (!this.lastPublishedPositionTimestamp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -228,7 +228,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
|
||||||
if (!room) {
|
if (!room) {
|
||||||
logger.warn(`Live timeline event ${eventPayload.event.getId()} received without associated room`);
|
logger.warn(`Live timeline event ${eventPayload.event.getId()} received without associated room`);
|
||||||
logger.warn(`Queuing failed room update for retry as a result.`);
|
logger.warn(`Queuing failed room update for retry as a result.`);
|
||||||
setTimeout(async () => {
|
window.setTimeout(async () => {
|
||||||
const updatedRoom = this.matrixClient.getRoom(roomId);
|
const updatedRoom = this.matrixClient.getRoom(roomId);
|
||||||
await tryUpdate(updatedRoom);
|
await tryUpdate(updatedRoom);
|
||||||
}, 100); // 100ms should be enough for the room to show up
|
}, 100); // 100ms should be enough for the room to show up
|
||||||
|
|
|
@ -122,9 +122,6 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
this.allowedCapabilities.add(
|
this.allowedCapabilities.add(
|
||||||
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomMember).raw,
|
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomMember).raw,
|
||||||
);
|
);
|
||||||
this.allowedCapabilities.add(
|
|
||||||
WidgetEventCapability.forStateEvent(EventDirection.Send, "org.matrix.msc3401.call").raw,
|
|
||||||
);
|
|
||||||
this.allowedCapabilities.add(
|
this.allowedCapabilities.add(
|
||||||
WidgetEventCapability.forStateEvent(EventDirection.Receive, "org.matrix.msc3401.call").raw,
|
WidgetEventCapability.forStateEvent(EventDirection.Receive, "org.matrix.msc3401.call").raw,
|
||||||
);
|
);
|
||||||
|
|
|
@ -298,7 +298,7 @@ export async function setTheme(theme?: string): Promise<void> {
|
||||||
|
|
||||||
// In case of theme toggling (white => black => white)
|
// In case of theme toggling (white => black => white)
|
||||||
// Chrome doesn't fire the `load` event when the white theme is selected the second times
|
// Chrome doesn't fire the `load` event when the white theme is selected the second times
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = window.setInterval(() => {
|
||||||
if (isStyleSheetLoaded()) {
|
if (isStyleSheetLoaded()) {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
styleSheet.onload = undefined;
|
styleSheet.onload = undefined;
|
||||||
|
|
|
@ -38,7 +38,7 @@ export const showToast = (deviceIds: Set<string>) => {
|
||||||
|
|
||||||
ToastStore.sharedInstance().addOrReplaceToast({
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
key: TOAST_KEY,
|
key: TOAST_KEY,
|
||||||
title: _t("You have unverified logins"),
|
title: _t("You have unverified sessions"),
|
||||||
icon: "verification_warning",
|
icon: "verification_warning",
|
||||||
props: {
|
props: {
|
||||||
description: _t("Review to ensure your account is safe"),
|
description: _t("Review to ensure your account is safe"),
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import filesize from 'filesize';
|
import { filesize } from 'filesize';
|
||||||
|
|
||||||
import { IMediaEventContent } from '../customisations/models/IMediaEventContent';
|
import { IMediaEventContent } from '../customisations/models/IMediaEventContent';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
|
|
|
@ -241,7 +241,7 @@ export default class MultiInviter {
|
||||||
break;
|
break;
|
||||||
case "M_LIMIT_EXCEEDED":
|
case "M_LIMIT_EXCEEDED":
|
||||||
// we're being throttled so wait a bit & try again
|
// we're being throttled so wait a bit & try again
|
||||||
setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.doInvite(address, ignoreProfile).then(resolve, reject);
|
this.doInvite(address, ignoreProfile).then(resolve, reject);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
return;
|
return;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue