Merge remote-tracking branch 'origin/develop' into feat/emoji-picker-rich-text-mode

This commit is contained in:
Florian Duros 2022-12-05 17:40:46 +01:00
commit 54e12d265b
No known key found for this signature in database
GPG key ID: 9700AA5870258A0B
139 changed files with 2830 additions and 3202 deletions

View file

@ -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: |

View file

@ -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

View file

@ -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/*

View file

@ -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

View file

@ -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:

View file

@ -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:

View file

@ -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);
});
}); });

View file

@ -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);
}); });

View file

@ -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;

View 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"
}
]
}

View 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
}
}

View file

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

View file

@ -0,0 +1 @@
{}

View file

@ -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,

View file

@ -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);

View file

@ -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 { };

View file

@ -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": {

View file

@ -114,6 +114,10 @@ limitations under the License.
} }
} }
} }
&:last-child {
margin-bottom: 0;
}
} }
.mx_BetaCard_betaPill { .mx_BetaCard_betaPill {

View file

@ -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: "⚠️ ";
}
} }

View file

@ -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;

View file

@ -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);
}
} }
} }

View file

@ -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;

View file

@ -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 {

View file

@ -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 {

View file

@ -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,
); );

View file

@ -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();

View file

@ -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

View file

@ -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) {

View file

@ -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);
} }

View file

@ -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);
} }

View file

@ -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);

View file

@ -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,
); );

View file

@ -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);
} }
} }

View file

@ -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)

View file

@ -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

View file

@ -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;

View file

@ -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() {

View file

@ -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);
} }

View file

@ -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);

View file

@ -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;

View file

@ -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";

View file

@ -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) {

View file

@ -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);
} }

View file

@ -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)
}; };

View file

@ -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,
}); });

View file

@ -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';

View file

@ -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';

View file

@ -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);

View file

@ -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,

View file

@ -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(),
}); });

View file

@ -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>
), ),
}) })
} }

View file

@ -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>

View file

@ -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);

View file

@ -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 => {

View file

@ -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) => {

View file

@ -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';

View file

@ -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,

View file

@ -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]);

View file

@ -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

View file

@ -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);
}); });

View file

@ -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>;

View file

@ -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,
); );

View file

@ -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,

View file

@ -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) => {

View file

@ -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"
/> />

View file

@ -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}
> >

View file

@ -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]);

View file

@ -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,
); );

View file

@ -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

View file

@ -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);
}; };

View file

@ -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;

View file

@ -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

View file

@ -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>
); );
} }

View file

@ -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();

View file

@ -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 });

View file

@ -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);

View file

@ -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);
}, []); }, []);

View file

@ -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);
} }

View file

@ -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);
} }
} }

View file

@ -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`;

View file

@ -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 {

View file

@ -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);

View file

@ -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);

View file

@ -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(() => {

View file

@ -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));

View file

@ -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",

View file

@ -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,
); );

View file

@ -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();
}; };

View file

@ -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(),
]; ];

View file

@ -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.

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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;
} }

View file

@ -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

View file

@ -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,
); );

View file

@ -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;

View file

@ -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"),

View file

@ -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';

View file

@ -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