Merge pull request #5912 from matrix-org/jryans/convert-flow-to-ts
Convert some Flow-typed files to TypeScript
This commit is contained in:
commit
dd8abb0206
40 changed files with 1062 additions and 849 deletions
8
src/@types/global.d.ts
vendored
8
src/@types/global.d.ts
vendored
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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");
|
|
@ -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>
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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>
|
|
@ -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 it’s a good place for many.");
|
text = _t("Matrix.org is the biggest public homeserver in the world, so it’s 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}>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
|
@ -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,
|
|
||||||
};
|
|
|
@ -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);
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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>
|
|
@ -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>
|
||||||
);
|
);
|
|
@ -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.")}
|
{_t("Guests cannot join this room even if explicitly invited.")}
|
||||||
<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}
|
|
@ -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, is ugly but necessary.
|
// Also, 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 ©
|
target="_blank">default cover photo</a> is ©
|
||||||
<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
|
target="_blank">Jesús Roncero</a> used under the terms of
|
||||||
<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 ©
|
target="_blank">twemoji-colr</a> font is ©
|
||||||
<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
|
target="_blank">Mozilla Foundation</a> used under the terms of
|
||||||
<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 ©
|
target="_blank">Twemoji</a> emoji art is ©
|
||||||
<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
|
target="_blank">Twitter, Inc and other contributors</a> used under the terms of
|
||||||
<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()}
|
||||||
|
>
|
||||||
<{ _t("click to reveal") }>
|
<{ _t("click to reveal") }>
|
||||||
</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>
|
|
@ -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>
|
</AccessibleButton>
|
||||||
<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")}
|
|
@ -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>
|
||||||
);
|
);
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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;
|
|
@ -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
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
|
@ -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;
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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({
|
||||||
|
|
Loading…
Reference in a new issue