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

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

View file

@ -43,7 +43,7 @@ jobs:
- name: Get commit details
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: |

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,6 +21,10 @@ import { SynapseInstance } from "../../plugins/synapsedocker";
describe("Login", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.stubDefaultServer();
});
afterEach(() => {
cy.stopSynapse(synapse);
});

View file

@ -22,6 +22,7 @@ describe("Registration", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.stubDefaultServer();
cy.visit("/#/register");
cy.startSynapse("consent").then(data => {
synapse = data;

View file

@ -0,0 +1,48 @@
{
"flows": [
{
"type": "m.login.sso",
"identity_providers": [
{
"id": "oidc-github",
"name": "GitHub",
"icon": "mxc://matrix.org/sVesTtrFDTpXRbYfpahuJsKP",
"brand": "github"
},
{
"id": "oidc-google",
"name": "Google",
"icon": "mxc://matrix.org/ZlnaaZNPxtUuQemvgQzlOlkz",
"brand": "google"
},
{
"id": "oidc-gitlab",
"name": "GitLab",
"icon": "mxc://matrix.org/MCVOEmFgVieKFshPxmnejWOq",
"brand": "gitlab"
},
{
"id": "oidc-facebook",
"name": "Facebook",
"icon": "mxc://matrix.org/nsyeLIgzxazZmJadflMAsAWG",
"brand": "facebook"
},
{
"id": "oidc-apple",
"name": "Apple",
"icon": "mxc://matrix.org/QQKNSOdLiMHtJhzeAObmkFiU",
"brand": "apple"
}
]
},
{
"type": "m.login.token"
},
{
"type": "m.login.password"
},
{
"type": "m.login.application_service"
}
]
}

View file

@ -0,0 +1,39 @@
{
"versions": [
"r0.0.1",
"r0.1.0",
"r0.2.0",
"r0.3.0",
"r0.4.0",
"r0.5.0",
"r0.6.0",
"r0.6.1",
"v1.1",
"v1.2",
"v1.3",
"v1.4"
],
"unstable_features": {
"org.matrix.label_based_filtering": true,
"org.matrix.e2e_cross_signing": true,
"org.matrix.msc2432": true,
"uk.half-shot.msc2666.mutual_rooms": true,
"io.element.e2ee_forced.public": false,
"io.element.e2ee_forced.private": false,
"io.element.e2ee_forced.trusted_private": false,
"org.matrix.msc3026.busy_presence": false,
"org.matrix.msc2285.stable": true,
"org.matrix.msc3827.stable": true,
"org.matrix.msc2716": false,
"org.matrix.msc3030": false,
"org.matrix.msc3440.stable": true,
"org.matrix.msc3771": true,
"org.matrix.msc3773": false,
"fi.mau.msc2815": false,
"org.matrix.msc3882": false,
"org.matrix.msc3881": false,
"org.matrix.msc3874": false,
"org.matrix.msc3886": false,
"org.matrix.msc3912": false
}
}

View file

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

View file

@ -0,0 +1 @@
{}

View file

@ -78,6 +78,7 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
const username = Cypress._.uniqueId("userId_");
const 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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -119,7 +119,7 @@ export default class PasswordReset {
this.checkEmailLinkClicked()
.then(() => resolve())
.catch(() => {
setTimeout(
window.setTimeout(
() => this.tryCheckEmailLinkClicked(resolve),
CHECK_EMAIL_VERIFIED_POLL_INTERVAL,
);

View file

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

View file

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

View file

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

View file

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

View file

@ -103,7 +103,7 @@ export default class EmojiProvider extends AutocompleteProvider {
return []; // don't give any suggestions if the user doesn't want them
}
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() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -35,7 +35,7 @@ export function UseCaseSelection({ onFinished }: Props) {
// Call onFinished 1.5s after `selection` becomes truthy, to give time for the animation to run
useEffect(() => {
if (selection) {
let handler: number | null = setTimeout(() => {
let handler: number | null = window.setTimeout(() => {
handler = null;
onFinished(selection);
}, TIMEOUT);

View file

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

View file

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

View file

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

View file

@ -270,6 +270,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
// Set a placeholder image when we can't decrypt the image.
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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -99,7 +99,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
// again and this time we want to show the newest breadcrumb because it'll be hidden
// 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) => {

View file

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

View file

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

View file

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

View file

@ -37,7 +37,7 @@ export function focusComposer(
if (timeoutId.current) {
clearTimeout(timeoutId.current);
}
timeoutId.current = setTimeout(
timeoutId.current = window.setTimeout(
() => composerElement.current?.focus(),
200,
);

View file

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

View file

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

View file

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

View file

@ -324,12 +324,13 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
let privilegedUsersSection = <div>{ _t('No users have specific privileges in this room') }</div>;
let 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,6 +19,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
import { 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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,6 +31,7 @@ export enum UIFeature {
AdvancedSettings = "UIFeature.advancedSettings",
RoomHistorySettings = "UIFeature.roomHistorySettings",
TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates",
BulkUnverifiedSessionsReminder = "UIFeature.BulkUnverifiedSessionsReminder",
}
export enum UIComponent {

View file

@ -39,7 +39,7 @@ export default abstract class SettingController {
*/
public getValueOverride(
level: SettingLevel,
roomId: string,
roomId: string | null,
calculatedValue: any,
calculatedAtLevel: SettingLevel,
): any {

View file

@ -437,7 +437,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
return;
}
this.locationInterval = setInterval(() => {
this.locationInterval = window.setInterval(() => {
if (!this.lastPublishedPositionTimestamp) {
return;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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