Apply prettier formatting
This commit is contained in:
parent
1cac306093
commit
526645c791
1576 changed files with 65385 additions and 62478 deletions
|
@ -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];
|
||||
|
||||
|
|
3
src/@types/diff-dom.d.ts
vendored
3
src/@types/diff-dom.d.ts
vendored
|
@ -26,8 +26,7 @@ declare module "diff-dom" {
|
|||
newValue: string;
|
||||
}
|
||||
|
||||
interface IOpts {
|
||||
}
|
||||
interface IOpts {}
|
||||
|
||||
export class DiffDOM {
|
||||
public constructor(opts?: IOpts);
|
||||
|
|
19
src/@types/global.d.ts
vendored
19
src/@types/global.d.ts
vendored
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
2
src/@types/raw-loader.d.ts
vendored
2
src/@types/raw-loader.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
|
2
src/@types/sanitize-html.d.ts
vendored
2
src/@types/sanitize-html.d.ts
vendored
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -57,4 +57,3 @@ export class BlurhashEncoder {
|
|||
return deferred.promise;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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?: {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -48,4 +48,3 @@ export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: n
|
|||
return Math.floor(heightMulti * fullHeight);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
364
src/Lifecycle.ts
364
src/Lifecycle.ts
|
@ -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,
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
55
src/Login.ts
55
src/Login.ts
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)}</>;
|
||||
}
|
||||
}
|
||||
|
|
126
src/Notifier.ts
126
src/Notifier.ts
|
@ -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,
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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" });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
12
src/Roles.ts
12
src/Roles.ts
|
@ -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"),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
12
src/Rooms.ts
12
src/Rooms.ts
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
|
|
41
src/Terms.ts
41
src/Terms.ts
|
@ -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) {
|
||||
|
|
|
@ -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)?.() || "";
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -45,7 +45,7 @@ export const ContextMenuButton: React.FC<IProps> = ({
|
|||
aria-haspopup={true}
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
{ children }
|
||||
{children}
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -42,7 +42,7 @@ export const ContextMenuTooltipButton: React.FC<IProps> = ({
|
|||
aria-expanded={isExpanded}
|
||||
forceHide={isExpanded}
|
||||
>
|
||||
{ children }
|
||||
{children}
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -36,7 +36,7 @@ export const MenuItemCheckbox: React.FC<IProps> = ({ children, label, active, di
|
|||
disabled={disabled}
|
||||
aria-label={label}
|
||||
>
|
||||
{ children }
|
||||
{children}
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -36,7 +36,7 @@ export const MenuItemRadio: React.FC<IProps> = ({ children, label, active, disab
|
|||
disabled={disabled}
|
||||
aria-label={label}
|
||||
>
|
||||
{ children }
|
||||
{children}
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -79,7 +79,7 @@ export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, label, onCh
|
|||
inputRef={ref}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
>
|
||||
{ children }
|
||||
{children}
|
||||
</StyledCheckbox>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -79,7 +79,7 @@ export const StyledMenuItemRadio: React.FC<IProps> = ({ children, label, onChang
|
|||
inputRef={ref}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
>
|
||||
{ children }
|
||||
{children}
|
||||
</StyledRadioButton>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 })));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue