Apply prettier formatting

This commit is contained in:
Michael Weimann 2022-12-12 12:24:14 +01:00
parent 1cac306093
commit 526645c791
No known key found for this signature in database
GPG key ID: 53F535A266BB9584
1576 changed files with 65385 additions and 62478 deletions

View file

@ -17,8 +17,8 @@ limitations under the License.
import React, { JSXElementConstructor } from "react";
// Based on https://stackoverflow.com/a/53229857/3532235
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>]?: never};
export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
export type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
export type XOR<T, U> = T | U extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
@ -26,28 +26,35 @@ export type ReactAnyComponent = React.Component | React.ExoticComponent;
// Utility type for string dot notation for accessing nested object properties
// Based on https://stackoverflow.com/a/58436959
type Join<K, P> = K extends string | number ?
P extends string | number ?
`${K}${"" extends P ? "" : "."}${P}`
: never : never;
type Join<K, P> = K extends string | number
? P extends string | number
? `${K}${"" extends P ? "" : "."}${P}`
: never
: never;
type Prev = [never, 0, 1, 2, 3, ...0[]];
export type Leaves<T, D extends number = 3> = [D] extends [never] ? never : T extends object ?
{ [K in keyof T]-?: Join<K, Leaves<T[K], Prev[D]>> }[keyof T] : "";
export type Leaves<T, D extends number = 3> = [D] extends [never]
? never
: T extends object
? { [K in keyof T]-?: Join<K, Leaves<T[K], Prev[D]>> }[keyof T]
: "";
export type RecursivePartial<T> = {
[P in keyof T]?:
T[P] extends (infer U)[] ? RecursivePartial<U>[] :
T[P] extends object ? RecursivePartial<T[P]> :
T[P];
[P in keyof T]?: T[P] extends (infer U)[]
? RecursivePartial<U>[]
: T[P] extends object
? RecursivePartial<T[P]>
: T[P];
};
// Inspired by https://stackoverflow.com/a/60206860
export type KeysWithObjectShape<Input> = {
[P in keyof Input]: Input[P] extends object
// Arrays are counted as objects - exclude them
? (Input[P] extends Array<unknown> ? never : P)
? // Arrays are counted as objects - exclude them
Input[P] extends Array<unknown>
? never
: P
: never;
}[keyof Input];

View file

@ -26,8 +26,7 @@ declare module "diff-dom" {
newValue: string;
}
interface IOpts {
}
interface IOpts {}
export class DiffDOM {
public constructor(opts?: IOpts);

View file

@ -73,7 +73,7 @@ declare global {
// https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029#issuecomment-869224737
// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
OffscreenCanvas?: {
new(width: number, height: number): OffscreenCanvas;
new (width: number, height: number): OffscreenCanvas;
};
mxContentMessages: ContentMessages;
@ -149,10 +149,7 @@ declare global {
// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
interface OffscreenCanvas {
convertToBlob(opts?: {
type?: string;
quality?: number;
}): Promise<Blob>;
convertToBlob(opts?: { type?: string; quality?: number }): Promise<Blob>;
}
interface HTMLAudioElement {
@ -201,11 +198,7 @@ declare global {
// https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278
interface AudioWorkletProcessor {
readonly port: MessagePort;
process(
inputs: Float32Array[][],
outputs: Float32Array[][],
parameters: Record<string, Float32Array>
): boolean;
process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record<string, Float32Array>): boolean;
}
// https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278
@ -222,11 +215,9 @@ declare global {
// https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278
function registerProcessor(
name: string,
processorCtor: (new (
options?: AudioWorkletNodeOptions
) => AudioWorkletProcessor) & {
processorCtor: (new (options?: AudioWorkletNodeOptions) => AudioWorkletProcessor) & {
parameterDescriptors?: AudioParamDescriptor[];
}
},
);
// eslint-disable-next-line no-var

View file

@ -21,15 +21,33 @@ export function polyfillTouchEvent() {
if (!window.TouchEvent) {
// We have no intention of actually using this, so just lie.
window.TouchEvent = class TouchEvent extends UIEvent {
public get altKey(): boolean { return false; }
public get changedTouches(): any { return []; }
public get ctrlKey(): boolean { return false; }
public get metaKey(): boolean { return false; }
public get shiftKey(): boolean { return false; }
public get targetTouches(): any { return []; }
public get touches(): any { return []; }
public get rotation(): number { return 0.0; }
public get scale(): number { return 0.0; }
public get altKey(): boolean {
return false;
}
public get changedTouches(): any {
return [];
}
public get ctrlKey(): boolean {
return false;
}
public get metaKey(): boolean {
return false;
}
public get shiftKey(): boolean {
return false;
}
public get targetTouches(): any {
return [];
}
public get touches(): any {
return [];
}
public get rotation(): number {
return 0.0;
}
public get scale(): number {
return 0.0;
}
constructor(eventType: string, params?: any) {
super(eventType, params);
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
declare module '!!raw-loader!*' {
declare module "!!raw-loader!*" {
const contents: string;
export default contents;
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import sanitizeHtml from 'sanitize-html';
import sanitizeHtml from "sanitize-html";
export interface IExtendedSanitizeOptions extends sanitizeHtml.IOptions {
// This option only exists in 2.x RCs so far, so not yet present in the

View file

@ -18,10 +18,10 @@ limitations under the License.
import { IRequestMsisdnTokenResponse, IRequestTokenResponse } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from './MatrixClientPeg';
import Modal from './Modal';
import { _t } from './languageHandler';
import IdentityAuthClient from './IdentityAuthClient';
import { MatrixClientPeg } from "./MatrixClientPeg";
import Modal from "./Modal";
import { _t } from "./languageHandler";
import IdentityAuthClient from "./IdentityAuthClient";
import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents";
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
@ -58,17 +58,22 @@ export default class AddThreepid {
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
*/
public addEmailAddress(emailAddress: string): Promise<IRequestTokenResponse> {
return MatrixClientPeg.get().requestAdd3pidEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
this.sessionId = res.sid;
return res;
}, function(err) {
if (err.errcode === 'M_THREEPID_IN_USE') {
err.message = _t('This email address is already in use');
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
throw err;
});
return MatrixClientPeg.get()
.requestAdd3pidEmailToken(emailAddress, this.clientSecret, 1)
.then(
(res) => {
this.sessionId = res.sid;
return res;
},
function (err) {
if (err.errcode === "M_THREEPID_IN_USE") {
err.message = _t("This email address is already in use");
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
throw err;
},
);
}
/**
@ -83,20 +88,22 @@ export default class AddThreepid {
// For separate bind, request a token directly from the IS.
const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken();
return MatrixClientPeg.get().requestEmailToken(
emailAddress, this.clientSecret, 1,
undefined, identityAccessToken,
).then((res) => {
this.sessionId = res.sid;
return res;
}, function(err) {
if (err.errcode === 'M_THREEPID_IN_USE') {
err.message = _t('This email address is already in use');
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
throw err;
});
return MatrixClientPeg.get()
.requestEmailToken(emailAddress, this.clientSecret, 1, undefined, identityAccessToken)
.then(
(res) => {
this.sessionId = res.sid;
return res;
},
function (err) {
if (err.errcode === "M_THREEPID_IN_USE") {
err.message = _t("This email address is already in use");
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
throw err;
},
);
} else {
// For tangled bind, request a token via the HS.
return this.addEmailAddress(emailAddress);
@ -111,20 +118,23 @@ export default class AddThreepid {
* @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken().
*/
public addMsisdn(phoneCountry: string, phoneNumber: string): Promise<IRequestMsisdnTokenResponse> {
return MatrixClientPeg.get().requestAdd3pidMsisdnToken(
phoneCountry, phoneNumber, this.clientSecret, 1,
).then((res) => {
this.sessionId = res.sid;
this.submitUrl = res.submit_url;
return res;
}, function(err) {
if (err.errcode === 'M_THREEPID_IN_USE') {
err.message = _t('This phone number is already in use');
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
throw err;
});
return MatrixClientPeg.get()
.requestAdd3pidMsisdnToken(phoneCountry, phoneNumber, this.clientSecret, 1)
.then(
(res) => {
this.sessionId = res.sid;
this.submitUrl = res.submit_url;
return res;
},
function (err) {
if (err.errcode === "M_THREEPID_IN_USE") {
err.message = _t("This phone number is already in use");
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
throw err;
},
);
}
/**
@ -140,20 +150,22 @@ export default class AddThreepid {
// For separate bind, request a token directly from the IS.
const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken();
return MatrixClientPeg.get().requestMsisdnToken(
phoneCountry, phoneNumber, this.clientSecret, 1,
undefined, identityAccessToken,
).then((res) => {
this.sessionId = res.sid;
return res;
}, function(err) {
if (err.errcode === 'M_THREEPID_IN_USE') {
err.message = _t('This phone number is already in use');
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
throw err;
});
return MatrixClientPeg.get()
.requestMsisdnToken(phoneCountry, phoneNumber, this.clientSecret, 1, undefined, identityAccessToken)
.then(
(res) => {
this.sessionId = res.sid;
return res;
},
function (err) {
if (err.errcode === "M_THREEPID_IN_USE") {
err.message = _t("This phone number is already in use");
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
throw err;
},
);
} else {
// For tangled bind, request a token via the HS.
return this.addMsisdn(phoneCountry, phoneNumber);
@ -194,8 +206,10 @@ export default class AddThreepid {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
body: _t("Confirm adding this email address by using " +
"Single Sign On to prove your identity."),
body: _t(
"Confirm adding this email address by using " +
"Single Sign On to prove your identity.",
),
continueText: _t("Single Sign On"),
continueKind: "primary",
},
@ -220,15 +234,18 @@ export default class AddThreepid {
}
}
} else {
await MatrixClientPeg.get().addThreePid({
sid: this.sessionId,
client_secret: this.clientSecret,
id_server: getIdServerDomain(),
}, this.bind);
await MatrixClientPeg.get().addThreePid(
{
sid: this.sessionId,
client_secret: this.clientSecret,
id_server: getIdServerDomain(),
},
this.bind,
);
}
} catch (err) {
if (err.httpStatus === 401) {
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
err.message = _t("Failed to verify email address: make sure you clicked the link in the email");
} else if (err.httpStatus) {
err.message += ` (Status ${err.httpStatus})`;
}
@ -240,7 +257,7 @@ export default class AddThreepid {
* @param {{type: string, session?: string}} auth UI auth object
* @return {Promise<Object>} Response from /3pid/add call (in current spec, an empty object)
*/
private makeAddThreepidOnlyRequest = (auth?: {type: string, session?: string}): Promise<{}> => {
private makeAddThreepidOnlyRequest = (auth?: { type: string; session?: string }): Promise<{}> => {
return MatrixClientPeg.get().addThreePidOnly({
sid: this.sessionId,
client_secret: this.clientSecret,
@ -258,8 +275,7 @@ export default class AddThreepid {
*/
public async haveMsisdnToken(msisdnToken: string): Promise<any[]> {
const authClient = new IdentityAuthClient();
const supportsSeparateAddAndBind =
await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind();
const supportsSeparateAddAndBind = await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind();
let result;
if (this.submitUrl) {
@ -307,8 +323,9 @@ export default class AddThreepid {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
body: _t("Confirm adding this phone number by using " +
"Single Sign On to prove your identity."),
body: _t(
"Confirm adding this phone number by using " + "Single Sign On to prove your identity.",
),
continueText: _t("Single Sign On"),
continueKind: "primary",
},
@ -333,11 +350,14 @@ export default class AddThreepid {
}
}
} else {
await MatrixClientPeg.get().addThreePid({
sid: this.sessionId,
client_secret: this.clientSecret,
id_server: getIdServerDomain(),
}, this.bind);
await MatrixClientPeg.get().addThreePid(
{
sid: this.sessionId,
client_secret: this.clientSecret,
id_server: getIdServerDomain(),
},
this.bind,
);
}
}
}

View file

@ -17,7 +17,7 @@ limitations under the License.
import React, { ComponentType } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from './languageHandler';
import { _t } from "./languageHandler";
import { IDialogProps } from "./components/views/dialogs/IDialogProps";
import BaseDialog from "./components/views/dialogs/BaseDialog";
import DialogButtons from "./components/views/elements/DialogButtons";
@ -50,21 +50,23 @@ export default class AsyncWrapper extends React.Component<IProps, IState> {
componentDidMount() {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/element-web/issues/3148
logger.log('Starting load of AsyncWrapper for modal');
this.props.prom.then((result) => {
if (this.unmounted) return;
logger.log("Starting load of AsyncWrapper for modal");
this.props.prom
.then((result) => {
if (this.unmounted) return;
// Take the 'default' member if it's there, then we support
// passing in just an import()ed module, since ES6 async import
// always returns a module *namespace*.
const component = (result as AsyncImport<ComponentType>).default
? (result as AsyncImport<ComponentType>).default
: result as ComponentType;
this.setState({ component });
}).catch((e) => {
logger.warn('AsyncWrapper promise failed', e);
this.setState({ error: e });
});
// Take the 'default' member if it's there, then we support
// passing in just an import()ed module, since ES6 async import
// always returns a module *namespace*.
const component = (result as AsyncImport<ComponentType>).default
? (result as AsyncImport<ComponentType>).default
: (result as ComponentType);
this.setState({ component });
})
.catch((e) => {
logger.warn("AsyncWrapper promise failed", e);
this.setState({ error: e });
});
}
componentWillUnmount() {
@ -80,17 +82,19 @@ export default class AsyncWrapper extends React.Component<IProps, IState> {
const Component = this.state.component;
return <Component {...this.props} />;
} else if (this.state.error) {
return <BaseDialog onFinished={this.props.onFinished} title={_t("Error")}>
{ _t("Unable to load! Check your network connectivity and try again.") }
<DialogButtons primaryButton={_t("Dismiss")}
onPrimaryButtonClick={this.onWrapperCancelClick}
hasCancel={false}
/>
</BaseDialog>;
return (
<BaseDialog onFinished={this.props.onFinished} title={_t("Error")}>
{_t("Unable to load! Check your network connectivity and try again.")}
<DialogButtons
primaryButton={_t("Dismiss")}
onPrimaryButtonClick={this.onWrapperCancelClick}
hasCancel={false}
/>
</BaseDialog>
);
} else {
// show a spinner until the component is loaded.
return <Spinner />;
}
}
}

View file

@ -20,7 +20,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
import { split } from "lodash";
import DMRoomMap from './utils/DMRoomMap';
import DMRoomMap from "./utils/DMRoomMap";
import { mediaFromMxc } from "./customisations/Media";
import { isLocalRoom } from "./utils/localRoom/isLocalRoom";
@ -39,7 +39,7 @@ export function avatarUrlForMember(
// member can be null here currently since on invites, the JS SDK
// does not have enough info to build a RoomMember object for
// the inviter.
url = defaultAvatarUrlForString(member ? member.userId : '');
url = defaultAvatarUrlForString(member ? member.userId : "");
}
return url;
}
@ -55,10 +55,15 @@ export function avatarUrlForUser(
}
function isValidHexColor(color: string): boolean {
return typeof color === "string" &&
return (
typeof color === "string" &&
(color.length === 7 || color.length === 9) &&
color.charAt(0) === "#" &&
!color.slice(1).split("").some(c => isNaN(parseInt(c, 16)));
!color
.slice(1)
.split("")
.some((c) => isNaN(parseInt(c, 16)))
);
}
function urlForColor(color: string): string {
@ -83,7 +88,7 @@ const colorToDataURLCache = new Map<string, string>();
export function defaultAvatarUrlForString(s: string): string {
if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake
const defaultColors = ['#0DBD8B', '#368bd6', '#ac3ba8'];
const defaultColors = ["#0DBD8B", "#368bd6", "#ac3ba8"];
let total = 0;
for (let i = 0; i < s.length; ++i) {
total += s.charCodeAt(i);
@ -124,7 +129,7 @@ export function getInitialLetter(name: string): string {
}
const initial = name[0];
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
if ((initial === "@" || initial === "#" || initial === "+") && name[1]) {
name = name.substring(1);
}
@ -143,10 +148,7 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
if (room.isSpaceRoom()) return null;
// If the room is not a DM don't fallback to a member avatar
if (
!DMRoomMap.shared().getUserIdForRoomId(room.roomId)
&& !(isLocalRoom(room))
) {
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId) && !isLocalRoom(room)) {
return null;
}

View file

@ -23,8 +23,8 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import dis from './dispatcher/dispatcher';
import BaseEventIndexManager from './indexing/BaseEventIndexManager';
import dis from "./dispatcher/dispatcher";
import BaseEventIndexManager from "./indexing/BaseEventIndexManager";
import { ActionPayload } from "./dispatcher/payloads";
import { CheckUpdatesPayload } from "./dispatcher/payloads/CheckUpdatesPayload";
import { Action } from "./dispatcher/actions";
@ -80,7 +80,7 @@ export default abstract class BasePlatform {
protected onAction = (payload: ActionPayload): void => {
switch (payload.action) {
case 'on_client_not_viable':
case "on_client_not_viable":
case Action.OnLoggedOut:
this.setNotificationCount(0);
break;
@ -200,7 +200,7 @@ export default abstract class BasePlatform {
body: msg,
silent: true, // we play our own sounds
};
if (avatarUrl) notifBody['icon'] = avatarUrl;
if (avatarUrl) notifBody["icon"] = avatarUrl;
const notification = new window.Notification(title, notifBody);
notification.onclick = () => {
@ -376,7 +376,8 @@ export default abstract class BasePlatform {
try {
const key = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: data.iv, additionalData }, data.cryptoKey,
{ name: "AES-GCM", iv: data.iv, additionalData },
data.cryptoKey,
data.encrypted,
);
return encodeUnpaddedBase64(key);
@ -400,9 +401,10 @@ export default abstract class BasePlatform {
const crypto = window.crypto;
const randomArray = new Uint8Array(32);
crypto.getRandomValues(randomArray);
const cryptoKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"],
);
const cryptoKey = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, [
"encrypt",
"decrypt",
]);
const iv = new Uint8Array(32);
crypto.getRandomValues(iv);
@ -415,9 +417,7 @@ export default abstract class BasePlatform {
additionalData[userId.length + 1 + i] = deviceId.charCodeAt(i);
}
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv, additionalData }, cryptoKey, randomArray,
);
const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, cryptoKey, randomArray);
try {
await idbSave("pickleKey", [userId, deviceId], { encrypted, iv, cryptoKey });

View file

@ -57,4 +57,3 @@ export class BlurhashEncoder {
return deferred.promise;
}
}

View file

@ -27,9 +27,9 @@ import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
import { removeElement } from "matrix-js-sdk/src/utils";
import { IEncryptedFile, IMediaEventContent, IMediaEventInfo } from "./customisations/models/IMediaEventContent";
import dis from './dispatcher/dispatcher';
import { _t } from './languageHandler';
import Modal from './Modal';
import dis from "./dispatcher/dispatcher";
import { _t } from "./languageHandler";
import Modal from "./Modal";
import Spinner from "./components/views/elements/Spinner";
import { Action } from "./dispatcher/actions";
import {
@ -73,11 +73,11 @@ async function loadImageElement(imageFile: File) {
const img = new Image();
const objectUrl = URL.createObjectURL(imageFile);
const imgPromise = new Promise((resolve, reject) => {
img.onload = function() {
img.onload = function () {
URL.revokeObjectURL(objectUrl);
resolve(img);
};
img.onerror = function(e) {
img.onerror = function (e) {
reject(e);
};
});
@ -92,11 +92,11 @@ async function loadImageElement(imageFile: File) {
// Thus we could slice the file down to only sniff the first 0x1000
// bytes (but this makes extractPngChunks choke on the corrupt file)
const headers = imageFile; //.slice(0, 0x1000);
parsePromise = readFileAsArrayBuffer(headers).then(arrayBuffer => {
parsePromise = readFileAsArrayBuffer(headers).then((arrayBuffer) => {
const buffer = new Uint8Array(arrayBuffer);
const chunks = extractPngChunks(buffer);
for (const chunk of chunks) {
if (chunk.name === 'pHYs') {
if (chunk.name === "pHYs") {
if (chunk.data.byteLength !== PHYS_HIDPI.length) return;
return chunk.data.every((val, i) => val === PHYS_HIDPI[i]);
}
@ -106,8 +106,8 @@ async function loadImageElement(imageFile: File) {
}
const [hidpi] = await Promise.all([parsePromise, imgPromise]);
const width = hidpi ? (img.width >> 1) : img.width;
const height = hidpi ? (img.height >> 1) : img.height;
const width = hidpi ? img.width >> 1 : img.width;
const height = hidpi ? img.height >> 1 : img.height;
return { width, height, img };
}
@ -154,7 +154,7 @@ async function infoForImageFile(
imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL ||
// thumbnail is not sufficiently smaller than original
(sizeDifference <= IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE &&
sizeDifference <= (imageFile.size * IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT))
sizeDifference <= imageFile.size * IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT)
) {
delete imageInfo["thumbnail_info"];
return imageInfo;
@ -185,13 +185,13 @@ function loadVideoElement(videoFile: File): Promise<HTMLVideoElement> {
const reader = new FileReader();
reader.onload = function(ev) {
reader.onload = function (ev) {
// Wait until we have enough data to thumbnail the first frame.
video.onloadeddata = async function() {
video.onloadeddata = async function () {
resolve(video);
video.pause();
};
video.onerror = function(e) {
video.onerror = function (e) {
reject(e);
};
@ -206,7 +206,7 @@ function loadVideoElement(videoFile: File): Promise<HTMLVideoElement> {
video.load();
video.play();
};
reader.onerror = function(e) {
reader.onerror = function (e) {
reject(e);
};
reader.readAsDataURL(videoFile);
@ -229,16 +229,19 @@ function infoForVideoFile(
const thumbnailType = "image/jpeg";
let videoInfo: Partial<IMediaEventInfo>;
return loadVideoElement(videoFile).then((video) => {
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
}).then((result) => {
videoInfo = result.info;
return uploadFile(matrixClient, roomId, result.thumbnail);
}).then((result) => {
videoInfo.thumbnail_url = result.url;
videoInfo.thumbnail_file = result.file;
return videoInfo;
});
return loadVideoElement(videoFile)
.then((video) => {
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
})
.then((result) => {
videoInfo = result.info;
return uploadFile(matrixClient, roomId, result.thumbnail);
})
.then((result) => {
videoInfo.thumbnail_url = result.url;
videoInfo.thumbnail_file = result.file;
return videoInfo;
});
}
/**
@ -250,10 +253,10 @@ function infoForVideoFile(
function readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function(e) {
reader.onload = function (e) {
resolve(e.target.result as ArrayBuffer);
};
reader.onerror = function(e) {
reader.onerror = function (e) {
reject(e);
};
reader.readAsArrayBuffer(file);
@ -280,7 +283,7 @@ export async function uploadFile(
file: File | Blob,
progressHandler?: UploadOpts["progressHandler"],
controller?: AbortController,
): Promise<{ url?: string, file?: IEncryptedFile }> {
): Promise<{ url?: string; file?: IEncryptedFile }> {
const abortController = controller ?? new AbortController();
// If the room is encrypted then encrypt the file before uploading it.
@ -357,13 +360,14 @@ export default class ContentMessages {
context = TimelineRenderingType.Room,
): Promise<void> {
if (matrixClient.isGuest()) {
dis.dispatch({ action: 'require_registration' });
dis.dispatch({ action: "require_registration" });
return;
}
const replyToEvent = SdkContextClass.instance.roomViewStore.getQuotingEvent();
if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
if (!this.mediaConfig) {
// hot-path optimization to not flash a spinner if we don't need to
const modal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner");
await this.ensureMediaConfigFetched(matrixClient);
modal.close();
}
@ -410,16 +414,8 @@ export default class ContentMessages {
}
}
promBefore = doMaybeLocalRoomAction(
roomId,
(actualRoomId) => this.sendContentToRoom(
file,
actualRoomId,
relation,
matrixClient,
replyToEvent,
loopPromiseBefore,
),
promBefore = doMaybeLocalRoomAction(roomId, (actualRoomId) =>
this.sendContentToRoom(file, actualRoomId, relation, matrixClient, replyToEvent, loopPromiseBefore),
);
}
@ -440,11 +436,13 @@ export default class ContentMessages {
}
public getCurrentUploads(relation?: IEventRelation): RoomUpload[] {
return this.inprogress.filter(roomUpload => {
return this.inprogress.filter((roomUpload) => {
const noRelation = !relation && !roomUpload.relation;
const matchingRelation = relation && roomUpload.relation
&& relation.rel_type === roomUpload.relation.rel_type
&& relation.event_id === roomUpload.relation.event_id;
const matchingRelation =
relation &&
roomUpload.relation &&
relation.rel_type === roomUpload.relation.rel_type &&
relation.event_id === roomUpload.relation.event_id;
return (noRelation || matchingRelation) && !roomUpload.cancelled;
});
@ -498,7 +496,7 @@ export default class ContentMessages {
}
try {
if (file.type.startsWith('image/')) {
if (file.type.startsWith("image/")) {
content.msgtype = MsgType.Image;
try {
const imageInfo = await infoForImageFile(matrixClient, roomId, file);
@ -508,9 +506,9 @@ export default class ContentMessages {
logger.error(e);
content.msgtype = MsgType.File;
}
} else if (file.type.indexOf('audio/') === 0) {
} else if (file.type.indexOf("audio/") === 0) {
content.msgtype = MsgType.Audio;
} else if (file.type.indexOf('video/') === 0) {
} else if (file.type.indexOf("video/") === 0) {
content.msgtype = MsgType.Video;
try {
const videoInfo = await infoForVideoFile(matrixClient, roomId, file);
@ -543,7 +541,7 @@ export default class ContentMessages {
}
dis.dispatch<UploadFinishedPayload>({ action: Action.UploadFinished, upload });
dis.dispatch({ action: 'message_sent' });
dis.dispatch({ action: "message_sent" });
} catch (error) {
// 413: File was too big or upset the server in some way:
// clear the media size limit so we fetch it again next time we try to upload
@ -554,26 +552,27 @@ export default class ContentMessages {
if (!upload.cancelled) {
let desc = _t("The file '%(fileName)s' failed to upload.", { fileName: upload.fileName });
if (error.httpStatus === 413) {
desc = _t(
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
{ fileName: upload.fileName },
);
desc = _t("The file '%(fileName)s' exceeds this homeserver's size limit for uploads", {
fileName: upload.fileName,
});
}
Modal.createDialog(ErrorDialog, {
title: _t('Upload Failed'),
title: _t("Upload Failed"),
description: desc,
});
dis.dispatch<UploadErrorPayload>({ action: Action.UploadFailed, upload, error });
}
} finally {
removeElement(this.inprogress, e => e.promise === upload.promise);
removeElement(this.inprogress, (e) => e.promise === upload.promise);
}
}
private isFileSizeAcceptable(file: File) {
if (this.mediaConfig !== null &&
if (
this.mediaConfig !== null &&
this.mediaConfig["m.upload.size"] !== undefined &&
file.size > this.mediaConfig["m.upload.size"]) {
file.size > this.mediaConfig["m.upload.size"]
) {
return false;
}
return true;
@ -583,16 +582,20 @@ export default class ContentMessages {
if (this.mediaConfig !== null) return;
logger.log("[Media Config] Fetching");
return matrixClient.getMediaConfig().then((config) => {
logger.log("[Media Config] Fetched config:", config);
return config;
}).catch(() => {
// Media repo can't or won't report limits, so provide an empty object (no limits).
logger.log("[Media Config] Could not fetch config, so not limiting uploads.");
return {};
}).then((config) => {
this.mediaConfig = config;
});
return matrixClient
.getMediaConfig()
.then((config) => {
logger.log("[Media Config] Fetched config:", config);
return config;
})
.catch(() => {
// Media repo can't or won't report limits, so provide an empty object (no limits).
logger.log("[Media Config] Could not fetch config, so not limiting uploads.");
return {};
})
.then((config) => {
this.mediaConfig = config;
});
}
static sharedInstance() {

View file

@ -16,45 +16,37 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { _t } from './languageHandler';
import { _t } from "./languageHandler";
function getDaysArray(): string[] {
return [
_t('Sun'),
_t('Mon'),
_t('Tue'),
_t('Wed'),
_t('Thu'),
_t('Fri'),
_t('Sat'),
];
return [_t("Sun"), _t("Mon"), _t("Tue"), _t("Wed"), _t("Thu"), _t("Fri"), _t("Sat")];
}
function getMonthsArray(): string[] {
return [
_t('Jan'),
_t('Feb'),
_t('Mar'),
_t('Apr'),
_t('May'),
_t('Jun'),
_t('Jul'),
_t('Aug'),
_t('Sep'),
_t('Oct'),
_t('Nov'),
_t('Dec'),
_t("Jan"),
_t("Feb"),
_t("Mar"),
_t("Apr"),
_t("May"),
_t("Jun"),
_t("Jul"),
_t("Aug"),
_t("Sep"),
_t("Oct"),
_t("Nov"),
_t("Dec"),
];
}
function pad(n: number): string {
return (n < 10 ? '0' : '') + n;
return (n < 10 ? "0" : "") + n;
}
function twelveHourTime(date: Date, showSeconds = false): string {
let hours = date.getHours() % 12;
const minutes = pad(date.getMinutes());
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
const ampm = date.getHours() >= 12 ? _t("PM") : _t("AM");
hours = hours ? hours : 12; // convert 0 -> 12
if (showSeconds) {
const seconds = pad(date.getSeconds());
@ -71,13 +63,13 @@ export function formatDate(date: Date, showTwelveHour = false): string {
return formatTime(date, showTwelveHour);
} else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
// TODO: use standard date localize function provided in counterpart
return _t('%(weekDayName)s %(time)s', {
return _t("%(weekDayName)s %(time)s", {
weekDayName: days[date.getDay()],
time: formatTime(date, showTwelveHour),
});
} else if (now.getFullYear() === date.getFullYear()) {
// TODO: use standard date localize function provided in counterpart
return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', {
return _t("%(weekDayName)s, %(monthName)s %(day)s %(time)s", {
weekDayName: days[date.getDay()],
monthName: months[date.getMonth()],
day: date.getDate(),
@ -90,7 +82,7 @@ export function formatDate(date: Date, showTwelveHour = false): string {
export function formatFullDateNoTime(date: Date): string {
const days = getDaysArray();
const months = getMonthsArray();
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', {
return _t("%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s", {
weekDayName: days[date.getDay()],
monthName: months[date.getMonth()],
day: date.getDate(),
@ -101,7 +93,7 @@ export function formatFullDateNoTime(date: Date): string {
export function formatFullDate(date: Date, showTwelveHour = false, showSeconds = true): string {
const days = getDaysArray();
const months = getMonthsArray();
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', {
return _t("%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", {
weekDayName: days[date.getDay()],
monthName: months[date.getMonth()],
day: date.getDate(),
@ -114,23 +106,29 @@ export function formatFullTime(date: Date, showTwelveHour = false): string {
if (showTwelveHour) {
return twelveHourTime(date, true);
}
return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
return pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds());
}
export function formatTime(date: Date, showTwelveHour = false): string {
if (showTwelveHour) {
return twelveHourTime(date);
}
return pad(date.getHours()) + ':' + pad(date.getMinutes());
return pad(date.getHours()) + ":" + pad(date.getMinutes());
}
export function formatSeconds(inSeconds: number): string {
const isNegative = inSeconds < 0;
inSeconds = Math.abs(inSeconds);
const hours = Math.floor(inSeconds / (60 * 60)).toFixed(0).padStart(2, '0');
const minutes = Math.floor((inSeconds % (60 * 60)) / 60).toFixed(0).padStart(2, '0');
const seconds = Math.floor(((inSeconds % (60 * 60)) % 60)).toFixed(0).padStart(2, '0');
const hours = Math.floor(inSeconds / (60 * 60))
.toFixed(0)
.padStart(2, "0");
const minutes = Math.floor((inSeconds % (60 * 60)) / 60)
.toFixed(0)
.padStart(2, "0");
const seconds = Math.floor((inSeconds % (60 * 60)) % 60)
.toFixed(0)
.padStart(2, "0");
let output = "";
if (hours !== "00") output += `${hours}:`;
@ -146,7 +144,7 @@ export function formatSeconds(inSeconds: number): string {
export function formatTimeLeft(inSeconds: number): string {
const hours = Math.floor(inSeconds / (60 * 60)).toFixed(0);
const minutes = Math.floor((inSeconds % (60 * 60)) / 60).toFixed(0);
const seconds = Math.floor(((inSeconds % (60 * 60)) % 60)).toFixed(0);
const seconds = Math.floor((inSeconds % (60 * 60)) % 60).toFixed(0);
if (hours !== "0") {
return _t("%(hours)sh %(minutes)sm %(seconds)ss left", {
@ -192,8 +190,8 @@ export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): bo
export function formatFullDateNoDay(date: Date) {
return _t("%(date)s at %(time)s", {
date: date.toLocaleDateString().replace(/\//g, '-'),
time: date.toLocaleTimeString().replace(/:/g, '-'),
date: date.toLocaleDateString().replace(/\//g, "-"),
time: date.toLocaleTimeString().replace(/:/g, "-"),
});
}
@ -208,13 +206,7 @@ export function formatFullDateNoDayISO(date: Date): string {
}
export function formatFullDateNoDayNoTime(date: Date) {
return (
date.getFullYear() +
"/" +
pad(date.getMonth() + 1) +
"/" +
pad(date.getDate())
);
return date.getFullYear() + "/" + pad(date.getMonth() + 1) + "/" + pad(date.getDate());
}
export function formatRelativeTime(date: Date, showTwelveHour = false): string {
@ -244,15 +236,15 @@ const DAY_MS = HOUR_MS * 24;
*/
export function formatDuration(durationMs: number): string {
if (durationMs >= DAY_MS) {
return _t('%(value)sd', { value: Math.round(durationMs / DAY_MS) });
return _t("%(value)sd", { value: Math.round(durationMs / DAY_MS) });
}
if (durationMs >= HOUR_MS) {
return _t('%(value)sh', { value: Math.round(durationMs / HOUR_MS) });
return _t("%(value)sh", { value: Math.round(durationMs / HOUR_MS) });
}
if (durationMs >= MINUTE_MS) {
return _t('%(value)sm', { value: Math.round(durationMs / MINUTE_MS) });
return _t("%(value)sm", { value: Math.round(durationMs / MINUTE_MS) });
}
return _t('%(value)ss', { value: Math.round(durationMs / 1000) });
return _t("%(value)ss", { value: Math.round(durationMs / 1000) });
}
/**
@ -267,13 +259,13 @@ export function formatPreciseDuration(durationMs: number): string {
const seconds = Math.floor((durationMs % MINUTE_MS) / 1000);
if (days > 0) {
return _t('%(days)sd %(hours)sh %(minutes)sm %(seconds)ss', { days, hours, minutes, seconds });
return _t("%(days)sd %(hours)sh %(minutes)sm %(seconds)ss", { days, hours, minutes, seconds });
}
if (hours > 0) {
return _t('%(hours)sh %(minutes)sm %(seconds)ss', { hours, minutes, seconds });
return _t("%(hours)sh %(minutes)sm %(seconds)ss", { hours, minutes, seconds });
}
if (minutes > 0) {
return _t('%(minutes)sm %(seconds)ss', { minutes, seconds });
return _t("%(minutes)sm %(seconds)ss", { minutes, seconds });
}
return _t('%(value)ss', { value: seconds });
return _t("%(value)ss", { value: seconds });
}

View file

@ -18,7 +18,7 @@ import { DecryptionError } from "matrix-js-sdk/src/crypto/algorithms";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Error as ErrorEvent } from "@matrix-org/analytics-events/types/typescript/Error";
import { PosthogAnalytics } from './PosthogAnalytics';
import { PosthogAnalytics } from "./PosthogAnalytics";
export class DecryptionFailure {
public readonly ts: number;
@ -35,28 +35,31 @@ type TrackingFn = (count: number, trackedErrCode: ErrorCode, rawError: string) =
export type ErrCodeMapFn = (errcode: string) => ErrorCode;
export class DecryptionFailureTracker {
private static internalInstance = new DecryptionFailureTracker((total, errorCode, rawError) => {
for (let i = 0; i < total; i++) {
PosthogAnalytics.instance.trackEvent<ErrorEvent>({
eventName: "Error",
domain: "E2EE",
name: errorCode,
context: `mxc_crypto_error_type_${rawError}`,
});
}
}, (errorCode) => {
// Map JS-SDK error codes to tracker codes for aggregation
switch (errorCode) {
case 'MEGOLM_UNKNOWN_INBOUND_SESSION_ID':
return 'OlmKeysNotSentError';
case 'OLM_UNKNOWN_MESSAGE_INDEX':
return 'OlmIndexError';
case undefined:
return 'OlmUnspecifiedError';
default:
return 'UnknownError';
}
});
private static internalInstance = new DecryptionFailureTracker(
(total, errorCode, rawError) => {
for (let i = 0; i < total; i++) {
PosthogAnalytics.instance.trackEvent<ErrorEvent>({
eventName: "Error",
domain: "E2EE",
name: errorCode,
context: `mxc_crypto_error_type_${rawError}`,
});
}
},
(errorCode) => {
// Map JS-SDK error codes to tracker codes for aggregation
switch (errorCode) {
case "MEGOLM_UNKNOWN_INBOUND_SESSION_ID":
return "OlmKeysNotSentError";
case "OLM_UNKNOWN_MESSAGE_INDEX":
return "OlmIndexError";
case undefined:
return "OlmUnspecifiedError";
default:
return "UnknownError";
}
},
);
// Map of event IDs to DecryptionFailure items.
public failures: Map<string, DecryptionFailure> = new Map();
@ -108,12 +111,12 @@ export class DecryptionFailureTracker {
* trackedErrorCode. If not provided, the `.code` of errors will be used.
*/
private constructor(private readonly fn: TrackingFn, private readonly errorCodeMapFn: ErrCodeMapFn) {
if (!fn || typeof fn !== 'function') {
throw new Error('DecryptionFailureTracker requires tracking function');
if (!fn || typeof fn !== "function") {
throw new Error("DecryptionFailureTracker requires tracking function");
}
if (typeof errorCodeMapFn !== 'function') {
throw new Error('DecryptionFailureTracker second constructor argument should be a function');
if (typeof errorCodeMapFn !== "function") {
throw new Error("DecryptionFailureTracker second constructor argument should be a function");
}
}
@ -145,7 +148,9 @@ export class DecryptionFailureTracker {
public addVisibleEvent(e: MatrixEvent): void {
const eventId = e.getId();
if (this.trackedEvents.has(eventId)) { return; }
if (this.trackedEvents.has(eventId)) {
return;
}
this.visibleEvents.add(eventId);
if (this.failures.has(eventId) && !this.visibleFailures.has(eventId)) {
@ -156,7 +161,9 @@ export class DecryptionFailureTracker {
public addDecryptionFailure(failure: DecryptionFailure): void {
const eventId = failure.failedEventId;
if (this.trackedEvents.has(eventId)) { return; }
if (this.trackedEvents.has(eventId)) {
return;
}
this.failures.set(eventId, failure);
if (this.visibleEvents.has(eventId) && !this.visibleFailures.has(eventId)) {
@ -179,10 +186,7 @@ export class DecryptionFailureTracker {
DecryptionFailureTracker.CHECK_INTERVAL_MS,
);
this.trackInterval = window.setInterval(
() => this.trackFailures(),
DecryptionFailureTracker.TRACK_INTERVAL_MS,
);
this.trackInterval = window.setInterval(() => this.trackFailures(), DecryptionFailureTracker.TRACK_INTERVAL_MS);
}
/**

View file

@ -20,7 +20,7 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { ClientEvent, EventType, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { SyncState } from "matrix-js-sdk/src/sync";
import { MatrixClientPeg } from './MatrixClientPeg';
import { MatrixClientPeg } from "./MatrixClientPeg";
import dis from "./dispatcher/dispatcher";
import {
hideToast as hideBulkUnverifiedSessionsToast,
@ -36,16 +36,13 @@ import {
showToast as showUnverifiedSessionsToast,
} from "./toasts/UnverifiedSessionToast";
import { accessSecretStorage, isSecretStorageBeingAccessed } from "./SecurityManager";
import { isSecureBackupRequired } from './utils/WellKnownUtils';
import { isSecureBackupRequired } from "./utils/WellKnownUtils";
import { ActionPayload } from "./dispatcher/payloads";
import { Action } from "./dispatcher/actions";
import { isLoggedIn } from "./utils/login";
import SdkConfig from "./SdkConfig";
import PlatformPeg from "./PlatformPeg";
import {
recordClientInformation,
removeClientInformation,
} from "./utils/device/clientInformation";
import { recordClientInformation, removeClientInformation } from "./utils/device/clientInformation";
import SettingsStore, { CallbackFn } from "./settings/SettingsStore";
import { UIFeature } from "./settings/UIFeature";
import { isBulkUnverifiedDeviceReminderSnoozed } from "./utils/device/snoozeBulkUnverifiedDeviceReminder";
@ -88,11 +85,11 @@ export default class DeviceListener {
MatrixClientPeg.get().on(ClientEvent.AccountData, this.onAccountData);
MatrixClientPeg.get().on(ClientEvent.Sync, this.onSync);
MatrixClientPeg.get().on(RoomStateEvent.Events, this.onRoomStateEvents);
this.shouldRecordClientInformation = SettingsStore.getValue('deviceClientInformationOptIn');
this.shouldRecordClientInformation = SettingsStore.getValue("deviceClientInformationOptIn");
// only configurable in config, so we don't need to watch the value
this.enableBulkUnverifiedSessionsReminder = SettingsStore.getValue(UIFeature.BulkUnverifiedSessionsReminder);
this.deviceClientInformationSettingWatcherRef = SettingsStore.watchSetting(
'deviceClientInformationOptIn',
"deviceClientInformationOptIn",
null,
this.onRecordClientInformationSettingChange,
);
@ -138,7 +135,7 @@ export default class DeviceListener {
* @param {String[]} deviceIds List of device IDs to dismiss notifications for
*/
public async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
logger.log("Dismissing unverified sessions: " + Array.from(deviceIds).join(','));
logger.log("Dismissing unverified sessions: " + Array.from(deviceIds).join(","));
for (const d of deviceIds) {
this.dismissed.add(d);
}
@ -154,9 +151,7 @@ export default class DeviceListener {
private ensureDeviceIdsAtStartPopulated() {
if (this.ourDeviceIdsAtStart === null) {
const cli = MatrixClientPeg.get();
this.ourDeviceIdsAtStart = new Set(
cli.getStoredDevicesForUser(cli.getUserId()).map(d => d.deviceId),
);
this.ourDeviceIdsAtStart = new Set(cli.getStoredDevicesForUser(cli.getUserId()).map((d) => d.deviceId));
}
}
@ -199,16 +194,16 @@ export default class DeviceListener {
// * completed secret storage creation
// which result in account data changes affecting checks below.
if (
ev.getType().startsWith('m.secret_storage.') ||
ev.getType().startsWith('m.cross_signing.') ||
ev.getType() === 'm.megolm_backup.v1'
ev.getType().startsWith("m.secret_storage.") ||
ev.getType().startsWith("m.cross_signing.") ||
ev.getType() === "m.megolm_backup.v1"
) {
this.recheck();
}
};
private onSync = (state: SyncState, prevState?: SyncState) => {
if (state === 'PREPARED' && prevState === null) {
if (state === "PREPARED" && prevState === null) {
this.recheck();
}
};
@ -230,7 +225,7 @@ export default class DeviceListener {
// The server doesn't tell us when key backup is set up, so we poll
// & cache the result
private async getKeyBackupInfo() {
const now = (new Date()).getTime();
const now = new Date().getTime();
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this.keyBackupFetchedAt = now;
@ -244,7 +239,7 @@ export default class DeviceListener {
if (isSecretStorageBeingAccessed()) return false;
// Show setup toasts once the user is in at least one encrypted room.
const cli = MatrixClientPeg.get();
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
return cli && cli.getRooms().some((r) => cli.isRoomEncrypted(r.roomId));
}
private async recheck() {
@ -272,10 +267,7 @@ export default class DeviceListener {
await cli.downloadKeys([cli.getUserId()]);
// cross signing isn't enabled - nag to enable it
// There are 3 different toasts for:
if (
!cli.getCrossSigningId() &&
cli.getStoredCrossSigningForUser(cli.getUserId())
) {
if (!cli.getCrossSigningId() && cli.getStoredCrossSigningForUser(cli.getUserId())) {
// Cross-signing on account but this device doesn't trust the master key (verify this session)
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
this.checkKeyBackupStatus();
@ -311,8 +303,8 @@ 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();
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
@ -332,19 +324,19 @@ export default class DeviceListener {
}
}
logger.debug("Old unverified sessions: " + Array.from(oldUnverifiedDeviceIds).join(','));
logger.debug("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(','));
logger.debug("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(','));
logger.debug("Old unverified sessions: " + Array.from(oldUnverifiedDeviceIds).join(","));
logger.debug("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(","));
logger.debug("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(","));
const isBulkUnverifiedSessionsReminderSnoozed = isBulkUnverifiedDeviceReminderSnoozed();
// Display or hide the batch toast for old unverified sessions
// don't show the toast if the current device is unverified
if (
oldUnverifiedDeviceIds.size > 0
&& isCurrentDeviceTrusted
&& this.enableBulkUnverifiedSessionsReminder
&& !isBulkUnverifiedSessionsReminderSnoozed
oldUnverifiedDeviceIds.size > 0 &&
isCurrentDeviceTrusted &&
this.enableBulkUnverifiedSessionsReminder &&
!isBulkUnverifiedSessionsReminderSnoozed
) {
showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
} else {
@ -381,7 +373,11 @@ export default class DeviceListener {
};
private onRecordClientInformationSettingChange: CallbackFn = (
_originalSettingName, _roomId, _level, _newLevel, newValue,
_originalSettingName,
_roomId,
_level,
_newLevel,
newValue,
) => {
const prevValue = this.shouldRecordClientInformation;
@ -395,18 +391,14 @@ export default class DeviceListener {
private updateClientInformation = async () => {
try {
if (this.shouldRecordClientInformation) {
await recordClientInformation(
MatrixClientPeg.get(),
SdkConfig.get(),
PlatformPeg.get(),
);
await recordClientInformation(MatrixClientPeg.get(), SdkConfig.get(), PlatformPeg.get());
} else {
await removeClientInformation(MatrixClientPeg.get());
}
} catch (error) {
// this is a best effort operation
// log the error without rethrowing
logger.error('Failed to update client information', error);
logger.error("Failed to update client information", error);
}
};
}

View file

@ -17,29 +17,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ReactNode } from 'react';
import sanitizeHtml from 'sanitize-html';
import cheerio from 'cheerio';
import classNames from 'classnames';
import EMOJIBASE_REGEX from 'emojibase-regex';
import { split } from 'lodash';
import katex from 'katex';
import { decode } from 'html-entities';
import { IContent } from 'matrix-js-sdk/src/models/event';
import { Optional } from 'matrix-events-sdk';
import React, { ReactNode } from "react";
import sanitizeHtml from "sanitize-html";
import cheerio from "cheerio";
import classNames from "classnames";
import EMOJIBASE_REGEX from "emojibase-regex";
import { split } from "lodash";
import katex from "katex";
import { decode } from "html-entities";
import { IContent } from "matrix-js-sdk/src/models/event";
import { Optional } from "matrix-events-sdk";
import {
_linkifyElement,
_linkifyString,
ELEMENT_URL_PATTERN,
options as linkifyMatrixOptions,
} from './linkify-matrix';
import { IExtendedSanitizeOptions } from './@types/sanitize-html';
import SettingsStore from './settings/SettingsStore';
} from "./linkify-matrix";
import { IExtendedSanitizeOptions } from "./@types/sanitize-html";
import SettingsStore from "./settings/SettingsStore";
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
import { getEmojiFromUnicode } from "./emoji";
import { mediaFromMxc } from "./customisations/Media";
import { stripHTMLReply, stripPlainReply } from './utils/Reply';
import { stripHTMLReply, stripPlainReply } from "./utils/Reply";
// Anything outside the basic multilingual plane will be a surrogate pair
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
@ -55,7 +55,7 @@ const ZWJ_REGEX = /[\u200D\u2003]/g;
// Regex pattern for whitespace characters
const WHITESPACE_REGEX = /\s/g;
const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, "i");
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
@ -107,7 +107,7 @@ function mightContainEmoji(str: string): boolean {
*/
export function unicodeToShortcode(char: string): string {
const shortcodes = getEmojiFromUnicode(char)?.shortcodes;
return shortcodes?.length ? `:${shortcodes[0]}:` : '';
return shortcodes?.length ? `:${shortcodes[0]}:` : "";
}
/*
@ -126,7 +126,7 @@ export function getHtmlText(insaneHtml: string): string {
allowedAttributes: {},
selfClosing: [],
allowedSchemes: [],
disallowedTagsMode: 'discard',
disallowedTagsMode: "discard",
});
}
@ -147,11 +147,12 @@ export function isUrlPermitted(inputUrl: string): boolean {
}
}
const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to matrix
const transformTags: IExtendedSanitizeOptions["transformTags"] = {
// custom to matrix
// add blank targets to all hyperlinks except vector URLs
'a': function(tagName: string, attribs: sanitizeHtml.Attributes) {
"a": function (tagName: string, attribs: sanitizeHtml.Attributes) {
if (attribs.href) {
attribs.target = '_blank'; // by default
attribs.target = "_blank"; // by default
const transformed = tryTransformPermalinkToLocalHref(attribs.href); // only used to check if it is a link that can be handled locally
if (
@ -165,10 +166,10 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
delete attribs.href;
}
attribs.rel = 'noreferrer noopener'; // https://mathiasbynens.github.io/rel-noopener/
attribs.rel = "noreferrer noopener"; // https://mathiasbynens.github.io/rel-noopener/
return { tagName, attribs };
},
'img': function(tagName: string, attribs: sanitizeHtml.Attributes) {
"img": function (tagName: string, attribs: sanitizeHtml.Attributes) {
let src = attribs.src;
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
// because transformTags is used _before_ we filter by allowedSchemesByTag and
@ -208,18 +209,18 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height);
return { tagName, attribs };
},
'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {
if (typeof attribs.class !== 'undefined') {
"code": function (tagName: string, attribs: sanitizeHtml.Attributes) {
if (typeof attribs.class !== "undefined") {
// Filter out all classes other than ones starting with language- for syntax highlighting.
const classes = attribs.class.split(/\s/).filter(function(cl) {
return cl.startsWith('language-') && !cl.startsWith('language-_');
const classes = attribs.class.split(/\s/).filter(function (cl) {
return cl.startsWith("language-") && !cl.startsWith("language-_");
});
attribs.class = classes.join(' ');
attribs.class = classes.join(" ");
}
return { tagName, attribs };
},
// eslint-disable-next-line @typescript-eslint/naming-convention
'*': function(tagName: string, attribs: sanitizeHtml.Attributes) {
"*": function (tagName: string, attribs: sanitizeHtml.Attributes) {
// Delete any style previously assigned, style is an allowedTag for font, span & img,
// because attributes are stripped after transforming.
// For img this is trusted as it is generated wholly within the img transformation method.
@ -230,8 +231,8 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
// Sanitise and transform data-mx-color and data-mx-bg-color to their CSS
// equivalents
const customCSSMapper = {
'data-mx-color': 'color',
'data-mx-bg-color': 'background-color',
"data-mx-color": "color",
"data-mx-bg-color": "background-color",
// $customAttributeKey: $cssAttributeKey
};
@ -239,8 +240,9 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
Object.keys(customCSSMapper).forEach((customAttributeKey) => {
const cssAttributeKey = customCSSMapper[customAttributeKey];
const customAttributeValue = attribs[customAttributeKey];
if (customAttributeValue &&
typeof customAttributeValue === 'string' &&
if (
customAttributeValue &&
typeof customAttributeValue === "string" &&
COLOR_REGEX.test(customAttributeValue)
) {
style += cssAttributeKey + ":" + customAttributeValue + ";";
@ -258,28 +260,61 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
const sanitizeHtmlParams: IExtendedSanitizeOptions = {
allowedTags: [
'font', // custom to matrix for IRC-style font coloring
'del', // for markdown
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub',
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
'details', 'summary',
"font", // custom to matrix for IRC-style font coloring
"del", // for markdown
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"blockquote",
"p",
"a",
"ul",
"ol",
"sup",
"sub",
"nl",
"li",
"b",
"i",
"u",
"strong",
"em",
"strike",
"code",
"hr",
"br",
"div",
"table",
"thead",
"caption",
"tbody",
"tr",
"th",
"td",
"pre",
"span",
"img",
"details",
"summary",
],
allowedAttributes: {
// attribute sanitization happens after transformations, so we have to accept `style` for font, span & img
// but strip during the transformation.
// custom ones first:
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
span: ['data-mx-maths', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix
div: ['data-mx-maths'],
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
font: ["color", "data-mx-bg-color", "data-mx-color", "style"], // custom to matrix
span: ["data-mx-maths", "data-mx-bg-color", "data-mx-color", "data-mx-spoiler", "style"], // custom to matrix
div: ["data-mx-maths"],
a: ["href", "name", "target", "rel"], // remote target: custom to matrix
// img tags also accept width/height, we just map those to max-width & max-height during transformation
img: ['src', 'alt', 'title', 'style'],
ol: ['start'],
code: ['class'], // We don't actually allow all classes, we filter them in transformTags
img: ["src", "alt", "title", "style"],
ol: ["start"],
code: ["class"], // We don't actually allow all classes, we filter them in transformTags
},
// Lots of these won't come up by default because we don't allow them
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
selfClosing: ["img", "br", "hr", "area", "base", "basefont", "input", "link", "meta"],
// URL schemes we permit
allowedSchemes: PERMITTED_URL_SCHEMES,
allowProtocolRelative: false,
@ -292,8 +327,8 @@ const sanitizeHtmlParams: IExtendedSanitizeOptions = {
const composerSanitizeHtmlParams: IExtendedSanitizeOptions = {
...sanitizeHtmlParams,
transformTags: {
'code': transformTags['code'],
'*': transformTags['*'],
"code": transformTags["code"],
"*": transformTags["*"],
},
};
@ -301,17 +336,25 @@ const composerSanitizeHtmlParams: IExtendedSanitizeOptions = {
const topicSanitizeHtmlParams: IExtendedSanitizeOptions = {
...sanitizeHtmlParams,
allowedTags: [
'font', // custom to matrix for IRC-style font coloring
'del', // for markdown
'a', 'sup', 'sub',
'b', 'i', 'u', 'strong', 'em', 'strike', 'br', 'div',
'span',
"font", // custom to matrix for IRC-style font coloring
"del", // for markdown
"a",
"sup",
"sub",
"b",
"i",
"u",
"strong",
"em",
"strike",
"br",
"div",
"span",
],
};
abstract class BaseHighlighter<T extends React.ReactNode> {
constructor(public highlightClass: string, public highlightLink: string) {
}
constructor(public highlightClass: string, public highlightLink: string) {}
/**
* apply the highlights to a section of text
@ -408,8 +451,11 @@ export interface IOptsReturnString extends IOpts {
const emojiToHtmlSpan = (emoji: string) =>
`<span class='mx_Emoji' title='${unicodeToShortcode(emoji)}'>${emoji}</span>`;
const emojiToJsxSpan = (emoji: string, key: number) =>
<span key={key} className='mx_Emoji' title={unicodeToShortcode(emoji)}>{ emoji }</span>;
const emojiToJsxSpan = (emoji: string, key: number) => (
<span key={key} className="mx_Emoji" title={unicodeToShortcode(emoji)}>
{emoji}
</span>
);
/**
* Wraps emojis in <span> to style them separately from the rest of message. Consecutive emojis (and modifiers) are wrapped
@ -422,15 +468,15 @@ const emojiToJsxSpan = (emoji: string, key: number) =>
function formatEmojis(message: string, isHtmlMessage: boolean): (JSX.Element | string)[] {
const emojiToSpan = isHtmlMessage ? emojiToHtmlSpan : emojiToJsxSpan;
const result: (JSX.Element | string)[] = [];
let text = '';
let text = "";
let key = 0;
// We use lodash's grapheme splitter to avoid breaking apart compound emojis
for (const char of split(message, '')) {
for (const char of split(message, "")) {
if (EMOJIBASE_REGEX.test(char)) {
if (text) {
result.push(text);
text = '';
text = "";
}
result.push(emojiToSpan(char, key));
key++;
@ -480,8 +526,8 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
?.filter((highlight: string): boolean => !highlight.includes("<"))
.map((highlight: string): string => sanitizeHtml(highlight, sanitizeParams));
let formattedBody = typeof content.formatted_body === 'string' ? content.formatted_body : null;
const plainBody = typeof content.body === 'string' ? content.body : "";
let formattedBody = typeof content.formatted_body === "string" ? content.formatted_body : null;
const plainBody = typeof content.body === "string" ? content.body : "";
if (opts.stripReplyFallback && formattedBody) formattedBody = stripHTMLReply(formattedBody);
strippedBody = opts.stripReplyFallback ? stripPlainReply(plainBody) : plainBody;
@ -498,8 +544,8 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
// are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
// by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure.
sanitizeParams.textFilter = function(safeText) {
return highlighter.applyHighlights(safeText, safeHighlights).join('');
sanitizeParams.textFilter = function (safeText) {
return highlighter.applyHighlights(safeText, safeHighlights).join("");
};
}
@ -516,23 +562,21 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
if (isHtmlMessage && SettingsStore.getValue("feature_latex_maths")) {
// @ts-ignore - The types for `replaceWith` wrongly expect
// Cheerio instance to be returned.
phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
return katex.renderToString(
decode(phtml(e).attr('data-mx-maths')),
{
throwOnError: false,
// @ts-ignore - `e` can be an Element, not just a Node
displayMode: e.name == 'div',
output: "htmlAndMathml",
});
phtml('div, span[data-mx-maths!=""]').replaceWith(function (i, e) {
return katex.renderToString(decode(phtml(e).attr("data-mx-maths")), {
throwOnError: false,
// @ts-ignore - `e` can be an Element, not just a Node
displayMode: e.name == "div",
output: "htmlAndMathml",
});
});
safeBody = phtml.html();
}
if (bodyHasEmoji) {
safeBody = formatEmojis(safeBody, true).join('');
safeBody = formatEmojis(safeBody, true).join("");
}
} else if (highlighter) {
safeBody = highlighter.applyHighlights(plainBody, safeHighlights).join('');
safeBody = highlighter.applyHighlights(plainBody, safeHighlights).join("");
}
} finally {
delete sanitizeParams.textFilter;
@ -545,34 +589,34 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
let emojiBody = false;
if (!opts.disableBigEmoji && bodyHasEmoji) {
let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : '';
let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : "";
// Ignore spaces in body text. Emojis with spaces in between should
// still be counted as purely emoji messages.
contentBodyTrimmed = contentBodyTrimmed.replace(WHITESPACE_REGEX, '');
contentBodyTrimmed = contentBodyTrimmed.replace(WHITESPACE_REGEX, "");
// Remove zero width joiner characters from emoji messages. This ensures
// that emojis that are made up of multiple unicode characters are still
// presented as large.
contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, '');
contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, "");
const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed);
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length &&
// Prevent user pills expanding for users with only emoji in
// their username. Permalinks (links in pills) can be any URL
// now, so we just check for an HTTP-looking thing.
(
strippedBody === safeBody || // replies have the html fallbacks, account for that here
content.formatted_body === undefined ||
(!content.formatted_body.includes("http:") &&
!content.formatted_body.includes("https:"))
);
emojiBody =
match &&
match[0] &&
match[0].length === contentBodyTrimmed.length &&
// Prevent user pills expanding for users with only emoji in
// their username. Permalinks (links in pills) can be any URL
// now, so we just check for an HTTP-looking thing.
(strippedBody === safeBody || // replies have the html fallbacks, account for that here
content.formatted_body === undefined ||
(!content.formatted_body.includes("http:") && !content.formatted_body.includes("https:")));
}
const className = classNames({
'mx_EventTile_body': true,
'mx_EventTile_bigEmoji': emojiBody,
'markdown-body': isHtmlMessage && !emojiBody,
"mx_EventTile_body": true,
"mx_EventTile_bigEmoji": emojiBody,
"markdown-body": isHtmlMessage && !emojiBody,
});
let emojiBodyElements: JSX.Element[];
@ -580,16 +624,19 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[];
}
return safeBody ?
return safeBody ? (
<span
key="body"
ref={opts.ref}
className={className}
dangerouslySetInnerHTML={{ __html: safeBody }}
dir="auto"
/> : <span key="body" ref={opts.ref} className={className} dir="auto">
{ emojiBodyElements || strippedBody }
</span>;
/>
) : (
<span key="body" ref={opts.ref} className={className} dir="auto">
{emojiBodyElements || strippedBody}
</span>
);
}
/**
@ -620,7 +667,7 @@ export function topicToHtml(
if (isFormattedTopic) {
safeTopic = sanitizeHtml(htmlTopic, allowExtendedHtml ? sanitizeHtmlParams : topicSanitizeHtmlParams);
if (topicHasEmoji) {
safeTopic = formatEmojis(safeTopic, true).join('');
safeTopic = formatEmojis(safeTopic, true).join("");
}
}
} catch {
@ -632,15 +679,13 @@ export function topicToHtml(
emojiBodyElements = formatEmojis(topic, false);
}
return isFormattedTopic
? <span
ref={ref}
dangerouslySetInnerHTML={{ __html: safeTopic }}
dir="auto"
/>
: <span ref={ref} dir="auto">
{ emojiBodyElements || topic }
</span>;
return isFormattedTopic ? (
<span ref={ref} dangerouslySetInnerHTML={{ __html: safeTopic }} dir="auto" />
) : (
<span ref={ref} dir="auto">
{emojiBodyElements || topic}
</span>
);
}
/**

View file

@ -57,7 +57,7 @@ export interface IConfigOptions {
branding?: {
welcome_background_url?: string | string[]; // chosen at random if array
auth_header_logo_url?: string;
auth_footer_links?: {text: string, url: string}[];
auth_footer_links?: { text: string; url: string }[];
};
map_style_url?: string; // for location-shared maps
@ -163,7 +163,7 @@ export interface IConfigOptions {
enable_presence_by_hs_url?: Record<string, boolean>; // <HomeserverName, Enabled>
terms_and_conditions_links?: { url: string, text: string }[];
terms_and_conditions_links?: { url: string; text: string }[];
latex_maths_delims?: {
inline?: {

View file

@ -15,19 +15,19 @@ limitations under the License.
*/
import React from "react";
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
import { createClient, MatrixClient } from 'matrix-js-sdk/src/matrix';
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from './MatrixClientPeg';
import Modal from './Modal';
import { _t } from './languageHandler';
import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
import { MatrixClientPeg } from "./MatrixClientPeg";
import Modal from "./Modal";
import { _t } from "./languageHandler";
import { Service, startTermsFlow, TermsNotSignedError } from "./Terms";
import {
doesAccountDataHaveIdentityServer,
doesIdentityServerHaveTerms,
setToDefaultIdentityServer,
} from './utils/IdentityServerUtils';
} from "./utils/IdentityServerUtils";
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
import { abbreviateUrl } from "./utils/UrlUtils";
@ -101,10 +101,7 @@ export default class IdentityAuthClient {
try {
await this.checkToken(token);
} catch (e) {
if (
e instanceof TermsNotSignedError ||
e instanceof AbortedIdentityActionError
) {
if (e instanceof TermsNotSignedError || e instanceof AbortedIdentityActionError) {
// Retrying won't help this
throw e;
}
@ -128,11 +125,7 @@ export default class IdentityAuthClient {
} catch (e) {
if (e.errcode === "M_TERMS_NOT_SIGNED") {
logger.log("Identity server requires new terms to be agreed to");
await startTermsFlow([new Service(
SERVICE_TYPES.IS,
identityServerUrl,
token,
)]);
await startTermsFlow([new Service(SERVICE_TYPES.IS, identityServerUrl, token)]);
return;
}
throw e;
@ -147,17 +140,18 @@ export default class IdentityAuthClient {
title: _t("Identity server has no terms of service"),
description: (
<div>
<p>{ _t(
"This action requires accessing the default identity server " +
"<server /> to validate an email address or phone number, " +
"but the server does not have any terms of service.", {},
{
server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
},
) }</p>
<p>{ _t(
"Only continue if you trust the owner of the server.",
) }</p>
<p>
{_t(
"This action requires accessing the default identity server " +
"<server /> to validate an email address or phone number, " +
"but the server does not have any terms of service.",
{},
{
server: () => <b>{abbreviateUrl(identityServerUrl)}</b>,
},
)}
</p>
<p>{_t("Only continue if you trust the owner of the server.")}</p>
</div>
),
button: _t("Trust"),
@ -166,9 +160,7 @@ export default class IdentityAuthClient {
if (confirmed) {
setToDefaultIdentityServer();
} else {
throw new AbortedIdentityActionError(
"User aborted identity server action without terms",
);
throw new AbortedIdentityActionError("User aborted identity server action without terms");
}
}
@ -182,8 +174,7 @@ export default class IdentityAuthClient {
public async registerForToken(check = true): Promise<string> {
const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken();
// XXX: The spec is `token`, but we used `access_token` for a Sydent release.
const { access_token: accessToken, token } =
await this.matrixClient.registerWithIdentityServer(hsOpenIdToken);
const { access_token: accessToken, token } = await this.matrixClient.registerWithIdentityServer(hsOpenIdToken);
const identityAccessToken = token ? token : accessToken;
if (check) await this.checkToken(identityAccessToken);
return identityAccessToken;

View file

@ -48,4 +48,3 @@ export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: n
return Math.floor(heightMulti * fullHeight);
}
}

View file

@ -19,11 +19,7 @@ import { IS_MAC, Key } from "./Keyboard";
import SettingsStore from "./settings/SettingsStore";
import SdkConfig from "./SdkConfig";
import { IKeyBindingsProvider, KeyBinding } from "./KeyBindingsManager";
import {
CATEGORIES,
CategoryName,
KeyBindingAction,
} from "./accessibility/KeyboardShortcuts";
import { CATEGORIES, CategoryName, KeyBindingAction } from "./accessibility/KeyboardShortcuts";
import { getKeyboardShortcuts } from "./accessibility/KeyboardShortcutUtils";
export const getBindingsByCategory = (category: CategoryName): KeyBinding[] => {
@ -39,7 +35,7 @@ export const getBindingsByCategory = (category: CategoryName): KeyBinding[] => {
const messageComposerBindings = (): KeyBinding[] => {
const bindings = getBindingsByCategory(CategoryName.COMPOSER);
if (SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend')) {
if (SettingsStore.getValue("MessageComposerInput.ctrlEnterToSend")) {
bindings.push({
action: KeyBindingAction.SendMessage,
keyCombo: {
@ -128,7 +124,7 @@ const roomListBindings = (): KeyBinding[] => {
const roomBindings = (): KeyBinding[] => {
const bindings = getBindingsByCategory(CategoryName.ROOM);
if (SettingsStore.getValue('ctrlFForSearch')) {
if (SettingsStore.getValue("ctrlFForSearch")) {
bindings.push({
action: KeyBindingAction.SearchInRoom,
keyCombo: {

View file

@ -16,8 +16,8 @@ limitations under the License.
*/
import { KeyBindingAction } from "./accessibility/KeyboardShortcuts";
import { defaultBindingsProvider } from './KeyBindingsDefaults';
import { IS_MAC } from './Keyboard';
import { defaultBindingsProvider } from "./KeyBindingsDefaults";
import { IS_MAC } from "./Keyboard";
/**
* Represent a key combination.
@ -72,27 +72,18 @@ export function isKeyComboMatch(ev: KeyboardEvent | React.KeyboardEvent, combo:
// When ctrlOrCmd is set, the keys need do evaluated differently on PC and Mac
if (combo.ctrlOrCmdKey) {
if (onMac) {
if (!evMeta
|| evCtrl !== comboCtrl
|| evAlt !== comboAlt
|| evShift !== comboShift) {
if (!evMeta || evCtrl !== comboCtrl || evAlt !== comboAlt || evShift !== comboShift) {
return false;
}
} else {
if (!evCtrl
|| evMeta !== comboMeta
|| evAlt !== comboAlt
|| evShift !== comboShift) {
if (!evCtrl || evMeta !== comboMeta || evAlt !== comboAlt || evShift !== comboShift) {
return false;
}
}
return true;
}
if (evMeta !== comboMeta
|| evCtrl !== comboCtrl
|| evAlt !== comboAlt
|| evShift !== comboShift) {
if (evMeta !== comboMeta || evCtrl !== comboCtrl || evAlt !== comboAlt || evShift !== comboShift) {
return false;
}
@ -114,9 +105,7 @@ export class KeyBindingsManager {
* To overwrite the default key bindings add a new providers before the default provider, e.g. a provider for
* customized key bindings.
*/
bindingsProviders: IKeyBindingsProvider[] = [
defaultBindingsProvider,
];
bindingsProviders: IKeyBindingsProvider[] = [defaultBindingsProvider];
/**
* Finds a matching KeyAction for a given KeyboardEvent
@ -127,7 +116,7 @@ export class KeyBindingsManager {
): KeyBindingAction | undefined {
for (const getter of getters) {
const bindings = getter();
const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, IS_MAC));
const binding = bindings.find((it) => isKeyComboMatch(ev, it.keyCombo, IS_MAC));
if (binding) {
return binding.action;
}
@ -136,35 +125,59 @@ export class KeyBindingsManager {
}
getMessageComposerAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getMessageComposerBindings), ev);
return this.getAction(
this.bindingsProviders.map((it) => it.getMessageComposerBindings),
ev,
);
}
getAutocompleteAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getAutocompleteBindings), ev);
return this.getAction(
this.bindingsProviders.map((it) => it.getAutocompleteBindings),
ev,
);
}
getRoomListAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getRoomListBindings), ev);
return this.getAction(
this.bindingsProviders.map((it) => it.getRoomListBindings),
ev,
);
}
getRoomAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getRoomBindings), ev);
return this.getAction(
this.bindingsProviders.map((it) => it.getRoomBindings),
ev,
);
}
getNavigationAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getNavigationBindings), ev);
return this.getAction(
this.bindingsProviders.map((it) => it.getNavigationBindings),
ev,
);
}
getAccessibilityAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getAccessibilityBindings), ev);
return this.getAction(
this.bindingsProviders.map((it) => it.getAccessibilityBindings),
ev,
);
}
getCallAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getCallBindings), ev);
return this.getAction(
this.bindingsProviders.map((it) => it.getCallBindings),
ev,
);
}
getLabsAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getLabsBindings), ev);
return this.getAction(
this.bindingsProviders.map((it) => it.getLabsBindings),
ev,
);
}
}

View file

@ -74,7 +74,7 @@ export const Key = {
Z: "z",
};
export const IS_MAC = navigator.platform.toUpperCase().includes('MAC');
export const IS_MAC = navigator.platform.toUpperCase().includes("MAC");
export function isOnlyCtrlOrCmdKeyEvent(ev) {
if (IS_MAC) {

View file

@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import {
CallError,
CallErrorCode,
@ -27,19 +27,19 @@ import {
CallType,
MatrixCall,
} from "matrix-js-sdk/src/webrtc/call";
import { logger } from 'matrix-js-sdk/src/logger';
import EventEmitter from 'events';
import { logger } from "matrix-js-sdk/src/logger";
import EventEmitter from "events";
import { RuleId, TweakName, Tweaks } from "matrix-js-sdk/src/@types/PushRules";
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
import { SyncState } from "matrix-js-sdk/src/sync";
import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler";
import { MatrixClientPeg } from './MatrixClientPeg';
import Modal from './Modal';
import { _t } from './languageHandler';
import dis from './dispatcher/dispatcher';
import WidgetUtils from './utils/WidgetUtils';
import SettingsStore from './settings/SettingsStore';
import { MatrixClientPeg } from "./MatrixClientPeg";
import Modal from "./Modal";
import { _t } from "./languageHandler";
import dis from "./dispatcher/dispatcher";
import WidgetUtils from "./utils/WidgetUtils";
import SettingsStore from "./settings/SettingsStore";
import { WidgetType } from "./widgets/WidgetType";
import { SettingLevel } from "./settings/SettingLevel";
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
@ -48,66 +48,63 @@ import WidgetStore from "./stores/WidgetStore";
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
import { UIFeature } from "./settings/UIFeature";
import { Action } from './dispatcher/actions';
import VoipUserMapper from './VoipUserMapper';
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
import SdkConfig from './SdkConfig';
import { ensureDMExists } from './createRoom';
import { Container, WidgetLayoutStore } from './stores/widgets/WidgetLayoutStore';
import IncomingLegacyCallToast, { getIncomingLegacyCallToastKey } from './toasts/IncomingLegacyCallToast';
import ToastStore from './stores/ToastStore';
import Resend from './Resend';
import { Action } from "./dispatcher/actions";
import VoipUserMapper from "./VoipUserMapper";
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from "./widgets/ManagedHybrid";
import SdkConfig from "./SdkConfig";
import { ensureDMExists } from "./createRoom";
import { Container, WidgetLayoutStore } from "./stores/widgets/WidgetLayoutStore";
import IncomingLegacyCallToast, { getIncomingLegacyCallToastKey } from "./toasts/IncomingLegacyCallToast";
import ToastStore from "./stores/ToastStore";
import Resend from "./Resend";
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
import { KIND_CALL_TRANSFER } from "./components/views/dialogs/InviteDialogTypes";
import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogPayload";
import { findDMForUser } from './utils/dm/findDMForUser';
import { getJoinedNonFunctionalMembers } from './utils/room/getJoinedNonFunctionalMembers';
import { localNotificationsAreSilenced } from './utils/notifications';
import { findDMForUser } from "./utils/dm/findDMForUser";
import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers";
import { localNotificationsAreSilenced } from "./utils/notifications";
export const PROTOCOL_PSTN = 'm.protocol.pstn';
export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';
export const PROTOCOL_SIP_NATIVE = 'im.vector.protocol.sip_native';
export const PROTOCOL_SIP_VIRTUAL = 'im.vector.protocol.sip_virtual';
export const PROTOCOL_PSTN = "m.protocol.pstn";
export const PROTOCOL_PSTN_PREFIXED = "im.vector.protocol.pstn";
export const PROTOCOL_SIP_NATIVE = "im.vector.protocol.sip_native";
export const PROTOCOL_SIP_VIRTUAL = "im.vector.protocol.sip_virtual";
const CHECK_PROTOCOLS_ATTEMPTS = 3;
type MediaEventType = keyof HTMLMediaElementEventMap;
const MEDIA_ERROR_EVENT_TYPES: MediaEventType[] = [
'error',
"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',
"emptied",
// The user agent is trying to fetch media data, but data is unexpectedly not
// forthcoming.
'stalled',
"stalled",
// Media data loading has been suspended.
'suspend',
"suspend",
// Playback has stopped because of a temporary lack of data
'waiting',
"waiting",
];
const MEDIA_DEBUG_EVENT_TYPES: MediaEventType[] = [
'play',
'pause',
'playing',
'ended',
'loadeddata',
'loadedmetadata',
'canplay',
'canplaythrough',
'volumechange',
"play",
"pause",
"playing",
"ended",
"loadeddata",
"loadedmetadata",
"canplay",
"canplaythrough",
"volumechange",
];
const MEDIA_EVENT_TYPES = [
...MEDIA_ERROR_EVENT_TYPES,
...MEDIA_DEBUG_EVENT_TYPES,
];
const MEDIA_EVENT_TYPES = [...MEDIA_ERROR_EVENT_TYPES, ...MEDIA_DEBUG_EVENT_TYPES];
export enum AudioID {
Ring = 'ringAudio',
Ringback = 'ringbackAudio',
CallEnd = 'callendAudio',
Busy = 'busyAudio',
Ring = "ringAudio",
Ringback = "ringbackAudio",
CallEnd = "callendAudio",
Busy = "busyAudio",
}
/* istanbul ignore next */
@ -203,12 +200,12 @@ export default class LegacyCallHandler extends EventEmitter {
// end up causing the audio elements with our ring/ringback etc
// audio clips in to play.
if (navigator.mediaSession) {
navigator.mediaSession.setActionHandler('play', function() {});
navigator.mediaSession.setActionHandler('pause', function() {});
navigator.mediaSession.setActionHandler('seekbackward', function() {});
navigator.mediaSession.setActionHandler('seekforward', function() {});
navigator.mediaSession.setActionHandler('previoustrack', function() {});
navigator.mediaSession.setActionHandler('nexttrack', function() {});
navigator.mediaSession.setActionHandler("play", function () {});
navigator.mediaSession.setActionHandler("pause", function () {});
navigator.mediaSession.setActionHandler("seekbackward", function () {});
navigator.mediaSession.setActionHandler("seekforward", function () {});
navigator.mediaSession.setActionHandler("previoustrack", function () {});
navigator.mediaSession.setActionHandler("nexttrack", function () {});
}
if (SettingsStore.getValue(UIFeature.Voip)) {
@ -299,10 +296,7 @@ export default class LegacyCallHandler extends EventEmitter {
*/
private areAnyCallsUnsilenced(): boolean {
for (const call of this.calls.values()) {
if (
call.state === CallState.Ringing &&
!this.isCallSilenced(call.callId)
) {
if (call.state === CallState.Ringing && !this.isCallSilenced(call.callId)) {
return true;
}
}
@ -359,38 +353,35 @@ export default class LegacyCallHandler extends EventEmitter {
public async pstnLookup(phoneNumber: string): Promise<ThirdpartyLookupResponse[]> {
try {
return await MatrixClientPeg.get().getThirdpartyUser(
this.pstnSupportPrefixed ? PROTOCOL_PSTN_PREFIXED : PROTOCOL_PSTN, {
'm.id.phone': phoneNumber,
this.pstnSupportPrefixed ? PROTOCOL_PSTN_PREFIXED : PROTOCOL_PSTN,
{
"m.id.phone": phoneNumber,
},
);
} catch (e) {
logger.warn('Failed to lookup user from phone number', e);
logger.warn("Failed to lookup user from phone number", e);
return Promise.resolve([]);
}
}
public async sipVirtualLookup(nativeMxid: string): Promise<ThirdpartyLookupResponse[]> {
try {
return await MatrixClientPeg.get().getThirdpartyUser(
PROTOCOL_SIP_VIRTUAL, {
'native_mxid': nativeMxid,
},
);
return await MatrixClientPeg.get().getThirdpartyUser(PROTOCOL_SIP_VIRTUAL, {
native_mxid: nativeMxid,
});
} catch (e) {
logger.warn('Failed to query SIP identity for user', e);
logger.warn("Failed to query SIP identity for user", e);
return Promise.resolve([]);
}
}
public async sipNativeLookup(virtualMxid: string): Promise<ThirdpartyLookupResponse[]> {
try {
return await MatrixClientPeg.get().getThirdpartyUser(
PROTOCOL_SIP_NATIVE, {
'virtual_mxid': virtualMxid,
},
);
return await MatrixClientPeg.get().getThirdpartyUser(PROTOCOL_SIP_NATIVE, {
virtual_mxid: virtualMxid,
});
} catch (e) {
logger.warn('Failed to query identity for SIP user', e);
logger.warn("Failed to query identity for SIP user", e);
return Promise.resolve([]);
}
}
@ -404,8 +395,7 @@ export default class LegacyCallHandler extends EventEmitter {
const mappedRoomId = LegacyCallHandler.instance.roomIdForCall(call);
if (this.getCallForRoom(mappedRoomId)) {
logger.log(
"Got incoming call for room " + mappedRoomId +
" but there's already a call for this room: ignoring",
"Got incoming call for room " + mappedRoomId + " but there's already a call for this room: ignoring",
);
return;
}
@ -491,7 +481,7 @@ export default class LegacyCallHandler extends EventEmitter {
if (audio.muted) {
logger.error(
`${logPrefix} <audio> element was unexpectedly muted but we recovered ` +
`gracefully by unmuting it`,
`gracefully by unmuting it`,
);
// Recover gracefully
audio.muted = false;
@ -511,10 +501,13 @@ export default class LegacyCallHandler extends EventEmitter {
}
};
if (this.audioPromises.has(audioId)) {
this.audioPromises.set(audioId, this.audioPromises.get(audioId).then(() => {
audio.load();
return playAudio();
}));
this.audioPromises.set(
audioId,
this.audioPromises.get(audioId).then(() => {
audio.load();
return playAudio();
}),
);
} else {
this.audioPromises.set(audioId, playAudio());
}
@ -577,7 +570,7 @@ export default class LegacyCallHandler extends EventEmitter {
}
Modal.createDialog(ErrorDialog, {
title: _t('Call Failed'),
title: _t("Call Failed"),
description: err.message,
});
});
@ -653,7 +646,7 @@ export default class LegacyCallHandler extends EventEmitter {
const mappedRoomId = this.roomIdForCall(call);
this.setCallState(call, newState);
dis.dispatch({
action: 'call_state',
action: "call_state",
room_id: mappedRoomId,
state: newState,
});
@ -673,14 +666,13 @@ export default class LegacyCallHandler extends EventEmitter {
switch (newState) {
case CallState.Ringing: {
const incomingCallPushRule = (
new PushProcessor(MatrixClientPeg.get()).getPushRuleById(RuleId.IncomingCall)
const incomingCallPushRule = new PushProcessor(MatrixClientPeg.get()).getPushRuleById(
RuleId.IncomingCall,
);
const pushRuleEnabled = incomingCallPushRule?.enabled;
const tweakSetToRing = incomingCallPushRule?.actions.some((action: Tweaks) => (
action.set_tweak === TweakName.Sound &&
action.value === "ring"
));
const tweakSetToRing = incomingCallPushRule?.actions.some(
(action: Tweaks) => action.set_tweak === TweakName.Sound && action.value === "ring",
);
if (pushRuleEnabled && tweakSetToRing && !this.isForcedSilent()) {
this.play(AudioID.Ring);
@ -714,11 +706,10 @@ export default class LegacyCallHandler extends EventEmitter {
}
Modal.createDialog(ErrorDialog, {
title, description,
title,
description,
});
} else if (
hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
) {
} else if (hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting) {
Modal.createDialog(ErrorDialog, {
title: _t("Answered Elsewhere"),
description: _t("The call was answered on another device."),
@ -738,51 +729,50 @@ export default class LegacyCallHandler extends EventEmitter {
const stats = await call.getCurrentCallStats();
logger.debug(
`Call completed. Call ID: ${call.callId}, virtual room ID: ${call.roomId}, ` +
`user-facing room ID: ${mappedRoomId}, direction: ${call.direction}, ` +
`our Party ID: ${call.ourPartyId}, hangup party: ${call.hangupParty}, ` +
`hangup reason: ${call.hangupReason}`,
`user-facing room ID: ${mappedRoomId}, direction: ${call.direction}, ` +
`our Party ID: ${call.ourPartyId}, hangup party: ${call.hangupParty}, ` +
`hangup reason: ${call.hangupReason}`,
);
if (!stats) {
logger.debug(
"Call statistics are undefined. The call has " +
"probably failed before a peerConn was established",
"Call statistics are undefined. The call has " + "probably failed before a peerConn was established",
);
return;
}
logger.debug("Local candidates:");
for (const cand of stats.filter(item => item.type === 'local-candidate')) {
for (const cand of stats.filter((item) => item.type === "local-candidate")) {
const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip'
logger.debug(
`${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` +
`protocol: ${cand.protocol}, relay protocol: ${cand.relayProtocol}, network type: ${cand.networkType}`,
`protocol: ${cand.protocol}, relay protocol: ${cand.relayProtocol}, network type: ${cand.networkType}`,
);
}
logger.debug("Remote candidates:");
for (const cand of stats.filter(item => item.type === 'remote-candidate')) {
for (const cand of stats.filter((item) => item.type === "remote-candidate")) {
const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip'
logger.debug(
`${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` +
`protocol: ${cand.protocol}`,
`protocol: ${cand.protocol}`,
);
}
logger.debug("Candidate pairs:");
for (const pair of stats.filter(item => item.type === 'candidate-pair')) {
for (const pair of stats.filter((item) => item.type === "candidate-pair")) {
logger.debug(
`${pair.localCandidateId} / ${pair.remoteCandidateId} - state: ${pair.state}, ` +
`nominated: ${pair.nominated}, ` +
`requests sent ${pair.requestsSent}, requests received ${pair.requestsReceived}, ` +
`responses received: ${pair.responsesReceived}, responses sent: ${pair.responsesSent}, ` +
`bytes received: ${pair.bytesReceived}, bytes sent: ${pair.bytesSent}, `,
`nominated: ${pair.nominated}, ` +
`requests sent ${pair.requestsSent}, requests received ${pair.requestsReceived}, ` +
`responses received: ${pair.responsesReceived}, responses sent: ${pair.responsesSent}, ` +
`bytes received: ${pair.bytesReceived}, bytes sent: ${pair.bytesSent}, `,
);
}
logger.debug("Outbound RTP:");
for (const s of stats.filter(item => item.type === 'outbound-rtp')) {
for (const s of stats.filter((item) => item.type === "outbound-rtp")) {
logger.debug(s);
}
logger.debug("Inbound RTP:");
for (const s of stats.filter(item => item.type === 'inbound-rtp')) {
for (const s of stats.filter((item) => item.type === "inbound-rtp")) {
logger.debug(s);
}
}
@ -790,9 +780,7 @@ export default class LegacyCallHandler extends EventEmitter {
private setCallState(call: MatrixCall, status: CallState): void {
const mappedRoomId = LegacyCallHandler.instance.roomIdForCall(call);
logger.log(
`Call state in ${mappedRoomId} changed to ${status}`,
);
logger.log(`Call state in ${mappedRoomId} changed to ${status}`);
const toastKey = getIncomingLegacyCallToastKey(call.callId);
if (status === CallState.Ringing) {
@ -818,31 +806,44 @@ export default class LegacyCallHandler extends EventEmitter {
private showICEFallbackPrompt(): void {
const cli = MatrixClientPeg.get();
const code = sub => <code>{ sub }</code>;
Modal.createDialog(QuestionDialog, {
title: _t("Call failed due to misconfigured server"),
description: <div>
<p>{ _t(
"Please ask the administrator of your homeserver " +
"(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
"order for calls to work reliably.",
{ homeserverDomain: cli.getDomain() }, { code },
) }</p>
<p>{ _t(
"Alternatively, you can try to use the public server at " +
"<code>turn.matrix.org</code>, but this will not be as reliable, and " +
"it will share your IP address with that server. You can also manage " +
"this in Settings.",
null, { code },
) }</p>
</div>,
button: _t('Try using turn.matrix.org'),
cancelButton: _t('OK'),
onFinished: (allow) => {
SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
cli.setFallbackICEServerAllowed(allow);
const code = (sub) => <code>{sub}</code>;
Modal.createDialog(
QuestionDialog,
{
title: _t("Call failed due to misconfigured server"),
description: (
<div>
<p>
{_t(
"Please ask the administrator of your homeserver " +
"(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
"order for calls to work reliably.",
{ homeserverDomain: cli.getDomain() },
{ code },
)}
</p>
<p>
{_t(
"Alternatively, you can try to use the public server at " +
"<code>turn.matrix.org</code>, but this will not be as reliable, and " +
"it will share your IP address with that server. You can also manage " +
"this in Settings.",
null,
{ code },
)}
</p>
</div>
),
button: _t("Try using turn.matrix.org"),
cancelButton: _t("OK"),
onFinished: (allow) => {
SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
cli.setFallbackICEServerAllowed(allow);
},
},
}, null, true);
null,
true,
);
}
private showMediaCaptureError(call: MatrixCall): void {
@ -851,27 +852,37 @@ export default class LegacyCallHandler extends EventEmitter {
if (call.type === CallType.Voice) {
title = _t("Unable to access microphone");
description = <div>
{ _t(
"Call failed because microphone could not be accessed. " +
"Check that a microphone is plugged in and set up correctly.",
) }
</div>;
description = (
<div>
{_t(
"Call failed because microphone could not be accessed. " +
"Check that a microphone is plugged in and set up correctly.",
)}
</div>
);
} else if (call.type === CallType.Video) {
title = _t("Unable to access webcam / microphone");
description = <div>
{ _t("Call failed because webcam or microphone could not be accessed. Check that:") }
<ul>
<li>{ _t("A microphone and webcam are plugged in and set up correctly") }</li>
<li>{ _t("Permission is granted to use the webcam") }</li>
<li>{ _t("No other application is using the webcam") }</li>
</ul>
</div>;
description = (
<div>
{_t("Call failed because webcam or microphone could not be accessed. Check that:")}
<ul>
<li>{_t("A microphone and webcam are plugged in and set up correctly")}</li>
<li>{_t("Permission is granted to use the webcam")}</li>
<li>{_t("No other application is using the webcam")}</li>
</ul>
</div>
);
}
Modal.createDialog(ErrorDialog, {
title, description,
}, null, true);
Modal.createDialog(
ErrorDialog,
{
title,
description,
},
null,
true,
);
}
private async placeMatrixCall(roomId: string, type: CallType, transferee?: MatrixCall): Promise<void> {
@ -898,7 +909,7 @@ export default class LegacyCallHandler extends EventEmitter {
this.addCallForRoom(roomId, call);
} catch (e) {
Modal.createDialog(ErrorDialog, {
title: _t('Already in call'),
title: _t("Already in call"),
description: _t("You're already in a call with this person."),
});
return;
@ -913,7 +924,7 @@ export default class LegacyCallHandler extends EventEmitter {
if (type === CallType.Voice) {
call.placeVoiceCall();
} else if (type === 'video') {
} else if (type === "video") {
call.placeVideoCall();
} else {
logger.error("Unknown conf call type: " + type);
@ -930,16 +941,16 @@ export default class LegacyCallHandler extends EventEmitter {
// if the runtime env doesn't do VoIP, whine.
if (!MatrixClientPeg.get().supportsVoip()) {
Modal.createDialog(ErrorDialog, {
title: _t('Calls are unsupported'),
description: _t('You cannot place calls in this browser.'),
title: _t("Calls are unsupported"),
description: _t("You cannot place calls in this browser."),
});
return;
}
if (MatrixClientPeg.get().getSyncState() === SyncState.Error) {
Modal.createDialog(ErrorDialog, {
title: _t('Connectivity to the server has been lost'),
description: _t('You cannot place calls without a connection to the server.'),
title: _t("Connectivity to the server has been lost"),
description: _t("You cannot place calls without a connection to the server."),
});
return;
}
@ -947,7 +958,7 @@ export default class LegacyCallHandler extends EventEmitter {
// don't allow > 2 calls to be placed.
if (this.getAllActiveCalls().length > 1) {
Modal.createDialog(ErrorDialog, {
title: _t('Too Many Calls'),
title: _t("Too Many Calls"),
description: _t("You've reached the maximum number of simultaneous calls."),
});
return;
@ -965,13 +976,14 @@ export default class LegacyCallHandler extends EventEmitter {
const members = getJoinedNonFunctionalMembers(room);
if (members.length <= 1) {
Modal.createDialog(ErrorDialog, {
description: _t('You cannot place a call with yourself.'),
description: _t("You cannot place a call with yourself."),
});
} else if (members.length === 2) {
logger.info(`Place ${type} call in ${roomId}`);
await this.placeMatrixCall(roomId, type, transferee);
} else { // > 2
} else {
// > 2
await this.placeJitsiCall(roomId, type);
}
}
@ -1010,7 +1022,7 @@ export default class LegacyCallHandler extends EventEmitter {
if (this.getAllActiveCalls().length > 1) {
Modal.createDialog(ErrorDialog, {
title: _t('Too Many Calls'),
title: _t("Too Many Calls"),
description: _t("You've reached the maximum number of simultaneous calls."),
});
return;
@ -1066,7 +1078,9 @@ export default class LegacyCallHandler extends EventEmitter {
}
public async startTransferToPhoneNumber(
call: MatrixCall, destination: string, consultFirst: boolean,
call: MatrixCall,
destination: string,
consultFirst: boolean,
): Promise<void> {
if (consultFirst) {
// if we're consulting, we just start by placing a call to the transfer
@ -1087,9 +1101,7 @@ export default class LegacyCallHandler extends EventEmitter {
await this.startTransferToMatrixID(call, results[0].userid, consultFirst);
}
public async startTransferToMatrixID(
call: MatrixCall, destination: string, consultFirst: boolean,
): Promise<void> {
public async startTransferToMatrixID(call: MatrixCall, destination: string, consultFirst: boolean): Promise<void> {
if (consultFirst) {
const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), destination);
@ -1107,8 +1119,8 @@ export default class LegacyCallHandler extends EventEmitter {
} catch (e) {
logger.log("Failed to transfer call", e);
Modal.createDialog(ErrorDialog, {
title: _t('Transfer Failed'),
description: _t('Failed to transfer call'),
title: _t("Transfer Failed"),
description: _t("Failed to transfer call"),
});
}
}
@ -1145,10 +1157,10 @@ export default class LegacyCallHandler extends EventEmitter {
const client = MatrixClientPeg.get();
logger.info(`Place conference call in ${roomId}`);
dis.dispatch({ action: 'appsDrawer', show: true });
dis.dispatch({ action: "appsDrawer", show: true });
// Prevent double clicking the call button
const widget = WidgetStore.instance.getApps(roomId).find(app => WidgetType.JITSI.matches(app.type));
const widget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.JITSI.matches(app.type));
if (widget) {
// If there already is a Jitsi widget, pin it
WidgetLayoutStore.instance.moveToContainer(client.getRoom(roomId), widget, Container.Top);
@ -1156,12 +1168,12 @@ export default class LegacyCallHandler extends EventEmitter {
}
try {
await WidgetUtils.addJitsiWidget(roomId, type, 'Jitsi', false);
logger.log('Jitsi widget added');
await WidgetUtils.addJitsiWidget(roomId, type, "Jitsi", false);
logger.log("Jitsi widget added");
} catch (e) {
if (e.errcode === 'M_FORBIDDEN') {
if (e.errcode === "M_FORBIDDEN") {
Modal.createDialog(ErrorDialog, {
title: _t('Permission Required'),
title: _t("Permission Required"),
description: _t("You do not have permission to start a conference call in this room"),
});
}
@ -1175,8 +1187,8 @@ export default class LegacyCallHandler extends EventEmitter {
const roomInfo = WidgetStore.instance.getRoom(roomId);
if (!roomInfo) return; // "should never happen" clauses go here
const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
jitsiWidgets.forEach(w => {
const jitsiWidgets = roomInfo.widgets.filter((w) => WidgetType.JITSI.matches(w.type));
jitsiWidgets.forEach((w) => {
const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(w));
if (!messaging) return; // more "should never happen" words

View file

@ -17,27 +17,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { createClient } from 'matrix-js-sdk/src/matrix';
import { createClient } from "matrix-js-sdk/src/matrix";
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes";
import { QueryDict } from 'matrix-js-sdk/src/utils';
import { QueryDict } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";
import { IMatrixClientCreds, MatrixClientPeg } from './MatrixClientPeg';
import { IMatrixClientCreds, MatrixClientPeg } from "./MatrixClientPeg";
import SecurityCustomisations from "./customisations/Security";
import EventIndexPeg from './indexing/EventIndexPeg';
import createMatrixClient from './utils/createMatrixClient';
import Notifier from './Notifier';
import UserActivity from './UserActivity';
import Presence from './Presence';
import dis from './dispatcher/dispatcher';
import DMRoomMap from './utils/DMRoomMap';
import Modal from './Modal';
import ActiveWidgetStore from './stores/ActiveWidgetStore';
import EventIndexPeg from "./indexing/EventIndexPeg";
import createMatrixClient from "./utils/createMatrixClient";
import Notifier from "./Notifier";
import UserActivity from "./UserActivity";
import Presence from "./Presence";
import dis from "./dispatcher/dispatcher";
import DMRoomMap from "./utils/DMRoomMap";
import Modal from "./Modal";
import ActiveWidgetStore from "./stores/ActiveWidgetStore";
import PlatformPeg from "./PlatformPeg";
import { sendLoginRequest } from "./Login";
import * as StorageManager from './utils/StorageManager';
import * as StorageManager from "./utils/StorageManager";
import SettingsStore from "./settings/SettingsStore";
import ToastStore from "./stores/ToastStore";
import { IntegrationManagers } from "./integrations/IntegrationManagers";
@ -47,7 +47,7 @@ import { Jitsi } from "./widgets/Jitsi";
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform";
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
import { PosthogAnalytics } from "./PosthogAnalytics";
import LegacyCallHandler from './LegacyCallHandler';
import LegacyCallHandler from "./LegacyCallHandler";
import LifecycleCustomisations from "./customisations/Lifecycle";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import { _t } from "./languageHandler";
@ -61,7 +61,7 @@ import { DialogOpener } from "./utils/DialogOpener";
import { Action } from "./dispatcher/actions";
import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler";
import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload";
import { SdkContextClass } from './contexts/SDKContext';
import { SdkContextClass } from "./contexts/SDKContext";
const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url";
@ -129,19 +129,18 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
enableGuest = false;
}
if (
enableGuest &&
fragmentQueryParams.guest_user_id &&
fragmentQueryParams.guest_access_token
) {
if (enableGuest && fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_access_token) {
logger.log("Using guest access credentials");
return doSetLoggedIn({
userId: fragmentQueryParams.guest_user_id as string,
accessToken: fragmentQueryParams.guest_access_token as string,
homeserverUrl: guestHsUrl,
identityServerUrl: guestIsUrl,
guest: true,
}, true).then(() => true);
return doSetLoggedIn(
{
userId: fragmentQueryParams.guest_user_id as string,
accessToken: fragmentQueryParams.guest_access_token as string,
homeserverUrl: guestHsUrl,
identityServerUrl: guestIsUrl,
guest: true,
},
true,
).then(() => true);
}
const success = await restoreFromLocalStorage({
ignoreGuest: Boolean(opts.ignoreGuest),
@ -205,90 +204,94 @@ export function attemptTokenLogin(
logger.warn("Cannot log in with token: can't determine HS URL to use");
Modal.createDialog(ErrorDialog, {
title: _t("We couldn't log you in"),
description: _t("We asked the browser to remember which homeserver you use to let you sign in, " +
"but unfortunately your browser has forgotten it. Go to the sign in page and try again."),
description: _t(
"We asked the browser to remember which homeserver you use to let you sign in, " +
"but unfortunately your browser has forgotten it. Go to the sign in page and try again.",
),
button: _t("Try again"),
});
return Promise.resolve(false);
}
return sendLoginRequest(
homeserver,
identityServer,
"m.login.token", {
token: queryParams.loginToken as string,
initial_device_display_name: defaultDeviceDisplayName,
},
).then(function(creds) {
logger.log("Logged in with token");
return clearStorage().then(async () => {
await persistCredentials(creds);
// remember that we just logged in
sessionStorage.setItem("mx_fresh_login", String(true));
return true;
return sendLoginRequest(homeserver, identityServer, "m.login.token", {
token: queryParams.loginToken as string,
initial_device_display_name: defaultDeviceDisplayName,
})
.then(function (creds) {
logger.log("Logged in with token");
return clearStorage().then(async () => {
await persistCredentials(creds);
// remember that we just logged in
sessionStorage.setItem("mx_fresh_login", String(true));
return true;
});
})
.catch((err) => {
Modal.createDialog(ErrorDialog, {
title: _t("We couldn't log you in"),
description:
err.name === "ConnectionError"
? _t(
"Your homeserver was unreachable and was not able to log you in. Please try again. " +
"If this continues, please contact your homeserver administrator.",
)
: _t(
"Your homeserver rejected your log in attempt. " +
"This could be due to things just taking too long. Please try again. " +
"If this continues, please contact your homeserver administrator.",
),
button: _t("Try again"),
onFinished: (tryAgain) => {
if (tryAgain) {
const cli = createClient({
baseUrl: homeserver,
idBaseUrl: identityServer,
});
const idpId = localStorage.getItem(SSO_IDP_ID_KEY) || undefined;
PlatformPeg.get().startSingleSignOn(cli, "sso", fragmentAfterLogin, idpId);
}
},
});
logger.error("Failed to log in with login token:");
logger.error(err);
return false;
});
}).catch((err) => {
Modal.createDialog(ErrorDialog, {
title: _t("We couldn't log you in"),
description: err.name === "ConnectionError"
? _t("Your homeserver was unreachable and was not able to log you in. Please try again. " +
"If this continues, please contact your homeserver administrator.")
: _t("Your homeserver rejected your log in attempt. " +
"This could be due to things just taking too long. Please try again. " +
"If this continues, please contact your homeserver administrator."),
button: _t("Try again"),
onFinished: tryAgain => {
if (tryAgain) {
const cli = createClient({
baseUrl: homeserver,
idBaseUrl: identityServer,
});
const idpId = localStorage.getItem(SSO_IDP_ID_KEY) || undefined;
PlatformPeg.get().startSingleSignOn(cli, "sso", fragmentAfterLogin, idpId);
}
},
});
logger.error("Failed to log in with login token:");
logger.error(err);
return false;
});
}
export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
if (e.reason === InvalidStoreError.TOGGLED_LAZY_LOADING) {
return Promise.resolve().then(() => {
const lazyLoadEnabled = e.value;
if (lazyLoadEnabled) {
return new Promise((resolve) => {
Modal.createDialog(LazyLoadingResyncDialog, {
onFinished: resolve,
return Promise.resolve()
.then(() => {
const lazyLoadEnabled = e.value;
if (lazyLoadEnabled) {
return new Promise((resolve) => {
Modal.createDialog(LazyLoadingResyncDialog, {
onFinished: resolve,
});
});
});
} else {
// show warning about simultaneous use
// between LL/non-LL version on same host.
// as disabling LL when previously enabled
// is a strong indicator of this (/develop & /app)
return new Promise((resolve) => {
Modal.createDialog(LazyLoadingDisabledDialog, {
onFinished: resolve,
host: window.location.host,
} else {
// show warning about simultaneous use
// between LL/non-LL version on same host.
// as disabling LL when previously enabled
// is a strong indicator of this (/develop & /app)
return new Promise((resolve) => {
Modal.createDialog(LazyLoadingDisabledDialog, {
onFinished: resolve,
host: window.location.host,
});
});
});
}
}).then(() => {
return MatrixClientPeg.get().store.deleteAllData();
}).then(() => {
PlatformPeg.get().reload();
});
}
})
.then(() => {
return MatrixClientPeg.get().store.deleteAllData();
})
.then(() => {
PlatformPeg.get().reload();
});
}
}
function registerAsGuest(
hsUrl: string,
isUrl: string,
defaultDeviceDisplayName: string,
): Promise<boolean> {
function registerAsGuest(hsUrl: string, isUrl: string, defaultDeviceDisplayName: string): Promise<boolean> {
logger.log(`Doing guest login on ${hsUrl}`);
// create a temporary MatrixClient to do the login
@ -296,24 +299,32 @@ function registerAsGuest(
baseUrl: hsUrl,
});
return client.registerGuest({
body: {
initial_device_display_name: defaultDeviceDisplayName,
},
}).then((creds) => {
logger.log(`Registered as guest: ${creds.user_id}`);
return doSetLoggedIn({
userId: creds.user_id,
deviceId: creds.device_id,
accessToken: creds.access_token,
homeserverUrl: hsUrl,
identityServerUrl: isUrl,
guest: true,
}, true).then(() => true);
}, (err) => {
logger.error("Failed to register as guest", err);
return false;
});
return client
.registerGuest({
body: {
initial_device_display_name: defaultDeviceDisplayName,
},
})
.then(
(creds) => {
logger.log(`Registered as guest: ${creds.user_id}`);
return doSetLoggedIn(
{
userId: creds.user_id,
deviceId: creds.device_id,
accessToken: creds.access_token,
homeserverUrl: hsUrl,
identityServerUrl: isUrl,
guest: true,
},
true,
).then(() => true);
},
(err) => {
logger.error("Failed to register as guest", err);
return false;
},
);
}
export interface IStoredSession {
@ -354,8 +365,7 @@ export async function getStoredSessionVars(): Promise<IStoredSession> {
}
// if we pre-date storing "mx_has_access_token", but we retrieved an access
// token, then we should say we have an access token
const hasAccessToken =
(localStorage.getItem("mx_has_access_token") === "true") || !!accessToken;
const hasAccessToken = localStorage.getItem("mx_has_access_token") === "true" || !!accessToken;
const userId = localStorage.getItem("mx_user_id");
const deviceId = localStorage.getItem("mx_device_id");
@ -378,20 +388,22 @@ async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array> {
for (let i = 0; i < pickleKey.length; i++) {
pickleKeyBuffer[i] = pickleKey.charCodeAt(i);
}
const hkdfKey = await window.crypto.subtle.importKey(
"raw", pickleKeyBuffer, "HKDF", false, ["deriveBits"],
);
const hkdfKey = await window.crypto.subtle.importKey("raw", pickleKeyBuffer, "HKDF", false, ["deriveBits"]);
pickleKeyBuffer.fill(0);
return new Uint8Array(await window.crypto.subtle.deriveBits(
{
name: "HKDF", hash: "SHA-256",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
salt: new Uint8Array(32), info: new Uint8Array(0),
},
hkdfKey,
256,
));
return new Uint8Array(
await window.crypto.subtle.deriveBits(
{
name: "HKDF",
hash: "SHA-256",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
salt: new Uint8Array(32),
info: new Uint8Array(0),
},
hkdfKey,
256,
),
);
}
async function abortLogin() {
@ -400,9 +412,7 @@ async function abortLogin() {
await clearStorage();
// This error feels a bit clunky, but we want to make sure we don't go any
// further and instead head back to sign in.
throw new AbortLoginAndRebuildStorage(
"Aborting login in progress because of storage inconsistency",
);
throw new AbortLoginAndRebuildStorage("Aborting login in progress because of storage inconsistency");
}
}
@ -452,16 +462,19 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
sessionStorage.removeItem("mx_fresh_login");
logger.log(`Restoring session for ${userId}`);
await doSetLoggedIn({
userId: userId,
deviceId: deviceId,
accessToken: decryptedAccessToken as string,
homeserverUrl: hsUrl,
identityServerUrl: isUrl,
guest: isGuest,
pickleKey: pickleKey,
freshLogin: freshLogin,
}, false);
await doSetLoggedIn(
{
userId: userId,
deviceId: deviceId,
accessToken: decryptedAccessToken as string,
homeserverUrl: hsUrl,
identityServerUrl: isUrl,
guest: isGuest,
pickleKey: pickleKey,
freshLogin: freshLogin,
},
false,
);
return true;
} else {
logger.log("No previous session found.");
@ -503,9 +516,10 @@ async function handleLoadSessionFailure(e: Error): Promise<boolean> {
export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<MatrixClient> {
credentials.freshLogin = true;
stopMatrixClient();
const pickleKey = credentials.userId && credentials.deviceId
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
: null;
const pickleKey =
credentials.userId && credentials.deviceId
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
: null;
if (pickleKey) {
logger.log("Created pickle key");
@ -561,20 +575,22 @@ export async function hydrateSession(credentials: IMatrixClientCreds): Promise<M
*
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
*/
async function doSetLoggedIn(
credentials: IMatrixClientCreds,
clearStorageEnabled: boolean,
): Promise<MatrixClient> {
async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnabled: boolean): Promise<MatrixClient> {
credentials.guest = Boolean(credentials.guest);
const softLogout = isSoftLogout();
logger.log(
"setLoggedIn: mxid: " + credentials.userId +
" deviceId: " + credentials.deviceId +
" guest: " + credentials.guest +
" hs: " + credentials.homeserverUrl +
" softLogout: " + softLogout,
"setLoggedIn: mxid: " +
credentials.userId +
" deviceId: " +
credentials.deviceId +
" guest: " +
credentials.guest +
" hs: " +
credentials.homeserverUrl +
" softLogout: " +
softLogout,
" freshLogin: " + credentials.freshLogin,
);
@ -585,7 +601,7 @@ async function doSetLoggedIn(
//
// we fire it *synchronously* to make sure it fires before on_logged_in.
// (dis.dispatch uses `window.setTimeout`, which does not guarantee ordering.)
dis.dispatch({ action: 'on_logging_in' }, true);
dis.dispatch({ action: "on_logging_in" }, true);
if (clearStorageEnabled) {
await clearStorage();
@ -633,13 +649,13 @@ async function doSetLoggedIn(
}
dis.fire(Action.OnLoggedIn);
await startMatrixClient(/*startSyncing=*/!softLogout);
await startMatrixClient(/*startSyncing=*/ !softLogout);
return client;
}
function showStorageEvictedDialog(): Promise<boolean> {
return new Promise(resolve => {
return new Promise((resolve) => {
Modal.createDialog(StorageEvictedDialog, {
onFinished: resolve,
});
@ -648,7 +664,7 @@ function showStorageEvictedDialog(): Promise<boolean> {
// Note: Babel 6 requires the `transform-builtin-extend` plugin for this to satisfy
// `instanceof`. Babel 7 supports this natively in their class handling.
class AbortLoginAndRebuildStorage extends Error { }
class AbortLoginAndRebuildStorage extends Error {}
async function persistCredentials(credentials: IMatrixClientCreds): Promise<void> {
localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
@ -680,10 +696,7 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise<void
// save either the encrypted access token, or the plain access
// token if we were unable to encrypt (e.g. if the browser doesn't
// have WebCrypto).
await StorageManager.idbSave(
"account", "mx_access_token",
encryptedAccessToken || credentials.accessToken,
);
await StorageManager.idbSave("account", "mx_access_token", encryptedAccessToken || credentials.accessToken);
} catch (e) {
// if we couldn't save to indexedDB, fall back to localStorage. We
// store the access token unencrypted since localStorage only saves
@ -693,9 +706,7 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise<void
localStorage.setItem("mx_has_pickle_key", String(true));
} else {
try {
await StorageManager.idbSave(
"account", "mx_access_token", credentials.accessToken,
);
await StorageManager.idbSave("account", "mx_access_token", credentials.accessToken);
} catch (e) {
localStorage.setItem("mx_access_token", credentials.accessToken);
}
@ -767,8 +778,8 @@ export function softLogout(): void {
// Ensure that we dispatch a view change **before** stopping the client so
// so that React components unmount first. This avoids React soft crashes
// that can occur when components try to use a null client.
dis.dispatch({ action: 'on_client_not_viable' }); // generic version of on_logged_out
stopMatrixClient(/*unsetClient=*/false);
dis.dispatch({ action: "on_client_not_viable" }); // generic version of on_logged_out
stopMatrixClient(/*unsetClient=*/ false);
// DO NOT CALL LOGOUT. A soft logout preserves data, logout does not.
}
@ -794,7 +805,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
// to add listeners for the 'sync' event so otherwise we'd have
// a race condition (and we need to dispatch synchronously for this
// to work).
dis.dispatch({ action: 'will_start_client' }, true);
dis.dispatch({ action: "will_start_client" }, true);
// reset things first just in case
SdkContextClass.instance.typingStore.reset();
@ -840,7 +851,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
// dispatch that we finished starting up to wire up any other bits
// of the matrix client that cannot be set prior to starting up.
dis.dispatch({ action: 'client_started' });
dis.dispatch({ action: "client_started" });
if (isSoftLogout()) {
softLogout();
@ -894,7 +905,7 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
// now restore those invites and registration time
if (!opts?.deleteEverything) {
pendingInvites.forEach(i => {
pendingInvites.forEach((i) => {
const roomId = i.roomId;
delete i.roomId; // delete to avoid confusing the store
ThreepidInviteStore.instance.storeInvite(roomId, i);
@ -954,9 +965,12 @@ window.mxLoginWithAccessToken = async (hsUrl: string, accessToken: string): Prom
accessToken,
});
const { user_id: userId } = await tempClient.whoami();
await doSetLoggedIn({
homeserverUrl: hsUrl,
accessToken,
userId,
}, true);
await doSetLoggedIn(
{
homeserverUrl: hsUrl,
accessToken,
userId,
},
true,
);
};

View file

@ -25,7 +25,7 @@ export function getConfigLivestreamUrl() {
}
// Dummy rtmp URL used to signal that we want a special audio-only stream
const AUDIOSTREAM_DUMMY_URL = 'rtmp://audiostream.dummy/';
const AUDIOSTREAM_DUMMY_URL = "rtmp://audiostream.dummy/";
async function createLiveStream(roomId: string) {
const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
@ -33,7 +33,7 @@ async function createLiveStream(roomId: string) {
const url = getConfigLivestreamUrl() + "/createStream";
const response = await window.fetch(url, {
method: 'POST',
method: "POST",
headers: {
"Content-Type": "application/json",
},
@ -44,7 +44,7 @@ async function createLiveStream(roomId: string) {
});
const respBody = await response.json();
return respBody['stream_id'];
return respBody["stream_id"];
}
export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string) {

View file

@ -37,12 +37,7 @@ export default class Login {
private defaultDeviceDisplayName: string;
private tempClient: MatrixClient;
constructor(
hsUrl: string,
isUrl: string,
fallbackHsUrl?: string,
opts?: ILoginOptions,
) {
constructor(hsUrl: string, isUrl: string, fallbackHsUrl?: string, opts?: ILoginOptions) {
this.hsUrl = hsUrl;
this.isUrl = isUrl;
this.fallbackHsUrl = fallbackHsUrl;
@ -102,7 +97,7 @@ export default class Login {
let identifier;
if (phoneCountry && phoneNumber) {
identifier = {
type: 'm.id.phone',
type: "m.id.phone",
country: phoneCountry,
phone: phoneNumber,
// XXX: Synapse historically wanted `number` and not `phone`
@ -110,13 +105,13 @@ export default class Login {
};
} else if (isEmail) {
identifier = {
type: 'm.id.thirdparty',
medium: 'email',
type: "m.id.thirdparty",
medium: "email",
address: username,
};
} else {
identifier = {
type: 'm.id.user',
type: "m.id.user",
user: username,
};
}
@ -128,30 +123,30 @@ export default class Login {
};
const tryFallbackHs = (originalError) => {
return sendLoginRequest(
this.fallbackHsUrl, this.isUrl, 'm.login.password', loginParams,
).catch((fallbackError) => {
logger.log("fallback HS login failed", fallbackError);
// throw the original error
throw originalError;
});
return sendLoginRequest(this.fallbackHsUrl, this.isUrl, "m.login.password", loginParams).catch(
(fallbackError) => {
logger.log("fallback HS login failed", fallbackError);
// throw the original error
throw originalError;
},
);
};
let originalLoginError = null;
return sendLoginRequest(
this.hsUrl, this.isUrl, 'm.login.password', loginParams,
).catch((error) => {
originalLoginError = error;
if (error.httpStatus === 403) {
if (this.fallbackHsUrl) {
return tryFallbackHs(originalLoginError);
return sendLoginRequest(this.hsUrl, this.isUrl, "m.login.password", loginParams)
.catch((error) => {
originalLoginError = error;
if (error.httpStatus === 403) {
if (this.fallbackHsUrl) {
return tryFallbackHs(originalLoginError);
}
}
}
throw originalLoginError;
}).catch((error) => {
logger.log("Login failed", error);
throw error;
});
throw originalLoginError;
})
.catch((error) => {
logger.log("Login failed", error);
throw error;
});
}
}

View file

@ -16,20 +16,19 @@ limitations under the License.
*/
import "./@types/commonmark"; // import better types than @types/commonmark
import * as commonmark from 'commonmark';
import * as commonmark from "commonmark";
import { escape } from "lodash";
import { logger } from 'matrix-js-sdk/src/logger';
import { logger } from "matrix-js-sdk/src/logger";
import { linkify } from './linkify-matrix';
import { linkify } from "./linkify-matrix";
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
const ALLOWED_HTML_TAGS = ["sub", "sup", "del", "u"];
// These types of node are definitely text
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
const TEXT_NODES = ["text", "softbreak", "linebreak", "paragraph", "document"];
function isAllowedHtmlTag(node: commonmark.Node): boolean {
if (node.literal != null &&
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) {
if (node.literal != null && node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) {
return true;
}
@ -59,14 +58,14 @@ function isMultiLine(node: commonmark.Node): boolean {
function getTextUntilEndOrLinebreak(node: commonmark.Node) {
let currentNode = node;
let text = '';
while (currentNode !== null && currentNode.type !== 'softbreak' && currentNode.type !== 'linebreak') {
let text = "";
while (currentNode !== null && currentNode.type !== "softbreak" && currentNode.type !== "linebreak") {
const { literal, type } = currentNode;
if (type === 'text' && literal) {
if (type === "text" && literal) {
let n = 0;
let char = literal[n];
while (char !== ' ' && char !== null && n <= literal.length) {
if (char === ' ') {
while (char !== " " && char !== null && n <= literal.length) {
if (char === " ") {
break;
}
if (char) {
@ -75,7 +74,7 @@ function getTextUntilEndOrLinebreak(node: commonmark.Node) {
n += 1;
char = literal[n];
}
if (char === ' ') {
if (char === " ") {
break;
}
}
@ -85,8 +84,8 @@ function getTextUntilEndOrLinebreak(node: commonmark.Node) {
}
const formattingChangesByNodeType = {
'emph': '_',
'strong': '__',
emph: "_",
strong: "__",
};
/**
@ -98,7 +97,7 @@ const innerNodeLiteral = (node: commonmark.Node): string => {
const walker = node.walker();
let step: commonmark.NodeWalkingStep;
while (step = walker.next()) {
while ((step = walker.next())) {
const currentNode = step.node;
const currentNodeLiteral = currentNode.literal;
if (step.entering && currentNode.type === "text" && currentNodeLiteral) {
@ -141,13 +140,13 @@ export default class Markdown {
private repairLinks(parsed: commonmark.Node) {
const walker = parsed.walker();
let event: commonmark.NodeWalkingStep = null;
let text = '';
let text = "";
let isInPara = false;
let previousNode: commonmark.Node | null = null;
let shouldUnlinkFormattingNode = false;
while ((event = walker.next())) {
const { node } = event;
if (node.type === 'paragraph') {
if (node.type === "paragraph") {
if (event.entering) {
isInPara = true;
} else {
@ -157,25 +156,25 @@ export default class Markdown {
if (isInPara) {
// Clear saved string when line ends
if (
node.type === 'softbreak' ||
node.type === 'linebreak' ||
node.type === "softbreak" ||
node.type === "linebreak" ||
// Also start calculating the text from the beginning on any spaces
(node.type === 'text' && node.literal === ' ')
(node.type === "text" && node.literal === " ")
) {
text = '';
text = "";
continue;
}
// Break up text nodes on spaces, so that we don't shoot past them without resetting
if (node.type === 'text') {
if (node.type === "text") {
const [thisPart, ...nextParts] = node.literal.split(/( )/);
node.literal = thisPart;
text += thisPart;
// Add the remaining parts as siblings
nextParts.reverse().forEach(part => {
nextParts.reverse().forEach((part) => {
if (part) {
const nextNode = new commonmark.Node('text');
const nextNode = new commonmark.Node("text");
nextNode.literal = part;
node.insertAfter(nextNode);
// Make the iterator aware of the newly inserted node
@ -185,7 +184,7 @@ export default class Markdown {
}
// We should not do this if previous node was not a textnode, as we can't combine it then.
if ((node.type === 'emph' || node.type === 'strong') && previousNode.type === 'text') {
if ((node.type === "emph" || node.type === "strong") && previousNode.type === "text") {
if (event.entering) {
const foundLinks = linkify.find(text);
for (const { value } of foundLinks) {
@ -201,10 +200,10 @@ export default class Markdown {
const newLinks = linkify.find(newText);
// Should always find only one link here, if it finds more it means that the algorithm is broken
if (newLinks.length === 1) {
const emphasisTextNode = new commonmark.Node('text');
const emphasisTextNode = new commonmark.Node("text");
emphasisTextNode.literal = nonEmphasizedText;
previousNode.insertAfter(emphasisTextNode);
node.firstChild.literal = '';
node.firstChild.literal = "";
event = node.walker().next();
// Remove `em` opening and closing nodes
node.unlink();
@ -239,12 +238,12 @@ export default class Markdown {
const walker = this.parsed.walker();
let ev: commonmark.NodeWalkingStep;
while (ev = walker.next()) {
while ((ev = walker.next())) {
const node = ev.node;
if (TEXT_NODES.indexOf(node.type) > -1) {
// definitely text
continue;
} else if (node.type == 'html_inline' || node.type == 'html_block') {
} else if (node.type == "html_inline" || node.type == "html_block") {
// if it's an allowed html tag, we need to render it and therefore
// we will need to use HTML. If it's not allowed, it's not HTML since
// we'll just be treating it as text.
@ -267,7 +266,7 @@ export default class Markdown {
// so if these are just newline characters then the
// block quote ends up all on one line
// (https://github.com/vector-im/element-web/issues/3154)
softbreak: '<br />',
softbreak: "<br />",
});
// Trying to strip out the wrapping <p/> causes a lot more complication
@ -279,7 +278,7 @@ export default class Markdown {
//
// Let's try sending with <p/>s anyway for now, though.
const realParagraph = renderer.paragraph;
renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
renderer.paragraph = function (node: commonmark.Node, entering: boolean) {
// If there is only one top level node, just return the
// bare text: it's a single line of text and so should be
// 'inline', rather than unnecessarily wrapped in its own
@ -288,31 +287,31 @@ export default class Markdown {
// However, if it's a blockquote, adds a p tag anyway
// in order to avoid deviation to commonmark and unexpected
// results when parsing the formatted HTML.
if (node.parent.type === 'block_quote'|| isMultiLine(node)) {
if (node.parent.type === "block_quote" || isMultiLine(node)) {
realParagraph.call(this, node, entering);
}
};
renderer.link = function(node, entering) {
renderer.link = function (node, entering) {
const attrs = this.attrs(node);
if (entering) {
attrs.push(['href', this.esc(node.destination)]);
attrs.push(["href", this.esc(node.destination)]);
if (node.title) {
attrs.push(['title', this.esc(node.title)]);
attrs.push(["title", this.esc(node.title)]);
}
// Modified link behaviour to treat them all as external and
// thus opening in a new tab.
if (externalLinks) {
attrs.push(['target', '_blank']);
attrs.push(['rel', 'noreferrer noopener']);
attrs.push(["target", "_blank"]);
attrs.push(["rel", "noreferrer noopener"]);
}
this.tag('a', attrs);
this.tag("a", attrs);
} else {
this.tag('/a');
this.tag("/a");
}
};
renderer.html_inline = function(node: commonmark.Node) {
renderer.html_inline = function (node: commonmark.Node) {
if (isAllowedHtmlTag(node)) {
this.lit(node.literal);
} else {
@ -320,7 +319,7 @@ export default class Markdown {
}
};
renderer.html_block = function(node: commonmark.Node) {
renderer.html_block = function (node: commonmark.Node) {
/*
// as with `paragraph`, we only insert line breaks
// if there are multiple lines in the markdown.
@ -348,19 +347,19 @@ export default class Markdown {
toPlaintext(): string {
const renderer = new commonmark.HtmlRenderer({ safe: false });
renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
renderer.paragraph = function (node: commonmark.Node, entering: boolean) {
// as with toHTML, only append lines to paragraphs if there are
// multiple paragraphs
if (isMultiLine(node)) {
if (!entering && node.next) {
this.lit('\n\n');
this.lit("\n\n");
}
}
};
renderer.html_block = function(node: commonmark.Node) {
renderer.html_block = function (node: commonmark.Node) {
this.lit(node.literal);
if (isMultiLine(node) && node.next) this.lit('\n\n');
if (isMultiLine(node) && node.next) this.lit("\n\n");
};
return renderer.render(this.parsed);

View file

@ -17,26 +17,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ICreateClientOpts, PendingEventOrdering, RoomNameState, RoomNameType } from 'matrix-js-sdk/src/matrix';
import { IStartClientOpts, MatrixClient } from 'matrix-js-sdk/src/client';
import { MemoryStore } from 'matrix-js-sdk/src/store/memory';
import * as utils from 'matrix-js-sdk/src/utils';
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
import { verificationMethods } from 'matrix-js-sdk/src/crypto';
import { ICreateClientOpts, PendingEventOrdering, RoomNameState, RoomNameType } from "matrix-js-sdk/src/matrix";
import { IStartClientOpts, MatrixClient } from "matrix-js-sdk/src/client";
import { MemoryStore } from "matrix-js-sdk/src/store/memory";
import * as utils from "matrix-js-sdk/src/utils";
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
import { verificationMethods } from "matrix-js-sdk/src/crypto";
import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode";
import { logger } from "matrix-js-sdk/src/logger";
import createMatrixClient from './utils/createMatrixClient';
import SettingsStore from './settings/SettingsStore';
import MatrixActionCreators from './actions/MatrixActionCreators';
import Modal from './Modal';
import createMatrixClient from "./utils/createMatrixClient";
import SettingsStore from "./settings/SettingsStore";
import MatrixActionCreators from "./actions/MatrixActionCreators";
import Modal from "./Modal";
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
import * as StorageManager from './utils/StorageManager';
import IdentityAuthClient from './IdentityAuthClient';
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
import * as StorageManager from "./utils/StorageManager";
import IdentityAuthClient from "./IdentityAuthClient";
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from "./SecurityManager";
import SecurityCustomisations from "./customisations/Security";
import { SlidingSyncManager } from './SlidingSyncManager';
import { SlidingSyncManager } from "./SlidingSyncManager";
import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog";
import { _t } from "./languageHandler";
@ -156,10 +156,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
}
public currentUserIsJustRegistered(): boolean {
return (
this.matrixClient &&
this.matrixClient.credentials.userId === this.justRegisteredUserId
);
return this.matrixClient && this.matrixClient.credentials.userId === this.justRegisteredUserId;
}
public userRegisteredWithinLastHours(hours: number): boolean {
@ -170,7 +167,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
try {
const registrationTime = parseInt(window.localStorage.getItem("mx_registration_time"), 10);
const diff = Date.now() - registrationTime;
return (diff / 36e5) <= hours;
return diff / 36e5 <= hours;
} catch (e) {
return false;
}
@ -191,20 +188,20 @@ class MatrixClientPegClass implements IMatrixClientPeg {
}
public async assign(): Promise<any> {
for (const dbType of ['indexeddb', 'memory']) {
for (const dbType of ["indexeddb", "memory"]) {
try {
const promise = this.matrixClient.store.startup();
logger.log("MatrixClientPeg: waiting for MatrixClient store to initialise");
await promise;
break;
} catch (err) {
if (dbType === 'indexeddb') {
logger.error('Error starting matrixclient store - falling back to memory store', err);
if (dbType === "indexeddb") {
logger.error("Error starting matrixclient store - falling back to memory store", err);
this.matrixClient.store = new MemoryStore({
localStorage: localStorage,
});
} else {
logger.error('Failed to start memory store!', err);
logger.error("Failed to start memory store!", err);
throw err;
}
}
@ -216,13 +213,13 @@ class MatrixClientPegClass implements IMatrixClientPeg {
if (!SettingsStore.getValue("lowBandwidth") && this.matrixClient.initCrypto) {
await this.matrixClient.initCrypto();
this.matrixClient.setCryptoTrustCrossSignedDevices(
!SettingsStore.getValue('e2ee.manuallyVerifyAllSessions'),
!SettingsStore.getValue("e2ee.manuallyVerifyAllSessions"),
);
await tryToUnlockSecretStorageWithDehydrationKey(this.matrixClient);
StorageManager.setCryptoInitialised(true);
}
} catch (e) {
if (e && e.name === 'InvalidCryptoStoreError') {
if (e && e.name === "InvalidCryptoStoreError") {
// The js-sdk found a crypto DB too new for it to use
Modal.createDialog(CryptoStoreTooNewDialog);
}
@ -345,8 +342,8 @@ class MatrixClientPegClass implements IMatrixClientPeg {
deviceId: creds.deviceId,
pickleKey: creds.pickleKey,
timelineSupport: true,
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'),
fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'),
forceTURN: !SettingsStore.getValue("webRtcAllowPeerToPeer"),
fallbackICEServerAllowed: !!SettingsStore.getValue("fallbackICEServerAllowed"),
// Gather up to 20 ICE candidates when a call arrives: this should be more than we'd
// ever normally need, so effectively this should make all the gathering happen when
// the call arrives.

View file

@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import EventEmitter from 'events';
import EventEmitter from "events";
import { logger } from "matrix-js-sdk/src/logger";
import SettingsStore from "./settings/SettingsStore";
import { SettingLevel } from "./settings/SettingLevel";
import { MatrixClientPeg } from "./MatrixClientPeg";
import { _t } from './languageHandler';
import { _t } from "./languageHandler";
// XXX: MediaDeviceKind is a union type, so we make our own enum
export enum MediaDeviceKindEnum {
@ -48,7 +48,7 @@ export default class MediaDeviceHandler extends EventEmitter {
public static async hasAnyLabeledDevices(): Promise<boolean> {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.some(d => Boolean(d.label));
return devices.some((d) => Boolean(d.label));
}
/**
@ -76,7 +76,7 @@ export default class MediaDeviceHandler extends EventEmitter {
devices.forEach((device) => output[device.kind].push(device));
return output;
} catch (error) {
logger.warn('Unable to refresh WebRTC Devices: ', error);
logger.warn("Unable to refresh WebRTC Devices: ", error);
}
}
@ -84,11 +84,11 @@ export default class MediaDeviceHandler extends EventEmitter {
// Note we're looking for a device with deviceId 'default' but adding a device
// with deviceId == the empty string: this is because Chrome gives us a device
// with deviceId 'default', so we're looking for this, not the one we are adding.
if (!devices.some((i) => i.deviceId === 'default')) {
devices.unshift({ deviceId: '', label: _t('Default Device') });
return '';
if (!devices.some((i) => i.deviceId === "default")) {
devices.unshift({ deviceId: "", label: _t("Default Device") });
return "";
} else {
return 'default';
return "default";
}
};
@ -140,9 +140,15 @@ export default class MediaDeviceHandler extends EventEmitter {
public async setDevice(deviceId: string, kind: MediaDeviceKindEnum): Promise<void> {
switch (kind) {
case MediaDeviceKindEnum.AudioOutput: this.setAudioOutput(deviceId); break;
case MediaDeviceKindEnum.AudioInput: await this.setAudioInput(deviceId); break;
case MediaDeviceKindEnum.VideoInput: await this.setVideoInput(deviceId); break;
case MediaDeviceKindEnum.AudioOutput:
this.setAudioOutput(deviceId);
break;
case MediaDeviceKindEnum.AudioInput:
await this.setAudioInput(deviceId);
break;
case MediaDeviceKindEnum.VideoInput:
await this.setVideoInput(deviceId);
break;
}
}
@ -192,9 +198,12 @@ export default class MediaDeviceHandler extends EventEmitter {
*/
public static getDevice(kind: MediaDeviceKindEnum): string {
switch (kind) {
case MediaDeviceKindEnum.AudioOutput: return this.getAudioOutput();
case MediaDeviceKindEnum.AudioInput: return this.getAudioInput();
case MediaDeviceKindEnum.VideoInput: return this.getVideoInput();
case MediaDeviceKindEnum.AudioOutput:
return this.getAudioOutput();
case MediaDeviceKindEnum.AudioInput:
return this.getAudioInput();
case MediaDeviceKindEnum.VideoInput:
return this.getVideoInput();
}
}

View file

@ -15,14 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import React from "react";
import ReactDOM from "react-dom";
import classNames from "classnames";
import { defer, sleep } from "matrix-js-sdk/src/utils";
import { TypedEventEmitter } from 'matrix-js-sdk/src/models/typed-event-emitter';
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
import dis from './dispatcher/dispatcher';
import AsyncWrapper from './AsyncWrapper';
import dis from "./dispatcher/dispatcher";
import AsyncWrapper from "./AsyncWrapper";
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
@ -172,40 +172,43 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
props: IProps<T>,
): [IHandle<T>["close"], IHandle<T>["finished"]] {
const deferred = defer<T>();
return [async (...args: T) => {
if (modal.beforeClosePromise) {
await modal.beforeClosePromise;
} else if (modal.onBeforeClose) {
modal.beforeClosePromise = modal.onBeforeClose(modal.closeReason);
const shouldClose = await modal.beforeClosePromise;
modal.beforeClosePromise = null;
if (!shouldClose) {
return;
return [
async (...args: T) => {
if (modal.beforeClosePromise) {
await modal.beforeClosePromise;
} else if (modal.onBeforeClose) {
modal.beforeClosePromise = modal.onBeforeClose(modal.closeReason);
const shouldClose = await modal.beforeClosePromise;
modal.beforeClosePromise = null;
if (!shouldClose) {
return;
}
}
deferred.resolve(args);
if (props && props.onFinished) props.onFinished.apply(null, args);
const i = this.modals.indexOf(modal);
if (i >= 0) {
this.modals.splice(i, 1);
}
}
deferred.resolve(args);
if (props && props.onFinished) props.onFinished.apply(null, args);
const i = this.modals.indexOf(modal);
if (i >= 0) {
this.modals.splice(i, 1);
}
if (this.priorityModal === modal) {
this.priorityModal = null;
if (this.priorityModal === modal) {
this.priorityModal = null;
// XXX: This is destructive
this.modals = [];
}
// XXX: This is destructive
this.modals = [];
}
if (this.staticModal === modal) {
this.staticModal = null;
if (this.staticModal === modal) {
this.staticModal = null;
// XXX: This is destructive
this.modals = [];
}
// XXX: This is destructive
this.modals = [];
}
this.reRender();
}, deferred.promise];
this.reRender();
},
deferred.promise,
];
}
/**
@ -314,7 +317,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
};
private getCurrentModal(): IModal<any> {
return this.priorityModal ? this.priorityModal : (this.modals[0] || this.staticModal);
return this.priorityModal ? this.priorityModal : this.modals[0] || this.staticModal;
}
private async reRender() {
@ -325,7 +328,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
// If there is no modal to render, make all of Element available
// to screen reader users again
dis.dispatch({
action: 'aria_unhide_main_app',
action: "aria_unhide_main_app",
});
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer());
@ -336,7 +339,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
// so they won't be able to navigate into it and act on it using
// screen reader specific features
dis.dispatch({
action: 'aria_hide_main_app',
action: "aria_hide_main_app",
});
if (this.staticModal) {
@ -344,9 +347,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
const staticDialog = (
<div className={classes}>
<div className="mx_Dialog">
{ this.staticModal.elem }
</div>
<div className="mx_Dialog">{this.staticModal.elem}</div>
<div
data-testid="dialog-background"
className="mx_Dialog_background mx_Dialog_staticBackground"
@ -369,9 +370,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
const dialog = (
<div className={classes}>
<div className="mx_Dialog">
{ modal.elem }
</div>
<div className="mx_Dialog">{modal.elem}</div>
<div
data-testid="dialog-background"
className="mx_Dialog_background"

View file

@ -95,9 +95,7 @@ export default class NodeAnimator extends React.Component<IProps> {
newProps.style = startStyle;
}
newProps.ref = ((n) => this.collectNode(
c.key, n, restingStyle,
));
newProps.ref = (n) => this.collectNode(c.key, n, restingStyle);
this.children[c.key] = React.cloneElement(c, newProps);
}
@ -105,11 +103,7 @@ export default class NodeAnimator extends React.Component<IProps> {
}
private collectNode(k: string, node: React.ReactInstance, restingStyle: React.CSSProperties): void {
if (
node &&
this.nodes[k] === undefined &&
this.props.startStyles.length > 0
) {
if (node && this.nodes[k] === undefined && this.props.startStyles.length > 0) {
const startStyles = this.props.startStyles;
const domNode = ReactDom.findDOMNode(node);
// start from startStyle 1: 0 is the one we gave it
@ -127,8 +121,6 @@ export default class NodeAnimator extends React.Component<IProps> {
}
public render(): JSX.Element {
return (
<>{ Object.values(this.children) }</>
);
return <>{Object.values(this.children)}</>;
}
}

View file

@ -23,21 +23,19 @@ import { ClientEvent } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import { M_LOCATION } from "matrix-js-sdk/src/@types/location";
import {
PermissionChanged as PermissionChangedEvent,
} from "@matrix-org/analytics-events/types/typescript/PermissionChanged";
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 { MatrixClientPeg } from "./MatrixClientPeg";
import { PosthogAnalytics } from "./PosthogAnalytics";
import SdkConfig from './SdkConfig';
import PlatformPeg from './PlatformPeg';
import * as TextForEvent from './TextForEvent';
import * as Avatar from './Avatar';
import dis from './dispatcher/dispatcher';
import { _t } from './languageHandler';
import Modal from './Modal';
import SdkConfig from "./SdkConfig";
import PlatformPeg from "./PlatformPeg";
import * as TextForEvent from "./TextForEvent";
import * as Avatar from "./Avatar";
import dis from "./dispatcher/dispatcher";
import { _t } from "./languageHandler";
import Modal from "./Modal";
import SettingsStore from "./settings/SettingsStore";
import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast";
import { SettingLevel } from "./settings/SettingLevel";
@ -89,14 +87,14 @@ export const Notifier = {
// or not
pendingEncryptedEventIds: [],
notificationMessageForEvent: function(ev: MatrixEvent): string {
notificationMessageForEvent: function (ev: MatrixEvent): string {
if (msgTypeHandlers.hasOwnProperty(ev.getContent().msgtype)) {
return msgTypeHandlers[ev.getContent().msgtype](ev);
}
return TextForEvent.textForEvent(ev);
},
_displayPopupNotification: function(ev: MatrixEvent, room: Room): void {
_displayPopupNotification: function (ev: MatrixEvent, room: Room): void {
const plaf = PlatformPeg.get();
const cli = MatrixClientPeg.get();
if (!plaf) {
@ -121,7 +119,7 @@ export const Notifier = {
if (ev.getContent().body && !msgTypeHandlers.hasOwnProperty(ev.getContent().msgtype)) {
msg = ev.getContent().body;
}
} else if (ev.getType() === 'm.room.member') {
} else if (ev.getType() === "m.room.member") {
// context is all in the message here, we don't need
// to display sender info
title = room.name;
@ -135,12 +133,12 @@ export const Notifier = {
}
if (!this.isBodyEnabled()) {
msg = '';
msg = "";
}
let avatarUrl = null;
if (ev.sender && !SettingsStore.getValue("lowBandwidth")) {
avatarUrl = Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop');
avatarUrl = Avatar.avatarUrlForMember(ev.sender, 40, 40, "crop");
}
const notif = plaf.displayNotification(title, msg, avatarUrl, room, ev);
@ -153,7 +151,7 @@ export const Notifier = {
}
},
getSoundForRoom: function(roomId: string) {
getSoundForRoom: function (roomId: string) {
// We do no caching here because the SDK caches setting
// and the browser will cache the sound.
const content = SettingsStore.getValue("notificationSound", roomId);
@ -181,18 +179,19 @@ export const Notifier = {
};
},
_playAudioNotification: async function(ev: MatrixEvent, room: Room): Promise<void> {
_playAudioNotification: async function (ev: MatrixEvent, room: Room): Promise<void> {
const cli = MatrixClientPeg.get();
if (localNotificationsAreSilenced(cli)) {
return;
}
const sound = this.getSoundForRoom(room.roomId);
logger.log(`Got sound ${sound && sound.name || "default"} for ${room.roomId}`);
logger.log(`Got sound ${(sound && sound.name) || "default"} for ${room.roomId}`);
try {
const selector =
document.querySelector<HTMLAudioElement>(sound ? `audio[src='${sound.url}']` : "#messageAudio");
const selector = document.querySelector<HTMLAudioElement>(
sound ? `audio[src='${sound.url}']` : "#messageAudio",
);
let audioElement = selector;
if (!selector) {
if (!sound) {
@ -211,7 +210,7 @@ export const Notifier = {
}
},
start: function(this: typeof Notifier) {
start: function (this: typeof Notifier) {
// do not re-bind in the case of repeated call
this.boundOnEvent = this.boundOnEvent || this.onEvent.bind(this);
this.boundOnSyncStateChange = this.boundOnSyncStateChange || this.onSyncStateChange.bind(this);
@ -226,7 +225,7 @@ export const Notifier = {
this.isSyncing = false;
},
stop: function(this: typeof Notifier) {
stop: function (this: typeof Notifier) {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener(RoomEvent.Timeline, this.boundOnEvent);
MatrixClientPeg.get().removeListener(RoomEvent.Receipt, this.boundOnRoomReceipt);
@ -236,12 +235,12 @@ export const Notifier = {
this.isSyncing = false;
},
supportsDesktopNotifications: function() {
supportsDesktopNotifications: function () {
const plaf = PlatformPeg.get();
return plaf && plaf.supportsNotifications();
},
setEnabled: function(enable: boolean, callback?: () => void) {
setEnabled: function (enable: boolean, callback?: () => void) {
const plaf = PlatformPeg.get();
if (!plaf) return;
@ -258,16 +257,22 @@ export const Notifier = {
if (enable) {
// Attempt to get permission from user
plaf.requestNotificationPermission().then((result) => {
if (result !== 'granted') {
if (result !== "granted") {
// The permission request was dismissed or denied
// TODO: Support alternative branding in messaging
const brand = SdkConfig.get().brand;
const description = result === 'denied'
? _t('%(brand)s does not have permission to send you notifications - ' +
'please check your browser settings', { brand })
: _t('%(brand)s was not given permission to send notifications - please try again', { brand });
const description =
result === "denied"
? _t(
"%(brand)s does not have permission to send you notifications - " +
"please check your browser settings",
{ brand },
)
: _t("%(brand)s was not given permission to send notifications - please try again", {
brand,
});
Modal.createDialog(ErrorDialog, {
title: _t('Unable to enable Notifications'),
title: _t("Unable to enable Notifications"),
description,
});
return;
@ -301,11 +306,11 @@ export const Notifier = {
this.setPromptHidden(true);
},
isEnabled: function() {
isEnabled: function () {
return this.isPossible() && SettingsStore.getValue("notificationsEnabled");
},
isPossible: function() {
isPossible: function () {
const plaf = PlatformPeg.get();
if (!plaf) return false;
if (!plaf.supportsNotifications()) return false;
@ -314,16 +319,16 @@ export const Notifier = {
return true; // possible, but not necessarily enabled
},
isBodyEnabled: function() {
isBodyEnabled: function () {
return this.isEnabled() && SettingsStore.getValue("notificationBodyEnabled");
},
isAudioEnabled: function() {
isAudioEnabled: function () {
// We don't route Audio via the HTML Notifications API so it is possible regardless of other things
return SettingsStore.getValue("audioNotificationsEnabled");
},
setPromptHidden: function(this: typeof Notifier, hidden: boolean, persistent = true) {
setPromptHidden: function (this: typeof Notifier, hidden: boolean, persistent = true) {
this.toolbarHidden = hidden;
hideNotificationsToast();
@ -334,17 +339,22 @@ export const Notifier = {
}
},
shouldShowPrompt: function() {
shouldShowPrompt: function () {
const client = MatrixClientPeg.get();
if (!client) {
return false;
}
const isGuest = client.isGuest();
return !isGuest && this.supportsDesktopNotifications() && !isPushNotifyDisabled() &&
!this.isEnabled() && !this._isPromptHidden();
return (
!isGuest &&
this.supportsDesktopNotifications() &&
!isPushNotifyDisabled() &&
!this.isEnabled() &&
!this._isPromptHidden()
);
},
_isPromptHidden: function(this: typeof Notifier) {
_isPromptHidden: function (this: typeof Notifier) {
// Check localStorage for any such meta data
if (global.localStorage) {
return global.localStorage.getItem("notifications_hidden") === "true";
@ -353,7 +363,12 @@ export const Notifier = {
return this.toolbarHidden;
},
onSyncStateChange: function(this: typeof Notifier, state: SyncState, prevState?: SyncState, data?: ISyncStateData) {
onSyncStateChange: function (
this: typeof Notifier,
state: SyncState,
prevState?: SyncState,
data?: ISyncStateData,
) {
if (state === SyncState.Syncing) {
this.isSyncing = true;
} else if (state === SyncState.Stopped || state === SyncState.Error) {
@ -361,15 +376,12 @@ export const Notifier = {
}
// wait for first non-cached sync to complete
if (
![SyncState.Stopped, SyncState.Error].includes(state) &&
!data?.fromCache
) {
if (![SyncState.Stopped, SyncState.Error].includes(state) && !data?.fromCache) {
createLocalNotificationSettingsIfNeeded(MatrixClientPeg.get());
}
},
onEvent: function(
onEvent: function (
this: typeof Notifier,
ev: MatrixEvent,
room: Room | undefined,
@ -397,7 +409,7 @@ export const Notifier = {
this._evaluateEvent(ev);
},
onEventDecrypted: function(ev: MatrixEvent) {
onEventDecrypted: function (ev: MatrixEvent) {
// 'decrypted' means the decryption process has finished: it may have failed,
// in which case it might decrypt soon if the keys arrive
if (ev.isDecryptionFailure()) return;
@ -409,7 +421,7 @@ export const Notifier = {
this._evaluateEvent(ev);
},
onRoomReceipt: function(ev: MatrixEvent, room: Room) {
onRoomReceipt: function (ev: MatrixEvent, room: Room) {
if (room.getUnreadNotificationCount() === 0) {
// ideally we would clear each notification when it was read,
// but we have no way, given a read receipt, to know whether
@ -427,7 +439,7 @@ export const Notifier = {
}
},
_evaluateEvent: function(ev: MatrixEvent) {
_evaluateEvent: function (ev: MatrixEvent) {
let roomId = ev.getRoomId();
if (LegacyCallHandler.instance.getSupportsVirtualRooms()) {
// Attempt to translate a virtual room to a native one
@ -450,17 +462,12 @@ export const Notifier = {
const store = SdkContextClass.instance.roomViewStore;
const isViewingRoom = store.getRoomId() === room.roomId;
const threadId: string | undefined = ev.getId() !== ev.threadRootId
? ev.threadRootId
: undefined;
const threadId: string | undefined = ev.getId() !== ev.threadRootId ? ev.threadRootId : undefined;
const isViewingThread = store.getThreadId() === threadId;
const isViewingEventTimeline = isViewingRoom && (!threadId || isViewingThread);
if (isViewingEventTimeline &&
UserActivity.sharedInstance().userActiveRecently() &&
!Modal.hasDialogs()
) {
if (isViewingEventTimeline && UserActivity.sharedInstance().userActiveRecently() && !Modal.hasDialogs()) {
// don't bother notifying as user was recently active in this room
return;
}
@ -478,11 +485,8 @@ export const Notifier = {
/**
* Some events require special handling such as showing in-app toasts
*/
_performCustomEventHandling: function(ev: MatrixEvent) {
if (
ElementCall.CALL_EVENT_TYPE.names.includes(ev.getType())
&& SettingsStore.getValue("feature_group_calls")
) {
_performCustomEventHandling: function (ev: MatrixEvent) {
if (ElementCall.CALL_EVENT_TYPE.names.includes(ev.getType()) && SettingsStore.getValue("feature_group_calls")) {
ToastStore.sharedInstance().addOrReplaceToast({
key: getIncomingCallToastKey(ev.getStateKey()),
priority: 100,

View file

@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { createClient, IRequestTokenResponse, MatrixClient } from 'matrix-js-sdk/src/matrix';
import { createClient, IRequestTokenResponse, MatrixClient } from "matrix-js-sdk/src/matrix";
import { _t } from './languageHandler';
import { _t } from "./languageHandler";
/**
* Allows a user to reset their password on a homeserver.
@ -73,8 +73,8 @@ export default class PasswordReset {
this.sessionId = result.sid;
return result;
} catch (err: any) {
if (err.errcode === 'M_THREEPID_NOT_FOUND') {
err.message = _t('This email address was not found');
if (err.errcode === "M_THREEPID_NOT_FOUND") {
err.message = _t("This email address was not found");
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
@ -88,17 +88,20 @@ export default class PasswordReset {
*/
public requestResetToken(emailAddress: string): Promise<IRequestTokenResponse> {
this.sendAttempt++;
return this.client.requestPasswordEmailToken(emailAddress, this.clientSecret, this.sendAttempt).then((res) => {
this.sessionId = res.sid;
return res;
}, function(err) {
if (err.errcode === 'M_THREEPID_NOT_FOUND') {
err.message = _t('This email address was not found');
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
throw err;
});
return this.client.requestPasswordEmailToken(emailAddress, this.clientSecret, this.sendAttempt).then(
(res) => {
this.sessionId = res.sid;
return res;
},
function (err) {
if (err.errcode === "M_THREEPID_NOT_FOUND") {
err.message = _t("This email address was not found");
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
throw err;
},
);
}
public async setNewPassword(password: string): Promise<void> {
@ -120,22 +123,27 @@ export default class PasswordReset {
};
try {
await this.client.setPassword({
// Note: Though this sounds like a login type for identity servers only, it
// has a dual purpose of being used for homeservers too.
type: "m.login.email.identity",
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/matrix-org/synapse/issues/5665
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds: creds,
threepidCreds: creds,
}, this.password, this.logoutDevices);
await this.client.setPassword(
{
// Note: Though this sounds like a login type for identity servers only, it
// has a dual purpose of being used for homeservers too.
type: "m.login.email.identity",
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/matrix-org/synapse/issues/5665
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds: creds,
threepidCreds: creds,
},
this.password,
this.logoutDevices,
);
} catch (err: any) {
if (err.httpStatus === 401) {
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
err.message = _t("Failed to verify email address: make sure you clicked the link in the email");
} else if (err.httpStatus === 404) {
err.message =
_t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.');
err.message = _t(
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.",
);
} else if (err.httpStatus) {
err.message += ` (Status ${err.httpStatus})`;
}
@ -143,4 +151,3 @@ export default class PasswordReset {
}
}
}

View file

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import posthog, { PostHog, Properties } from 'posthog-js';
import posthog, { PostHog, Properties } from "posthog-js";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import { UserProperties } from "@matrix-org/analytics-events/types/typescript/UserProperties";
import { Signup } from '@matrix-org/analytics-events/types/typescript/Signup';
import { Signup } from "@matrix-org/analytics-events/types/typescript/Signup";
import PlatformPeg from './PlatformPeg';
import SdkConfig from './SdkConfig';
import PlatformPeg from "./PlatformPeg";
import SdkConfig from "./SdkConfig";
import { MatrixClientPeg } from "./MatrixClientPeg";
import SettingsStore from "./settings/SettingsStore";
import { ScreenName } from "./PosthogTrackers";
@ -52,8 +52,8 @@ export interface IPosthogEvent {
eventName: string;
// do not allow these to be sent manually, we enqueue them all for caching purposes
"$set"?: void;
"$set_once"?: void;
$set?: void;
$set_once?: void;
}
export interface IPostHogEventOptions {
@ -63,22 +63,32 @@ export interface IPostHogEventOptions {
export enum Anonymity {
Disabled,
Anonymous,
Pseudonymous
Pseudonymous,
}
const whitelistedScreens = new Set([
"register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
"start_sso", "start_cas", "complete_security", "post_registration", "room", "user",
"register",
"login",
"forgot_password",
"soft_logout",
"new",
"settings",
"welcome",
"home",
"start",
"directory",
"start_sso",
"start_cas",
"complete_security",
"post_registration",
"room",
"user",
]);
export function getRedactedCurrentLocation(
origin: string,
hash: string,
pathname: string,
): string {
export function getRedactedCurrentLocation(origin: string, hash: string, pathname: string): string {
// Redact PII from the current location.
// For known screens, assumes a URL structure of /<screen name>/might/be/pii
if (origin.startsWith('file://')) {
if (origin.startsWith("file://")) {
pathname = "/<redacted_file_scheme_url>/";
}
@ -210,13 +220,13 @@ export class PosthogAnalytics {
if (this.anonymity == Anonymity.Anonymous) {
// drop referrer information for anonymous users
properties['$referrer'] = null;
properties['$referring_domain'] = null;
properties['$initial_referrer'] = null;
properties['$initial_referring_domain'] = null;
properties["$referrer"] = null;
properties["$referring_domain"] = null;
properties["$initial_referrer"] = null;
properties["$initial_referring_domain"] = null;
// drop device ID, which is a UUID persisted in local storage
properties['$device_id'] = null;
properties["$device_id"] = null;
}
return properties;
@ -280,7 +290,7 @@ export class PosthogAnalytics {
}
private static getRandomAnalyticsId(): string {
return [...crypto.getRandomValues(new Uint8Array(16))].map((c) => c.toString(16)).join('');
return [...crypto.getRandomValues(new Uint8Array(16))].map((c) => c.toString(16)).join("");
}
public async identifyUser(client: MatrixClient, analyticsIdGenerator: () => string): Promise<void> {
@ -297,8 +307,10 @@ export class PosthogAnalytics {
// until the next time account data is refreshed and this function is called (most likely on next
// page load). This will happen pretty infrequently, so we can tolerate the possibility.
analyticsID = analyticsIdGenerator();
await client.setAccountData(PosthogAnalytics.ANALYTICS_EVENT_TYPE,
Object.assign({ id: analyticsID }, accountData));
await client.setAccountData(
PosthogAnalytics.ANALYTICS_EVENT_TYPE,
Object.assign({ id: analyticsID }, accountData),
);
}
this.posthog.identify(analyticsID);
} catch (e) {
@ -320,10 +332,7 @@ export class PosthogAnalytics {
this.setAnonymity(Anonymity.Disabled);
}
public trackEvent<E extends IPosthogEvent>(
{ eventName, ...properties }: E,
options?: IPostHogEventOptions,
): void {
public trackEvent<E extends IPosthogEvent>({ eventName, ...properties }: E, options?: IPostHogEventOptions): void {
if (this.anonymity == Anonymity.Disabled || this.anonymity == Anonymity.Anonymous) return;
this.capture(eventName, properties, options);
}
@ -383,10 +392,13 @@ export class PosthogAnalytics {
// * When the user changes their preferences on this device
// Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings
// won't be called (i.e. this.anonymity will be left as the default, until the setting changes)
SettingsStore.watchSetting("pseudonymousAnalyticsOptIn", null,
SettingsStore.watchSetting(
"pseudonymousAnalyticsOptIn",
null,
(originalSettingName, changedInRoomId, atLevel, newValueAtLevel, newValue) => {
this.updateAnonymityFromSettings(!!newValue);
});
},
);
}
public setAuthenticationType(authenticationType: Signup["authenticationType"]): void {
@ -404,9 +416,12 @@ export class PosthogAnalytics {
options.timestamp = new Date(registrationTime);
}
return this.trackEvent<Signup>({
eventName: "Signup",
authenticationType: this.authenticationType,
}, options);
return this.trackEvent<Signup>(
{
eventName: "Signup",
authenticationType: this.authenticationType,
},
options,
);
}
}

View file

@ -65,9 +65,8 @@ export default class PosthogTrackers {
}
private trackPage(durationMs?: number): void {
const screenName = this.view === Views.LOGGED_IN
? loggedInPageTypeMap[this.pageType]
: notLoggedInMap[this.view];
const screenName =
this.view === Views.LOGGED_IN ? loggedInPageTypeMap[this.pageType] : notLoggedInMap[this.view];
PosthogAnalytics.instance.trackEvent<ScreenEvent>({
eventName: "$pageview",
$current_url: screenName,

View file

@ -20,7 +20,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "./MatrixClientPeg";
import dis from "./dispatcher/dispatcher";
import Timer from './utils/Timer';
import Timer from "./utils/Timer";
import { ActionPayload } from "./dispatcher/payloads";
// Time in ms after that a user is considered as unavailable/away
@ -49,7 +49,9 @@ class Presence {
try {
await this.unavailableTimer.finished();
this.setState(State.Unavailable);
} catch (e) { /* aborted, stop got called */ }
} catch (e) {
/* aborted, stop got called */
}
}
}
@ -76,7 +78,7 @@ class Presence {
}
private onAction = (payload: ActionPayload) => {
if (payload.action === 'user_activity') {
if (payload.action === "user_activity") {
this.setState(State.Online);
this.unavailableTimer.restart();
}

View file

@ -22,9 +22,9 @@ limitations under the License.
import React from "react";
import dis from './dispatcher/dispatcher';
import Modal from './Modal';
import { _t } from './languageHandler';
import dis from "./dispatcher/dispatcher";
import Modal from "./Modal";
import { _t } from "./languageHandler";
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
import { Action } from "./dispatcher/actions";
@ -46,7 +46,7 @@ export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/;
*/
export async function startAnyRegistrationFlow(
// eslint-disable-next-line camelcase
options: { go_home_on_cancel?: boolean, go_welcome_on_cancel?: boolean, screen_after?: boolean},
options: { go_home_on_cancel?: boolean; go_welcome_on_cancel?: boolean; screen_after?: boolean },
): Promise<void> {
if (options === undefined) options = {};
const modal = Modal.createDialog(QuestionDialog, {
@ -60,19 +60,19 @@ export async function startAnyRegistrationFlow(
key="start_login"
onClick={() => {
modal.close();
dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after });
dis.dispatch({ action: "start_login", screenAfterLogin: options.screen_after });
}}
>
{ _t('Sign In') }
{_t("Sign In")}
</button>,
],
onFinished: (proceed) => {
if (proceed) {
dis.dispatch({ action: 'start_registration', screenAfterLogin: options.screen_after });
dis.dispatch({ action: "start_registration", screenAfterLogin: options.screen_after });
} else if (options.go_home_on_cancel) {
dis.dispatch({ action: Action.ViewHomePage });
} else if (options.go_welcome_on_cancel) {
dis.dispatch({ action: 'view_welcome_page' });
dis.dispatch({ action: "view_welcome_page" });
}
},
});

View file

@ -14,42 +14,54 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
import { Room } from 'matrix-js-sdk/src/models/room';
import { MatrixEvent, EventStatus } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from './MatrixClientPeg';
import dis from './dispatcher/dispatcher';
import { MatrixClientPeg } from "./MatrixClientPeg";
import dis from "./dispatcher/dispatcher";
export default class Resend {
public static resendUnsentEvents(room: Room): Promise<void[]> {
return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) {
return ev.status === EventStatus.NOT_SENT;
}).map(function(event: MatrixEvent) {
return Resend.resend(event);
}));
return Promise.all(
room
.getPendingEvents()
.filter(function (ev: MatrixEvent) {
return ev.status === EventStatus.NOT_SENT;
})
.map(function (event: MatrixEvent) {
return Resend.resend(event);
}),
);
}
public static cancelUnsentEvents(room: Room): void {
room.getPendingEvents().filter(function(ev: MatrixEvent) {
return ev.status === EventStatus.NOT_SENT;
}).forEach(function(event: MatrixEvent) {
Resend.removeFromQueue(event);
});
room.getPendingEvents()
.filter(function (ev: MatrixEvent) {
return ev.status === EventStatus.NOT_SENT;
})
.forEach(function (event: MatrixEvent) {
Resend.removeFromQueue(event);
});
}
public static resend(event: MatrixEvent): Promise<void> {
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
return MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
dis.dispatch({
action: 'message_sent',
event: event,
});
}, function(err: Error) {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/element-web/issues/3148
logger.log('Resend got send failure: ' + err.name + '(' + err + ')');
});
return MatrixClientPeg.get()
.resendEvent(event, room)
.then(
function (res) {
dis.dispatch({
action: "message_sent",
event: event,
});
},
function (err: Error) {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/element-web/issues/3148
logger.log("Resend got send failure: " + err.name + "(" + err + ")");
},
);
}
public static removeFromQueue(event: MatrixEvent): void {

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { _t } from './languageHandler';
import { _t } from "./languageHandler";
export function levelRoleMap(usersDefault: number): Record<number | "undefined", string> {
return {
undefined: _t('Default'),
0: _t('Restricted'),
[usersDefault]: _t('Default'),
50: _t('Moderator'),
100: _t('Admin'),
undefined: _t("Default"),
0: _t("Restricted"),
[usersDefault]: _t("Default"),
50: _t("Moderator"),
100: _t("Admin"),
};
}

View file

@ -21,10 +21,10 @@ import { User } from "matrix-js-sdk/src/models/user";
import { logger } from "matrix-js-sdk/src/logger";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClientPeg } from './MatrixClientPeg';
import MultiInviter, { CompletionStates } from './utils/MultiInviter';
import Modal from './Modal';
import { _t } from './languageHandler';
import { MatrixClientPeg } from "./MatrixClientPeg";
import MultiInviter, { CompletionStates } from "./utils/MultiInviter";
import Modal from "./Modal";
import { _t } from "./languageHandler";
import InviteDialog from "./components/views/dialogs/InviteDialog";
import BaseAvatar from "./components/views/avatars/BaseAvatar";
import { mediaFromMxc } from "./customisations/Media";
@ -55,27 +55,34 @@ export function inviteMultipleToRoom(
progressCallback?: () => void,
): Promise<IInviteResult> {
const inviter = new MultiInviter(roomId, progressCallback);
return inviter.invite(addresses, undefined, sendSharedHistoryKeys)
.then(states => Promise.resolve({ states, inviter }));
return inviter
.invite(addresses, undefined, sendSharedHistoryKeys)
.then((states) => Promise.resolve({ states, inviter }));
}
export function showStartChatInviteDialog(initialText = ""): void {
// This dialog handles the room creation internally - we don't need to worry about it.
Modal.createDialog(
InviteDialog, { kind: KIND_DM, initialText },
/*className=*/"mx_InviteDialog_flexWrapper", /*isPriority=*/false, /*isStatic=*/true,
InviteDialog,
{ kind: KIND_DM, initialText },
/*className=*/ "mx_InviteDialog_flexWrapper",
/*isPriority=*/ false,
/*isStatic=*/ true,
);
}
export function showRoomInviteDialog(roomId: string, initialText = ""): void {
// This dialog handles the room creation internally - we don't need to worry about it.
Modal.createDialog(
InviteDialog, {
InviteDialog,
{
kind: KIND_INVITE,
initialText,
roomId,
},
/*className=*/"mx_InviteDialog_flexWrapper", /*isPriority=*/false, /*isStatic=*/true,
/*className=*/ "mx_InviteDialog_flexWrapper",
/*isPriority=*/ false,
/*isStatic=*/ true,
);
}
@ -88,8 +95,8 @@ export function isValid3pidInvite(event: MatrixEvent): boolean {
if (!event || event.getType() !== EventType.RoomThirdPartyInvite) return false;
// any events without these keys are not valid 3pid invites, so we ignore them
const requiredKeys = ['key_validity_url', 'public_key', 'display_name'];
if (requiredKeys.some(key => !event.getContent()[key])) {
const requiredKeys = ["key_validity_url", "public_key", "display_name"];
if (requiredKeys.some((key) => !event.getContent()[key])) {
return false;
}
@ -103,16 +110,18 @@ export function inviteUsersToRoom(
sendSharedHistoryKeys = false,
progressCallback?: () => void,
): Promise<void> {
return inviteMultipleToRoom(roomId, userIds, sendSharedHistoryKeys, progressCallback).then((result) => {
const room = MatrixClientPeg.get().getRoom(roomId);
showAnyInviteErrors(result.states, room, result.inviter);
}).catch((err) => {
logger.error(err.stack);
Modal.createDialog(ErrorDialog, {
title: _t("Failed to invite"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
return inviteMultipleToRoom(roomId, userIds, sendSharedHistoryKeys, progressCallback)
.then((result) => {
const room = MatrixClientPeg.get().getRoom(roomId);
showAnyInviteErrors(result.states, room, result.inviter);
})
.catch((err) => {
logger.error(err.stack);
Modal.createDialog(ErrorDialog, {
title: _t("Failed to invite"),
description: err && err.message ? err.message : _t("Operation failed"),
});
});
});
}
export function showAnyInviteErrors(
@ -122,7 +131,7 @@ export function showAnyInviteErrors(
userMap?: Map<string, Member>,
): boolean {
// Show user any errors
const failedUsers = Object.keys(states).filter(a => states[a] === 'error');
const failedUsers = Object.keys(states).filter((a) => states[a] === "error");
if (failedUsers.length === 1 && inviter.fatal) {
// Just get the first message because there was a fatal problem on the first
// user. This usually means that no other users were attempted, making it
@ -144,36 +153,46 @@ export function showAnyInviteErrors(
const cli = MatrixClientPeg.get();
if (errorList.length > 0) {
// React 16 doesn't let us use `errorList.join(<br />)` anymore, so this is our solution
const description = <div className="mx_InviteDialog_multiInviterError">
<h4>{ _t("We sent the others, but the below people couldn't be invited to <RoomName/>", {}, {
RoomName: () => <b>{ room.name }</b>,
}) }</h4>
<div>
{ failedUsers.map(addr => {
const user = userMap?.get(addr) || cli.getUser(addr);
const name = (user as Member).name || (user as User).rawDisplayName;
const avatarUrl = (user as Member).getMxcAvatarUrl?.() || (user as User).avatarUrl;
return <div key={addr} className="mx_InviteDialog_tile mx_InviteDialog_tile--inviterError">
<div className="mx_InviteDialog_tile_avatarStack">
<BaseAvatar
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null}
name={name}
idName={user.userId}
width={36}
height={36}
/>
</div>
<div className="mx_InviteDialog_tile_nameStack">
<span className="mx_InviteDialog_tile_nameStack_name">{ name }</span>
<span className="mx_InviteDialog_tile_nameStack_userId">{ user.userId }</span>
</div>
<div className="mx_InviteDialog_tile--inviterError_errorText">
{ inviter.getErrorText(addr) }
</div>
</div>;
}) }
const description = (
<div className="mx_InviteDialog_multiInviterError">
<h4>
{_t(
"We sent the others, but the below people couldn't be invited to <RoomName/>",
{},
{
RoomName: () => <b>{room.name}</b>,
},
)}
</h4>
<div>
{failedUsers.map((addr) => {
const user = userMap?.get(addr) || cli.getUser(addr);
const name = (user as Member).name || (user as User).rawDisplayName;
const avatarUrl = (user as Member).getMxcAvatarUrl?.() || (user as User).avatarUrl;
return (
<div key={addr} className="mx_InviteDialog_tile mx_InviteDialog_tile--inviterError">
<div className="mx_InviteDialog_tile_avatarStack">
<BaseAvatar
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null}
name={name}
idName={user.userId}
width={36}
height={36}
/>
</div>
<div className="mx_InviteDialog_tile_nameStack">
<span className="mx_InviteDialog_tile_nameStack_name">{name}</span>
<span className="mx_InviteDialog_tile_nameStack_userId">{user.userId}</span>
</div>
<div className="mx_InviteDialog_tile--inviterError_errorText">
{inviter.getErrorText(addr)}
</div>
</div>
);
})}
</div>
</div>
</div>;
);
Modal.createDialog(ErrorDialog, {
title: _t("Some invites couldn't be sent"),

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import {
ConditionKind,
@ -24,16 +24,16 @@ import {
PushRuleKind,
TweakName,
} from "matrix-js-sdk/src/@types/PushRules";
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from './MatrixClientPeg';
import { MatrixClientPeg } from "./MatrixClientPeg";
export enum RoomNotifState {
AllMessagesLoud = 'all_messages_loud',
AllMessages = 'all_messages',
MentionsOnly = 'mentions_only',
Mute = 'mute',
AllMessagesLoud = "all_messages_loud",
AllMessages = "all_messages",
MentionsOnly = "mentions_only",
Mute = "mute",
}
export function getRoomNotifsState(client: MatrixClient, roomId: string): RoomNotifState {
@ -49,7 +49,7 @@ export function getRoomNotifsState(client: MatrixClient, roomId: string): RoomNo
// for everything else, look at the room rule.
let roomRule = null;
try {
roomRule = client.getRoomPushRule('global', roomId);
roomRule = client.getRoomPushRule("global", roomId);
} catch (err) {
// Possible that the client doesn't have pushRules yet. If so, it
// hasn't started either, so indicate that this room is not notifying.
@ -79,14 +79,10 @@ export function setRoomNotifsState(roomId: string, newState: RoomNotifState): Pr
}
}
export function getUnreadNotificationCount(
room: Room,
type: NotificationCountType,
threadId?: string,
): number {
let notificationCount = (!!threadId
export function getUnreadNotificationCount(room: Room, type: NotificationCountType, threadId?: string): number {
let notificationCount = !!threadId
? room.getThreadUnreadNotificationCount(threadId, type)
: room.getUnreadNotificationCount(type));
: room.getUnreadNotificationCount(type);
// Check notification counts in the old room just in case there's some lost
// there. We only go one level down to avoid performance issues, and theory
@ -114,9 +110,9 @@ function setRoomNotifsStateMuted(roomId: string): Promise<any> {
const promises = [];
// delete the room rule
const roomRule = cli.getRoomPushRule('global', roomId);
const roomRule = cli.getRoomPushRule("global", roomId);
if (roomRule) {
promises.push(cli.deletePushRule('global', PushRuleKind.RoomSpecific, roomRule.rule_id));
promises.push(cli.deletePushRule("global", PushRuleKind.RoomSpecific, roomRule.rule_id));
}
// add/replace an override rule to squelch everything in this room
@ -124,18 +120,18 @@ function setRoomNotifsStateMuted(roomId: string): Promise<any> {
// is an override rule, not a room rule: it still pertains to this room
// though, so using the room ID as the rule ID is logical and prevents
// duplicate copies of the rule.
promises.push(cli.addPushRule('global', PushRuleKind.Override, roomId, {
conditions: [
{
kind: ConditionKind.EventMatch,
key: 'room_id',
pattern: roomId,
},
],
actions: [
PushRuleActionName.DontNotify,
],
}));
promises.push(
cli.addPushRule("global", PushRuleKind.Override, roomId, {
conditions: [
{
kind: ConditionKind.EventMatch,
key: "room_id",
pattern: roomId,
},
],
actions: [PushRuleActionName.DontNotify],
}),
);
return Promise.all(promises);
}
@ -146,34 +142,36 @@ function setRoomNotifsStateUnmuted(roomId: string, newState: RoomNotifState): Pr
const overrideMuteRule = findOverrideMuteRule(roomId);
if (overrideMuteRule) {
promises.push(cli.deletePushRule('global', PushRuleKind.Override, overrideMuteRule.rule_id));
promises.push(cli.deletePushRule("global", PushRuleKind.Override, overrideMuteRule.rule_id));
}
if (newState === RoomNotifState.AllMessages) {
const roomRule = cli.getRoomPushRule('global', roomId);
const roomRule = cli.getRoomPushRule("global", roomId);
if (roomRule) {
promises.push(cli.deletePushRule('global', PushRuleKind.RoomSpecific, roomRule.rule_id));
promises.push(cli.deletePushRule("global", PushRuleKind.RoomSpecific, roomRule.rule_id));
}
} else if (newState === RoomNotifState.MentionsOnly) {
promises.push(cli.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
actions: [
PushRuleActionName.DontNotify,
],
}));
promises.push(
cli.addPushRule("global", PushRuleKind.RoomSpecific, roomId, {
actions: [PushRuleActionName.DontNotify],
}),
);
// https://matrix.org/jira/browse/SPEC-400
promises.push(cli.setPushRuleEnabled('global', PushRuleKind.RoomSpecific, roomId, true));
promises.push(cli.setPushRuleEnabled("global", PushRuleKind.RoomSpecific, roomId, true));
} else if (newState === RoomNotifState.AllMessagesLoud) {
promises.push(cli.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
actions: [
PushRuleActionName.Notify,
{
set_tweak: TweakName.Sound,
value: 'default',
},
],
}));
promises.push(
cli.addPushRule("global", PushRuleKind.RoomSpecific, roomId, {
actions: [
PushRuleActionName.Notify,
{
set_tweak: TweakName.Sound,
value: "default",
},
],
}),
);
// https://matrix.org/jira/browse/SPEC-400
promises.push(cli.setPushRuleEnabled('global', PushRuleKind.RoomSpecific, roomId, true));
promises.push(cli.setPushRuleEnabled("global", PushRuleKind.RoomSpecific, roomId, true));
}
return Promise.all(promises);
@ -197,9 +195,9 @@ function isRuleForRoom(roomId: string, rule: IPushRule): boolean {
return false;
}
const cond = rule.conditions[0];
return (cond.kind === ConditionKind.EventMatch && cond.key === 'room_id' && cond.pattern === roomId);
return cond.kind === ConditionKind.EventMatch && cond.key === "room_id" && cond.pattern === roomId;
}
function isMuteRule(rule: IPushRule): boolean {
return (rule.actions.length === 1 && rule.actions[0] === PushRuleActionName.DontNotify);
return rule.actions.length === 1 && rule.actions[0] === PushRuleActionName.DontNotify;
}

View file

@ -17,8 +17,8 @@ limitations under the License.
import { Room } from "matrix-js-sdk/src/models/room";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClientPeg } from './MatrixClientPeg';
import AliasCustomisations from './customisations/Alias';
import { MatrixClientPeg } from "./MatrixClientPeg";
import AliasCustomisations from "./customisations/Alias";
/**
* Given a room object, return the alias we should use for it,
@ -30,9 +30,7 @@ import AliasCustomisations from './customisations/Alias';
* @returns {string} A display alias for the given room
*/
export function getDisplayAliasForRoom(room: Room): string | undefined {
return getDisplayAliasForAliasSet(
room.getCanonicalAlias(), room.getAltAliases(),
);
return getDisplayAliasForAliasSet(room.getCanonicalAlias(), room.getAltAliases());
}
// The various display alias getters should all feed through this one path so
@ -47,9 +45,7 @@ export function getDisplayAliasForAliasSet(canonicalAlias: string, altAliases: s
export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise<void> {
let newTarget;
if (isDirect) {
const guessedUserId = guessDMRoomTargetId(
room, MatrixClientPeg.get().getUserId(),
);
const guessedUserId = guessDMRoomTargetId(room, MatrixClientPeg.get().getUserId());
newTarget = guessedUserId;
} else {
newTarget = null;

View file

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import url from 'url';
import url from "url";
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { IOpenIDToken } from 'matrix-js-sdk/src/matrix';
import { IOpenIDToken } from "matrix-js-sdk/src/matrix";
import SettingsStore from "./settings/SettingsStore";
import { Service, startTermsFlow, TermsInteractionCallback, TermsNotSignedError } from './Terms';
import { Service, startTermsFlow, TermsInteractionCallback, TermsNotSignedError } from "./Terms";
import { MatrixClientPeg } from "./MatrixClientPeg";
import SdkConfig from "./SdkConfig";
import { WidgetType } from "./widgets/WidgetType";
@ -129,58 +129,63 @@ export default class ScalarAuthClient {
}
private checkToken(token: string): Promise<string> {
return this.getAccountName(token).then(userId => {
const me = MatrixClientPeg.get().getUserId();
if (userId !== me) {
throw new Error("Scalar token is owned by someone else: " + me);
}
return token;
}).catch((e) => {
if (e instanceof TermsNotSignedError) {
logger.log("Integration manager requires new terms to be agreed to");
// The terms endpoints are new and so live on standard _matrix prefixes,
// but IM rest urls are currently configured with paths, so remove the
// path from the base URL before passing it to the js-sdk
return this.getAccountName(token)
.then((userId) => {
const me = MatrixClientPeg.get().getUserId();
if (userId !== me) {
throw new Error("Scalar token is owned by someone else: " + me);
}
return token;
})
.catch((e) => {
if (e instanceof TermsNotSignedError) {
logger.log("Integration manager requires new terms to be agreed to");
// The terms endpoints are new and so live on standard _matrix prefixes,
// but IM rest urls are currently configured with paths, so remove the
// path from the base URL before passing it to the js-sdk
// We continue to use the full URL for the calls done by
// matrix-react-sdk, but the standard terms API called
// by the js-sdk lives on the standard _matrix path. This means we
// don't support running IMs on a non-root path, but it's the only
// realistic way of transitioning to _matrix paths since configs in
// the wild contain bits of the API path.
// We continue to use the full URL for the calls done by
// matrix-react-sdk, but the standard terms API called
// by the js-sdk lives on the standard _matrix path. This means we
// don't support running IMs on a non-root path, but it's the only
// realistic way of transitioning to _matrix paths since configs in
// the wild contain bits of the API path.
// Once we've fully transitioned to _matrix URLs, we can give people
// a grace period to update their configs, then use the rest url as
// a regular base url.
const parsedImRestUrl = url.parse(this.apiUrl);
parsedImRestUrl.path = '';
parsedImRestUrl.pathname = '';
return startTermsFlow([new Service(
SERVICE_TYPES.IM,
url.format(parsedImRestUrl),
token,
)], this.termsInteractionCallback).then(() => {
return token;
});
} else {
throw e;
}
});
// Once we've fully transitioned to _matrix URLs, we can give people
// a grace period to update their configs, then use the rest url as
// a regular base url.
const parsedImRestUrl = url.parse(this.apiUrl);
parsedImRestUrl.path = "";
parsedImRestUrl.pathname = "";
return startTermsFlow(
[new Service(SERVICE_TYPES.IM, url.format(parsedImRestUrl), token)],
this.termsInteractionCallback,
).then(() => {
return token;
});
} else {
throw e;
}
});
}
registerForToken(): Promise<string> {
// Get openid bearer token from the HS as the first part of our dance
return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => {
// Now we can send that to scalar and exchange it for a scalar token
return this.exchangeForScalarToken(tokenObject);
}).then((token) => {
// Validate it (this mostly checks to see if the IM needs us to agree to some terms)
return this.checkToken(token);
}).then((token) => {
this.scalarToken = token;
this.writeTokenToStore();
return token;
});
return MatrixClientPeg.get()
.getOpenIdToken()
.then((tokenObject) => {
// Now we can send that to scalar and exchange it for a scalar token
return this.exchangeForScalarToken(tokenObject);
})
.then((token) => {
// Validate it (this mostly checks to see if the IM needs us to agree to some terms)
return this.checkToken(token);
})
.then((token) => {
this.scalarToken = token;
this.writeTokenToStore();
return token;
});
}
public async exchangeForScalarToken(openidTokenObject: IOpenIDToken): Promise<string> {
@ -208,7 +213,7 @@ export default class ScalarAuthClient {
}
public async getScalarPageTitle(url: string): Promise<string> {
const scalarPageLookupUrl = new URL(this.getStarterLink(this.apiUrl + '/widgets/title_lookup'));
const scalarPageLookupUrl = new URL(this.getStarterLink(this.apiUrl + "/widgets/title_lookup"));
scalarPageLookupUrl.searchParams.set("curl", encodeURIComponent(url));
const res = await fetch(scalarPageLookupUrl, {
@ -260,10 +265,10 @@ export default class ScalarAuthClient {
url += "&room_name=" + encodeURIComponent(roomName);
url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme"));
if (id) {
url += '&integ_id=' + encodeURIComponent(id);
url += "&integ_id=" + encodeURIComponent(id);
}
if (screen) {
url += '&screen=' + encodeURIComponent(screen);
url += "&screen=" + encodeURIComponent(screen);
}
return url;
}

View file

@ -266,18 +266,18 @@ Response:
- The openId token object as described in https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3useruseridopenidrequest_token
*/
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from './MatrixClientPeg';
import dis from './dispatcher/dispatcher';
import WidgetUtils from './utils/WidgetUtils';
import { _t } from './languageHandler';
import { MatrixClientPeg } from "./MatrixClientPeg";
import dis from "./dispatcher/dispatcher";
import WidgetUtils from "./utils/WidgetUtils";
import { _t } from "./languageHandler";
import { IntegrationManagers } from "./integrations/IntegrationManagers";
import { WidgetType } from "./widgets/WidgetType";
import { objectClone } from "./utils/objects";
import { EffectiveMembership, getEffectiveMembership } from './utils/membership';
import { SdkContextClass } from './contexts/SDKContext';
import { EffectiveMembership, getEffectiveMembership } from "./utils/membership";
import { SdkContextClass } from "./contexts/SDKContext";
enum Action {
CloseScalar = "close_scalar",
@ -294,7 +294,7 @@ enum Action {
BotOptions = "bot_options",
SetBotOptions = "set_bot_options",
SetBotPower = "set_bot_power",
GetOpenIdToken = "get_open_id_token"
GetOpenIdToken = "get_open_id_token",
}
function sendResponse(event: MessageEvent<any>, res: any): void {
@ -323,7 +323,7 @@ function inviteUser(event: MessageEvent<any>, roomId: string, userId: string): v
logger.log(`Received request to invite ${userId} into room ${roomId}`);
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
sendError(event, _t("You need to be logged in."));
return;
}
const room = client.getRoom(roomId);
@ -338,13 +338,16 @@ function inviteUser(event: MessageEvent<any>, roomId: string, userId: string): v
}
}
client.invite(roomId, userId).then(function() {
sendResponse(event, {
success: true,
});
}, function(err) {
sendError(event, _t('You need to be able to invite users to do that.'), err);
});
client.invite(roomId, userId).then(
function () {
sendResponse(event, {
success: true,
});
},
function (err) {
sendError(event, _t("You need to be able to invite users to do that."), err);
},
);
}
function kickUser(event: MessageEvent<any>, roomId: string, userId: string): void {
@ -367,13 +370,16 @@ function kickUser(event: MessageEvent<any>, roomId: string, userId: string): voi
}
const reason = event.data.reason;
client.kick(roomId, userId, reason).then(() => {
sendResponse(event, {
success: true,
client
.kick(roomId, userId, reason)
.then(() => {
sendResponse(event, {
success: true,
});
})
.catch((err) => {
sendError(event, _t("You need to be able to kick users to do that."), err);
});
}).catch((err) => {
sendError(event, _t("You need to be able to kick users to do that."), err);
});
}
function setWidget(event: MessageEvent<any>, roomId: string | null): void {
@ -391,9 +397,10 @@ function setWidget(event: MessageEvent<any>, roomId: string | null): void {
return;
}
if (widgetUrl !== null) { // if url is null it is being deleted, don't need to check name/type/etc
if (widgetUrl !== null) {
// if url is null it is being deleted, don't need to check name/type/etc
// check types of fields
if (widgetName !== undefined && typeof widgetName !== 'string') {
if (widgetName !== undefined && typeof widgetName !== "string") {
sendError(event, _t("Unable to create widget."), new Error("Optional field 'name' must be a string."));
return;
}
@ -401,7 +408,7 @@ function setWidget(event: MessageEvent<any>, roomId: string | null): void {
sendError(event, _t("Unable to create widget."), new Error("Optional field 'data' must be an Object."));
return;
}
if (widgetAvatarUrl !== undefined && typeof widgetAvatarUrl !== 'string') {
if (widgetAvatarUrl !== undefined && typeof widgetAvatarUrl !== "string") {
sendError(
event,
_t("Unable to create widget."),
@ -409,11 +416,11 @@ function setWidget(event: MessageEvent<any>, roomId: string | null): void {
);
return;
}
if (typeof widgetType !== 'string') {
if (typeof widgetType !== "string") {
sendError(event, _t("Unable to create widget."), new Error("Field 'type' must be a string."));
return;
}
if (typeof widgetUrl !== 'string') {
if (typeof widgetUrl !== "string") {
sendError(event, _t("Unable to create widget."), new Error("Field 'url' must be a string or null."));
return;
}
@ -423,35 +430,48 @@ function setWidget(event: MessageEvent<any>, roomId: string | null): void {
widgetType = WidgetType.fromString(widgetType);
if (userWidget) {
WidgetUtils.setUserWidget(widgetId, widgetType, widgetUrl, widgetName, widgetData).then(() => {
sendResponse(event, {
success: true,
});
dis.dispatch({ action: "user_widget_updated" });
}).catch((e) => {
sendError(event, _t('Unable to create widget.'), e);
});
} else { // Room widget
if (!roomId) {
sendError(event, _t('Missing roomId.'), null);
return;
}
WidgetUtils.setRoomWidget(roomId, widgetId, widgetType, widgetUrl, widgetName, widgetData, widgetAvatarUrl)
WidgetUtils.setUserWidget(widgetId, widgetType, widgetUrl, widgetName, widgetData)
.then(() => {
sendResponse(event, {
success: true,
});
}, (err) => {
sendError(event, _t('Failed to send request.'), err);
dis.dispatch({ action: "user_widget_updated" });
})
.catch((e) => {
sendError(event, _t("Unable to create widget."), e);
});
} else {
// Room widget
if (!roomId) {
sendError(event, _t("Missing roomId."), null);
return;
}
WidgetUtils.setRoomWidget(
roomId,
widgetId,
widgetType,
widgetUrl,
widgetName,
widgetData,
widgetAvatarUrl,
).then(
() => {
sendResponse(event, {
success: true,
});
},
(err) => {
sendError(event, _t("Failed to send request."), err);
},
);
}
}
function getWidgets(event: MessageEvent<any>, roomId: string): void {
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
sendError(event, _t("You need to be logged in."));
return;
}
let widgetStateEvents = [];
@ -459,7 +479,7 @@ function getWidgets(event: MessageEvent<any>, roomId: string): void {
if (roomId) {
const room = client.getRoom(roomId);
if (!room) {
sendError(event, _t('This room is not recognised.'));
sendError(event, _t("This room is not recognised."));
return;
}
// XXX: This gets the raw event object (I think because we can't
@ -477,12 +497,12 @@ function getWidgets(event: MessageEvent<any>, roomId: string): void {
function getRoomEncState(event: MessageEvent<any>, roomId: string): void {
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
sendError(event, _t("You need to be logged in."));
return;
}
const room = client.getRoom(roomId);
if (!room) {
sendError(event, _t('This room is not recognised.'));
sendError(event, _t("This room is not recognised."));
return;
}
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
@ -491,52 +511,62 @@ function getRoomEncState(event: MessageEvent<any>, roomId: string): void {
}
function setPlumbingState(event: MessageEvent<any>, roomId: string, status: string): void {
if (typeof status !== 'string') {
throw new Error('Plumbing state status should be a string');
if (typeof status !== "string") {
throw new Error("Plumbing state status should be a string");
}
logger.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`);
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
sendError(event, _t("You need to be logged in."));
return;
}
client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).then(() => {
sendResponse(event, {
success: true,
});
}, (err) => {
sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
});
client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).then(
() => {
sendResponse(event, {
success: true,
});
},
(err) => {
sendError(event, err.message ? err.message : _t("Failed to send request."), err);
},
);
}
function setBotOptions(event: MessageEvent<any>, roomId: string, userId: string): void {
logger.log(`Received request to set options for bot ${userId} in room ${roomId}`);
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
sendError(event, _t("You need to be logged in."));
return;
}
client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).then(() => {
sendResponse(event, {
success: true,
});
}, (err) => {
sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
});
client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).then(
() => {
sendResponse(event, {
success: true,
});
},
(err) => {
sendError(event, err.message ? err.message : _t("Failed to send request."), err);
},
);
}
async function setBotPower(
event: MessageEvent<any>, roomId: string, userId: string, level: number, ignoreIfGreater?: boolean,
event: MessageEvent<any>,
roomId: string,
userId: string,
level: number,
ignoreIfGreater?: boolean,
): Promise<void> {
if (!(Number.isInteger(level) && level >= 0)) {
sendError(event, _t('Power level must be positive integer.'));
sendError(event, _t("Power level must be positive integer."));
return;
}
logger.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`);
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
sendError(event, _t("You need to be logged in."));
return;
}
@ -553,17 +583,20 @@ async function setBotPower(
});
}
}
await client.setPowerLevel(roomId, userId, level, new MatrixEvent(
{
await client.setPowerLevel(
roomId,
userId,
level,
new MatrixEvent({
type: "m.room.power_levels",
content: powerLevels,
},
));
}),
);
return sendResponse(event, {
success: true,
});
} catch (err) {
sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
sendError(event, err.message ? err.message : _t("Failed to send request."), err);
}
}
@ -585,12 +618,12 @@ function botOptions(event: MessageEvent<any>, roomId: string, userId: string): v
function getMembershipCount(event: MessageEvent<any>, roomId: string): void {
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
sendError(event, _t("You need to be logged in."));
return;
}
const room = client.getRoom(roomId);
if (!room) {
sendError(event, _t('This room is not recognised.'));
sendError(event, _t("This room is not recognised."));
return;
}
const count = room.getJoinedMemberCount();
@ -602,16 +635,16 @@ function canSendEvent(event: MessageEvent<any>, roomId: string): void {
const isState = Boolean(event.data.is_state);
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
sendError(event, _t("You need to be logged in."));
return;
}
const room = client.getRoom(roomId);
if (!room) {
sendError(event, _t('This room is not recognised.'));
sendError(event, _t("This room is not recognised."));
return;
}
if (room.getMyMembership() !== "join") {
sendError(event, _t('You are not in this room.'));
sendError(event, _t("You are not in this room."));
return;
}
const me = client.credentials.userId;
@ -624,7 +657,7 @@ function canSendEvent(event: MessageEvent<any>, roomId: string): void {
}
if (!canSend) {
sendError(event, _t('You do not have permission to do that in this room.'));
sendError(event, _t("You do not have permission to do that in this room."));
return;
}
@ -634,12 +667,12 @@ function canSendEvent(event: MessageEvent<any>, roomId: string): void {
function returnStateEvent(event: MessageEvent<any>, roomId: string, eventType: string, stateKey: string): void {
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
sendError(event, _t("You need to be logged in."));
return;
}
const room = client.getRoom(roomId);
if (!room) {
sendError(event, _t('This room is not recognised.'));
sendError(event, _t("This room is not recognised."));
return;
}
const stateEvent = room.currentState.getStateEvents(eventType, stateKey);
@ -656,12 +689,13 @@ async function getOpenIdToken(event: MessageEvent<any>) {
sendResponse(event, tokenObject);
} catch (ex) {
logger.warn("Unable to fetch openId token.", ex);
sendError(event, 'Unable to fetch openId token.');
sendError(event, "Unable to fetch openId token.");
}
}
const onMessage = function(event: MessageEvent<any>): void {
if (!event.origin) { // stupid chrome
const onMessage = function (event: MessageEvent<any>): void {
if (!event.origin) {
// stupid chrome
// @ts-ignore
event.origin = event.originalEvent.origin;
}
@ -717,13 +751,13 @@ const onMessage = function(event: MessageEvent<any>): void {
getOpenIdToken(event);
return;
} else {
sendError(event, _t('Missing room_id in request'));
sendError(event, _t("Missing room_id in request"));
return;
}
}
if (roomId !== SdkContextClass.instance.roomViewStore.getRoomId()) {
sendError(event, _t('Room %(roomId)s not visible', { roomId: roomId }));
sendError(event, _t("Room %(roomId)s not visible", { roomId: roomId }));
return;
}
@ -755,7 +789,7 @@ const onMessage = function(event: MessageEvent<any>): void {
}
if (!userId) {
sendError(event, _t('Missing user_id in request'));
sendError(event, _t("Missing user_id in request"));
return;
}
switch (event.data.action) {
@ -778,7 +812,7 @@ const onMessage = function(event: MessageEvent<any>): void {
setBotPower(event, roomId, userId, event.data.level, event.data.ignoreIfGreater);
break;
default:
logger.warn("Unhandled postMessage event with action '" + event.data.action +"'");
logger.warn("Unhandled postMessage event with action '" + event.data.action + "'");
break;
}
};
@ -800,10 +834,7 @@ export function stopListening(): void {
}
if (listenerCount < 0) {
// Make an error so we get a stack trace
const e = new Error(
"ScalarMessaging: mismatched startListening / stopListening detected." +
" Negative count",
);
const e = new Error("ScalarMessaging: mismatched startListening / stopListening detected." + " Negative count");
logger.error(e);
}
}

View file

@ -67,7 +67,8 @@ export default class SdkConfig {
public static get(): IConfigOptions;
public static get<K extends keyof IConfigOptions>(key: K, altCaseName?: string): IConfigOptions[K];
public static get<K extends keyof IConfigOptions = never>(
key?: K, altCaseName?: string,
key?: K,
altCaseName?: string,
): IConfigOptions | IConfigOptions[K] {
if (key === undefined) {
// safe to cast as a fallback - we want to break the runtime contract in this case
@ -77,7 +78,8 @@ export default class SdkConfig {
}
public static getObject<K extends KeysWithObjectShape<IConfigOptions>>(
key: K, altCaseName?: string,
key: K,
altCaseName?: string,
): Optional<SnakedObject<IConfigOptions[K]>> {
const val = SdkConfig.get(key, altCaseName);
if (val !== null && val !== undefined) {

View file

@ -36,7 +36,7 @@ async function serverSideSearch(
term: string,
roomId: string = undefined,
abortSignal?: AbortSignal,
): Promise<{ response: ISearchResponse, query: ISearchRequestBody }> {
): Promise<{ response: ISearchResponse; query: ISearchRequestBody }> {
const client = MatrixClientPeg.get();
const filter: IRoomEventFilter = {
@ -160,7 +160,7 @@ async function localSearch(
searchTerm: string,
roomId: string = undefined,
processResult = true,
): Promise<{ response: IResultRoomEvents, query: ISearchArgs }> {
): Promise<{ response: IResultRoomEvents; query: ISearchArgs }> {
const eventIndex = EventIndexPeg.get();
const searchArgs: ISearchArgs = {

View file

@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ICryptoCallbacks } from 'matrix-js-sdk/src/matrix';
import { ISecretStorageKeyInfo } from 'matrix-js-sdk/src/crypto/api';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
import { ICryptoCallbacks } from "matrix-js-sdk/src/matrix";
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { deriveKey } from "matrix-js-sdk/src/crypto/key_passphrase";
import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto/recoverykey";
import { encodeBase64 } from "matrix-js-sdk/src/crypto/olmlib";
import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
import { DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import { logger } from "matrix-js-sdk/src/logger";
import { ComponentType } from "react";
import Modal from './Modal';
import { MatrixClientPeg } from './MatrixClientPeg';
import { _t } from './languageHandler';
import { isSecureBackupRequired } from './utils/WellKnownUtils';
import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog';
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
import Modal from "./Modal";
import { MatrixClientPeg } from "./MatrixClientPeg";
import { _t } from "./languageHandler";
import { isSecureBackupRequired } from "./utils/WellKnownUtils";
import AccessSecretStorageDialog from "./components/views/dialogs/security/AccessSecretStorageDialog";
import RestoreKeyBackupDialog from "./components/views/dialogs/security/RestoreKeyBackupDialog";
import SettingsStore from "./settings/SettingsStore";
import SecurityCustomisations from "./customisations/Security";
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
@ -83,27 +83,23 @@ async function confirmToDismiss(): Promise<boolean> {
return !sure;
}
type KeyParams = { passphrase: string, recoveryKey: string };
type KeyParams = { passphrase: string; recoveryKey: string };
function makeInputToKey(
keyInfo: ISecretStorageKeyInfo,
): (keyParams: KeyParams) => Promise<Uint8Array> {
function makeInputToKey(keyInfo: ISecretStorageKeyInfo): (keyParams: KeyParams) => Promise<Uint8Array> {
return async ({ passphrase, recoveryKey }) => {
if (passphrase) {
return deriveKey(
passphrase,
keyInfo.passphrase.salt,
keyInfo.passphrase.iterations,
);
return deriveKey(passphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations);
} else {
return decodeRecoveryKey(recoveryKey);
}
};
}
async function getSecretStorageKey(
{ keys: keyInfos }: { keys: Record<string, ISecretStorageKeyInfo> },
): Promise<[string, Uint8Array]> {
async function getSecretStorageKey({
keys: keyInfos,
}: {
keys: Record<string, ISecretStorageKeyInfo>;
}): Promise<[string, Uint8Array]> {
const cli = MatrixClientPeg.get();
let keyId = await cli.getDefaultSecretStorageKeyId();
let keyInfo: ISecretStorageKeyInfo;
@ -234,11 +230,7 @@ export async function getDehydrationKey(
return key;
}
function cacheSecretStorageKey(
keyId: string,
keyInfo: ISecretStorageKeyInfo,
key: Uint8Array,
): void {
function cacheSecretStorageKey(keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array): void {
if (isCachingAllowed()) {
secretStorageKeys[keyId] = key;
secretStorageKeyInfo[keyId] = keyInfo;
@ -271,17 +263,13 @@ async function onSecretRequested(
const keyId = name.replace("m.cross_signing.", "");
const key = await callbacks.getCrossSigningKeyCache(keyId);
if (!key) {
logger.log(
`${keyId} requested by ${deviceId}, but not found in cache`,
);
logger.log(`${keyId} requested by ${deviceId}, but not found in cache`);
}
return key && encodeBase64(key);
} else if (name === "m.megolm_backup.v1") {
const key = await client.crypto.getSessionBackupPrivateKey();
if (!key) {
logger.log(
`session backup key requested by ${deviceId}, but not found in cache`,
);
logger.log(`session backup key requested by ${deviceId}, but not found in cache`);
}
return key && encodeBase64(key);
}
@ -298,9 +286,16 @@ export const crossSigningCallbacks: ICryptoCallbacks = {
export async function promptForBackupPassphrase(): Promise<Uint8Array> {
let key: Uint8Array;
const { finished } = Modal.createDialog(RestoreKeyBackupDialog, {
showSummary: false, keyCallback: k => key = k,
}, null, /* priority = */ false, /* static = */ true);
const { finished } = Modal.createDialog(
RestoreKeyBackupDialog,
{
showSummary: false,
keyCallback: (k) => (key = k),
},
null,
/* priority = */ false,
/* static = */ true,
);
const success = await finished;
if (!success) throw new Error("Key backup prompt cancelled");
@ -329,7 +324,7 @@ export async function promptForBackupPassphrase(): Promise<Uint8Array> {
* bootstrapped. Optional.
* @param {bool} [forceReset] Reset secret storage even if it's already set up
*/
export async function accessSecretStorage(func = async () => { }, forceReset = false) {
export async function accessSecretStorage(func = async () => {}, forceReset = false) {
const cli = MatrixClientPeg.get();
secretStorageBeingAccessed = true;
try {
@ -337,9 +332,9 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
// This dialog calls bootstrap itself after guiding the user through
// passphrase creation.
const { finished } = Modal.createDialogAsync(
import(
"./async-components/views/dialogs/security/CreateSecretStorageDialog"
) as unknown as Promise<ComponentType<{}>>,
import("./async-components/views/dialogs/security/CreateSecretStorageDialog") as unknown as Promise<
ComponentType<{}>
>,
{
forceReset,
},
@ -412,9 +407,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
}
// FIXME: this function name is a bit of a mouthful
export async function tryToUnlockSecretStorageWithDehydrationKey(
client: MatrixClient,
): Promise<void> {
export async function tryToUnlockSecretStorageWithDehydrationKey(client: MatrixClient): Promise<void> {
const key = dehydrationCache.key;
let restoringBackup = false;
if (key && (await client.isSecretStorageReady())) {
@ -437,15 +430,14 @@ export async function tryToUnlockSecretStorageWithDehydrationKey(
if (backupInfo) {
restoringBackup = true;
// don't await, because this can take a long time
client.restoreKeyBackupWithSecretStorage(backupInfo)
.finally(() => {
secretStorageBeingAccessed = false;
nonInteractive = false;
if (!isCachingAllowed()) {
secretStorageKeys = {};
secretStorageKeyInfo = {};
}
});
client.restoreKeyBackupWithSecretStorage(backupInfo).finally(() => {
secretStorageBeingAccessed = false;
nonInteractive = false;
if (!isCachingAllowed()) {
secretStorageKeys = {};
secretStorageKeyInfo = {};
}
});
}
} finally {
dehydrationCache = {};

View file

@ -39,7 +39,7 @@ export default class SendHistoryManager {
let index = 0;
let itemJSON;
while (itemJSON = sessionStorage.getItem(`${this.prefix}[${index}]`)) {
while ((itemJSON = sessionStorage.getItem(`${this.prefix}[${index}]`))) {
try {
this.history.push(JSON.parse(itemJSON));
} catch (e) {

File diff suppressed because it is too large Load diff

View file

@ -44,8 +44,8 @@ limitations under the License.
* list ops)
*/
import { MatrixClient } from 'matrix-js-sdk/src/matrix';
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { EventType } from "matrix-js-sdk/src/@types/event";
import {
MSC3575Filter,
MSC3575List,
@ -53,9 +53,9 @@ import {
MSC3575_STATE_KEY_ME,
MSC3575_WILDCARD,
SlidingSync,
} from 'matrix-js-sdk/src/sliding-sync';
} from "matrix-js-sdk/src/sliding-sync";
import { logger } from "matrix-js-sdk/src/logger";
import { IDeferred, defer, sleep } from 'matrix-js-sdk/src/utils';
import { IDeferred, defer, sleep } from "matrix-js-sdk/src/utils";
// how long to long poll for
const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000;
@ -66,7 +66,8 @@ const DEFAULT_ROOM_SUBSCRIPTION_INFO = {
// missing required_state which will change depending on the kind of room
include_old_rooms: {
timeline_limit: 0,
required_state: [ // state needed to handle space navigation and tombstone chains
required_state: [
// state needed to handle space navigation and tombstone chains
[EventType.RoomCreate, ""],
[EventType.RoomTombstone, ""],
[EventType.SpaceChild, MSC3575_WILDCARD],
@ -77,21 +78,27 @@ const DEFAULT_ROOM_SUBSCRIPTION_INFO = {
};
// lazy load room members so rooms like Matrix HQ don't take forever to load
const UNENCRYPTED_SUBSCRIPTION_NAME = "unencrypted";
const UNENCRYPTED_SUBSCRIPTION = Object.assign({
required_state: [
[MSC3575_WILDCARD, MSC3575_WILDCARD], // all events
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // except for m.room.members, get our own membership
[EventType.RoomMember, MSC3575_STATE_KEY_LAZY], // ...and lazy load the rest.
],
}, DEFAULT_ROOM_SUBSCRIPTION_INFO);
const UNENCRYPTED_SUBSCRIPTION = Object.assign(
{
required_state: [
[MSC3575_WILDCARD, MSC3575_WILDCARD], // all events
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // except for m.room.members, get our own membership
[EventType.RoomMember, MSC3575_STATE_KEY_LAZY], // ...and lazy load the rest.
],
},
DEFAULT_ROOM_SUBSCRIPTION_INFO,
);
// we need all the room members in encrypted rooms because we need to know which users to encrypt
// messages for.
const ENCRYPTED_SUBSCRIPTION = Object.assign({
required_state: [
[MSC3575_WILDCARD, MSC3575_WILDCARD], // all events
],
}, DEFAULT_ROOM_SUBSCRIPTION_INFO);
const ENCRYPTED_SUBSCRIPTION = Object.assign(
{
required_state: [
[MSC3575_WILDCARD, MSC3575_WILDCARD], // all events
],
},
DEFAULT_ROOM_SUBSCRIPTION_INFO,
);
export type PartialSlidingSyncRequest = {
filters?: MSC3575Filter;
@ -130,16 +137,12 @@ export class SlidingSyncManager {
this.listIdToIndex = {};
// by default use the encrypted subscription as that gets everything, which is a safer
// default than potentially missing member events.
this.slidingSync = new SlidingSync(
proxyUrl, [], ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS,
);
this.slidingSync = new SlidingSync(proxyUrl, [], ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS);
this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION);
// set the space list
this.slidingSync.setList(this.getOrAllocateListIndex(SlidingSyncManager.ListSpaces), {
ranges: [[0, 20]],
sort: [
"by_name",
],
sort: ["by_name"],
slow_get_all_rooms: true,
timeline_limit: 0,
required_state: [
@ -207,18 +210,14 @@ export class SlidingSyncManager {
* @param updateArgs The fields to update on the list.
* @returns The complete list request params
*/
public async ensureListRegistered(
listIndex: number, updateArgs: PartialSlidingSyncRequest,
): Promise<MSC3575List> {
public async ensureListRegistered(listIndex: number, updateArgs: PartialSlidingSyncRequest): Promise<MSC3575List> {
logger.debug("ensureListRegistered:::", listIndex, updateArgs);
await this.configureDefer.promise;
let list = this.slidingSync.getList(listIndex);
if (!list) {
list = {
ranges: [[0, 20]],
sort: [
"by_notification_level", "by_recency",
],
sort: ["by_notification_level", "by_recency"],
timeline_limit: 1, // most recent message display: though this seems to only be needed for favourites?
required_state: [
[EventType.RoomJoinRules, ""], // the public icon on the room list
@ -310,9 +309,12 @@ export class SlidingSyncManager {
let hasMore = true;
let firstTime = true;
while (hasMore) {
const endIndex = startIndex + batchSize-1;
const endIndex = startIndex + batchSize - 1;
try {
const ranges = [[0, batchSize-1], [startIndex, endIndex]];
const ranges = [
[0, batchSize - 1],
[startIndex, endIndex],
];
if (firstTime) {
await this.slidingSync.setList(listIndex, {
// e.g [0,19] [20,39] then [0,19] [40,59]. We keep [0,20] constantly to ensure
@ -334,7 +336,8 @@ export class SlidingSyncManager {
// on the user's account. This means some data in the search dialog results may be inaccurate
// e.g membership of space, but this will be corrected when the user clicks on the room
// as the direct room subscription does include old room iterations.
filters: { // we get spaces via a different list, so filter them out
filters: {
// we get spaces via a different list, so filter them out
not_room_types: ["m.space"],
},
});
@ -347,7 +350,7 @@ export class SlidingSyncManager {
// do nothing, as we reject only when we get interrupted but that's fine as the next
// request will include our data
}
hasMore = (endIndex+1) < this.slidingSync.getListData(listIndex)?.joinedCount;
hasMore = endIndex + 1 < this.slidingSync.getListData(listIndex)?.joinedCount;
startIndex += batchSize;
firstTime = false;
}

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import classNames from 'classnames';
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
import classNames from "classnames";
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from './MatrixClientPeg';
import Modal from './Modal';
import { MatrixClientPeg } from "./MatrixClientPeg";
import Modal from "./Modal";
import TermsDialog from "./components/views/dialogs/TermsDialog";
export class TermsNotSignedError extends Error {}
@ -34,8 +34,7 @@ export class Service {
* @param {string} baseUrl The Base URL of the service (ie. before '/_matrix')
* @param {string} accessToken The user's access token for the service
*/
constructor(public serviceType: SERVICE_TYPES, public baseUrl: string, public accessToken: string) {
}
constructor(public serviceType: SERVICE_TYPES, public baseUrl: string, public accessToken: string) {}
}
export interface LocalisedPolicy {
@ -77,9 +76,7 @@ export async function startTermsFlow(
services: Service[],
interactionCallback: TermsInteractionCallback = dialogTermsInteractionCallback,
) {
const termsPromises = services.map(
(s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl),
);
const termsPromises = services.map((s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl));
/*
* a /terms response looks like:
@ -101,10 +98,12 @@ export async function startTermsFlow(
*/
const terms: { policies: Policies }[] = await Promise.all(termsPromises);
const policiesAndServicePairs = terms.map((t, i) => { return { 'service': services[i], 'policies': t.policies }; });
const policiesAndServicePairs = terms.map((t, i) => {
return { service: services[i], policies: t.policies };
});
// fetch the set of agreed policy URLs from account data
const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms');
const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData("m.accepted_terms");
let agreedUrlSet: Set<string>;
if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) {
agreedUrlSet = new Set();
@ -124,7 +123,7 @@ export async function startTermsFlow(
for (const [policyName, policy] of Object.entries(policies)) {
let policyAgreed = false;
for (const lang of Object.keys(policy)) {
if (lang === 'version') continue;
if (lang === "version") continue;
if (agreedUrlSet.has(policy[lang].url)) {
policyAgreed = true;
break;
@ -143,7 +142,7 @@ export async function startTermsFlow(
const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]);
logger.log("User has agreed to URLs", newlyAgreedUrls);
// Merge with previously agreed URLs
newlyAgreedUrls.forEach(url => agreedUrlSet.add(url));
newlyAgreedUrls.forEach((url) => agreedUrlSet.add(url));
} else {
logger.log("User has already agreed to all required policies");
}
@ -151,7 +150,7 @@ export async function startTermsFlow(
// We only ever add to the set of URLs, so if anything has changed then we'd see a different length
if (agreedUrlSet.size !== numAcceptedBeforeAgreement) {
const newAcceptedTerms = { accepted: Array.from(agreedUrlSet) };
await MatrixClientPeg.get().setAccountData('m.accepted_terms', newAcceptedTerms);
await MatrixClientPeg.get().setAccountData("m.accepted_terms", newAcceptedTerms);
}
const agreePromises = policiesAndServicePairs.map((policiesAndService) => {
@ -161,7 +160,7 @@ export async function startTermsFlow(
const urlsForService = Array.from(agreedUrlSet).filter((url) => {
for (const policy of Object.values(policiesAndService.policies)) {
for (const lang of Object.keys(policy)) {
if (lang === 'version') continue;
if (lang === "version") continue;
if (policy[lang].url === url) return true;
}
}
@ -190,10 +189,14 @@ export async function dialogTermsInteractionCallback(
): Promise<string[]> {
logger.log("Terms that need agreement", policiesAndServicePairs);
const { finished } = Modal.createDialog<[boolean, string[]]>(TermsDialog, {
policiesAndServicePairs,
agreedUrls,
}, classNames("mx_TermsDialog", extraClassNames));
const { finished } = Modal.createDialog<[boolean, string[]]>(
TermsDialog,
{
policiesAndServicePairs,
agreedUrls,
},
classNames("mx_TermsDialog", extraClassNames),
);
const [done, _agreedUrls] = await finished;
if (!done) {

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { logger } from "matrix-js-sdk/src/logger";
import { removeDirectionOverrideChars } from 'matrix-js-sdk/src/utils';
import { removeDirectionOverrideChars } from "matrix-js-sdk/src/utils";
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
import {
@ -30,21 +30,21 @@ import {
PollStartEvent,
} from "matrix-events-sdk";
import { _t } from './languageHandler';
import * as Roles from './Roles';
import { _t } from "./languageHandler";
import * as Roles from "./Roles";
import { isValid3pidInvite } from "./RoomInvite";
import SettingsStore from "./settings/SettingsStore";
import { ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES } from "./mjolnir/BanList";
import { WIDGET_LAYOUT_EVENT_TYPE } from "./stores/widgets/WidgetLayoutStore";
import { RightPanelPhases } from './stores/right-panel/RightPanelStorePhases';
import { Action } from './dispatcher/actions';
import defaultDispatcher from './dispatcher/dispatcher';
import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases";
import { Action } from "./dispatcher/actions";
import defaultDispatcher from "./dispatcher/dispatcher";
import { MatrixClientPeg } from "./MatrixClientPeg";
import { ROOM_SECURITY_TAB } from "./components/views/dialogs/RoomSettingsDialog";
import AccessibleButton from './components/views/elements/AccessibleButton';
import RightPanelStore from './stores/right-panel/RightPanelStore';
import AccessibleButton from "./components/views/elements/AccessibleButton";
import RightPanelStore from "./stores/right-panel/RightPanelStore";
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
import { isLocationEvent } from './utils/EventUtils';
import { isLocationEvent } from "./utils/EventUtils";
import { ElementCall } from "./models/Call";
export function getSenderName(event: MatrixEvent): string {
@ -74,7 +74,7 @@ function textForCallEvent(event: MatrixEvent): () => string {
function textForCallInviteEvent(event: MatrixEvent): () => string | null {
const senderName = getSenderName(event);
// FIXME: Find a better way to determine this from the event?
const isVoice = !event.getContent().offer?.sdp?.includes('m=video');
const isVoice = !event.getContent().offer?.sdp?.includes("m=video");
const isSupported = MatrixClientPeg.get().supportsVoip();
// This ladder could be reduced down to a couple string variables, however other languages
@ -100,52 +100,60 @@ function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents
const reason = content.reason;
switch (content.membership) {
case 'invite': {
case "invite": {
const threePidContent = content.third_party_invite;
if (threePidContent) {
if (threePidContent.display_name) {
return () => _t('%(targetName)s accepted the invitation for %(displayName)s', {
targetName,
displayName: threePidContent.display_name,
});
return () =>
_t("%(targetName)s accepted the invitation for %(displayName)s", {
targetName,
displayName: threePidContent.display_name,
});
} else {
return () => _t('%(targetName)s accepted an invitation', { targetName });
return () => _t("%(targetName)s accepted an invitation", { targetName });
}
} else {
return () => _t('%(senderName)s invited %(targetName)s', { senderName, targetName });
return () => _t("%(senderName)s invited %(targetName)s", { senderName, targetName });
}
}
case 'ban':
return () => reason
? _t('%(senderName)s banned %(targetName)s: %(reason)s', { senderName, targetName, reason })
: _t('%(senderName)s banned %(targetName)s', { senderName, targetName });
case 'join':
if (prevContent && prevContent.membership === 'join') {
case "ban":
return () =>
reason
? _t("%(senderName)s banned %(targetName)s: %(reason)s", { senderName, targetName, reason })
: _t("%(senderName)s banned %(targetName)s", { senderName, targetName });
case "join":
if (prevContent && prevContent.membership === "join") {
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s', {
// We're taking the display namke directly from the event content here so we need
// to strip direction override chars which the js-sdk would normally do when
// calculating the display name
oldDisplayName: removeDirectionOverrideChars(prevContent.displayname),
displayName: removeDirectionOverrideChars(content.displayname),
});
return () =>
_t("%(oldDisplayName)s changed their display name to %(displayName)s", {
// We're taking the display namke directly from the event content here so we need
// to strip direction override chars which the js-sdk would normally do when
// calculating the display name
oldDisplayName: removeDirectionOverrideChars(prevContent.displayname),
displayName: removeDirectionOverrideChars(content.displayname),
});
} else if (!prevContent.displayname && content.displayname) {
return () => _t('%(senderName)s set their display name to %(displayName)s', {
senderName: ev.getSender(),
displayName: removeDirectionOverrideChars(content.displayname),
});
return () =>
_t("%(senderName)s set their display name to %(displayName)s", {
senderName: ev.getSender(),
displayName: removeDirectionOverrideChars(content.displayname),
});
} else if (prevContent.displayname && !content.displayname) {
return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s)', {
senderName,
oldDisplayName: removeDirectionOverrideChars(prevContent.displayname),
});
return () =>
_t("%(senderName)s removed their display name (%(oldDisplayName)s)", {
senderName,
oldDisplayName: removeDirectionOverrideChars(prevContent.displayname),
});
} else if (prevContent.avatar_url && !content.avatar_url) {
return () => _t('%(senderName)s removed their profile picture', { senderName });
} else if (prevContent.avatar_url && content.avatar_url &&
prevContent.avatar_url !== content.avatar_url) {
return () => _t('%(senderName)s changed their profile picture', { senderName });
return () => _t("%(senderName)s removed their profile picture", { senderName });
} else if (
prevContent.avatar_url &&
content.avatar_url &&
prevContent.avatar_url !== content.avatar_url
) {
return () => _t("%(senderName)s changed their profile picture", { senderName });
} else if (!prevContent.avatar_url && content.avatar_url) {
return () => _t('%(senderName)s set a profile picture', { senderName });
return () => _t("%(senderName)s set a profile picture", { senderName });
} else if (showHiddenEvents ?? SettingsStore.getValue("showHiddenEventsInTimeline")) {
// This is a null rejoin, it will only be visible if using 'show hidden events' (labs)
return () => _t("%(senderName)s made no change", { senderName });
@ -154,35 +162,38 @@ function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents
}
} else {
if (!ev.target) logger.warn("Join message has no target! -- " + ev.getContent().state_key);
return () => _t('%(targetName)s joined the room', { targetName });
return () => _t("%(targetName)s joined the room", { targetName });
}
case 'leave':
case "leave":
if (ev.getSender() === ev.getStateKey()) {
if (prevContent.membership === "invite") {
return () => _t('%(targetName)s rejected the invitation', { targetName });
return () => _t("%(targetName)s rejected the invitation", { targetName });
} else {
return () => reason
? _t('%(targetName)s left the room: %(reason)s', { targetName, reason })
: _t('%(targetName)s left the room', { targetName });
return () =>
reason
? _t("%(targetName)s left the room: %(reason)s", { targetName, reason })
: _t("%(targetName)s left the room", { targetName });
}
} else if (prevContent.membership === "ban") {
return () => _t('%(senderName)s unbanned %(targetName)s', { senderName, targetName });
return () => _t("%(senderName)s unbanned %(targetName)s", { senderName, targetName });
} else if (prevContent.membership === "invite") {
return () => reason
? _t('%(senderName)s withdrew %(targetName)s\'s invitation: %(reason)s', {
senderName,
targetName,
reason,
})
: _t('%(senderName)s withdrew %(targetName)s\'s invitation', { senderName, targetName });
return () =>
reason
? _t("%(senderName)s withdrew %(targetName)s's invitation: %(reason)s", {
senderName,
targetName,
reason,
})
: _t("%(senderName)s withdrew %(targetName)s's invitation", { senderName, targetName });
} else if (prevContent.membership === "join") {
return () => reason
? _t('%(senderName)s removed %(targetName)s: %(reason)s', {
senderName,
targetName,
reason,
})
: _t('%(senderName)s removed %(targetName)s', { senderName, targetName });
return () =>
reason
? _t("%(senderName)s removed %(targetName)s: %(reason)s", {
senderName,
targetName,
reason,
})
: _t("%(senderName)s removed %(targetName)s", { senderName, targetName });
} else {
return null;
}
@ -191,39 +202,42 @@ function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents
function textForTopicEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
senderDisplayName,
topic: ev.getContent().topic,
});
return () =>
_t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
senderDisplayName,
topic: ev.getContent().topic,
});
}
function textForRoomAvatarEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev?.sender?.name || ev.getSender();
return () => _t('%(senderDisplayName)s changed the room avatar.', { senderDisplayName });
return () => _t("%(senderDisplayName)s changed the room avatar.", { senderDisplayName });
}
function textForRoomNameEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
return () => _t('%(senderDisplayName)s removed the room name.', { senderDisplayName });
return () => _t("%(senderDisplayName)s removed the room name.", { senderDisplayName });
}
if (ev.getPrevContent().name) {
return () => _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
senderDisplayName,
oldRoomName: ev.getPrevContent().name,
newRoomName: ev.getContent().name,
});
return () =>
_t("%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.", {
senderDisplayName,
oldRoomName: ev.getPrevContent().name,
newRoomName: ev.getContent().name,
});
}
return () => _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
senderDisplayName,
roomName: ev.getContent().name,
});
return () =>
_t("%(senderDisplayName)s changed the room name to %(roomName)s.", {
senderDisplayName,
roomName: ev.getContent().name,
});
}
function textForTombstoneEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return () => _t('%(senderDisplayName)s upgraded this room.', { senderDisplayName });
return () => _t("%(senderDisplayName)s upgraded this room.", { senderDisplayName });
}
const onViewJoinRuleSettingsClick = () => {
@ -237,33 +251,44 @@ function textForJoinRulesEvent(ev: MatrixEvent, allowJSX: boolean): () => Render
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
switch (ev.getContent().join_rule) {
case JoinRule.Public:
return () => _t('%(senderDisplayName)s made the room public to whoever knows the link.', {
senderDisplayName,
});
return () =>
_t("%(senderDisplayName)s made the room public to whoever knows the link.", {
senderDisplayName,
});
case JoinRule.Invite:
return () => _t('%(senderDisplayName)s made the room invite only.', {
senderDisplayName,
});
return () =>
_t("%(senderDisplayName)s made the room invite only.", {
senderDisplayName,
});
case JoinRule.Restricted:
if (allowJSX) {
return () => <span>
{ _t('%(senderDisplayName)s changed who can join this room. <a>View settings</a>.', {
senderDisplayName,
}, {
"a": (sub) => <AccessibleButton kind='link_inline' onClick={onViewJoinRuleSettingsClick}>
{ sub }
</AccessibleButton>,
}) }
</span>;
return () => (
<span>
{_t(
"%(senderDisplayName)s changed who can join this room. <a>View settings</a>.",
{
senderDisplayName,
},
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={onViewJoinRuleSettingsClick}>
{sub}
</AccessibleButton>
),
},
)}
</span>
);
}
return () => _t('%(senderDisplayName)s changed who can join this room.', { senderDisplayName });
return () => _t("%(senderDisplayName)s changed who can join this room.", { senderDisplayName });
default:
// The spec supports "knock" and "private", however nothing implements these.
return () => _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
senderDisplayName,
rule: ev.getContent().join_rule,
});
return () =>
_t("%(senderDisplayName)s changed the join rule to %(rule)s", {
senderDisplayName,
rule: ev.getContent().join_rule,
});
}
}
@ -271,15 +296,16 @@ function textForGuestAccessEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
switch (ev.getContent().guest_access) {
case GuestAccess.CanJoin:
return () => _t('%(senderDisplayName)s has allowed guests to join the room.', { senderDisplayName });
return () => _t("%(senderDisplayName)s has allowed guests to join the room.", { senderDisplayName });
case GuestAccess.Forbidden:
return () => _t('%(senderDisplayName)s has prevented guests from joining the room.', { senderDisplayName });
return () => _t("%(senderDisplayName)s has prevented guests from joining the room.", { senderDisplayName });
default:
// There's no other options we can expect, however just for safety's sake we'll do this.
return () => _t('%(senderDisplayName)s changed guest access to %(rule)s', {
senderDisplayName,
rule: ev.getContent().guest_access,
});
return () =>
_t("%(senderDisplayName)s changed guest access to %(rule)s", {
senderDisplayName,
rule: ev.getContent().guest_access,
});
}
}
@ -306,8 +332,8 @@ function textForServerACLEvent(ev: MatrixEvent): () => string | null {
// If we know for sure everyone is banned, mark the room as obliterated
if (current.allow.length === 0) {
return () => getText() + " " +
_t("🎉 All servers are banned from participating! This room can no longer be used.");
return () =>
getText() + " " + _t("🎉 All servers are banned from participating! This room can no longer be used.");
}
return getText;
@ -339,12 +365,12 @@ function textForMessageEvent(ev: MatrixEvent): () => string | null {
if (ev.getContent().msgtype === MsgType.Emote) {
message = "* " + senderDisplayName + " " + message;
} else if (ev.getContent().msgtype === MsgType.Image) {
message = _t('%(senderDisplayName)s sent an image.', { senderDisplayName });
message = _t("%(senderDisplayName)s sent an image.", { senderDisplayName });
} else if (ev.getType() == EventType.Sticker) {
message = _t('%(senderDisplayName)s sent a sticker.', { senderDisplayName });
message = _t("%(senderDisplayName)s sent a sticker.", { senderDisplayName });
} else {
// in this case, parse it as a plain text message
message = senderDisplayName + ': ' + message;
message = senderDisplayName + ": " + message;
}
return message;
};
@ -356,87 +382,105 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
const oldAltAliases = ev.getPrevContent().alt_aliases || [];
const newAlias = ev.getContent().alias;
const newAltAliases = ev.getContent().alt_aliases || [];
const removedAltAliases = oldAltAliases.filter(alias => !newAltAliases.includes(alias));
const addedAltAliases = newAltAliases.filter(alias => !oldAltAliases.includes(alias));
const removedAltAliases = oldAltAliases.filter((alias) => !newAltAliases.includes(alias));
const addedAltAliases = newAltAliases.filter((alias) => !oldAltAliases.includes(alias));
if (!removedAltAliases.length && !addedAltAliases.length) {
if (newAlias) {
return () => _t('%(senderName)s set the main address for this room to %(address)s.', {
senderName,
address: ev.getContent().alias,
});
return () =>
_t("%(senderName)s set the main address for this room to %(address)s.", {
senderName,
address: ev.getContent().alias,
});
} else if (oldAlias) {
return () => _t('%(senderName)s removed the main address for this room.', {
senderName,
});
return () =>
_t("%(senderName)s removed the main address for this room.", {
senderName,
});
}
} else if (newAlias === oldAlias) {
if (addedAltAliases.length && !removedAltAliases.length) {
return () => _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
senderName,
addresses: addedAltAliases.join(", "),
count: addedAltAliases.length,
});
return () =>
_t("%(senderName)s added the alternative addresses %(addresses)s for this room.", {
senderName,
addresses: addedAltAliases.join(", "),
count: addedAltAliases.length,
});
}
if (removedAltAliases.length && !addedAltAliases.length) {
return () => _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
senderName,
addresses: removedAltAliases.join(", "),
count: removedAltAliases.length,
});
return () =>
_t("%(senderName)s removed the alternative addresses %(addresses)s for this room.", {
senderName,
addresses: removedAltAliases.join(", "),
count: removedAltAliases.length,
});
}
if (removedAltAliases.length && addedAltAliases.length) {
return () => _t('%(senderName)s changed the alternative addresses for this room.', {
senderName,
});
return () =>
_t("%(senderName)s changed the alternative addresses for this room.", {
senderName,
});
}
} else {
// both alias and alt_aliases where modified
return () => _t('%(senderName)s changed the main and alternative addresses for this room.', {
senderName,
});
return () =>
_t("%(senderName)s changed the main and alternative addresses for this room.", {
senderName,
});
}
// in case there is no difference between the two events,
// say something as we can't simply hide the tile from here
return () => _t('%(senderName)s changed the addresses for this room.', {
senderName,
});
return () =>
_t("%(senderName)s changed the addresses for this room.", {
senderName,
});
}
function textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
const senderName = getSenderName(event);
if (!isValid3pidInvite(event)) {
return () => _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
senderName,
targetDisplayName: event.getPrevContent().display_name || _t("Someone"),
});
return () =>
_t("%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.", {
senderName,
targetDisplayName: event.getPrevContent().display_name || _t("Someone"),
});
}
return () => _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
senderName,
targetDisplayName: event.getContent().display_name,
});
return () =>
_t("%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.", {
senderName,
targetDisplayName: event.getContent().display_name,
});
}
function textForHistoryVisibilityEvent(event: MatrixEvent): () => string | null {
const senderName = getSenderName(event);
switch (event.getContent().history_visibility) {
case HistoryVisibility.Invited:
return () => _t('%(senderName)s made future room history visible to all room members, '
+ 'from the point they are invited.', { senderName });
return () =>
_t(
"%(senderName)s made future room history visible to all room members, " +
"from the point they are invited.",
{ senderName },
);
case HistoryVisibility.Joined:
return () => _t('%(senderName)s made future room history visible to all room members, '
+ 'from the point they joined.', { senderName });
return () =>
_t(
"%(senderName)s made future room history visible to all room members, " +
"from the point they joined.",
{ senderName },
);
case HistoryVisibility.Shared:
return () => _t('%(senderName)s made future room history visible to all room members.', { senderName });
return () => _t("%(senderName)s made future room history visible to all room members.", { senderName });
case HistoryVisibility.WorldReadable:
return () => _t('%(senderName)s made future room history visible to anyone.', { senderName });
return () => _t("%(senderName)s made future room history visible to anyone.", { senderName });
default:
return () => _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
senderName,
visibility: event.getContent().history_visibility,
});
return () =>
_t("%(senderName)s made future room history visible to unknown (%(visibility)s).", {
senderName,
visibility: event.getContent().history_visibility,
});
}
}
@ -474,7 +518,9 @@ function textForPowerEvent(event: MatrixEvent): () => string | null {
if (!Number.isInteger(to)) {
to = currentUserDefault;
}
if (from === previousUserDefault && to === currentUserDefault) { return; }
if (from === previousUserDefault && to === currentUserDefault) {
return;
}
if (to !== from) {
const name = getRoomMemberDisplayname(event, userId);
diffs.push({ userId, name, from, to });
@ -485,16 +531,19 @@ function textForPowerEvent(event: MatrixEvent): () => string | null {
}
// XXX: This is also surely broken for i18n
return () => _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
senderName,
powerLevelDiffText: diffs.map(diff =>
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
userId: diff.name,
fromPowerLevel: Roles.textualPowerLevel(diff.from, previousUserDefault),
toPowerLevel: Roles.textualPowerLevel(diff.to, currentUserDefault),
}),
).join(", "),
});
return () =>
_t("%(senderName)s changed the power level of %(powerLevelDiffText)s.", {
senderName,
powerLevelDiffText: diffs
.map((diff) =>
_t("%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s", {
userId: diff.name,
fromPowerLevel: Roles.textualPowerLevel(diff.from, previousUserDefault),
toPowerLevel: Roles.textualPowerLevel(diff.to, currentUserDefault),
}),
)
.join(", "),
});
}
const onPinnedOrUnpinnedMessageClick = (messageId: string, roomId: string): void => {
@ -518,8 +567,8 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => Render
const pinned = event.getContent().pinned ?? [];
const previouslyPinned = event.getPrevContent().pinned ?? [];
const newlyPinned = pinned.filter(item => previouslyPinned.indexOf(item) < 0);
const newlyUnpinned = previouslyPinned.filter(item => pinned.indexOf(item) < 0);
const newlyPinned = pinned.filter((item) => previouslyPinned.indexOf(item) < 0);
const newlyUnpinned = previouslyPinned.filter((item) => pinned.indexOf(item) < 0);
if (newlyPinned.length === 1 && newlyUnpinned.length === 0) {
// A single message was pinned, include a link to that message.
@ -528,20 +577,25 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => Render
return () => (
<span>
{ _t(
{_t(
"%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.",
{ senderName },
{
"a": (sub) =>
<AccessibleButton kind='link_inline' onClick={(e) => onPinnedOrUnpinnedMessageClick(messageId, roomId)}>
{ sub }
</AccessibleButton>,
"b": (sub) =>
<AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub }
</AccessibleButton>,
a: (sub) => (
<AccessibleButton
kind="link_inline"
onClick={(e) => onPinnedOrUnpinnedMessageClick(messageId, roomId)}
>
{sub}
</AccessibleButton>
),
b: (sub) => (
<AccessibleButton kind="link_inline" onClick={onPinnedMessagesClick}>
{sub}
</AccessibleButton>
),
},
) }
)}
</span>
);
}
@ -556,20 +610,25 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => Render
return () => (
<span>
{ _t(
{_t(
"%(senderName)s unpinned <a>a message</a> from this room. See all <b>pinned messages</b>.",
{ senderName },
{
"a": (sub) =>
<AccessibleButton kind='link_inline' onClick={(e) => onPinnedOrUnpinnedMessageClick(messageId, roomId)}>
{ sub }
</AccessibleButton>,
"b": (sub) =>
<AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub }
</AccessibleButton>,
a: (sub) => (
<AccessibleButton
kind="link_inline"
onClick={(e) => onPinnedOrUnpinnedMessageClick(messageId, roomId)}
>
{sub}
</AccessibleButton>
),
b: (sub) => (
<AccessibleButton kind="link_inline" onClick={onPinnedMessagesClick}>
{sub}
</AccessibleButton>
),
},
) }
)}
</span>
);
}
@ -580,16 +639,17 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => Render
if (allowJSX) {
return () => (
<span>
{ _t(
{_t(
"%(senderName)s changed the <a>pinned messages</a> for the room.",
{ senderName },
{
"a": (sub) =>
<AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub }
</AccessibleButton>,
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={onPinnedMessagesClick}>
{sub}
</AccessibleButton>
),
},
) }
)}
</span>
);
}
@ -602,7 +662,7 @@ function textForWidgetEvent(event: MatrixEvent): () => string | null {
const { name: prevName, type: prevType, url: prevUrl } = event.getPrevContent();
const { name, type, url } = event.getContent() || {};
let widgetName = name || prevName || type || prevType || '';
let widgetName = name || prevName || type || prevType || "";
// Apply sentence case to widget name
if (widgetName && widgetName.length > 0) {
widgetName = widgetName[0].toUpperCase() + widgetName.slice(1);
@ -612,18 +672,24 @@ function textForWidgetEvent(event: MatrixEvent): () => string | null {
// equivalent to that condition.
if (url) {
if (prevUrl) {
return () => _t('%(widgetName)s widget modified by %(senderName)s', {
widgetName, senderName,
});
return () =>
_t("%(widgetName)s widget modified by %(senderName)s", {
widgetName,
senderName,
});
} else {
return () => _t('%(widgetName)s widget added by %(senderName)s', {
widgetName, senderName,
});
return () =>
_t("%(widgetName)s widget added by %(senderName)s", {
widgetName,
senderName,
});
}
} else {
return () => _t('%(widgetName)s widget removed by %(senderName)s', {
widgetName, senderName,
});
return () =>
_t("%(widgetName)s widget removed by %(senderName)s", {
widgetName,
senderName,
});
}
}
@ -640,14 +706,17 @@ function textForMjolnirEvent(event: MatrixEvent): () => string | null {
// Rule removed
if (!entity) {
if (USER_RULE_TYPES.includes(event.getType())) {
return () => _t("%(senderName)s removed the rule banning users matching %(glob)s",
{ senderName, glob: prevEntity });
return () =>
_t("%(senderName)s removed the rule banning users matching %(glob)s", { senderName, glob: prevEntity });
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
return () => _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
{ senderName, glob: prevEntity });
return () =>
_t("%(senderName)s removed the rule banning rooms matching %(glob)s", { senderName, glob: prevEntity });
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
return () => _t("%(senderName)s removed the rule banning servers matching %(glob)s",
{ senderName, glob: prevEntity });
return () =>
_t("%(senderName)s removed the rule banning servers matching %(glob)s", {
senderName,
glob: prevEntity,
});
}
// Unknown type. We'll say something, but we shouldn't end up here.
@ -660,69 +729,109 @@ function textForMjolnirEvent(event: MatrixEvent): () => string | null {
// Rule updated
if (entity === prevEntity) {
if (USER_RULE_TYPES.includes(event.getType())) {
return () => _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
{ senderName, glob: entity, reason });
return () =>
_t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s", {
senderName,
glob: entity,
reason,
});
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
return () => _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
{ senderName, glob: entity, reason });
return () =>
_t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s", {
senderName,
glob: entity,
reason,
});
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
return () => _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
{ senderName, glob: entity, reason });
return () =>
_t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s", {
senderName,
glob: entity,
reason,
});
}
// Unknown type. We'll say something but we shouldn't end up here.
return () => _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
{ senderName, glob: entity, reason });
return () =>
_t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s", {
senderName,
glob: entity,
reason,
});
}
// New rule
if (!prevEntity) {
if (USER_RULE_TYPES.includes(event.getType())) {
return () => _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
{ senderName, glob: entity, reason });
return () =>
_t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s", {
senderName,
glob: entity,
reason,
});
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
return () => _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
{ senderName, glob: entity, reason });
return () =>
_t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s", {
senderName,
glob: entity,
reason,
});
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
return () => _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
{ senderName, glob: entity, reason });
return () =>
_t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s", {
senderName,
glob: entity,
reason,
});
}
// Unknown type. We'll say something but we shouldn't end up here.
return () => _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
{ senderName, glob: entity, reason });
return () =>
_t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s", {
senderName,
glob: entity,
reason,
});
}
// else the entity !== prevEntity - count as a removal & add
if (USER_RULE_TYPES.includes(event.getType())) {
return () => _t(
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{ senderName, oldGlob: prevEntity, newGlob: entity, reason },
);
return () =>
_t(
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{ senderName, oldGlob: prevEntity, newGlob: entity, reason },
);
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
return () => _t(
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{ senderName, oldGlob: prevEntity, newGlob: entity, reason },
);
return () =>
_t(
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{ senderName, oldGlob: prevEntity, newGlob: entity, reason },
);
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
return () => _t(
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{ senderName, oldGlob: prevEntity, newGlob: entity, reason },
);
return () =>
_t(
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{ senderName, oldGlob: prevEntity, newGlob: entity, reason },
);
}
// Unknown type. We'll say something but we shouldn't end up here.
return () => _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
"for %(reason)s", { senderName, oldGlob: prevEntity, newGlob: entity, reason });
return () =>
_t(
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
"for %(reason)s",
{ senderName, oldGlob: prevEntity, newGlob: entity, reason },
);
}
export function textForLocationEvent(event: MatrixEvent): () => string | null {
return () => _t("%(senderName)s has shared their location", {
senderName: getSenderName(event),
});
return () =>
_t("%(senderName)s has shared their location", {
senderName: getSenderName(event),
});
}
function textForRedactedPollAndMessageEvent(ev: MatrixEvent): string {
@ -742,12 +851,12 @@ function textForRedactedPollAndMessageEvent(ev: MatrixEvent): string {
function textForPollStartEvent(event: MatrixEvent): () => string | null {
return () => {
let message = '';
let message = "";
if (event.isRedacted()) {
message = textForRedactedPollAndMessageEvent(event);
const senderDisplayName = event.sender?.name ?? event.getSender();
message = senderDisplayName + ': ' + message;
message = senderDisplayName + ": " + message;
} else {
message = _t("%(senderName)s has started a poll - %(pollQuestion)s", {
senderName: getSenderName(event),
@ -760,17 +869,16 @@ function textForPollStartEvent(event: MatrixEvent): () => string | null {
}
function textForPollEndEvent(event: MatrixEvent): () => string | null {
return () => _t("%(senderName)s has ended a poll", {
senderName: getSenderName(event),
});
return () =>
_t("%(senderName)s has ended a poll", {
senderName: getSenderName(event),
});
}
type Renderable = string | JSX.Element | null;
interface IHandlers {
[type: string]:
(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean) =>
(() => Renderable);
[type: string]: (ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean) => () => Renderable;
}
const handlers: IHandlers = {
@ -799,7 +907,7 @@ const stateHandlers: IHandlers = {
[EventType.RoomGuestAccess]: textForGuestAccessEvent,
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
'im.vector.modular.widgets': textForWidgetEvent,
"im.vector.modular.widgets": textForWidgetEvent,
[WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent,
};
@ -835,5 +943,5 @@ export function textForEvent(ev: MatrixEvent): string;
export function textForEvent(ev: MatrixEvent, allowJSX: true, showHiddenEvents?: boolean): string | JSX.Element;
export function textForEvent(ev: MatrixEvent, allowJSX = false, showHiddenEvents?: boolean): string | JSX.Element {
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
return handler?.(ev, allowJSX, showHiddenEvents)?.() || '';
return handler?.(ev, allowJSX, showHiddenEvents)?.() || "";
}

View file

@ -20,7 +20,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { M_BEACON } from "matrix-js-sdk/src/@types/beacon";
import { MatrixClientPeg } from "./MatrixClientPeg";
import shouldHideEvent from './shouldHideEvent';
import shouldHideEvent from "./shouldHideEvent";
import { haveRendererForEvent } from "./events/EventTileFactory";
import SettingsStore from "./settings/SettingsStore";

View file

@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import dis from './dispatcher/dispatcher';
import Timer from './utils/Timer';
import dis from "./dispatcher/dispatcher";
import Timer from "./utils/Timer";
// important these are larger than the timeouts of timers
// used with UserActivity.timeWhileActive*,
@ -95,14 +95,18 @@ export default class UserActivity {
if (index === -1) {
attachedTimers.push(timer);
// remove when done or aborted
timer.finished().finally(() => {
const index = attachedTimers.indexOf(timer);
if (index !== -1) { // should never be -1
attachedTimers.splice(index, 1);
}
// as we fork the promise here,
// avoid unhandled rejection warnings
}).catch((err) => {});
timer
.finished()
.finally(() => {
const index = attachedTimers.indexOf(timer);
if (index !== -1) {
// should never be -1
attachedTimers.splice(index, 1);
}
// as we fork the promise here,
// avoid unhandled rejection warnings
})
.catch((err) => {});
}
}
@ -110,9 +114,9 @@ export default class UserActivity {
* Start listening to user activity
*/
public start() {
this.document.addEventListener('mousedown', this.onUserActivity);
this.document.addEventListener('mousemove', this.onUserActivity);
this.document.addEventListener('keydown', this.onUserActivity);
this.document.addEventListener("mousedown", this.onUserActivity);
this.document.addEventListener("mousemove", this.onUserActivity);
this.document.addEventListener("keydown", this.onUserActivity);
this.document.addEventListener("visibilitychange", this.onPageVisibilityChanged);
this.window.addEventListener("blur", this.onWindowBlurred);
this.window.addEventListener("focus", this.onUserActivity);
@ -120,7 +124,7 @@ export default class UserActivity {
// itself being scrolled. Need to use addEventListener's useCapture.
// also this needs to be the wheel event, not scroll, as scroll is
// fired when the view scrolls down for a new message.
this.window.addEventListener('wheel', this.onUserActivity, {
this.window.addEventListener("wheel", this.onUserActivity, {
passive: true,
capture: true,
});
@ -130,10 +134,10 @@ export default class UserActivity {
* Stop tracking user activity
*/
public stop() {
this.document.removeEventListener('mousedown', this.onUserActivity);
this.document.removeEventListener('mousemove', this.onUserActivity);
this.document.removeEventListener('keydown', this.onUserActivity);
this.window.removeEventListener('wheel', this.onUserActivity, {
this.document.removeEventListener("mousedown", this.onUserActivity);
this.document.removeEventListener("mousemove", this.onUserActivity);
this.document.removeEventListener("keydown", this.onUserActivity);
this.window.removeEventListener("wheel", this.onUserActivity, {
capture: true,
});
this.document.removeEventListener("visibilitychange", this.onPageVisibilityChanged);
@ -164,7 +168,7 @@ export default class UserActivity {
return this.activeRecentlyTimeout.isRunning();
}
private onPageVisibilityChanged = e => {
private onPageVisibilityChanged = (e) => {
if (this.document.visibilityState === "hidden") {
this.activeNowTimeout.abort();
this.activeRecentlyTimeout.abort();
@ -191,10 +195,10 @@ export default class UserActivity {
this.lastScreenY = event.screenY;
}
dis.dispatch({ action: 'user_activity' });
dis.dispatch({ action: "user_activity" });
if (!this.activeNowTimeout.isRunning()) {
this.activeNowTimeout.start();
dis.dispatch({ action: 'user_activity_start' });
dis.dispatch({ action: "user_activity_start" });
UserActivity.runTimersUntilTimeout(this.attachedActiveNowTimers, this.activeNowTimeout);
} else {
@ -214,7 +218,9 @@ export default class UserActivity {
attachedTimers.forEach((t) => t.start());
try {
await timeout.finished();
} catch (_e) { /* aborted */ }
} catch (_e) {
/* aborted */
}
attachedTimers.forEach((t) => t.abort());
}
}

View file

@ -14,16 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from 'matrix-js-sdk/src/models/room';
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { EventType } from "matrix-js-sdk/src/@types/event";
import { ensureVirtualRoomExists } from './createRoom';
import { ensureVirtualRoomExists } from "./createRoom";
import { MatrixClientPeg } from "./MatrixClientPeg";
import DMRoomMap from "./utils/DMRoomMap";
import LegacyCallHandler from './LegacyCallHandler';
import LegacyCallHandler from "./LegacyCallHandler";
import { VIRTUAL_ROOM_EVENT_TYPE } from "./call-types";
import { findDMForUser } from './utils/dm/findDMForUser';
import { findDMForUser } from "./utils/dm/findDMForUser";
// Functions for mapping virtual users & rooms. Currently the only lookup
// is sip virtual: there could be others in the future.
@ -92,9 +92,9 @@ export default class VoipUserMapper {
if (!virtualRoom) return null;
const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE);
if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null;
const nativeRoomID = virtualRoomEvent.getContent()['native_room'];
const nativeRoomID = virtualRoomEvent.getContent()["native_room"];
const nativeRoom = MatrixClientPeg.get().getRoom(nativeRoomID);
if (!nativeRoom || nativeRoom.getMyMembership() !== 'join') return null;
if (!nativeRoom || nativeRoom.getMyMembership() !== "join") return null;
return nativeRoomID;
}

View file

@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixClientPeg } from "./MatrixClientPeg";
import { _t } from './languageHandler';
import { _t } from "./languageHandler";
export function usersTypingApartFromMeAndIgnored(room: Room): RoomMember[] {
return usersTyping(room, [MatrixClientPeg.get().getUserId()].concat(MatrixClientPeg.get().getIgnoredUsers()));
@ -57,20 +57,20 @@ export function whoIsTypingString(whoIsTyping: RoomMember[], limit: number): str
}
if (whoIsTyping.length === 0) {
return '';
return "";
} else if (whoIsTyping.length === 1) {
return _t('%(displayName)s is typing …', { displayName: whoIsTyping[0].name });
return _t("%(displayName)s is typing …", { displayName: whoIsTyping[0].name });
}
const names = whoIsTyping.map(m => m.name);
const names = whoIsTyping.map((m) => m.name);
if (othersCount >= 1) {
return _t('%(names)s and %(count)s others are typing …', {
names: names.slice(0, limit - 1).join(', '),
return _t("%(names)s and %(count)s others are typing …", {
names: names.slice(0, limit - 1).join(", "),
count: othersCount,
});
} else {
const lastPerson = names.pop();
return _t('%(names)s and %(lastPerson)s are typing …', { names: names.join(', '), lastPerson: lastPerson });
return _t("%(names)s and %(lastPerson)s are typing …", { names: names.join(", "), lastPerson: lastPerson });
}
}

View file

@ -34,7 +34,7 @@ import {
* have to be manually mirrored in KeyBindingDefaults.
*/
const getUIOnlyShortcuts = (): IKeyboardShortcuts => {
const ctrlEnterToSend = SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend');
const ctrlEnterToSend = SettingsStore.getValue("MessageComposerInput.ctrlEnterToSend");
const keyboardShortcuts: IKeyboardShortcuts = {
[KeyBindingAction.SendMessage]: {
@ -94,26 +94,25 @@ const getUIOnlyShortcuts = (): IKeyboardShortcuts => {
export const getKeyboardShortcuts = (): IKeyboardShortcuts => {
const overrideBrowserShortcuts = PlatformPeg.get().overrideBrowserShortcuts();
return Object.keys(KEYBOARD_SHORTCUTS).filter((k: KeyBindingAction) => {
if (KEYBOARD_SHORTCUTS[k]?.controller?.settingDisabled) return false;
if (MAC_ONLY_SHORTCUTS.includes(k) && !IS_MAC) return false;
if (DESKTOP_SHORTCUTS.includes(k) && !overrideBrowserShortcuts) return false;
return Object.keys(KEYBOARD_SHORTCUTS)
.filter((k: KeyBindingAction) => {
if (KEYBOARD_SHORTCUTS[k]?.controller?.settingDisabled) return false;
if (MAC_ONLY_SHORTCUTS.includes(k) && !IS_MAC) return false;
if (DESKTOP_SHORTCUTS.includes(k) && !overrideBrowserShortcuts) return false;
return true;
}).reduce((o, key) => {
o[key] = KEYBOARD_SHORTCUTS[key];
return o;
}, {} as IKeyboardShortcuts);
return true;
})
.reduce((o, key) => {
o[key] = KEYBOARD_SHORTCUTS[key];
return o;
}, {} as IKeyboardShortcuts);
};
/**
* Gets keyboard shortcuts that should be presented to the user in the UI.
*/
export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => {
const entries = [
...Object.entries(getUIOnlyShortcuts()),
...Object.entries(getKeyboardShortcuts()),
];
const entries = [...Object.entries(getUIOnlyShortcuts()), ...Object.entries(getKeyboardShortcuts())];
return entries.reduce((acc, [key, value]) => {
acc[key] = value;

View file

@ -23,103 +23,103 @@ import { KeyCombo } from "../KeyBindingsManager";
export enum KeyBindingAction {
/** Send a message */
SendMessage = 'KeyBinding.sendMessageInComposer',
SendMessage = "KeyBinding.sendMessageInComposer",
/** Go backwards through the send history and use the message in composer view */
SelectPrevSendHistory = 'KeyBinding.previousMessageInComposerHistory',
SelectPrevSendHistory = "KeyBinding.previousMessageInComposerHistory",
/** Go forwards through the send history */
SelectNextSendHistory = 'KeyBinding.nextMessageInComposerHistory',
SelectNextSendHistory = "KeyBinding.nextMessageInComposerHistory",
/** Start editing the user's last sent message */
EditPrevMessage = 'KeyBinding.editPreviousMessage',
EditPrevMessage = "KeyBinding.editPreviousMessage",
/** Start editing the user's next sent message */
EditNextMessage = 'KeyBinding.editNextMessage',
EditNextMessage = "KeyBinding.editNextMessage",
/** Cancel editing a message or cancel replying to a message */
CancelReplyOrEdit = 'KeyBinding.cancelReplyInComposer',
CancelReplyOrEdit = "KeyBinding.cancelReplyInComposer",
/** Show the sticker picker */
ShowStickerPicker = 'KeyBinding.showStickerPicker',
ShowStickerPicker = "KeyBinding.showStickerPicker",
/** Set bold format the current selection */
FormatBold = 'KeyBinding.toggleBoldInComposer',
FormatBold = "KeyBinding.toggleBoldInComposer",
/** Set italics format the current selection */
FormatItalics = 'KeyBinding.toggleItalicsInComposer',
FormatItalics = "KeyBinding.toggleItalicsInComposer",
/** Insert link for current selection */
FormatLink = 'KeyBinding.FormatLink',
FormatLink = "KeyBinding.FormatLink",
/** Set code format for current selection */
FormatCode = 'KeyBinding.FormatCode',
FormatCode = "KeyBinding.FormatCode",
/** Format the current selection as quote */
FormatQuote = 'KeyBinding.toggleQuoteInComposer',
FormatQuote = "KeyBinding.toggleQuoteInComposer",
/** Undo the last editing */
EditUndo = 'KeyBinding.editUndoInComposer',
EditUndo = "KeyBinding.editUndoInComposer",
/** Redo editing */
EditRedo = 'KeyBinding.editRedoInComposer',
EditRedo = "KeyBinding.editRedoInComposer",
/** Insert new line */
NewLine = 'KeyBinding.newLineInComposer',
NewLine = "KeyBinding.newLineInComposer",
/** Move the cursor to the start of the message */
MoveCursorToStart = 'KeyBinding.jumpToStartInComposer',
MoveCursorToStart = "KeyBinding.jumpToStartInComposer",
/** Move the cursor to the end of the message */
MoveCursorToEnd = 'KeyBinding.jumpToEndInComposer',
MoveCursorToEnd = "KeyBinding.jumpToEndInComposer",
/** Accepts chosen autocomplete selection */
CompleteAutocomplete = 'KeyBinding.completeAutocomplete',
CompleteAutocomplete = "KeyBinding.completeAutocomplete",
/** Accepts chosen autocomplete selection or,
* if the autocompletion window is not shown, open the window and select the first selection */
ForceCompleteAutocomplete = 'KeyBinding.forceCompleteAutocomplete',
ForceCompleteAutocomplete = "KeyBinding.forceCompleteAutocomplete",
/** Move to the previous autocomplete selection */
PrevSelectionInAutocomplete = 'KeyBinding.previousOptionInAutoComplete',
PrevSelectionInAutocomplete = "KeyBinding.previousOptionInAutoComplete",
/** Move to the next autocomplete selection */
NextSelectionInAutocomplete = 'KeyBinding.nextOptionInAutoComplete',
NextSelectionInAutocomplete = "KeyBinding.nextOptionInAutoComplete",
/** Close the autocompletion window */
CancelAutocomplete = 'KeyBinding.cancelAutoComplete',
CancelAutocomplete = "KeyBinding.cancelAutoComplete",
/** Clear room list filter field */
ClearRoomFilter = 'KeyBinding.clearRoomFilter',
ClearRoomFilter = "KeyBinding.clearRoomFilter",
/** Navigate up/down in the room list */
PrevRoom = 'KeyBinding.downerRoom',
PrevRoom = "KeyBinding.downerRoom",
/** Navigate down in the room list */
NextRoom = 'KeyBinding.upperRoom',
NextRoom = "KeyBinding.upperRoom",
/** Select room from the room list */
SelectRoomInRoomList = 'KeyBinding.selectRoomInRoomList',
SelectRoomInRoomList = "KeyBinding.selectRoomInRoomList",
/** Collapse room list section */
CollapseRoomListSection = 'KeyBinding.collapseSectionInRoomList',
CollapseRoomListSection = "KeyBinding.collapseSectionInRoomList",
/** Expand room list section, if already expanded, jump to first room in the selection */
ExpandRoomListSection = 'KeyBinding.expandSectionInRoomList',
ExpandRoomListSection = "KeyBinding.expandSectionInRoomList",
/** Scroll up in the timeline */
ScrollUp = 'KeyBinding.scrollUpInTimeline',
ScrollUp = "KeyBinding.scrollUpInTimeline",
/** Scroll down in the timeline */
ScrollDown = 'KeyBinding.scrollDownInTimeline',
ScrollDown = "KeyBinding.scrollDownInTimeline",
/** Dismiss read marker and jump to bottom */
DismissReadMarker = 'KeyBinding.dismissReadMarkerAndJumpToBottom',
DismissReadMarker = "KeyBinding.dismissReadMarkerAndJumpToBottom",
/** Jump to oldest unread message */
JumpToOldestUnread = 'KeyBinding.jumpToOldestUnreadMessage',
JumpToOldestUnread = "KeyBinding.jumpToOldestUnreadMessage",
/** Upload a file */
UploadFile = 'KeyBinding.uploadFileToRoom',
UploadFile = "KeyBinding.uploadFileToRoom",
/** Focus search message in a room (must be enabled) */
SearchInRoom = 'KeyBinding.searchInRoom',
SearchInRoom = "KeyBinding.searchInRoom",
/** Jump to the first (downloaded) message in the room */
JumpToFirstMessage = 'KeyBinding.jumpToFirstMessageInTimeline',
JumpToFirstMessage = "KeyBinding.jumpToFirstMessageInTimeline",
/** Jump to the latest message in the room */
JumpToLatestMessage = 'KeyBinding.jumpToLastMessageInTimeline',
JumpToLatestMessage = "KeyBinding.jumpToLastMessageInTimeline",
/** Jump to room search (search for a room) */
FilterRooms = 'KeyBinding.filterRooms',
FilterRooms = "KeyBinding.filterRooms",
/** Toggle the space panel */
ToggleSpacePanel = 'KeyBinding.toggleSpacePanel',
ToggleSpacePanel = "KeyBinding.toggleSpacePanel",
/** Toggle the room side panel */
ToggleRoomSidePanel = 'KeyBinding.toggleRightPanel',
ToggleRoomSidePanel = "KeyBinding.toggleRightPanel",
/** Toggle the user menu */
ToggleUserMenu = 'KeyBinding.toggleTopLeftMenu',
ToggleUserMenu = "KeyBinding.toggleTopLeftMenu",
/** Toggle the short cut help dialog */
ShowKeyboardSettings = 'KeyBinding.showKeyBindingsSettings',
ShowKeyboardSettings = "KeyBinding.showKeyBindingsSettings",
/** Got to the Element home screen */
GoToHome = 'KeyBinding.goToHomeView',
GoToHome = "KeyBinding.goToHomeView",
/** Select prev room */
SelectPrevRoom = 'KeyBinding.previousRoom',
SelectPrevRoom = "KeyBinding.previousRoom",
/** Select next room */
SelectNextRoom = 'KeyBinding.nextRoom',
SelectNextRoom = "KeyBinding.nextRoom",
/** Select prev room with unread messages */
SelectPrevUnreadRoom = 'KeyBinding.previousUnreadRoom',
SelectPrevUnreadRoom = "KeyBinding.previousUnreadRoom",
/** Select next room with unread messages */
SelectNextUnreadRoom = 'KeyBinding.nextUnreadRoom',
SelectNextUnreadRoom = "KeyBinding.nextUnreadRoom",
/** Switches to a space by number */
SwitchToSpaceByNumber = "KeyBinding.switchToSpaceByNumber",
@ -151,20 +151,20 @@ export enum KeyBindingAction {
Comma = "KeyBinding.comma",
/** Toggle visibility of hidden events */
ToggleHiddenEventVisibility = 'KeyBinding.toggleHiddenEventVisibility',
ToggleHiddenEventVisibility = "KeyBinding.toggleHiddenEventVisibility",
}
type KeyboardShortcutSetting = IBaseSetting<KeyCombo>;
export type IKeyboardShortcuts = {
// TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager
[k in (KeyBindingAction)]?: KeyboardShortcutSetting;
[k in KeyBindingAction]?: KeyboardShortcutSetting;
};
export interface ICategory {
categoryLabel?: string;
// TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager
settingNames: (KeyBindingAction)[];
settingNames: KeyBindingAction[];
}
export enum CategoryName {
@ -227,13 +227,12 @@ export const CATEGORIES: Record<CategoryName, ICategory> = {
KeyBindingAction.SelectPrevSendHistory,
KeyBindingAction.ShowStickerPicker,
],
}, [CategoryName.CALLS]: {
},
[CategoryName.CALLS]: {
categoryLabel: _td("Calls"),
settingNames: [
KeyBindingAction.ToggleMicInCall,
KeyBindingAction.ToggleWebcamInCall,
],
}, [CategoryName.ROOM]: {
settingNames: [KeyBindingAction.ToggleMicInCall, KeyBindingAction.ToggleWebcamInCall],
},
[CategoryName.ROOM]: {
categoryLabel: _td("Room"),
settingNames: [
KeyBindingAction.SearchInRoom,
@ -245,7 +244,8 @@ export const CATEGORIES: Record<CategoryName, ICategory> = {
KeyBindingAction.JumpToFirstMessage,
KeyBindingAction.JumpToLatestMessage,
],
}, [CategoryName.ROOM_LIST]: {
},
[CategoryName.ROOM_LIST]: {
categoryLabel: _td("Room List"),
settingNames: [
KeyBindingAction.SelectRoomInRoomList,
@ -255,7 +255,8 @@ export const CATEGORIES: Record<CategoryName, ICategory> = {
KeyBindingAction.NextRoom,
KeyBindingAction.PrevRoom,
],
}, [CategoryName.ACCESSIBILITY]: {
},
[CategoryName.ACCESSIBILITY]: {
categoryLabel: _td("Accessibility"),
settingNames: [
KeyBindingAction.Escape,
@ -271,7 +272,8 @@ export const CATEGORIES: Record<CategoryName, ICategory> = {
KeyBindingAction.ArrowDown,
KeyBindingAction.Comma,
],
}, [CategoryName.NAVIGATION]: {
},
[CategoryName.NAVIGATION]: {
categoryLabel: _td("Navigation"),
settingNames: [
KeyBindingAction.ToggleUserMenu,
@ -289,7 +291,8 @@ export const CATEGORIES: Record<CategoryName, ICategory> = {
KeyBindingAction.PreviousVisitedRoomOrSpace,
KeyBindingAction.NextVisitedRoomOrSpace,
],
}, [CategoryName.AUTOCOMPLETE]: {
},
[CategoryName.AUTOCOMPLETE]: {
categoryLabel: _td("Autocomplete"),
settingNames: [
KeyBindingAction.CancelAutocomplete,
@ -298,11 +301,10 @@ export const CATEGORIES: Record<CategoryName, ICategory> = {
KeyBindingAction.CompleteAutocomplete,
KeyBindingAction.ForceCompleteAutocomplete,
],
}, [CategoryName.LABS]: {
},
[CategoryName.LABS]: {
categoryLabel: _td("Labs"),
settingNames: [
KeyBindingAction.ToggleHiddenEventVisibility,
],
settingNames: [KeyBindingAction.ToggleHiddenEventVisibility],
},
};
@ -313,9 +315,7 @@ export const DESKTOP_SHORTCUTS = [
KeyBindingAction.NextVisitedRoomOrSpace,
];
export const MAC_ONLY_SHORTCUTS = [
KeyBindingAction.OpenUserSettings,
];
export const MAC_ONLY_SHORTCUTS = [KeyBindingAction.OpenUserSettings];
// This is very intentionally modelled after SETTINGS as it will make it easier
// to implement customizable keyboard shortcuts

View file

@ -117,7 +117,7 @@ export const reducer = (state: IState, action: IAction) => {
}
case Type.Unregister: {
const oldIndex = state.refs.findIndex(r => r === action.payload.ref);
const oldIndex = state.refs.findIndex((r) => r === action.payload.ref);
if (oldIndex === -1) {
return state; // already removed, this should not happen
@ -129,8 +129,8 @@ export const reducer = (state: IState, action: IAction) => {
if (oldIndex >= state.refs.length) {
state.activeRef = findSiblingElement(state.refs, state.refs.length - 1, true);
} else {
state.activeRef = findSiblingElement(state.refs, oldIndex)
|| findSiblingElement(state.refs, oldIndex, true);
state.activeRef =
findSiblingElement(state.refs, oldIndex) || findSiblingElement(state.refs, oldIndex, true);
}
if (document.activeElement === document.body) {
// if the focus got reverted to the body then the user was likely focused on the unmounted element
@ -159,9 +159,7 @@ interface IProps {
handleHomeEnd?: boolean;
handleUpDown?: boolean;
handleLeftRight?: boolean;
children(renderProps: {
onKeyDownHandler(ev: React.KeyboardEvent);
});
children(renderProps: { onKeyDownHandler(ev: React.KeyboardEvent) });
onKeyDown?(ev: React.KeyboardEvent, state: IState);
}
@ -199,96 +197,107 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
const context = useMemo<IContext>(() => ({ state, dispatch }), [state]);
const onKeyDownHandler = useCallback((ev: React.KeyboardEvent) => {
if (onKeyDown) {
onKeyDown(ev, context.state);
if (ev.defaultPrevented) {
return;
const onKeyDownHandler = useCallback(
(ev: React.KeyboardEvent) => {
if (onKeyDown) {
onKeyDown(ev, context.state);
if (ev.defaultPrevented) {
return;
}
}
}
let handled = false;
const action = getKeyBindingsManager().getAccessibilityAction(ev);
let focusRef: RefObject<HTMLElement>;
// Don't interfere with input default keydown behaviour
// but allow people to move focus from it with Tab.
if (checkInputableElement(ev.target as HTMLElement)) {
switch (action) {
case KeyBindingAction.Tab:
handled = true;
if (context.state.refs.length > 0) {
const idx = context.state.refs.indexOf(context.state.activeRef);
focusRef = findSiblingElement(context.state.refs, idx + (ev.shiftKey ? -1 : 1), ev.shiftKey);
}
break;
}
} else {
// check if we actually have any items
switch (action) {
case KeyBindingAction.Home:
if (handleHomeEnd) {
handled = true;
// move focus to first (visible) item
focusRef = findSiblingElement(context.state.refs, 0);
}
break;
case KeyBindingAction.End:
if (handleHomeEnd) {
handled = true;
// move focus to last (visible) item
focusRef = findSiblingElement(context.state.refs, context.state.refs.length - 1, true);
}
break;
case KeyBindingAction.ArrowDown:
case KeyBindingAction.ArrowRight:
if ((action === KeyBindingAction.ArrowDown && handleUpDown) ||
(action === KeyBindingAction.ArrowRight && handleLeftRight)
) {
let handled = false;
const action = getKeyBindingsManager().getAccessibilityAction(ev);
let focusRef: RefObject<HTMLElement>;
// Don't interfere with input default keydown behaviour
// but allow people to move focus from it with Tab.
if (checkInputableElement(ev.target as HTMLElement)) {
switch (action) {
case KeyBindingAction.Tab:
handled = true;
if (context.state.refs.length > 0) {
const idx = context.state.refs.indexOf(context.state.activeRef);
focusRef = findSiblingElement(context.state.refs, idx + 1);
focusRef = findSiblingElement(
context.state.refs,
idx + (ev.shiftKey ? -1 : 1),
ev.shiftKey,
);
}
}
break;
break;
}
} else {
// check if we actually have any items
switch (action) {
case KeyBindingAction.Home:
if (handleHomeEnd) {
handled = true;
// move focus to first (visible) item
focusRef = findSiblingElement(context.state.refs, 0);
}
break;
case KeyBindingAction.ArrowUp:
case KeyBindingAction.ArrowLeft:
if ((action === KeyBindingAction.ArrowUp && handleUpDown) ||
(action === KeyBindingAction.ArrowLeft && handleLeftRight)
) {
handled = true;
if (context.state.refs.length > 0) {
const idx = context.state.refs.indexOf(context.state.activeRef);
focusRef = findSiblingElement(context.state.refs, idx - 1, true);
case KeyBindingAction.End:
if (handleHomeEnd) {
handled = true;
// move focus to last (visible) item
focusRef = findSiblingElement(context.state.refs, context.state.refs.length - 1, true);
}
}
break;
break;
case KeyBindingAction.ArrowDown:
case KeyBindingAction.ArrowRight:
if (
(action === KeyBindingAction.ArrowDown && handleUpDown) ||
(action === KeyBindingAction.ArrowRight && handleLeftRight)
) {
handled = true;
if (context.state.refs.length > 0) {
const idx = context.state.refs.indexOf(context.state.activeRef);
focusRef = findSiblingElement(context.state.refs, idx + 1);
}
}
break;
case KeyBindingAction.ArrowUp:
case KeyBindingAction.ArrowLeft:
if (
(action === KeyBindingAction.ArrowUp && handleUpDown) ||
(action === KeyBindingAction.ArrowLeft && handleLeftRight)
) {
handled = true;
if (context.state.refs.length > 0) {
const idx = context.state.refs.indexOf(context.state.activeRef);
focusRef = findSiblingElement(context.state.refs, idx - 1, true);
}
}
break;
}
}
}
if (handled) {
ev.preventDefault();
ev.stopPropagation();
}
if (handled) {
ev.preventDefault();
ev.stopPropagation();
}
if (focusRef) {
focusRef.current?.focus();
// programmatic focus doesn't fire the onFocus handler, so we must do the do ourselves
dispatch({
type: Type.SetFocus,
payload: {
ref: focusRef,
},
});
}
}, [context, onKeyDown, handleHomeEnd, handleUpDown, handleLeftRight]);
if (focusRef) {
focusRef.current?.focus();
// programmatic focus doesn't fire the onFocus handler, so we must do the do ourselves
dispatch({
type: Type.SetFocus,
payload: {
ref: focusRef,
},
});
}
},
[context, onKeyDown, handleHomeEnd, handleUpDown, handleLeftRight],
);
return <RovingTabIndexContext.Provider value={context}>
{ children({ onKeyDownHandler }) }
</RovingTabIndexContext.Provider>;
return (
<RovingTabIndexContext.Provider value={context}>
{children({ onKeyDownHandler })}
</RovingTabIndexContext.Provider>
);
};
// Hook to register a roving tab index

View file

@ -20,8 +20,7 @@ import { RovingTabIndexProvider } from "./RovingTabIndex";
import { getKeyBindingsManager } from "../KeyBindingsManager";
import { KeyBindingAction } from "./KeyboardShortcuts";
interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> {
}
interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> {}
// This component implements the Toolbar design pattern from the WAI-ARIA Authoring Practices guidelines.
// https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar
@ -39,7 +38,7 @@ const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
switch (action) {
case KeyBindingAction.ArrowUp:
case KeyBindingAction.ArrowDown:
if (target.hasAttribute('aria-haspopup')) {
if (target.hasAttribute("aria-haspopup")) {
target.click();
}
break;
@ -54,11 +53,15 @@ const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
}
};
return <RovingTabIndexProvider handleHomeEnd handleLeftRight onKeyDown={onKeyDown}>
{ ({ onKeyDownHandler }) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar">
{ children }
</div> }
</RovingTabIndexProvider>;
return (
<RovingTabIndexProvider handleHomeEnd handleLeftRight onKeyDown={onKeyDown}>
{({ onKeyDownHandler }) => (
<div {...props} onKeyDown={onKeyDownHandler} role="toolbar">
{children}
</div>
)}
</RovingTabIndexProvider>
);
};
export default Toolbar;

View file

@ -45,7 +45,7 @@ export const ContextMenuButton: React.FC<IProps> = ({
aria-haspopup={true}
aria-expanded={isExpanded}
>
{ children }
{children}
</AccessibleButton>
);
};

View file

@ -42,7 +42,7 @@ export const ContextMenuTooltipButton: React.FC<IProps> = ({
aria-expanded={isExpanded}
forceHide={isExpanded}
>
{ children }
{children}
</AccessibleTooltipButton>
);
};

View file

@ -24,7 +24,9 @@ interface IProps extends React.HTMLAttributes<HTMLDivElement> {
// Semantic component for representing a role=group for grouping menu radios/checkboxes
export const MenuGroup: React.FC<IProps> = ({ children, label, ...props }) => {
return <div {...props} role="group" aria-label={label}>
{ children }
</div>;
return (
<div {...props} role="group" aria-label={label}>
{children}
</div>
);
};

View file

@ -30,14 +30,16 @@ export const MenuItem: React.FC<IProps> = ({ children, label, tooltip, ...props
const ariaLabel = props["aria-label"] || label;
if (tooltip) {
return <RovingAccessibleTooltipButton {...props} role="menuitem" aria-label={ariaLabel} title={tooltip}>
{ children }
</RovingAccessibleTooltipButton>;
return (
<RovingAccessibleTooltipButton {...props} role="menuitem" aria-label={ariaLabel} title={tooltip}>
{children}
</RovingAccessibleTooltipButton>
);
}
return (
<RovingAccessibleButton {...props} role="menuitem" aria-label={ariaLabel}>
{ children }
{children}
</RovingAccessibleButton>
);
};

View file

@ -36,7 +36,7 @@ export const MenuItemCheckbox: React.FC<IProps> = ({ children, label, active, di
disabled={disabled}
aria-label={label}
>
{ children }
{children}
</RovingAccessibleButton>
);
};

View file

@ -36,7 +36,7 @@ export const MenuItemRadio: React.FC<IProps> = ({ children, label, active, disab
disabled={disabled}
aria-label={label}
>
{ children }
{children}
</RovingAccessibleButton>
);
};

View file

@ -79,7 +79,7 @@ export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, label, onCh
inputRef={ref}
tabIndex={isActive ? 0 : -1}
>
{ children }
{children}
</StyledCheckbox>
);
};

View file

@ -79,7 +79,7 @@ export const StyledMenuItemRadio: React.FC<IProps> = ({ children, label, onChang
inputRef={ref}
tabIndex={isActive ? 0 : -1}
>
{ children }
{children}
</StyledRadioButton>
);
};

View file

@ -27,14 +27,15 @@ interface IProps extends Omit<React.ComponentProps<typeof AccessibleButton>, "in
// Wrapper to allow use of useRovingTabIndex for simple AccessibleButtons outside of React Functional Components.
export const RovingAccessibleButton: React.FC<IProps> = ({ inputRef, onFocus, ...props }) => {
const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef);
return <AccessibleButton
{...props}
onFocus={event => {
onFocusInternal();
onFocus?.(event);
}}
inputRef={ref}
tabIndex={isActive ? 0 : -1}
/>;
return (
<AccessibleButton
{...props}
onFocus={(event) => {
onFocusInternal();
onFocus?.(event);
}}
inputRef={ref}
tabIndex={isActive ? 0 : -1}
/>
);
};

View file

@ -28,14 +28,15 @@ interface IProps extends Omit<ATBProps, "inputRef" | "tabIndex"> {
// Wrapper to allow use of useRovingTabIndex for simple AccessibleTooltipButtons outside of React Functional Components.
export const RovingAccessibleTooltipButton: React.FC<IProps> = ({ inputRef, onFocus, ...props }) => {
const [onFocusInternal, isActive, ref] = useRovingTabIndex(inputRef);
return <AccessibleTooltipButton
{...props}
onFocus={event => {
onFocusInternal();
onFocus?.(event);
}}
inputRef={ref}
tabIndex={isActive ? 0 : -1}
/>;
return (
<AccessibleTooltipButton
{...props}
onFocus={(event) => {
onFocusInternal();
onFocus?.(event);
}}
inputRef={ref}
tabIndex={isActive ? 0 : -1}
/>
);
};

View file

@ -21,11 +21,7 @@ import { FocusHandler, Ref } from "./types";
interface IProps {
inputRef?: Ref;
children(renderProps: {
onFocus: FocusHandler;
isActive: boolean;
ref: Ref;
});
children(renderProps: { onFocus: FocusHandler; isActive: boolean; ref: Ref });
}
// Wrapper to allow use of useRovingTabIndex outside of React Functional Components.

View file

@ -34,7 +34,7 @@ import { ActionPayload } from "../dispatcher/payloads";
*/
function createSyncAction(matrixClient: MatrixClient, state: string, prevState: string): ActionPayload {
return {
action: 'MatrixActions.sync',
action: "MatrixActions.sync",
state,
prevState,
matrixClient,
@ -60,7 +60,7 @@ function createSyncAction(matrixClient: MatrixClient, state: string, prevState:
*/
function createAccountDataAction(matrixClient: MatrixClient, accountDataEvent: MatrixEvent): ActionPayload {
return {
action: 'MatrixActions.accountData',
action: "MatrixActions.accountData",
event: accountDataEvent,
event_type: accountDataEvent.getType(),
event_content: accountDataEvent.getContent(),
@ -92,7 +92,7 @@ function createRoomAccountDataAction(
room: Room,
): ActionPayload {
return {
action: 'MatrixActions.Room.accountData',
action: "MatrixActions.Room.accountData",
event: accountDataEvent,
event_type: accountDataEvent.getType(),
event_content: accountDataEvent.getContent(),
@ -116,7 +116,7 @@ function createRoomAccountDataAction(
* @returns {RoomAction} an action of type `MatrixActions.Room`.
*/
function createRoomAction(matrixClient: MatrixClient, room: Room): ActionPayload {
return { action: 'MatrixActions.Room', room };
return { action: "MatrixActions.Room", room };
}
/**
@ -137,7 +137,7 @@ function createRoomAction(matrixClient: MatrixClient, room: Room): ActionPayload
* @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`.
*/
function createRoomTagsAction(matrixClient: MatrixClient, roomTagsEvent: MatrixEvent, room: Room): ActionPayload {
return { action: 'MatrixActions.Room.tags', room };
return { action: "MatrixActions.Room.tags", room };
}
/**
@ -151,7 +151,7 @@ function createRoomTagsAction(matrixClient: MatrixClient, roomTagsEvent: MatrixE
*/
function createRoomReceiptAction(matrixClient: MatrixClient, event: MatrixEvent, room: Room): ActionPayload {
return {
action: 'MatrixActions.Room.receipt',
action: "MatrixActions.Room.receipt",
event,
room,
matrixClient,
@ -169,7 +169,7 @@ function createRoomReceiptAction(matrixClient: MatrixClient, event: MatrixEvent,
* @property {Room} room the Room whose tags changed.
*/
export interface IRoomTimelineActionPayload extends Pick<ActionPayload, "action"> {
action: 'MatrixActions.Room.timeline';
action: "MatrixActions.Room.timeline";
event: MatrixEvent;
room: Room | null;
isLiveEvent?: boolean;
@ -185,7 +185,7 @@ export interface IRoomTimelineActionPayload extends Pick<ActionPayload, "action"
* @property {MatrixEvent | null} lastStateEvent the previous value for this (event-type, state-key) tuple in room state
*/
export interface IRoomStateEventsActionPayload extends Pick<ActionPayload, "action"> {
action: 'MatrixActions.RoomState.events';
action: "MatrixActions.RoomState.events";
event: MatrixEvent;
state: RoomState;
lastStateEvent: MatrixEvent | null;
@ -218,7 +218,7 @@ function createRoomTimelineAction(
data: IRoomTimelineData,
): IRoomTimelineActionPayload {
return {
action: 'MatrixActions.Room.timeline',
action: "MatrixActions.Room.timeline",
event: timelineEvent,
isLiveEvent: data.liveEvent,
isLiveUnfilteredRoomTimelineEvent: room && data.timeline.getTimelineSet() === room.getUnfilteredTimelineSet(),
@ -244,7 +244,7 @@ function createRoomStateEventsAction(
lastStateEvent: MatrixEvent | null,
): IRoomStateEventsActionPayload {
return {
action: 'MatrixActions.RoomState.events',
action: "MatrixActions.RoomState.events",
event,
state,
lastStateEvent,
@ -277,7 +277,7 @@ function createSelfMembershipAction(
membership: string,
oldMembership: string,
): ActionPayload {
return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership };
return { action: "MatrixActions.Room.myMembership", room, membership, oldMembership };
}
/**
@ -297,7 +297,7 @@ function createSelfMembershipAction(
* @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`.
*/
function createEventDecryptedAction(matrixClient: MatrixClient, event: MatrixEvent): ActionPayload {
return { action: 'MatrixActions.Event.decrypted', event };
return { action: "MatrixActions.Event.decrypted", event };
}
type Listener = () => void;

View file

@ -19,15 +19,15 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import { asyncAction } from './actionCreators';
import Modal from '../Modal';
import * as Rooms from '../Rooms';
import { _t } from '../languageHandler';
import { asyncAction } from "./actionCreators";
import Modal from "../Modal";
import * as Rooms from "../Rooms";
import { _t } from "../languageHandler";
import { AsyncActionPayload } from "../dispatcher/payloads";
import RoomListStore from "../stores/room-list/RoomListStore";
import { SortAlgorithm } from "../stores/room-list/algorithms/models";
import { DefaultTagID } from "../stores/room-list/models";
import ErrorDialog from '../components/views/dialogs/ErrorDialog';
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
export default class RoomListActions {
/**
@ -47,9 +47,12 @@ export default class RoomListActions {
* @see asyncAction
*/
public static tagRoom(
matrixClient: MatrixClient, room: Room,
oldTag: string, newTag: string,
oldIndex: number | null, newIndex: number | null,
matrixClient: MatrixClient,
room: Room,
oldTag: string,
newTag: string,
oldIndex: number | null,
newIndex: number | null,
): AsyncActionPayload {
let metaData = null;
@ -62,91 +65,87 @@ export default class RoomListActions {
// If the room was moved "down" (increasing index) in the same list we
// need to use the orders of the tiles with indices shifted by +1
const offset = (
newTag === oldTag && oldIndex < newIndex
) ? 1 : 0;
const offset = newTag === oldTag && oldIndex < newIndex ? 1 : 0;
const indexBefore = offset + newIndex - 1;
const indexAfter = offset + newIndex;
const prevOrder = indexBefore <= 0 ?
0 : newList[indexBefore].tags[newTag].order;
const nextOrder = indexAfter >= newList.length ?
1 : newList[indexAfter].tags[newTag].order;
const prevOrder = indexBefore <= 0 ? 0 : newList[indexBefore].tags[newTag].order;
const nextOrder = indexAfter >= newList.length ? 1 : newList[indexAfter].tags[newTag].order;
metaData = {
order: (prevOrder + nextOrder) / 2.0,
};
}
return asyncAction('RoomListActions.tagRoom', () => {
const promises = [];
const roomId = room.roomId;
return asyncAction(
"RoomListActions.tagRoom",
() => {
const promises = [];
const roomId = room.roomId;
// Evil hack to get DMs behaving
if ((oldTag === undefined && newTag === DefaultTagID.DM) ||
(oldTag === DefaultTagID.DM && newTag === undefined)
) {
return Rooms.guessAndSetDMRoom(
room, newTag === DefaultTagID.DM,
).catch((err) => {
logger.error("Failed to set DM tag " + err);
Modal.createDialog(ErrorDialog, {
title: _t('Failed to set direct message tag'),
description: ((err && err.message) ? err.message : _t('Operation failed')),
// Evil hack to get DMs behaving
if (
(oldTag === undefined && newTag === DefaultTagID.DM) ||
(oldTag === DefaultTagID.DM && newTag === undefined)
) {
return Rooms.guessAndSetDMRoom(room, newTag === DefaultTagID.DM).catch((err) => {
logger.error("Failed to set DM tag " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Failed to set direct message tag"),
description: err && err.message ? err.message : _t("Operation failed"),
});
});
});
}
}
const hasChangedSubLists = oldTag !== newTag;
const hasChangedSubLists = oldTag !== newTag;
// More evilness: We will still be dealing with moving to favourites/low prio,
// but we avoid ever doing a request with TAG_DM.
//
// if we moved lists, remove the old tag
if (oldTag && oldTag !== DefaultTagID.DM &&
hasChangedSubLists
) {
const promiseToDelete = matrixClient.deleteRoomTag(
roomId, oldTag,
).catch(function(err) {
logger.error("Failed to remove tag " + oldTag + " from room: " + err);
Modal.createDialog(ErrorDialog, {
title: _t('Failed to remove tag %(tagName)s from room', { tagName: oldTag }),
description: ((err && err.message) ? err.message : _t('Operation failed')),
});
});
promises.push(promiseToDelete);
}
// if we moved lists or the ordering changed, add the new tag
if (newTag && newTag !== DefaultTagID.DM &&
(hasChangedSubLists || metaData)
) {
// metaData is the body of the PUT to set the tag, so it must
// at least be an empty object.
metaData = metaData || {};
const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) {
logger.error("Failed to add tag " + newTag + " to room: " + err);
Modal.createDialog(ErrorDialog, {
title: _t('Failed to add tag %(tagName)s to room', { tagName: newTag }),
description: ((err && err.message) ? err.message : _t('Operation failed')),
// More evilness: We will still be dealing with moving to favourites/low prio,
// but we avoid ever doing a request with TAG_DM.
//
// if we moved lists, remove the old tag
if (oldTag && oldTag !== DefaultTagID.DM && hasChangedSubLists) {
const promiseToDelete = matrixClient.deleteRoomTag(roomId, oldTag).catch(function (err) {
logger.error("Failed to remove tag " + oldTag + " from room: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Failed to remove tag %(tagName)s from room", { tagName: oldTag }),
description: err && err.message ? err.message : _t("Operation failed"),
});
});
throw err;
});
promises.push(promiseToDelete);
}
promises.push(promiseToAdd);
}
// if we moved lists or the ordering changed, add the new tag
if (newTag && newTag !== DefaultTagID.DM && (hasChangedSubLists || metaData)) {
// metaData is the body of the PUT to set the tag, so it must
// at least be an empty object.
metaData = metaData || {};
return Promise.all(promises);
}, () => {
// For an optimistic update
return {
room, oldTag, newTag, metaData,
};
});
const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function (err) {
logger.error("Failed to add tag " + newTag + " to room: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Failed to add tag %(tagName)s to room", { tagName: newTag }),
description: err && err.message ? err.message : _t("Operation failed"),
});
throw err;
});
promises.push(promiseToAdd);
}
return Promise.all(promises);
},
() => {
// For an optimistic update
return {
room,
oldTag,
newTag,
metaData,
};
},
);
}
}

View file

@ -47,14 +47,16 @@ import { AsyncActionPayload } from "../dispatcher/payloads";
export function asyncAction(id: string, fn: () => Promise<any>, pendingFn: () => any | null): AsyncActionPayload {
const helper = (dispatch) => {
dispatch({
action: id + '.pending',
request: typeof pendingFn === 'function' ? pendingFn() : undefined,
});
fn().then((result) => {
dispatch({ action: id + '.success', result });
}).catch((err) => {
dispatch({ action: id + '.failure', err });
action: id + ".pending",
request: typeof pendingFn === "function" ? pendingFn() : undefined,
});
fn()
.then((result) => {
dispatch({ action: id + ".success", result });
})
.catch((err) => {
dispatch({ action: id + ".failure", err });
});
};
return new AsyncActionPayload(helper);
}

View file

@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Spinner from "../../../../components/views/elements/Spinner";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import dis from "../../../../dispatcher/dispatcher";
import { _t } from '../../../../languageHandler';
import { _t } from "../../../../languageHandler";
import SettingsStore from "../../../../settings/SettingsStore";
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
import { Action } from "../../../../dispatcher/actions";
@ -50,7 +50,7 @@ export default class DisableEventIndexDialog extends React.Component<IProps, ISt
disabling: true,
});
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
await SettingsStore.setValue("enableEventIndexing", null, SettingLevel.DEVICE, false);
await EventIndexPeg.deleteEventIndex();
this.props.onFinished(true);
dis.fire(Action.ViewUserSettings);
@ -59,10 +59,10 @@ export default class DisableEventIndexDialog extends React.Component<IProps, ISt
public render(): React.ReactNode {
return (
<BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}>
{ _t("If disabled, messages from encrypted rooms won't appear in search results.") }
{ this.state.disabling ? <Spinner /> : <div /> }
{_t("If disabled, messages from encrypted rooms won't appear in search results.")}
{this.state.disabling ? <Spinner /> : <div />}
<DialogButtons
primaryButton={_t('Disable')}
primaryButton={_t("Disable")}
onPrimaryButtonClick={this.onDisable}
primaryButtonClass="danger"
cancelButtonClass="warning"

View file

@ -14,16 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { _t } from '../../../../languageHandler';
import SdkConfig from '../../../../SdkConfig';
import { _t } from "../../../../languageHandler";
import SdkConfig from "../../../../SdkConfig";
import SettingsStore from "../../../../settings/SettingsStore";
import Modal from '../../../../Modal';
import Modal from "../../../../Modal";
import { formatBytes, formatCountLong } from "../../../../utils/FormattingUtils";
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
import { SettingLevel } from "../../../../settings/SettingLevel";
import Field from '../../../../components/views/elements/Field';
import Field from "../../../../components/views/elements/Field";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
@ -52,8 +52,7 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
crawlingRoomsCount: 0,
roomCount: 0,
currentRoom: null,
crawlerSleepTime:
SettingsStore.getValueAt(SettingLevel.DEVICE, 'crawlerSleepTime'),
crawlerSleepTime: SettingsStore.getValueAt(SettingLevel.DEVICE, "crawlerSleepTime"),
};
}
@ -149,43 +148,48 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
if (this.state.currentRoom === null) {
crawlerState = _t("Not currently indexing messages for any room.");
} else {
crawlerState = (
_t("Currently indexing: %(currentRoom)s", { currentRoom: this.state.currentRoom })
);
crawlerState = _t("Currently indexing: %(currentRoom)s", { currentRoom: this.state.currentRoom });
}
const doneRooms = Math.max(0, (this.state.roomCount - this.state.crawlingRoomsCount));
const doneRooms = Math.max(0, this.state.roomCount - this.state.crawlingRoomsCount);
const eventIndexingSettings = (
<div>
{ _t(
{_t(
"%(brand)s is securely caching encrypted messages locally for them " +
"to appear in search results:",
"to appear in search results:",
{ brand },
) }
<div className='mx_SettingsTab_subsectionText'>
{ crawlerState }<br />
{ _t("Space used:") } { formatBytes(this.state.eventIndexSize, 0) }<br />
{ _t("Indexed messages:") } { formatCountLong(this.state.eventCount) }<br />
{ _t("Indexed rooms:") } { _t("%(doneRooms)s out of %(totalRooms)s", {
)}
<div className="mx_SettingsTab_subsectionText">
{crawlerState}
<br />
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}
<br />
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}
<br />
{_t("Indexed rooms:")}{" "}
{_t("%(doneRooms)s out of %(totalRooms)s", {
doneRooms: formatCountLong(doneRooms),
totalRooms: formatCountLong(this.state.roomCount),
}) } <br />
})}{" "}
<br />
<Field
label={_t('Message downloading sleep time(ms)')}
type='number'
label={_t("Message downloading sleep time(ms)")}
type="number"
value={this.state.crawlerSleepTime.toString()}
onChange={this.onCrawlerSleepTimeChange} />
onChange={this.onCrawlerSleepTimeChange}
/>
</div>
</div>
);
return (
<BaseDialog className='mx_ManageEventIndexDialog'
<BaseDialog
className="mx_ManageEventIndexDialog"
onFinished={this.props.onFinished}
title={_t("Message search")}
>
{ eventIndexingSettings }
{eventIndexingSettings}
<DialogButtons
primaryButton={_t("Done")}
onPrimaryButtonClick={this.props.onFinished}

View file

@ -15,14 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import FileSaver from 'file-saver';
import React, { createRef } from "react";
import FileSaver from "file-saver";
import { IPreparedKeyBackupVersion } from "matrix-js-sdk/src/crypto/backup";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { _t, _td } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../SecurityManager';
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import { _t, _td } from "../../../../languageHandler";
import { accessSecretStorage } from "../../../../SecurityManager";
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import { copyNode } from "../../../../utils/strings";
import PassphraseField from "../../../../components/views/auth/PassphraseField";
@ -73,9 +73,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
this.state = {
secureSecretStorage: null,
phase: Phase.Passphrase,
passPhrase: '',
passPhrase: "",
passPhraseValid: false,
passPhraseConfirm: '',
passPhraseConfirm: "",
copied: false,
downloaded: false,
};
@ -106,9 +106,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
private onDownloadClick = (): void => {
const blob = new Blob([this.keyBackupInfo.recovery_key], {
type: 'text/plain;charset=us-ascii',
type: "text/plain;charset=us-ascii",
});
FileSaver.saveAs(blob, 'security-key.txt');
FileSaver.saveAs(blob, "security-key.txt");
this.setState({
downloaded: true,
@ -126,16 +126,13 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
try {
if (secureSecretStorage) {
await accessSecretStorage(async () => {
info = await MatrixClientPeg.get().prepareKeyBackupVersion(
null /* random key */,
{ secureSecretStorage: true },
);
info = await MatrixClientPeg.get().prepareKeyBackupVersion(null /* random key */, {
secureSecretStorage: true,
});
info = await MatrixClientPeg.get().createKeyBackupVersion(info);
});
} else {
info = await MatrixClientPeg.get().createKeyBackupVersion(
this.keyBackupInfo,
);
info = await MatrixClientPeg.get().createKeyBackupVersion(this.keyBackupInfo);
}
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
this.setState({
@ -206,9 +203,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
private onSetAgainClick = (): void => {
this.setState({
passPhrase: '',
passPhrase: "",
passPhraseValid: false,
passPhraseConfirm: '',
passPhraseConfirm: "",
phase: Phase.Passphrase,
});
};
@ -238,49 +235,56 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
};
private renderPhasePassPhrase(): JSX.Element {
return <form onSubmit={this.onPassPhraseNextClick}>
<p>{ _t(
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
{ b: sub => <b>{ sub }</b> },
) }</p>
<p>{ _t(
"We'll store an encrypted copy of your keys on our server. " +
"Secure your backup with a Security Phrase.",
) }</p>
<p>{ _t("For maximum security, this should be different from your account password.") }</p>
return (
<form onSubmit={this.onPassPhraseNextClick}>
<p>
{_t(
"<b>Warning</b>: You should only set up key backup from a trusted computer.",
{},
{ b: (sub) => <b>{sub}</b> },
)}
</p>
<p>
{_t(
"We'll store an encrypted copy of your keys on our server. " +
"Secure your backup with a Security Phrase.",
)}
</p>
<p>{_t("For maximum security, this should be different from your account password.")}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<PassphraseField
className="mx_CreateKeyBackupDialog_passPhraseInput"
onChange={this.onPassPhraseChange}
minScore={PASSWORD_MIN_SCORE}
value={this.state.passPhrase}
onValidate={this.onPassPhraseValidate}
fieldRef={this.passphraseField}
autoFocus={true}
label={_td("Enter a Security Phrase")}
labelEnterPassword={_td("Enter a Security Phrase")}
labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")}
/>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<PassphraseField
className="mx_CreateKeyBackupDialog_passPhraseInput"
onChange={this.onPassPhraseChange}
minScore={PASSWORD_MIN_SCORE}
value={this.state.passPhrase}
onValidate={this.onPassPhraseValidate}
fieldRef={this.passphraseField}
autoFocus={true}
label={_td("Enter a Security Phrase")}
labelEnterPassword={_td("Enter a Security Phrase")}
labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")}
/>
</div>
</div>
</div>
<DialogButtons
primaryButton={_t('Next')}
onPrimaryButtonClick={this.onPassPhraseNextClick}
hasCancel={false}
disabled={!this.state.passPhraseValid}
/>
<DialogButtons
primaryButton={_t("Next")}
onPrimaryButtonClick={this.onPassPhraseNextClick}
hasCancel={false}
disabled={!this.state.passPhraseValid}
/>
<details>
<summary>{ _t("Advanced") }</summary>
<AccessibleButton kind='primary' onClick={this.onSkipPassPhraseClick}>
{ _t("Set up with a Security Key") }
</AccessibleButton>
</details>
</form>;
<details>
<summary>{_t("Advanced")}</summary>
<AccessibleButton kind="primary" onClick={this.onSkipPassPhraseClick}>
{_t("Set up with a Security Key")}
</AccessibleButton>
</details>
</form>
);
}
private renderPhasePassPhraseConfirm(): JSX.Element {
@ -303,68 +307,71 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
let passPhraseMatch = null;
if (matchText) {
passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
<div>{ matchText }</div>
<AccessibleButton kind="link" onClick={this.onSetAgainClick}>
{ changeText }
</AccessibleButton>
</div>;
}
return <form onSubmit={this.onPassPhraseConfirmNextClick}>
<p>{ _t(
"Enter your Security Phrase a second time to confirm it.",
) }</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<div>
<input type="password"
onChange={this.onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Repeat your Security Phrase...")}
autoFocus={true}
/>
</div>
{ passPhraseMatch }
passPhraseMatch = (
<div className="mx_CreateKeyBackupDialog_passPhraseMatch">
<div>{matchText}</div>
<AccessibleButton kind="link" onClick={this.onSetAgainClick}>
{changeText}
</AccessibleButton>
</div>
</div>
<DialogButtons
primaryButton={_t('Next')}
onPrimaryButtonClick={this.onPassPhraseConfirmNextClick}
hasCancel={false}
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
/>
</form>;
);
}
return (
<form onSubmit={this.onPassPhraseConfirmNextClick}>
<p>{_t("Enter your Security Phrase a second time to confirm it.")}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<div>
<input
type="password"
onChange={this.onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Repeat your Security Phrase...")}
autoFocus={true}
/>
</div>
{passPhraseMatch}
</div>
</div>
<DialogButtons
primaryButton={_t("Next")}
onPrimaryButtonClick={this.onPassPhraseConfirmNextClick}
hasCancel={false}
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
/>
</form>
);
}
private renderPhaseShowKey(): JSX.Element {
return <div>
<p>{ _t(
"Your Security Key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your Security Phrase.",
) }</p>
<p>{ _t(
"Keep a copy of it somewhere secure, like a password manager or even a safe.",
) }</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
{ _t("Your Security Key") }
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this.recoveryKeyNode}>{ this.keyBackupInfo.recovery_key }</code>
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
<button className="mx_Dialog_primary" onClick={this.onCopyClick}>
{ _t("Copy") }
</button>
<button className="mx_Dialog_primary" onClick={this.onDownloadClick}>
{ _t("Download") }
</button>
return (
<div>
<p>
{_t(
"Your Security Key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your Security Phrase.",
)}
</p>
<p>{_t("Keep a copy of it somewhere secure, like a password manager or even a safe.")}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">{_t("Your Security Key")}</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this.recoveryKeyNode}>{this.keyBackupInfo.recovery_key}</code>
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
<button className="mx_Dialog_primary" onClick={this.onCopyClick}>
{_t("Copy")}
</button>
<button className="mx_Dialog_primary" onClick={this.onDownloadClick}>
{_t("Download")}
</button>
</div>
</div>
</div>
</div>
</div>;
);
}
private renderPhaseKeepItSafe(): JSX.Element {
@ -372,77 +379,81 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
if (this.state.copied) {
introText = _t(
"Your Security Key has been <b>copied to your clipboard</b>, paste it to:",
{}, { b: s => <b>{ s }</b> },
{},
{ b: (s) => <b>{s}</b> },
);
} else if (this.state.downloaded) {
introText = _t(
"Your Security Key is in your <b>Downloads</b> folder.",
{}, { b: s => <b>{ s }</b> },
);
introText = _t("Your Security Key is in your <b>Downloads</b> folder.", {}, { b: (s) => <b>{s}</b> });
}
return <div>
{ introText }
<ul>
<li>{ _t("<b>Print it</b> and store it somewhere safe", {}, { b: s => <b>{ s }</b> }) }</li>
<li>{ _t("<b>Save it</b> on a USB key or backup drive", {}, { b: s => <b>{ s }</b> }) }</li>
<li>{ _t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{ s }</b> }) }</li>
</ul>
<DialogButtons primaryButton={_t("Continue")}
onPrimaryButtonClick={this.createBackup}
hasCancel={false}>
<button onClick={this.onKeepItSafeBackClick}>{ _t("Back") }</button>
</DialogButtons>
</div>;
return (
<div>
{introText}
<ul>
<li>{_t("<b>Print it</b> and store it somewhere safe", {}, { b: (s) => <b>{s}</b> })}</li>
<li>{_t("<b>Save it</b> on a USB key or backup drive", {}, { b: (s) => <b>{s}</b> })}</li>
<li>{_t("<b>Copy it</b> to your personal cloud storage", {}, { b: (s) => <b>{s}</b> })}</li>
</ul>
<DialogButtons
primaryButton={_t("Continue")}
onPrimaryButtonClick={this.createBackup}
hasCancel={false}
>
<button onClick={this.onKeepItSafeBackClick}>{_t("Back")}</button>
</DialogButtons>
</div>
);
}
private renderBusyPhase(): JSX.Element {
return <div>
<Spinner />
</div>;
return (
<div>
<Spinner />
</div>
);
}
private renderPhaseDone(): JSX.Element {
return <div>
<p>{ _t(
"Your keys are being backed up (the first backup could take a few minutes).",
) }</p>
<DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this.onDone}
hasCancel={false}
/>
</div>;
return (
<div>
<p>{_t("Your keys are being backed up (the first backup could take a few minutes).")}</p>
<DialogButtons primaryButton={_t("OK")} onPrimaryButtonClick={this.onDone} hasCancel={false} />
</div>
);
}
private renderPhaseOptOutConfirm(): JSX.Element {
return <div>
{ _t(
"Without setting up Secure Message Recovery, you won't be able to restore your " +
"encrypted message history if you log out or use another session.",
) }
<DialogButtons primaryButton={_t('Set up Secure Message Recovery')}
onPrimaryButtonClick={this.onSetUpClick}
hasCancel={false}
>
<button onClick={this.onCancel}>I understand, continue without</button>
</DialogButtons>
</div>;
return (
<div>
{_t(
"Without setting up Secure Message Recovery, you won't be able to restore your " +
"encrypted message history if you log out or use another session.",
)}
<DialogButtons
primaryButton={_t("Set up Secure Message Recovery")}
onPrimaryButtonClick={this.onSetUpClick}
hasCancel={false}
>
<button onClick={this.onCancel}>I understand, continue without</button>
</DialogButtons>
</div>
);
}
private titleForPhase(phase: Phase): string {
switch (phase) {
case Phase.Passphrase:
return _t('Secure your backup with a Security Phrase');
return _t("Secure your backup with a Security Phrase");
case Phase.PassphraseConfirm:
return _t('Confirm your Security Phrase');
return _t("Confirm your Security Phrase");
case Phase.OptOutConfirm:
return _t('Warning!');
return _t("Warning!");
case Phase.ShowKey:
case Phase.KeepItSafe:
return _t('Make a copy of your Security Key');
return _t("Make a copy of your Security Key");
case Phase.BackingUp:
return _t('Starting backup...');
return _t("Starting backup...");
case Phase.Done:
return _t('Success!');
return _t("Success!");
default:
return _t("Create key backup");
}
@ -451,15 +462,17 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
public render(): JSX.Element {
let content;
if (this.state.error) {
content = <div>
<p>{ _t("Unable to create key backup") }</p>
<DialogButtons
primaryButton={_t('Retry')}
onPrimaryButtonClick={this.createBackup}
hasCancel={true}
onCancel={this.onCancel}
/>
</div>;
content = (
<div>
<p>{_t("Unable to create key backup")}</p>
<DialogButtons
primaryButton={_t("Retry")}
onPrimaryButtonClick={this.createBackup}
hasCancel={true}
onCancel={this.onCancel}
/>
</div>
);
} else {
switch (this.state.phase) {
case Phase.Passphrase:
@ -487,14 +500,13 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
}
return (
<BaseDialog className='mx_CreateKeyBackupDialog'
<BaseDialog
className="mx_CreateKeyBackupDialog"
onFinished={this.props.onFinished}
title={this.titleForPhase(this.state.phase)}
hasCancel={[Phase.Passphrase, Phase.Done].includes(this.state.phase)}
>
<div>
{ content }
</div>
<div>{content}</div>
</BaseDialog>
);
}

View file

@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import FileSaver from 'file-saver';
import React, { createRef } from "react";
import FileSaver from "file-saver";
import { logger } from "matrix-js-sdk/src/logger";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
@ -24,14 +24,14 @@ import { CrossSigningKeys } from "matrix-js-sdk/src/matrix";
import { IRecoveryKey } from "matrix-js-sdk/src/crypto/api";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { _t, _td } from '../../../../languageHandler';
import Modal from '../../../../Modal';
import { promptForBackupPassphrase } from '../../../../SecurityManager';
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import { _t, _td } from "../../../../languageHandler";
import Modal from "../../../../Modal";
import { promptForBackupPassphrase } from "../../../../SecurityManager";
import { copyNode } from "../../../../utils/strings";
import { SSOAuthEntry } from "../../../../components/views/auth/InteractiveAuthEntryComponents";
import PassphraseField from "../../../../components/views/auth/PassphraseField";
import StyledRadioButton from '../../../../components/views/elements/StyledRadioButton';
import StyledRadioButton from "../../../../components/views/elements/StyledRadioButton";
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
@ -40,7 +40,7 @@ import {
getSecureBackupSetupMethods,
isSecureBackupRequired,
SecureBackupSetupMethod,
} from '../../../../utils/WellKnownUtils';
} from "../../../../utils/WellKnownUtils";
import SecurityCustomisations from "../../../../customisations/Security";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import Field from "../../../../components/views/elements/Field";
@ -129,9 +129,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
this.state = {
phase: Phase.Loading,
passPhrase: '',
passPhrase: "",
passPhraseValid: false,
passPhraseConfirm: '',
passPhraseConfirm: "",
copied: false,
downloaded: false,
setPassphrase: false,
@ -169,16 +169,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
this.fetchBackupInfo();
}
private async fetchBackupInfo(): Promise<{ backupInfo: IKeyBackupInfo, backupSigStatus: TrustInfo }> {
private async fetchBackupInfo(): Promise<{ backupInfo: IKeyBackupInfo; backupSigStatus: TrustInfo }> {
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
const backupSigStatus = (
const backupSigStatus =
// we may not have started crypto yet, in which case we definitely don't trust the backup
MatrixClientPeg.get().isCryptoEnabled() && (await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo))
);
MatrixClientPeg.get().isCryptoEnabled() && (await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo));
const { forceReset } = this.props;
const phase = (backupInfo && !forceReset) ? Phase.Migrate : Phase.ChooseKeyPassphrase;
const phase = backupInfo && !forceReset ? Phase.Migrate : Phase.ChooseKeyPassphrase;
this.setState({
phase,
@ -207,8 +206,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
logger.log("uploadDeviceSigningKeys advertised no flows!");
return;
}
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {
return f.stages.length === 1 && f.stages[0] === 'm.login.password';
const canUploadKeysWithPasswordOnly = error.data.flows.some((f) => {
return f.stages.length === 1 && f.stages[0] === "m.login.password";
});
this.setState({
canUploadKeysWithPasswordOnly,
@ -228,8 +227,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private onChooseKeyPassphraseFormSubmit = async (): Promise<void> => {
if (this.state.passPhraseKeySelected === SecureBackupSetupMethod.Key) {
this.recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
this.recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
this.setState({
copied: false,
downloaded: false,
@ -265,9 +263,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private onDownloadClick = (): void => {
const blob = new Blob([this.recoveryKey.encodedPrivateKey], {
type: 'text/plain;charset=us-ascii',
type: "text/plain;charset=us-ascii",
});
FileSaver.saveAs(blob, 'security-key.txt');
FileSaver.saveAs(blob, "security-key.txt");
this.setState({
downloaded: true,
@ -277,9 +275,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private doBootstrapUIAuth = async (makeRequest: (authData: any) => Promise<{}>): Promise<void> => {
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
await makeRequest({
type: 'm.login.password',
type: "m.login.password",
identifier: {
type: 'm.id.user',
type: "m.id.user",
user: MatrixClientPeg.get().getUserId(),
},
// TODO: Remove `user` once servers support proper UIA
@ -367,7 +365,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
} catch (e) {
if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) {
this.setState({
accountPassword: '',
accountPassword: "",
accountPasswordCorrect: false,
phase: Phase.Migrate,
});
@ -385,20 +383,22 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private restoreBackup = async (): Promise<void> => {
// It's possible we'll need the backup key later on for bootstrapping,
// so let's stash it here, rather than prompting for it twice.
const keyCallback = k => this.backupKey = k;
const keyCallback = (k) => (this.backupKey = k);
const { finished } = Modal.createDialog(RestoreKeyBackupDialog, {
showSummary: false,
keyCallback,
}, null, /* priority = */ false, /* static = */ false);
const { finished } = Modal.createDialog(
RestoreKeyBackupDialog,
{
showSummary: false,
keyCallback,
},
null,
/* priority = */ false,
/* static = */ false,
);
await finished;
const { backupSigStatus } = await this.fetchBackupInfo();
if (
backupSigStatus.usable &&
this.state.canUploadKeysWithPasswordOnly &&
this.state.accountPassword
) {
if (backupSigStatus.usable && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
this.bootstrapSecretStorage();
}
};
@ -439,8 +439,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
this.recoveryKey =
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
this.recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
this.setState({
copied: false,
downloaded: false,
@ -451,9 +450,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private onSetAgainClick = (): void => {
this.setState({
passPhrase: '',
passPhrase: "",
passPhraseValid: false,
passPhraseConfirm: '',
passPhraseConfirm: "",
phase: Phase.Passphrase,
});
};
@ -494,9 +493,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
>
<div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup" />
{ _t("Generate a Security Key") }
{_t("Generate a Security Key")}
</div>
<div>
{_t(
"We'll generate a Security Key for you to store somewhere safe, like a password manager or a safe.",
)}
</div>
<div>{ _t("We'll generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }</div>
</StyledRadioButton>
);
}
@ -513,9 +516,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
>
<div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase" />
{ _t("Enter a Security Phrase") }
{_t("Enter a Security Phrase")}
</div>
<div>
{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}
</div>
<div>{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }</div>
</StyledRadioButton>
);
}
@ -527,22 +532,26 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
? this.renderOptionPassphrase()
: null;
return <form onSubmit={this.onChooseKeyPassphraseFormSubmit}>
<p className="mx_CreateSecretStorageDialog_centeredBody">{ _t(
"Safeguard against losing access to encrypted messages & data by " +
"backing up encryption keys on your server.",
) }</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
{ optionKey }
{ optionPassphrase }
</div>
<DialogButtons
primaryButton={_t("Continue")}
onPrimaryButtonClick={this.onChooseKeyPassphraseFormSubmit}
onCancel={this.onCancelClick}
hasCancel={this.state.canSkip}
/>
</form>;
return (
<form onSubmit={this.onChooseKeyPassphraseFormSubmit}>
<p className="mx_CreateSecretStorageDialog_centeredBody">
{_t(
"Safeguard against losing access to encrypted messages & data by " +
"backing up encryption keys on your server.",
)}
</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
{optionKey}
{optionPassphrase}
</div>
<DialogButtons
primaryButton={_t("Continue")}
onPrimaryButtonClick={this.onChooseKeyPassphraseFormSubmit}
onCancel={this.onCancelClick}
hasCancel={this.state.canSkip}
/>
</form>
);
}
private renderPhaseMigrate(): JSX.Element {
@ -555,83 +564,94 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
let authPrompt;
let nextCaption = _t("Next");
if (this.state.canUploadKeysWithPasswordOnly) {
authPrompt = <div>
<div>{ _t("Enter your account password to confirm the upgrade:") }</div>
<div><Field
type="password"
label={_t("Password")}
value={this.state.accountPassword}
onChange={this.onAccountPasswordChange}
forceValidity={this.state.accountPasswordCorrect === false ? false : null}
autoFocus={true}
/></div>
</div>;
authPrompt = (
<div>
<div>{_t("Enter your account password to confirm the upgrade:")}</div>
<div>
<Field
type="password"
label={_t("Password")}
value={this.state.accountPassword}
onChange={this.onAccountPasswordChange}
forceValidity={this.state.accountPasswordCorrect === false ? false : null}
autoFocus={true}
/>
</div>
</div>
);
} else if (!this.state.backupSigStatus.usable) {
authPrompt = <div>
<div>{ _t("Restore your key backup to upgrade your encryption") }</div>
</div>;
authPrompt = (
<div>
<div>{_t("Restore your key backup to upgrade your encryption")}</div>
</div>
);
nextCaption = _t("Restore");
} else {
authPrompt = <p>
{ _t("You'll need to authenticate with the server to confirm the upgrade.") }
</p>;
authPrompt = <p>{_t("You'll need to authenticate with the server to confirm the upgrade.")}</p>;
}
return <form onSubmit={this.onMigrateFormSubmit}>
<p>{ _t(
"Upgrade this session to allow it to verify other sessions, " +
"granting them access to encrypted messages and marking them " +
"as trusted for other users.",
) }</p>
<div>{ authPrompt }</div>
<DialogButtons
primaryButton={nextCaption}
onPrimaryButtonClick={this.onMigrateFormSubmit}
hasCancel={false}
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
>
<button type="button" className="danger" onClick={this.onCancelClick}>
{ _t('Skip') }
</button>
</DialogButtons>
</form>;
return (
<form onSubmit={this.onMigrateFormSubmit}>
<p>
{_t(
"Upgrade this session to allow it to verify other sessions, " +
"granting them access to encrypted messages and marking them " +
"as trusted for other users.",
)}
</p>
<div>{authPrompt}</div>
<DialogButtons
primaryButton={nextCaption}
onPrimaryButtonClick={this.onMigrateFormSubmit}
hasCancel={false}
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
>
<button type="button" className="danger" onClick={this.onCancelClick}>
{_t("Skip")}
</button>
</DialogButtons>
</form>
);
}
private renderPhasePassPhrase(): JSX.Element {
return <form onSubmit={this.onPassPhraseNextClick}>
<p>{ _t(
"Enter a security phrase only you know, as it's used to safeguard your data. " +
"To be secure, you shouldn't re-use your account password.",
) }</p>
return (
<form onSubmit={this.onPassPhraseNextClick}>
<p>
{_t(
"Enter a security phrase only you know, as it's used to safeguard your data. " +
"To be secure, you shouldn't re-use your account password.",
)}
</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<PassphraseField
className="mx_CreateSecretStorageDialog_passPhraseField"
onChange={this.onPassPhraseChange}
minScore={PASSWORD_MIN_SCORE}
value={this.state.passPhrase}
onValidate={this.onPassPhraseValidate}
fieldRef={this.passphraseField}
autoFocus={true}
label={_td("Enter a Security Phrase")}
labelEnterPassword={_td("Enter a Security Phrase")}
labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")}
/>
</div>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<PassphraseField
className="mx_CreateSecretStorageDialog_passPhraseField"
onChange={this.onPassPhraseChange}
minScore={PASSWORD_MIN_SCORE}
value={this.state.passPhrase}
onValidate={this.onPassPhraseValidate}
fieldRef={this.passphraseField}
autoFocus={true}
label={_td("Enter a Security Phrase")}
labelEnterPassword={_td("Enter a Security Phrase")}
labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")}
/>
</div>
<DialogButtons
primaryButton={_t('Continue')}
onPrimaryButtonClick={this.onPassPhraseNextClick}
hasCancel={false}
disabled={!this.state.passPhraseValid}
>
<button type="button"
onClick={this.onCancelClick}
className="danger"
>{ _t("Cancel") }</button>
</DialogButtons>
</form>;
<DialogButtons
primaryButton={_t("Continue")}
onPrimaryButtonClick={this.onPassPhraseNextClick}
hasCancel={false}
disabled={!this.state.passPhraseValid}
>
<button type="button" onClick={this.onCancelClick} className="danger">
{_t("Cancel")}
</button>
</DialogButtons>
</form>
);
}
private renderPhasePassPhraseConfirm(): JSX.Element {
@ -654,166 +674,188 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
let passPhraseMatch = null;
if (matchText) {
passPhraseMatch = <div>
<div>{ matchText }</div>
<AccessibleButton kind="link" onClick={this.onSetAgainClick}>
{ changeText }
</AccessibleButton>
</div>;
}
return <form onSubmit={this.onPassPhraseConfirmNextClick}>
<p>{ _t(
"Enter your Security Phrase a second time to confirm it.",
) }</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field
type="password"
onChange={this.onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateSecretStorageDialog_passPhraseField"
label={_t("Confirm your Security Phrase")}
autoFocus={true}
autoComplete="new-password"
/>
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">
{ passPhraseMatch }
passPhraseMatch = (
<div>
<div>{matchText}</div>
<AccessibleButton kind="link" onClick={this.onSetAgainClick}>
{changeText}
</AccessibleButton>
</div>
</div>
<DialogButtons
primaryButton={_t('Continue')}
onPrimaryButtonClick={this.onPassPhraseConfirmNextClick}
hasCancel={false}
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
>
<button type="button"
onClick={this.onCancelClick}
className="danger"
>{ _t("Skip") }</button>
</DialogButtons>
</form>;
);
}
return (
<form onSubmit={this.onPassPhraseConfirmNextClick}>
<p>{_t("Enter your Security Phrase a second time to confirm it.")}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field
type="password"
onChange={this.onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateSecretStorageDialog_passPhraseField"
label={_t("Confirm your Security Phrase")}
autoFocus={true}
autoComplete="new-password"
/>
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">{passPhraseMatch}</div>
</div>
<DialogButtons
primaryButton={_t("Continue")}
onPrimaryButtonClick={this.onPassPhraseConfirmNextClick}
hasCancel={false}
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
>
<button type="button" onClick={this.onCancelClick} className="danger">
{_t("Skip")}
</button>
</DialogButtons>
</form>
);
}
private renderPhaseShowKey(): JSX.Element {
let continueButton;
if (this.state.phase === Phase.ShowKey) {
continueButton = <DialogButtons primaryButton={_t("Continue")}
disabled={!this.state.downloaded && !this.state.copied && !this.state.setPassphrase}
onPrimaryButtonClick={this.onShowKeyContinueClick}
hasCancel={false}
/>;
continueButton = (
<DialogButtons
primaryButton={_t("Continue")}
disabled={!this.state.downloaded && !this.state.copied && !this.state.setPassphrase}
onPrimaryButtonClick={this.onShowKeyContinueClick}
hasCancel={false}
/>
);
} else {
continueButton = <div className="mx_CreateSecretStorageDialog_continueSpinner">
<InlineSpinner />
</div>;
continueButton = (
<div className="mx_CreateSecretStorageDialog_continueSpinner">
<InlineSpinner />
</div>
);
}
return <div>
<p>{ _t(
"Store your Security Key somewhere safe, like a password manager or a safe, " +
"as it's used to safeguard your encrypted data.",
) }</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer mx_CreateSecretStorageDialog_recoveryKeyPrimarycontainer">
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey">
<code ref={this.recoveryKeyNode}>{ this.recoveryKey.encodedPrivateKey }</code>
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<AccessibleButton kind='primary'
className="mx_Dialog_primary"
onClick={this.onDownloadClick}
disabled={this.state.phase === Phase.Storing}
>
{ _t("Download") }
</AccessibleButton>
<span>{ _t("%(downloadButton)s or %(copyButton)s", {
downloadButton: "",
copyButton: "",
}) }</span>
<AccessibleButton
kind='primary'
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
onClick={this.onCopyClick}
disabled={this.state.phase === Phase.Storing}
>
{ this.state.copied ? _t("Copied!") : _t("Copy") }
</AccessibleButton>
return (
<div>
<p>
{_t(
"Store your Security Key somewhere safe, like a password manager or a safe, " +
"as it's used to safeguard your encrypted data.",
)}
</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer mx_CreateSecretStorageDialog_recoveryKeyPrimarycontainer">
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey">
<code ref={this.recoveryKeyNode}>{this.recoveryKey.encodedPrivateKey}</code>
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<AccessibleButton
kind="primary"
className="mx_Dialog_primary"
onClick={this.onDownloadClick}
disabled={this.state.phase === Phase.Storing}
>
{_t("Download")}
</AccessibleButton>
<span>
{_t("%(downloadButton)s or %(copyButton)s", {
downloadButton: "",
copyButton: "",
})}
</span>
<AccessibleButton
kind="primary"
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
onClick={this.onCopyClick}
disabled={this.state.phase === Phase.Storing}
>
{this.state.copied ? _t("Copied!") : _t("Copy")}
</AccessibleButton>
</div>
</div>
</div>
{continueButton}
</div>
{ continueButton }
</div>;
);
}
private renderBusyPhase(): JSX.Element {
return <div>
<Spinner />
</div>;
return (
<div>
<Spinner />
</div>
);
}
private renderPhaseLoadError(): JSX.Element {
return <div>
<p>{ _t("Unable to query secret storage status") }</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this.onLoadRetryClick}
hasCancel={this.state.canSkip}
onCancel={this.onCancel}
/>
return (
<div>
<p>{_t("Unable to query secret storage status")}</p>
<div className="mx_Dialog_buttons">
<DialogButtons
primaryButton={_t("Retry")}
onPrimaryButtonClick={this.onLoadRetryClick}
hasCancel={this.state.canSkip}
onCancel={this.onCancel}
/>
</div>
</div>
</div>;
);
}
private renderPhaseSkipConfirm(): JSX.Element {
return <div>
<p>{ _t(
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
) }</p>
<p>{ _t(
"You can also set up Secure Backup & manage your keys in Settings.",
) }</p>
<DialogButtons primaryButton={_t('Go back')}
onPrimaryButtonClick={this.onGoBackClick}
hasCancel={false}
>
<button type="button" className="danger" onClick={this.onCancel}>{ _t('Cancel') }</button>
</DialogButtons>
</div>;
return (
<div>
<p>
{_t("If you cancel now, you may lose encrypted messages & data if you lose access to your logins.")}
</p>
<p>{_t("You can also set up Secure Backup & manage your keys in Settings.")}</p>
<DialogButtons
primaryButton={_t("Go back")}
onPrimaryButtonClick={this.onGoBackClick}
hasCancel={false}
>
<button type="button" className="danger" onClick={this.onCancel}>
{_t("Cancel")}
</button>
</DialogButtons>
</div>
);
}
private titleForPhase(phase: Phase): string {
switch (phase) {
case Phase.ChooseKeyPassphrase:
return _t('Set up Secure Backup');
return _t("Set up Secure Backup");
case Phase.Migrate:
return _t('Upgrade your encryption');
return _t("Upgrade your encryption");
case Phase.Passphrase:
return _t('Set a Security Phrase');
return _t("Set a Security Phrase");
case Phase.PassphraseConfirm:
return _t('Confirm Security Phrase');
return _t("Confirm Security Phrase");
case Phase.ConfirmSkip:
return _t('Are you sure?');
return _t("Are you sure?");
case Phase.ShowKey:
return _t('Save your Security Key');
return _t("Save your Security Key");
case Phase.Storing:
return _t('Setting up keys');
return _t("Setting up keys");
default:
return '';
return "";
}
}
public render(): JSX.Element {
let content;
if (this.state.error) {
content = <div>
<p>{ _t("Unable to set up secret storage") }</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this.bootstrapSecretStorage}
hasCancel={this.state.canSkip}
onCancel={this.onCancel}
/>
content = (
<div>
<p>{_t("Unable to set up secret storage")}</p>
<div className="mx_Dialog_buttons">
<DialogButtons
primaryButton={_t("Retry")}
onPrimaryButtonClick={this.bootstrapSecretStorage}
hasCancel={this.state.canSkip}
onCancel={this.onCancel}
/>
</div>
</div>
</div>;
);
} else {
switch (this.state.phase) {
case Phase.Loading:
@ -851,32 +893,31 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
case Phase.Passphrase:
case Phase.PassphraseConfirm:
titleClass = [
'mx_CreateSecretStorageDialog_titleWithIcon',
'mx_CreateSecretStorageDialog_securePhraseTitle',
"mx_CreateSecretStorageDialog_titleWithIcon",
"mx_CreateSecretStorageDialog_securePhraseTitle",
];
break;
case Phase.ShowKey:
titleClass = [
'mx_CreateSecretStorageDialog_titleWithIcon',
'mx_CreateSecretStorageDialog_secureBackupTitle',
"mx_CreateSecretStorageDialog_titleWithIcon",
"mx_CreateSecretStorageDialog_secureBackupTitle",
];
break;
case Phase.ChooseKeyPassphrase:
titleClass = 'mx_CreateSecretStorageDialog_centeredTitle';
titleClass = "mx_CreateSecretStorageDialog_centeredTitle";
break;
}
return (
<BaseDialog className='mx_CreateSecretStorageDialog'
<BaseDialog
className="mx_CreateSecretStorageDialog"
onFinished={this.props.onFinished}
title={this.titleForPhase(this.state.phase)}
titleClass={titleClass}
hasCancel={this.props.hasCancel && [Phase.Passphrase].includes(this.state.phase)}
fixedWidth={false}
>
<div>
{ content }
</div>
<div>{content}</div>
</BaseDialog>
);
}

View file

@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import FileSaver from 'file-saver';
import React from 'react';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import FileSaver from "file-saver";
import React from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../../languageHandler';
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
import { _t } from "../../../../languageHandler";
import * as MegolmExportEncryption from "../../../../utils/MegolmExportEncryption";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Field from "../../../../components/views/elements/Field";
@ -68,11 +68,11 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
const passphrase = this.state.passphrase1;
if (passphrase !== this.state.passphrase2) {
this.setState({ errStr: _t('Passphrases must match') });
this.setState({ errStr: _t("Passphrases must match") });
return false;
}
if (!passphrase) {
this.setState({ errStr: _t('Passphrase must not be empty') });
this.setState({ errStr: _t("Passphrase must not be empty") });
return false;
}
@ -83,29 +83,31 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
private startExport(passphrase: string): void {
// extra Promise.resolve() to turn synchronous exceptions into
// asynchronous ones.
Promise.resolve().then(() => {
return this.props.matrixClient.exportRoomKeys();
}).then((k) => {
return MegolmExportEncryption.encryptMegolmKeyFile(
JSON.stringify(k), passphrase,
);
}).then((f) => {
const blob = new Blob([f], {
type: 'text/plain;charset=us-ascii',
Promise.resolve()
.then(() => {
return this.props.matrixClient.exportRoomKeys();
})
.then((k) => {
return MegolmExportEncryption.encryptMegolmKeyFile(JSON.stringify(k), passphrase);
})
.then((f) => {
const blob = new Blob([f], {
type: "text/plain;charset=us-ascii",
});
FileSaver.saveAs(blob, "element-keys.txt");
this.props.onFinished(true);
})
.catch((e) => {
logger.error("Error exporting e2e keys:", e);
if (this.unmounted) {
return;
}
const msg = e.friendlyText || _t("Unknown error");
this.setState({
errStr: msg,
phase: Phase.Edit,
});
});
FileSaver.saveAs(blob, 'element-keys.txt');
this.props.onFinished(true);
}).catch((e) => {
logger.error("Error exporting e2e keys:", e);
if (this.unmounted) {
return;
}
const msg = e.friendlyText || _t('Unknown error');
this.setState({
errStr: msg,
phase: Phase.Edit,
});
});
this.setState({
errStr: null,
@ -126,54 +128,53 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
};
public render(): JSX.Element {
const disableForm = (this.state.phase === Phase.Exporting);
const disableForm = this.state.phase === Phase.Exporting;
return (
<BaseDialog className='mx_exportE2eKeysDialog'
<BaseDialog
className="mx_exportE2eKeysDialog"
onFinished={this.props.onFinished}
title={_t("Export room keys")}
>
<form onSubmit={this.onPassphraseFormSubmit}>
<div className="mx_Dialog_content">
<p>
{ _t(
'This process allows you to export the keys for messages ' +
'you have received in encrypted rooms to a local file. You ' +
'will then be able to import the file into another Matrix ' +
'client in the future, so that client will also be able to ' +
'decrypt these messages.',
) }
{_t(
"This process allows you to export the keys for messages " +
"you have received in encrypted rooms to a local file. You " +
"will then be able to import the file into another Matrix " +
"client in the future, so that client will also be able to " +
"decrypt these messages.",
)}
</p>
<p>
{ _t(
'The exported file will allow anyone who can read it to decrypt ' +
'any encrypted messages that you can see, so you should be ' +
'careful to keep it secure. To help with this, you should enter ' +
'a passphrase below, which will be used to encrypt the exported ' +
'data. It will only be possible to import the data by using the ' +
'same passphrase.',
) }
{_t(
"The exported file will allow anyone who can read it to decrypt " +
"any encrypted messages that you can see, so you should be " +
"careful to keep it secure. To help with this, you should enter " +
"a passphrase below, which will be used to encrypt the exported " +
"data. It will only be possible to import the data by using the " +
"same passphrase.",
)}
</p>
<div className='error'>
{ this.state.errStr }
</div>
<div className='mx_E2eKeysDialog_inputTable'>
<div className='mx_E2eKeysDialog_inputRow'>
<div className="error">{this.state.errStr}</div>
<div className="mx_E2eKeysDialog_inputTable">
<div className="mx_E2eKeysDialog_inputRow">
<Field
label={_t("Enter passphrase")}
value={this.state.passphrase1}
onChange={e => this.onPassphraseChange(e, "passphrase1")}
onChange={(e) => this.onPassphraseChange(e, "passphrase1")}
autoFocus={true}
size={64}
type="password"
disabled={disableForm}
/>
</div>
<div className='mx_E2eKeysDialog_inputRow'>
<div className="mx_E2eKeysDialog_inputRow">
<Field
label={_t("Confirm passphrase")}
value={this.state.passphrase2}
onChange={e => this.onPassphraseChange(e, "passphrase2")}
onChange={(e) => this.onPassphraseChange(e, "passphrase2")}
size={64}
type="password"
disabled={disableForm}
@ -181,15 +182,15 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
</div>
</div>
</div>
<div className='mx_Dialog_buttons'>
<div className="mx_Dialog_buttons">
<input
className='mx_Dialog_primary'
type='submit'
value={_t('Export')}
className="mx_Dialog_primary"
type="submit"
value={_t("Export")}
disabled={disableForm}
/>
<button onClick={this.onCancelClick} disabled={disableForm}>
{ _t("Cancel") }
{_t("Cancel")}
</button>
</div>
</form>

View file

@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import React, { createRef } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
import { _t } from '../../../../languageHandler';
import * as MegolmExportEncryption from "../../../../utils/MegolmExportEncryption";
import { _t } from "../../../../languageHandler";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Field from "../../../../components/views/elements/Field";
@ -75,7 +75,7 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
private onFormChange = (): void => {
const files = this.file.current.files || [];
this.setState({
enableSubmit: (this.state.passphrase !== "" && files.length > 0),
enableSubmit: this.state.passphrase !== "" && files.length > 0,
});
};
@ -97,26 +97,28 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
phase: Phase.Importing,
});
return readFileAsArrayBuffer(file).then((arrayBuffer) => {
return MegolmExportEncryption.decryptMegolmKeyFile(
arrayBuffer, passphrase,
);
}).then((keys) => {
return this.props.matrixClient.importRoomKeys(JSON.parse(keys));
}).then(() => {
// TODO: it would probably be nice to give some feedback about what we've imported here.
this.props.onFinished(true);
}).catch((e) => {
logger.error("Error importing e2e keys:", e);
if (this.unmounted) {
return;
}
const msg = e.friendlyText || _t('Unknown error');
this.setState({
errStr: msg,
phase: Phase.Edit,
return readFileAsArrayBuffer(file)
.then((arrayBuffer) => {
return MegolmExportEncryption.decryptMegolmKeyFile(arrayBuffer, passphrase);
})
.then((keys) => {
return this.props.matrixClient.importRoomKeys(JSON.parse(keys));
})
.then(() => {
// TODO: it would probably be nice to give some feedback about what we've imported here.
this.props.onFinished(true);
})
.catch((e) => {
logger.error("Error importing e2e keys:", e);
if (this.unmounted) {
return;
}
const msg = e.friendlyText || _t("Unknown error");
this.setState({
errStr: msg,
phase: Phase.Edit,
});
});
});
}
private onCancelClick = (ev: React.MouseEvent): boolean => {
@ -126,50 +128,48 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
};
public render(): JSX.Element {
const disableForm = (this.state.phase !== Phase.Edit);
const disableForm = this.state.phase !== Phase.Edit;
return (
<BaseDialog className='mx_importE2eKeysDialog'
<BaseDialog
className="mx_importE2eKeysDialog"
onFinished={this.props.onFinished}
title={_t("Import room keys")}
>
<form onSubmit={this.onFormSubmit}>
<div className="mx_Dialog_content">
<p>
{ _t(
'This process allows you to import encryption keys ' +
'that you had previously exported from another Matrix ' +
'client. You will then be able to decrypt any ' +
'messages that the other client could decrypt.',
) }
{_t(
"This process allows you to import encryption keys " +
"that you had previously exported from another Matrix " +
"client. You will then be able to decrypt any " +
"messages that the other client could decrypt.",
)}
</p>
<p>
{ _t(
'The export file will be protected with a passphrase. ' +
'You should enter the passphrase here, to decrypt the file.',
) }
{_t(
"The export file will be protected with a passphrase. " +
"You should enter the passphrase here, to decrypt the file.",
)}
</p>
<div className='error'>
{ this.state.errStr }
</div>
<div className='mx_E2eKeysDialog_inputTable'>
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='importFile'>
{ _t("File to import") }
</label>
<div className="error">{this.state.errStr}</div>
<div className="mx_E2eKeysDialog_inputTable">
<div className="mx_E2eKeysDialog_inputRow">
<div className="mx_E2eKeysDialog_inputLabel">
<label htmlFor="importFile">{_t("File to import")}</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<div className="mx_E2eKeysDialog_inputCell">
<input
ref={this.file}
id='importFile'
type='file'
id="importFile"
type="file"
autoFocus={true}
onChange={this.onFormChange}
disabled={disableForm} />
disabled={disableForm}
/>
</div>
</div>
<div className='mx_E2eKeysDialog_inputRow'>
<div className="mx_E2eKeysDialog_inputRow">
<Field
label={_t("Enter passphrase")}
value={this.state.passphrase}
@ -181,15 +181,15 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
</div>
</div>
</div>
<div className='mx_Dialog_buttons'>
<div className="mx_Dialog_buttons">
<input
className='mx_Dialog_primary'
type='submit'
value={_t('Import')}
className="mx_Dialog_primary"
type="submit"
value={_t("Import")}
disabled={!this.state.enableSubmit || disableForm}
/>
<button onClick={this.onCancelClick} disabled={disableForm}>
{ _t("Cancel") }
{_t("Cancel")}
</button>
</div>
</form>

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from "react";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal";
@ -43,61 +43,66 @@ export default class NewRecoveryMethodDialog extends React.PureComponent<IProps>
};
private onSetupClick = async (): Promise<void> => {
Modal.createDialog(RestoreKeyBackupDialog, {
onFinished: this.props.onFinished,
}, null, /* priority = */ false, /* static = */ true);
Modal.createDialog(
RestoreKeyBackupDialog,
{
onFinished: this.props.onFinished,
},
null,
/* priority = */ false,
/* static = */ true,
);
};
public render(): JSX.Element {
const title = <span className="mx_KeyBackupFailedDialog_title">
{ _t("New Recovery Method") }
</span>;
const title = <span className="mx_KeyBackupFailedDialog_title">{_t("New Recovery Method")}</span>;
const newMethodDetected = <p>{ _t(
"A new Security Phrase and key for Secure Messages have been detected.",
) }</p>;
const newMethodDetected = <p>{_t("A new Security Phrase and key for Secure Messages have been detected.")}</p>;
const hackWarning = <p className="warning">{ _t(
"If you didn't set the new recovery method, an " +
"attacker may be trying to access your account. " +
"Change your account password and set a new recovery " +
"method immediately in Settings.",
) }</p>;
const hackWarning = (
<p className="warning">
{_t(
"If you didn't set the new recovery method, an " +
"attacker may be trying to access your account. " +
"Change your account password and set a new recovery " +
"method immediately in Settings.",
)}
</p>
);
let content;
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
content = <div>
{ newMethodDetected }
<p>{ _t(
"This session is encrypting history using the new recovery method.",
) }</p>
{ hackWarning }
<DialogButtons
primaryButton={_t("OK")}
onPrimaryButtonClick={this.onOkClick}
cancelButton={_t("Go to Settings")}
onCancel={this.onGoToSettingsClick}
/>
</div>;
content = (
<div>
{newMethodDetected}
<p>{_t("This session is encrypting history using the new recovery method.")}</p>
{hackWarning}
<DialogButtons
primaryButton={_t("OK")}
onPrimaryButtonClick={this.onOkClick}
cancelButton={_t("Go to Settings")}
onCancel={this.onGoToSettingsClick}
/>
</div>
);
} else {
content = <div>
{ newMethodDetected }
{ hackWarning }
<DialogButtons
primaryButton={_t("Set up Secure Messages")}
onPrimaryButtonClick={this.onSetupClick}
cancelButton={_t("Go to Settings")}
onCancel={this.onGoToSettingsClick}
/>
</div>;
content = (
<div>
{newMethodDetected}
{hackWarning}
<DialogButtons
primaryButton={_t("Set up Secure Messages")}
onPrimaryButtonClick={this.onSetupClick}
cancelButton={_t("Go to Settings")}
onCancel={this.onGoToSettingsClick}
/>
</div>
);
}
return (
<BaseDialog className="mx_KeyBackupFailedDialog"
onFinished={this.props.onFinished}
title={title}
>
{ content }
<BaseDialog className="mx_KeyBackupFailedDialog" onFinished={this.props.onFinished} title={title}>
{content}
</BaseDialog>
);
}

View file

@ -37,36 +37,40 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent<IPr
this.props.onFinished();
Modal.createDialogAsync(
import("./CreateKeyBackupDialog") as unknown as Promise<ComponentType<{}>>,
null, null, /* priority = */ false, /* static = */ true,
null,
null,
/* priority = */ false,
/* static = */ true,
);
};
public render(): JSX.Element {
const title = <span className="mx_KeyBackupFailedDialog_title">
{ _t("Recovery Method Removed") }
</span>;
const title = <span className="mx_KeyBackupFailedDialog_title">{_t("Recovery Method Removed")}</span>;
return (
<BaseDialog className="mx_KeyBackupFailedDialog"
onFinished={this.props.onFinished}
title={title}
>
<BaseDialog className="mx_KeyBackupFailedDialog" onFinished={this.props.onFinished} title={title}>
<div>
<p>{ _t(
"This session has detected that your Security Phrase and key " +
"for Secure Messages have been removed.",
) }</p>
<p>{ _t(
"If you did this accidentally, you can setup Secure Messages on " +
"this session which will re-encrypt this session's message " +
"history with a new recovery method.",
) }</p>
<p className="warning">{ _t(
"If you didn't remove the recovery method, an " +
"attacker may be trying to access your account. " +
"Change your account password and set a new recovery " +
"method immediately in Settings.",
) }</p>
<p>
{_t(
"This session has detected that your Security Phrase and key " +
"for Secure Messages have been removed.",
)}
</p>
<p>
{_t(
"If you did this accidentally, you can setup Secure Messages on " +
"this session which will re-encrypt this session's message " +
"history with a new recovery method.",
)}
</p>
<p className="warning">
{_t(
"If you didn't remove the recovery method, an " +
"attacker may be trying to access your account. " +
"Change your account password and set a new recovery " +
"method immediately in Settings.",
)}
</p>
<DialogButtons
primaryButton={_t("Set up Secure Messages")}
onPrimaryButtonClick={this.onSetupClick}

View file

@ -45,7 +45,7 @@ export const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
function makePlaybackWaveform(input: number[]): number[] {
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
const noiseWaveform = input.map(v => Math.abs(v));
const noiseWaveform = input.map((v) => Math.abs(v));
// Then, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
// We also rescale the waveform to be 0-1 so we end up with a clamped waveform to rely upon.
@ -174,7 +174,8 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
// Overall, the point of this is to avoid memory-related issues due to storing a massive
// audio buffer in memory, as that can balloon to far greater than the input buffer's
// byte length.
if (this.buf.byteLength > 5 * 1024 * 1024) { // 5mb
if (this.buf.byteLength > 5 * 1024 * 1024) {
// 5mb
logger.log("Audio file too large: processing through <audio /> element");
this.element = document.createElement("AUDIO") as HTMLAudioElement;
const prom = new Promise((resolve, reject) => {
@ -186,25 +187,33 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
} else {
// Safari compat: promise API not supported on this function
this.audioBuf = await new Promise((resolve, reject) => {
this.context.decodeAudioData(this.buf, b => resolve(b), async e => {
try {
// This error handler is largely for Safari as well, which doesn't support Opus/Ogg
// very well.
logger.error("Error decoding recording: ", e);
logger.warn("Trying to re-encode to WAV instead...");
this.context.decodeAudioData(
this.buf,
(b) => resolve(b),
async (e) => {
try {
// This error handler is largely for Safari as well, which doesn't support Opus/Ogg
// very well.
logger.error("Error decoding recording: ", e);
logger.warn("Trying to re-encode to WAV instead...");
const wav = await decodeOgg(this.buf);
const wav = await decodeOgg(this.buf);
// noinspection ES6MissingAwait - not needed when using callbacks
this.context.decodeAudioData(wav, b => resolve(b), e => {
logger.error("Still failed to decode recording: ", e);
// noinspection ES6MissingAwait - not needed when using callbacks
this.context.decodeAudioData(
wav,
(b) => resolve(b),
(e) => {
logger.error("Still failed to decode recording: ", e);
reject(e);
},
);
} catch (e) {
logger.error("Caught decoding error:", e);
reject(e);
});
} catch (e) {
logger.error("Caught decoding error:", e);
reject(e);
}
});
}
},
);
});
// Update the waveform to the real waveform once we have channel data to use. We don't

View file

@ -64,8 +64,7 @@ export class PlaybackClock implements IDestroyable {
private clipDuration = 0;
private placeholderDuration = 0;
public constructor(private context: AudioContext) {
}
public constructor(private context: AudioContext) {}
public get durationSeconds(): number {
return this.clipDuration || this.placeholderDuration;
@ -104,7 +103,7 @@ export class PlaybackClock implements IDestroyable {
* @param {MatrixEvent} event The event to use for placeholders.
*/
public populatePlaceholdersFrom(event: MatrixEvent) {
const durationMs = Number(event.getContent()['info']?.['duration']);
const durationMs = Number(event.getContent()["info"]?.["duration"]);
if (Number.isFinite(durationMs)) this.placeholderDuration = durationMs / 1000;
}

View file

@ -40,12 +40,12 @@ export class PlaybackManager {
*/
public pauseAllExcept(playback?: Playback) {
this.instances
.filter(p => p !== playback && p.currentState === PlaybackState.Playing)
.forEach(p => p.pause());
.filter((p) => p !== playback && p.currentState === PlaybackState.Playing)
.forEach((p) => p.pause());
}
public destroyPlaybackInstance(playback: ManagedPlayback) {
this.instances = this.instances.filter(p => p !== playback);
this.instances = this.instances.filter((p) => p !== playback);
}
public createPlaybackInstance(buf: ArrayBuffer, waveform = DEFAULT_WAVEFORM): Playback {

View file

@ -116,8 +116,8 @@ export class PlaybackQueue {
const instance = this.playbacks.get(next);
if (!instance) {
logger.warn(
"Voice message queue desync: Missing playback for next message: "
+ `Current=${this.currentPlaybackId} Last=${last} Next=${next}`,
"Voice message queue desync: Missing playback for next message: " +
`Current=${this.currentPlaybackId} Last=${last} Next=${next}`,
);
} else {
this.playbackIdOrder = orderClone;
@ -175,8 +175,8 @@ export class PlaybackQueue {
}
} else {
logger.warn(
"Voice message queue desync: Expected playback stop to be last in order. "
+ `Current=${this.currentPlaybackId} Last=${last} EventID=${mxEvent.getId()}`,
"Voice message queue desync: Expected playback stop to be last in order. " +
`Current=${this.currentPlaybackId} Last=${last} EventID=${mxEvent.getId()}`,
);
}
}
@ -188,8 +188,8 @@ export class PlaybackQueue {
if (order.length === 0 || order[order.length - 1] !== this.currentPlaybackId) {
const lastInstance = this.playbacks.get(this.currentPlaybackId);
if (
lastInstance.currentState === PlaybackState.Playing
|| lastInstance.currentState === PlaybackState.Paused
lastInstance.currentState === PlaybackState.Playing ||
lastInstance.currentState === PlaybackState.Paused
) {
order.push(this.currentPlaybackId);
}

View file

@ -36,7 +36,7 @@ function roundTimeToTargetFreq(seconds: number): number {
function nextTimeForTargetFreq(roundedSeconds: number): number {
// The extra round is just to make sure we cut off any floating point issues
return roundTimeToTargetFreq(roundedSeconds + (1 / TARGET_AMPLITUDE_FREQUENCY));
return roundTimeToTargetFreq(roundedSeconds + 1 / TARGET_AMPLITUDE_FREQUENCY);
}
class MxVoiceWorklet extends AudioWorkletProcessor {

View file

@ -37,10 +37,7 @@ export class VoiceMessageRecording implements IDestroyable {
private buffer = new Uint8Array(0); // use this.audioBuffer to access
private playback: Playback;
public constructor(
private matrixClient: MatrixClient,
private voiceRecording: VoiceRecording,
) {
public constructor(private matrixClient: MatrixClient, private voiceRecording: VoiceRecording) {
this.voiceRecording.onDataAvailable = this.onDataAvailable;
}
@ -106,12 +103,9 @@ export class VoiceMessageRecording implements IDestroyable {
const { url: mxc, file: encrypted } = await uploadFile(
this.matrixClient,
inRoomId,
new Blob(
[this.audioBuffer],
{
type: this.contentType,
},
),
new Blob([this.audioBuffer], {
type: this.contentType,
}),
);
this.lastUpload = { mxc, encrypted };
this.emit(RecordingState.Uploaded);

View file

@ -15,8 +15,8 @@ limitations under the License.
*/
// @ts-ignore
import Recorder from 'opus-recorder/dist/recorder.min.js';
import encoderPath from 'opus-recorder/dist/encoderWorker.min.js';
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";
import { logger } from "matrix-js-sdk/src/logger";
@ -137,15 +137,15 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
// Dev note: we can't use `addEventListener` for some reason. It just doesn't work.
this.recorderWorklet.port.onmessage = (ev) => {
switch (ev.data['ev']) {
switch (ev.data["ev"]) {
case PayloadEvent.Timekeep:
this.processAudioUpdate(ev.data['timeSeconds']);
this.processAudioUpdate(ev.data["timeSeconds"]);
break;
case PayloadEvent.AmplitudeMark:
// Sanity check to make sure we're adding about one sample per second
if (ev.data['forIndex'] === this.amplitudes.length) {
this.amplitudes.push(ev.data['amplitude']);
this.liveWaveform.pushValue(ev.data['amplitude']);
if (ev.data["forIndex"] === this.amplitudes.length) {
this.amplitudes.push(ev.data["amplitude"]);
this.liveWaveform.pushValue(ev.data["amplitude"]);
}
break;
}
@ -159,8 +159,9 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
this.recorderProcessor.addEventListener("audioprocess", this.onAudioProcess);
}
const recorderOptions = this.shouldRecordInHighQuality() ?
highQualityRecorderOptions : voiceRecorderOptions;
const recorderOptions = this.shouldRecordInHighQuality()
? highQualityRecorderOptions
: voiceRecorderOptions;
const { encoderApplication, bitrate } = recorderOptions;
this.recorder = new Recorder({
@ -184,12 +185,13 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
this.recorder.ondataavailable = (data: ArrayBuffer) => this?.onDataAvailable(data);
} catch (e) {
logger.error("Error starting recording: ", e);
if (e instanceof DOMException) { // Unhelpful DOMExceptions are common - parse them sanely
if (e instanceof DOMException) {
// Unhelpful DOMExceptions are common - parse them sanely
logger.error(`${e.name} (${e.code}): ${e.message}`);
}
// Clean up as best as possible
if (this.recorderStream) this.recorderStream.getTracks().forEach(t => t.stop());
if (this.recorderStream) this.recorderStream.getTracks().forEach((t) => t.stop());
if (this.recorderSource) this.recorderSource.disconnect();
if (this.recorder) this.recorder.close();
if (this.recorderContext) {
@ -221,7 +223,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
if (!this.recording) return;
this.observable.update({
waveform: this.liveWaveform.value.map(v => clamp(v, 0, 1)),
waveform: this.liveWaveform.value.map((v) => clamp(v, 0, 1)),
timeSeconds: timeSeconds,
});
@ -243,7 +245,8 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
}
const secondsLeft = TARGET_MAX_LENGTH - this.recorderSeconds;
if (secondsLeft < 0) { // go over to make sure we definitely capture that last frame
if (secondsLeft < 0) {
// go over to make sure we definitely capture that last frame
// noinspection JSIgnoredPromiseFromCall - we aren't concerned with it overlapping
this.stop();
} else if (secondsLeft <= TARGET_WARN_TIME_LEFT) {
@ -256,7 +259,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
/**
* {@link https://github.com/chris-rudmin/opus-recorder#instance-fields ref for recorderSeconds}
*/
*/
public get recorderSeconds() {
return this.recorder.encodedSamplePosition / 48000;
}
@ -295,7 +298,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
await this.recorderContext.close();
// Now stop all the media tracks so we can release them back to the user/OS
this.recorderStream.getTracks().forEach(t => t.stop());
this.recorderStream.getTracks().forEach((t) => t.stop());
// Finally do our post-processing and clean up
this.recording = false;

View file

@ -15,9 +15,9 @@ limitations under the License.
*/
// @ts-ignore - we know that this is not a module. We're looking for a path.
import decoderWasmPath from 'opus-recorder/dist/decoderWorker.min.wasm';
import wavEncoderPath from 'opus-recorder/dist/waveWorker.min.js';
import decoderPath from 'opus-recorder/dist/decoderWorker.min.js';
import decoderWasmPath from "opus-recorder/dist/decoderWorker.min.wasm";
import wavEncoderPath from "opus-recorder/dist/waveWorker.min.js";
import decoderPath from "opus-recorder/dist/decoderWorker.min.js";
import { logger } from "matrix-js-sdk/src/logger";
import { SAMPLE_RATE } from "./VoiceRecording";
@ -38,46 +38,54 @@ export function createAudioContext(opts?: AudioContextOptions): AudioContext {
export function decodeOgg(audioBuffer: ArrayBuffer): Promise<ArrayBuffer> {
// Condensed version of decoder example, using a promise:
// https://github.com/chris-rudmin/opus-recorder/blob/master/example/decoder.html
return new Promise((resolve) => { // no reject because the workers don't seem to have a fail path
return new Promise((resolve) => {
// no reject because the workers don't seem to have a fail path
logger.log("Decoder WASM path: " + decoderWasmPath); // so we use the variable (avoid tree shake)
const typedArray = new Uint8Array(audioBuffer);
const decoderWorker = new Worker(decoderPath);
const wavWorker = new Worker(wavEncoderPath);
decoderWorker.postMessage({
command: 'init',
command: "init",
decoderSampleRate: SAMPLE_RATE,
outputBufferSampleRate: SAMPLE_RATE,
});
wavWorker.postMessage({
command: 'init',
command: "init",
wavBitDepth: 24, // standard for 48khz (SAMPLE_RATE)
wavSampleRate: SAMPLE_RATE,
});
decoderWorker.onmessage = (ev) => {
if (ev.data === null) { // null == done
wavWorker.postMessage({ command: 'done' });
if (ev.data === null) {
// null == done
wavWorker.postMessage({ command: "done" });
return;
}
wavWorker.postMessage({
command: 'encode',
buffers: ev.data,
}, ev.data.map(b => b.buffer));
wavWorker.postMessage(
{
command: "encode",
buffers: ev.data,
},
ev.data.map((b) => b.buffer),
);
};
wavWorker.onmessage = (ev) => {
if (ev.data.message === 'page') {
if (ev.data.message === "page") {
// The encoding comes through as a single page
resolve(new Blob([ev.data.page], { type: "audio/wav" }).arrayBuffer());
}
};
decoderWorker.postMessage({
command: 'decode',
pages: typedArray,
}, [typedArray.buffer]);
decoderWorker.postMessage(
{
command: "decode",
pages: typedArray,
},
[typedArray.buffer],
);
});
}

View file

@ -16,10 +16,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { TimelineRenderingType } from '../contexts/RoomContext';
import type { ICompletion, ISelectionRange } from './Autocompleter';
import { TimelineRenderingType } from "../contexts/RoomContext";
import type { ICompletion, ISelectionRange } from "./Autocompleter";
export interface ICommand {
command: string | null;
@ -44,13 +44,13 @@ export default abstract class AutocompleteProvider {
protected constructor({ commandRegex, forcedCommandRegex, renderingType }: IAutocompleteOptions) {
if (commandRegex) {
if (!commandRegex.global) {
throw new Error('commandRegex must have global flag set');
throw new Error("commandRegex must have global flag set");
}
this.commandRegex = commandRegex;
}
if (forcedCommandRegex) {
if (!forcedCommandRegex.global) {
throw new Error('forcedCommandRegex must have global flag set');
throw new Error("forcedCommandRegex must have global flag set");
}
this.forcedCommandRegex = forcedCommandRegex;
}

View file

@ -15,18 +15,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ReactElement } from 'react';
import { Room } from 'matrix-js-sdk/src/models/room';
import { ReactElement } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import CommandProvider from './CommandProvider';
import RoomProvider from './RoomProvider';
import UserProvider from './UserProvider';
import EmojiProvider from './EmojiProvider';
import NotifProvider from './NotifProvider';
import CommandProvider from "./CommandProvider";
import RoomProvider from "./RoomProvider";
import UserProvider from "./UserProvider";
import EmojiProvider from "./EmojiProvider";
import NotifProvider from "./NotifProvider";
import { timeout } from "../utils/promise";
import AutocompleteProvider, { ICommand } from "./AutocompleteProvider";
import SpaceProvider from "./SpaceProvider";
import { TimelineRenderingType } from '../contexts/RoomContext';
import { TimelineRenderingType } from "../contexts/RoomContext";
export interface ISelectionRange {
beginning?: boolean; // whether the selection is in the first block of the editor or not
@ -47,14 +47,7 @@ export interface ICompletion {
href?: string;
}
const PROVIDERS = [
UserProvider,
RoomProvider,
EmojiProvider,
NotifProvider,
CommandProvider,
SpaceProvider,
];
const PROVIDERS = [UserProvider, RoomProvider, EmojiProvider, NotifProvider, CommandProvider, SpaceProvider];
// Providers will get rejected if they take longer than this.
const PROVIDER_COMPLETION_TIMEOUT = 3000;
@ -94,28 +87,32 @@ export default class Autocompleter {
to predict whether an action will actually do what is intended
*/
// list of results from each provider, each being a list of completions or null if it times out
const completionsList: ICompletion[][] = await Promise.all(this.providers.map(async provider => {
return timeout(
provider.getCompletions(query, selection, force, limit),
null,
PROVIDER_COMPLETION_TIMEOUT,
);
}));
const completionsList: ICompletion[][] = await Promise.all(
this.providers.map(async (provider) => {
return timeout(
provider.getCompletions(query, selection, force, limit),
null,
PROVIDER_COMPLETION_TIMEOUT,
);
}),
);
// map then filter to maintain the index for the map-operation, for this.providers to line up
return completionsList.map((completions, i) => {
if (!completions || !completions.length) return;
return completionsList
.map((completions, i) => {
if (!completions || !completions.length) return;
return {
completions,
provider: this.providers[i],
return {
completions,
provider: this.providers[i],
/* the currently matched "command" the completer tried to complete
* we pass this through so that Autocomplete can figure out when to
* re-show itself once hidden.
*/
command: this.providers[i].getCurrentCommand(query, selection, force),
};
}).filter(Boolean);
/* the currently matched "command" the completer tried to complete
* we pass this through so that Autocomplete can figure out when to
* re-show itself once hidden.
*/
command: this.providers[i].getCurrentCommand(query, selection, force),
};
})
.filter(Boolean);
}
}

View file

@ -17,16 +17,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { Room } from 'matrix-js-sdk/src/models/room';
import React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import QueryMatcher from './QueryMatcher';
import { TextualCompletion } from './Components';
import { _t } from "../languageHandler";
import AutocompleteProvider from "./AutocompleteProvider";
import QueryMatcher from "./QueryMatcher";
import { TextualCompletion } from "./Components";
import { ICompletion, ISelectionRange } from "./Autocompleter";
import { Command, Commands, CommandMap } from '../SlashCommands';
import { TimelineRenderingType } from '../contexts/RoomContext';
import { Command, Commands, CommandMap } from "../SlashCommands";
import { TimelineRenderingType } from "../contexts/RoomContext";
const COMMAND_RE = /(^\/\w*)(?: .*)?/g;
@ -36,7 +36,7 @@ export default class CommandProvider extends AutocompleteProvider {
constructor(room: Room, renderingType?: TimelineRenderingType) {
super({ commandRegex: COMMAND_RE, renderingType });
this.matcher = new QueryMatcher(Commands, {
keys: ['command', 'args', 'description'],
keys: ["command", "args", "description"],
funcs: [({ aliases }) => aliases.join(" ")], // aliases
context: renderingType,
});
@ -62,7 +62,7 @@ export default class CommandProvider extends AutocompleteProvider {
matches = [CommandMap.get(name)];
}
} else {
if (query === '/') {
if (query === "/") {
// If they have just entered `/` show everything
// We exclude the limit on purpose to have a comprehensive list
matches = Commands;
@ -72,31 +72,36 @@ export default class CommandProvider extends AutocompleteProvider {
}
}
return matches.filter(cmd => {
const display = !cmd.renderingTypes || cmd.renderingTypes.includes(this.renderingType);
return cmd.isEnabled() && display;
}).map((result) => {
let completion = result.getCommand() + ' ';
const usedAlias = result.aliases.find(alias => `/${alias}` === command[1]);
// If the command (or an alias) is the same as the one they entered, we don't want to discard their arguments
if (usedAlias || result.getCommand() === command[1]) {
completion = command[0];
}
return matches
.filter((cmd) => {
const display = !cmd.renderingTypes || cmd.renderingTypes.includes(this.renderingType);
return cmd.isEnabled() && display;
})
.map((result) => {
let completion = result.getCommand() + " ";
const usedAlias = result.aliases.find((alias) => `/${alias}` === command[1]);
// If the command (or an alias) is the same as the one they entered, we don't want to discard their arguments
if (usedAlias || result.getCommand() === command[1]) {
completion = command[0];
}
return {
completion,
type: "command",
component: <TextualCompletion
title={`/${usedAlias || result.command}`}
subtitle={result.args}
description={_t(result.description)} />,
range,
};
});
return {
completion,
type: "command",
component: (
<TextualCompletion
title={`/${usedAlias || result.command}`}
subtitle={result.args}
description={_t(result.description)}
/>
),
range,
};
});
}
getName() {
return '*️⃣ ' + _t('Commands');
return "*️⃣ " + _t("Commands");
}
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
@ -106,7 +111,7 @@ export default class CommandProvider extends AutocompleteProvider {
role="presentation"
aria-label={_t("Command Autocomplete")}
>
{ completions }
{completions}
</div>
);
}

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { forwardRef } from 'react';
import classNames from 'classnames';
import React, { forwardRef } from "react";
import classNames from "classnames";
/* These were earlier stateless functional components but had to be converted
since we need to use refs/findDOMNode to access the underlying DOM node to focus the correct completion,
@ -31,24 +31,18 @@ interface ITextualCompletionProps {
}
export const TextualCompletion = forwardRef<ITextualCompletionProps, any>((props, ref) => {
const {
title,
subtitle,
description,
className,
'aria-selected': ariaSelectedAttribute,
...restProps
} = props;
const { title, subtitle, description, className, "aria-selected": ariaSelectedAttribute, ...restProps } = props;
return (
<div {...restProps}
className={classNames('mx_Autocomplete_Completion_block', className)}
<div
{...restProps}
className={classNames("mx_Autocomplete_Completion_block", className)}
role="option"
aria-selected={ariaSelectedAttribute}
ref={ref}
>
<span className="mx_Autocomplete_Completion_title">{ title }</span>
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
<span className="mx_Autocomplete_Completion_description">{ description }</span>
<span className="mx_Autocomplete_Completion_title">{title}</span>
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
<span className="mx_Autocomplete_Completion_description">{description}</span>
</div>
);
});
@ -64,20 +58,21 @@ export const PillCompletion = forwardRef<IPillCompletionProps, any>((props, ref)
description,
className,
children,
'aria-selected': ariaSelectedAttribute,
"aria-selected": ariaSelectedAttribute,
...restProps
} = props;
return (
<div {...restProps}
className={classNames('mx_Autocomplete_Completion_pill', className)}
<div
{...restProps}
className={classNames("mx_Autocomplete_Completion_pill", className)}
role="option"
aria-selected={ariaSelectedAttribute}
ref={ref}
>
{ children }
<span className="mx_Autocomplete_Completion_title">{ title }</span>
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
<span className="mx_Autocomplete_Completion_description">{ description }</span>
{children}
<span className="mx_Autocomplete_Completion_title">{title}</span>
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
<span className="mx_Autocomplete_Completion_description">{description}</span>
</div>
);
});

View file

@ -18,26 +18,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { uniq, sortBy } from 'lodash';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import { Room } from 'matrix-js-sdk/src/models/room';
import React from "react";
import { uniq, sortBy } from "lodash";
import EMOTICON_REGEX from "emojibase-regex/emoticon";
import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import QueryMatcher from './QueryMatcher';
import { PillCompletion } from './Components';
import { ICompletion, ISelectionRange } from './Autocompleter';
import { _t } from "../languageHandler";
import AutocompleteProvider from "./AutocompleteProvider";
import QueryMatcher from "./QueryMatcher";
import { PillCompletion } from "./Components";
import { ICompletion, ISelectionRange } from "./Autocompleter";
import SettingsStore from "../settings/SettingsStore";
import { EMOJI, IEmoji, getEmojiFromUnicode } from '../emoji';
import { TimelineRenderingType } from '../contexts/RoomContext';
import * as recent from '../emojipicker/recent';
import { EMOJI, IEmoji, getEmojiFromUnicode } from "../emoji";
import { TimelineRenderingType } from "../contexts/RoomContext";
import * as recent from "../emojipicker/recent";
const LIMIT = 20;
// Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase
// anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g');
const EMOJI_REGEX = new RegExp("(" + EMOTICON_REGEX.source + "|(?:^|\\s):[+-\\w]*:?)$", "g");
interface ISortedEmoji {
emoji: IEmoji;
@ -80,12 +80,12 @@ export default class EmojiProvider extends AutocompleteProvider {
super({ commandRegex: EMOJI_REGEX, renderingType });
this.matcher = new QueryMatcher<ISortedEmoji>(SORTED_EMOJI, {
keys: [],
funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)],
funcs: [(o) => o.emoji.shortcodes.map((s) => `:${s}:`)],
// For matching against ascii equivalents
shouldMatchWordsOnly: false,
});
this.nameMatcher = new QueryMatcher(SORTED_EMOJI, {
keys: ['emoji.label'],
keys: ["emoji.label"],
// For removing punctuation
shouldMatchWordsOnly: true,
});
@ -115,39 +115,37 @@ export default class EmojiProvider extends AutocompleteProvider {
let sorters = [];
// make sure that emoticons come first
sorters.push(c => score(matchedString, c.emoji.emoticon || ""));
sorters.push((c) => score(matchedString, c.emoji.emoticon || ""));
// then sort by score (Infinity if matchedString not in shortcode)
sorters.push(c => score(matchedString, c.emoji.shortcodes[0]));
sorters.push((c) => score(matchedString, c.emoji.shortcodes[0]));
// then sort by max score of all shortcodes, trim off the `:`
const trimmedMatch = colonsTrimmed(matchedString);
sorters.push(c => Math.min(
...c.emoji.shortcodes.map(s => score(trimmedMatch, s)),
));
sorters.push((c) => Math.min(...c.emoji.shortcodes.map((s) => score(trimmedMatch, s))));
// If the matchedString is not empty, sort by length of shortcode. Example:
// matchedString = ":bookmark"
// completions = [":bookmark:", ":bookmark_tabs:", ...]
if (matchedString.length > 1) {
sorters.push(c => c.emoji.shortcodes[0].length);
sorters.push((c) => c.emoji.shortcodes[0].length);
}
// Finally, sort by original ordering
sorters.push(c => c._orderBy);
sorters.push((c) => c._orderBy);
completions = sortBy<ISortedEmoji>(uniq(completions), sorters);
completions = completions.slice(0, LIMIT);
// Do a second sort to place emoji matching with frequently used one on top
sorters = [];
this.recentlyUsed.forEach(emoji => {
sorters.push(c => score(emoji.shortcodes[0], c.emoji.shortcodes[0]));
this.recentlyUsed.forEach((emoji) => {
sorters.push((c) => score(emoji.shortcodes[0], c.emoji.shortcodes[0]));
});
completions = sortBy<ISortedEmoji>(uniq(completions), sorters);
return completions.map(c => ({
return completions.map((c) => ({
completion: c.emoji.unicode,
component: (
<PillCompletion title={`:${c.emoji.shortcodes[0]}:`} aria-label={c.emoji.unicode}>
<span>{ c.emoji.unicode }</span>
<span>{c.emoji.unicode}</span>
</PillCompletion>
),
range,
@ -157,7 +155,7 @@ export default class EmojiProvider extends AutocompleteProvider {
}
getName() {
return '😃 ' + _t('Emoji');
return "😃 " + _t("Emoji");
}
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
@ -167,7 +165,7 @@ export default class EmojiProvider extends AutocompleteProvider {
role="presentation"
aria-label={_t("Emoji Autocomplete")}
>
{ completions }
{completions}
</div>
);
}

View file

@ -14,16 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import AutocompleteProvider from './AutocompleteProvider';
import { _t } from '../languageHandler';
import { MatrixClientPeg } from '../MatrixClientPeg';
import { PillCompletion } from './Components';
import AutocompleteProvider from "./AutocompleteProvider";
import { _t } from "../languageHandler";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { PillCompletion } from "./Components";
import { ICompletion, ISelectionRange } from "./Autocompleter";
import RoomAvatar from '../components/views/avatars/RoomAvatar';
import { TimelineRenderingType } from '../contexts/RoomContext';
import RoomAvatar from "../components/views/avatars/RoomAvatar";
import { TimelineRenderingType } from "../contexts/RoomContext";
const AT_ROOM_REGEX = /@\S*/g;
@ -32,38 +32,36 @@ export default class NotifProvider extends AutocompleteProvider {
super({ commandRegex: AT_ROOM_REGEX, renderingType });
}
async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
async getCompletions(query: string, selection: ISelectionRange, force = false, limit = -1): Promise<ICompletion[]> {
const client = MatrixClientPeg.get();
if (!this.room.currentState.mayTriggerNotifOfType('room', client.credentials.userId)) return [];
if (!this.room.currentState.mayTriggerNotifOfType("room", client.credentials.userId)) return [];
const { command, range } = this.getCurrentCommand(query, selection, force);
if (command?.[0].length > 1 &&
['@room', '@channel', '@everyone', '@here'].some(c => c.startsWith(command[0]))
if (
command?.[0].length > 1 &&
["@room", "@channel", "@everyone", "@here"].some((c) => c.startsWith(command[0]))
) {
return [{
completion: '@room',
completionId: '@room',
type: "at-room",
suffix: ' ',
component: (
<PillCompletion title="@room" description={_t("Notify the whole room")}>
<RoomAvatar width={24} height={24} room={this.room} />
</PillCompletion>
),
range,
}];
return [
{
completion: "@room",
completionId: "@room",
type: "at-room",
suffix: " ",
component: (
<PillCompletion title="@room" description={_t("Notify the whole room")}>
<RoomAvatar width={24} height={24} room={this.room} />
</PillCompletion>
),
range,
},
];
}
return [];
}
getName() {
return '❗️ ' + _t('Room Notification');
return "❗️ " + _t("Room Notification");
}
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
@ -73,7 +71,7 @@ export default class NotifProvider extends AutocompleteProvider {
role="presentation"
aria-label={_t("Notification Autocomplete")}
>
{ completions }
{completions}
</div>
);
}

View file

@ -16,10 +16,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { at, uniq } from 'lodash';
import { at, uniq } from "lodash";
import { removeHiddenChars } from "matrix-js-sdk/src/utils";
import { TimelineRenderingType } from '../contexts/RoomContext';
import { TimelineRenderingType } from "../contexts/RoomContext";
import { Leaves } from "../@types/common";
interface IOptions<T extends {}> {
@ -47,7 +47,7 @@ interface IOptions<T extends {}> {
*/
export default class QueryMatcher<T extends {}> {
private _options: IOptions<T>;
private _items: Map<string, {object: T, keyWeight: number}[]>;
private _items: Map<string, { object: T; keyWeight: number }[]>;
constructor(objects: T[], options: IOptions<T> = { keys: [] }) {
this._options = options;
@ -99,7 +99,7 @@ export default class QueryMatcher<T extends {}> {
match(query: string, limit = -1): T[] {
query = this.processQuery(query);
if (this._options.shouldMatchWordsOnly) {
query = query.replace(/[^\w]/g, '');
query = query.replace(/[^\w]/g, "");
}
if (query.length === 0) {
return [];
@ -111,13 +111,11 @@ export default class QueryMatcher<T extends {}> {
for (const [key, candidates] of this._items.entries()) {
let resultKey = key;
if (this._options.shouldMatchWordsOnly) {
resultKey = resultKey.replace(/[^\w]/g, '');
resultKey = resultKey.replace(/[^\w]/g, "");
}
const index = resultKey.indexOf(query);
if (index !== -1) {
matches.push(
...candidates.map((candidate) => ({ index, ...candidate })),
);
matches.push(...candidates.map((candidate) => ({ index, ...candidate })));
}
}

View file

@ -20,14 +20,14 @@ import React from "react";
import { sortBy, uniqBy } from "lodash";
import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import { MatrixClientPeg } from '../MatrixClientPeg';
import QueryMatcher from './QueryMatcher';
import { PillCompletion } from './Components';
import { _t } from "../languageHandler";
import AutocompleteProvider from "./AutocompleteProvider";
import { MatrixClientPeg } from "../MatrixClientPeg";
import QueryMatcher from "./QueryMatcher";
import { PillCompletion } from "./Components";
import { makeRoomPermalink } from "../utils/permalinks/Permalinks";
import { ICompletion, ISelectionRange } from "./Autocompleter";
import RoomAvatar from '../components/views/avatars/RoomAvatar';
import RoomAvatar from "../components/views/avatars/RoomAvatar";
import { TimelineRenderingType } from "../contexts/RoomContext";
const ROOM_REGEX = /\B#\S*/g;
@ -51,7 +51,7 @@ export default class RoomProvider extends AutocompleteProvider {
constructor(room: Room, renderingType?: TimelineRenderingType) {
super({ commandRegex: ROOM_REGEX, renderingType });
this.matcher = new QueryMatcher([], {
keys: ['displayedAlias', 'matchName'],
keys: ["displayedAlias", "matchName"],
});
}
@ -59,15 +59,10 @@ export default class RoomProvider extends AutocompleteProvider {
const cli = MatrixClientPeg.get();
// filter out spaces here as they get their own autocomplete provider
return cli.getVisibleRooms().filter(r => !r.isSpaceRoom());
return cli.getVisibleRooms().filter((r) => !r.isSpaceRoom());
}
async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
async getCompletions(query: string, selection: ISelectionRange, force = false, limit = -1): Promise<ICompletion[]> {
let completions = [];
const { command, range } = this.getCurrentCommand(query, selection, force);
if (command) {
@ -77,7 +72,7 @@ export default class RoomProvider extends AutocompleteProvider {
aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name));
}
if (room.getAltAliases().length) {
const altAliases = room.getAltAliases().map(alias => matcherObject(room, alias));
const altAliases = room.getAltAliases().map((alias) => matcherObject(room, alias));
aliases = aliases.concat(altAliases);
}
return aliases;
@ -85,9 +80,9 @@ export default class RoomProvider extends AutocompleteProvider {
// Filter out any matches where the user will have also autocompleted new rooms
matcherObjects = matcherObjects.filter((r) => {
const tombstone = r.room.currentState.getStateEvents("m.room.tombstone", "");
if (tombstone && tombstone.getContent() && tombstone.getContent()['replacement_room']) {
if (tombstone && tombstone.getContent() && tombstone.getContent()["replacement_room"]) {
const hasReplacementRoom = matcherObjects.some(
(r2) => r2.room.roomId === tombstone.getContent()['replacement_room'],
(r2) => r2.room.roomId === tombstone.getContent()["replacement_room"],
);
return !hasReplacementRoom;
}
@ -102,27 +97,29 @@ export default class RoomProvider extends AutocompleteProvider {
(c) => c.displayedAlias.length,
]);
completions = uniqBy(completions, (match) => match.room);
completions = completions.map((room) => {
return {
completion: room.displayedAlias,
completionId: room.room.roomId,
type: "room",
suffix: ' ',
href: makeRoomPermalink(room.displayedAlias),
component: (
<PillCompletion title={room.room.name} description={room.displayedAlias}>
<RoomAvatar width={24} height={24} room={room.room} />
</PillCompletion>
),
range,
};
}).filter((completion) => !!completion.completion && completion.completion.length > 0);
completions = completions
.map((room) => {
return {
completion: room.displayedAlias,
completionId: room.room.roomId,
type: "room",
suffix: " ",
href: makeRoomPermalink(room.displayedAlias),
component: (
<PillCompletion title={room.room.name} description={room.displayedAlias}>
<RoomAvatar width={24} height={24} room={room.room} />
</PillCompletion>
),
range,
};
})
.filter((completion) => !!completion.completion && completion.completion.length > 0);
}
return completions;
}
getName() {
return _t('Rooms');
return _t("Rooms");
}
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
@ -132,7 +129,7 @@ export default class RoomProvider extends AutocompleteProvider {
role="presentation"
aria-label={_t("Room Autocomplete")}
>
{ completions }
{completions}
</div>
);
}

View file

@ -16,13 +16,15 @@ limitations under the License.
import React from "react";
import { _t } from '../languageHandler';
import { MatrixClientPeg } from '../MatrixClientPeg';
import { _t } from "../languageHandler";
import { MatrixClientPeg } from "../MatrixClientPeg";
import RoomProvider from "./RoomProvider";
export default class SpaceProvider extends RoomProvider {
protected getRooms() {
return MatrixClientPeg.get().getVisibleRooms().filter(r => r.isSpaceRoom());
return MatrixClientPeg.get()
.getVisibleRooms()
.filter((r) => r.isSpaceRoom());
}
getName() {
@ -36,7 +38,7 @@ export default class SpaceProvider extends RoomProvider {
role="listbox"
aria-label={_t("Space Autocomplete")}
>
{ completions }
{completions}
</div>
);
}

View file

@ -17,24 +17,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { sortBy } from 'lodash';
import React from "react";
import { sortBy } from "lodash";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { RoomState, RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set";
import { MatrixClientPeg } from '../MatrixClientPeg';
import QueryMatcher from './QueryMatcher';
import { PillCompletion } from './Components';
import AutocompleteProvider from './AutocompleteProvider';
import { _t } from '../languageHandler';
import { MatrixClientPeg } from "../MatrixClientPeg";
import QueryMatcher from "./QueryMatcher";
import { PillCompletion } from "./Components";
import AutocompleteProvider from "./AutocompleteProvider";
import { _t } from "../languageHandler";
import { makeUserPermalink } from "../utils/permalinks/Permalinks";
import { ICompletion, ISelectionRange } from "./Autocompleter";
import MemberAvatar from '../components/views/avatars/MemberAvatar';
import { TimelineRenderingType } from '../contexts/RoomContext';
import UserIdentifierCustomisations from '../customisations/UserIdentifier';
import MemberAvatar from "../components/views/avatars/MemberAvatar";
import { TimelineRenderingType } from "../contexts/RoomContext";
import UserIdentifierCustomisations from "../customisations/UserIdentifier";
const USER_REGEX = /\B@\S*/g;
@ -55,8 +55,8 @@ export default class UserProvider extends AutocompleteProvider {
});
this.room = room;
this.matcher = new QueryMatcher([], {
keys: ['name'],
funcs: [obj => obj.userId.slice(1)], // index by user id minus the leading '@'
keys: ["name"],
funcs: [(obj) => obj.userId.slice(1)], // index by user id minus the leading '@'
shouldMatchWordsOnly: false,
});
@ -117,21 +117,22 @@ export default class UserProvider extends AutocompleteProvider {
const fullMatch = command[0];
// Don't search if the query is a single "@"
if (fullMatch && fullMatch !== '@') {
if (fullMatch && fullMatch !== "@") {
// Don't include the '@' in our search query - it's only used as a way to trigger completion
const query = fullMatch.startsWith('@') ? fullMatch.substring(1) : fullMatch;
const query = fullMatch.startsWith("@") ? fullMatch.substring(1) : fullMatch;
completions = this.matcher.match(query, limit).map((user) => {
const description = UserIdentifierCustomisations.getDisplayUserIdentifier(
user.userId, { roomId: this.room.roomId, withDisplayName: true },
);
const displayName = (user.name || user.userId || '');
const description = UserIdentifierCustomisations.getDisplayUserIdentifier(user.userId, {
roomId: this.room.roomId,
withDisplayName: true,
});
const displayName = user.name || user.userId || "";
return {
// Length of completion should equal length of text in decorator. draft-js
// relies on the length of the entity === length of the text in the decoration.
completion: user.rawDisplayName,
completionId: user.userId,
type: "user",
suffix: (selection.beginning && range.start === 0) ? ': ' : ' ',
suffix: selection.beginning && range.start === 0 ? ": " : " ",
href: makeUserPermalink(user.userId),
component: (
<PillCompletion title={displayName} description={description}>
@ -146,7 +147,7 @@ export default class UserProvider extends AutocompleteProvider {
}
getName(): string {
return _t('Users');
return _t("Users");
}
private makeUsers() {
@ -161,7 +162,7 @@ export default class UserProvider extends AutocompleteProvider {
this.users = this.room.getJoinedMembers().filter(({ userId }) => userId !== currentUserId);
this.users = this.users.concat(this.room.getMembersWithMembership("invite"));
this.users = sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20);
this.users = sortBy(this.users, (member) => 1e20 - lastSpoken[member.userId] || 1e20);
this.matcher.setObjects(this.users);
}
@ -173,7 +174,9 @@ export default class UserProvider extends AutocompleteProvider {
// Move the user that spoke to the front of the array
this.users.splice(
this.users.findIndex((user2) => user2.userId === user.userId), 1);
this.users.findIndex((user2) => user2.userId === user.userId),
1,
);
this.users = [user, ...this.users];
this.matcher.setObjects(this.users);
@ -186,7 +189,7 @@ export default class UserProvider extends AutocompleteProvider {
role="presentation"
aria-label={_t("User Autocomplete")}
>
{ completions }
{completions}
</div>
);
}

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