Merge pull request #5912 from matrix-org/jryans/convert-flow-to-ts

Convert some Flow-typed files to TypeScript
This commit is contained in:
J. Ryan Stinnett 2021-04-27 13:44:47 +01:00 committed by GitHub
commit dd8abb0206
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1062 additions and 849 deletions

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -40,6 +40,8 @@ import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import VoipUserMapper from "../VoipUserMapper"; import VoipUserMapper from "../VoipUserMapper";
import {SpaceStoreClass} from "../stores/SpaceStore"; import {SpaceStoreClass} from "../stores/SpaceStore";
import {VoiceRecording} from "../voice/VoiceRecording"; import {VoiceRecording} from "../voice/VoiceRecording";
import TypingStore from "../stores/TypingStore";
import { EventIndexPeg } from "../indexing/EventIndexPeg";
declare global { declare global {
interface Window { interface Window {
@ -72,11 +74,15 @@ declare global {
mxVoipUserMapper: VoipUserMapper; mxVoipUserMapper: VoipUserMapper;
mxSpaceStore: SpaceStoreClass; mxSpaceStore: SpaceStoreClass;
mxVoiceRecorder: typeof VoiceRecording; mxVoiceRecorder: typeof VoiceRecording;
mxTypingStore: TypingStore;
mxEventIndexPeg: EventIndexPeg;
} }
interface Document { interface Document {
// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess // https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess
hasStorageAccess?: () => Promise<boolean>; hasStorageAccess?: () => Promise<boolean>;
// https://developer.mozilla.org/en-US/docs/Web/API/Document/requestStorageAccess
requestStorageAccess?: () => Promise<undefined>;
// Safari & IE11 only have this prefixed: we used prefixed versions // Safari & IE11 only have this prefixed: we used prefixed versions
// previously so let's continue to support them for now // previously so let's continue to support them for now

View file

@ -673,7 +673,7 @@ export default class CallHandler {
call.placeScreenSharingCall( call.placeScreenSharingCall(
remoteElement, remoteElement,
localElement, localElement,
async () : Promise<DesktopCapturerSource> => { async (): Promise<DesktopCapturerSource> => {
const {finished} = Modal.createDialog(DesktopCapturerSourcePicker); const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished; const [source] = await finished;
return source; return source;

View file

@ -231,8 +231,10 @@ export class KeyBindingsManager {
/** /**
* Finds a matching KeyAction for a given KeyboardEvent * Finds a matching KeyAction for a given KeyboardEvent
*/ */
private getAction<T extends string>(getters: KeyBindingGetter<T>[], ev: KeyboardEvent | React.KeyboardEvent) private getAction<T extends string>(
: T | undefined { getters: KeyBindingGetter<T>[],
ev: KeyboardEvent | React.KeyboardEvent,
): T | undefined {
for (const getter of getters) { for (const getter of getters) {
const bindings = getter(); const bindings = getter();
const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac)); const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac));

View file

@ -1,9 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015-2021 The Matrix.org Foundation C.I.C.
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -59,7 +56,7 @@ export type LoginFlow = ISSOFlow | IPasswordFlow;
// TODO: Move this to JS SDK // TODO: Move this to JS SDK
/* eslint-disable camelcase */ /* eslint-disable camelcase */
interface ILoginParams { interface ILoginParams {
identifier?: string; identifier?: object;
password?: string; password?: string;
token?: string; token?: string;
device_id?: string; device_id?: string;

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,13 +16,14 @@ limitations under the License.
import url from 'url'; import url from 'url';
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; import { Service, startTermsFlow, TermsInteractionCallback, TermsNotSignedError } from './Terms';
import {MatrixClientPeg} from "./MatrixClientPeg"; import {MatrixClientPeg} from "./MatrixClientPeg";
import request from "browser-request"; import request from "browser-request";
import SdkConfig from "./SdkConfig"; import SdkConfig from "./SdkConfig";
import {WidgetType} from "./widgets/WidgetType"; import {WidgetType} from "./widgets/WidgetType";
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types"; import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
import { Room } from "matrix-js-sdk/src/models/room";
// The version of the integration manager API we're intending to work with // The version of the integration manager API we're intending to work with
const imApiVersion = "1.1"; const imApiVersion = "1.1";
@ -31,9 +31,11 @@ const imApiVersion = "1.1";
// TODO: Generify the name of this class and all components within - it's not just for Scalar. // TODO: Generify the name of this class and all components within - it's not just for Scalar.
export default class ScalarAuthClient { export default class ScalarAuthClient {
constructor(apiUrl, uiUrl) { private scalarToken: string;
this.apiUrl = apiUrl; private termsInteractionCallback: TermsInteractionCallback;
this.uiUrl = uiUrl; private isDefaultManager: boolean;
constructor(private apiUrl: string, private uiUrl: string) {
this.scalarToken = null; this.scalarToken = null;
// `undefined` to allow `startTermsFlow` to fallback to a default // `undefined` to allow `startTermsFlow` to fallback to a default
// callback if this is unset. // callback if this is unset.
@ -46,7 +48,7 @@ export default class ScalarAuthClient {
this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl; this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl;
} }
_writeTokenToStore() { private writeTokenToStore() {
window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken); window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken);
if (this.isDefaultManager) { if (this.isDefaultManager) {
// We remove the old token from storage to migrate upwards. This is safe // We remove the old token from storage to migrate upwards. This is safe
@ -56,7 +58,7 @@ export default class ScalarAuthClient {
} }
} }
_readTokenFromStore() { private readTokenFromStore(): string {
let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl); let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl);
if (!token && this.isDefaultManager) { if (!token && this.isDefaultManager) {
token = window.localStorage.getItem("mx_scalar_token"); token = window.localStorage.getItem("mx_scalar_token");
@ -64,33 +66,33 @@ export default class ScalarAuthClient {
return token; return token;
} }
_readToken() { private readToken(): string {
if (this.scalarToken) return this.scalarToken; if (this.scalarToken) return this.scalarToken;
return this._readTokenFromStore(); return this.readTokenFromStore();
} }
setTermsInteractionCallback(callback) { setTermsInteractionCallback(callback) {
this.termsInteractionCallback = callback; this.termsInteractionCallback = callback;
} }
connect() { connect(): Promise<void> {
return this.getScalarToken().then((tok) => { return this.getScalarToken().then((tok) => {
this.scalarToken = tok; this.scalarToken = tok;
}); });
} }
hasCredentials() { hasCredentials(): boolean {
return this.scalarToken != null; // undef or null return this.scalarToken != null; // undef or null
} }
// Returns a promise that resolves to a scalar_token string // Returns a promise that resolves to a scalar_token string
getScalarToken() { getScalarToken(): Promise<string> {
const token = this._readToken(); const token = this.readToken();
if (!token) { if (!token) {
return this.registerForToken(); return this.registerForToken();
} else { } else {
return this._checkToken(token).catch((e) => { return this.checkToken(token).catch((e) => {
if (e instanceof TermsNotSignedError) { if (e instanceof TermsNotSignedError) {
// retrying won't help this // retrying won't help this
throw e; throw e;
@ -100,7 +102,7 @@ export default class ScalarAuthClient {
} }
} }
_getAccountName(token) { private getAccountName(token: string): Promise<string> {
const url = this.apiUrl + "/account"; const url = this.apiUrl + "/account";
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
@ -125,8 +127,8 @@ export default class ScalarAuthClient {
}); });
} }
_checkToken(token) { private checkToken(token: string): Promise<string> {
return this._getAccountName(token).then(userId => { return this.getAccountName(token).then(userId => {
const me = MatrixClientPeg.get().getUserId(); const me = MatrixClientPeg.get().getUserId();
if (userId !== me) { if (userId !== me) {
throw new Error("Scalar token is owned by someone else: " + me); throw new Error("Scalar token is owned by someone else: " + me);
@ -154,7 +156,7 @@ export default class ScalarAuthClient {
parsedImRestUrl.pathname = ''; parsedImRestUrl.pathname = '';
return startTermsFlow([new Service( return startTermsFlow([new Service(
SERVICE_TYPES.IM, SERVICE_TYPES.IM,
parsedImRestUrl.format(), url.format(parsedImRestUrl),
token, token,
)], this.termsInteractionCallback).then(() => { )], this.termsInteractionCallback).then(() => {
return token; return token;
@ -165,22 +167,22 @@ export default class ScalarAuthClient {
}); });
} }
registerForToken() { registerForToken(): Promise<string> {
// Get openid bearer token from the HS as the first part of our dance // Get openid bearer token from the HS as the first part of our dance
return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => { return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => {
// Now we can send that to scalar and exchange it for a scalar token // Now we can send that to scalar and exchange it for a scalar token
return this.exchangeForScalarToken(tokenObject); return this.exchangeForScalarToken(tokenObject);
}).then((token) => { }).then((token) => {
// Validate it (this mostly checks to see if the IM needs us to agree to some terms) // Validate it (this mostly checks to see if the IM needs us to agree to some terms)
return this._checkToken(token); return this.checkToken(token);
}).then((token) => { }).then((token) => {
this.scalarToken = token; this.scalarToken = token;
this._writeTokenToStore(); this.writeTokenToStore();
return token; return token;
}); });
} }
exchangeForScalarToken(openidTokenObject) { exchangeForScalarToken(openidTokenObject: any): Promise<string> {
const scalarRestUrl = this.apiUrl; const scalarRestUrl = this.apiUrl;
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
@ -194,7 +196,7 @@ export default class ScalarAuthClient {
if (err) { if (err) {
reject(err); reject(err);
} else if (response.statusCode / 100 !== 2) { } else if (response.statusCode / 100 !== 2) {
reject({statusCode: response.statusCode}); reject(new Error(`Scalar request failed: ${response.statusCode}`));
} else if (!body || !body.scalar_token) { } else if (!body || !body.scalar_token) {
reject(new Error("Missing scalar_token in response")); reject(new Error("Missing scalar_token in response"));
} else { } else {
@ -204,7 +206,7 @@ export default class ScalarAuthClient {
}); });
} }
getScalarPageTitle(url) { getScalarPageTitle(url: string): Promise<string> {
let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup'; let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup';
scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl); scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl);
scalarPageLookupUrl += '&curl=' + encodeURIComponent(url); scalarPageLookupUrl += '&curl=' + encodeURIComponent(url);
@ -218,7 +220,7 @@ export default class ScalarAuthClient {
if (err) { if (err) {
reject(err); reject(err);
} else if (response.statusCode / 100 !== 2) { } else if (response.statusCode / 100 !== 2) {
reject({statusCode: response.statusCode}); reject(new Error(`Scalar request failed: ${response.statusCode}`));
} else if (!body) { } else if (!body) {
reject(new Error("Missing page title in response")); reject(new Error("Missing page title in response"));
} else { } else {
@ -240,10 +242,10 @@ export default class ScalarAuthClient {
* @param {string} widgetId The widget ID to disable assets for * @param {string} widgetId The widget ID to disable assets for
* @return {Promise} Resolves on completion * @return {Promise} Resolves on completion
*/ */
disableWidgetAssets(widgetType: WidgetType, widgetId) { disableWidgetAssets(widgetType: WidgetType, widgetId: string): Promise<void> {
let url = this.apiUrl + '/widgets/set_assets_state'; let url = this.apiUrl + '/widgets/set_assets_state';
url = this.getStarterLink(url); url = this.getStarterLink(url);
return new Promise((resolve, reject) => { return new Promise<void>((resolve, reject) => {
request({ request({
method: 'GET', // XXX: Actions shouldn't be GET requests method: 'GET', // XXX: Actions shouldn't be GET requests
uri: url, uri: url,
@ -257,7 +259,7 @@ export default class ScalarAuthClient {
if (err) { if (err) {
reject(err); reject(err);
} else if (response.statusCode / 100 !== 2) { } else if (response.statusCode / 100 !== 2) {
reject({statusCode: response.statusCode}); reject(new Error(`Scalar request failed: ${response.statusCode}`));
} else if (!body) { } else if (!body) {
reject(new Error("Failed to set widget assets state")); reject(new Error("Failed to set widget assets state"));
} else { } else {
@ -267,7 +269,7 @@ export default class ScalarAuthClient {
}); });
} }
getScalarInterfaceUrlForRoom(room, screen, id) { getScalarInterfaceUrlForRoom(room: Room, screen: string, id: string): string {
const roomId = room.roomId; const roomId = room.roomId;
const roomName = room.name; const roomName = room.name;
let url = this.uiUrl; let url = this.uiUrl;
@ -284,7 +286,7 @@ export default class ScalarAuthClient {
return url; return url;
} }
getStarterLink(starterLinkUrl) { getStarterLink(starterLinkUrl: string): string {
return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken); return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken);
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,7 +17,7 @@ limitations under the License.
import classNames from 'classnames'; import classNames from 'classnames';
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import * as sdk from './'; import * as sdk from '.';
import Modal from './Modal'; import Modal from './Modal';
export class TermsNotSignedError extends Error {} export class TermsNotSignedError extends Error {}
@ -32,13 +32,30 @@ export class Service {
* @param {string} baseUrl The Base URL of the service (ie. before '/_matrix') * @param {string} baseUrl The Base URL of the service (ie. before '/_matrix')
* @param {string} accessToken The user's access token for the service * @param {string} accessToken The user's access token for the service
*/ */
constructor(serviceType, baseUrl, accessToken) { constructor(public serviceType: string, public baseUrl: string, public accessToken: string) {
this.serviceType = serviceType;
this.baseUrl = baseUrl;
this.accessToken = accessToken;
} }
} }
interface Policy {
// @ts-ignore: No great way to express indexed types together with other keys
version: string;
[lang: string]: {
url: string;
};
}
type Policies = {
[policy: string]: Policy,
};
export type TermsInteractionCallback = (
policiesAndServicePairs: {
service: Service,
policies: Policies,
}[],
agreedUrls: string[],
extraClassNames?: string,
) => Promise<string[]>;
/** /**
* Start a flow where the user is presented with terms & conditions for some services * Start a flow where the user is presented with terms & conditions for some services
* *
@ -51,8 +68,8 @@ export class Service {
* if they cancel. * if they cancel.
*/ */
export async function startTermsFlow( export async function startTermsFlow(
services, services: Service[],
interactionCallback = dialogTermsInteractionCallback, interactionCallback: TermsInteractionCallback = dialogTermsInteractionCallback,
) { ) {
const termsPromises = services.map( const termsPromises = services.map(
(s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl), (s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl),
@ -77,7 +94,7 @@ export async function startTermsFlow(
* } * }
*/ */
const terms = await Promise.all(termsPromises); 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 // fetch the set of agreed policy URLs from account data
@ -158,10 +175,13 @@ export async function startTermsFlow(
} }
export function dialogTermsInteractionCallback( export function dialogTermsInteractionCallback(
policiesAndServicePairs, policiesAndServicePairs: {
agreedUrls, service: Service,
extraClassNames, policies: { [policy: string]: Policy },
) { }[],
agreedUrls: string[],
extraClassNames?: string,
): Promise<string[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
console.log("Terms that need agreement", policiesAndServicePairs); console.log("Terms that need agreement", policiesAndServicePairs);
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog"); const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,7 +16,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import * as sdk from '../../../../index'; import * as sdk from '../../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import SdkConfig from '../../../../SdkConfig'; import SdkConfig from '../../../../SdkConfig';
import SettingsStore from "../../../../settings/SettingsStore"; import SettingsStore from "../../../../settings/SettingsStore";
@ -26,14 +25,23 @@ import {formatBytes, formatCountLong} from "../../../../utils/FormattingUtils";
import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import EventIndexPeg from "../../../../indexing/EventIndexPeg";
import {SettingLevel} from "../../../../settings/SettingLevel"; import {SettingLevel} from "../../../../settings/SettingLevel";
interface IProps {
onFinished: (confirmed: boolean) => void;
}
interface IState {
eventIndexSize: number;
eventCount: number;
crawlingRoomsCount: number;
roomCount: number;
currentRoom: string;
crawlerSleepTime: number;
}
/* /*
* Allows the user to introspect the event index state and disable it. * Allows the user to introspect the event index state and disable it.
*/ */
export default class ManageEventIndexDialog extends React.Component { export default class ManageEventIndexDialog extends React.Component<IProps, IState> {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
constructor(props) { constructor(props) {
super(props); super(props);
@ -84,7 +92,7 @@ export default class ManageEventIndexDialog extends React.Component {
} }
} }
async componentDidMount(): void { async componentDidMount(): Promise<void> {
let eventIndexSize = 0; let eventIndexSize = 0;
let crawlingRoomsCount = 0; let crawlingRoomsCount = 0;
let roomCount = 0; let roomCount = 0;
@ -123,14 +131,14 @@ export default class ManageEventIndexDialog extends React.Component {
}); });
} }
_onDisable = async () => { private onDisable = async () => {
Modal.createTrackedDialogAsync("Disable message search", "Disable message search", Modal.createTrackedDialogAsync("Disable message search", "Disable message search",
import("./DisableEventIndexDialog"), import("./DisableEventIndexDialog"),
null, null, /* priority = */ false, /* static = */ true, null, null, /* priority = */ false, /* static = */ true,
); );
}; };
_onCrawlerSleepTimeChange = (e) => { private onCrawlerSleepTimeChange = (e) => {
this.setState({crawlerSleepTime: e.target.value}); this.setState({crawlerSleepTime: e.target.value});
SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value); SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value);
}; };
@ -144,7 +152,7 @@ export default class ManageEventIndexDialog extends React.Component {
crawlerState = _t("Not currently indexing messages for any room."); crawlerState = _t("Not currently indexing messages for any room.");
} else { } else {
crawlerState = ( crawlerState = (
_t("Currently indexing: %(currentRoom)s", { currentRoom: this.state.currentRoom }) _t("Currently indexing: %(currentRoom)s", { currentRoom: this.state.currentRoom })
); );
} }
@ -169,7 +177,7 @@ export default class ManageEventIndexDialog extends React.Component {
label={_t('Message downloading sleep time(ms)')} label={_t('Message downloading sleep time(ms)')}
type='number' type='number'
value={this.state.crawlerSleepTime} value={this.state.crawlerSleepTime}
onChange={this._onCrawlerSleepTimeChange} /> onChange={this.onCrawlerSleepTimeChange} />
</div> </div>
</div> </div>
); );
@ -188,7 +196,7 @@ export default class ManageEventIndexDialog extends React.Component {
onPrimaryButtonClick={this.props.onFinished} onPrimaryButtonClick={this.props.onFinished}
primaryButtonClass="primary" primaryButtonClass="primary"
cancelButton={_t("Disable")} cancelButton={_t("Disable")}
onCancel={this._onDisable} onCancel={this.onDisable}
cancelButtonClass="danger" cancelButtonClass="danger"
/> />
</BaseDialog> </BaseDialog>

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2015, 2016, 2017, 2018, 2019 The Matrix.org Foundation C.I.C. Copyright 2015-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -94,7 +94,7 @@ interface IState {
// be seeing. // be seeing.
serverIsAlive: boolean; serverIsAlive: boolean;
serverErrorIsFatal: boolean; serverErrorIsFatal: boolean;
serverDeadError: string; serverDeadError?: ReactNode;
} }
/* /*

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2015, 2016, 2017, 2018, 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2015-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -95,7 +95,7 @@ interface IState {
// be seeing. // be seeing.
serverIsAlive: boolean; serverIsAlive: boolean;
serverErrorIsFatal: boolean; serverErrorIsFatal: boolean;
serverDeadError: string; serverDeadError?: ReactNode;
// Our matrix client - part of state because we can't render the UI auth // Our matrix client - part of state because we can't render the UI auth
// component without it. // component without it.

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,14 +15,13 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import {_t} from '../../../languageHandler'; import {_t} from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import * as Lifecycle from '../../../Lifecycle'; import * as Lifecycle from '../../../Lifecycle';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {sendLoginRequest} from "../../../Login"; import {ISSOFlow, LoginFlow, sendLoginRequest} from "../../../Login";
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform"; import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
import SSOButtons from "../../views/elements/SSOButtons"; import SSOButtons from "../../views/elements/SSOButtons";
@ -42,26 +41,38 @@ const FLOWS_TO_VIEWS = {
"m.login.sso": LOGIN_VIEW.SSO, "m.login.sso": LOGIN_VIEW.SSO,
}; };
@replaceableComponent("structures.auth.SoftLogout") interface IProps {
export default class SoftLogout extends React.Component { // Query parameters from MatrixChat
static propTypes = { realQueryParams: {
// Query parameters from MatrixChat loginToken?: string;
realQueryParams: PropTypes.object, // {loginToken}
// Called when the SSO login completes
onTokenLoginCompleted: PropTypes.func,
}; };
fragmentAfterLogin?: string;
constructor() { // Called when the SSO login completes
super(); onTokenLoginCompleted: () => void,
}
interface IState {
loginView: number;
keyBackupNeeded: boolean;
busy: boolean;
password: string;
errorText: string;
flows: LoginFlow[];
}
@replaceableComponent("structures.auth.SoftLogout")
export default class SoftLogout extends React.Component<IProps, IState> {
constructor(props) {
super(props);
this.state = { this.state = {
loginView: LOGIN_VIEW.LOADING, loginView: LOGIN_VIEW.LOADING,
keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount) keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount)
busy: false, busy: false,
password: "", password: "",
errorText: "", errorText: "",
flows: [],
}; };
} }
@ -72,7 +83,7 @@ export default class SoftLogout extends React.Component {
return; return;
} }
this._initLogin(); this.initLogin();
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli.isCryptoEnabled()) { if (cli.isCryptoEnabled()) {
@ -94,7 +105,7 @@ export default class SoftLogout extends React.Component {
}); });
}; };
async _initLogin() { private async initLogin() {
const queryParams = this.props.realQueryParams; const queryParams = this.props.realQueryParams;
const hasAllParams = queryParams && queryParams['loginToken']; const hasAllParams = queryParams && queryParams['loginToken'];
if (hasAllParams) { if (hasAllParams) {
@ -189,7 +200,7 @@ export default class SoftLogout extends React.Component {
}); });
} }
_renderSignInSection() { private renderSignInSection() {
if (this.state.loginView === LOGIN_VIEW.LOADING) { if (this.state.loginView === LOGIN_VIEW.LOADING) {
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />; return <Spinner />;
@ -247,7 +258,7 @@ export default class SoftLogout extends React.Component {
} // else we already have a message and should use it (key backup warning) } // else we already have a message and should use it (key backup warning)
const loginType = this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"; const loginType = this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso";
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType); const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
return ( return (
<div> <div>
@ -289,7 +300,7 @@ export default class SoftLogout extends React.Component {
<h3>{_t("Sign in")}</h3> <h3>{_t("Sign in")}</h3>
<div> <div>
{this._renderSignInSection()} {this.renderSignInSection()}
</div> </div>
<h3>{_t("Clear personal data")}</h3> <h3>{_t("Clear personal data")}</h3>

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -110,7 +110,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
console.error(e); console.error(e);
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e); const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
if (stateForError.isFatalError) { if (stateForError.serverErrorIsFatal) {
let error = _t("Unable to validate homeserver"); let error = _t("Unable to validate homeserver");
if (e.translatedMessage) { if (e.translatedMessage) {
error = e.translatedMessage; error = e.translatedMessage;
@ -168,7 +168,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
text = _t("Matrix.org is the biggest public homeserver in the world, so its a good place for many."); text = _t("Matrix.org is the biggest public homeserver in the world, so its a good place for many.");
} }
let defaultServerName = this.defaultServer.hsName; let defaultServerName: React.ReactNode = this.defaultServer.hsName;
if (this.defaultServer.hsNameIsDifferent) { if (this.defaultServer.hsNameIsDifferent) {
defaultServerName = ( defaultServerName = (
<TextWithTooltip class="mx_Login_underlinedServerName" tooltip={this.defaultServer.hsUrl}> <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={this.defaultServer.hsUrl}>

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -67,7 +67,7 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }
</AccessibleButton>; </AccessibleButton>;
} }
let serverName = serverConfig.isNameResolvable ? serverConfig.hsName : serverConfig.hsUrl; let serverName: React.ReactNode = serverConfig.isNameResolvable ? serverConfig.hsName : serverConfig.hsUrl;
if (serverConfig.hsNameIsDifferent) { if (serverConfig.hsNameIsDifferent) {
serverName = <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={serverConfig.hsUrl}> serverName = <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={serverConfig.hsUrl}>
{serverConfig.hsName} {serverConfig.hsName}

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,11 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {createRef} from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import classNames from "classnames"; import classNames from "classnames";
import {EventType} from "matrix-js-sdk/src/@types/event";
import {EventStatus} from 'matrix-js-sdk/src/models/event'; import { EventType } from "matrix-js-sdk/src/@types/event";
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Relations } from "matrix-js-sdk/src/models/relations";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import ReplyThread from "../elements/ReplyThread"; import ReplyThread from "../elements/ReplyThread";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -27,7 +29,7 @@ import * as TextForEvent from "../../../TextForEvent";
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {Layout, LayoutPropType} from "../../../settings/Layout"; import {Layout} from "../../../settings/Layout";
import {formatTime} from "../../../DateUtils"; import {formatTime} from "../../../DateUtils";
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList"; import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
@ -40,6 +42,8 @@ import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStor
import {objectHasDiff} from "../../../utils/objects"; import {objectHasDiff} from "../../../utils/objects";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import Tooltip from "../elements/Tooltip"; import Tooltip from "../elements/Tooltip";
import { EditorStateTransfer } from "../../../utils/EditorStateTransfer";
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
import NotificationBadge from "./NotificationBadge"; import NotificationBadge from "./NotificationBadge";
@ -171,101 +175,130 @@ const MAX_READ_AVATARS = 5;
// | '--------------------------------------' | // | '--------------------------------------' |
// '----------------------------------------------------------' // '----------------------------------------------------------'
interface IReadReceiptProps {
userId: string;
roomMember: RoomMember;
ts: number;
}
interface IProps {
// the MatrixEvent to show
mxEvent: MatrixEvent;
// true if mxEvent is redacted. This is a prop because using mxEvent.isRedacted()
// might not be enough when deciding shouldComponentUpdate - prevProps.mxEvent
// references the same this.props.mxEvent.
isRedacted?: boolean;
// true if this is a continuation of the previous event (which has the
// effect of not showing another avatar/displayname
continuation?: boolean;
// true if this is the last event in the timeline (which has the effect
// of always showing the timestamp)
last?: boolean;
// true if the event is the last event in a section (adds a css class for
// targeting)
lastInSection?: boolean;
// True if the event is the last successful (sent) event.
lastSuccessful?: boolean;
// true if this is search context (which has the effect of greying out
// the text
contextual?: boolean;
// a list of words to highlight, ordered by longest first
highlights?: string[];
// link URL for the highlights
highlightLink?: string;
// should show URL previews for this event
showUrlPreview?: boolean;
// is this the focused event
isSelectedEvent?: boolean;
// callback called when dynamic content in events are loaded
onHeightChanged?: () => void;
// a list of read-receipts we should show. Each object has a 'roomMember' and 'ts'.
readReceipts?: IReadReceiptProps[];
// opaque readreceipt info for each userId; used by ReadReceiptMarker
// to manage its animations. Should be an empty object when the room
// first loads
readReceiptMap?: any;
// A function which is used to check if the parent panel is being
// unmounted, to avoid unnecessary work. Should return true if we
// are being unmounted.
checkUnmounting?: () => boolean;
// the status of this event - ie, mxEvent.status. Denormalised to here so
// that we can tell when it changes.
eventSendStatus?: string;
// the shape of the tile. by default, the layout is intended for the
// normal room timeline. alternative values are: "file_list", "file_grid"
// and "notif". This could be done by CSS, but it'd be horribly inefficient.
// It could also be done by subclassing EventTile, but that'd be quite
// boiilerplatey. So just make the necessary render decisions conditional
// for now.
tileShape?: 'notif' | 'file_grid' | 'reply' | 'reply_preview';
// show twelve hour timestamps
isTwelveHour?: boolean;
// helper function to access relations for this event
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;
// whether to show reactions for this event
showReactions?: boolean;
// which layout to use
layout: Layout;
// whether or not to show flair at all
enableFlair?: boolean;
// whether or not to show read receipts
showReadReceipts?: boolean;
// Used while editing, to pass the event, and to preserve editor state
// from one editor instance to another when remounting the editor
// upon receiving the remote echo for an unsent event.
editState?: EditorStateTransfer;
// Event ID of the event replacing the content of this event, if any
replacingEventId?: string;
// Helper to build permalinks for the room
permalinkCreator?: RoomPermalinkCreator;
}
interface IState {
// Whether the action bar is focused.
actionBarFocused: boolean;
// Whether all read receipts are being displayed. If not, only display
// a truncation of them.
allReadAvatars: boolean;
// Whether the event's sender has been verified.
verified: string;
// Whether onRequestKeysClick has been called since mounting.
previouslyRequestedKeys: boolean;
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions: Relations;
}
@replaceableComponent("views.rooms.EventTile") @replaceableComponent("views.rooms.EventTile")
export default class EventTile extends React.Component { export default class EventTile extends React.Component<IProps, IState> {
static propTypes = { private suppressReadReceiptAnimation: boolean;
/* the MatrixEvent to show */ private isListeningForReceipts: boolean;
mxEvent: PropTypes.object.isRequired, private tile = React.createRef();
private replyThread = React.createRef();
/* true if mxEvent is redacted. This is a prop because using mxEvent.isRedacted()
* might not be enough when deciding shouldComponentUpdate - prevProps.mxEvent
* references the same this.props.mxEvent.
*/
isRedacted: PropTypes.bool,
/* true if this is a continuation of the previous event (which has the
* effect of not showing another avatar/displayname
*/
continuation: PropTypes.bool,
/* true if this is the last event in the timeline (which has the effect
* of always showing the timestamp)
*/
last: PropTypes.bool,
// true if the event is the last event in a section (adds a css class for
// targeting)
lastInSection: PropTypes.bool,
// True if the event is the last successful (sent) event.
isLastSuccessful: PropTypes.bool,
/* true if this is search context (which has the effect of greying out
* the text
*/
contextual: PropTypes.bool,
/* a list of words to highlight, ordered by longest first */
highlights: PropTypes.array,
/* link URL for the highlights */
highlightLink: PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: PropTypes.bool,
/* is this the focused event */
isSelectedEvent: PropTypes.bool,
/* callback called when dynamic content in events are loaded */
onHeightChanged: PropTypes.func,
/* a list of read-receipts we should show. Each object has a 'roomMember' and 'ts'. */
readReceipts: PropTypes.arrayOf(PropTypes.object),
/* opaque readreceipt info for each userId; used by ReadReceiptMarker
* to manage its animations. Should be an empty object when the room
* first loads
*/
readReceiptMap: PropTypes.object,
/* A function which is used to check if the parent panel is being
* unmounted, to avoid unnecessary work. Should return true if we
* are being unmounted.
*/
checkUnmounting: PropTypes.func,
/* the status of this event - ie, mxEvent.status. Denormalised to here so
* that we can tell when it changes. */
eventSendStatus: PropTypes.string,
/* the shape of the tile. by default, the layout is intended for the
* normal room timeline. alternative values are: "file_list", "file_grid"
* and "notif". This could be done by CSS, but it'd be horribly inefficient.
* It could also be done by subclassing EventTile, but that'd be quite
* boiilerplatey. So just make the necessary render decisions conditional
* for now.
*/
tileShape: PropTypes.string,
// show twelve hour timestamps
isTwelveHour: PropTypes.bool,
// helper function to access relations for this event
getRelationsForEvent: PropTypes.func,
// whether to show reactions for this event
showReactions: PropTypes.bool,
// which layout to use
layout: LayoutPropType,
// whether or not to show flair at all
enableFlair: PropTypes.bool,
// whether or not to show read receipts
showReadReceipts: PropTypes.bool,
};
static defaultProps = { static defaultProps = {
// no-op function because onHeightChanged is optional yet some sub-components assume its existence // no-op function because onHeightChanged is optional yet some sub-components assume its existence
@ -292,26 +325,22 @@ export default class EventTile extends React.Component {
}; };
// don't do RR animations until we are mounted // don't do RR animations until we are mounted
this._suppressReadReceiptAnimation = true; this.suppressReadReceiptAnimation = true;
this._tile = createRef();
this._replyThread = createRef();
// Throughout the component we manage a read receipt listener to see if our tile still // Throughout the component we manage a read receipt listener to see if our tile still
// qualifies for a "sent" or "sending" state (based on their relevant conditions). We // qualifies for a "sent" or "sending" state (based on their relevant conditions). We
// don't want to over-subscribe to the read receipt events being fired, so we use a flag // don't want to over-subscribe to the read receipt events being fired, so we use a flag
// to determine if we've already subscribed and use a combination of other flags to find // to determine if we've already subscribed and use a combination of other flags to find
// out if we should even be subscribed at all. // out if we should even be subscribed at all.
this._isListeningForReceipts = false; this.isListeningForReceipts = false;
} }
/** /**
* When true, the tile qualifies for some sort of special read receipt. This could be a 'sending' * When true, the tile qualifies for some sort of special read receipt. This could be a 'sending'
* or 'sent' receipt, for example. * or 'sent' receipt, for example.
* @returns {boolean} * @returns {boolean}
* @private
*/ */
get _isEligibleForSpecialReceipt() { private get isEligibleForSpecialReceipt() {
// First, if there are other read receipts then just short-circuit this. // First, if there are other read receipts then just short-circuit this.
if (this.props.readReceipts && this.props.readReceipts.length > 0) return false; if (this.props.readReceipts && this.props.readReceipts.length > 0) return false;
if (!this.props.mxEvent) return false; if (!this.props.mxEvent) return false;
@ -340,9 +369,9 @@ export default class EventTile extends React.Component {
return true; return true;
} }
get _shouldShowSentReceipt() { private get shouldShowSentReceipt() {
// If we're not even eligible, don't show the receipt. // If we're not even eligible, don't show the receipt.
if (!this._isEligibleForSpecialReceipt) return false; if (!this.isEligibleForSpecialReceipt) return false;
// We only show the 'sent' receipt on the last successful event. // We only show the 'sent' receipt on the last successful event.
if (!this.props.lastSuccessful) return false; if (!this.props.lastSuccessful) return false;
@ -360,9 +389,9 @@ export default class EventTile extends React.Component {
return true; return true;
} }
get _shouldShowSendingReceipt() { private get shouldShowSendingReceipt() {
// If we're not even eligible, don't show the receipt. // If we're not even eligible, don't show the receipt.
if (!this._isEligibleForSpecialReceipt) return false; if (!this.isEligibleForSpecialReceipt) return false;
// Check the event send status to see if we are pending. Null/undefined status means the // Check the event send status to see if we are pending. Null/undefined status means the
// message was sent, so check for that and 'sent' explicitly. // message was sent, so check for that and 'sent' explicitly.
@ -376,22 +405,22 @@ export default class EventTile extends React.Component {
// TODO: [REACT-WARNING] Move into constructor // TODO: [REACT-WARNING] Move into constructor
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this._verifyEvent(this.props.mxEvent); this.verifyEvent(this.props.mxEvent);
} }
componentDidMount() { componentDidMount() {
this._suppressReadReceiptAnimation = false; this.suppressReadReceiptAnimation = false;
const client = this.context; const client = this.context;
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged); client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
client.on("userTrustStatusChanged", this.onUserVerificationChanged); client.on("userTrustStatusChanged", this.onUserVerificationChanged);
this.props.mxEvent.on("Event.decrypted", this._onDecrypted); this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
if (this.props.showReactions) { if (this.props.showReactions) {
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated); this.props.mxEvent.on("Event.relationsCreated", this.onReactionsCreated);
} }
if (this._shouldShowSentReceipt || this._shouldShowSendingReceipt) { if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) {
client.on("Room.receipt", this._onRoomReceipt); client.on("Room.receipt", this.onRoomReceipt);
this._isListeningForReceipts = true; this.isListeningForReceipts = true;
} }
} }
@ -401,7 +430,7 @@ export default class EventTile extends React.Component {
// re-check the sender verification as outgoing events progress through // re-check the sender verification as outgoing events progress through
// the send process. // the send process.
if (nextProps.eventSendStatus !== this.props.eventSendStatus) { if (nextProps.eventSendStatus !== this.props.eventSendStatus) {
this._verifyEvent(nextProps.mxEvent); this.verifyEvent(nextProps.mxEvent);
} }
} }
@ -410,35 +439,35 @@ export default class EventTile extends React.Component {
return true; return true;
} }
return !this._propsEqual(this.props, nextProps); return !this.propsEqual(this.props, nextProps);
} }
componentWillUnmount() { componentWillUnmount() {
const client = this.context; const client = this.context;
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged); client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
client.removeListener("Room.receipt", this._onRoomReceipt); client.removeListener("Room.receipt", this.onRoomReceipt);
this._isListeningForReceipts = false; this.isListeningForReceipts = false;
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted); this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted);
if (this.props.showReactions) { if (this.props.showReactions) {
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated); this.props.mxEvent.removeListener("Event.relationsCreated", this.onReactionsCreated);
} }
} }
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
// If we're not listening for receipts and expect to be, register a listener. // If we're not listening for receipts and expect to be, register a listener.
if (!this._isListeningForReceipts && (this._shouldShowSentReceipt || this._shouldShowSendingReceipt)) { if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) {
this.context.on("Room.receipt", this._onRoomReceipt); this.context.on("Room.receipt", this.onRoomReceipt);
this._isListeningForReceipts = true; this.isListeningForReceipts = true;
} }
} }
_onRoomReceipt = (ev, room) => { private onRoomReceipt = (ev, room) => {
// ignore events for other rooms // ignore events for other rooms
const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
if (room !== tileRoom) return; if (room !== tileRoom) return;
if (!this._shouldShowSentReceipt && !this._shouldShowSendingReceipt && !this._isListeningForReceipts) { if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt && !this.isListeningForReceipts) {
return; return;
} }
@ -446,36 +475,36 @@ export default class EventTile extends React.Component {
// the getters we use here to determine what needs rendering. // the getters we use here to determine what needs rendering.
this.forceUpdate(() => { this.forceUpdate(() => {
// Per elsewhere in this file, we can remove the listener once we will have no further purpose for it. // Per elsewhere in this file, we can remove the listener once we will have no further purpose for it.
if (!this._shouldShowSentReceipt && !this._shouldShowSendingReceipt) { if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt) {
this.context.removeListener("Room.receipt", this._onRoomReceipt); this.context.removeListener("Room.receipt", this.onRoomReceipt);
this._isListeningForReceipts = false; this.isListeningForReceipts = false;
} }
}); });
}; };
/** called when the event is decrypted after we show it. /** called when the event is decrypted after we show it.
*/ */
_onDecrypted = () => { private onDecrypted = () => {
// we need to re-verify the sending device. // we need to re-verify the sending device.
// (we call onHeightChanged in _verifyEvent to handle the case where decryption // (we call onHeightChanged in verifyEvent to handle the case where decryption
// has caused a change in size of the event tile) // has caused a change in size of the event tile)
this._verifyEvent(this.props.mxEvent); this.verifyEvent(this.props.mxEvent);
this.forceUpdate(); this.forceUpdate();
}; };
onDeviceVerificationChanged = (userId, device) => { private onDeviceVerificationChanged = (userId, device) => {
if (userId === this.props.mxEvent.getSender()) { if (userId === this.props.mxEvent.getSender()) {
this._verifyEvent(this.props.mxEvent); this.verifyEvent(this.props.mxEvent);
} }
}; };
onUserVerificationChanged = (userId, _trustStatus) => { private onUserVerificationChanged = (userId, _trustStatus) => {
if (userId === this.props.mxEvent.getSender()) { if (userId === this.props.mxEvent.getSender()) {
this._verifyEvent(this.props.mxEvent); this.verifyEvent(this.props.mxEvent);
} }
}; };
async _verifyEvent(mxEvent) { private async verifyEvent(mxEvent) {
if (!mxEvent.isEncrypted()) { if (!mxEvent.isEncrypted()) {
return; return;
} }
@ -529,7 +558,7 @@ export default class EventTile extends React.Component {
}, this.props.onHeightChanged); // Decryption may have caused a change in size }, this.props.onHeightChanged); // Decryption may have caused a change in size
} }
_propsEqual(objA, objB) { private propsEqual(objA, objB) {
const keysA = Object.keys(objA); const keysA = Object.keys(objA);
const keysB = Object.keys(objB); const keysB = Object.keys(objB);
@ -596,7 +625,7 @@ export default class EventTile extends React.Component {
}; };
getReadAvatars() { getReadAvatars() {
if (this._shouldShowSentReceipt || this._shouldShowSendingReceipt) { if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) {
return <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />; return <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />;
} }
@ -643,7 +672,7 @@ export default class EventTile extends React.Component {
leftOffset={left} hidden={hidden} leftOffset={left} hidden={hidden}
readReceiptInfo={readReceiptInfo} readReceiptInfo={readReceiptInfo}
checkUnmounting={this.props.checkUnmounting} checkUnmounting={this.props.checkUnmounting}
suppressAnimation={this._suppressReadReceiptAnimation} suppressAnimation={this.suppressReadReceiptAnimation}
onClick={this.toggleAllReadAvatars} onClick={this.toggleAllReadAvatars}
timestamp={receipt.ts} timestamp={receipt.ts}
showTwelveHour={this.props.isTwelveHour} showTwelveHour={this.props.isTwelveHour}
@ -700,7 +729,7 @@ export default class EventTile extends React.Component {
}); });
}; };
_renderE2EPadlock() { private renderE2EPadlock() {
const ev = this.props.mxEvent; const ev = this.props.mxEvent;
// event could not be decrypted // event could not be decrypted
@ -749,9 +778,9 @@ export default class EventTile extends React.Component {
}); });
}; };
getTile = () => this._tile.current; getTile = () => this.tile.current;
getReplyThread = () => this._replyThread.current; getReplyThread = () => this.replyThread.current;
getReactions = () => { getReactions = () => {
if ( if (
@ -771,11 +800,11 @@ export default class EventTile extends React.Component {
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction"); return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
}; };
_onReactionsCreated = (relationType, eventType) => { private onReactionsCreated = (relationType, eventType) => {
if (relationType !== "m.annotation" || eventType !== "m.reaction") { if (relationType !== "m.annotation" || eventType !== "m.reaction") {
return; return;
} }
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated); this.props.mxEvent.removeListener("Event.relationsCreated", this.onReactionsCreated);
this.setState({ this.setState({
reactions: this.getReactions(), reactions: this.getReactions(),
}); });
@ -896,7 +925,7 @@ export default class EventTile extends React.Component {
// so that the correct avatar is shown as the text is // so that the correct avatar is shown as the text is
// `$target accepted the invitation for $email` // `$target accepted the invitation for $email`
if (this.props.mxEvent.getContent().third_party_invite) { if (this.props.mxEvent.getContent().third_party_invite) {
member = this.props.mxEvent.target; member = this.props.mxEvent.target;
} else { } else {
member = this.props.mxEvent.sender; member = this.props.mxEvent.sender;
} }
@ -913,8 +942,9 @@ export default class EventTile extends React.Component {
if (needsSenderProfile) { if (needsSenderProfile) {
if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') { if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') {
sender = <SenderProfile onClick={this.onSenderProfileClick} sender = <SenderProfile onClick={this.onSenderProfileClick}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
enableFlair={this.props.enableFlair} />; enableFlair={this.props.enableFlair}
/>;
} else { } else {
sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={this.props.enableFlair} />; sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={this.props.enableFlair} />;
} }
@ -977,18 +1007,18 @@ export default class EventTile extends React.Component {
} }
const linkedTimestamp = <a const linkedTimestamp = <a
href={permalink} href={permalink}
onClick={this.onPermalinkClicked} onClick={this.onPermalinkClicked}
aria-label={formatTime(new Date(this.props.mxEvent.getTs()), this.props.isTwelveHour)} aria-label={formatTime(new Date(this.props.mxEvent.getTs()), this.props.isTwelveHour)}
> >
{ timestamp } { timestamp }
</a>; </a>;
const useIRCLayout = this.props.layout == Layout.IRC; const useIRCLayout = this.props.layout == Layout.IRC;
const groupTimestamp = !useIRCLayout ? linkedTimestamp : null; const groupTimestamp = !useIRCLayout ? linkedTimestamp : null;
const ircTimestamp = useIRCLayout ? linkedTimestamp : null; const ircTimestamp = useIRCLayout ? linkedTimestamp : null;
const groupPadlock = !useIRCLayout && !isBubbleMessage && this._renderE2EPadlock(); const groupPadlock = !useIRCLayout && !isBubbleMessage && this.renderE2EPadlock();
const ircPadlock = useIRCLayout && !isBubbleMessage && this._renderE2EPadlock(); const ircPadlock = useIRCLayout && !isBubbleMessage && this.renderE2EPadlock();
let msgOption; let msgOption;
if (this.props.showReadReceipts) { if (this.props.showReadReceipts) {
@ -1019,12 +1049,13 @@ export default class EventTile extends React.Component {
</a> </a>
</div> </div>
<div className="mx_EventTile_line"> <div className="mx_EventTile_line">
<EventTileType ref={this._tile} <EventTileType ref={this.tile}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
highlights={this.props.highlights} highlights={this.props.highlights}
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged} /> onHeightChanged={this.props.onHeightChanged}
/>
</div> </div>
</div> </div>
); );
@ -1033,13 +1064,14 @@ export default class EventTile extends React.Component {
return ( return (
<div className={classes} aria-live={ariaLive} aria-atomic="true"> <div className={classes} aria-live={ariaLive} aria-atomic="true">
<div className="mx_EventTile_line"> <div className="mx_EventTile_line">
<EventTileType ref={this._tile} <EventTileType ref={this.tile}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
highlights={this.props.highlights} highlights={this.props.highlights}
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
onHeightChanged={this.props.onHeightChanged} /> onHeightChanged={this.props.onHeightChanged}
/>
</div> </div>
<a <a
className="mx_EventTile_senderDetailsLink" className="mx_EventTile_senderDetailsLink"
@ -1063,7 +1095,7 @@ export default class EventTile extends React.Component {
this.props.mxEvent, this.props.mxEvent,
this.props.onHeightChanged, this.props.onHeightChanged,
this.props.permalinkCreator, this.props.permalinkCreator,
this._replyThread, this.replyThread,
); );
} }
return ( return (
@ -1076,13 +1108,14 @@ export default class EventTile extends React.Component {
{ groupTimestamp } { groupTimestamp }
{ groupPadlock } { groupPadlock }
{ thread } { thread }
<EventTileType ref={this._tile} <EventTileType ref={this.tile}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
highlights={this.props.highlights} highlights={this.props.highlights}
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
onHeightChanged={this.props.onHeightChanged} onHeightChanged={this.props.onHeightChanged}
replacingEventId={this.props.replacingEventId} replacingEventId={this.props.replacingEventId}
showUrlPreview={false} /> showUrlPreview={false}
/>
</div> </div>
</div> </div>
); );
@ -1092,7 +1125,7 @@ export default class EventTile extends React.Component {
this.props.mxEvent, this.props.mxEvent,
this.props.onHeightChanged, this.props.onHeightChanged,
this.props.permalinkCreator, this.props.permalinkCreator,
this._replyThread, this.replyThread,
this.props.layout, this.props.layout,
); );
@ -1106,15 +1139,16 @@ export default class EventTile extends React.Component {
{ groupTimestamp } { groupTimestamp }
{ groupPadlock } { groupPadlock }
{ thread } { thread }
<EventTileType ref={this._tile} <EventTileType ref={this.tile}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
replacingEventId={this.props.replacingEventId} replacingEventId={this.props.replacingEventId}
editState={this.props.editState} editState={this.props.editState}
highlights={this.props.highlights} highlights={this.props.highlights}
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
onHeightChanged={this.props.onHeightChanged} /> onHeightChanged={this.props.onHeightChanged}
/>
{ keyRequestInfo } { keyRequestInfo }
{ reactionsRow } { reactionsRow }
{ actionBar } { actionBar }
@ -1183,18 +1217,26 @@ function E2ePadlockUnknown(props) {
function E2ePadlockUnauthenticated(props) { function E2ePadlockUnauthenticated(props) {
return ( return (
<E2ePadlock title={_t("The authenticity of this encrypted message can't be guaranteed on this device.")} icon="unauthenticated" {...props} /> <E2ePadlock
title={_t("The authenticity of this encrypted message can't be guaranteed on this device.")}
icon="unauthenticated"
{...props}
/>
); );
} }
class E2ePadlock extends React.Component { interface IE2ePadlockProps {
static propTypes = { icon: string;
icon: PropTypes.string.isRequired, title: string;
title: PropTypes.string.isRequired, }
};
constructor() { interface IE2ePadlockState {
super(); hover: boolean;
}
class E2ePadlock extends React.Component<IE2ePadlockProps, IE2ePadlockState> {
constructor(props) {
super(props);
this.state = { this.state = {
hover: false, hover: false,
@ -1212,14 +1254,13 @@ class E2ePadlock extends React.Component {
render() { render() {
let tooltip = null; let tooltip = null;
if (this.state.hover) { if (this.state.hover) {
tooltip = <Tooltip className="mx_EventTile_e2eIcon_tooltip" label={this.props.title} dir="auto" />; tooltip = <Tooltip className="mx_EventTile_e2eIcon_tooltip" label={this.props.title} />;
} }
const classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`; const classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`;
return ( return (
<div <div
className={classes} className={classes}
onClick={this.onClick}
onMouseEnter={this.onHoverStart} onMouseEnter={this.onHoverStart}
onMouseLeave={this.onHoverEnd} onMouseLeave={this.onHoverEnd}
>{tooltip}</div> >{tooltip}</div>
@ -1236,8 +1277,8 @@ interface ISentReceiptState {
} }
class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptState> { class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptState> {
constructor() { constructor(props) {
super(); super(props);
this.state = { this.state = {
hover: false, hover: false,

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2015-2018, 2020, 2021 The Matrix.org Foundation C.I.C. Copyright 2015-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -13,15 +13,18 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {createRef} from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {Room} from "matrix-js-sdk/src/models/room";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { ActionPayload } from "../../../dispatcher/payloads";
import Stickerpicker from './Stickerpicker'; import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks'; import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import ContentMessages from '../../../ContentMessages'; import ContentMessages from '../../../ContentMessages';
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
@ -35,19 +38,26 @@ import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
import {RecordingState} from "../../../voice/VoiceRecording"; import {RecordingState} from "../../../voice/VoiceRecording";
import Tooltip, {Alignment} from "../elements/Tooltip"; import Tooltip, {Alignment} from "../elements/Tooltip";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import { E2EStatus } from '../../../utils/ShieldUtils';
import SendMessageComposer from "./SendMessageComposer";
function ComposerAvatar(props) { interface IComposerAvatarProps {
me: object;
}
function ComposerAvatar(props: IComposerAvatarProps) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
return <div className="mx_MessageComposer_avatar"> return <div className="mx_MessageComposer_avatar">
<MemberStatusMessageAvatar member={props.me} width={24} height={24} /> <MemberStatusMessageAvatar member={props.me} width={24} height={24} />
</div>; </div>;
} }
ComposerAvatar.propTypes = { interface ISendButtonProps {
me: PropTypes.object.isRequired, onClick: () => void;
}; }
function SendButton(props) { function SendButton(props: ISendButtonProps) {
return ( return (
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_MessageComposer_sendMessage" className="mx_MessageComposer_sendMessage"
@ -57,10 +67,6 @@ function SendButton(props) {
); );
} }
SendButton.propTypes = {
onClick: PropTypes.func.isRequired,
};
const EmojiButton = ({addEmoji}) => { const EmojiButton = ({addEmoji}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@ -68,7 +74,7 @@ const EmojiButton = ({addEmoji}) => {
if (menuDisplayed) { if (menuDisplayed) {
const buttonRect = button.current.getBoundingClientRect(); const buttonRect = button.current.getBoundingClientRect();
const EmojiPicker = sdk.getComponent('emojipicker.EmojiPicker'); const EmojiPicker = sdk.getComponent('emojipicker.EmojiPicker');
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} catchTab={false}> contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} /> <EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
</ContextMenu>; </ContextMenu>;
} }
@ -98,39 +104,39 @@ const EmojiButton = ({addEmoji}) => {
</React.Fragment>; </React.Fragment>;
}; };
class UploadButton extends React.Component { interface IUploadButtonProps {
static propTypes = { roomId: string;
roomId: PropTypes.string.isRequired, }
}
class UploadButton extends React.Component<IUploadButtonProps> {
private uploadInput = React.createRef<HTMLInputElement>();
private dispatcherRef: string;
constructor(props) { constructor(props) {
super(props); super(props);
this.onUploadClick = this.onUploadClick.bind(this);
this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this);
this._uploadInput = createRef(); this.dispatcherRef = dis.register(this.onAction);
this._dispatcherRef = dis.register(this.onAction);
} }
componentWillUnmount() { componentWillUnmount() {
dis.unregister(this._dispatcherRef); dis.unregister(this.dispatcherRef);
} }
onAction = payload => { private onAction = (payload: ActionPayload) => {
if (payload.action === "upload_file") { if (payload.action === "upload_file") {
this.onUploadClick(); this.onUploadClick();
} }
}; };
onUploadClick(ev) { private onUploadClick = () => {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'require_registration'}); dis.dispatch({action: 'require_registration'});
return; return;
} }
this._uploadInput.current.click(); this.uploadInput.current.click();
} }
onUploadFileInputChange(ev) { private onUploadFileInputChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
if (ev.target.files.length === 0) return; if (ev.target.files.length === 0) return;
// take a copy so we can safely reset the value of the form control // take a copy so we can safely reset the value of the form control
@ -160,7 +166,7 @@ class UploadButton extends React.Component {
title={_t('Upload file')} title={_t('Upload file')}
> >
<input <input
ref={this._uploadInput} ref={this.uploadInput}
type="file" type="file"
style={uploadInputStyle} style={uploadInputStyle}
multiple multiple
@ -171,19 +177,34 @@ class UploadButton extends React.Component {
} }
} }
interface IProps {
room: Room;
resizeNotifier: ResizeNotifier;
permalinkCreator: RoomPermalinkCreator;
replyToEvent?: MatrixEvent;
e2eStatus?: E2EStatus;
}
interface IState {
tombstone: MatrixEvent;
canSendMessages: boolean;
isComposerEmpty: boolean;
haveRecording: boolean;
recordingTimeLeftSeconds?: number;
me?: RoomMember;
}
@replaceableComponent("views.rooms.MessageComposer") @replaceableComponent("views.rooms.MessageComposer")
export default class MessageComposer extends React.Component { export default class MessageComposer extends React.Component<IProps, IState> {
private dispatcherRef: string;
private messageComposerInput: SendMessageComposer;
constructor(props) { constructor(props) {
super(props); super(props);
this.onInputStateChanged = this.onInputStateChanged.bind(this); VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate);
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
this._onTombstoneClick = this._onTombstoneClick.bind(this);
this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
VoiceRecordingStore.instance.on(UPDATE_EVENT, this._onVoiceStoreUpdate);
this._dispatcherRef = null;
this.state = { this.state = {
tombstone: this._getRoomTombstone(), tombstone: this.getRoomTombstone(),
canSendMessages: this.props.room.maySendMessage(), canSendMessages: this.props.room.maySendMessage(),
isComposerEmpty: true, isComposerEmpty: true,
haveRecording: false, haveRecording: false,
@ -191,7 +212,13 @@ export default class MessageComposer extends React.Component {
}; };
} }
onAction = (payload) => { componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
this.waitForOwnMember();
}
private onAction = (payload: ActionPayload) => {
if (payload.action === 'reply_to_event') { if (payload.action === 'reply_to_event') {
// add a timeout for the reply preview to be rendered, so // add a timeout for the reply preview to be rendered, so
// that the ScrollPanel listening to the resizeNotifier can // that the ScrollPanel listening to the resizeNotifier can
@ -203,13 +230,7 @@ export default class MessageComposer extends React.Component {
} }
}; };
componentDidMount() { private waitForOwnMember() {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
this._waitForOwnMember();
}
_waitForOwnMember() {
// if we have the member already, do that // if we have the member already, do that
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()); const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
if (me) { if (me) {
@ -227,34 +248,28 @@ export default class MessageComposer extends React.Component {
componentWillUnmount() { componentWillUnmount() {
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents); MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
} }
VoiceRecordingStore.instance.off(UPDATE_EVENT, this._onVoiceStoreUpdate); VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate);
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
_onRoomStateEvents(ev, state) { private onRoomStateEvents = (ev, state) => {
if (ev.getRoomId() !== this.props.room.roomId) return; if (ev.getRoomId() !== this.props.room.roomId) return;
if (ev.getType() === 'm.room.tombstone') { if (ev.getType() === 'm.room.tombstone') {
this.setState({tombstone: this._getRoomTombstone()}); this.setState({tombstone: this.getRoomTombstone()});
} }
if (ev.getType() === 'm.room.power_levels') { if (ev.getType() === 'm.room.power_levels') {
this.setState({canSendMessages: this.props.room.maySendMessage()}); this.setState({canSendMessages: this.props.room.maySendMessage()});
} }
} }
_getRoomTombstone() { private getRoomTombstone() {
return this.props.room.currentState.getStateEvents('m.room.tombstone', ''); return this.props.room.currentState.getStateEvents('m.room.tombstone', '');
} }
onInputStateChanged(inputState) { private onTombstoneClick = (ev) => {
// Merge the new input state with old to support partial updates
inputState = Object.assign({}, this.state.inputState, inputState);
this.setState({inputState});
}
_onTombstoneClick(ev) {
ev.preventDefault(); ev.preventDefault();
const replacementRoomId = this.state.tombstone.getContent()['replacement_room']; const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
@ -284,7 +299,7 @@ export default class MessageComposer extends React.Component {
}); });
} }
renderPlaceholderText() { private renderPlaceholderText = () => {
if (this.props.replyToEvent) { if (this.props.replyToEvent) {
if (this.props.e2eStatus) { if (this.props.e2eStatus) {
return _t('Send an encrypted reply…'); return _t('Send an encrypted reply…');
@ -317,7 +332,7 @@ export default class MessageComposer extends React.Component {
}); });
} }
_onVoiceStoreUpdate = () => { private onVoiceStoreUpdate = () => {
const recording = VoiceRecordingStore.instance.activeRecording; const recording = VoiceRecordingStore.instance.activeRecording;
this.setState({haveRecording: !!recording}); this.setState({haveRecording: !!recording});
if (recording) { if (recording) {
@ -386,7 +401,7 @@ export default class MessageComposer extends React.Component {
const continuesLink = replacementRoomId ? ( const continuesLink = replacementRoomId ? (
<a href={makeRoomPermalink(replacementRoomId)} <a href={makeRoomPermalink(replacementRoomId)}
className="mx_MessageComposer_roomReplaced_link" className="mx_MessageComposer_roomReplaced_link"
onClick={this._onTombstoneClick} onClick={this.onTombstoneClick}
> >
{_t("The conversation continues here.")} {_t("The conversation continues here.")}
</a> </a>
@ -394,7 +409,9 @@ export default class MessageComposer extends React.Component {
controls.push(<div className="mx_MessageComposer_replaced_wrapper" key="room_replaced"> controls.push(<div className="mx_MessageComposer_replaced_wrapper" key="room_replaced">
<div className="mx_MessageComposer_replaced_valign"> <div className="mx_MessageComposer_replaced_valign">
<img className="mx_MessageComposer_roomReplaced_icon" src={require("../../../../res/img/room_replaced.svg")} /> <img className="mx_MessageComposer_roomReplaced_icon"
src={require("../../../../res/img/room_replaced.svg")}
/>
<span className="mx_MessageComposer_roomReplaced_header"> <span className="mx_MessageComposer_roomReplaced_header">
{_t("This room has been replaced and is no longer active.")} {_t("This room has been replaced and is no longer active.")}
</span><br /> </span><br />
@ -431,14 +448,3 @@ export default class MessageComposer extends React.Component {
); );
} }
} }
MessageComposer.propTypes = {
// js-sdk Room object
room: PropTypes.object.isRequired,
// string representing the current voip call state
callState: PropTypes.string,
// string representing the current room app drawer state
showApps: PropTypes.bool,
};

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd. Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,9 +15,9 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {Room} from "matrix-js-sdk/src/models/room";
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import * as sdk from "../../../index"; import * as sdk from "../../../index";
@ -27,11 +27,22 @@ import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName"; import RoomName from "../elements/RoomName";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
event: MatrixEvent;
}
interface IState {
stateKey: string;
roomId: string;
displayName: string;
invited: boolean;
canKick: boolean;
senderName: string;
}
@replaceableComponent("views.rooms.ThirdPartyMemberInfo") @replaceableComponent("views.rooms.ThirdPartyMemberInfo")
export default class ThirdPartyMemberInfo extends React.Component { export default class ThirdPartyMemberInfo extends React.Component<IProps, IState> {
static propTypes = { private room: Room;
event: PropTypes.instanceOf(MatrixEvent).isRequired,
};
constructor(props) { constructor(props) {
super(props); super(props);

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -28,10 +28,17 @@ import {SettingLevel} from "../../../settings/SettingLevel";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import SeshatResetDialog from '../dialogs/SeshatResetDialog'; import SeshatResetDialog from '../dialogs/SeshatResetDialog';
interface IState {
enabling: boolean;
eventIndexSize: number;
roomCount: number;
eventIndexingEnabled: boolean;
}
@replaceableComponent("views.settings.EventIndexPanel") @replaceableComponent("views.settings.EventIndexPanel")
export default class EventIndexPanel extends React.Component { export default class EventIndexPanel extends React.Component<{}, IState> {
constructor() { constructor(props) {
super(); super(props);
this.state = { this.state = {
enabling: false, enabling: false,
@ -68,7 +75,7 @@ export default class EventIndexPanel extends React.Component {
} }
} }
async componentDidMount(): void { componentDidMount(): void {
this.updateState(); this.updateState();
} }
@ -102,8 +109,10 @@ export default class EventIndexPanel extends React.Component {
}); });
} }
_onManage = async () => { private onManage = async () => {
Modal.createTrackedDialogAsync('Message search', 'Message search', Modal.createTrackedDialogAsync('Message search', 'Message search',
// @ts-ignore: TS doesn't seem to like the type of this now that it
// has also been converted to TS as well, but I can't figure out why...
import('../../../async-components/views/dialogs/eventindex/ManageEventIndexDialog'), import('../../../async-components/views/dialogs/eventindex/ManageEventIndexDialog'),
{ {
onFinished: () => {}, onFinished: () => {},
@ -111,7 +120,7 @@ export default class EventIndexPanel extends React.Component {
); );
} }
_onEnable = async () => { private onEnable = async () => {
this.setState({ this.setState({
enabling: true, enabling: true,
}); });
@ -123,14 +132,13 @@ export default class EventIndexPanel extends React.Component {
await this.updateState(); await this.updateState();
} }
_confirmEventStoreReset = () => { private confirmEventStoreReset = () => {
const self = this;
const { close } = Modal.createDialog(SeshatResetDialog, { const { close } = Modal.createDialog(SeshatResetDialog, {
onFinished: async (success) => { onFinished: async (success) => {
if (success) { if (success) {
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
await EventIndexPeg.deleteEventIndex(); await EventIndexPeg.deleteEventIndex();
await self._onEnable(); await this.onEnable();
close(); close();
} }
}, },
@ -145,20 +153,19 @@ export default class EventIndexPanel extends React.Component {
if (EventIndexPeg.get() !== null) { if (EventIndexPeg.get() !== null) {
eventIndexingSettings = ( eventIndexingSettings = (
<div> <div>
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>{_t(
{_t("Securely cache encrypted messages locally for them " + "Securely cache encrypted messages locally for them " +
"to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", "to appear in search results, using %(size)s to store messages from %(rooms)s rooms.",
{ {
size: formatBytes(this.state.eventIndexSize, 0), size: formatBytes(this.state.eventIndexSize, 0),
// This drives the singular / plural string // This drives the singular / plural string
// selection for "room" / "rooms" only. // selection for "room" / "rooms" only.
count: this.state.roomCount, count: this.state.roomCount,
rooms: formatCountLong(this.state.roomCount), rooms: formatCountLong(this.state.roomCount),
}, },
)} )}</div>
</div>
<div> <div>
<AccessibleButton kind="primary" onClick={this._onManage}> <AccessibleButton kind="primary" onClick={this.onManage}>
{_t("Manage")} {_t("Manage")}
</AccessibleButton> </AccessibleButton>
</div> </div>
@ -167,13 +174,13 @@ export default class EventIndexPanel extends React.Component {
} else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) { } else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) {
eventIndexingSettings = ( eventIndexingSettings = (
<div> <div>
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>{_t(
{_t( "Securely cache encrypted messages locally for them to " + "Securely cache encrypted messages locally for them to " +
"appear in search results.")} "appear in search results.",
</div> )}</div>
<div> <div>
<AccessibleButton kind="primary" disabled={this.state.enabling} <AccessibleButton kind="primary" disabled={this.state.enabling}
onClick={this._onEnable}> onClick={this.onEnable}>
{_t("Enable")} {_t("Enable")}
</AccessibleButton> </AccessibleButton>
{this.state.enabling ? <InlineSpinner /> : <div />} {this.state.enabling ? <InlineSpinner /> : <div />}
@ -188,40 +195,36 @@ export default class EventIndexPanel extends React.Component {
); );
eventIndexingSettings = ( eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>{_t(
"%(brand)s is missing some components required for securely " +
"caching encrypted messages locally. If you'd like to " +
"experiment with this feature, build a custom %(brand)s Desktop " +
"with <nativeLink>search components added</nativeLink>.",
{ {
_t( "%(brand)s is missing some components required for securely " + brand,
"caching encrypted messages locally. If you'd like to " + },
"experiment with this feature, build a custom %(brand)s Desktop " + {
"with <nativeLink>search components added</nativeLink>.", nativeLink: sub => <a href={nativeLink}
{ target="_blank" rel="noreferrer noopener"
brand, >{sub}</a>,
}, },
{ )}</div>
'nativeLink': (sub) => <a href={nativeLink} target="_blank"
rel="noreferrer noopener">{sub}</a>,
},
)
}
</div>
); );
} else if (!EventIndexPeg.platformHasSupport()) { } else if (!EventIndexPeg.platformHasSupport()) {
eventIndexingSettings = ( eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>{_t(
"%(brand)s can't securely cache encrypted messages locally " +
"while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> " +
"for encrypted messages to appear in search results.",
{ {
_t( "%(brand)s can't securely cache encrypted messages locally " + brand,
"while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> " + },
"for encrypted messages to appear in search results.", {
{ desktopLink: sub => <a href="https://element.io/get-started"
brand, target="_blank" rel="noreferrer noopener"
}, >{sub}</a>,
{ },
'desktopLink': (sub) => <a href="https://element.io/get-started" )}</div>
target="_blank" rel="noreferrer noopener">{sub}</a>,
},
)
}
</div>
); );
} else { } else {
eventIndexingSettings = ( eventIndexingSettings = (
@ -233,19 +236,18 @@ export default class EventIndexPanel extends React.Component {
} }
</p> </p>
{EventIndexPeg.error && ( {EventIndexPeg.error && (
<details> <details>
<summary>{_t("Advanced")}</summary> <summary>{_t("Advanced")}</summary>
<code> <code>
{EventIndexPeg.error.message} {EventIndexPeg.error.message}
</code> </code>
<p> <p>
<AccessibleButton key="delete" kind="danger" onClick={this._confirmEventStoreReset}> <AccessibleButton key="delete" kind="danger" onClick={this.confirmEventStoreReset}>
{_t("Reset")} {_t("Reset")}
</AccessibleButton> </AccessibleButton>
</p> </p>
</details> </details>
)} )}
</div> </div>
); );
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,7 +16,6 @@ limitations under the License.
import url from 'url'; import url from 'url';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
@ -28,6 +27,7 @@ import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils";
import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils'; import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils';
import {timeout} from "../../../utils/promise"; import {timeout} from "../../../utils/promise";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import { ActionPayload } from '../../../dispatcher/payloads';
// We'll wait up to this long when checking for 3PID bindings on the IS. // We'll wait up to this long when checking for 3PID bindings on the IS.
const REACHABILITY_TIMEOUT = 10000; // ms const REACHABILITY_TIMEOUT = 10000; // ms
@ -59,16 +59,28 @@ async function checkIdentityServerUrl(u) {
} }
} }
@replaceableComponent("views.settings.SetIdServer") interface IProps {
export default class SetIdServer extends React.Component { // Whether or not the ID server is missing terms. This affects the text
static propTypes = { // shown to the user.
// Whether or not the ID server is missing terms. This affects the text missingTerms: boolean;
// shown to the user. }
missingTerms: PropTypes.bool,
};
constructor() { interface IState {
super(); defaultIdServer?: string;
currentClientIdServer: string;
idServer?: string;
error?: string;
busy: boolean;
disconnectBusy: boolean;
checking: boolean;
}
@replaceableComponent("views.settings.SetIdServer")
export default class SetIdServer extends React.Component<IProps, IState> {
private dispatcherRef: string;
constructor(props) {
super(props);
let defaultIdServer = ''; let defaultIdServer = '';
if (!MatrixClientPeg.get().getIdentityServerUrl() && getDefaultIdentityServerUrl()) { if (!MatrixClientPeg.get().getIdentityServerUrl() && getDefaultIdentityServerUrl()) {
@ -96,7 +108,7 @@ export default class SetIdServer extends React.Component {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
onAction = (payload) => { private onAction = (payload: ActionPayload) => {
// We react to changes in the ID server in the event the user is staring at this form // We react to changes in the ID server in the event the user is staring at this form
// when changing their identity server on another device. // when changing their identity server on another device.
if (payload.action !== "id_server_changed") return; if (payload.action !== "id_server_changed") return;
@ -106,13 +118,13 @@ export default class SetIdServer extends React.Component {
}); });
}; };
_onIdentityServerChanged = (ev) => { private onIdentityServerChanged = (ev) => {
const u = ev.target.value; const u = ev.target.value;
this.setState({idServer: u}); this.setState({idServer: u});
}; };
_getTooltip = () => { private getTooltip = () => {
if (this.state.checking) { if (this.state.checking) {
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
return <div> return <div>
@ -126,11 +138,11 @@ export default class SetIdServer extends React.Component {
} }
}; };
_idServerChangeEnabled = () => { private idServerChangeEnabled = () => {
return !!this.state.idServer && !this.state.busy; return !!this.state.idServer && !this.state.busy;
}; };
_saveIdServer = (fullUrl) => { private saveIdServer = (fullUrl) => {
// Account data change will update localstorage, client, etc through dispatcher // Account data change will update localstorage, client, etc through dispatcher
MatrixClientPeg.get().setAccountData("m.identity_server", { MatrixClientPeg.get().setAccountData("m.identity_server", {
base_url: fullUrl, base_url: fullUrl,
@ -143,7 +155,7 @@ export default class SetIdServer extends React.Component {
}); });
}; };
_checkIdServer = async (e) => { private checkIdServer = async (e) => {
e.preventDefault(); e.preventDefault();
const { idServer, currentClientIdServer } = this.state; const { idServer, currentClientIdServer } = this.state;
@ -166,14 +178,14 @@ export default class SetIdServer extends React.Component {
// Double check that the identity server even has terms of service. // Double check that the identity server even has terms of service.
const hasTerms = await doesIdentityServerHaveTerms(fullUrl); const hasTerms = await doesIdentityServerHaveTerms(fullUrl);
if (!hasTerms) { if (!hasTerms) {
const [confirmed] = await this._showNoTermsWarning(fullUrl); const [confirmed] = await this.showNoTermsWarning(fullUrl);
save = confirmed; save = confirmed;
} }
// Show a general warning, possibly with details about any bound // Show a general warning, possibly with details about any bound
// 3PIDs that would be left behind. // 3PIDs that would be left behind.
if (save && currentClientIdServer && fullUrl !== currentClientIdServer) { if (save && currentClientIdServer && fullUrl !== currentClientIdServer) {
const [confirmed] = await this._showServerChangeWarning({ const [confirmed] = await this.showServerChangeWarning({
title: _t("Change identity server"), title: _t("Change identity server"),
unboundMessage: _t( unboundMessage: _t(
"Disconnect from the identity server <current /> and " + "Disconnect from the identity server <current /> and " +
@ -189,7 +201,7 @@ export default class SetIdServer extends React.Component {
} }
if (save) { if (save) {
this._saveIdServer(fullUrl); this.saveIdServer(fullUrl);
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -204,7 +216,7 @@ export default class SetIdServer extends React.Component {
}); });
}; };
_showNoTermsWarning(fullUrl) { private showNoTermsWarning(fullUrl) {
const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { const { finished } = Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, {
title: _t("Identity server has no terms of service"), title: _t("Identity server has no terms of service"),
@ -223,10 +235,10 @@ export default class SetIdServer extends React.Component {
return finished; return finished;
} }
_onDisconnectClicked = async () => { private onDisconnectClicked = async () => {
this.setState({disconnectBusy: true}); this.setState({disconnectBusy: true});
try { try {
const [confirmed] = await this._showServerChangeWarning({ const [confirmed] = await this.showServerChangeWarning({
title: _t("Disconnect identity server"), title: _t("Disconnect identity server"),
unboundMessage: _t( unboundMessage: _t(
"Disconnect from the identity server <idserver />?", {}, "Disconnect from the identity server <idserver />?", {},
@ -235,14 +247,14 @@ export default class SetIdServer extends React.Component {
button: _t("Disconnect"), button: _t("Disconnect"),
}); });
if (confirmed) { if (confirmed) {
this._disconnectIdServer(); this.disconnectIdServer();
} }
} finally { } finally {
this.setState({disconnectBusy: false}); this.setState({disconnectBusy: false});
} }
}; };
async _showServerChangeWarning({ title, unboundMessage, button }) { private async showServerChangeWarning({ title, unboundMessage, button }) {
const { currentClientIdServer } = this.state; const { currentClientIdServer } = this.state;
let threepids = []; let threepids = [];
@ -318,7 +330,7 @@ export default class SetIdServer extends React.Component {
return finished; return finished;
} }
_disconnectIdServer = () => { private disconnectIdServer = () => {
// Account data change will update localstorage, client, etc through dispatcher // Account data change will update localstorage, client, etc through dispatcher
MatrixClientPeg.get().setAccountData("m.identity_server", { MatrixClientPeg.get().setAccountData("m.identity_server", {
base_url: null, // clear base_url: null, // clear
@ -371,7 +383,7 @@ export default class SetIdServer extends React.Component {
let discoSection; let discoSection;
if (idServerUrl) { if (idServerUrl) {
let discoButtonContent = _t("Disconnect"); let discoButtonContent: React.ReactNode = _t("Disconnect");
let discoBodyText = _t( let discoBodyText = _t(
"Disconnecting from your identity server will mean you " + "Disconnecting from your identity server will mean you " +
"won't be discoverable by other users and you won't be " + "won't be discoverable by other users and you won't be " +
@ -391,14 +403,14 @@ export default class SetIdServer extends React.Component {
} }
discoSection = <div> discoSection = <div>
<span className="mx_SettingsTab_subsectionText">{discoBodyText}</span> <span className="mx_SettingsTab_subsectionText">{discoBodyText}</span>
<AccessibleButton onClick={this._onDisconnectClicked} kind="danger_sm"> <AccessibleButton onClick={this.onDisconnectClicked} kind="danger_sm">
{discoButtonContent} {discoButtonContent}
</AccessibleButton> </AccessibleButton>
</div>; </div>;
} }
return ( return (
<form className="mx_SettingsTab_section mx_SetIdServer" onSubmit={this._checkIdServer}> <form className="mx_SettingsTab_section mx_SetIdServer" onSubmit={this.checkIdServer}>
<span className="mx_SettingsTab_subheading"> <span className="mx_SettingsTab_subheading">
{sectionTitle} {sectionTitle}
</span> </span>
@ -411,15 +423,15 @@ export default class SetIdServer extends React.Component {
autoComplete="off" autoComplete="off"
placeholder={this.state.defaultIdServer} placeholder={this.state.defaultIdServer}
value={this.state.idServer} value={this.state.idServer}
onChange={this._onIdentityServerChanged} onChange={this.onIdentityServerChanged}
tooltipContent={this._getTooltip()} tooltipContent={this.getTooltip()}
tooltipClassName="mx_SetIdServer_tooltip" tooltipClassName="mx_SetIdServer_tooltip"
disabled={this.state.busy} disabled={this.state.busy}
forceValidity={this.state.error ? false : null} forceValidity={this.state.error ? false : null}
/> />
<AccessibleButton type="submit" kind="primary_sm" <AccessibleButton type="submit" kind="primary_sm"
onClick={this._checkIdServer} onClick={this.checkIdServer}
disabled={!this._idServerChangeEnabled()} disabled={!this.idServerChangeEnabled()}
>{_t("Change")}</AccessibleButton> >{_t("Change")}</AccessibleButton>
{discoSection} {discoSection}
</form> </form>

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019, 2021 The Matrix.org Foundation C.I.C. Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,7 +15,6 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import {_t, _td} from "../../../../../languageHandler"; import {_t, _td} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../.."; import * as sdk from "../../../../..";
@ -23,6 +22,9 @@ import AccessibleButton from "../../../elements/AccessibleButton";
import Modal from "../../../../../Modal"; import Modal from "../../../../../Modal";
import {replaceableComponent} from "../../../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import {EventType} from "matrix-js-sdk/src/@types/event"; import {EventType} from "matrix-js-sdk/src/@types/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomState } from "matrix-js-sdk/src/models/room-state";
const plEventsToLabels = { const plEventsToLabels = {
// These will be translated for us later. // These will be translated for us later.
@ -63,15 +65,15 @@ function parseIntWithDefault(val, def) {
return isNaN(res) ? def : res; return isNaN(res) ? def : res;
} }
export class BannedUser extends React.Component { interface IBannedUserProps {
static propTypes = { canUnban?: boolean;
canUnban: PropTypes.bool, member: RoomMember;
member: PropTypes.object.isRequired, // js-sdk RoomMember by: string;
by: PropTypes.string.isRequired, reason?: string;
reason: PropTypes.string, }
};
_onUnbanClick = (e) => { export class BannedUser extends React.Component<IBannedUserProps> {
private onUnbanClick = (e) => {
MatrixClientPeg.get().unban(this.props.member.roomId, this.props.member.userId).catch((err) => { MatrixClientPeg.get().unban(this.props.member.roomId, this.props.member.userId).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to unban: " + err); console.error("Failed to unban: " + err);
@ -87,8 +89,10 @@ export class BannedUser extends React.Component {
if (this.props.canUnban) { if (this.props.canUnban) {
unbanButton = ( unbanButton = (
<AccessibleButton kind='danger_sm' onClick={this._onUnbanClick} <AccessibleButton className='mx_RolesRoomSettingsTab_unbanBtn'
className='mx_RolesRoomSettingsTab_unbanBtn'> kind='danger_sm'
onClick={this.onUnbanClick}
>
{ _t('Unban') } { _t('Unban') }
</AccessibleButton> </AccessibleButton>
); );
@ -107,29 +111,29 @@ export class BannedUser extends React.Component {
} }
} }
@replaceableComponent("views.settings.tabs.room.RolesRoomSettingsTab") interface IProps {
export default class RolesRoomSettingsTab extends React.Component { roomId: string;
static propTypes = { }
roomId: PropTypes.string.isRequired,
};
componentDidMount(): void { @replaceableComponent("views.settings.tabs.room.RolesRoomSettingsTab")
MatrixClientPeg.get().on("RoomState.members", this._onRoomMembership); export default class RolesRoomSettingsTab extends React.Component<IProps> {
componentDidMount() {
MatrixClientPeg.get().on("RoomState.members", this.onRoomMembership);
} }
componentWillUnmount(): void { componentWillUnmount() {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (client) { if (client) {
client.removeListener("RoomState.members", this._onRoomMembership); client.removeListener("RoomState.members", this.onRoomMembership);
} }
} }
_onRoomMembership = (event, state, member) => { private onRoomMembership = (event: MatrixEvent, state: RoomState, member: RoomMember) => {
if (state.roomId !== this.props.roomId) return; if (state.roomId !== this.props.roomId) return;
this.forceUpdate(); this.forceUpdate();
}; };
_populateDefaultPlEvents(eventsSection, stateLevel, eventsLevel) { private populateDefaultPlEvents(eventsSection: Record<string, number>, stateLevel: number, eventsLevel: number) {
for (const desiredEvent of Object.keys(plEventsToShow)) { for (const desiredEvent of Object.keys(plEventsToShow)) {
if (!(desiredEvent in eventsSection)) { if (!(desiredEvent in eventsSection)) {
eventsSection[desiredEvent] = (plEventsToShow[desiredEvent].isState ? stateLevel : eventsLevel); eventsSection[desiredEvent] = (plEventsToShow[desiredEvent].isState ? stateLevel : eventsLevel);
@ -137,7 +141,7 @@ export default class RolesRoomSettingsTab extends React.Component {
} }
} }
_onPowerLevelsChanged = (value, powerLevelKey) => { private onPowerLevelsChanged = (inputValue: string, powerLevelKey: string) => {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId); const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
@ -148,7 +152,7 @@ export default class RolesRoomSettingsTab extends React.Component {
const eventsLevelPrefix = "event_levels_"; const eventsLevelPrefix = "event_levels_";
value = parseInt(value); const value = parseInt(inputValue);
if (powerLevelKey.startsWith(eventsLevelPrefix)) { if (powerLevelKey.startsWith(eventsLevelPrefix)) {
// deep copy "events" object, Object.assign itself won't deep copy // deep copy "events" object, Object.assign itself won't deep copy
@ -182,7 +186,7 @@ export default class RolesRoomSettingsTab extends React.Component {
}); });
}; };
_onUserPowerLevelChanged = (value, powerLevelKey) => { private onUserPowerLevelChanged = (value: string, powerLevelKey: string) => {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId); const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
@ -266,7 +270,7 @@ export default class RolesRoomSettingsTab extends React.Component {
currentUserLevel = defaultUserLevel; currentUserLevel = defaultUserLevel;
} }
this._populateDefaultPlEvents( this.populateDefaultPlEvents(
eventsLevels, eventsLevels,
parseIntWithDefault(plContent.state_default, powerLevelDescriptors.state_default.defaultValue), parseIntWithDefault(plContent.state_default, powerLevelDescriptors.state_default.defaultValue),
parseIntWithDefault(plContent.events_default, powerLevelDescriptors.events_default.defaultValue), parseIntWithDefault(plContent.events_default, powerLevelDescriptors.events_default.defaultValue),
@ -288,7 +292,7 @@ export default class RolesRoomSettingsTab extends React.Component {
label={user} label={user}
key={user} key={user}
powerLevelKey={user} // Will be sent as the second parameter to `onChange` powerLevelKey={user} // Will be sent as the second parameter to `onChange`
onChange={this._onUserPowerLevelChanged} onChange={this.onUserPowerLevelChanged}
/>, />,
); );
} else if (userLevels[user] < defaultUserLevel) { // muted } else if (userLevels[user] < defaultUserLevel) { // muted
@ -299,7 +303,7 @@ export default class RolesRoomSettingsTab extends React.Component {
label={user} label={user}
key={user} key={user}
powerLevelKey={user} // Will be sent as the second parameter to `onChange` powerLevelKey={user} // Will be sent as the second parameter to `onChange`
onChange={this._onUserPowerLevelChanged} onChange={this.onUserPowerLevelChanged}
/>, />,
); );
} }
@ -345,8 +349,9 @@ export default class RolesRoomSettingsTab extends React.Component {
if (sender) bannedBy = sender.name; if (sender) bannedBy = sender.name;
return ( return (
<BannedUser key={member.userId} canUnban={canBanUsers} <BannedUser key={member.userId} canUnban={canBanUsers}
member={member} reason={banEvent.reason} member={member} reason={banEvent.reason}
by={bannedBy} /> by={bannedBy}
/>
); );
})} })}
</ul> </ul>
@ -373,7 +378,7 @@ export default class RolesRoomSettingsTab extends React.Component {
usersDefault={defaultUserLevel} usersDefault={defaultUserLevel}
disabled={!canChangeLevels || currentUserLevel < value} disabled={!canChangeLevels || currentUserLevel < value}
powerLevelKey={key} // Will be sent as the second parameter to `onChange` powerLevelKey={key} // Will be sent as the second parameter to `onChange`
onChange={this._onPowerLevelsChanged} onChange={this.onPowerLevelsChanged}
/> />
</div>; </div>;
}); });
@ -398,7 +403,7 @@ export default class RolesRoomSettingsTab extends React.Component {
usersDefault={defaultUserLevel} usersDefault={defaultUserLevel}
disabled={!canChangeLevels || currentUserLevel < eventsLevels[eventType]} disabled={!canChangeLevels || currentUserLevel < eventsLevels[eventType]}
powerLevelKey={"event_levels_" + eventType} powerLevelKey={"event_levels_" + eventType}
onChange={this._onPowerLevelsChanged} onChange={this.onPowerLevelsChanged}
/> />
</div> </div>
); );

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {_t} from "../../../../../languageHandler"; import {_t} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../.."; import * as sdk from "../../../../..";
@ -26,64 +26,92 @@ import StyledRadioGroup from '../../../elements/StyledRadioGroup';
import {SettingLevel} from "../../../../../settings/SettingLevel"; import {SettingLevel} from "../../../../../settings/SettingLevel";
import SettingsStore from "../../../../../settings/SettingsStore"; import SettingsStore from "../../../../../settings/SettingsStore";
import {UIFeature} from "../../../../../settings/UIFeature"; import {UIFeature} from "../../../../../settings/UIFeature";
import {replaceableComponent} from "../../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../../utils/replaceableComponent";
// Knock and private are reserved keywords which are not yet implemented.
enum JoinRule {
Public = "public",
Knock = "knock",
Invite = "invite",
Private = "private",
}
enum GuestAccess {
CanJoin = "can_join",
Forbidden = "forbidden",
}
enum HistoryVisibility {
Invited = "invited",
Joined = "joined",
Shared = "shared",
WorldReadable = "world_readable",
}
interface IProps {
roomId: string;
}
interface IState {
joinRule: JoinRule;
guestAccess: GuestAccess;
history: HistoryVisibility;
hasAliases: boolean;
encrypted: boolean;
}
@replaceableComponent("views.settings.tabs.room.SecurityRoomSettingsTab") @replaceableComponent("views.settings.tabs.room.SecurityRoomSettingsTab")
export default class SecurityRoomSettingsTab extends React.Component { export default class SecurityRoomSettingsTab extends React.Component<IProps, IState> {
static propTypes = { constructor(props) {
roomId: PropTypes.string.isRequired, super(props);
};
constructor() {
super();
this.state = { this.state = {
joinRule: "invite", joinRule: JoinRule.Invite,
guestAccess: "can_join", guestAccess: GuestAccess.CanJoin,
history: "shared", history: HistoryVisibility.Shared,
hasAliases: false, hasAliases: false,
encrypted: false, encrypted: false,
}; };
} }
// TODO: [REACT-WARNING] Move this to constructor // TODO: [REACT-WARNING] Move this to constructor
async UNSAFE_componentWillMount(): void { // eslint-disable-line camelcase async UNSAFE_componentWillMount() { // eslint-disable-line camelcase
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent); MatrixClientPeg.get().on("RoomState.events", this.onStateEvent);
const room = MatrixClientPeg.get().getRoom(this.props.roomId); const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const state = room.currentState; const state = room.currentState;
const joinRule = this._pullContentPropertyFromEvent( const joinRule: JoinRule = this.pullContentPropertyFromEvent(
state.getStateEvents("m.room.join_rules", ""), state.getStateEvents("m.room.join_rules", ""),
'join_rule', 'join_rule',
'invite', JoinRule.Invite,
); );
const guestAccess = this._pullContentPropertyFromEvent( const guestAccess: GuestAccess = this.pullContentPropertyFromEvent(
state.getStateEvents("m.room.guest_access", ""), state.getStateEvents("m.room.guest_access", ""),
'guest_access', 'guest_access',
'forbidden', GuestAccess.Forbidden,
); );
const history = this._pullContentPropertyFromEvent( const history: HistoryVisibility = this.pullContentPropertyFromEvent(
state.getStateEvents("m.room.history_visibility", ""), state.getStateEvents("m.room.history_visibility", ""),
'history_visibility', 'history_visibility',
'shared', HistoryVisibility.Shared,
); );
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId); const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
this.setState({joinRule, guestAccess, history, encrypted}); this.setState({joinRule, guestAccess, history, encrypted});
const hasAliases = await this._hasAliases(); const hasAliases = await this.hasAliases();
this.setState({hasAliases}); this.setState({hasAliases});
} }
_pullContentPropertyFromEvent(event, key, defaultValue) { private pullContentPropertyFromEvent<T>(event: MatrixEvent, key: string, defaultValue: T): T {
if (!event || !event.getContent()) return defaultValue; if (!event || !event.getContent()) return defaultValue;
return event.getContent()[key] || defaultValue; return event.getContent()[key] || defaultValue;
} }
componentWillUnmount(): void { componentWillUnmount() {
MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent); MatrixClientPeg.get().removeListener("RoomState.events", this.onStateEvent);
} }
_onStateEvent = (e) => { private onStateEvent = (e: MatrixEvent) => {
const refreshWhenTypes = [ const refreshWhenTypes = [
'm.room.join_rules', 'm.room.join_rules',
'm.room.guest_access', 'm.room.guest_access',
@ -93,7 +121,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
if (refreshWhenTypes.includes(e.getType())) this.forceUpdate(); if (refreshWhenTypes.includes(e.getType())) this.forceUpdate();
}; };
_onEncryptionChange = (e) => { private onEncryptionChange = (e: React.ChangeEvent) => {
Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, { Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, {
title: _t('Enable encryption?'), title: _t('Enable encryption?'),
description: _t( description: _t(
@ -102,10 +130,9 @@ export default class SecurityRoomSettingsTab extends React.Component {
"may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>", "may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>",
{}, {},
{ {
'a': (sub) => { a: sub => <a href="https://element.io/help#encryption"
return <a rel='noreferrer noopener' target='_blank' rel="noreferrer noopener" target="_blank"
href='https://element.io/help#encryption'>{sub}</a>; >{sub}</a>,
},
}, },
), ),
onFinished: (confirm) => { onFinished: (confirm) => {
@ -127,12 +154,12 @@ export default class SecurityRoomSettingsTab extends React.Component {
}); });
}; };
_fixGuestAccess = (e) => { private fixGuestAccess = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const joinRule = "invite"; const joinRule = JoinRule.Invite;
const guestAccess = "can_join"; const guestAccess = GuestAccess.CanJoin;
const beforeJoinRule = this.state.joinRule; const beforeJoinRule = this.state.joinRule;
const beforeGuestAccess = this.state.guestAccess; const beforeGuestAccess = this.state.guestAccess;
@ -149,7 +176,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
}); });
}; };
_onRoomAccessRadioToggle = (roomAccess) => { private onRoomAccessRadioToggle = (roomAccess: string) => {
// join_rule // join_rule
// INVITE | PUBLIC // INVITE | PUBLIC
// ----------------------+---------------- // ----------------------+----------------
@ -163,20 +190,20 @@ export default class SecurityRoomSettingsTab extends React.Component {
// invite them, you clearly want them to join, whether they're a // invite them, you clearly want them to join, whether they're a
// guest or not. In practice, guest_access should probably have // guest or not. In practice, guest_access should probably have
// been implemented as part of the join_rules enum. // been implemented as part of the join_rules enum.
let joinRule = "invite"; let joinRule = JoinRule.Invite;
let guestAccess = "can_join"; let guestAccess = GuestAccess.CanJoin;
switch (roomAccess) { switch (roomAccess) {
case "invite_only": case "invite_only":
// no change - use defaults above // no change - use defaults above
break; break;
case "public_no_guests": case "public_no_guests":
joinRule = "public"; joinRule = JoinRule.Public;
guestAccess = "forbidden"; guestAccess = GuestAccess.Forbidden;
break; break;
case "public_with_guests": case "public_with_guests":
joinRule = "public"; joinRule = JoinRule.Public;
guestAccess = "can_join"; guestAccess = GuestAccess.CanJoin;
break; break;
} }
@ -195,7 +222,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
}); });
}; };
_onHistoryRadioToggle = (history) => { private onHistoryRadioToggle = (history: HistoryVisibility) => {
const beforeHistory = this.state.history; const beforeHistory = this.state.history;
this.setState({history: history}); this.setState({history: history});
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.history_visibility", { MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.history_visibility", {
@ -206,11 +233,11 @@ export default class SecurityRoomSettingsTab extends React.Component {
}); });
}; };
_updateBlacklistDevicesFlag = (checked) => { private updateBlacklistDevicesFlag = (checked: boolean) => {
MatrixClientPeg.get().getRoom(this.props.roomId).setBlacklistUnverifiedDevices(checked); MatrixClientPeg.get().getRoom(this.props.roomId).setBlacklistUnverifiedDevices(checked);
}; };
async _hasAliases() { private async hasAliases(): Promise<boolean> {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (await cli.doesServerSupportUnstableFeature("org.matrix.msc2432")) { if (await cli.doesServerSupportUnstableFeature("org.matrix.msc2432")) {
const response = await cli.unstableGetLocalAliases(this.props.roomId); const response = await cli.unstableGetLocalAliases(this.props.roomId);
@ -224,7 +251,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
} }
} }
_renderRoomAccess() { private renderRoomAccess() {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId); const room = client.getRoom(this.props.roomId);
const joinRule = this.state.joinRule; const joinRule = this.state.joinRule;
@ -240,7 +267,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} /> <img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
<span> <span>
{_t("Guests cannot join this room even if explicitly invited.")}&nbsp; {_t("Guests cannot join this room even if explicitly invited.")}&nbsp;
<a href="" onClick={this._fixGuestAccess}>{_t("Click here to fix")}</a> <a href="" onClick={this.fixGuestAccess}>{_t("Click here to fix")}</a>
</span> </span>
</div> </div>
); );
@ -265,7 +292,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
<StyledRadioGroup <StyledRadioGroup
name="roomVis" name="roomVis"
value={joinRule} value={joinRule}
onChange={this._onRoomAccessRadioToggle} onChange={this.onRoomAccessRadioToggle}
definitions={[ definitions={[
{ {
value: "invite_only", value: "invite_only",
@ -291,7 +318,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
); );
} }
_renderHistory() { private renderHistory() {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const history = this.state.history; const history = this.state.history;
const state = client.getRoom(this.props.roomId).currentState; const state = client.getRoom(this.props.roomId).currentState;
@ -306,25 +333,25 @@ export default class SecurityRoomSettingsTab extends React.Component {
<StyledRadioGroup <StyledRadioGroup
name="historyVis" name="historyVis"
value={history} value={history}
onChange={this._onHistoryRadioToggle} onChange={this.onHistoryRadioToggle}
definitions={[ definitions={[
{ {
value: "world_readable", value: HistoryVisibility.WorldReadable,
disabled: !canChangeHistory, disabled: !canChangeHistory,
label: _t("Anyone"), label: _t("Anyone"),
}, },
{ {
value: "shared", value: HistoryVisibility.Shared,
disabled: !canChangeHistory, disabled: !canChangeHistory,
label: _t('Members only (since the point in time of selecting this option)'), label: _t('Members only (since the point in time of selecting this option)'),
}, },
{ {
value: "invited", value: HistoryVisibility.Invited,
disabled: !canChangeHistory, disabled: !canChangeHistory,
label: _t('Members only (since they were invited)'), label: _t('Members only (since they were invited)'),
}, },
{ {
value: "joined", value: HistoryVisibility.Joined,
disabled: !canChangeHistory, disabled: !canChangeHistory,
label: _t('Members only (since they joined)'), label: _t('Members only (since they joined)'),
}, },
@ -348,7 +375,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
encryptionSettings = <SettingsFlag encryptionSettings = <SettingsFlag
name="blacklistUnverifiedDevices" name="blacklistUnverifiedDevices"
level={SettingLevel.ROOM_DEVICE} level={SettingLevel.ROOM_DEVICE}
onChange={this._updateBlacklistDevicesFlag} onChange={this.updateBlacklistDevicesFlag}
roomId={this.props.roomId} roomId={this.props.roomId}
/>; />;
} }
@ -356,7 +383,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
let historySection = (<> let historySection = (<>
<span className='mx_SettingsTab_subheading'>{_t("Who can read history?")}</span> <span className='mx_SettingsTab_subheading'>{_t("Who can read history?")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
{this._renderHistory()} {this.renderHistory()}
</div> </div>
</>); </>);
if (!SettingsStore.getValue(UIFeature.RoomHistorySettings)) { if (!SettingsStore.getValue(UIFeature.RoomHistorySettings)) {
@ -373,15 +400,16 @@ export default class SecurityRoomSettingsTab extends React.Component {
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>
<span>{_t("Once enabled, encryption cannot be disabled.")}</span> <span>{_t("Once enabled, encryption cannot be disabled.")}</span>
</div> </div>
<LabelledToggleSwitch value={isEncrypted} onChange={this._onEncryptionChange} <LabelledToggleSwitch value={isEncrypted} onChange={this.onEncryptionChange}
label={_t("Encrypted")} disabled={!canEnableEncryption} /> label={_t("Encrypted")} disabled={!canEnableEncryption}
/>
</div> </div>
{encryptionSettings} {encryptionSettings}
</div> </div>
<span className='mx_SettingsTab_subheading'>{_t("Who can access this room?")}</span> <span className='mx_SettingsTab_subheading'>{_t("Who can access this room?")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
{this._renderRoomAccess()} {this.renderRoomAccess()}
</div> </div>
{historySection} {historySection}

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,27 +15,31 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import {_t, getCurrentLanguage} from "../../../../../languageHandler"; import {_t, getCurrentLanguage} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import SdkConfig from "../../../../../SdkConfig"; import SdkConfig from "../../../../../SdkConfig";
import createRoom from "../../../../../createRoom"; import createRoom from "../../../../../createRoom";
import Modal from "../../../../../Modal"; import Modal from "../../../../../Modal";
import * as sdk from "../../../../../"; import * as sdk from "../../../../..";
import PlatformPeg from "../../../../../PlatformPeg"; import PlatformPeg from "../../../../../PlatformPeg";
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
import UpdateCheckButton from "../../UpdateCheckButton"; import UpdateCheckButton from "../../UpdateCheckButton";
import {replaceableComponent} from "../../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../../utils/replaceableComponent";
interface IProps {
closeSettingsFn: () => {};
}
interface IState {
appVersion: string;
canUpdate: boolean;
}
@replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab") @replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab")
export default class HelpUserSettingsTab extends React.Component { export default class HelpUserSettingsTab extends React.Component<IProps, IState> {
static propTypes = { constructor(props) {
closeSettingsFn: PropTypes.func.isRequired, super(props);
};
constructor() {
super();
this.state = { this.state = {
appVersion: null, appVersion: null,
@ -53,7 +56,7 @@ export default class HelpUserSettingsTab extends React.Component {
}); });
} }
_onClearCacheAndReload = (e) => { private onClearCacheAndReload = (e) => {
if (!PlatformPeg.get()) return; if (!PlatformPeg.get()) return;
// Dev note: please keep this log line, it's useful when troubleshooting a MatrixClient suddenly // Dev note: please keep this log line, it's useful when troubleshooting a MatrixClient suddenly
@ -65,7 +68,7 @@ export default class HelpUserSettingsTab extends React.Component {
}); });
}; };
_onBugReport = (e) => { private onBugReport = (e) => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) { if (!BugReportDialog) {
return; return;
@ -73,7 +76,7 @@ export default class HelpUserSettingsTab extends React.Component {
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
}; };
_onStartBotChat = (e) => { private onStartBotChat = (e) => {
this.props.closeSettingsFn(); this.props.closeSettingsFn();
createRoom({ createRoom({
dmUserId: SdkConfig.get().welcomeUserId, dmUserId: SdkConfig.get().welcomeUserId,
@ -81,7 +84,7 @@ export default class HelpUserSettingsTab extends React.Component {
}); });
}; };
_showSpoiler = (event) => { private showSpoiler = (event) => {
const target = event.target; const target = event.target;
target.innerHTML = target.getAttribute('data-spoiler'); target.innerHTML = target.getAttribute('data-spoiler');
@ -93,7 +96,7 @@ export default class HelpUserSettingsTab extends React.Component {
selection.addRange(range); selection.addRange(range);
}; };
_renderLegal() { private renderLegal() {
const tocLinks = SdkConfig.get().terms_and_conditions_links; const tocLinks = SdkConfig.get().terms_and_conditions_links;
if (!tocLinks) return null; if (!tocLinks) return null;
@ -114,7 +117,7 @@ export default class HelpUserSettingsTab extends React.Component {
); );
} }
_renderCredits() { private renderCredits() {
// Note: This is not translated because it is legal text. // Note: This is not translated because it is legal text.
// Also, &nbsp; is ugly but necessary. // Also, &nbsp; is ugly but necessary.
return ( return (
@ -122,28 +125,28 @@ export default class HelpUserSettingsTab extends React.Component {
<span className='mx_SettingsTab_subheading'>{_t("Credits")}</span> <span className='mx_SettingsTab_subheading'>{_t("Credits")}</span>
<ul> <ul>
<li> <li>
The <a href="themes/element/img/backgrounds/lake.jpg" rel="noreferrer noopener" target="_blank"> The <a href="themes/element/img/backgrounds/lake.jpg" rel="noreferrer noopener"
default cover photo</a> is ©&nbsp; target="_blank">default cover photo</a> is ©&nbsp;
<a href="https://www.flickr.com/golan" rel="noreferrer noopener" target="_blank">Jesús Roncero</a>{' '} <a href="https://www.flickr.com/golan" rel="noreferrer noopener"
used under the terms of&nbsp; target="_blank">Jesús Roncero</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noreferrer noopener" target="_blank"> <a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noreferrer noopener"
CC-BY-SA 4.0</a>. target="_blank">CC-BY-SA 4.0</a>.
</li> </li>
<li> <li>
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noreferrer noopener" The <a href="https://github.com/matrix-org/twemoji-colr" rel="noreferrer noopener"
target="_blank"> twemoji-colr</a> font is ©&nbsp; target="_blank">twemoji-colr</a> font is ©&nbsp;
<a href="https://mozilla.org" rel="noreferrer noopener" target="_blank">Mozilla Foundation</a>{' '} <a href="https://mozilla.org" rel="noreferrer noopener"
used under the terms of&nbsp; target="_blank">Mozilla Foundation</a> used under the terms of&nbsp;
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank"> <a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener"
Apache 2.0</a>. target="_blank">Apache 2.0</a>.
</li> </li>
<li> <li>
The <a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank"> The <a href="https://twemoji.twitter.com/" rel="noreferrer noopener"
Twemoji</a> emoji art is ©&nbsp; target="_blank">Twemoji</a> emoji art is ©&nbsp;
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">Twitter, Inc and other <a href="https://twemoji.twitter.com/" rel="noreferrer noopener"
contributors</a> used under the terms of&nbsp; target="_blank">Twitter, Inc and other contributors</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noreferrer noopener" target="_blank"> <a href="https://creativecommons.org/licenses/by/4.0/" rel="noreferrer noopener"
CC-BY 4.0</a>. target="_blank">CC-BY 4.0</a>.
</li> </li>
</ul> </ul>
</div> </div>
@ -188,7 +191,7 @@ export default class HelpUserSettingsTab extends React.Component {
}, },
)} )}
<div> <div>
<AccessibleButton onClick={this._onStartBotChat} kind='primary'> <AccessibleButton onClick={this.onStartBotChat} kind='primary'>
{_t("Chat with %(brand)s Bot", { brand })} {_t("Chat with %(brand)s Bot", { brand })}
</AccessibleButton> </AccessibleButton>
</div> </div>
@ -212,28 +215,27 @@ export default class HelpUserSettingsTab extends React.Component {
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span> <span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span>
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>
{ {_t(
_t( "If you've submitted a bug via GitHub, debug logs can help " + "If you've submitted a bug via GitHub, debug logs can help " +
"us track down the problem. Debug logs contain application " + "us track down the problem. Debug logs contain application " +
"usage data including your username, the IDs or aliases of " + "usage data including your username, the IDs or aliases of " +
"the rooms or groups you have visited and the usernames of " + "the rooms or groups you have visited and the usernames of " +
"other users. They do not contain messages.", "other users. They do not contain messages.",
) )}
}
<div className='mx_HelpUserSettingsTab_debugButton'> <div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this._onBugReport} kind='primary'> <AccessibleButton onClick={this.onBugReport} kind='primary'>
{_t("Submit debug logs")} {_t("Submit debug logs")}
</AccessibleButton> </AccessibleButton>
</div> </div>
{ {_t(
_t( "To report a Matrix-related security issue, please read the Matrix.org " + "To report a Matrix-related security issue, please read the Matrix.org " +
"<a>Security Disclosure Policy</a>.", {}, "<a>Security Disclosure Policy</a>.", {},
{ {
'a': (sub) => a: sub => <a href="https://matrix.org/security-disclosure-policy/"
<a href="https://matrix.org/security-disclosure-policy/" rel="noreferrer noopener" target="_blank"
rel="noreferrer noopener" target="_blank">{sub}</a>, >{sub}</a>,
}) },
} )}
</div> </div>
</div> </div>
); );
@ -260,20 +262,21 @@ export default class HelpUserSettingsTab extends React.Component {
{updateButton} {updateButton}
</div> </div>
</div> </div>
{this._renderLegal()} {this.renderLegal()}
{this._renderCredits()} {this.renderCredits()}
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'> <div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span> <span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span>
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>
{_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br /> {_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br />
{_t("Identity Server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br /> {_t("Identity Server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br />
{_t("Access Token:") + ' '} {_t("Access Token:") + ' '}
<AccessibleButton element="span" onClick={this._showSpoiler} <AccessibleButton element="span" onClick={this.showSpoiler}
data-spoiler={MatrixClientPeg.get().getAccessToken()}> data-spoiler={MatrixClientPeg.get().getAccessToken()}
>
&lt;{ _t("click to reveal") }&gt; &lt;{ _t("click to reveal") }&gt;
</AccessibleButton> </AccessibleButton>
<div className='mx_HelpUserSettingsTab_debugButton'> <div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'> <AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")} {_t("Clear cache and reload")}
</AccessibleButton> </AccessibleButton>
</div> </div>

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -25,10 +25,16 @@ import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../../index"; import * as sdk from "../../../../../index";
import {replaceableComponent} from "../../../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../../../utils/replaceableComponent";
interface IState {
busy: boolean;
newPersonalRule: string;
newList: string;
}
@replaceableComponent("views.settings.tabs.user.MjolnirUserSettingsTab") @replaceableComponent("views.settings.tabs.user.MjolnirUserSettingsTab")
export default class MjolnirUserSettingsTab extends React.Component { export default class MjolnirUserSettingsTab extends React.Component<{}, IState> {
constructor() { constructor(props) {
super(); super(props);
this.state = { this.state = {
busy: false, busy: false,
@ -37,15 +43,15 @@ export default class MjolnirUserSettingsTab extends React.Component {
}; };
} }
_onPersonalRuleChanged = (e) => { private onPersonalRuleChanged = (e) => {
this.setState({newPersonalRule: e.target.value}); this.setState({newPersonalRule: e.target.value});
}; };
_onNewListChanged = (e) => { private onNewListChanged = (e) => {
this.setState({newList: e.target.value}); this.setState({newList: e.target.value});
}; };
_onAddPersonalRule = async (e) => { private onAddPersonalRule = async (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -72,7 +78,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
} }
}; };
_onSubscribeList = async (e) => { private onSubscribeList = async (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -94,7 +100,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
} }
}; };
async _removePersonalRule(rule: ListRule) { private async removePersonalRule(rule: ListRule) {
this.setState({busy: true}); this.setState({busy: true});
try { try {
const list = Mjolnir.sharedInstance().getPersonalList(); const list = Mjolnir.sharedInstance().getPersonalList();
@ -112,7 +118,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
} }
} }
async _unsubscribeFromList(list: BanList) { private async unsubscribeFromList(list: BanList) {
this.setState({busy: true}); this.setState({busy: true});
try { try {
await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId); await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId);
@ -130,7 +136,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
} }
} }
_viewListRules(list: BanList) { private viewListRules(list: BanList) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const room = MatrixClientPeg.get().getRoom(list.roomId); const room = MatrixClientPeg.get().getRoom(list.roomId);
@ -161,7 +167,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
}); });
} }
_renderPersonalBanListRules() { private renderPersonalBanListRules() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const list = Mjolnir.sharedInstance().getPersonalList(); const list = Mjolnir.sharedInstance().getPersonalList();
@ -174,7 +180,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
<li key={rule.entity} className="mx_MjolnirUserSettingsTab_listItem"> <li key={rule.entity} className="mx_MjolnirUserSettingsTab_listItem">
<AccessibleButton <AccessibleButton
kind="danger_sm" kind="danger_sm"
onClick={() => this._removePersonalRule(rule)} onClick={() => this.removePersonalRule(rule)}
disabled={this.state.busy} disabled={this.state.busy}
> >
{_t("Remove")} {_t("Remove")}
@ -192,7 +198,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
); );
} }
_renderSubscribedBanLists() { private renderSubscribedBanLists() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const personalList = Mjolnir.sharedInstance().getPersonalList(); const personalList = Mjolnir.sharedInstance().getPersonalList();
@ -209,14 +215,14 @@ export default class MjolnirUserSettingsTab extends React.Component {
<li key={list.roomId} className="mx_MjolnirUserSettingsTab_listItem"> <li key={list.roomId} className="mx_MjolnirUserSettingsTab_listItem">
<AccessibleButton <AccessibleButton
kind="danger_sm" kind="danger_sm"
onClick={() => this._unsubscribeFromList(list)} onClick={() => this.unsubscribeFromList(list)}
disabled={this.state.busy} disabled={this.state.busy}
> >
{_t("Unsubscribe")} {_t("Unsubscribe")}
</AccessibleButton>&nbsp; </AccessibleButton>&nbsp;
<AccessibleButton <AccessibleButton
kind="primary_sm" kind="primary_sm"
onClick={() => this._viewListRules(list)} onClick={() => this.viewListRules(list)}
disabled={this.state.busy} disabled={this.state.busy}
> >
{_t("View rules")} {_t("View rules")}
@ -271,21 +277,21 @@ export default class MjolnirUserSettingsTab extends React.Component {
)} )}
</div> </div>
<div> <div>
{this._renderPersonalBanListRules()} {this.renderPersonalBanListRules()}
</div> </div>
<div> <div>
<form onSubmit={this._onAddPersonalRule} autoComplete="off"> <form onSubmit={this.onAddPersonalRule} autoComplete="off">
<Field <Field
type="text" type="text"
label={_t("Server or user ID to ignore")} label={_t("Server or user ID to ignore")}
placeholder={_t("eg: @bot:* or example.org")} placeholder={_t("eg: @bot:* or example.org")}
value={this.state.newPersonalRule} value={this.state.newPersonalRule}
onChange={this._onPersonalRuleChanged} onChange={this.onPersonalRuleChanged}
/> />
<AccessibleButton <AccessibleButton
type="submit" type="submit"
kind="primary" kind="primary"
onClick={this._onAddPersonalRule} onClick={this.onAddPersonalRule}
disabled={this.state.busy} disabled={this.state.busy}
> >
{_t("Ignore")} {_t("Ignore")}
@ -303,20 +309,20 @@ export default class MjolnirUserSettingsTab extends React.Component {
)}</span> )}</span>
</div> </div>
<div> <div>
{this._renderSubscribedBanLists()} {this.renderSubscribedBanLists()}
</div> </div>
<div> <div>
<form onSubmit={this._onSubscribeList} autoComplete="off"> <form onSubmit={this.onSubscribeList} autoComplete="off">
<Field <Field
type="text" type="text"
label={_t("Room ID or address of ban list")} label={_t("Room ID or address of ban list")}
value={this.state.newList} value={this.state.newList}
onChange={this._onNewListChanged} onChange={this.onNewListChanged}
/> />
<AccessibleButton <AccessibleButton
type="submit" type="submit"
kind="primary" kind="primary"
onClick={this._onSubscribeList} onClick={this.onSubscribeList}
disabled={this.state.busy} disabled={this.state.busy}
> >
{_t("Subscribe")} {_t("Subscribe")}

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -23,10 +23,24 @@ import Field from "../../../elements/Field";
import * as sdk from "../../../../.."; import * as sdk from "../../../../..";
import PlatformPeg from "../../../../../PlatformPeg"; import PlatformPeg from "../../../../../PlatformPeg";
import {SettingLevel} from "../../../../../settings/SettingLevel"; import {SettingLevel} from "../../../../../settings/SettingLevel";
import {replaceableComponent} from "../../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../../utils/replaceableComponent";
interface IState {
autoLaunch: boolean;
autoLaunchSupported: boolean;
warnBeforeExit: boolean;
warnBeforeExitSupported: boolean;
alwaysShowMenuBarSupported: boolean;
alwaysShowMenuBar: boolean;
minimizeToTraySupported: boolean;
minimizeToTray: boolean;
autocompleteDelay: string;
readMarkerInViewThresholdMs: string;
readMarkerOutOfViewThresholdMs: string;
}
@replaceableComponent("views.settings.tabs.user.PreferencesUserSettingsTab") @replaceableComponent("views.settings.tabs.user.PreferencesUserSettingsTab")
export default class PreferencesUserSettingsTab extends React.Component { export default class PreferencesUserSettingsTab extends React.Component<{}, IState> {
static ROOM_LIST_SETTINGS = [ static ROOM_LIST_SETTINGS = [
'breadcrumbs', 'breadcrumbs',
]; ];
@ -68,8 +82,8 @@ export default class PreferencesUserSettingsTab extends React.Component {
// Autocomplete delay (niche text box) // Autocomplete delay (niche text box)
]; ];
constructor() { constructor(props) {
super(); super(props);
this.state = { this.state = {
autoLaunch: false, autoLaunch: false,
@ -89,7 +103,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
}; };
} }
async componentDidMount(): void { async componentDidMount() {
const platform = PlatformPeg.get(); const platform = PlatformPeg.get();
const autoLaunchSupported = await platform.supportsAutoLaunch(); const autoLaunchSupported = await platform.supportsAutoLaunch();
@ -128,38 +142,38 @@ export default class PreferencesUserSettingsTab extends React.Component {
}); });
} }
_onAutoLaunchChange = (checked) => { private onAutoLaunchChange = (checked: boolean) => {
PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked})); PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked}));
}; };
_onWarnBeforeExitChange = (checked) => { private onWarnBeforeExitChange = (checked: boolean) => {
PlatformPeg.get().setWarnBeforeExit(checked).then(() => this.setState({warnBeforeExit: checked})); PlatformPeg.get().setWarnBeforeExit(checked).then(() => this.setState({warnBeforeExit: checked}));
} }
_onAlwaysShowMenuBarChange = (checked) => { private onAlwaysShowMenuBarChange = (checked: boolean) => {
PlatformPeg.get().setAutoHideMenuBarEnabled(!checked).then(() => this.setState({alwaysShowMenuBar: checked})); PlatformPeg.get().setAutoHideMenuBarEnabled(!checked).then(() => this.setState({alwaysShowMenuBar: checked}));
}; };
_onMinimizeToTrayChange = (checked) => { private onMinimizeToTrayChange = (checked: boolean) => {
PlatformPeg.get().setMinimizeToTrayEnabled(checked).then(() => this.setState({minimizeToTray: checked})); PlatformPeg.get().setMinimizeToTrayEnabled(checked).then(() => this.setState({minimizeToTray: checked}));
}; };
_onAutocompleteDelayChange = (e) => { private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({autocompleteDelay: e.target.value}); this.setState({autocompleteDelay: e.target.value});
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value); SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
}; };
_onReadMarkerInViewThresholdMs = (e) => { private onReadMarkerInViewThresholdMs = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({readMarkerInViewThresholdMs: e.target.value}); this.setState({readMarkerInViewThresholdMs: e.target.value});
SettingsStore.setValue("readMarkerInViewThresholdMs", null, SettingLevel.DEVICE, e.target.value); SettingsStore.setValue("readMarkerInViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
}; };
_onReadMarkerOutOfViewThresholdMs = (e) => { private onReadMarkerOutOfViewThresholdMs = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({readMarkerOutOfViewThresholdMs: e.target.value}); this.setState({readMarkerOutOfViewThresholdMs: e.target.value});
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value); SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
}; };
_renderGroup(settingIds) { private renderGroup(settingIds: string[]): React.ReactNodeArray {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
return settingIds.filter(SettingsStore.isEnabled).map(i => { return settingIds.filter(SettingsStore.isEnabled).map(i => {
return <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />; return <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />;
@ -171,7 +185,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
if (this.state.autoLaunchSupported) { if (this.state.autoLaunchSupported) {
autoLaunchOption = <LabelledToggleSwitch autoLaunchOption = <LabelledToggleSwitch
value={this.state.autoLaunch} value={this.state.autoLaunch}
onChange={this._onAutoLaunchChange} onChange={this.onAutoLaunchChange}
label={_t('Start automatically after system login')} />; label={_t('Start automatically after system login')} />;
} }
@ -179,7 +193,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
if (this.state.warnBeforeExitSupported) { if (this.state.warnBeforeExitSupported) {
warnBeforeExitOption = <LabelledToggleSwitch warnBeforeExitOption = <LabelledToggleSwitch
value={this.state.warnBeforeExit} value={this.state.warnBeforeExit}
onChange={this._onWarnBeforeExitChange} onChange={this.onWarnBeforeExitChange}
label={_t('Warn before quitting')} />; label={_t('Warn before quitting')} />;
} }
@ -187,7 +201,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
if (this.state.alwaysShowMenuBarSupported) { if (this.state.alwaysShowMenuBarSupported) {
autoHideMenuOption = <LabelledToggleSwitch autoHideMenuOption = <LabelledToggleSwitch
value={this.state.alwaysShowMenuBar} value={this.state.alwaysShowMenuBar}
onChange={this._onAlwaysShowMenuBarChange} onChange={this.onAlwaysShowMenuBarChange}
label={_t('Always show the window menu bar')} />; label={_t('Always show the window menu bar')} />;
} }
@ -195,7 +209,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
if (this.state.minimizeToTraySupported) { if (this.state.minimizeToTraySupported) {
minimizeToTrayOption = <LabelledToggleSwitch minimizeToTrayOption = <LabelledToggleSwitch
value={this.state.minimizeToTray} value={this.state.minimizeToTray}
onChange={this._onMinimizeToTrayChange} onChange={this.onMinimizeToTrayChange}
label={_t('Show tray icon and minimize window to it on close')} />; label={_t('Show tray icon and minimize window to it on close')} />;
} }
@ -205,22 +219,22 @@ export default class PreferencesUserSettingsTab extends React.Component {
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Room list")}</span> <span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
{this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)} {this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
</div> </div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span> <span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
{this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)} {this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
</div> </div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Timeline")}</span> <span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
{this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)} {this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
</div> </div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("General")}</span> <span className="mx_SettingsTab_subheading">{_t("General")}</span>
{this._renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)} {this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)}
{minimizeToTrayOption} {minimizeToTrayOption}
{autoHideMenuOption} {autoHideMenuOption}
{autoLaunchOption} {autoLaunchOption}
@ -229,17 +243,17 @@ export default class PreferencesUserSettingsTab extends React.Component {
label={_t('Autocomplete delay (ms)')} label={_t('Autocomplete delay (ms)')}
type='number' type='number'
value={this.state.autocompleteDelay} value={this.state.autocompleteDelay}
onChange={this._onAutocompleteDelayChange} /> onChange={this.onAutocompleteDelayChange} />
<Field <Field
label={_t('Read Marker lifetime (ms)')} label={_t('Read Marker lifetime (ms)')}
type='number' type='number'
value={this.state.readMarkerInViewThresholdMs} value={this.state.readMarkerInViewThresholdMs}
onChange={this._onReadMarkerInViewThresholdMs} /> onChange={this.onReadMarkerInViewThresholdMs} />
<Field <Field
label={_t('Read Marker off-screen lifetime (ms)')} label={_t('Read Marker off-screen lifetime (ms)')}
type='number' type='number'
value={this.state.readMarkerOutOfViewThresholdMs} value={this.state.readMarkerOutOfViewThresholdMs}
onChange={this._onReadMarkerOutOfViewThresholdMs} /> onChange={this.onReadMarkerOutOfViewThresholdMs} />
</div> </div>
</div> </div>
); );

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -133,6 +133,10 @@ export default abstract class BaseEventIndexManager {
throw new Error("Unimplemented"); throw new Error("Unimplemented");
} }
async isEventIndexEmpty(): Promise<boolean> {
throw new Error("Unimplemented");
}
/** /**
* Check if our event index is empty. * Check if our event index is empty.
*/ */

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -27,12 +27,11 @@ import {SettingLevel} from "../settings/SettingLevel";
const INDEX_VERSION = 1; const INDEX_VERSION = 1;
class EventIndexPeg { export class EventIndexPeg {
constructor() { public index: EventIndex = null;
this.index = null; public error: Error = null;
this._supportIsInstalled = false;
this.error = null; private _supportIsInstalled = false;
}
/** /**
* Initialize the EventIndexPeg and if event indexing is enabled initialize * Initialize the EventIndexPeg and if event indexing is enabled initialize
@ -181,7 +180,7 @@ class EventIndexPeg {
} }
} }
if (!global.mxEventIndexPeg) { if (!window.mxEventIndexPeg) {
global.mxEventIndexPeg = new EventIndexPeg(); window.mxEventIndexPeg = new EventIndexPeg();
} }
export default global.mxEventIndexPeg; export default window.mxEventIndexPeg;

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -25,15 +25,23 @@ const TYPING_SERVER_TIMEOUT = 30000;
* Tracks typing state for users. * Tracks typing state for users.
*/ */
export default class TypingStore { export default class TypingStore {
private typingStates: {
[roomId: string]: {
isTyping: boolean,
userTimer: Timer,
serverTimer: Timer,
},
};
constructor() { constructor() {
this.reset(); this.reset();
} }
static sharedInstance(): TypingStore { static sharedInstance(): TypingStore {
if (global.mxTypingStore === undefined) { if (window.mxTypingStore === undefined) {
global.mxTypingStore = new TypingStore(); window.mxTypingStore = new TypingStore();
} }
return global.mxTypingStore; return window.mxTypingStore;
} }
/** /**
@ -41,7 +49,7 @@ export default class TypingStore {
* MatrixClientPeg client changes. * MatrixClientPeg client changes.
*/ */
reset() { reset() {
this._typingStates = { this.typingStates = {
// "roomId": { // "roomId": {
// isTyping: bool, // Whether the user is typing or not // isTyping: bool, // Whether the user is typing or not
// userTimer: Timer, // Local timeout for "user has stopped typing" // userTimer: Timer, // Local timeout for "user has stopped typing"
@ -59,14 +67,14 @@ export default class TypingStore {
if (!SettingsStore.getValue('sendTypingNotifications')) return; if (!SettingsStore.getValue('sendTypingNotifications')) return;
if (SettingsStore.getValue('lowBandwidth')) return; if (SettingsStore.getValue('lowBandwidth')) return;
let currentTyping = this._typingStates[roomId]; let currentTyping = this.typingStates[roomId];
if ((!isTyping && !currentTyping) || (currentTyping && currentTyping.isTyping === isTyping)) { if ((!isTyping && !currentTyping) || (currentTyping && currentTyping.isTyping === isTyping)) {
// No change in state, so don't do anything. We'll let the timer run its course. // No change in state, so don't do anything. We'll let the timer run its course.
return; return;
} }
if (!currentTyping) { if (!currentTyping) {
currentTyping = this._typingStates[roomId] = { currentTyping = this.typingStates[roomId] = {
isTyping: isTyping, isTyping: isTyping,
serverTimer: new Timer(TYPING_SERVER_TIMEOUT), serverTimer: new Timer(TYPING_SERVER_TIMEOUT),
userTimer: new Timer(TYPING_USER_TIMEOUT), userTimer: new Timer(TYPING_USER_TIMEOUT),
@ -78,7 +86,7 @@ export default class TypingStore {
if (isTyping) { if (isTyping) {
if (!currentTyping.serverTimer.isRunning()) { if (!currentTyping.serverTimer.isRunning()) {
currentTyping.serverTimer.restart().finished().then(() => { currentTyping.serverTimer.restart().finished().then(() => {
const currentTyping = this._typingStates[roomId]; const currentTyping = this.typingStates[roomId];
if (currentTyping) currentTyping.isTyping = false; if (currentTyping) currentTyping.isTyping = false;
// The server will (should) time us out on typing, so we don't // The server will (should) time us out on typing, so we don't

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2018 New Vector Ltd Copyright 2018-2021 The Matrix.org Foundation C.I.C.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,6 +15,8 @@ limitations under the License.
*/ */
import EventEmitter from 'events'; import EventEmitter from 'events';
import { IWidget } from 'matrix-widget-api';
import MatrixEvent from "matrix-js-sdk/src/models/event";
import {WidgetType} from "../widgets/WidgetType"; import {WidgetType} from "../widgets/WidgetType";
/** /**
@ -23,14 +24,20 @@ import {WidgetType} from "../widgets/WidgetType";
* proxying through state from the js-sdk. * proxying through state from the js-sdk.
*/ */
class WidgetEchoStore extends EventEmitter { class WidgetEchoStore extends EventEmitter {
private roomWidgetEcho: {
[roomId: string]: {
[widgetId: string]: IWidget,
},
};
constructor() { constructor() {
super(); super();
this._roomWidgetEcho = { this.roomWidgetEcho = {
// Map as below. Object is the content of the widget state event, // Map as below. Object is the content of the widget state event,
// so for widgets that have been deleted locally, the object is empty. // so for widgets that have been deleted locally, the object is empty.
// roomId: { // roomId: {
// widgetId: [object] // widgetId: IWidget
// } // }
}; };
} }
@ -42,14 +49,14 @@ class WidgetEchoStore extends EventEmitter {
* and we don't really need the actual widget events anyway since we just want to * and we don't really need the actual widget events anyway since we just want to
* show a spinner / prevent widgets being added twice. * show a spinner / prevent widgets being added twice.
* *
* @param {Room} roomId The ID of the room to get widgets for * @param {string} roomId The ID of the room to get widgets for
* @param {MatrixEvent[]} currentRoomWidgets Current widgets for the room * @param {MatrixEvent[]} currentRoomWidgets Current widgets for the room
* @returns {MatrixEvent[]} List of widgets in the room, minus any pending removal * @returns {MatrixEvent[]} List of widgets in the room, minus any pending removal
*/ */
getEchoedRoomWidgets(roomId, currentRoomWidgets) { getEchoedRoomWidgets(roomId: string, currentRoomWidgets: MatrixEvent[]): MatrixEvent[] {
const echoedWidgets = []; const echoedWidgets = [];
const roomEchoState = Object.assign({}, this._roomWidgetEcho[roomId]); const roomEchoState = Object.assign({}, this.roomWidgetEcho[roomId]);
for (const w of currentRoomWidgets) { for (const w of currentRoomWidgets) {
const widgetId = w.getStateKey(); const widgetId = w.getStateKey();
@ -65,8 +72,8 @@ class WidgetEchoStore extends EventEmitter {
return echoedWidgets; return echoedWidgets;
} }
roomHasPendingWidgetsOfType(roomId, currentRoomWidgets, type: WidgetType) { roomHasPendingWidgetsOfType(roomId: string, currentRoomWidgets: MatrixEvent[], type?: WidgetType): boolean {
const roomEchoState = Object.assign({}, this._roomWidgetEcho[roomId]); const roomEchoState = Object.assign({}, this.roomWidgetEcho[roomId]);
// any widget IDs that are already in the room are not pending, so // any widget IDs that are already in the room are not pending, so
// echoes for them don't count as pending. // echoes for them don't count as pending.
@ -85,20 +92,20 @@ class WidgetEchoStore extends EventEmitter {
} }
} }
roomHasPendingWidgets(roomId, currentRoomWidgets) { roomHasPendingWidgets(roomId: string, currentRoomWidgets: MatrixEvent[]): boolean {
return this.roomHasPendingWidgetsOfType(roomId, currentRoomWidgets); return this.roomHasPendingWidgetsOfType(roomId, currentRoomWidgets);
} }
setRoomWidgetEcho(roomId, widgetId, state) { setRoomWidgetEcho(roomId: string, widgetId: string, state: IWidget) {
if (this._roomWidgetEcho[roomId] === undefined) this._roomWidgetEcho[roomId] = {}; if (this.roomWidgetEcho[roomId] === undefined) this.roomWidgetEcho[roomId] = {};
this._roomWidgetEcho[roomId][widgetId] = state; this.roomWidgetEcho[roomId][widgetId] = state;
this.emit('update', roomId, widgetId); this.emit('update', roomId, widgetId);
} }
removeRoomWidgetEcho(roomId, widgetId) { removeRoomWidgetEcho(roomId: string, widgetId: string) {
delete this._roomWidgetEcho[roomId][widgetId]; delete this.roomWidgetEcho[roomId][widgetId];
if (Object.keys(this._roomWidgetEcho[roomId]).length === 0) delete this._roomWidgetEcho[roomId]; if (Object.keys(this.roomWidgetEcho[roomId]).length === 0) delete this.roomWidgetEcho[roomId];
this.emit('update', roomId, widgetId); this.emit('update', roomId, widgetId);
} }
} }

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { ReactNode } from 'react';
import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery"; import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery";
import {_t, _td, newTranslatableError} from "../languageHandler"; import {_t, _td, newTranslatableError} from "../languageHandler";
import {makeType} from "./TypeUtils"; import {makeType} from "./TypeUtils";
import SdkConfig from '../SdkConfig'; import SdkConfig from '../SdkConfig';
const LIVELINESS_DISCOVERY_ERRORS = [ const LIVELINESS_DISCOVERY_ERRORS: string[] = [
AutoDiscovery.ERROR_INVALID_HOMESERVER, AutoDiscovery.ERROR_INVALID_HOMESERVER,
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER, AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
]; ];
@ -40,17 +39,23 @@ export class ValidatedServerConfig {
warning: string; warning: string;
} }
export interface IAuthComponentState {
serverIsAlive: boolean;
serverErrorIsFatal: boolean;
serverDeadError?: ReactNode;
}
export default class AutoDiscoveryUtils { export default class AutoDiscoveryUtils {
/** /**
* Checks if a given error or error message is considered an error * Checks if a given error or error message is considered an error
* relating to the liveliness of the server. Must be an error returned * relating to the liveliness of the server. Must be an error returned
* from this AutoDiscoveryUtils class. * from this AutoDiscoveryUtils class.
* @param {string|Error} error The error to check * @param {string | Error} error The error to check
* @returns {boolean} True if the error is a liveliness error. * @returns {boolean} True if the error is a liveliness error.
*/ */
static isLivelinessError(error: string|Error): boolean { static isLivelinessError(error: string | Error): boolean {
if (!error) return false; if (!error) return false;
return !!LIVELINESS_DISCOVERY_ERRORS.find(e => e === error || e === error.message); return !!LIVELINESS_DISCOVERY_ERRORS.find(e => typeof error === "string" ? e === error : e === error.message);
} }
/** /**
@ -61,7 +66,7 @@ export default class AutoDiscoveryUtils {
* implementation for known values. * implementation for known values.
* @returns {*} The state for the component, given the error. * @returns {*} The state for the component, given the error.
*/ */
static authComponentStateForError(err: string | Error | null, pageName = "login"): Object { static authComponentStateForError(err: string | Error | null, pageName = "login"): IAuthComponentState {
if (!err) { if (!err) {
return { return {
serverIsAlive: true, serverIsAlive: true,
@ -70,7 +75,7 @@ export default class AutoDiscoveryUtils {
}; };
} }
let title = _t("Cannot reach homeserver"); let title = _t("Cannot reach homeserver");
let body = _t("Ensure you have a stable internet connection, or get in touch with the server admin"); let body: ReactNode = _t("Ensure you have a stable internet connection, or get in touch with the server admin");
if (!AutoDiscoveryUtils.isLivelinessError(err)) { if (!AutoDiscoveryUtils.isLivelinessError(err)) {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
title = _t("Your %(brand)s is misconfigured", { brand }); title = _t("Your %(brand)s is misconfigured", { brand });
@ -92,7 +97,7 @@ export default class AutoDiscoveryUtils {
} }
let isFatalError = true; let isFatalError = true;
const errorMessage = err.message ? err.message : err; const errorMessage = typeof err === "string" ? err : err.message;
if (errorMessage === AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER) { if (errorMessage === AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER) {
isFatalError = false; isFatalError = false;
title = _t("Cannot reach identity server"); title = _t("Cannot reach identity server");
@ -141,7 +146,10 @@ export default class AutoDiscoveryUtils {
* @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration. * @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration.
*/ */
static async validateServerConfigWithStaticUrls( static async validateServerConfigWithStaticUrls(
homeserverUrl: string, identityUrl: string, syntaxOnly = false): ValidatedServerConfig { homeserverUrl: string,
identityUrl?: string,
syntaxOnly = false,
): Promise<ValidatedServerConfig> {
if (!homeserverUrl) { if (!homeserverUrl) {
throw newTranslatableError(_td("No homeserver URL provided")); throw newTranslatableError(_td("No homeserver URL provided"));
} }
@ -171,7 +179,7 @@ export default class AutoDiscoveryUtils {
* @param {string} serverName The homeserver domain name (eg: "matrix.org") to validate. * @param {string} serverName The homeserver domain name (eg: "matrix.org") to validate.
* @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration. * @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration.
*/ */
static async validateServerName(serverName: string): ValidatedServerConfig { static async validateServerName(serverName: string): Promise<ValidatedServerConfig> {
const result = await AutoDiscovery.findClientConfig(serverName); const result = await AutoDiscovery.findClientConfig(serverName);
return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result);
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -32,15 +32,15 @@ try {
const SYNC_STORE_NAME = "riot-web-sync"; const SYNC_STORE_NAME = "riot-web-sync";
const CRYPTO_STORE_NAME = "matrix-js-sdk:crypto"; const CRYPTO_STORE_NAME = "matrix-js-sdk:crypto";
function log(msg) { function log(msg: string) {
console.log(`StorageManager: ${msg}`); console.log(`StorageManager: ${msg}`);
} }
function error(msg) { function error(msg: string, ...args: string[]) {
console.error(`StorageManager: ${msg}`); console.error(`StorageManager: ${msg}`, ...args);
} }
function track(action) { function track(action: string) {
Analytics.trackEvent("StorageManager", action); Analytics.trackEvent("StorageManager", action);
} }
@ -73,7 +73,7 @@ export async function checkConsistency() {
dataInLocalStorage = localStorage.length > 0; dataInLocalStorage = localStorage.length > 0;
log(`Local storage contains data? ${dataInLocalStorage}`); log(`Local storage contains data? ${dataInLocalStorage}`);
cryptoInited = localStorage.getItem("mx_crypto_initialised"); cryptoInited = !!localStorage.getItem("mx_crypto_initialised");
log(`Crypto initialised? ${cryptoInited}`); log(`Crypto initialised? ${cryptoInited}`);
} else { } else {
healthy = false; healthy = false;

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2018 New Vector Ltd Copyright 2018, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -26,44 +26,48 @@ Once a timer is finished or aborted, it can't be started again
a new one through `clone()` or `cloneIfRun()`. a new one through `clone()` or `cloneIfRun()`.
*/ */
export default class Timer { export default class Timer {
constructor(timeout) { private timerHandle: NodeJS.Timeout;
this._timeout = timeout; private startTs: number;
this._onTimeout = this._onTimeout.bind(this); private promise: Promise<void>;
this._setNotStarted(); private resolve: () => void;
private reject: (Error) => void;
constructor(private timeout: number) {
this.setNotStarted();
} }
_setNotStarted() { private setNotStarted() {
this._timerHandle = null; this.timerHandle = null;
this._startTs = null; this.startTs = null;
this._promise = new Promise((resolve, reject) => { this.promise = new Promise<void>((resolve, reject) => {
this._resolve = resolve; this.resolve = resolve;
this._reject = reject; this.reject = reject;
}).finally(() => { }).finally(() => {
this._timerHandle = null; this.timerHandle = null;
}); });
} }
_onTimeout() { private onTimeout = () => {
const now = Date.now(); const now = Date.now();
const elapsed = now - this._startTs; const elapsed = now - this.startTs;
if (elapsed >= this._timeout) { if (elapsed >= this.timeout) {
this._resolve(); this.resolve();
this._setNotStarted(); this.setNotStarted();
} else { } else {
const delta = this._timeout - elapsed; const delta = this.timeout - elapsed;
this._timerHandle = setTimeout(this._onTimeout, delta); this.timerHandle = setTimeout(this.onTimeout, delta);
} }
} }
changeTimeout(timeout) { changeTimeout(timeout: number) {
if (timeout === this._timeout) { if (timeout === this.timeout) {
return; return;
} }
const isSmallerTimeout = timeout < this._timeout; const isSmallerTimeout = timeout < this.timeout;
this._timeout = timeout; this.timeout = timeout;
if (this.isRunning() && isSmallerTimeout) { if (this.isRunning() && isSmallerTimeout) {
clearTimeout(this._timerHandle); clearTimeout(this.timerHandle);
this._onTimeout(); this.onTimeout();
} }
} }
@ -73,8 +77,8 @@ export default class Timer {
*/ */
start() { start() {
if (!this.isRunning()) { if (!this.isRunning()) {
this._startTs = Date.now(); this.startTs = Date.now();
this._timerHandle = setTimeout(this._onTimeout, this._timeout); this.timerHandle = setTimeout(this.onTimeout, this.timeout);
} }
return this; return this;
} }
@ -89,7 +93,7 @@ export default class Timer {
// can be called in fast succession, // can be called in fast succession,
// instead just take note and compare // instead just take note and compare
// when the already running timeout expires // when the already running timeout expires
this._startTs = Date.now(); this.startTs = Date.now();
return this; return this;
} else { } else {
return this.start(); return this.start();
@ -103,9 +107,9 @@ export default class Timer {
*/ */
abort() { abort() {
if (this.isRunning()) { if (this.isRunning()) {
clearTimeout(this._timerHandle); clearTimeout(this.timerHandle);
this._reject(new Error("Timer was aborted.")); this.reject(new Error("Timer was aborted."));
this._setNotStarted(); this.setNotStarted();
} }
return this; return this;
} }
@ -116,10 +120,10 @@ export default class Timer {
*@return {Promise} *@return {Promise}
*/ */
finished() { finished() {
return this._promise; return this.promise;
} }
isRunning() { isRunning() {
return this._timerHandle !== null; return this.timerHandle !== null;
} }
} }

View file

@ -20,31 +20,31 @@ import PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor";
* Generates permalinks that self-reference the running webapp * Generates permalinks that self-reference the running webapp
*/ */
export default class ElementPermalinkConstructor extends PermalinkConstructor { export default class ElementPermalinkConstructor extends PermalinkConstructor {
_elementUrl: string; private elementUrl: string;
constructor(elementUrl: string) { constructor(elementUrl: string) {
super(); super();
this._elementUrl = elementUrl; this.elementUrl = elementUrl;
if (!this._elementUrl.startsWith("http:") && !this._elementUrl.startsWith("https:")) { if (!this.elementUrl.startsWith("http:") && !this.elementUrl.startsWith("https:")) {
throw new Error("Element prefix URL does not appear to be an HTTP(S) URL"); throw new Error("Element prefix URL does not appear to be an HTTP(S) URL");
} }
} }
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string { forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
return `${this._elementUrl}/#/room/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`; return `${this.elementUrl}/#/room/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`;
} }
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string { forRoom(roomIdOrAlias: string, serverCandidates?: string[]): string {
return `${this._elementUrl}/#/room/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`; return `${this.elementUrl}/#/room/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`;
} }
forUser(userId: string): string { forUser(userId: string): string {
return `${this._elementUrl}/#/user/${userId}`; return `${this.elementUrl}/#/user/${userId}`;
} }
forGroup(groupId: string): string { forGroup(groupId: string): string {
return `${this._elementUrl}/#/group/${groupId}`; return `${this.elementUrl}/#/group/${groupId}`;
} }
forEntity(entityId: string): string { forEntity(entityId: string): string {
@ -58,11 +58,11 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
} }
isPermalinkHost(testHost: string): boolean { isPermalinkHost(testHost: string): boolean {
const parsedUrl = new URL(this._elementUrl); const parsedUrl = new URL(this.elementUrl);
return testHost === (parsedUrl.host || parsedUrl.hostname); // one of the hosts should match return testHost === (parsedUrl.host || parsedUrl.hostname); // one of the hosts should match
} }
encodeServerCandidates(candidates: string[]) { encodeServerCandidates(candidates?: string[]) {
if (!candidates || candidates.length === 0) return ''; if (!candidates || candidates.length === 0) return '';
return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`; return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
} }
@ -71,11 +71,11 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
// https://github.com/turt2live/matrix-js-bot-sdk/blob/7c4665c9a25c2c8e0fe4e509f2616505b5b66a1c/src/Permalinks.ts#L33-L61 // https://github.com/turt2live/matrix-js-bot-sdk/blob/7c4665c9a25c2c8e0fe4e509f2616505b5b66a1c/src/Permalinks.ts#L33-L61
// Adapted for Element's URL format // Adapted for Element's URL format
parsePermalink(fullUrl: string): PermalinkParts { parsePermalink(fullUrl: string): PermalinkParts {
if (!fullUrl || !fullUrl.startsWith(this._elementUrl)) { if (!fullUrl || !fullUrl.startsWith(this.elementUrl)) {
throw new Error("Does not appear to be a permalink"); throw new Error("Does not appear to be a permalink");
} }
const parts = fullUrl.substring(`${this._elementUrl}/#/`.length); const parts = fullUrl.substring(`${this.elementUrl}/#/`.length);
return ElementPermalinkConstructor.parseAppRoute(parts); return ElementPermalinkConstructor.parseAppRoute(parts);
} }

View file

@ -17,6 +17,9 @@ limitations under the License.
import isIp from "is-ip"; import isIp from "is-ip";
import * as utils from "matrix-js-sdk/src/utils"; import * as utils from "matrix-js-sdk/src/utils";
import {Room} from "matrix-js-sdk/src/models/room"; import {Room} from "matrix-js-sdk/src/models/room";
import {EventType} from "matrix-js-sdk/src/@types/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import {MatrixClientPeg} from "../../MatrixClientPeg"; import {MatrixClientPeg} from "../../MatrixClientPeg";
import SpecPermalinkConstructor, {baseUrl as matrixtoBaseUrl} from "./SpecPermalinkConstructor"; import SpecPermalinkConstructor, {baseUrl as matrixtoBaseUrl} from "./SpecPermalinkConstructor";
@ -74,29 +77,35 @@ const MAX_SERVER_CANDIDATES = 3;
// the list and magically have the link work. // the list and magically have the link work.
export class RoomPermalinkCreator { export class RoomPermalinkCreator {
private room: Room;
private roomId: string;
private highestPlUserId: string;
private populationMap: { [serverName: string]: number };
private bannedHostsRegexps: RegExp[];
private allowedHostsRegexps: RegExp[];
private _serverCandidates: string[];
private started: boolean;
// We support being given a roomId as a fallback in the event the `room` object // We support being given a roomId as a fallback in the event the `room` object
// doesn't exist or is not healthy for us to rely on. For example, loading a // doesn't exist or is not healthy for us to rely on. For example, loading a
// permalink to a room which the MatrixClient doesn't know about. // permalink to a room which the MatrixClient doesn't know about.
constructor(room, roomId = null) { constructor(room: Room, roomId: string = null) {
this._room = room; this.room = room;
this._roomId = room ? room.roomId : roomId; this.roomId = room ? room.roomId : roomId;
this._highestPlUserId = null; this.highestPlUserId = null;
this._populationMap = null; this.populationMap = null;
this._bannedHostsRegexps = null; this.bannedHostsRegexps = null;
this._allowedHostsRegexps = null; this.allowedHostsRegexps = null;
this._serverCandidates = null; this._serverCandidates = null;
this._started = false; this.started = false;
if (!this._roomId) { if (!this.roomId) {
throw new Error("Failed to resolve a roomId for the permalink creator to use"); throw new Error("Failed to resolve a roomId for the permalink creator to use");
} }
this.onMembership = this.onMembership.bind(this);
this.onRoomState = this.onRoomState.bind(this);
} }
load() { load() {
if (!this._room || !this._room.currentState) { if (!this.room || !this.room.currentState) {
// Under rare and unknown circumstances it is possible to have a room with no // Under rare and unknown circumstances it is possible to have a room with no
// currentState, at least potentially at the early stages of joining a room. // currentState, at least potentially at the early stages of joining a room.
// To avoid breaking everything, we'll just warn rather than throw as well as // To avoid breaking everything, we'll just warn rather than throw as well as
@ -104,23 +113,23 @@ export class RoomPermalinkCreator {
console.warn("Tried to load a permalink creator with no room state"); console.warn("Tried to load a permalink creator with no room state");
return; return;
} }
this._updateAllowedServers(); this.updateAllowedServers();
this._updateHighestPlUser(); this.updateHighestPlUser();
this._updatePopulationMap(); this.updatePopulationMap();
this._updateServerCandidates(); this.updateServerCandidates();
} }
start() { start() {
this.load(); this.load();
this._room.on("RoomMember.membership", this.onMembership); this.room.on("RoomMember.membership", this.onMembership);
this._room.on("RoomState.events", this.onRoomState); this.room.on("RoomState.events", this.onRoomState);
this._started = true; this.started = true;
} }
stop() { stop() {
this._room.removeListener("RoomMember.membership", this.onMembership); this.room.removeListener("RoomMember.membership", this.onMembership);
this._room.removeListener("RoomState.events", this.onRoomState); this.room.removeListener("RoomState.events", this.onRoomState);
this._started = false; this.started = false;
} }
get serverCandidates() { get serverCandidates() {
@ -128,44 +137,44 @@ export class RoomPermalinkCreator {
} }
isStarted() { isStarted() {
return this._started; return this.started;
} }
forEvent(eventId) { forEvent(eventId: string): string {
return getPermalinkConstructor().forEvent(this._roomId, eventId, this._serverCandidates); return getPermalinkConstructor().forEvent(this.roomId, eventId, this._serverCandidates);
} }
forShareableRoom() { forShareableRoom(): string {
if (this._room) { if (this.room) {
// Prefer to use canonical alias for permalink if possible // Prefer to use canonical alias for permalink if possible
const alias = this._room.getCanonicalAlias(); const alias = this.room.getCanonicalAlias();
if (alias) { if (alias) {
return getPermalinkConstructor().forRoom(alias, this._serverCandidates); return getPermalinkConstructor().forRoom(alias, this._serverCandidates);
} }
} }
return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates); return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
} }
forRoom() { forRoom(): string {
return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates); return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
} }
onRoomState(event) { private onRoomState = (event: MatrixEvent) => {
switch (event.getType()) { switch (event.getType()) {
case "m.room.server_acl": case EventType.RoomServerAcl:
this._updateAllowedServers(); this.updateAllowedServers();
this._updateHighestPlUser(); this.updateHighestPlUser();
this._updatePopulationMap(); this.updatePopulationMap();
this._updateServerCandidates(); this.updateServerCandidates();
return; return;
case "m.room.power_levels": case EventType.RoomPowerLevels:
this._updateHighestPlUser(); this.updateHighestPlUser();
this._updateServerCandidates(); this.updateServerCandidates();
return; return;
} }
} }
onMembership(evt, member, oldMembership) { private onMembership = (evt: MatrixEvent, member: RoomMember, oldMembership: string) => {
const userId = member.userId; const userId = member.userId;
const membership = member.membership; const membership = member.membership;
const serverName = getServerName(userId); const serverName = getServerName(userId);
@ -173,17 +182,17 @@ export class RoomPermalinkCreator {
const hasLeft = oldMembership === "join" && membership !== "join"; const hasLeft = oldMembership === "join" && membership !== "join";
if (hasLeft) { if (hasLeft) {
this._populationMap[serverName]--; this.populationMap[serverName]--;
} else if (hasJoined) { } else if (hasJoined) {
this._populationMap[serverName]++; this.populationMap[serverName]++;
} }
this._updateHighestPlUser(); this.updateHighestPlUser();
this._updateServerCandidates(); this.updateServerCandidates();
} }
_updateHighestPlUser() { private updateHighestPlUser() {
const plEvent = this._room.currentState.getStateEvents("m.room.power_levels", ""); const plEvent = this.room.currentState.getStateEvents("m.room.power_levels", "");
if (plEvent) { if (plEvent) {
const content = plEvent.getContent(); const content = plEvent.getContent();
if (content) { if (content) {
@ -191,14 +200,14 @@ export class RoomPermalinkCreator {
if (users) { if (users) {
const entries = Object.entries(users); const entries = Object.entries(users);
const allowedEntries = entries.filter(([userId]) => { const allowedEntries = entries.filter(([userId]) => {
const member = this._room.getMember(userId); const member = this.room.getMember(userId);
if (!member || member.membership !== "join") { if (!member || member.membership !== "join") {
return false; return false;
} }
const serverName = getServerName(userId); const serverName = getServerName(userId);
return !isHostnameIpAddress(serverName) && return !isHostnameIpAddress(serverName) &&
!isHostInRegex(serverName, this._bannedHostsRegexps) && !isHostInRegex(serverName, this.bannedHostsRegexps) &&
isHostInRegex(serverName, this._allowedHostsRegexps); isHostInRegex(serverName, this.allowedHostsRegexps);
}); });
const maxEntry = allowedEntries.reduce((max, entry) => { const maxEntry = allowedEntries.reduce((max, entry) => {
return (entry[1] > max[1]) ? entry : max; return (entry[1] > max[1]) ? entry : max;
@ -206,20 +215,20 @@ export class RoomPermalinkCreator {
const [userId, powerLevel] = maxEntry; const [userId, powerLevel] = maxEntry;
// object wasn't empty, and max entry wasn't a demotion from the default // object wasn't empty, and max entry wasn't a demotion from the default
if (userId !== null && powerLevel >= 50) { if (userId !== null && powerLevel >= 50) {
this._highestPlUserId = userId; this.highestPlUserId = userId;
return; return;
} }
} }
} }
} }
this._highestPlUserId = null; this.highestPlUserId = null;
} }
_updateAllowedServers() { private updateAllowedServers() {
const bannedHostsRegexps = []; const bannedHostsRegexps = [];
let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone
if (this._room.currentState) { if (this.room.currentState) {
const aclEvent = this._room.currentState.getStateEvents("m.room.server_acl", ""); const aclEvent = this.room.currentState.getStateEvents("m.room.server_acl", "");
if (aclEvent && aclEvent.getContent()) { if (aclEvent && aclEvent.getContent()) {
const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$"); const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$");
@ -231,35 +240,35 @@ export class RoomPermalinkCreator {
allowed.forEach(h => allowedHostsRegexps.push(getRegex(h))); allowed.forEach(h => allowedHostsRegexps.push(getRegex(h)));
} }
} }
this._bannedHostsRegexps = bannedHostsRegexps; this.bannedHostsRegexps = bannedHostsRegexps;
this._allowedHostsRegexps = allowedHostsRegexps; this.allowedHostsRegexps = allowedHostsRegexps;
} }
_updatePopulationMap() { private updatePopulationMap() {
const populationMap: { [server: string]: number } = {}; const populationMap: { [server: string]: number } = {};
for (const member of this._room.getJoinedMembers()) { for (const member of this.room.getJoinedMembers()) {
const serverName = getServerName(member.userId); const serverName = getServerName(member.userId);
if (!populationMap[serverName]) { if (!populationMap[serverName]) {
populationMap[serverName] = 0; populationMap[serverName] = 0;
} }
populationMap[serverName]++; populationMap[serverName]++;
} }
this._populationMap = populationMap; this.populationMap = populationMap;
} }
_updateServerCandidates() { private updateServerCandidates() {
let candidates = []; let candidates = [];
if (this._highestPlUserId) { if (this.highestPlUserId) {
candidates.push(getServerName(this._highestPlUserId)); candidates.push(getServerName(this.highestPlUserId));
} }
const serversByPopulation = Object.keys(this._populationMap) const serversByPopulation = Object.keys(this.populationMap)
.sort((a, b) => this._populationMap[b] - this._populationMap[a]) .sort((a, b) => this.populationMap[b] - this.populationMap[a])
.filter(a => { .filter(a => {
return !candidates.includes(a) && return !candidates.includes(a) &&
!isHostnameIpAddress(a) && !isHostnameIpAddress(a) &&
!isHostInRegex(a, this._bannedHostsRegexps) && !isHostInRegex(a, this.bannedHostsRegexps) &&
isHostInRegex(a, this._allowedHostsRegexps); isHostInRegex(a, this.allowedHostsRegexps);
}); });
const remainingServers = serversByPopulation.slice(0, MAX_SERVER_CANDIDATES - candidates.length); const remainingServers = serversByPopulation.slice(0, MAX_SERVER_CANDIDATES - candidates.length);
@ -273,11 +282,11 @@ export function makeGenericPermalink(entityId: string): string {
return getPermalinkConstructor().forEntity(entityId); return getPermalinkConstructor().forEntity(entityId);
} }
export function makeUserPermalink(userId) { export function makeUserPermalink(userId: string): string {
return getPermalinkConstructor().forUser(userId); return getPermalinkConstructor().forUser(userId);
} }
export function makeRoomPermalink(roomId) { export function makeRoomPermalink(roomId: string): string {
if (!roomId) { if (!roomId) {
throw new Error("can't permalink a falsey roomId"); throw new Error("can't permalink a falsey roomId");
} }
@ -296,7 +305,7 @@ export function makeRoomPermalink(roomId) {
return permalinkCreator.forRoom(); return permalinkCreator.forRoom();
} }
export function makeGroupPermalink(groupId) { export function makeGroupPermalink(groupId: string): string {
return getPermalinkConstructor().forGroup(groupId); return getPermalinkConstructor().forGroup(groupId);
} }
@ -428,24 +437,24 @@ export function parseAppLocalLink(localLink: string): PermalinkParts {
return null; return null;
} }
function getServerName(userId) { function getServerName(userId: string): string {
return userId.split(":").splice(1).join(":"); return userId.split(":").splice(1).join(":");
} }
function getHostnameFromMatrixDomain(domain) { function getHostnameFromMatrixDomain(domain: string): string {
if (!domain) return null; if (!domain) return null;
return new URL(`https://${domain}`).hostname; return new URL(`https://${domain}`).hostname;
} }
function isHostInRegex(hostname, regexps) { function isHostInRegex(hostname: string, regexps: RegExp[]) {
hostname = getHostnameFromMatrixDomain(hostname); hostname = getHostnameFromMatrixDomain(hostname);
if (!hostname) return true; // assumed if (!hostname) return true; // assumed
if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0]); if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0].toString());
return regexps.filter(h => h.test(hostname)).length > 0; return regexps.filter(h => h.test(hostname)).length > 0;
} }
function isHostnameIpAddress(hostname) { function isHostnameIpAddress(hostname: string): boolean {
hostname = getHostnameFromMatrixDomain(hostname); hostname = getHostnameFromMatrixDomain(hostname);
if (!hostname) return false; if (!hostname) return false;

View file

@ -29,7 +29,7 @@ describe('ScalarAuthClient', function() {
it('should request a new token if the old one fails', async function() { it('should request a new token if the old one fails', async function() {
const sac = new ScalarAuthClient(); const sac = new ScalarAuthClient();
sac._getAccountName = jest.fn((arg) => { sac.getAccountName = jest.fn((arg) => {
switch (arg) { switch (arg) {
case "brokentoken": case "brokentoken":
return Promise.reject({ return Promise.reject({