Merge remote-tracking branch 'origin/develop' into feat/emoji-picker-rich-text-mode
This commit is contained in:
commit
54e12d265b
139 changed files with 2830 additions and 3202 deletions
7
.github/workflows/cypress.yaml
vendored
7
.github/workflows/cypress.yaml
vendored
|
@ -43,7 +43,7 @@ jobs:
|
|||
- name: Get commit details
|
||||
id: commit
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
uses: actions/github-script@v5
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const response = await github.rest.git.getCommit({
|
||||
|
@ -82,7 +82,7 @@ jobs:
|
|||
# Run 4 instances in Parallel
|
||||
runner: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
# 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
|
||||
|
@ -96,7 +96,6 @@ jobs:
|
|||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
workflow: element-build-and-test.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: previewbuild
|
||||
path: webapp
|
||||
|
@ -147,7 +146,7 @@ jobs:
|
|||
|
||||
- name: Upload Artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cypress-results
|
||||
path: |
|
||||
|
|
4
.github/workflows/element-web.yaml
vendored
4
.github/workflows/element-web.yaml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
name: "Build Element-Web"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -46,7 +46,7 @@ jobs:
|
|||
working-directory: ./element-web
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: previewbuild
|
||||
path: element-web/webapp
|
||||
|
|
2
.github/workflows/i18n_check.yml
vendored
2
.github/workflows/i18n_check.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
|||
- name: "Get modified files"
|
||||
id: changed_files
|
||||
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:
|
||||
files: |
|
||||
src/i18n/strings/*
|
||||
|
|
2
.github/workflows/notify-element-web.yml
vendored
2
.github/workflows/notify-element-web.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
|||
if: github.repository == 'matrix-org/matrix-react-sdk'
|
||||
steps:
|
||||
- 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:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: vector-im/element-web
|
||||
|
|
10
.github/workflows/static_analysis.yaml
vendored
10
.github/workflows/static_analysis.yaml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
|||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -89,7 +89,7 @@ jobs:
|
|||
name: "Rethemendex Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- run: ./res/css/rethemendex.sh
|
||||
|
||||
|
@ -99,7 +99,7 @@ jobs:
|
|||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -116,7 +116,7 @@ jobs:
|
|||
name: "Style Lint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -133,7 +133,7 @@ jobs:
|
|||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
|
|
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
|
@ -38,7 +38,7 @@ jobs:
|
|||
run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
path: |
|
||||
|
@ -49,7 +49,7 @@ jobs:
|
|||
name: Element Web Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
|
|
|
@ -91,6 +91,17 @@ const bobJoin = function(this: CryptoTestContext) {
|
|||
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[]> => {
|
||||
return cy.wrap(new Promise<EmojiMapping[]>((resolve) => {
|
||||
const onShowSas = (event: ISasEvent) => {
|
||||
|
@ -174,4 +185,22 @@ describe("Cryptography", function() {
|
|||
testMessages.call(this);
|
||||
verify.call(this);
|
||||
});
|
||||
|
||||
it("should allow verification when there is no existing DM", function(this: CryptoTestContext) {
|
||||
cy.bootstrapCrossSigning();
|
||||
autoJoin(this.bob);
|
||||
|
||||
/* we need to have a room with the other user present, so we can open the verification panel */
|
||||
let roomId: string;
|
||||
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }).then(_room1Id => {
|
||||
roomId = _room1Id;
|
||||
cy.log(`Created test room ${roomId}`);
|
||||
cy.visit(`/#/room/${roomId}`);
|
||||
// wait for Bob to join the room, otherwise our attempt to open his user details may race
|
||||
// with his join.
|
||||
cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist");
|
||||
});
|
||||
|
||||
verify.call(this);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,6 +21,10 @@ import { SynapseInstance } from "../../plugins/synapsedocker";
|
|||
describe("Login", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.stubDefaultServer();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ describe("Registration", () => {
|
|||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.stubDefaultServer();
|
||||
cy.visit("/#/register");
|
||||
cy.startSynapse("consent").then(data => {
|
||||
synapse = data;
|
||||
|
|
48
cypress/fixtures/matrix-org-client-login.json
Normal file
48
cypress/fixtures/matrix-org-client-login.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"flows": [
|
||||
{
|
||||
"type": "m.login.sso",
|
||||
"identity_providers": [
|
||||
{
|
||||
"id": "oidc-github",
|
||||
"name": "GitHub",
|
||||
"icon": "mxc://matrix.org/sVesTtrFDTpXRbYfpahuJsKP",
|
||||
"brand": "github"
|
||||
},
|
||||
{
|
||||
"id": "oidc-google",
|
||||
"name": "Google",
|
||||
"icon": "mxc://matrix.org/ZlnaaZNPxtUuQemvgQzlOlkz",
|
||||
"brand": "google"
|
||||
},
|
||||
{
|
||||
"id": "oidc-gitlab",
|
||||
"name": "GitLab",
|
||||
"icon": "mxc://matrix.org/MCVOEmFgVieKFshPxmnejWOq",
|
||||
"brand": "gitlab"
|
||||
},
|
||||
{
|
||||
"id": "oidc-facebook",
|
||||
"name": "Facebook",
|
||||
"icon": "mxc://matrix.org/nsyeLIgzxazZmJadflMAsAWG",
|
||||
"brand": "facebook"
|
||||
},
|
||||
{
|
||||
"id": "oidc-apple",
|
||||
"name": "Apple",
|
||||
"icon": "mxc://matrix.org/QQKNSOdLiMHtJhzeAObmkFiU",
|
||||
"brand": "apple"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "m.login.token"
|
||||
},
|
||||
{
|
||||
"type": "m.login.password"
|
||||
},
|
||||
{
|
||||
"type": "m.login.application_service"
|
||||
}
|
||||
]
|
||||
}
|
39
cypress/fixtures/matrix-org-client-versions.json
Normal file
39
cypress/fixtures/matrix-org-client-versions.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"versions": [
|
||||
"r0.0.1",
|
||||
"r0.1.0",
|
||||
"r0.2.0",
|
||||
"r0.3.0",
|
||||
"r0.4.0",
|
||||
"r0.5.0",
|
||||
"r0.6.0",
|
||||
"r0.6.1",
|
||||
"v1.1",
|
||||
"v1.2",
|
||||
"v1.3",
|
||||
"v1.4"
|
||||
],
|
||||
"unstable_features": {
|
||||
"org.matrix.label_based_filtering": true,
|
||||
"org.matrix.e2e_cross_signing": true,
|
||||
"org.matrix.msc2432": true,
|
||||
"uk.half-shot.msc2666.mutual_rooms": true,
|
||||
"io.element.e2ee_forced.public": false,
|
||||
"io.element.e2ee_forced.private": false,
|
||||
"io.element.e2ee_forced.trusted_private": false,
|
||||
"org.matrix.msc3026.busy_presence": false,
|
||||
"org.matrix.msc2285.stable": true,
|
||||
"org.matrix.msc3827.stable": true,
|
||||
"org.matrix.msc2716": false,
|
||||
"org.matrix.msc3030": false,
|
||||
"org.matrix.msc3440.stable": true,
|
||||
"org.matrix.msc3771": true,
|
||||
"org.matrix.msc3773": false,
|
||||
"fi.mau.msc2815": false,
|
||||
"org.matrix.msc3882": false,
|
||||
"org.matrix.msc3881": false,
|
||||
"org.matrix.msc3874": false,
|
||||
"org.matrix.msc3886": false,
|
||||
"org.matrix.msc3912": false
|
||||
}
|
||||
}
|
8
cypress/fixtures/matrix-org-client-well-known.json
Normal file
8
cypress/fixtures/matrix-org-client-well-known.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"m.homeserver": {
|
||||
"base_url": "https://matrix-client.matrix.org"
|
||||
},
|
||||
"m.identity_server": {
|
||||
"base_url": "https://vector.im"
|
||||
}
|
||||
}
|
1
cypress/fixtures/vector-im-identity-v1.json
Normal file
1
cypress/fixtures/vector-im-identity-v1.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -78,6 +78,7 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
|
|||
const username = Cypress._.uniqueId("userId_");
|
||||
const password = Cypress._.uniqueId("password_");
|
||||
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 => {
|
||||
const cli = new win.matrixcs.MatrixClient({
|
||||
baseUrl: synapse.baseUrl,
|
||||
|
|
|
@ -103,6 +103,7 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
|
|||
return cy.registerUser(synapse, username, password, displayName).then(() => {
|
||||
return cy.loginUser(synapse, username, password);
|
||||
}).then(response => {
|
||||
cy.log(`Registered test user ${username} with displayname ${displayName}`);
|
||||
cy.window({ log: false }).then(win => {
|
||||
// Seed the localStorage with the required credentials
|
||||
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
|
||||
|
|
|
@ -20,10 +20,12 @@ declare global {
|
|||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
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;
|
||||
// Remove intercept on all /_matrix/ networking requests
|
||||
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
|
||||
export { };
|
||||
|
|
45
package.json
45
package.json
|
@ -57,10 +57,10 @@
|
|||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@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",
|
||||
"@sentry/browser": "^6.11.0",
|
||||
"@sentry/tracing": "^6.11.0",
|
||||
"@sentry/browser": "^7.0.0",
|
||||
"@sentry/tracing": "^7.0.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/geojson": "^7946.0.8",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
|
@ -72,18 +72,18 @@
|
|||
"counterpart": "^0.18.6",
|
||||
"diff-dom": "^4.2.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"emojibase": "6.0.2",
|
||||
"emojibase-data": "7.0.0",
|
||||
"emojibase-regex": "6.0.0",
|
||||
"emojibase": "6.1.0",
|
||||
"emojibase-data": "7.0.1",
|
||||
"emojibase-regex": "6.0.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "6.1.0",
|
||||
"flux": "2.1.1",
|
||||
"filesize": "10.0.5",
|
||||
"flux": "4.0.3",
|
||||
"focus-visible": "^5.2.0",
|
||||
"gfm.css": "^1.1.2",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"highlight.js": "^11.3.1",
|
||||
"html-entities": "^1.4.0",
|
||||
"html-entities": "^2.0.0",
|
||||
"is-ip": "^3.1.0",
|
||||
"jszip": "^3.7.0",
|
||||
"katex": "^0.12.0",
|
||||
|
@ -102,7 +102,6 @@
|
|||
"parse5": "^6.0.1",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.12.2",
|
||||
"prop-types": "^15.7.2",
|
||||
"qrcode": "1.4.4",
|
||||
"re-resizable": "^6.9.0",
|
||||
"react": "17.0.2",
|
||||
|
@ -141,7 +140,7 @@
|
|||
"@peculiar/webcrypto": "^1.4.1",
|
||||
"@percy/cli": "^1.11.0",
|
||||
"@percy/cypress": "^3.1.2",
|
||||
"@sentry/types": "^6.10.0",
|
||||
"@sentry/types": "^7.0.0",
|
||||
"@sinonjs/fake-timers": "^9.1.2",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
|
@ -149,7 +148,7 @@
|
|||
"@types/classnames": "^2.2.11",
|
||||
"@types/commonmark": "^0.27.4",
|
||||
"@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/enzyme": "^3.10.9",
|
||||
"@types/escape-html": "^1.0.1",
|
||||
|
@ -160,8 +159,8 @@
|
|||
"@types/katex": "^0.14.0",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/modernizr": "^3.5.3",
|
||||
"@types/node": "^14.18.28",
|
||||
"@types/pako": "^1.0.1",
|
||||
"@types/node": "^16",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/parse5": "^6.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "17.0.49",
|
||||
|
@ -175,11 +174,11 @@
|
|||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
||||
"allchange": "^1.1.0",
|
||||
"axe-core": "^4.4.3",
|
||||
"babel-jest": "^26.6.3",
|
||||
"blob-polyfill": "^6.0.20211015",
|
||||
"axe-core": "4.4.3",
|
||||
"babel-jest": "^29.0.0",
|
||||
"blob-polyfill": "^7.0.0",
|
||||
"chokidar": "^3.5.1",
|
||||
"cypress": "^10.3.0",
|
||||
"cypress": "^11.0.0",
|
||||
"cypress-axe": "^1.0.0",
|
||||
"cypress-real-events": "^1.7.1",
|
||||
"enzyme": "^3.11.0",
|
||||
|
@ -192,10 +191,10 @@
|
|||
"eslint-plugin-matrix-org": "^0.7.0",
|
||||
"eslint-plugin-react": "^7.28.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",
|
||||
"fs-extra": "^10.0.1",
|
||||
"glob": "^7.1.6",
|
||||
"fs-extra": "^11.0.0",
|
||||
"glob": "^8.0.0",
|
||||
"jest": "^29.2.2",
|
||||
"jest-canvas-mock": "^2.3.0",
|
||||
"jest-environment-jsdom": "^29.2.2",
|
||||
|
@ -210,9 +209,9 @@
|
|||
"rimraf": "^3.0.2",
|
||||
"rrweb-snapshot": "1.1.7",
|
||||
"stylelint": "^14.9.1",
|
||||
"stylelint-config-standard": "^26.0.0",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-scss": "^4.2.0",
|
||||
"typescript": "4.8.4",
|
||||
"typescript": "4.9.3",
|
||||
"walk": "^2.3.14"
|
||||
},
|
||||
"jest": {
|
||||
|
|
|
@ -114,6 +114,10 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_BetaCard_betaPill {
|
||||
|
|
|
@ -60,4 +60,8 @@ limitations under the License.
|
|||
font-family: $monospace-font-family !important;
|
||||
background-color: $rte-code-bg-color;
|
||||
}
|
||||
|
||||
.mx_SettingsTab_microcopy_warning::before {
|
||||
content: "⚠️ ";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,9 @@ limitations under the License.
|
|||
--EventTile_bubble_gap-inline: 5px;
|
||||
|
||||
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);
|
||||
font-size: $font-14px;
|
||||
|
||||
|
|
|
@ -462,6 +462,11 @@ $left-gutter: 64px;
|
|||
&.mx_EventTile_continuation {
|
||||
margin-top: 2px;
|
||||
}
|
||||
&.mx_EventTile_lastInSection {
|
||||
/* Other half of the gutter is provided by margin-top on the first
|
||||
tile of the section */
|
||||
margin-bottom: calc(var(--gutterSize) / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,11 @@ limitations under the License.
|
|||
}
|
||||
|
||||
> a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
grid-template:
|
||||
"sender" auto
|
||||
"message" auto
|
||||
/ auto;
|
||||
text-decoration: none;
|
||||
color: $secondary-content;
|
||||
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. */
|
||||
.mx_EventTile_content {
|
||||
grid-area: message;
|
||||
$reply-lines: 2;
|
||||
$line-height: $font-18px;
|
||||
|
||||
|
@ -102,7 +106,16 @@ limitations under the License.
|
|||
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 {
|
||||
grid-area: sender;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
|
|
@ -40,8 +40,9 @@ limitations under the License.
|
|||
display: flex;
|
||||
gap: $spacing-4;
|
||||
|
||||
i {
|
||||
flex-shrink: 0;
|
||||
.mx_Spinner {
|
||||
flex: 0 0 14px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
span {
|
||||
|
|
4
src/@types/global.d.ts
vendored
4
src/@types/global.d.ts
vendored
|
@ -149,14 +149,10 @@ declare global {
|
|||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
|
||||
interface OffscreenCanvas {
|
||||
height: number;
|
||||
width: number;
|
||||
getContext: HTMLCanvasElement["getContext"];
|
||||
convertToBlob(opts?: {
|
||||
type?: string;
|
||||
quality?: number;
|
||||
}): Promise<Blob>;
|
||||
transferToImageBitmap(): ImageBitmap;
|
||||
}
|
||||
|
||||
interface HTMLAudioElement {
|
||||
|
|
|
@ -174,12 +174,12 @@ export class DecryptionFailureTracker {
|
|||
* Start checking for and tracking failures.
|
||||
*/
|
||||
public start(): void {
|
||||
this.checkInterval = setInterval(
|
||||
this.checkInterval = window.setInterval(
|
||||
() => this.checkFailures(Date.now()),
|
||||
DecryptionFailureTracker.CHECK_INTERVAL_MS,
|
||||
);
|
||||
|
||||
this.trackInterval = setInterval(
|
||||
this.trackInterval = window.setInterval(
|
||||
() => this.trackFailures(),
|
||||
DecryptionFailureTracker.TRACK_INTERVAL_MS,
|
||||
);
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
removeClientInformation,
|
||||
} from "./utils/device/clientInformation";
|
||||
import SettingsStore, { CallbackFn } from "./settings/SettingsStore";
|
||||
import { UIFeature } from "./settings/UIFeature";
|
||||
|
||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
|
@ -68,6 +69,7 @@ export default class DeviceListener {
|
|||
private displayingToastsForDeviceIds = new Set<string>();
|
||||
private running = false;
|
||||
private shouldRecordClientInformation = false;
|
||||
private enableBulkUnverifiedSessionsReminder = true;
|
||||
private deviceClientInformationSettingWatcherRef: string | undefined;
|
||||
|
||||
public static sharedInstance() {
|
||||
|
@ -86,6 +88,8 @@ export default class DeviceListener {
|
|||
MatrixClientPeg.get().on(ClientEvent.Sync, this.onSync);
|
||||
MatrixClientPeg.get().on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.shouldRecordClientInformation = SettingsStore.getValue('deviceClientInformationOptIn');
|
||||
// only configurable in config, so we don't need to watch the value
|
||||
this.enableBulkUnverifiedSessionsReminder = SettingsStore.getValue(UIFeature.BulkUnverifiedSessionsReminder);
|
||||
this.deviceClientInformationSettingWatcherRef = SettingsStore.watchSetting(
|
||||
'deviceClientInformationOptIn',
|
||||
null,
|
||||
|
@ -306,6 +310,9 @@ export default class DeviceListener {
|
|||
// Unverified devices that have appeared since then
|
||||
const newUnverifiedDeviceIds = new Set<string>();
|
||||
|
||||
const isCurrentDeviceTrusted = crossSigningReady &&
|
||||
await (cli.checkDeviceTrust(cli.getUserId()!, cli.deviceId!)).isCrossSigningVerified();
|
||||
|
||||
// as long as cross-signing isn't ready,
|
||||
// you can't see or dismiss any device toasts
|
||||
if (crossSigningReady) {
|
||||
|
@ -313,7 +320,7 @@ export default class DeviceListener {
|
|||
for (const device of devices) {
|
||||
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 (this.ourDeviceIdsAtStart.has(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(','));
|
||||
|
||||
// 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);
|
||||
} else {
|
||||
hideBulkUnverifiedSessionsToast();
|
||||
|
|
|
@ -24,7 +24,7 @@ import classNames from 'classnames';
|
|||
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||
import { split } from 'lodash';
|
||||
import katex from 'katex';
|
||||
import { AllHtmlEntities } from 'html-entities';
|
||||
import { decode } from 'html-entities';
|
||||
import { IContent } from 'matrix-js-sdk/src/models/event';
|
||||
import { Optional } from 'matrix-events-sdk';
|
||||
|
||||
|
@ -518,7 +518,7 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
|
|||
// Cheerio instance to be returned.
|
||||
phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
|
||||
return katex.renderToString(
|
||||
AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),
|
||||
decode(phtml(e).attr('data-mx-maths')),
|
||||
{
|
||||
throwOnError: false,
|
||||
// @ts-ignore - `e` can be an Element, not just a Node
|
||||
|
|
|
@ -71,13 +71,52 @@ export const PROTOCOL_SIP_VIRTUAL = 'im.vector.protocol.sip_virtual';
|
|||
|
||||
const CHECK_PROTOCOLS_ATTEMPTS = 3;
|
||||
|
||||
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',
|
||||
Ringback = 'ringbackAudio',
|
||||
CallEnd = 'callendAudio',
|
||||
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 {
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
|
@ -119,6 +158,7 @@ export default class LegacyCallHandler extends EventEmitter {
|
|||
// call with a different party to this one.
|
||||
private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee)
|
||||
private audioPromises = new Map<AudioID, Promise<void>>();
|
||||
private audioElementsWithListeners = new Map<HTMLMediaElement, boolean>();
|
||||
private supportsPstnProtocol = null;
|
||||
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
|
||||
|
@ -176,6 +216,16 @@ export default class LegacyCallHandler extends EventEmitter {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -183,6 +233,39 @@ export default class LegacyCallHandler extends EventEmitter {
|
|||
if (cli) {
|
||||
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 {
|
||||
|
@ -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);
|
||||
} else {
|
||||
logger.log("Failed to check for protocol support: will retry", e);
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
this.checkProtocols(maxTries - 1);
|
||||
}, 10000);
|
||||
}
|
||||
|
@ -402,11 +485,21 @@ export default class LegacyCallHandler extends EventEmitter {
|
|||
// which listens?
|
||||
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
||||
if (audio) {
|
||||
this.addEventListenersForAudioElement(audio);
|
||||
const playAudio = async () => {
|
||||
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
|
||||
// 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();
|
||||
logger.debug(`${logPrefix} playing audio successfully`);
|
||||
} catch (e) {
|
||||
|
|
|
@ -584,7 +584,7 @@ async function doSetLoggedIn(
|
|||
// later than MatrixChat might assume.
|
||||
//
|
||||
// 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);
|
||||
|
||||
if (clearStorageEnabled) {
|
||||
|
@ -865,7 +865,7 @@ export async function onLoggedOut(): Promise<void> {
|
|||
if (SdkConfig.get().logout_redirect_url) {
|
||||
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
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
window.location.href = SdkConfig.get().logout_redirect_url;
|
||||
}, 100);
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@ export default class NodeAnimator extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
// and then we animate to the resting state
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
this.applyStyles(domNode as HTMLElement, restingStyle);
|
||||
}, 0);
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
PermissionChanged as PermissionChangedEvent,
|
||||
} from "@matrix-org/analytics-events/types/typescript/PermissionChanged";
|
||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { IRoomTimelineData } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||
|
@ -217,7 +218,7 @@ export const Notifier = {
|
|||
this.boundOnRoomReceipt = this.boundOnRoomReceipt || this.onRoomReceipt.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(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted);
|
||||
MatrixClientPeg.get().on(ClientEvent.Sync, this.boundOnSyncStateChange);
|
||||
|
@ -227,7 +228,7 @@ export const Notifier = {
|
|||
|
||||
stop: function(this: typeof Notifier) {
|
||||
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(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted);
|
||||
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 (ev.getSender() === MatrixClientPeg.get().getUserId()) return;
|
||||
|
||||
|
@ -428,6 +437,11 @@ export const Notifier = {
|
|||
}
|
||||
}
|
||||
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);
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ export default class PasswordReset {
|
|||
this.checkEmailLinkClicked()
|
||||
.then(() => resolve())
|
||||
.catch(() => {
|
||||
setTimeout(
|
||||
window.setTimeout(
|
||||
() => this.tryCheckEmailLinkClicked(resolve),
|
||||
CHECK_EMAIL_VERIFIED_POLL_INTERVAL,
|
||||
);
|
||||
|
|
|
@ -127,7 +127,7 @@ export class PlaybackClock implements IDestroyable {
|
|||
// cast to number because the types are wrong
|
||||
// 100ms interval to make sure the time is as accurate as possible without
|
||||
// being overly insane
|
||||
this.timerId = <number><any>setInterval(this.checkTime, 100);
|
||||
this.timerId = <number><any>window.setInterval(this.checkTime, 100);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -85,4 +85,4 @@ class MxVoiceWorklet extends AudioWorkletProcessor {
|
|||
|
||||
registerProcessor(WORKLET_NAME, MxVoiceWorklet);
|
||||
|
||||
export default null; // to appease module loaders (we never use the export)
|
||||
export default ""; // to appease module loaders (we never use the export)
|
||||
|
|
|
@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
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 { SimpleObservable } from "matrix-widget-api";
|
||||
import EventEmitter from "events";
|
||||
|
@ -32,12 +33,26 @@ import mxRecorderWorkletPath from "./RecorderWorklet";
|
|||
|
||||
const CHANNELS = 1; // stereo isn't important
|
||||
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_WARN_TIME_LEFT = 10; // 10 seconds, also somewhat arbitrary.
|
||||
|
||||
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 {
|
||||
waveform: number[]; // floating points between 0 (low) and 1 (high).
|
||||
timeSeconds: number; // float
|
||||
|
@ -88,13 +103,22 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
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() {
|
||||
try {
|
||||
this.recorderStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
channelCount: CHANNELS,
|
||||
noiseSuppression: true, // browsers ignore constraints they can't honour
|
||||
deviceId: MediaDeviceHandler.getAudioInput(),
|
||||
autoGainControl: { ideal: MediaDeviceHandler.getAudioAutoGainControl() },
|
||||
echoCancellation: { ideal: MediaDeviceHandler.getAudioEchoCancellation() },
|
||||
noiseSuppression: { ideal: MediaDeviceHandler.getAudioNoiseSuppression() },
|
||||
},
|
||||
});
|
||||
this.recorderContext = createAudioContext({
|
||||
|
@ -135,15 +159,19 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
this.recorderProcessor.addEventListener("audioprocess", this.onAudioProcess);
|
||||
}
|
||||
|
||||
const recorderOptions = this.shouldRecordInHighQuality() ?
|
||||
highQualityRecorderOptions : voiceRecorderOptions;
|
||||
const { encoderApplication, bitrate } = recorderOptions;
|
||||
|
||||
this.recorder = new Recorder({
|
||||
encoderPath, // magic from webpack
|
||||
encoderSampleRate: SAMPLE_RATE,
|
||||
encoderApplication: 2048, // voice (default is "audio")
|
||||
encoderApplication: encoderApplication,
|
||||
streamPages: true, // this speeds up the encoding process by using CPU over time
|
||||
encoderFrameSize: 20, // ms, arbitrary frame size we send to the encoder
|
||||
numberOfChannels: CHANNELS,
|
||||
sourceNode: this.recorderSource,
|
||||
encoderBitRate: BITRATE,
|
||||
encoderBitRate: bitrate,
|
||||
|
||||
// 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
|
||||
|
|
|
@ -35,7 +35,7 @@ export interface ISelectionRange {
|
|||
}
|
||||
|
||||
export interface ICompletion {
|
||||
type: "at-room" | "command" | "community" | "room" | "user";
|
||||
type?: "at-room" | "command" | "community" | "room" | "user";
|
||||
completion: string;
|
||||
completionId?: string;
|
||||
component?: ReactElement;
|
||||
|
|
|
@ -103,7 +103,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
return []; // don't give any suggestions if the user doesn't want them
|
||||
}
|
||||
|
||||
let completions = [];
|
||||
let completions: ISortedEmoji[] = [];
|
||||
const { command, range } = this.getCurrentCommand(query, selection);
|
||||
|
||||
if (command && command[0].length > 2) {
|
||||
|
@ -132,7 +132,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
}
|
||||
// Finally, sort by original ordering
|
||||
sorters.push(c => c._orderBy);
|
||||
completions = sortBy(uniq(completions), sorters);
|
||||
completions = sortBy<ISortedEmoji>(uniq(completions), sorters);
|
||||
|
||||
completions = completions.slice(0, LIMIT);
|
||||
|
||||
|
@ -141,9 +141,9 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
this.recentlyUsed.forEach(emoji => {
|
||||
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,
|
||||
component: (
|
||||
<PillCompletion title={`:${c.emoji.shortcodes[0]}:`} aria-label={c.emoji.unicode}>
|
||||
|
@ -153,7 +153,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
range,
|
||||
}));
|
||||
}
|
||||
return completions;
|
||||
return [];
|
||||
}
|
||||
|
||||
getName() {
|
||||
|
|
|
@ -127,7 +127,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
});
|
||||
|
||||
if (this.props.poll) {
|
||||
this.intervalId = setInterval(() => {
|
||||
this.intervalId = window.setInterval(() => {
|
||||
this.authLogic.poll();
|
||||
}, 2000);
|
||||
}
|
||||
|
|
|
@ -1965,7 +1965,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.accountPassword = password;
|
||||
// self-destruct the password after 5mins
|
||||
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
||||
this.accountPasswordTimer = setTimeout(() => {
|
||||
this.accountPasswordTimer = window.setTimeout(() => {
|
||||
this.accountPassword = null;
|
||||
this.accountPasswordTimer = null;
|
||||
}, 60 * 5 * 1000);
|
||||
|
|
|
@ -459,7 +459,7 @@ export default class ScrollPanel extends React.Component<IProps> {
|
|||
if (this.unfillDebouncer) {
|
||||
clearTimeout(this.unfillDebouncer);
|
||||
}
|
||||
this.unfillDebouncer = setTimeout(() => {
|
||||
this.unfillDebouncer = window.setTimeout(() => {
|
||||
this.unfillDebouncer = null;
|
||||
debuglog("unfilling now", { backwards, origExcessHeight });
|
||||
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
|
||||
// if messages are already cached in memory,
|
||||
// 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);
|
||||
}).finally(() => {
|
||||
this.pendingFillRequests[dir] = false;
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
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 { Optional } from "matrix-events-sdk";
|
||||
|
||||
|
|
|
@ -96,8 +96,9 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||
private async updateMode(mode: Mode) {
|
||||
this.setState({ phase: Phase.Loading });
|
||||
if (this.state.rendezvous) {
|
||||
this.state.rendezvous.onFailure = undefined;
|
||||
await this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled);
|
||||
const rendezvous = this.state.rendezvous;
|
||||
rendezvous.onFailure = undefined;
|
||||
await rendezvous.cancel(RendezvousFailureReason.UserCancelled);
|
||||
this.setState({ rendezvous: undefined });
|
||||
}
|
||||
if (mode === Mode.Show) {
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -27,11 +26,6 @@ interface IProps extends IContextMenuProps {
|
|||
}
|
||||
|
||||
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) {
|
||||
super(props);
|
||||
}
|
||||
|
|
|
@ -697,7 +697,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
|||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer);
|
||||
}
|
||||
this.debounceTimer = setTimeout(() => {
|
||||
this.debounceTimer = window.setTimeout(() => {
|
||||
this.updateSuggestions(term);
|
||||
}, 150); // 150ms debounce (human reaction time + some)
|
||||
};
|
||||
|
|
|
@ -48,7 +48,7 @@ async function syncHealthCheck(cli: MatrixClient): Promise<void> {
|
|||
*/
|
||||
async function proxyHealthCheck(endpoint: string, hsUrl?: string): Promise<void> {
|
||||
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", {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import filesize from "filesize";
|
||||
import { filesize } from "filesize";
|
||||
|
||||
import { Icon as FileIcon } from '../../../../res/img/feather-customised/files.svg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import filesize from 'filesize';
|
||||
import { filesize } from 'filesize';
|
||||
import React from 'react';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
|
|
@ -51,7 +51,7 @@ const VerificationRequestExplorer: React.FC<{
|
|||
if (request.timeout == 0) return;
|
||||
|
||||
/* Note that request.timeout is a getter, so its value changes */
|
||||
const id = setInterval(() => {
|
||||
const id = window.setInterval(() => {
|
||||
setRequestTimeout(request.timeout);
|
||||
}, 500);
|
||||
|
||||
|
|
|
@ -228,7 +228,7 @@ export const useWebSearchMetrics = (numResults: number, queryLength: number, via
|
|||
if (!queryLength) return;
|
||||
|
||||
// send metrics after a 1s debounce
|
||||
const timeoutId = setTimeout(() => {
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
PosthogAnalytics.instance.trackEvent<WebSearchEvent>({
|
||||
eventName: "WebSearch",
|
||||
viaSpotlight,
|
||||
|
|
|
@ -106,7 +106,7 @@ export default class DesktopCapturerSourcePicker extends React.Component<
|
|||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// setInterval() first waits and then executes, therefore
|
||||
// window.setInterval() first waits and then executes, therefore
|
||||
// we call getDesktopCapturerSources() here without any delay.
|
||||
// Otherwise the dialog would be left empty for some time.
|
||||
this.setState({
|
||||
|
@ -114,7 +114,7 @@ export default class DesktopCapturerSourcePicker extends React.Component<
|
|||
});
|
||||
|
||||
// We update the sources every 500ms to get newer thumbnails
|
||||
this.interval = setInterval(async () => {
|
||||
this.interval = window.setInterval(async () => {
|
||||
this.setState({
|
||||
sources: await getDesktopCapturerSources(),
|
||||
});
|
||||
|
|
|
@ -240,7 +240,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
|
|||
{ _t("In reply to <a>this message</a>",
|
||||
{},
|
||||
{ a: (sub) => (
|
||||
<a className="mx_reply_anchor" href={`#${eventId}`} scroll-to={eventId}> { sub } </a>
|
||||
<a className="mx_reply_anchor" href={`#${eventId}`} data-scroll-to={eventId}> { sub } </a>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -80,12 +80,13 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
|||
|
||||
if (!canChange && this.props.hideIfCannotSet) return null;
|
||||
|
||||
const label = this.props.label
|
||||
const label = (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 shouldWarn = SettingsStore.shouldHaveWarning(this.props.name);
|
||||
|
||||
let disabledDescription: JSX.Element;
|
||||
let disabledDescription: JSX.Element | null = null;
|
||||
if (this.props.disabled && this.props.disabledDescription) {
|
||||
disabledDescription = <div className="mx_SettingsFlag_microcopy">
|
||||
{ this.props.disabledDescription }
|
||||
|
@ -106,7 +107,20 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
|||
<label className="mx_SettingsFlag_label">
|
||||
<span className="mx_SettingsFlag_labelText">{ label }</span>
|
||||
{ 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> }
|
||||
{ disabledDescription }
|
||||
</label>
|
||||
|
|
|
@ -35,7 +35,7 @@ export function UseCaseSelection({ onFinished }: Props) {
|
|||
// Call onFinished 1.5s after `selection` becomes truthy, to give time for the animation to run
|
||||
useEffect(() => {
|
||||
if (selection) {
|
||||
let handler: number | null = setTimeout(() => {
|
||||
let handler: number | null = window.setTimeout(() => {
|
||||
handler = null;
|
||||
onFinished(selection);
|
||||
}, TIMEOUT);
|
||||
|
|
|
@ -191,7 +191,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
|||
this.setState({ filter });
|
||||
// Header underlines need to be updated, but updating requires knowing
|
||||
// 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 => {
|
||||
|
|
|
@ -31,8 +31,8 @@ class Search extends React.PureComponent<IProps> {
|
|||
private inputRef = React.createRef<HTMLInputElement>();
|
||||
|
||||
componentDidMount() {
|
||||
// For some reason, neither the autoFocus nor just calling focus() here worked, so here's a setTimeout
|
||||
setTimeout(() => this.inputRef.current.focus(), 0);
|
||||
// For some reason, neither the autoFocus nor just calling focus() here worked, so here's a window.setTimeout
|
||||
window.setTimeout(() => this.inputRef.current.focus(), 0);
|
||||
}
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import filesize from 'filesize';
|
||||
import { filesize } from 'filesize';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
|
|
@ -270,6 +270,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
|
||||
// Set a placeholder image when we can't decrypt the image.
|
||||
this.setState({ error });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
thumbUrl = this.getThumbUrl();
|
||||
|
@ -291,16 +292,27 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
img.crossOrigin = "Anonymous"; // CORS allow canvas access
|
||||
img.src = contentUrl;
|
||||
|
||||
await loadPromise;
|
||||
|
||||
const blob = await this.props.mediaEventHelper.sourceBlob.value;
|
||||
if (!await blobIsAnimated(content.info.mimetype, blob)) {
|
||||
isAnimated = false;
|
||||
try {
|
||||
await loadPromise;
|
||||
} catch (error) {
|
||||
logger.error("Unable to download attachment: ", error);
|
||||
this.setState({ error: error as Error });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAnimated) {
|
||||
const thumb = await createThumbnail(img, img.width, img.height, content.info.mimetype, false);
|
||||
thumbUrl = URL.createObjectURL(thumb.thumbnail);
|
||||
try {
|
||||
const blob = await this.props.mediaEventHelper.sourceBlob.value;
|
||||
if (!await blobIsAnimated(content.info?.mimetype, blob)) {
|
||||
isAnimated = false;
|
||||
}
|
||||
|
||||
if (isAnimated) {
|
||||
const thumb = await createThumbnail(img, img.width, img.height, content.info!.mimetype, false);
|
||||
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.
|
||||
if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) {
|
||||
this.clearBlurhashTimeout();
|
||||
this.timeout = setTimeout(() => {
|
||||
this.timeout = window.setTimeout(() => {
|
||||
if (!this.state.imgLoaded || !this.state.imgError) {
|
||||
this.setState({
|
||||
placeholder: Placeholder.Blurhash,
|
||||
|
|
|
@ -130,7 +130,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
if (codes.length > 0) {
|
||||
// Do this asynchronously: parsing code takes time and we don't
|
||||
// need to block the DOM update on it.
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
if (this.unmounted) return;
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
this.highlightCode(codes[i]);
|
||||
|
|
|
@ -111,8 +111,21 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
|
|||
const onStartVerification = useCallback(async () => {
|
||||
setRequesting(true);
|
||||
const cli = MatrixClientPeg.get();
|
||||
const roomId = await ensureDMExists(cli, member.userId);
|
||||
const verificationRequest_ = await cli.requestVerificationDM(member.userId, roomId);
|
||||
let verificationRequest_: VerificationRequest;
|
||||
try {
|
||||
const roomId = await ensureDMExists(cli, member.userId);
|
||||
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_);
|
||||
setPhase(verificationRequest_.phase);
|
||||
// Notify the RightPanelStore about this
|
||||
|
|
|
@ -127,7 +127,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.debounceCompletionsRequest = setTimeout(() => {
|
||||
this.debounceCompletionsRequest = window.setTimeout(() => {
|
||||
resolve(this.processQuery(query, selection));
|
||||
}, autocompleteDelay);
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
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 { 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
|
||||
// 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 anchor = <a href={this.props.link} target="_blank" rel="noreferrer noopener">{ title }</a>;
|
||||
|
|
|
@ -199,7 +199,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
// that the ScrollPanel listening to the resizeNotifier can
|
||||
// correctly measure it's new height and scroll down to keep
|
||||
// at the bottom if it already is
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
this.props.resizeNotifier.notifyTimelineHeightChanged();
|
||||
}, 100);
|
||||
}
|
||||
|
@ -395,7 +395,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
private onRecordingEndingSoon = ({ secondsLeft }) => {
|
||||
this.setState({ recordingTimeLeftSeconds: secondsLeft });
|
||||
setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000);
|
||||
window.setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000);
|
||||
};
|
||||
|
||||
private setStickerPickerOpen = (isStickerPickerOpen: boolean) => {
|
||||
|
@ -584,6 +584,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
setUpVoiceBroadcastPreRecording(
|
||||
this.props.room,
|
||||
MatrixClientPeg.get(),
|
||||
SdkContextClass.instance.voiceBroadcastPlaybacksStore,
|
||||
VoiceBroadcastRecordingsStore.instance(),
|
||||
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
|
||||
);
|
||||
|
|
|
@ -123,6 +123,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
}
|
||||
|
||||
const classes = classNames("mx_ReplyTile", {
|
||||
mx_ReplyTile_inline: msgType === MsgType.Emote,
|
||||
mx_ReplyTile_info: isInfoMessage && !mxEvent.isRedacted(),
|
||||
mx_ReplyTile_audio: msgType === MsgType.Audio,
|
||||
mx_ReplyTile_video: msgType === MsgType.Video,
|
||||
|
|
|
@ -99,7 +99,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
|||
// again and this time we want to show the newest breadcrumb because it'll be hidden
|
||||
// off screen for the animation.
|
||||
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) => {
|
||||
|
|
|
@ -385,7 +385,7 @@ const CallLayoutSelector: FC<CallLayoutSelectorProps> = ({ call }) => {
|
|||
"mx_RoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
|
||||
})}
|
||||
onClick={onClick}
|
||||
title={_t("Layout type")}
|
||||
title={_t("Change layout")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="layout"
|
||||
/>
|
||||
|
|
|
@ -747,13 +747,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
|
||||
public render(): React.ReactElement {
|
||||
const visibleTiles = this.renderVisibleTiles();
|
||||
const hidden = !this.state.rooms.length && !this.props.extraTiles?.length && this.props.alwaysVisible !== true;
|
||||
const classes = classNames({
|
||||
'mx_RoomSublist': true,
|
||||
'mx_RoomSublist_hasMenuOpen': !!this.state.contextMenuPosition,
|
||||
'mx_RoomSublist_minimized': this.props.isMinimized,
|
||||
'mx_RoomSublist_hidden': (
|
||||
!this.state.rooms.length && !this.props.extraTiles?.length && this.props.alwaysVisible !== true
|
||||
),
|
||||
'mx_RoomSublist_hidden': hidden,
|
||||
});
|
||||
|
||||
let content = null;
|
||||
|
@ -898,6 +897,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
ref={this.sublistRef}
|
||||
className={classes}
|
||||
role="group"
|
||||
aria-hidden={hidden}
|
||||
aria-label={this.props.label}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
|
|
|
@ -28,7 +28,7 @@ export function useIsFocused() {
|
|||
} else {
|
||||
// To avoid a blink when we switch mode between plain text and rich text mode
|
||||
// We delay the unfocused action
|
||||
timeoutIDRef.current = setTimeout(() => setIsFocused(false), 100);
|
||||
timeoutIDRef.current = window.setTimeout(() => setIsFocused(false), 100);
|
||||
}
|
||||
}, [setIsFocused, timeoutIDRef]);
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ export function focusComposer(
|
|||
if (timeoutId.current) {
|
||||
clearTimeout(timeoutId.current);
|
||||
}
|
||||
timeoutId.current = setTimeout(
|
||||
timeoutId.current = window.setTimeout(
|
||||
() => composerElement.current?.focus(),
|
||||
200,
|
||||
);
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
@ -66,11 +65,6 @@ interface IBridgeStateEvent {
|
|||
}
|
||||
|
||||
export default class BridgeTile extends React.PureComponent<IProps> {
|
||||
static propTypes = {
|
||||
ev: PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const content: IBridgeStateEvent = this.props.ev.getContent();
|
||||
// Validate
|
||||
|
|
|
@ -150,7 +150,7 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
|
|||
await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
|
||||
this.setState({ customThemeUrl: "", customThemeMessage: { text: _t("Theme added!"), isError: false } });
|
||||
|
||||
this.themeTimer = setTimeout(() => {
|
||||
this.themeTimer = window.setTimeout(() => {
|
||||
this.setState({ customThemeMessage: { text: "", isError: false } });
|
||||
}, 3000);
|
||||
};
|
||||
|
|
|
@ -64,12 +64,13 @@ const isDeviceSelected = (
|
|||
) => selectedDeviceIds.includes(deviceId);
|
||||
|
||||
// devices without timestamp metadata should be sorted last
|
||||
const sortDevicesByLatestActivity = (left: ExtendedDevice, right: ExtendedDevice) =>
|
||||
(right.last_seen_ts || 0) - (left.last_seen_ts || 0);
|
||||
const sortDevicesByLatestActivityThenDisplayName = (left: ExtendedDevice, right: ExtendedDevice) =>
|
||||
(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) =>
|
||||
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : [])
|
||||
.sort(sortDevicesByLatestActivity);
|
||||
.sort(sortDevicesByLatestActivityThenDisplayName);
|
||||
|
||||
const ALL_FILTER_ID = 'ALL';
|
||||
type DeviceFilterKey = DeviceSecurityVariation | typeof ALL_FILTER_ID;
|
||||
|
|
|
@ -324,12 +324,13 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
let privilegedUsersSection = <div>{ _t('No users have specific privileges in this room') }</div>;
|
||||
let mutedUsersSection;
|
||||
if (Object.keys(userLevels).length) {
|
||||
const privilegedUsers = [];
|
||||
const mutedUsers = [];
|
||||
const privilegedUsers: JSX.Element[] = [];
|
||||
const mutedUsers: JSX.Element[] = [];
|
||||
|
||||
Object.keys(userLevels).forEach((user) => {
|
||||
if (!Number.isInteger(userLevels[user])) { return; }
|
||||
const canChange = userLevels[user] < currentUserLevel && canChangeLevels;
|
||||
if (!Number.isInteger(userLevels[user])) return;
|
||||
const isMe = user === client.getUserId();
|
||||
const canChange = canChangeLevels && (userLevels[user] < currentUserLevel || isMe);
|
||||
if (userLevels[user] > defaultUserLevel) { // privileged
|
||||
privilegedUsers.push(
|
||||
<PowerSelector
|
||||
|
|
|
@ -19,7 +19,6 @@ import { sortBy } from "lodash";
|
|||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import BetaCard from "../../../beta/BetaCard";
|
||||
|
@ -28,24 +27,6 @@ import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
|
|||
import { LabGroup, labGroupNames } from "../../../../../settings/Settings";
|
||||
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 {
|
||||
showJumpToDate: boolean;
|
||||
showExploringPublicSpaces: boolean;
|
||||
|
@ -93,7 +74,7 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
|
|||
const groups = new EnhancedMap<LabGroup, JSX.Element[]>();
|
||||
labs.forEach(f => {
|
||||
groups.getOrCreate(SettingsStore.getLabGroup(f), []).push(
|
||||
<LabsSettingToggle featureId={f} key={f} />,
|
||||
<SettingsFlag level={SettingLevel.DEVICE} name={f} key={f} />,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -154,24 +135,42 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
|
|||
|
||||
return (
|
||||
<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'>
|
||||
{
|
||||
_t('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>.', {}, {
|
||||
'a': (sub) => {
|
||||
return <a
|
||||
href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
|
||||
rel='noreferrer noopener'
|
||||
target='_blank'
|
||||
>{ sub }</a>;
|
||||
},
|
||||
})
|
||||
_t(
|
||||
"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.",
|
||||
{ brand: SdkConfig.get("brand") },
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{ betaSection }
|
||||
{ labsSections }
|
||||
{ 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) => {
|
||||
return <a
|
||||
href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
|
||||
rel='noreferrer noopener'
|
||||
target='_blank'
|
||||
>{ sub }</a>;
|
||||
},
|
||||
})
|
||||
}
|
||||
</div>
|
||||
{ labsSections }
|
||||
</> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -127,7 +127,7 @@ const SessionManagerTab: React.FC = () => {
|
|||
const [expandedDeviceIds, setExpandedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
|
||||
const scrollIntoViewTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const scrollIntoViewTimeoutRef = useRef<number>();
|
||||
|
||||
const matrixClient = useContext(MatrixClientContext);
|
||||
const userId = matrixClient.getUserId();
|
||||
|
|
|
@ -57,7 +57,7 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
|
|||
async componentDidMount() {
|
||||
const { request } = this.props;
|
||||
if (request.timeout && request.timeout > 0) {
|
||||
this.intervalHandle = setInterval(() => {
|
||||
this.intervalHandle = window.setInterval(() => {
|
||||
let { counter } = this.state;
|
||||
counter = Math.max(0, counter - 1);
|
||||
this.setState({ counter });
|
||||
|
|
|
@ -55,7 +55,7 @@ export function UserOnboardingPage({ justRegistered = false }: Props) {
|
|||
const [showList, setShowList] = useState<boolean>(false);
|
||||
useEffect(() => {
|
||||
if (initialSyncComplete) {
|
||||
let handler: number | null = setTimeout(() => {
|
||||
let handler: number | null = window.setTimeout(() => {
|
||||
handler = null;
|
||||
setShowList(true);
|
||||
}, ANIMATION_DURATION);
|
||||
|
|
|
@ -43,7 +43,7 @@ interface GroupCallDurationProps {
|
|||
export const GroupCallDuration: FC<GroupCallDurationProps> = ({ groupCall }) => {
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setNow(Date.now()), 1000);
|
||||
const timer = window.setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -367,14 +367,14 @@ class PipView extends React.Component<IProps, IState> {
|
|||
const pipMode = true;
|
||||
let pipContent: CreatePipChildren | null = null;
|
||||
|
||||
if (this.props.voiceBroadcastPreRecording) {
|
||||
pipContent = this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording);
|
||||
}
|
||||
|
||||
if (this.props.voiceBroadcastPlayback) {
|
||||
pipContent = this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback);
|
||||
}
|
||||
|
||||
if (this.props.voiceBroadcastPreRecording) {
|
||||
pipContent = this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording);
|
||||
}
|
||||
|
||||
if (this.props.voiceBroadcastRecording) {
|
||||
pipContent = this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording);
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ export class MatrixDispatcher extends Dispatcher<ActionPayload> {
|
|||
// if you dispatch from within a dispatch, so rather than action
|
||||
// handlers having to worry about not calling anything that might
|
||||
// then dispatch, we just do dispatches asynchronously.
|
||||
setTimeout(super.dispatch.bind(this, payload), 0);
|
||||
window.setTimeout(super.dispatch.bind(this, payload), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { AllHtmlEntities } from 'html-entities';
|
||||
import { encode } from 'html-entities';
|
||||
import cheerio from 'cheerio';
|
||||
import escapeHtml from "escape-html";
|
||||
|
||||
|
@ -117,7 +117,7 @@ export function htmlSerializeFromMdIfNeeded(md: string, { forceHTML = false } =
|
|||
patternDefaults[patternName][patternType];
|
||||
|
||||
md = md.replace(RegExp(pattern, "gms"), function(m, p1, p2) {
|
||||
const p2e = AllHtmlEntities.encode(p2);
|
||||
const p2e = encode(p2);
|
||||
switch (patternType) {
|
||||
case "display":
|
||||
return `${p1}<div data-mx-maths="${p2e}">\n\n</div>\n\n`;
|
||||
|
|
|
@ -19,6 +19,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||
import { M_POLL_START, Optional } from "matrix-events-sdk";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { GroupCallIntent } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
|
||||
import EditorStateTransfer from "../utils/EditorStateTransfer";
|
||||
import { RoomPermalinkCreator } from "../utils/permalinks/Permalinks";
|
||||
|
@ -412,13 +413,9 @@ export function haveRendererForEvent(mxEvent: MatrixEvent, showHiddenEvents: boo
|
|||
return Boolean(mxEvent.getContent()['predecessor']);
|
||||
} else if (ElementCall.CALL_EVENT_TYPE.names.some(eventType => handler === STATE_EVENT_TILE_TYPES.get(eventType))) {
|
||||
const intent = mxEvent.getContent()['m.intent'];
|
||||
const prevContent = mxEvent.getPrevContent();
|
||||
// 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);
|
||||
const newlyStarted = Object.keys(mxEvent.getPrevContent()).length === 0;
|
||||
// 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) {
|
||||
return false;
|
||||
} else {
|
||||
|
|
|
@ -30,7 +30,7 @@ export function useDebouncedCallback<T extends any[]>(
|
|||
callback(...params);
|
||||
};
|
||||
if (enabled !== false) {
|
||||
handle = setTimeout(doSearch, DEBOUNCE_TIMEOUT);
|
||||
handle = window.setTimeout(doSearch, DEBOUNCE_TIMEOUT);
|
||||
return () => {
|
||||
if (handle) {
|
||||
clearTimeout(handle);
|
||||
|
|
|
@ -30,7 +30,7 @@ export const useTimeout = (handler: Handler, timeoutMs: number) => {
|
|||
|
||||
// Set up timer
|
||||
useEffect(() => {
|
||||
const timeoutID = setTimeout(() => {
|
||||
const timeoutID = window.setTimeout(() => {
|
||||
savedHandler.current();
|
||||
}, timeoutMs);
|
||||
return () => clearTimeout(timeoutID);
|
||||
|
@ -49,7 +49,7 @@ export const useInterval = (handler: Handler, intervalMs: number) => {
|
|||
|
||||
// Set up timer
|
||||
useEffect(() => {
|
||||
const intervalID = setInterval(() => {
|
||||
const intervalID = window.setInterval(() => {
|
||||
savedHandler.current();
|
||||
}, intervalMs);
|
||||
return () => clearInterval(intervalID);
|
||||
|
|
|
@ -28,7 +28,7 @@ export const useTimeoutToggle = (defaultValue: boolean, timeoutMs: number) => {
|
|||
|
||||
const toggle = () => {
|
||||
setValue(!defaultValue);
|
||||
timeoutId.current = setTimeout(() => setValue(defaultValue), timeoutMs);
|
||||
timeoutId.current = window.setTimeout(() => setValue(defaultValue), timeoutMs);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -68,7 +68,7 @@ function useUserOnboardingContextValue<T>(defaultValue: T, callback: (cli: Matri
|
|||
}
|
||||
setValue(await handler(cli));
|
||||
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));
|
||||
|
|
|
@ -660,6 +660,7 @@
|
|||
"Change input device": "Change input device",
|
||||
"Live": "Live",
|
||||
"Voice broadcast": "Voice broadcast",
|
||||
"Buffering…": "Buffering…",
|
||||
"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",
|
||||
"Your %(brand)s is misconfigured": "Your %(brand)s is misconfigured",
|
||||
|
@ -808,7 +809,7 @@
|
|||
"Yes": "Yes",
|
||||
"No": "No",
|
||||
"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": "Review",
|
||||
"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.",
|
||||
"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.",
|
||||
"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",
|
||||
"Message Pinning": "Message Pinning",
|
||||
"Threaded messaging": "Threaded messaging",
|
||||
|
@ -920,9 +922,11 @@
|
|||
"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.",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -932,15 +936,19 @@
|
|||
"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",
|
||||
"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)",
|
||||
"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",
|
||||
"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)",
|
||||
"Favourite Messages (under active development)": "Favourite Messages (under active development)",
|
||||
"Voice broadcast (under active development)": "Voice broadcast (under active development)",
|
||||
"Live Location Sharing": "Live Location Sharing",
|
||||
"Temporary implementation. Locations persist in room history.": "Temporary implementation. Locations persist in room history.",
|
||||
"Favourite Messages": "Favourite Messages",
|
||||
"Under active development.": "Under active development.",
|
||||
"Under active development": "Under active development",
|
||||
"Use new session manager": "Use 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.",
|
||||
|
@ -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 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",
|
||||
"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)",
|
||||
"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",
|
||||
|
@ -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.",
|
||||
"Clear cache and reload": "Clear cache and reload",
|
||||
"Keyboard": "Keyboard",
|
||||
"Labs": "Labs",
|
||||
"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>.",
|
||||
"Upcoming features": "Upcoming features",
|
||||
"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",
|
||||
"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.",
|
||||
|
@ -1950,7 +1961,7 @@
|
|||
"You do not have permission to start voice calls": "You do not have permission to start voice calls",
|
||||
"Freedom": "Freedom",
|
||||
"Spotlight": "Spotlight",
|
||||
"Layout type": "Layout type",
|
||||
"Change layout": "Change layout",
|
||||
"Forget room": "Forget room",
|
||||
"Hide Widgets": "Hide 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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -2560,6 +2573,7 @@
|
|||
"Join millions for free on the largest public server": "Join millions for free on the largest public server",
|
||||
"Homeserver": "Homeserver",
|
||||
"Help": "Help",
|
||||
"<w>WARNING:</w> <description/>": "<w>WARNING:</w> <description/>",
|
||||
"Choose a locale": "Choose a locale",
|
||||
"Continue with %(provider)s": "Continue with %(provider)s",
|
||||
"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",
|
||||
"Cancel All": "Cancel All",
|
||||
"Upload Error": "Upload Error",
|
||||
"Labs": "Labs",
|
||||
"Verify other device": "Verify other device",
|
||||
"Verification Request": "Verification Request",
|
||||
"Approve widget permissions": "Approve widget permissions",
|
||||
|
|
|
@ -377,7 +377,7 @@ export class JitsiCall extends Call {
|
|||
|
||||
this.participants = participants;
|
||||
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
|
||||
await this.addOurDevice();
|
||||
// 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}`);
|
||||
await this.addOurDevice();
|
||||
}, (this.STUCK_DEVICE_TIMEOUT_MS * 3) / 4);
|
||||
|
@ -647,7 +647,6 @@ export class ElementCall extends Call {
|
|||
client,
|
||||
);
|
||||
|
||||
this.on(CallEvent.ConnectionState, this.onConnectionState);
|
||||
this.on(CallEvent.Participants, this.onParticipants);
|
||||
groupCall.on(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants);
|
||||
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}`);
|
||||
}
|
||||
|
||||
this.groupCall.enteredViaAnotherSession = true;
|
||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||
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.ScreenshareRequest}`, this.onScreenshareRequest);
|
||||
super.setDisconnected();
|
||||
this.groupCall.enteredViaAnotherSession = false;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
WidgetStore.instance.removeVirtualWidget(this.widget.id, this.groupCall.room.roomId);
|
||||
this.off(CallEvent.ConnectionState, this.onConnectionState);
|
||||
this.off(CallEvent.Participants, this.onParticipants);
|
||||
this.groupCall.off(GroupCallEvent.ParticipantsChanged, this.onGroupCallParticipants);
|
||||
this.groupCall.off(GroupCallEvent.GroupCallStateChanged, this.onGroupCallState);
|
||||
|
@ -760,20 +760,6 @@ export class ElementCall extends Call {
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -782,15 +768,6 @@ export class ElementCall extends Call {
|
|||
&& 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 (
|
||||
participants: 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
|
||||
// probabilistically reduce event spam. If someone else beats us to it,
|
||||
// this timer will be automatically cleared upon the call's destruction.
|
||||
this.terminationTimer = setTimeout(
|
||||
this.terminationTimer = window.setTimeout(
|
||||
() => this.groupCall.terminate(),
|
||||
Math.random() * 6000 + 2000,
|
||||
);
|
||||
|
|
|
@ -154,7 +154,7 @@ export class IndexedDBLogStore {
|
|||
// @ts-ignore
|
||||
this.db = event.target.result;
|
||||
// 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();
|
||||
};
|
||||
|
||||
|
|
|
@ -206,7 +206,7 @@ export async function initSentry(sentryConfig: IConfigOptions["sentry"]): Promis
|
|||
new Sentry.Integrations.InboundFilters(),
|
||||
new Sentry.Integrations.FunctionToString(),
|
||||
new Sentry.Integrations.Breadcrumbs(),
|
||||
new Sentry.Integrations.UserAgent(),
|
||||
new Sentry.Integrations.HttpContext(),
|
||||
new Sentry.Integrations.Dedupe(),
|
||||
];
|
||||
|
||||
|
|
|
@ -122,13 +122,13 @@ export const labGroupNames: Record<LabGroup, string> = {
|
|||
[LabGroup.Developer]: _td("Developer"),
|
||||
};
|
||||
|
||||
export type SettingValueType = boolean |
|
||||
number |
|
||||
string |
|
||||
number[] |
|
||||
string[] |
|
||||
Record<string, unknown> |
|
||||
null;
|
||||
export type SettingValueType = boolean
|
||||
| number
|
||||
| string
|
||||
| number[]
|
||||
| string[]
|
||||
| Record<string, unknown>
|
||||
| null;
|
||||
|
||||
export interface IBaseSetting<T extends SettingValueType = SettingValueType> {
|
||||
isFeature?: false | undefined;
|
||||
|
@ -180,6 +180,9 @@ export interface IBaseSetting<T extends SettingValueType = SettingValueType> {
|
|||
extraSettings?: string[];
|
||||
requiresRefresh?: boolean;
|
||||
};
|
||||
|
||||
// Whether the setting should have a warning sign in the microcopy
|
||||
shouldWarn?: boolean;
|
||||
}
|
||||
|
||||
export interface IFeature extends Omit<IBaseSetting<boolean>, "isFeature"> {
|
||||
|
@ -245,8 +248,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
"feature_report_to_moderators": {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Moderation,
|
||||
displayName: _td("Report to moderators prototype. " +
|
||||
"In rooms that support moderation, the `report` button will let you report abuse to room moderators"),
|
||||
displayName: _td("Report to moderators"),
|
||||
description: _td(
|
||||
"In rooms that support moderation, "
|
||||
+"the “Report” button will let you report abuse to room moderators.",
|
||||
),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
|
@ -307,7 +313,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
"feature_wysiwyg_composer": {
|
||||
isFeature: true,
|
||||
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,
|
||||
default: false,
|
||||
},
|
||||
|
@ -321,7 +328,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
"feature_mjolnir": {
|
||||
isFeature: true,
|
||||
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,
|
||||
default: false,
|
||||
},
|
||||
|
@ -400,7 +408,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
isFeature: true,
|
||||
labsGroup: LabGroup.Rooms,
|
||||
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,
|
||||
},
|
||||
"feature_jump_to_date": {
|
||||
|
@ -425,7 +434,9 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
isFeature: true,
|
||||
labsGroup: LabGroup.Developer,
|
||||
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,
|
||||
controller: new SlidingSyncController(),
|
||||
},
|
||||
|
@ -453,23 +464,25 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
isFeature: true,
|
||||
labsGroup: LabGroup.Messaging,
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
displayName: _td(
|
||||
"Live Location Sharing (temporary implementation: locations persist in room history)",
|
||||
),
|
||||
displayName: _td("Live Location Sharing"),
|
||||
description: _td("Temporary implementation. Locations persist in room history."),
|
||||
shouldWarn: true,
|
||||
default: false,
|
||||
},
|
||||
"feature_favourite_messages": {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Messaging,
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
displayName: _td("Favourite Messages (under active development)"),
|
||||
displayName: _td("Favourite Messages"),
|
||||
description: _td("Under active development."),
|
||||
default: false,
|
||||
},
|
||||
[Features.VoiceBroadcast]: {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Messaging,
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
displayName: _td("Voice broadcast (under active development)"),
|
||||
displayName: _td("Voice broadcast"),
|
||||
description: _td("Under active development"),
|
||||
default: false,
|
||||
},
|
||||
"feature_new_device_manager": {
|
||||
|
@ -910,9 +923,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
},
|
||||
"lowBandwidth": {
|
||||
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,
|
||||
controller: new ReloadOnChangeController(),
|
||||
shouldWarn: true,
|
||||
},
|
||||
"fallbackICEServerAllowed": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
|
@ -1056,6 +1071,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: false,
|
||||
},
|
||||
"debug_legacy_call_handler": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: false,
|
||||
},
|
||||
"audioInputMuted": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: false,
|
||||
|
@ -1130,6 +1149,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
supportedLevels: LEVELS_UI_FEATURE,
|
||||
default: true,
|
||||
},
|
||||
[UIFeature.BulkUnverifiedSessionsReminder]: {
|
||||
supportedLevels: LEVELS_UI_FEATURE,
|
||||
default: true,
|
||||
},
|
||||
|
||||
// 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.
|
||||
|
|
|
@ -295,6 +295,16 @@ export default class SettingsStore {
|
|||
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"] {
|
||||
// consider a beta disabled if the config is explicitly set to false, in which case treat as normal Labs flag
|
||||
if (SettingsStore.isFeature(settingName)
|
||||
|
@ -355,7 +365,7 @@ export default class SettingsStore {
|
|||
public static getValueAt(
|
||||
level: SettingLevel,
|
||||
settingName: string,
|
||||
roomId: string = null,
|
||||
roomId: string | null = null,
|
||||
explicit = false,
|
||||
excludeDefault = false,
|
||||
): any {
|
||||
|
@ -420,7 +430,7 @@ export default class SettingsStore {
|
|||
private static getFinalValue(
|
||||
setting: ISetting,
|
||||
level: SettingLevel,
|
||||
roomId: string,
|
||||
roomId: string | null,
|
||||
calculatedValue: any,
|
||||
calculatedAtLevel: SettingLevel,
|
||||
): any {
|
||||
|
|
|
@ -31,6 +31,7 @@ export enum UIFeature {
|
|||
AdvancedSettings = "UIFeature.advancedSettings",
|
||||
RoomHistorySettings = "UIFeature.roomHistorySettings",
|
||||
TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates",
|
||||
BulkUnverifiedSessionsReminder = "UIFeature.BulkUnverifiedSessionsReminder",
|
||||
}
|
||||
|
||||
export enum UIComponent {
|
||||
|
|
|
@ -39,7 +39,7 @@ export default abstract class SettingController {
|
|||
*/
|
||||
public getValueOverride(
|
||||
level: SettingLevel,
|
||||
roomId: string,
|
||||
roomId: string | null,
|
||||
calculatedValue: any,
|
||||
calculatedAtLevel: SettingLevel,
|
||||
): any {
|
||||
|
|
|
@ -437,7 +437,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
return;
|
||||
}
|
||||
|
||||
this.locationInterval = setInterval(() => {
|
||||
this.locationInterval = window.setInterval(() => {
|
||||
if (!this.lastPublishedPositionTimestamp) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -228,7 +228,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
|
|||
if (!room) {
|
||||
logger.warn(`Live timeline event ${eventPayload.event.getId()} received without associated room`);
|
||||
logger.warn(`Queuing failed room update for retry as a result.`);
|
||||
setTimeout(async () => {
|
||||
window.setTimeout(async () => {
|
||||
const updatedRoom = this.matrixClient.getRoom(roomId);
|
||||
await tryUpdate(updatedRoom);
|
||||
}, 100); // 100ms should be enough for the room to show up
|
||||
|
|
|
@ -122,9 +122,6 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
|||
this.allowedCapabilities.add(
|
||||
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomMember).raw,
|
||||
);
|
||||
this.allowedCapabilities.add(
|
||||
WidgetEventCapability.forStateEvent(EventDirection.Send, "org.matrix.msc3401.call").raw,
|
||||
);
|
||||
this.allowedCapabilities.add(
|
||||
WidgetEventCapability.forStateEvent(EventDirection.Receive, "org.matrix.msc3401.call").raw,
|
||||
);
|
||||
|
|
|
@ -298,7 +298,7 @@ export async function setTheme(theme?: string): Promise<void> {
|
|||
|
||||
// In case of theme toggling (white => black => white)
|
||||
// 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()) {
|
||||
clearInterval(intervalId);
|
||||
styleSheet.onload = undefined;
|
||||
|
|
|
@ -38,7 +38,7 @@ export const showToast = (deviceIds: Set<string>) => {
|
|||
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: TOAST_KEY,
|
||||
title: _t("You have unverified logins"),
|
||||
title: _t("You have unverified sessions"),
|
||||
icon: "verification_warning",
|
||||
props: {
|
||||
description: _t("Review to ensure your account is safe"),
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import filesize from 'filesize';
|
||||
import { filesize } from 'filesize';
|
||||
|
||||
import { IMediaEventContent } from '../customisations/models/IMediaEventContent';
|
||||
import { _t } from '../languageHandler';
|
||||
|
|
|
@ -241,7 +241,7 @@ export default class MultiInviter {
|
|||
break;
|
||||
case "M_LIMIT_EXCEEDED":
|
||||
// we're being throttled so wait a bit & try again
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
this.doInvite(address, ignoreProfile).then(resolve, reject);
|
||||
}, 5000);
|
||||
return;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue