Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into mac-redo
This commit is contained in:
@@ -260,7 +260,6 @@ class Analytics {
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Analytics error: ", e);
|
||||
window.err = e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,8 @@ export function getInitialLetter(name) {
|
||||
}
|
||||
|
||||
export function avatarUrlForRoom(room, width, height, resizeMethod) {
|
||||
if (!room) return null; // null-guard
|
||||
|
||||
const explicitRoomAvatar = room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
width,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +19,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
import dis from './dispatcher';
|
||||
import BaseEventIndexManager from './indexing/BaseEventIndexManager';
|
||||
|
||||
@@ -162,4 +164,28 @@ export default class BasePlatform {
|
||||
getEventIndexingManager(): BaseEventIndexManager | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
setLanguage(preferredLangs: string[]) {}
|
||||
|
||||
getSSOCallbackUrl(hsUrl: string, isUrl: string): URL {
|
||||
const url = new URL(window.location.href);
|
||||
// XXX: at this point, the fragment will always be #/login, which is no
|
||||
// use to anyone. Ideally, we would get the intended fragment from
|
||||
// MatrixChat.screenAfterLogin so that you could follow #/room links etc
|
||||
// through an SSO login.
|
||||
url.hash = "";
|
||||
url.searchParams.set("homeserver", hsUrl);
|
||||
url.searchParams.set("identityServer", isUrl);
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin Single Sign On flows.
|
||||
* @param {MatrixClient} mxClient the matrix client using which we should start the flow
|
||||
* @param {"sso"|"cas"} loginType the type of SSO it is, CAS/SSO.
|
||||
*/
|
||||
startSingleSignOn(mxClient: MatrixClient, loginType: "sso"|"cas") {
|
||||
const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl());
|
||||
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ function _setCallListeners(call) {
|
||||
"if you proceed without verifying them, it will be "+
|
||||
"possible for someone to eavesdrop on your call.",
|
||||
),
|
||||
button: _t('Review Devices'),
|
||||
button: _t('Review Sessions'),
|
||||
onFinished: function(confirmed) {
|
||||
if (confirmed) {
|
||||
const room = MatrixClientPeg.get().getRoom(call.roomId);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
|
||||
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
|
||||
import { _t } from './languageHandler';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
|
||||
|
||||
// This stores the secret storage private keys in memory for the JS SDK. This is
|
||||
// only meant to act as a cache to avoid prompting the user multiple times
|
||||
@@ -125,8 +126,37 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
|
||||
return [name, key];
|
||||
}
|
||||
|
||||
const onSecretRequested = async function({
|
||||
user_id: userId,
|
||||
device_id: deviceId,
|
||||
request_id: requestId,
|
||||
name,
|
||||
device_trust: deviceTrust,
|
||||
}) {
|
||||
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
|
||||
const client = MatrixClientPeg.get();
|
||||
if (userId !== client.getUserId()) {
|
||||
return;
|
||||
}
|
||||
if (!deviceTrust || !deviceTrust.isVerified()) {
|
||||
console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`);
|
||||
return;
|
||||
}
|
||||
const callbacks = client.getCrossSigningCacheCallbacks();
|
||||
if (!callbacks.getCrossSigningKeyCache) return;
|
||||
if (name === "m.cross_signing.self_signing") {
|
||||
const key = await callbacks.getCrossSigningKeyCache("self_signing");
|
||||
return key && encodeBase64(key);
|
||||
} else if (name === "m.cross_signing.user_signing") {
|
||||
const key = await callbacks.getCrossSigningKeyCache("user_signing");
|
||||
return key && encodeBase64(key);
|
||||
}
|
||||
console.warn("onSecretRequested didn't recognise the secret named ", name);
|
||||
};
|
||||
|
||||
export const crossSigningCallbacks = {
|
||||
getSecretStorageKey,
|
||||
onSecretRequested,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -99,9 +99,13 @@ export default class DeviceListener {
|
||||
}
|
||||
|
||||
async _recheck() {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) return;
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
if (
|
||||
!SettingsStore.isFeatureEnabled("feature_cross_signing") ||
|
||||
!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
|
||||
) return;
|
||||
|
||||
if (!cli.isCryptoEnabled()) return;
|
||||
if (!cli.getCrossSigningId()) {
|
||||
if (this._dismissedThisDeviceToast) {
|
||||
|
||||
@@ -23,7 +23,6 @@ import ReplyThread from "./components/views/elements/ReplyThread";
|
||||
|
||||
import React from 'react';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import highlight from 'highlight.js';
|
||||
import * as linkify from 'linkifyjs';
|
||||
import linkifyMatrix from './linkify-matrix';
|
||||
import _linkifyElement from 'linkifyjs/element';
|
||||
@@ -160,7 +159,7 @@ const transformTags = { // custom to matrix
|
||||
delete attribs.target;
|
||||
}
|
||||
}
|
||||
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||
attribs.rel = 'noreferrer noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||
return { tagName, attribs };
|
||||
},
|
||||
'img': function(tagName, attribs) {
|
||||
@@ -467,11 +466,12 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||
/**
|
||||
* Linkifies the given string. This is a wrapper around 'linkifyjs/string'.
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
* @param {string} str string to linkify
|
||||
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
||||
* @returns {string} Linkified string
|
||||
*/
|
||||
export function linkifyString(str) {
|
||||
return _linkifyString(str);
|
||||
export function linkifyString(str, options = linkifyMatrix.options) {
|
||||
return _linkifyString(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,10 +489,11 @@ export function linkifyElement(element, options = linkifyMatrix.options) {
|
||||
* Linkify the given string and sanitize the HTML afterwards.
|
||||
*
|
||||
* @param {string} dirtyHtml The HTML string to sanitize and linkify
|
||||
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
||||
* @returns {string}
|
||||
*/
|
||||
export function linkifyAndSanitizeHtml(dirtyHtml) {
|
||||
return sanitizeHtml(linkifyString(dirtyHtml), sanitizeHtmlParams);
|
||||
export function linkifyAndSanitizeHtml(dirtyHtml, options = linkifyMatrix.options) {
|
||||
return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -181,24 +181,12 @@ export default class IdentityAuthClient {
|
||||
}
|
||||
|
||||
async registerForToken(check=true) {
|
||||
try {
|
||||
const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken();
|
||||
// XXX: The spec is `token`, but we used `access_token` for a Sydent release.
|
||||
const { access_token: accessToken, token } =
|
||||
await this._matrixClient.registerWithIdentityServer(hsOpenIdToken);
|
||||
const identityAccessToken = token ? token : accessToken;
|
||||
if (check) await this._checkToken(identityAccessToken);
|
||||
return identityAccessToken;
|
||||
} catch (e) {
|
||||
if (e.cors === "rejected" || e.httpStatus === 404) {
|
||||
// Assume IS only supports deprecated v1 API for now
|
||||
// TODO: Remove this path once v2 is only supported version
|
||||
// See https://github.com/vector-im/riot-web/issues/10443
|
||||
console.warn("IS doesn't support v2 auth");
|
||||
this.authEnabled = false;
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken();
|
||||
// XXX: The spec is `token`, but we used `access_token` for a Sydent release.
|
||||
const { access_token: accessToken, token } =
|
||||
await this._matrixClient.registerWithIdentityServer(hsOpenIdToken);
|
||||
const identityAccessToken = token ? token : accessToken;
|
||||
if (check) await this._checkToken(identityAccessToken);
|
||||
return identityAccessToken;
|
||||
}
|
||||
}
|
||||
|
||||
28
src/Login.js
28
src/Login.js
@@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
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");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -19,8 +20,6 @@ limitations under the License.
|
||||
|
||||
import Matrix from "matrix-js-sdk";
|
||||
|
||||
import url from 'url';
|
||||
|
||||
export default class Login {
|
||||
constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
|
||||
this._hsUrl = hsUrl;
|
||||
@@ -29,6 +28,7 @@ export default class Login {
|
||||
this._currentFlowIndex = 0;
|
||||
this._flows = [];
|
||||
this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
||||
this._tempClient = null; // memoize
|
||||
}
|
||||
|
||||
getHomeserverUrl() {
|
||||
@@ -40,10 +40,12 @@ export default class Login {
|
||||
}
|
||||
|
||||
setHomeserverUrl(hsUrl) {
|
||||
this._tempClient = null; // clear memoization
|
||||
this._hsUrl = hsUrl;
|
||||
}
|
||||
|
||||
setIdentityServerUrl(isUrl) {
|
||||
this._tempClient = null; // clear memoization
|
||||
this._isUrl = isUrl;
|
||||
}
|
||||
|
||||
@@ -52,8 +54,9 @@ export default class Login {
|
||||
* requests.
|
||||
* @returns {MatrixClient}
|
||||
*/
|
||||
_createTemporaryClient() {
|
||||
return Matrix.createClient({
|
||||
createTemporaryClient() {
|
||||
if (this._tempClient) return this._tempClient; // use memoization
|
||||
return this._tempClient = Matrix.createClient({
|
||||
baseUrl: this._hsUrl,
|
||||
idBaseUrl: this._isUrl,
|
||||
});
|
||||
@@ -61,7 +64,7 @@ export default class Login {
|
||||
|
||||
getFlows() {
|
||||
const self = this;
|
||||
const client = this._createTemporaryClient();
|
||||
const client = this.createTemporaryClient();
|
||||
return client.loginFlows().then(function(result) {
|
||||
self._flows = result.flows;
|
||||
self._currentFlowIndex = 0;
|
||||
@@ -139,21 +142,6 @@ export default class Login {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
getSsoLoginUrl(loginType) {
|
||||
const client = this._createTemporaryClient();
|
||||
const parsedUrl = url.parse(window.location.href, true);
|
||||
|
||||
// XXX: at this point, the fragment will always be #/login, which is no
|
||||
// use to anyone. Ideally, we would get the intended fragment from
|
||||
// MatrixChat.screenAfterLogin so that you could follow #/room links etc
|
||||
// through an SSO login.
|
||||
parsedUrl.hash = "";
|
||||
|
||||
parsedUrl.query["homeserver"] = client.getHomeserverUrl();
|
||||
parsedUrl.query["identityServer"] = client.getIdentityServerUrl();
|
||||
return client.getSsoLoginUrl(url.format(parsedUrl), loginType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ export default class Markdown {
|
||||
// thus opening in a new tab.
|
||||
if (externalLinks) {
|
||||
attrs.push(['target', '_blank']);
|
||||
attrs.push(['rel', 'noopener']);
|
||||
attrs.push(['rel', 'noreferrer noopener']);
|
||||
}
|
||||
this.tag('a', attrs);
|
||||
} else {
|
||||
|
||||
@@ -39,6 +39,8 @@ export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/;
|
||||
* If true, goes to the home page if the user cancels the action
|
||||
* @param {bool} options.go_welcome_on_cancel
|
||||
* If true, goes to the welcome page if the user cancels the action
|
||||
* @param {bool} options.screen_after
|
||||
* If present the screen to redirect to after a successful login or register.
|
||||
*/
|
||||
export async function startAnyRegistrationFlow(options) {
|
||||
if (options === undefined) options = {};
|
||||
@@ -66,13 +68,21 @@ export async function startAnyRegistrationFlow(options) {
|
||||
// });
|
||||
//} else {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
|
||||
title: _t("Registration Required"),
|
||||
description: _t("You need to register to do this. Would you like to register now?"),
|
||||
button: _t("Register"),
|
||||
const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
|
||||
hasCancelButton: true,
|
||||
quitOnly: true,
|
||||
title: _t("Sign In or Create Account"),
|
||||
description: _t("Use your account or create a new one to continue."),
|
||||
button: _t("Create Account"),
|
||||
extraButtons: [
|
||||
<button key="start_login" onClick={() => {
|
||||
modal.close();
|
||||
dis.dispatch({action: 'start_login', screenAfterLogin: options.screen_after});
|
||||
}}>{ _t('Sign In') }</button>,
|
||||
],
|
||||
onFinished: (proceed) => {
|
||||
if (proceed) {
|
||||
dis.dispatch({action: 'start_registration'});
|
||||
dis.dispatch({action: 'start_registration', screenAfterLogin: options.screen_after});
|
||||
} else if (options.go_home_on_cancel) {
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
} else if (options.go_welcome_on_cancel) {
|
||||
@@ -101,4 +111,3 @@ export async function startAnyRegistrationFlow(options) {
|
||||
// }
|
||||
// throw new Error("Register request succeeded when it should have returned 401!");
|
||||
// }
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
* of aliases. Otherwise return null;
|
||||
*/
|
||||
export function getDisplayAliasForRoom(room) {
|
||||
return room.getCanonicalAlias() || room.getAliases()[0];
|
||||
return room.getCanonicalAlias() || room.getAltAliases()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -893,6 +893,26 @@ export const CommandMap = {
|
||||
},
|
||||
category: CommandCategories.advanced,
|
||||
}),
|
||||
|
||||
whois: new Command({
|
||||
name: "whois",
|
||||
description: _td("Displays information about a user"),
|
||||
args: '<user-id>',
|
||||
runFn: function(roomId, userId) {
|
||||
if (!userId || !userId.startsWith("@") || !userId.includes(":")) {
|
||||
return reject(this.getUsage());
|
||||
}
|
||||
|
||||
const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
member: member || {userId},
|
||||
});
|
||||
return success();
|
||||
},
|
||||
category: CommandCategories.advanced,
|
||||
}),
|
||||
};
|
||||
/* eslint-enable babel/no-invalid-this */
|
||||
|
||||
|
||||
@@ -127,6 +127,13 @@ function textForRoomNameEvent(ev) {
|
||||
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
|
||||
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
|
||||
}
|
||||
if (ev.getPrevContent().name) {
|
||||
return _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
|
||||
senderDisplayName,
|
||||
oldRoomName: ev.getPrevContent().name,
|
||||
newRoomName: ev.getContent().name,
|
||||
});
|
||||
}
|
||||
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
|
||||
senderDisplayName,
|
||||
roomName: ev.getContent().name,
|
||||
@@ -269,85 +276,55 @@ function textForMessageEvent(ev) {
|
||||
return message;
|
||||
}
|
||||
|
||||
function textForRoomAliasesEvent(ev) {
|
||||
// An alternative implementation of this as a first-class event can be found at
|
||||
// https://github.com/matrix-org/matrix-react-sdk/blob/dc7212ec2bd12e1917233ed7153b3e0ef529a135/src/components/views/messages/RoomAliasesEvent.js
|
||||
// This feels a bit overkill though, and it's not clear the i18n really needs it
|
||||
// so instead it's landing as a simple textual event.
|
||||
|
||||
const maxShown = 3;
|
||||
|
||||
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const oldAliases = ev.getPrevContent().aliases || [];
|
||||
const newAliases = ev.getContent().aliases || [];
|
||||
|
||||
const addedAliases = newAliases.filter((x) => !oldAliases.includes(x));
|
||||
const removedAliases = oldAliases.filter((x) => !newAliases.includes(x));
|
||||
|
||||
if (!addedAliases.length && !removedAliases.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (addedAliases.length && !removedAliases.length) {
|
||||
if (addedAliases.length > maxShown) {
|
||||
return _t("%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room", {
|
||||
senderName: senderName,
|
||||
count: addedAliases.length - maxShown,
|
||||
addedAddresses: addedAliases.slice(0, maxShown).join(', '),
|
||||
});
|
||||
}
|
||||
return _t('%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.', {
|
||||
senderName: senderName,
|
||||
count: addedAliases.length,
|
||||
addedAddresses: addedAliases.join(', '),
|
||||
});
|
||||
} else if (!addedAliases.length && removedAliases.length) {
|
||||
if (removedAliases.length > maxShown) {
|
||||
return _t("%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room", {
|
||||
senderName: senderName,
|
||||
count: removedAliases.length - maxShown,
|
||||
removedAddresses: removedAliases.slice(0, maxShown).join(', '),
|
||||
});
|
||||
}
|
||||
return _t('%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.', {
|
||||
senderName: senderName,
|
||||
count: removedAliases.length,
|
||||
removedAddresses: removedAliases.join(', '),
|
||||
});
|
||||
} else {
|
||||
const combined = addedAliases.length + removedAliases.length;
|
||||
if (combined > maxShown) {
|
||||
return _t("%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room", {
|
||||
senderName: senderName,
|
||||
countAdded: addedAliases.length,
|
||||
countRemoved: removedAliases.length,
|
||||
});
|
||||
}
|
||||
return _t(
|
||||
'%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.', {
|
||||
senderName: senderName,
|
||||
addedAddresses: addedAliases.join(', '),
|
||||
removedAddresses: removedAliases.join(', '),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function textForCanonicalAliasEvent(ev) {
|
||||
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const oldAlias = ev.getPrevContent().alias;
|
||||
const oldAltAliases = ev.getPrevContent().alt_aliases || [];
|
||||
const newAlias = ev.getContent().alias;
|
||||
const newAltAliases = ev.getContent().alt_aliases || [];
|
||||
const removedAltAliases = oldAltAliases.filter(alias => !newAltAliases.includes(alias));
|
||||
const addedAltAliases = newAltAliases.filter(alias => !oldAltAliases.includes(alias));
|
||||
|
||||
if (newAlias) {
|
||||
return _t('%(senderName)s set the main address for this room to %(address)s.', {
|
||||
senderName: senderName,
|
||||
address: ev.getContent().alias,
|
||||
});
|
||||
} else if (oldAlias) {
|
||||
return _t('%(senderName)s removed the main address for this room.', {
|
||||
if (!removedAltAliases.length && !addedAltAliases.length) {
|
||||
if (newAlias) {
|
||||
return _t('%(senderName)s set the main address for this room to %(address)s.', {
|
||||
senderName: senderName,
|
||||
address: ev.getContent().alias,
|
||||
});
|
||||
} else if (oldAlias) {
|
||||
return _t('%(senderName)s removed the main address for this room.', {
|
||||
senderName: senderName,
|
||||
});
|
||||
}
|
||||
} else if (newAlias === oldAlias) {
|
||||
if (addedAltAliases.length && !removedAltAliases.length) {
|
||||
return _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
|
||||
senderName: senderName,
|
||||
addresses: addedAltAliases.join(", "),
|
||||
count: addedAltAliases.length,
|
||||
});
|
||||
} if (removedAltAliases.length && !addedAltAliases.length) {
|
||||
return _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
|
||||
senderName: senderName,
|
||||
addresses: removedAltAliases.join(", "),
|
||||
count: removedAltAliases.length,
|
||||
});
|
||||
} if (removedAltAliases.length && addedAltAliases.length) {
|
||||
return _t('%(senderName)s changed the alternative addresses for this room.', {
|
||||
senderName: senderName,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// both alias and alt_aliases where modified
|
||||
return _t('%(senderName)s changed the main and alternative addresses for this room.', {
|
||||
senderName: senderName,
|
||||
});
|
||||
}
|
||||
// in case there is no difference between the two events,
|
||||
// say something as we can't simply hide the tile from here
|
||||
return _t('%(senderName)s changed the addresses for this room.', {
|
||||
senderName: senderName,
|
||||
});
|
||||
}
|
||||
|
||||
function textForCallAnswerEvent(event) {
|
||||
@@ -612,7 +589,6 @@ const handlers = {
|
||||
};
|
||||
|
||||
const stateHandlers = {
|
||||
'm.room.aliases': textForRoomAliasesEvent,
|
||||
'm.room.canonical_alias': textForCanonicalAliasEvent,
|
||||
'm.room.name': textForRoomNameEvent,
|
||||
'm.room.topic': textForTopicEvent,
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { asyncAction } from './actionCreators';
|
||||
import RoomListStore from '../stores/RoomListStore';
|
||||
import RoomListStore, {TAG_DM} from '../stores/RoomListStore';
|
||||
import Modal from '../Modal';
|
||||
import * as Rooms from '../Rooms';
|
||||
import { _t } from '../languageHandler';
|
||||
@@ -73,11 +73,11 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
|
||||
const roomId = room.roomId;
|
||||
|
||||
// Evil hack to get DMs behaving
|
||||
if ((oldTag === undefined && newTag === 'im.vector.fake.direct') ||
|
||||
(oldTag === 'im.vector.fake.direct' && newTag === undefined)
|
||||
if ((oldTag === undefined && newTag === TAG_DM) ||
|
||||
(oldTag === TAG_DM && newTag === undefined)
|
||||
) {
|
||||
return Rooms.guessAndSetDMRoom(
|
||||
room, newTag === 'im.vector.fake.direct',
|
||||
room, newTag === TAG_DM,
|
||||
).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to set direct chat tag " + err);
|
||||
@@ -91,10 +91,10 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
|
||||
const hasChangedSubLists = oldTag !== newTag;
|
||||
|
||||
// More evilness: We will still be dealing with moving to favourites/low prio,
|
||||
// but we avoid ever doing a request with 'im.vector.fake.direct`.
|
||||
// but we avoid ever doing a request with TAG_DM.
|
||||
//
|
||||
// if we moved lists, remove the old tag
|
||||
if (oldTag && oldTag !== 'im.vector.fake.direct' &&
|
||||
if (oldTag && oldTag !== TAG_DM &&
|
||||
hasChangedSubLists
|
||||
) {
|
||||
const promiseToDelete = matrixClient.deleteRoomTag(
|
||||
@@ -112,7 +112,7 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
|
||||
}
|
||||
|
||||
// if we moved lists or the ordering changed, add the new tag
|
||||
if (newTag && newTag !== 'im.vector.fake.direct' &&
|
||||
if (newTag && newTag !== TAG_DM &&
|
||||
(hasChangedSubLists || metaData)
|
||||
) {
|
||||
// metaData is the body of the PUT to set the tag, so it must
|
||||
|
||||
@@ -141,15 +141,17 @@ export default class ManageEventIndexDialog extends React.Component {
|
||||
let crawlerState;
|
||||
|
||||
if (this.state.currentRoom === null) {
|
||||
crawlerState = _t("Not currently downloading messages for any room.");
|
||||
crawlerState = _t("Not currently indexing messages for any room.");
|
||||
} else {
|
||||
crawlerState = (
|
||||
_t("Downloading mesages for %(currentRoom)s.", { currentRoom: this.state.currentRoom })
|
||||
_t("Currently indexing: %(currentRoom)s.", { currentRoom: this.state.currentRoom })
|
||||
);
|
||||
}
|
||||
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
|
||||
const doneRooms = Math.max(0, (this.state.roomCount - this.state.crawlingRoomsCount));
|
||||
|
||||
const eventIndexingSettings = (
|
||||
<div>
|
||||
{
|
||||
@@ -158,13 +160,13 @@ export default class ManageEventIndexDialog extends React.Component {
|
||||
)
|
||||
}
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{crawlerState}<br />
|
||||
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}<br />
|
||||
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}<br />
|
||||
{_t("Indexed rooms:")} {_t("%(crawlingRooms)s out of %(totalRooms)s", {
|
||||
crawlingRooms: formatCountLong(this.state.crawlingRoomsCount),
|
||||
{_t("Indexed rooms:")} {_t("%(doneRooms)s out of %(totalRooms)s", {
|
||||
doneRooms: formatCountLong(doneRooms),
|
||||
totalRooms: formatCountLong(this.state.roomCount),
|
||||
})} <br />
|
||||
{crawlerState}<br />
|
||||
<Field
|
||||
id={"crawlerSleepTimeMs"}
|
||||
label={_t('Message downloading sleep time(ms)')}
|
||||
|
||||
@@ -53,7 +53,6 @@ function selectText(target) {
|
||||
*/
|
||||
export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
secureSecretStorage: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
@@ -65,7 +64,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
this._setZxcvbnResultTimeout = null;
|
||||
|
||||
this.state = {
|
||||
secureSecretStorage: props.secureSecretStorage,
|
||||
secureSecretStorage: null,
|
||||
phase: PHASE_PASSPHRASE,
|
||||
passPhrase: '',
|
||||
passPhraseConfirm: '',
|
||||
@@ -73,23 +72,20 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
downloaded: false,
|
||||
zxcvbnResult: null,
|
||||
};
|
||||
|
||||
if (this.state.secureSecretStorage === undefined) {
|
||||
this.state.secureSecretStorage =
|
||||
SettingsStore.isFeatureEnabled("feature_cross_signing");
|
||||
}
|
||||
|
||||
// If we're using secret storage, skip ahead to the backing up step, as
|
||||
// `accessSecretStorage` will handle passphrases as needed.
|
||||
if (this.state.secureSecretStorage) {
|
||||
this.state.phase = PHASE_BACKINGUP;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
async componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const secureSecretStorage = (
|
||||
SettingsStore.isFeatureEnabled("feature_cross_signing") &&
|
||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
|
||||
);
|
||||
this.setState({ secureSecretStorage });
|
||||
|
||||
// If we're using secret storage, skip ahead to the backing up step, as
|
||||
// `accessSecretStorage` will handle passphrases as needed.
|
||||
if (this.state.secureSecretStorage) {
|
||||
if (secureSecretStorage) {
|
||||
this.setState({ phase: PHASE_BACKINGUP });
|
||||
this._createBackup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
// does the server offer a UI auth flow with just m.login.password
|
||||
// for /keys/device_signing/upload?
|
||||
canUploadKeysWithPasswordOnly: null,
|
||||
accountPassword: props.accountPassword,
|
||||
accountPassword: props.accountPassword || "",
|
||||
accountPasswordCorrect: null,
|
||||
// status of the key backup toggle switch
|
||||
useKeyBackup: true,
|
||||
@@ -117,6 +117,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
backupInfo,
|
||||
backupSigStatus,
|
||||
});
|
||||
|
||||
return {
|
||||
backupInfo,
|
||||
backupSigStatus,
|
||||
};
|
||||
}
|
||||
|
||||
async _queryKeyUploadAuth() {
|
||||
@@ -269,13 +274,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, {showSummary: false}, null,
|
||||
/* priority = */ false, /* static = */ true,
|
||||
/* priority = */ false, /* static = */ false,
|
||||
);
|
||||
|
||||
await finished;
|
||||
await this._fetchBackupInfo();
|
||||
const { backupSigStatus } = await this._fetchBackupInfo();
|
||||
if (
|
||||
this.state.backupSigStatus.usable &&
|
||||
backupSigStatus.usable &&
|
||||
this.state.canUploadKeysWithPasswordOnly &&
|
||||
this.state.accountPassword
|
||||
) {
|
||||
@@ -400,12 +405,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
|
||||
let authPrompt;
|
||||
let nextCaption = _t("Next");
|
||||
if (!this.state.backupSigStatus.usable) {
|
||||
authPrompt = <div>
|
||||
<div>{_t("Restore your key backup to upgrade your encryption")}</div>
|
||||
</div>;
|
||||
nextCaption = _t("Restore");
|
||||
} else if (this.state.canUploadKeysWithPasswordOnly) {
|
||||
if (this.state.canUploadKeysWithPasswordOnly) {
|
||||
authPrompt = <div>
|
||||
<div>{_t("Enter your account password to confirm the upgrade:")}</div>
|
||||
<div><Field
|
||||
@@ -418,6 +418,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
autoFocus={true}
|
||||
/></div>
|
||||
</div>;
|
||||
} else if (!this.state.backupSigStatus.usable) {
|
||||
authPrompt = <div>
|
||||
<div>{_t("Restore your key backup to upgrade your encryption")}</div>
|
||||
</div>;
|
||||
nextCaption = _t("Restore");
|
||||
} else {
|
||||
authPrompt = <p>
|
||||
{_t("You'll need to authenticate with the server to confirm the upgrade.")}
|
||||
@@ -433,6 +438,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
<div>{authPrompt}</div>
|
||||
<DialogButtons
|
||||
primaryButton={nextCaption}
|
||||
onPrimaryButtonClick={this._onMigrateFormSubmit}
|
||||
hasCancel={false}
|
||||
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
|
||||
>
|
||||
|
||||
@@ -23,7 +23,6 @@ import AutocompleteProvider from './AutocompleteProvider';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import QueryMatcher from './QueryMatcher';
|
||||
import {PillCompletion} from './Components';
|
||||
import {getDisplayAliasForRoom} from '../Rooms';
|
||||
import * as sdk from '../index';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
||||
@@ -40,11 +39,19 @@ function score(query, space) {
|
||||
}
|
||||
}
|
||||
|
||||
function matcherObject(room, displayedAlias, matchName = "") {
|
||||
return {
|
||||
room,
|
||||
matchName,
|
||||
displayedAlias,
|
||||
};
|
||||
}
|
||||
|
||||
export default class RoomProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(ROOM_REGEX);
|
||||
this.matcher = new QueryMatcher([], {
|
||||
keys: ['displayedAlias', 'name'],
|
||||
keys: ['displayedAlias', 'matchName'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,16 +63,16 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||
if (command) {
|
||||
// the only reason we need to do this is because Fuse only matches on properties
|
||||
let matcherObjects = client.getVisibleRooms().filter(
|
||||
(room) => !!room && !!getDisplayAliasForRoom(room),
|
||||
).map((room) => {
|
||||
return {
|
||||
room: room,
|
||||
name: room.name,
|
||||
displayedAlias: getDisplayAliasForRoom(room),
|
||||
};
|
||||
});
|
||||
|
||||
let matcherObjects = client.getVisibleRooms().reduce((aliases, room) => {
|
||||
if (room.getCanonicalAlias()) {
|
||||
aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name));
|
||||
}
|
||||
if (room.getAltAliases().length) {
|
||||
const altAliases = room.getAltAliases().map(alias => matcherObject(room, alias));
|
||||
aliases = aliases.concat(altAliases);
|
||||
}
|
||||
return aliases;
|
||||
}, []);
|
||||
// Filter out any matches where the user will have also autocompleted new rooms
|
||||
matcherObjects = matcherObjects.filter((r) => {
|
||||
const tombstone = r.room.currentState.getStateEvents("m.room.tombstone", "");
|
||||
@@ -84,16 +91,16 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||
completions = _sortBy(completions, [
|
||||
(c) => score(matchedString, c.displayedAlias),
|
||||
(c) => c.displayedAlias.length,
|
||||
]).map((room) => {
|
||||
const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
|
||||
]);
|
||||
completions = completions.map((room) => {
|
||||
return {
|
||||
completion: displayAlias,
|
||||
completionId: displayAlias,
|
||||
completion: room.displayedAlias,
|
||||
completionId: room.room.roomId,
|
||||
type: "room",
|
||||
suffix: ' ',
|
||||
href: makeRoomPermalink(displayAlias),
|
||||
href: makeRoomPermalink(room.displayedAlias),
|
||||
component: (
|
||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.room.name} description={room.displayedAlias} />
|
||||
),
|
||||
range,
|
||||
};
|
||||
|
||||
@@ -255,7 +255,7 @@ export class ContextMenu extends React.Component {
|
||||
|
||||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
||||
chevronOffset.left = props.chevronOffset;
|
||||
} else {
|
||||
} else if (position.top !== undefined) {
|
||||
const target = position.top;
|
||||
|
||||
// By default, no adjustment is made
|
||||
|
||||
@@ -481,7 +481,7 @@ export default createReactClass({
|
||||
group_id: groupId,
|
||||
},
|
||||
});
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${groupId}`}});
|
||||
willDoOnboarding = true;
|
||||
}
|
||||
if (stateKey === GroupStore.STATE_KEY.Summary) {
|
||||
@@ -726,7 +726,7 @@ export default createReactClass({
|
||||
|
||||
_onJoinClick: async function() {
|
||||
if (this._matrixClient.isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${this.props.groupId}`}});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -821,10 +821,10 @@ export default createReactClass({
|
||||
{_t(
|
||||
"Want more than a community? <a>Get your own server</a>", {},
|
||||
{
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>,
|
||||
},
|
||||
)}
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener">
|
||||
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
|
||||
<img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||
</a>
|
||||
</div>;
|
||||
|
||||
@@ -161,6 +161,7 @@ export default createReactClass({
|
||||
_authStateUpdated: function(stageType, stageState) {
|
||||
const oldStage = this.state.authStage;
|
||||
this.setState({
|
||||
busy: false,
|
||||
authStage: stageType,
|
||||
stageState: stageState,
|
||||
errorText: stageState.error,
|
||||
@@ -184,11 +185,13 @@ export default createReactClass({
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
// The JS SDK eagerly reports itself as "not busy" right after any
|
||||
// immediate work has completed, but that's not really what we want at
|
||||
// the UI layer, so we ignore this signal and show a spinner until
|
||||
// there's a new screen to show the user. This is implemented by setting
|
||||
// `busy: false` in `_authStateUpdated`.
|
||||
// See also https://github.com/vector-im/riot-web/issues/12546
|
||||
},
|
||||
|
||||
_setFocus: function() {
|
||||
|
||||
@@ -337,13 +337,13 @@ const LoggedInView = createReactClass({
|
||||
|
||||
let handled = false;
|
||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey ||
|
||||
ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
|
||||
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey;
|
||||
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.PAGE_UP:
|
||||
case Key.PAGE_DOWN:
|
||||
if (!hasModifier) {
|
||||
if (!hasModifier && !isModifier) {
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
}
|
||||
@@ -384,7 +384,10 @@ const LoggedInView = createReactClass({
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
} else if (!hasModifier) {
|
||||
} else if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
// The above condition is crafted to _allow_ characters with Shift
|
||||
// already pressed (but not the Shift key down itself).
|
||||
|
||||
const isClickShortcut = ev.target !== document.body &&
|
||||
(ev.key === Key.SPACE || ev.key === Key.ENTER);
|
||||
|
||||
@@ -585,7 +588,8 @@ const LoggedInView = createReactClass({
|
||||
limitType={usageLimitEvent.getContent().limit_type}
|
||||
/>;
|
||||
} else if (this.props.showCookieBar &&
|
||||
this.props.config.piwik
|
||||
this.props.config.piwik &&
|
||||
navigator.doNotTrack !== "1"
|
||||
) {
|
||||
const policyUrl = this.props.config.piwik.policyUrl || null;
|
||||
topBar = <CookieBar policyUrl={policyUrl} />;
|
||||
|
||||
@@ -93,14 +93,19 @@ export default class MainSplit extends React.Component {
|
||||
const bodyView = React.Children.only(this.props.children);
|
||||
const panelView = this.props.panel;
|
||||
|
||||
if (this.props.collapsedRhs || !panelView) {
|
||||
return bodyView;
|
||||
} else {
|
||||
return <div className="mx_MainSplit" ref={this._setResizeContainerRef}>
|
||||
{ bodyView }
|
||||
const hasResizer = !this.props.collapsedRhs && panelView;
|
||||
|
||||
let children;
|
||||
if (hasResizer) {
|
||||
children = <React.Fragment>
|
||||
<ResizeHandle reverse={true} />
|
||||
{ panelView }
|
||||
</div>;
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <div className="mx_MainSplit" ref={hasResizer ? this._setResizeContainerRef : undefined}>
|
||||
{ bodyView }
|
||||
{ children }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,13 +559,19 @@ export default createReactClass({
|
||||
case 'view_user_info':
|
||||
this._viewUser(payload.userId, payload.subAction);
|
||||
break;
|
||||
case 'view_room':
|
||||
case 'view_room': {
|
||||
// Takes either a room ID or room alias: if switching to a room the client is already
|
||||
// known to be in (eg. user clicks on a room in the recents panel), supply the ID
|
||||
// If the user is clicking on a room in the context of the alias being presented
|
||||
// to them, supply the room alias. If both are supplied, the room ID will be ignored.
|
||||
this._viewRoom(payload);
|
||||
const promise = this._viewRoom(payload);
|
||||
if (payload.deferred_action) {
|
||||
promise.then(() => {
|
||||
dis.dispatch(payload.deferred_action);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'view_prev_room':
|
||||
this._viewNextRoom(-1);
|
||||
break;
|
||||
@@ -862,7 +868,7 @@ export default createReactClass({
|
||||
waitFor = this.firstSyncPromise.promise;
|
||||
}
|
||||
|
||||
waitFor.then(() => {
|
||||
return waitFor.then(() => {
|
||||
let presentedId = roomInfo.room_alias || roomInfo.room_id;
|
||||
const room = MatrixClientPeg.get().getRoom(roomInfo.room_id);
|
||||
if (room) {
|
||||
@@ -885,7 +891,7 @@ export default createReactClass({
|
||||
presentedId += "/" + roomInfo.event_id;
|
||||
}
|
||||
newState.ready = true;
|
||||
this.setState(newState, ()=>{
|
||||
this.setState(newState, () => {
|
||||
this.notifyNewScreen('room/' + presentedId);
|
||||
});
|
||||
});
|
||||
@@ -1008,6 +1014,10 @@ export default createReactClass({
|
||||
// needs to be reset so that they can revisit /user/.. // (and trigger
|
||||
// `_chatCreateOrReuse` again)
|
||||
go_welcome_on_cancel: true,
|
||||
screen_after: {
|
||||
screen: `user/${this.props.config.welcomeUserId}`,
|
||||
params: { action: 'chat' },
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1175,8 +1185,17 @@ export default createReactClass({
|
||||
* Called when a new logged in session has started
|
||||
*/
|
||||
_onLoggedIn: async function() {
|
||||
ThemeController.isLogin = false;
|
||||
this.setStateForNewView({ view: VIEWS.LOGGED_IN });
|
||||
if (MatrixClientPeg.currentUserIsJustRegistered()) {
|
||||
// If a specific screen is set to be shown after login, show that above
|
||||
// all else, as it probably means the user clicked on something already.
|
||||
if (this._screenAfterLogin && this._screenAfterLogin.screen) {
|
||||
this.showScreen(
|
||||
this._screenAfterLogin.screen,
|
||||
this._screenAfterLogin.params,
|
||||
);
|
||||
this._screenAfterLogin = null;
|
||||
} else if (MatrixClientPeg.currentUserIsJustRegistered()) {
|
||||
MatrixClientPeg.setJustRegisteredUserId(null);
|
||||
|
||||
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
||||
@@ -1374,7 +1393,8 @@ export default createReactClass({
|
||||
cancelButton: _t('Dismiss'),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
window.open(consentUri, '_blank');
|
||||
const wnd = window.open(consentUri, '_blank');
|
||||
wnd.opener = null;
|
||||
}
|
||||
},
|
||||
}, null, true);
|
||||
@@ -1475,26 +1495,29 @@ export default createReactClass({
|
||||
}
|
||||
});
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
cli.on("crypto.verification.request", request => {
|
||||
if (request.pending) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: 'verifreq_' + request.channel.transactionId,
|
||||
title: _t("Verification Request"),
|
||||
icon: "verification",
|
||||
props: {request},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cli.on("crypto.verification.start", (verifier) => {
|
||||
cli.on("crypto.verification.request", request => {
|
||||
const isFlagOn = SettingsStore.isFeatureEnabled("feature_cross_signing");
|
||||
|
||||
if (!isFlagOn && !request.channel.deviceId) {
|
||||
request.cancel({code: "m.invalid_message", reason: "This client has cross-signing disabled"});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.verifier) {
|
||||
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
|
||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
||||
verifier,
|
||||
verifier: request.verifier,
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
});
|
||||
}
|
||||
} else if (request.pending) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: 'verifreq_' + request.channel.transactionId,
|
||||
title: _t("Verification Request"),
|
||||
icon: "verification",
|
||||
props: {request},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
});
|
||||
}
|
||||
});
|
||||
// Fire the tinter right on startup to ensure the default theme is applied
|
||||
// A later sync can/will correct the tint to be the right value for the user
|
||||
const colorScheme = SettingsStore.getValue("roomColor");
|
||||
@@ -1888,7 +1911,10 @@ export default createReactClass({
|
||||
// secret storage.
|
||||
SettingsStore.setFeatureEnabled("feature_cross_signing", true);
|
||||
this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY });
|
||||
} else if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
} else if (
|
||||
SettingsStore.isFeatureEnabled("feature_cross_signing") &&
|
||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
|
||||
) {
|
||||
// This will only work if the feature is set to 'enable' in the config,
|
||||
// since it's too early in the lifecycle for users to have turned the
|
||||
// labs flag on.
|
||||
|
||||
@@ -28,6 +28,7 @@ import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import {_t} from "../../languageHandler";
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import {textForEvent} from "../../TextForEvent";
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
@@ -873,6 +874,11 @@ class CreationGrouper {
|
||||
}
|
||||
|
||||
getTiles() {
|
||||
// If we don't have any events to group, don't even try to group them. The logic
|
||||
// below assumes that we have a group of events to deal with, but we might not if
|
||||
// the events we were supposed to group were redacted.
|
||||
if (!this.events || !this.events.length) return [];
|
||||
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
||||
|
||||
@@ -950,15 +956,30 @@ class MemberGrouper {
|
||||
}
|
||||
|
||||
shouldGroup(ev) {
|
||||
if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
|
||||
return false;
|
||||
}
|
||||
return isMembershipChange(ev);
|
||||
}
|
||||
|
||||
add(ev) {
|
||||
if (ev.getType() === 'm.room.member') {
|
||||
// We'll just double check that it's worth our time to do so, through an
|
||||
// ugly hack. If textForEvent returns something, we should group it for
|
||||
// rendering but if it doesn't then we'll exclude it.
|
||||
const renderText = textForEvent(ev);
|
||||
if (!renderText || renderText.trim().length === 0) return; // quietly ignore
|
||||
}
|
||||
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(ev.getId());
|
||||
this.events.push(ev);
|
||||
}
|
||||
|
||||
getTiles() {
|
||||
// If we don't have any events to group, don't even try to group them. The logic
|
||||
// below assumes that we have a group of events to deal with, but we might not if
|
||||
// the events we were supposed to group were redacted.
|
||||
if (!this.events || !this.events.length) return [];
|
||||
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||
|
||||
|
||||
@@ -182,6 +182,7 @@ export default class RightPanel extends React.Component {
|
||||
member: payload.member,
|
||||
event: payload.event,
|
||||
verificationRequest: payload.verificationRequest,
|
||||
verificationRequestPromise: payload.verificationRequestPromise,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -231,6 +232,7 @@ export default class RightPanel extends React.Component {
|
||||
onClose={onClose}
|
||||
phase={this.state.phase}
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
verificationRequestPromise={this.state.verificationRequestPromise}
|
||||
/>;
|
||||
} else {
|
||||
panel = <MemberInfo
|
||||
|
||||
@@ -406,6 +406,12 @@ export default createReactClass({
|
||||
// would normally decide what the name is.
|
||||
name: room.name || room_alias || _t('Unnamed room'),
|
||||
};
|
||||
|
||||
if (this.state.roomServer) {
|
||||
payload.opts = {
|
||||
viaServers: [this.state.roomServer],
|
||||
};
|
||||
}
|
||||
}
|
||||
// It's not really possible to join Matrix rooms by ID because the HS has no way to know
|
||||
// which servers to start querying. However, there's no other way to join rooms in
|
||||
|
||||
@@ -46,8 +46,6 @@ export default class RoomSubList extends React.PureComponent {
|
||||
tagName: PropTypes.string,
|
||||
addRoomLabel: PropTypes.string,
|
||||
|
||||
order: PropTypes.string.isRequired,
|
||||
|
||||
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
|
||||
isInvite: PropTypes.bool,
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@ export default createReactClass({
|
||||
peekLoading: false,
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}, (err) => {
|
||||
}).catch((err) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
@@ -355,7 +355,7 @@ export default createReactClass({
|
||||
// This won't necessarily be a MatrixError, but we duck-type
|
||||
// here and say if it's got an 'errcode' key with the right value,
|
||||
// it means we can't peek.
|
||||
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
|
||||
if (err.errcode === "M_GUEST_ACCESS_FORBIDDEN" || err.errcode === 'M_FORBIDDEN') {
|
||||
// This is fine: the room just isn't peekable (we assume).
|
||||
this.setState({
|
||||
peekLoading: false,
|
||||
@@ -365,8 +365,6 @@ export default createReactClass({
|
||||
}
|
||||
});
|
||||
} else if (room) {
|
||||
//viewing a previously joined room, try to lazy load members
|
||||
|
||||
// Stop peeking because we have joined this room previously
|
||||
MatrixClientPeg.get().stopPeeking();
|
||||
this.setState({isPeeking: false});
|
||||
@@ -460,8 +458,6 @@ export default createReactClass({
|
||||
// (We could use isMounted, but facebook have deprecated that.)
|
||||
this.unmounted = true;
|
||||
|
||||
SettingsStore.unwatchSetting(this._ciderWatcherRef);
|
||||
|
||||
// update the scroll map before we get unmounted
|
||||
if (this.state.roomId) {
|
||||
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
|
||||
@@ -618,6 +614,22 @@ export default createReactClass({
|
||||
this.onCancelSearchClick();
|
||||
}
|
||||
break;
|
||||
case 'quote':
|
||||
if (this.state.searchResults) {
|
||||
const roomId = payload.event.getRoomId();
|
||||
if (roomId === this.state.roomId) {
|
||||
this.onCancelSearchClick();
|
||||
}
|
||||
|
||||
setImmediate(() => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
deferred_action: payload,
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -523,7 +523,7 @@ export default createReactClass({
|
||||
scrollRelative: function(mult) {
|
||||
const scrollNode = this._getScrollNode();
|
||||
const delta = mult * scrollNode.clientHeight * 0.5;
|
||||
scrollNode.scrollTop = scrollNode.scrollTop + delta;
|
||||
scrollNode.scrollBy(0, delta);
|
||||
this._saveScrollState();
|
||||
},
|
||||
|
||||
@@ -705,17 +705,15 @@ export default createReactClass({
|
||||
// the currently filled piece of the timeline
|
||||
if (trackedNode) {
|
||||
const oldTop = trackedNode.offsetTop;
|
||||
// changing the height might change the scrollTop
|
||||
// if the new height is smaller than the scrollTop.
|
||||
// We calculate the diff that needs to be applied
|
||||
// ourselves, so be sure to measure the
|
||||
// scrollTop before changing the height.
|
||||
const preexistingScrollTop = sn.scrollTop;
|
||||
itemlist.style.height = `${newHeight}px`;
|
||||
const newTop = trackedNode.offsetTop;
|
||||
const topDiff = newTop - oldTop;
|
||||
sn.scrollTop = preexistingScrollTop + topDiff;
|
||||
debuglog("updateHeight to", {newHeight, topDiff, preexistingScrollTop});
|
||||
// important to scroll by a relative amount as
|
||||
// reading scrollTop and then setting it might
|
||||
// yield out of date values and cause a jump
|
||||
// when setting it
|
||||
sn.scrollBy(0, topDiff);
|
||||
debuglog("updateHeight to", {newHeight, topDiff});
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -767,6 +765,7 @@ export default createReactClass({
|
||||
},
|
||||
|
||||
_topFromBottom(node) {
|
||||
// current capped height - distance from top = distance from bottom of container to top of tracked element
|
||||
return this._itemlist.current.clientHeight - node.offsetTop;
|
||||
},
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 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");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -18,41 +18,54 @@ limitations under the License.
|
||||
|
||||
import * as React from "react";
|
||||
import {_t} from '../../languageHandler';
|
||||
import PropTypes from "prop-types";
|
||||
import * as PropTypes from "prop-types";
|
||||
import * as sdk from "../../index";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Represents a tab for the TabbedView.
|
||||
*/
|
||||
export class Tab {
|
||||
public label: string;
|
||||
public icon: string;
|
||||
public body: React.ReactNode;
|
||||
|
||||
/**
|
||||
* Creates a new tab.
|
||||
* @param {string} tabLabel The untranslated tab label.
|
||||
* @param {string} tabIconClass The class for the tab icon. This should be a simple mask.
|
||||
* @param {string} tabJsx The JSX for the tab container.
|
||||
* @param {React.ReactNode} tabJsx The JSX for the tab container.
|
||||
*/
|
||||
constructor(tabLabel, tabIconClass, tabJsx) {
|
||||
constructor(tabLabel: string, tabIconClass: string, tabJsx: React.ReactNode) {
|
||||
this.label = tabLabel;
|
||||
this.icon = tabIconClass;
|
||||
this.body = tabJsx;
|
||||
}
|
||||
}
|
||||
|
||||
export default class TabbedView extends React.Component {
|
||||
interface IProps {
|
||||
tabs: Tab[];
|
||||
}
|
||||
|
||||
interface IState {
|
||||
activeTabIndex: number;
|
||||
}
|
||||
|
||||
export default class TabbedView extends React.Component<IProps, IState> {
|
||||
static propTypes = {
|
||||
// The tabs to show
|
||||
tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeTabIndex: 0,
|
||||
};
|
||||
}
|
||||
|
||||
_getActiveTabIndex() {
|
||||
private _getActiveTabIndex() {
|
||||
if (!this.state || !this.state.activeTabIndex) return 0;
|
||||
return this.state.activeTabIndex;
|
||||
}
|
||||
@@ -62,7 +75,7 @@ export default class TabbedView extends React.Component {
|
||||
* @param {Tab} tab the tab to show
|
||||
* @private
|
||||
*/
|
||||
_setActiveTab(tab) {
|
||||
private _setActiveTab(tab: Tab) {
|
||||
const idx = this.props.tabs.indexOf(tab);
|
||||
if (idx !== -1) {
|
||||
this.setState({activeTabIndex: idx});
|
||||
@@ -71,7 +84,7 @@ export default class TabbedView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_renderTabLabel(tab) {
|
||||
private _renderTabLabel(tab: Tab) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let classes = "mx_TabbedView_tabLabel ";
|
||||
@@ -97,7 +110,7 @@ export default class TabbedView extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
_renderTabPanel(tab) {
|
||||
private _renderTabPanel(tab: Tab): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
|
||||
<div className='mx_TabbedView_tabPanelContent'>
|
||||
@@ -107,7 +120,7 @@ export default class TabbedView extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): React.ReactNode {
|
||||
const labels = this.props.tabs.map(tab => this._renderTabLabel(tab));
|
||||
const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]);
|
||||
|
||||
@@ -54,17 +54,39 @@ export default class CompleteSecurity extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onStartClick = async () => {
|
||||
_onUsePassphraseClick = async () => {
|
||||
this.setState({
|
||||
phase: PHASE_BUSY,
|
||||
});
|
||||
const cli = MatrixClientPeg.get();
|
||||
const backupInfo = await cli.getKeyBackupVersion();
|
||||
this.setState({backupInfo});
|
||||
try {
|
||||
await accessSecretStorage(async () => {
|
||||
await cli.checkOwnCrossSigningTrust();
|
||||
if (backupInfo) await cli.restoreKeyBackupWithSecretStorage(backupInfo);
|
||||
const backupInfo = await cli.getKeyBackupVersion();
|
||||
this.setState({backupInfo});
|
||||
|
||||
// The control flow is fairly twisted here...
|
||||
// For the purposes of completing security, we only wait on getting
|
||||
// as far as the trust check and then show a green shield.
|
||||
// We also begin the key backup restore as well, which we're
|
||||
// awaiting inside `accessSecretStorage` only so that it keeps your
|
||||
// passphase cached for that work. This dialog itself will only wait
|
||||
// on the first trust check, and the key backup restore will happen
|
||||
// in the background.
|
||||
await new Promise((resolve, reject) => {
|
||||
try {
|
||||
accessSecretStorage(async () => {
|
||||
await cli.checkOwnCrossSigningTrust();
|
||||
resolve();
|
||||
if (backupInfo) {
|
||||
// A complete restore can take many minutes for large
|
||||
// accounts / slow servers, so we allow the dialog
|
||||
// to advance before this.
|
||||
await cli.restoreKeyBackupWithSecretStorage(backupInfo);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
if (cli.getCrossSigningId()) {
|
||||
@@ -147,13 +169,27 @@ export default class CompleteSecurity extends React.Component {
|
||||
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
|
||||
/>;
|
||||
} else if (phase === PHASE_INTRO) {
|
||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||
title = _t("Complete security");
|
||||
body = (
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Verify this session to grant it access to encrypted messages.",
|
||||
"Open an existing session & use it to verify this one, " +
|
||||
"granting it access to encrypted messages.",
|
||||
)}</p>
|
||||
<p className="mx_CompleteSecurity_waiting"><InlineSpinner />{_t("Waiting…")}</p>
|
||||
<p>{_t(
|
||||
"If you can’t access one, <button>use your recovery key or passphrase.</button>",
|
||||
{}, {
|
||||
button: sub => <AccessibleButton element="span"
|
||||
className="mx_linkButton"
|
||||
onClick={this._onUsePassphraseClick}
|
||||
>
|
||||
{sub}
|
||||
</AccessibleButton>,
|
||||
})}</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
@@ -161,12 +197,6 @@ export default class CompleteSecurity extends React.Component {
|
||||
>
|
||||
{_t("Skip")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.onStartClick}
|
||||
>
|
||||
{_t("Start")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -27,6 +27,8 @@ import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from "classnames";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import SSOButton from "../../views/elements/SSOButton";
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
@@ -120,8 +122,8 @@ export default createReactClass({
|
||||
'm.login.password': this._renderPasswordStep,
|
||||
|
||||
// CAS and SSO are the same thing, modulo the url we link to
|
||||
'm.login.cas': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("cas")),
|
||||
'm.login.sso': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("sso")),
|
||||
'm.login.cas': () => this._renderSsoStep("cas"),
|
||||
'm.login.sso': () => this._renderSsoStep("sso"),
|
||||
};
|
||||
|
||||
this._initLoginLogic();
|
||||
@@ -245,6 +247,7 @@ export default createReactClass({
|
||||
}
|
||||
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: errorText,
|
||||
// 401 would be the sensible status code for 'incorrect password'
|
||||
// but the login API gives a 403 https://matrix.org/jira/browse/SYN-744
|
||||
@@ -252,13 +255,6 @@ export default createReactClass({
|
||||
// We treat both as an incorrect password
|
||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus === 403,
|
||||
});
|
||||
}).finally(() => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -344,6 +340,21 @@ export default createReactClass({
|
||||
this.props.onRegisterClick();
|
||||
},
|
||||
|
||||
onTryRegisterClick: function(ev) {
|
||||
const step = this._getCurrentFlowStep();
|
||||
if (step === 'm.login.sso' || step === 'm.login.cas') {
|
||||
// If we're showing SSO it means that registration is also probably disabled,
|
||||
// so intercept the click and instead pretend the user clicked 'Sign in with SSO'.
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas';
|
||||
PlatformPeg.get().startSingleSignOn(this._loginLogic.createTemporaryClient(), ssoKind);
|
||||
} else {
|
||||
// Don't intercept - just go through to the register page
|
||||
this.onRegisterClick(ev);
|
||||
}
|
||||
},
|
||||
|
||||
async onServerDetailsNextPhaseClick() {
|
||||
this.setState({
|
||||
phase: PHASE_LOGIN,
|
||||
@@ -481,7 +492,7 @@ export default createReactClass({
|
||||
"Either use HTTPS or <a>enable unsafe scripts</a>.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a target="_blank" rel="noopener"
|
||||
return <a target="_blank" rel="noreferrer noopener"
|
||||
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
|
||||
>
|
||||
{ sub }
|
||||
@@ -496,11 +507,10 @@ export default createReactClass({
|
||||
"<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " +
|
||||
"is not blocking requests.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a target="_blank" rel="noopener" href={this.props.serverConfig.hsUrl}>
|
||||
'a': (sub) =>
|
||||
<a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}>
|
||||
{ sub }
|
||||
</a>;
|
||||
},
|
||||
</a>,
|
||||
},
|
||||
) }
|
||||
</span>;
|
||||
@@ -586,7 +596,7 @@ export default createReactClass({
|
||||
);
|
||||
},
|
||||
|
||||
_renderSsoStep: function(url) {
|
||||
_renderSsoStep: function(loginType) {
|
||||
const SignInToText = sdk.getComponent('views.auth.SignInToText');
|
||||
|
||||
let onEditServerDetailsClick = null;
|
||||
@@ -607,7 +617,10 @@ export default createReactClass({
|
||||
<SignInToText serverConfig={this.props.serverConfig}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick} />
|
||||
|
||||
<a href={url} className="mx_Login_sso_link mx_Login_submit">{ _t('Sign in with single sign-on') }</a>
|
||||
<SSOButton
|
||||
className="mx_Login_sso_link mx_Login_submit"
|
||||
matrixClient={this._loginLogic.createTemporaryClient()}
|
||||
loginType={loginType} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -655,7 +668,7 @@ export default createReactClass({
|
||||
{ serverDeadSection }
|
||||
{ this.renderServerComponent() }
|
||||
{ this.renderLoginComponentForStep() }
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onRegisterClick} href="#">
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#">
|
||||
{ _t('Create account') }
|
||||
</a>
|
||||
</AuthBody>
|
||||
|
||||
@@ -31,6 +31,8 @@ import classNames from "classnames";
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import Login from "../../../Login";
|
||||
import dis from "../../../dispatcher";
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
@@ -232,6 +234,13 @@ export default createReactClass({
|
||||
serverRequiresIdServer,
|
||||
busy: false,
|
||||
});
|
||||
const showGenericError = (e) => {
|
||||
this.setState({
|
||||
errorText: _t("Unable to query for supported registration methods."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
};
|
||||
try {
|
||||
await this._makeRegisterRequest({});
|
||||
// This should never succeed since we specified an empty
|
||||
@@ -243,18 +252,32 @@ export default createReactClass({
|
||||
flows: e.data.flows,
|
||||
});
|
||||
} else if (e.httpStatus === 403 && e.errcode === "M_UNKNOWN") {
|
||||
this.setState({
|
||||
errorText: _t("Registration has been disabled on this homeserver."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
// At this point registration is pretty much disabled, but before we do that let's
|
||||
// quickly check to see if the server supports SSO instead. If it does, we'll send
|
||||
// the user off to the login page to figure their account out.
|
||||
try {
|
||||
const loginLogic = new Login(hsUrl, isUrl, null, {
|
||||
defaultDeviceDisplayName: "riot login check", // We shouldn't ever be used
|
||||
});
|
||||
const flows = await loginLogic.getFlows();
|
||||
const hasSsoFlow = flows.find(f => f.type === 'm.login.sso' || f.type === 'm.login.cas');
|
||||
if (hasSsoFlow) {
|
||||
// Redirect to login page - server probably expects SSO only
|
||||
dis.dispatch({action: 'start_login'});
|
||||
} else {
|
||||
this.setState({
|
||||
errorText: _t("Registration has been disabled on this homeserver."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to get login flows to check for SSO support", e);
|
||||
showGenericError(e);
|
||||
}
|
||||
} else {
|
||||
console.log("Unable to query for supported registration methods.", e);
|
||||
this.setState({
|
||||
errorText: _t("Unable to query for supported registration methods."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
showGenericError(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -23,8 +23,8 @@ import * as Lifecycle from '../../../Lifecycle';
|
||||
import Modal from '../../../Modal';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {sendLoginRequest} from "../../../Login";
|
||||
import url from 'url';
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import SSOButton from "../../views/elements/SSOButton";
|
||||
|
||||
const LOGIN_VIEW = {
|
||||
LOADING: 1,
|
||||
@@ -55,7 +55,6 @@ export default class SoftLogout extends React.Component {
|
||||
this.state = {
|
||||
loginView: LOGIN_VIEW.LOADING,
|
||||
keyBackupNeeded: true, // assume we do while we figure it out (see componentWillMount)
|
||||
ssoUrl: null,
|
||||
|
||||
busy: false,
|
||||
password: "",
|
||||
@@ -105,18 +104,6 @@ export default class SoftLogout extends React.Component {
|
||||
|
||||
const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED;
|
||||
this.setState({loginView: chosenView});
|
||||
|
||||
if (chosenView === LOGIN_VIEW.CAS || chosenView === LOGIN_VIEW.SSO) {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
const appUrl = url.parse(window.location.href, true);
|
||||
appUrl.hash = ""; // Clear #/soft_logout off the URL
|
||||
appUrl.query["homeserver"] = client.getHomeserverUrl();
|
||||
appUrl.query["identityServer"] = client.getIdentityServerUrl();
|
||||
|
||||
const ssoUrl = client.getSsoLoginUrl(url.format(appUrl), chosenView === LOGIN_VIEW.CAS ? "cas" : "sso");
|
||||
this.setState({ssoUrl});
|
||||
}
|
||||
}
|
||||
|
||||
onPasswordChange = (ev) => {
|
||||
@@ -195,14 +182,6 @@ export default class SoftLogout extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
onSsoLogin = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.setState({busy: true});
|
||||
window.location.href = this.state.ssoUrl;
|
||||
};
|
||||
|
||||
_renderSignInSection() {
|
||||
if (this.state.loginView === LOGIN_VIEW.LOADING) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
@@ -257,8 +236,6 @@ export default class SoftLogout extends React.Component {
|
||||
}
|
||||
|
||||
if (this.state.loginView === LOGIN_VIEW.SSO || this.state.loginView === LOGIN_VIEW.CAS) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
if (!introText) {
|
||||
introText = _t("Sign in and regain access to your account.");
|
||||
} // else we already have a message and should use it (key backup warning)
|
||||
@@ -266,9 +243,9 @@ export default class SoftLogout extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
<p>{introText}</p>
|
||||
<AccessibleButton kind='primary' onClick={this.onSsoLogin}>
|
||||
{_t('Sign in with single sign-on')}
|
||||
</AccessibleButton>
|
||||
<SSOButton
|
||||
matrixClient={MatrixClientPeg.get()}
|
||||
loginType={this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default createReactClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_AuthFooter">
|
||||
<a href="https://matrix.org" target="_blank" rel="noopener">{ _t("powered by Matrix") }</a>
|
||||
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -331,7 +331,7 @@ export const TermsAuthEntry = createReactClass({
|
||||
checkboxes.push(
|
||||
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
||||
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
|
||||
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a>
|
||||
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
|
||||
</label>,
|
||||
);
|
||||
}
|
||||
@@ -604,6 +604,7 @@ export const FallbackAuthEntry = createReactClass({
|
||||
this.props.authSessionId,
|
||||
);
|
||||
this._popupWindow = window.open(url);
|
||||
this._popupWindow.opener = null;
|
||||
},
|
||||
|
||||
_onReceiveMessage: function(event) {
|
||||
|
||||
@@ -99,7 +99,7 @@ export default class ModularServerConfig extends ServerConfig {
|
||||
"Enter the location of your Modular homeserver. It may use your own " +
|
||||
"domain name or be a subdomain of <a>modular.im</a>.",
|
||||
{}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noopener">
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@ export const TYPES = {
|
||||
label: () => _t('Premium'),
|
||||
logo: () => <img src={require('../../../../res/img/modular-bw-logo.svg')} />,
|
||||
description: () => _t('Premium hosting for organisations <a>Learn more</a>', {}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noopener">
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>,
|
||||
}),
|
||||
|
||||
@@ -90,7 +90,8 @@ export default createReactClass({
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
|
||||
if (!pinnedEvent) return false;
|
||||
return pinnedEvent.getContent().pinned.includes(this.props.mxEvent.getId());
|
||||
const content = pinnedEvent.getContent();
|
||||
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
||||
},
|
||||
|
||||
onResendClick: function() {
|
||||
@@ -420,7 +421,7 @@ export default createReactClass({
|
||||
onClick={this.onPermalinkClick}
|
||||
href={permalink}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
||||
? _t('Share Permalink') : _t('Share Message') }
|
||||
@@ -445,7 +446,7 @@ export default createReactClass({
|
||||
element="a"
|
||||
className="mx_MessageContextMenu_field"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noreferrer noopener"
|
||||
onClick={this.closeMenu}
|
||||
href={mxEvent.event.content.external_url}
|
||||
>
|
||||
|
||||
@@ -68,10 +68,11 @@ export default class TopLeftMenu extends React.Component {
|
||||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex={-1}>{sub}</a>,
|
||||
a: sub =>
|
||||
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" tabIndex={-1}>{sub}</a>,
|
||||
},
|
||||
)}
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
|
||||
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
|
||||
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||
</a>
|
||||
</div>;
|
||||
|
||||
@@ -72,7 +72,7 @@ export default createReactClass({
|
||||
<button onClick={this._onInviteNeverWarnClicked}>
|
||||
{ _t('Invite anyway and never warn me again') }
|
||||
</button>
|
||||
<button onClick={this._onInviteClicked} autoFocus="true">
|
||||
<button onClick={this._onInviteClicked} autoFocus={true}>
|
||||
{ _t('Invite anyway') }
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default class ChangelogDialog extends React.Component {
|
||||
_elementsForCommit(commit) {
|
||||
return (
|
||||
<li key={commit.sha} className="mx_ChangelogDialog_li">
|
||||
<a href={commit.html_url} target="_blank" rel="noopener">
|
||||
<a href={commit.html_url} target="_blank" rel="noreferrer noopener">
|
||||
{commit.commit.message.split('\n')[0]}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||
@@ -22,6 +22,16 @@ import { _t } from '../../../languageHandler';
|
||||
import { Room } from "matrix-js-sdk";
|
||||
import Field from "../elements/Field";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
|
||||
import {
|
||||
PHASE_UNSENT,
|
||||
PHASE_REQUESTED,
|
||||
PHASE_READY,
|
||||
PHASE_DONE,
|
||||
PHASE_STARTED,
|
||||
PHASE_CANCELLED,
|
||||
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
class GenericEditor extends React.PureComponent {
|
||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
||||
@@ -605,12 +615,97 @@ class ServersInRoomList extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const PHASE_MAP = {
|
||||
[PHASE_UNSENT]: "unsent",
|
||||
[PHASE_REQUESTED]: "requested",
|
||||
[PHASE_READY]: "ready",
|
||||
[PHASE_DONE]: "done",
|
||||
[PHASE_STARTED]: "started",
|
||||
[PHASE_CANCELLED]: "cancelled",
|
||||
};
|
||||
|
||||
function VerificationRequest({txnId, request}) {
|
||||
const [, updateState] = useState();
|
||||
const [timeout, setRequestTimeout] = useState(request.timeout);
|
||||
|
||||
/* Re-render if something changes state */
|
||||
useEventEmitter(request, "change", updateState);
|
||||
|
||||
/* Keep re-rendering if there's a timeout */
|
||||
useEffect(() => {
|
||||
if (request.timeout == 0) return;
|
||||
|
||||
/* Note that request.timeout is a getter, so its value changes */
|
||||
const id = setInterval(() => {
|
||||
setRequestTimeout(request.timeout);
|
||||
}, 500);
|
||||
|
||||
return () => { clearInterval(id); };
|
||||
}, [request]);
|
||||
|
||||
return (<div className="mx_DevTools_VerificationRequest">
|
||||
<dl>
|
||||
<dt>Transaction</dt>
|
||||
<dd>{txnId}</dd>
|
||||
<dt>Phase</dt>
|
||||
<dd>{PHASE_MAP[request.phase] || request.phase}</dd>
|
||||
<dt>Timeout</dt>
|
||||
<dd>{Math.floor(timeout / 1000)}</dd>
|
||||
<dt>Methods</dt>
|
||||
<dd>{request.methods && request.methods.join(", ")}</dd>
|
||||
<dt>requestingUserId</dt>
|
||||
<dd>{request.requestingUserId}</dd>
|
||||
<dt>observeOnly</dt>
|
||||
<dd>{JSON.stringify(request.observeOnly)}</dd>
|
||||
</dl>
|
||||
</div>);
|
||||
}
|
||||
|
||||
class VerificationExplorer extends React.Component {
|
||||
static getLabel() {
|
||||
return _t("Verification Requests");
|
||||
}
|
||||
|
||||
/* Ensure this.context is the cli */
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
onNewRequest = () => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const cli = this.context;
|
||||
cli.on("crypto.verification.request", this.onNewRequest);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const cli = this.context;
|
||||
cli.off("crypto.verification.request", this.onNewRequest);
|
||||
}
|
||||
|
||||
render() {
|
||||
const cli = this.context;
|
||||
const room = this.props.room;
|
||||
const inRoomChannel = cli._crypto._inRoomVerificationRequests;
|
||||
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
|
||||
|
||||
return (<div>
|
||||
<div className="mx_Dialog_content">
|
||||
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
|
||||
<VerificationRequest txnId={txnId} request={request} key={txnId} />,
|
||||
)}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
const Entries = [
|
||||
SendCustomEvent,
|
||||
RoomStateExplorer,
|
||||
SendAccountData,
|
||||
AccountDataExplorer,
|
||||
ServersInRoomList,
|
||||
VerificationExplorer,
|
||||
];
|
||||
|
||||
export default class DevtoolsDialog extends React.PureComponent {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {humanizeTime} from "../../../utils/humanize";
|
||||
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
@@ -218,7 +219,7 @@ class DMRoomTile extends React.PureComponent {
|
||||
}
|
||||
|
||||
// Push any text we missed (end of text)
|
||||
if (i < (str.length - 1)) {
|
||||
if (i < str.length) {
|
||||
result.push(<span key={i + 'end'}>{str.substring(i)}</span>);
|
||||
}
|
||||
|
||||
@@ -332,7 +333,23 @@ export default class InviteDialog extends React.PureComponent {
|
||||
}
|
||||
|
||||
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals();
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||
|
||||
// Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the
|
||||
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
|
||||
const taggedRooms = RoomListStore.getRoomLists();
|
||||
const dmTaggedRooms = taggedRooms[TAG_DM];
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
for (const dmRoom of dmTaggedRooms) {
|
||||
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
|
||||
for (const member of otherMembers) {
|
||||
if (rooms[member.userId]) continue; // already have a room
|
||||
|
||||
console.warn(`Adding DM room for ${member.userId} as ${dmRoom.roomId} from tag, not DM map`);
|
||||
rooms[member.userId] = dmRoom;
|
||||
}
|
||||
}
|
||||
|
||||
const recents = [];
|
||||
for (const userId in rooms) {
|
||||
// Filter out user IDs that are already in the room / should be excluded
|
||||
@@ -512,9 +529,27 @@ export default class InviteDialog extends React.PureComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
_convertFilter(): Member[] {
|
||||
// Check to see if there's anything to convert first
|
||||
if (!this.state.filterText || !this.state.filterText.includes('@')) return this.state.targets || [];
|
||||
|
||||
let newMember: Member;
|
||||
if (this.state.filterText.startsWith('@')) {
|
||||
// Assume mxid
|
||||
newMember = new DirectoryMember({user_id: this.state.filterText, display_name: null, avatar_url: null});
|
||||
} else {
|
||||
// Assume email
|
||||
newMember = new ThreepidMember(this.state.filterText);
|
||||
}
|
||||
const newTargets = [...(this.state.targets || []), newMember];
|
||||
this.setState({targets: newTargets, filterText: ''});
|
||||
return newTargets;
|
||||
}
|
||||
|
||||
_startDm = async () => {
|
||||
this.setState({busy: true});
|
||||
const targetIds = this.state.targets.map(t => t.userId);
|
||||
const targets = this._convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
|
||||
// Check if there is already a DM with these people and reuse it if possible.
|
||||
const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
|
||||
@@ -529,7 +564,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const createRoomOptions = {};
|
||||
const createRoomOptions = {inlineErrors: true};
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
// Check whether all users have uploaded device keys before.
|
||||
@@ -544,9 +579,12 @@ export default class InviteDialog extends React.PureComponent {
|
||||
// Check if it's a traditional DM and create the room if required.
|
||||
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
|
||||
let createRoomPromise = Promise.resolve();
|
||||
if (targetIds.length === 1) {
|
||||
const isSelf = targetIds.length === 1 && targetIds[0] === MatrixClientPeg.get().getUserId();
|
||||
if (targetIds.length === 1 && !isSelf) {
|
||||
createRoomOptions.dmUserId = targetIds[0];
|
||||
createRoomPromise = createRoom(createRoomOptions);
|
||||
} else if (isSelf) {
|
||||
createRoomPromise = createRoom(createRoomOptions);
|
||||
} else {
|
||||
// Create a boring room and try to invite the targets manually.
|
||||
createRoomPromise = createRoom(createRoomOptions).then(roomId => {
|
||||
@@ -573,7 +611,9 @@ export default class InviteDialog extends React.PureComponent {
|
||||
|
||||
_inviteUsers = () => {
|
||||
this.setState({busy: true});
|
||||
const targetIds = this.state.targets.map(t => t.userId);
|
||||
this._convertFilter();
|
||||
const targets = this._convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
if (!room) {
|
||||
@@ -630,13 +670,14 @@ export default class InviteDialog extends React.PureComponent {
|
||||
|
||||
// While we're here, try and autocomplete a search result for the mxid itself
|
||||
// if there's no matches (and the input looks like a mxid).
|
||||
if (term[0] === '@' && term.indexOf(':') > 1 && r.results.length === 0) {
|
||||
if (term[0] === '@' && term.indexOf(':') > 1) {
|
||||
try {
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(term);
|
||||
if (profile) {
|
||||
// If we have a profile, we have enough information to assume that
|
||||
// the mxid can be invited - add it to the list
|
||||
r.results.push({
|
||||
// the mxid can be invited - add it to the list. We stick it at the
|
||||
// top so it is most obviously presented to the user.
|
||||
r.results.splice(0, 0, {
|
||||
user_id: term,
|
||||
display_name: profile['displayname'],
|
||||
avatar_url: profile['avatar_url'],
|
||||
@@ -645,6 +686,14 @@ export default class InviteDialog extends React.PureComponent {
|
||||
} catch (e) {
|
||||
console.warn("Non-fatal error trying to make an invite for a user ID");
|
||||
console.warn(e);
|
||||
|
||||
// Add a result anyways, just without a profile. We stick it at the
|
||||
// top so it is most obviously presented to the user.
|
||||
r.results.splice(0, 0, {
|
||||
user_id: term,
|
||||
display_name: term,
|
||||
avatar_url: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,7 +818,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
];
|
||||
const toAdd = [];
|
||||
const failed = [];
|
||||
const potentialAddresses = text.split(/[\s,]+/);
|
||||
const potentialAddresses = text.split(/[\s,]+/).map(p => p.trim()).filter(p => !!p); // filter empty strings
|
||||
for (const address of potentialAddresses) {
|
||||
const member = possibleMembers.find(m => m.userId === address);
|
||||
if (member) {
|
||||
@@ -857,24 +906,24 @@ export default class InviteDialog extends React.PureComponent {
|
||||
// Mix in the server results if we have any, but only if we're searching. We track the additional
|
||||
// members separately because we want to filter sourceMembers but trust the mixin arrays to have
|
||||
// the right members in them.
|
||||
let additionalMembers = [];
|
||||
let priorityAdditionalMembers = []; // Shows up before our own suggestions, higher quality
|
||||
let otherAdditionalMembers = []; // Shows up after our own suggestions, lower quality
|
||||
const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin;
|
||||
if (this.state.filterText && hasMixins && kind === 'suggestions') {
|
||||
// We don't want to duplicate members though, so just exclude anyone we've already seen.
|
||||
const notAlreadyExists = (u: Member): boolean => {
|
||||
return !sourceMembers.some(m => m.userId === u.userId)
|
||||
&& !additionalMembers.some(m => m.userId === u.userId);
|
||||
&& !priorityAdditionalMembers.some(m => m.userId === u.userId)
|
||||
&& !otherAdditionalMembers.some(m => m.userId === u.userId);
|
||||
};
|
||||
|
||||
const uniqueServerResults = this.state.serverResultsMixin.filter(notAlreadyExists);
|
||||
additionalMembers = additionalMembers.concat(...uniqueServerResults);
|
||||
|
||||
const uniqueThreepidResults = this.state.threepidResultsMixin.filter(notAlreadyExists);
|
||||
additionalMembers = additionalMembers.concat(...uniqueThreepidResults);
|
||||
otherAdditionalMembers = this.state.serverResultsMixin.filter(notAlreadyExists);
|
||||
priorityAdditionalMembers = this.state.threepidResultsMixin.filter(notAlreadyExists);
|
||||
}
|
||||
const hasAdditionalMembers = priorityAdditionalMembers.length > 0 || otherAdditionalMembers.length > 0;
|
||||
|
||||
// Hide the section if there's nothing to filter by
|
||||
if (sourceMembers.length === 0 && additionalMembers.length === 0) return null;
|
||||
if (sourceMembers.length === 0 && !hasAdditionalMembers) return null;
|
||||
|
||||
// Do some simple filtering on the input before going much further. If we get no results, say so.
|
||||
if (this.state.filterText) {
|
||||
@@ -882,7 +931,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
sourceMembers = sourceMembers
|
||||
.filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy));
|
||||
|
||||
if (sourceMembers.length === 0 && additionalMembers.length === 0) {
|
||||
if (sourceMembers.length === 0 && !hasAdditionalMembers) {
|
||||
return (
|
||||
<div className='mx_InviteDialog_section'>
|
||||
<h3>{sectionName}</h3>
|
||||
@@ -894,7 +943,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
|
||||
// Now we mix in the additional members. Again, we presume these have already been filtered. We
|
||||
// also assume they are more relevant than our suggestions and prepend them to the list.
|
||||
sourceMembers = [...additionalMembers, ...sourceMembers];
|
||||
sourceMembers = [...priorityAdditionalMembers, ...sourceMembers, ...otherAdditionalMembers];
|
||||
|
||||
// If we're going to hide one member behind 'show more', just use up the space of the button
|
||||
// with the member's tile instead.
|
||||
@@ -1014,7 +1063,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
"If you can't find someone, ask them for their username, share your " +
|
||||
"username (%(userId)s) or <a>profile link</a>.",
|
||||
{userId},
|
||||
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
|
||||
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{sub}</a>},
|
||||
);
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
@@ -1023,12 +1072,17 @@ export default class InviteDialog extends React.PureComponent {
|
||||
helpText = _t(
|
||||
"If you can't find someone, ask them for their username (e.g. @user:server.com) or " +
|
||||
"<a>share this room</a>.", {},
|
||||
{a: (sub) => <a href={makeRoomPermalink(this.props.roomId)} rel="noopener" target="_blank">{sub}</a>},
|
||||
{
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
},
|
||||
);
|
||||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
}
|
||||
|
||||
const hasSelection = this.state.targets.length > 0
|
||||
|| (this.state.filterText && this.state.filterText.includes('@'));
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_InviteDialog'
|
||||
@@ -1045,7 +1099,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
kind="primary"
|
||||
onClick={goButtonFn}
|
||||
className='mx_InviteDialog_goButton'
|
||||
disabled={this.state.busy}
|
||||
disabled={this.state.busy || !hasSelection}
|
||||
>
|
||||
{buttonText}
|
||||
</AccessibleButton>
|
||||
|
||||
@@ -23,6 +23,7 @@ import VerificationRequestDialog from './VerificationRequestDialog';
|
||||
import BaseDialog from './BaseDialog';
|
||||
import DialogButtons from '../elements/DialogButtons';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
@replaceableComponent("views.dialogs.NewSessionReviewDialog")
|
||||
export default class NewSessionReviewDialog extends React.PureComponent {
|
||||
@@ -33,20 +34,38 @@ export default class NewSessionReviewDialog extends React.PureComponent {
|
||||
}
|
||||
|
||||
onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, {
|
||||
headerImage: require("../../../../res/img/e2e/warning.svg"),
|
||||
title: _t("Your account is not secure"),
|
||||
description: <div>
|
||||
{_t("One of the following may be compromised:")}
|
||||
<ul>
|
||||
<li>{_t("Your password")}</li>
|
||||
<li>{_t("Your homeserver")}</li>
|
||||
<li>{_t("This session, or the other session")}</li>
|
||||
<li>{_t("The internet connection either session is using")}</li>
|
||||
</ul>
|
||||
<div>
|
||||
{_t("We recommend you change your password and recovery key in Settings immediately")}
|
||||
</div>
|
||||
</div>,
|
||||
onFinished: () => this.props.onFinished(false),
|
||||
});
|
||||
}
|
||||
|
||||
onContinueClick = async () => {
|
||||
onContinueClick = () => {
|
||||
const { userId, device } = this.props;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const request = await cli.requestVerification(
|
||||
const requestPromise = cli.requestVerification(
|
||||
userId,
|
||||
[device.deviceId],
|
||||
);
|
||||
|
||||
this.props.onFinished(true);
|
||||
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
|
||||
verificationRequest: request,
|
||||
verificationRequestPromise: requestPromise,
|
||||
member: cli.getUser(userId),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export default createReactClass({
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
headerImage: PropTypes.string,
|
||||
quitOnly: PropTypes.bool, // quitOnly doesn't show the cancel button just the quit [x].
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@@ -42,6 +43,7 @@ export default createReactClass({
|
||||
focus: true,
|
||||
hasCancelButton: true,
|
||||
danger: false,
|
||||
quitOnly: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -73,7 +75,7 @@ export default createReactClass({
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
primaryButtonClass={primaryButtonClass}
|
||||
cancelButton={this.props.cancelButton}
|
||||
hasCancel={this.props.hasCancelButton}
|
||||
hasCancel={this.props.hasCancelButton && !this.props.quitOnly}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
focus={this.props.focus}
|
||||
onCancel={this.onCancel}
|
||||
|
||||
@@ -218,7 +218,7 @@ export default class ShareDialog extends React.Component {
|
||||
</div>
|
||||
<div className="mx_ShareDialog_social_container">
|
||||
{
|
||||
socials.map((social) => <a rel="noopener"
|
||||
socials.map((social) => <a rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
key={social.name}
|
||||
name={social.name}
|
||||
|
||||
@@ -135,7 +135,7 @@ export default class TermsDialog extends React.PureComponent {
|
||||
rows.push(<tr key={termDoc[termsLang].url}>
|
||||
<td className="mx_TermsDialog_service">{serviceName}</td>
|
||||
<td className="mx_TermsDialog_summary">{summary}</td>
|
||||
<td>{termDoc[termsLang].name} <a rel="noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
<td>{termDoc[termsLang].name} <a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
<span className="mx_TermsDialog_link" />
|
||||
</a></td>
|
||||
<td><TermsCheckbox
|
||||
|
||||
@@ -22,7 +22,8 @@ import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class VerificationRequestDialog extends React.Component {
|
||||
static propTypes = {
|
||||
verificationRequest: PropTypes.object.isRequired,
|
||||
verificationRequest: PropTypes.object,
|
||||
verificationRequestPromise: PropTypes.object,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -34,6 +35,8 @@ export default class VerificationRequestDialog extends React.Component {
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
|
||||
const member = this.props.member ||
|
||||
MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId);
|
||||
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
|
||||
contentId="mx_Dialog_content"
|
||||
title={_t("Verification Request")}
|
||||
@@ -42,14 +45,19 @@ export default class VerificationRequestDialog extends React.Component {
|
||||
<EncryptionPanel
|
||||
layout="dialog"
|
||||
verificationRequest={this.props.verificationRequest}
|
||||
verificationRequestPromise={this.props.verificationRequestPromise}
|
||||
onClose={this.props.onFinished}
|
||||
member={MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId)}
|
||||
member={member}
|
||||
/>
|
||||
</BaseDialog>;
|
||||
}
|
||||
|
||||
onFinished() {
|
||||
this.props.verificationRequest.cancel();
|
||||
async onFinished() {
|
||||
this.props.onFinished();
|
||||
let request = this.props.verificationRequest;
|
||||
if (!request && this.props.verificationRequestPromise) {
|
||||
request = await this.props.verificationRequestPromise;
|
||||
}
|
||||
request.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,7 +552,7 @@ export default class AppTile extends React.Component {
|
||||
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes');
|
||||
Object.assign(document.createElement('a'),
|
||||
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener'}).click();
|
||||
{ target: '_blank', href: this._getSafeUrl(), rel: 'noreferrer noopener'}).click();
|
||||
}
|
||||
|
||||
_onReloadWidgetClick() {
|
||||
|
||||
@@ -78,8 +78,7 @@ export class EditableItem extends React.Component {
|
||||
|
||||
return (
|
||||
<div className="mx_EditableItem">
|
||||
<img src={require("../../../../res/img/feather-customised/cancel.svg")} width={14} height={14}
|
||||
onClick={this._onRemove} className="mx_EditableItem_delete" alt={_t("Remove")} />
|
||||
<div onClick={this._onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
|
||||
<span className="mx_EditableItem_item">{this.props.value}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -123,8 +122,9 @@ export default class EditableItemList extends React.Component {
|
||||
<form onSubmit={this._onItemAdded} autoComplete="off"
|
||||
noValidate={true} className="mx_EditableItemList_newItem">
|
||||
<Field id={`mx_EditableItemList_new_${this.props.id}`} label={this.props.placeholder} type="text"
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary">
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
||||
list={this.props.suggestionsListId} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit">
|
||||
{_t("Add")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
|
||||
@@ -32,6 +32,8 @@ export default class Field extends React.PureComponent {
|
||||
element: PropTypes.oneOf(["input", "select", "textarea"]),
|
||||
// The field's type (when used as an <input>). Defaults to "text".
|
||||
type: PropTypes.string,
|
||||
// id of a <datalist> element for suggestions
|
||||
list: PropTypes.string,
|
||||
// The field's label string.
|
||||
label: PropTypes.string,
|
||||
// The field's placeholder string. Defaults to the label.
|
||||
@@ -157,7 +159,7 @@ export default class Field extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
element, prefix, postfix, className, onValidate, children,
|
||||
tooltipContent, flagInvalid, tooltipClassName, ...inputProps} = this.props;
|
||||
tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props;
|
||||
|
||||
const inputElement = element || "input";
|
||||
|
||||
@@ -169,6 +171,7 @@ export default class Field extends React.PureComponent {
|
||||
inputProps.onFocus = this.onFocus;
|
||||
inputProps.onChange = this.onChange;
|
||||
inputProps.onBlur = this.onBlur;
|
||||
inputProps.list = list;
|
||||
|
||||
const fieldInput = React.createElement(inputElement, inputProps, children);
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export default class ImageView extends React.Component {
|
||||
getName() {
|
||||
let name = this.props.name;
|
||||
if (name && this.props.link) {
|
||||
name = <a href={ this.props.link } target="_blank" rel="noopener">{ name }</a>;
|
||||
name = <a href={ this.props.link } target="_blank" rel="noreferrer noopener">{ name }</a>;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import classNames from 'classnames';
|
||||
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { getDisplayAliasForRoom } from '../../../Rooms';
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
@@ -129,7 +128,7 @@ const Pill = createReactClass({
|
||||
const localRoom = resourceId[0] === '#' ?
|
||||
MatrixClientPeg.get().getRooms().find((r) => {
|
||||
return r.getCanonicalAlias() === resourceId ||
|
||||
r.getAliases().includes(resourceId);
|
||||
r.getAltAliases().includes(resourceId);
|
||||
}) : MatrixClientPeg.get().getRoom(resourceId);
|
||||
room = localRoom;
|
||||
if (!localRoom) {
|
||||
@@ -237,12 +236,12 @@ const Pill = createReactClass({
|
||||
case Pill.TYPE_ROOM_MENTION: {
|
||||
const room = this.state.room;
|
||||
if (room) {
|
||||
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
||||
linkText = resource;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
|
||||
}
|
||||
pillClass = 'mx_RoomPill';
|
||||
}
|
||||
pillClass = 'mx_RoomPill';
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
|
||||
@@ -92,6 +92,7 @@ export default class RoomAliasField extends React.PureComponent {
|
||||
invalid: () => _t("Please provide a room alias"),
|
||||
}, {
|
||||
key: "taken",
|
||||
final: true,
|
||||
test: async ({value}) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
|
||||
41
src/components/views/elements/SSOButton.js
Normal file
41
src/components/views/elements/SSOButton.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
|
||||
const SSOButton = ({matrixClient, loginType, ...props}) => {
|
||||
const onClick = () => {
|
||||
PlatformPeg.get().startSingleSignOn(matrixClient, loginType);
|
||||
};
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props} kind="primary" onClick={onClick}>
|
||||
{_t("Sign in with single sign-on")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
SSOButton.propTypes = {
|
||||
matrixClient: PropTypes.object.isRequired, // does not use context as may use a temporary client
|
||||
loginType: PropTypes.oneOf(["sso", "cas"]), // defaults to "sso" in base-apis
|
||||
};
|
||||
|
||||
export default SSOButton;
|
||||
@@ -28,9 +28,11 @@ import classNames from 'classnames';
|
||||
* An array of rules describing how to check to input value. Each rule in an object
|
||||
* and may have the following properties:
|
||||
* - `key`: A unique ID for the rule. Required.
|
||||
* - `skip`: A function used to determine whether the rule should even be evaluated.
|
||||
* - `test`: A function used to determine the rule's current validity. Required.
|
||||
* - `valid`: Function returning text to show when the rule is valid. Only shown if set.
|
||||
* - `invalid`: Function returning text to show when the rule is invalid. Only shown if set.
|
||||
* - `final`: A Boolean if true states that this rule will only be considered if all rules before it returned valid.
|
||||
* @returns {Function}
|
||||
* A validation function that takes in the current input value and returns
|
||||
* the overall validity and a feedback UI that can be rendered for more detail.
|
||||
@@ -51,9 +53,20 @@ export default function withValidation({ description, rules }) {
|
||||
if (!rule.key || !rule.test) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!valid && rule.final) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = { value, allowEmpty };
|
||||
|
||||
if (rule.skip && rule.skip.call(this, data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We're setting `this` to whichever component holds the validation
|
||||
// function. That allows rules to access the state of the component.
|
||||
const ruleValid = await rule.test.call(this, { value, allowEmpty });
|
||||
const ruleValid = await rule.test.call(this, data);
|
||||
valid = valid && ruleValid;
|
||||
if (ruleValid && rule.valid) {
|
||||
// If the rule's result is valid and has text to show for
|
||||
|
||||
@@ -17,89 +17,87 @@ limitations under the License.
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||
import * as qs from "qs";
|
||||
import QRCode from "qrcode-react";
|
||||
import {MatrixClientPeg} from "../../../../MatrixClientPeg";
|
||||
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import {ToDeviceChannel} from "matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel";
|
||||
import {decodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
|
||||
import Spinner from "../Spinner";
|
||||
import * as QRCode from "qrcode";
|
||||
|
||||
const CODE_VERSION = 0x02; // the version of binary QR codes we support
|
||||
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
|
||||
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
|
||||
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
|
||||
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
|
||||
|
||||
@replaceableComponent("views.elements.crypto.VerificationQRCode")
|
||||
export default class VerificationQRCode extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// Common for all kinds of QR codes
|
||||
keys: PropTypes.array.isRequired, // array of [Key ID, Base64 Key] pairs
|
||||
action: PropTypes.string.isRequired,
|
||||
keyholderUserId: PropTypes.string.isRequired,
|
||||
|
||||
// User verification use case only
|
||||
secret: PropTypes.string,
|
||||
otherUserKey: PropTypes.string, // Base64 key being verified
|
||||
otherUserDeviceKey: PropTypes.string, // Base64 key of the other user's device (or what we think it is; optional)
|
||||
requestEventId: PropTypes.string, // for DM verification only
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
action: "verify",
|
||||
prefix: PropTypes.string.isRequired,
|
||||
version: PropTypes.number.isRequired,
|
||||
mode: PropTypes.number.isRequired,
|
||||
transactionId: PropTypes.string.isRequired, // or requestEventId
|
||||
firstKeyB64: PropTypes.string.isRequired,
|
||||
secondKeyB64: PropTypes.string.isRequired,
|
||||
secretB64: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
static async getPropsForRequest(verificationRequest: VerificationRequest) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const myUserId = cli.getUserId();
|
||||
const otherUserId = verificationRequest.otherUserId;
|
||||
const myDeviceId = cli.getDeviceId();
|
||||
const otherDevice = verificationRequest.targetDevice;
|
||||
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
|
||||
|
||||
const qrProps = {
|
||||
secret: verificationRequest.encodedSharedSecret,
|
||||
keyholderUserId: myUserId,
|
||||
action: "verify",
|
||||
keys: [], // array of pairs: keyId, base64Key
|
||||
otherUserKey: "", // base64key
|
||||
otherUserDeviceKey: "", // base64key
|
||||
requestEventId: "", // we figure this out in a moment
|
||||
};
|
||||
let mode = MODE_VERIFY_OTHER_USER;
|
||||
if (myUserId === otherUserId) {
|
||||
// Mode changes depending on whether or not we trust the master cross signing key
|
||||
const myTrust = cli.checkUserTrust(myUserId);
|
||||
if (myTrust.isCrossSigningVerified()) {
|
||||
mode = MODE_VERIFY_SELF_TRUSTED;
|
||||
} else {
|
||||
mode = MODE_VERIFY_SELF_UNTRUSTED;
|
||||
}
|
||||
}
|
||||
|
||||
const requestEvent = verificationRequest.requestEvent;
|
||||
qrProps.requestEventId = requestEvent.getId()
|
||||
const transactionId = requestEvent.getId()
|
||||
? requestEvent.getId()
|
||||
: ToDeviceChannel.getTransactionId(requestEvent);
|
||||
|
||||
// Populate the keys we need depending on which direction and users are involved in the verification.
|
||||
if (myUserId === otherUserId) {
|
||||
if (!otherDeviceId) {
|
||||
// Existing scanning New session's QR code
|
||||
qrProps.otherUserDeviceKey = null;
|
||||
} else {
|
||||
// New scanning Existing session's QR code
|
||||
const myDevices = (await cli.getStoredDevicesForUser(myUserId)) || [];
|
||||
const device = myDevices.find(d => d.deviceId === otherDeviceId);
|
||||
if (device) qrProps.otherUserDeviceKey = device.getFingerprint();
|
||||
}
|
||||
const qrProps = {
|
||||
prefix: BINARY_PREFIX,
|
||||
version: CODE_VERSION,
|
||||
mode,
|
||||
transactionId,
|
||||
firstKeyB64: '', // worked out shortly
|
||||
secondKeyB64: '', // worked out shortly
|
||||
secretB64: verificationRequest.encodedSharedSecret,
|
||||
};
|
||||
|
||||
// Either direction shares these next few props
|
||||
const myCrossSigningInfo = cli.getStoredCrossSigningForUser(myUserId);
|
||||
const myDevices = (await cli.getStoredDevicesForUser(myUserId)) || [];
|
||||
|
||||
const xsignInfo = cli.getStoredCrossSigningForUser(myUserId);
|
||||
qrProps.otherUserKey = xsignInfo.getId("master");
|
||||
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||
// First key is our master cross signing key
|
||||
qrProps.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||
|
||||
qrProps.keys = [
|
||||
[myDeviceId, cli.getDeviceEd25519Key()],
|
||||
[xsignInfo.getId("master"), xsignInfo.getId("master")],
|
||||
];
|
||||
} else {
|
||||
// Doesn't matter which direction the verification is, we always show the same QR code
|
||||
// for not-ourself verification.
|
||||
const myXsignInfo = cli.getStoredCrossSigningForUser(myUserId);
|
||||
const otherXsignInfo = cli.getStoredCrossSigningForUser(otherUserId);
|
||||
const otherDevices = (await cli.getStoredDevicesForUser(otherUserId)) || [];
|
||||
const otherDevice = otherDevices.find(d => d.deviceId === otherDeviceId);
|
||||
// Second key is the other user's master cross signing key
|
||||
const otherUserCrossSigningInfo = cli.getStoredCrossSigningForUser(otherUserId);
|
||||
qrProps.secondKeyB64 = otherUserCrossSigningInfo.getId("master");
|
||||
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
||||
// First key is our master cross signing key
|
||||
qrProps.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||
|
||||
qrProps.keys = [
|
||||
[myDeviceId, cli.getDeviceEd25519Key()],
|
||||
[myXsignInfo.getId("master"), myXsignInfo.getId("master")],
|
||||
];
|
||||
qrProps.otherUserKey = otherXsignInfo.getId("master");
|
||||
if (otherDevice) qrProps.otherUserDeviceKey = otherDevice.getFingerprint();
|
||||
// Second key is the other device's device key
|
||||
const otherDevice = verificationRequest.targetDevice;
|
||||
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
|
||||
const device = myDevices.find(d => d.deviceId === otherDeviceId);
|
||||
qrProps.secondKeyB64 = device.getFingerprint();
|
||||
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
|
||||
// First key is our device's key
|
||||
qrProps.firstKeyB64 = cli.getDeviceEd25519Key();
|
||||
|
||||
// Second key is what we think our master cross signing key is
|
||||
qrProps.secondKeyB64 = myCrossSigningInfo.getId("master");
|
||||
}
|
||||
|
||||
return qrProps;
|
||||
@@ -107,21 +105,63 @@ export default class VerificationQRCode extends React.PureComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
dataUri: null,
|
||||
};
|
||||
this.generateQrCode();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps): void {
|
||||
if (JSON.stringify(this.props) === JSON.stringify(prevProps)) return; // No prop change
|
||||
|
||||
this.generateQRCode();
|
||||
}
|
||||
|
||||
async generateQrCode() {
|
||||
let buf = Buffer.alloc(0); // we'll concat our way through life
|
||||
|
||||
const appendByte = (b: number) => {
|
||||
const tmpBuf = Buffer.from([b]);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendInt = (i: number) => {
|
||||
const tmpBuf = Buffer.alloc(2);
|
||||
tmpBuf.writeInt16BE(i, 0);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendStr = (s: string, enc: string, withLengthPrefix = true) => {
|
||||
const tmpBuf = Buffer.from(s, enc);
|
||||
if (withLengthPrefix) appendInt(tmpBuf.byteLength);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendEncBase64 = (b64: string) => {
|
||||
const b = decodeBase64(b64);
|
||||
const tmpBuf = Buffer.from(b);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
|
||||
// Actually build the buffer for the QR code
|
||||
appendStr(this.props.prefix, "ascii", false);
|
||||
appendByte(this.props.version);
|
||||
appendByte(this.props.mode);
|
||||
appendStr(this.props.transactionId, "utf-8");
|
||||
appendEncBase64(this.props.firstKeyB64);
|
||||
appendEncBase64(this.props.secondKeyB64);
|
||||
appendEncBase64(this.props.secretB64);
|
||||
|
||||
// Now actually assemble the QR code's data URI
|
||||
const uri = await QRCode.toDataURL([{data: buf, mode: 'byte'}], {
|
||||
errorCorrectionLevel: 'L', // we want it as trivial-looking as possible
|
||||
});
|
||||
this.setState({dataUri: uri});
|
||||
}
|
||||
|
||||
render() {
|
||||
const query = {
|
||||
request: this.props.requestEventId,
|
||||
action: this.props.action,
|
||||
other_user_key: this.props.otherUserKey,
|
||||
secret: this.props.secret,
|
||||
};
|
||||
for (const key of this.props.keys) {
|
||||
query[`key_${key[0]}`] = key[1];
|
||||
if (!this.state.dataUri) {
|
||||
return <div className='mx_VerificationQRCode'><Spinner /></div>;
|
||||
}
|
||||
|
||||
const uri = `https://matrix.to/#/${this.props.keyholderUserId}?${qs.stringify(query)}`;
|
||||
|
||||
return <QRCode value={uri} size={512} logoWidth={64} logo={require("../../../../../res/img/matrix-m.svg")} />;
|
||||
return <img src={this.state.dataUri} className='mx_VerificationQRCode' />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ export default createReactClass({
|
||||
|
||||
const av = (
|
||||
<BaseAvatar
|
||||
aria-hidden="true"
|
||||
name={this.props.member.displayname || this.props.member.userId}
|
||||
idName={this.props.member.userId}
|
||||
width={36} height={36}
|
||||
|
||||
@@ -20,7 +20,7 @@ import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
|
||||
import {formatTime} from '../../../DateUtils';
|
||||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import {pillifyLinks} from '../../../utils/pillify';
|
||||
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
@@ -53,6 +53,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
||||
this.state = {canRedact, sendStatus: event.getAssociatedStatus()};
|
||||
|
||||
this._content = createRef();
|
||||
this._pills = [];
|
||||
}
|
||||
|
||||
_onAssociatedStatusChanged = () => {
|
||||
@@ -81,7 +82,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
||||
pillifyLinks() {
|
||||
// not present for redacted events
|
||||
if (this._content.current) {
|
||||
pillifyLinks(this._content.current.children, this.props.mxEvent);
|
||||
pillifyLinks(this._content.current.children, this.props.mxEvent, this._pills);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +91,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unmountPills(this._pills);
|
||||
const event = this.props.mxEvent;
|
||||
if (event.localRedactionEvent()) {
|
||||
event.localRedactionEvent().off("status", this._onAssociatedStatusChanged);
|
||||
|
||||
@@ -247,6 +247,8 @@ export default createReactClass({
|
||||
});
|
||||
};
|
||||
|
||||
// This button should actually Download because usercontent/ will try to click itself
|
||||
// but it is not guaranteed between various browsers' settings.
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MFileBody_download">
|
||||
@@ -269,6 +271,8 @@ export default createReactClass({
|
||||
// We can't provide a Content-Disposition header like we would for HTTP.
|
||||
download: fileName,
|
||||
textContent: _t("Download %(text)s", { text: text }),
|
||||
// only auto-download if a user triggered this iframe explicitly
|
||||
auto: !this.props.decryptedBlob,
|
||||
}, "*");
|
||||
};
|
||||
|
||||
@@ -290,14 +294,14 @@ export default createReactClass({
|
||||
src={`${url}?origin=${encodeURIComponent(window.location.origin)}`}
|
||||
onLoad={onIframeLoad}
|
||||
ref={this._iframe}
|
||||
sandbox="allow-scripts allow-downloads" />
|
||||
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
} else if (contentUrl) {
|
||||
const downloadProps = {
|
||||
target: "_blank",
|
||||
rel: "noopener",
|
||||
rel: "noreferrer noopener",
|
||||
|
||||
// We set the href regardless of whether or not we intercept the download
|
||||
// because we don't really want to convert the file to a blob eagerly, and
|
||||
|
||||
@@ -62,8 +62,8 @@ export default class MKeyVerificationRequest extends React.Component {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
try {
|
||||
await request.accept();
|
||||
this._openRequest();
|
||||
await request.accept();
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
}
|
||||
@@ -136,9 +136,9 @@ export default class MKeyVerificationRequest extends React.Component {
|
||||
} else if (request.cancelled) {
|
||||
stateLabel = this._cancelledLabel(request.cancellingUserId);
|
||||
} else if (request.accepting) {
|
||||
stateLabel = _t("accepting …");
|
||||
stateLabel = _t("Accepting …");
|
||||
} else if (request.declining) {
|
||||
stateLabel = _t("declining …");
|
||||
stateLabel = _t("Declining …");
|
||||
}
|
||||
stateNode = (<div className="mx_cryptoEvent_state">{stateLabel}</div>);
|
||||
}
|
||||
|
||||
@@ -121,10 +121,12 @@ export default class MessageActionBar extends React.PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
||||
this.props.mxEvent.on("Event.beforeRedaction", this.onBeforeRedaction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted);
|
||||
this.props.mxEvent.removeListener("Event.beforeRedaction", this.onBeforeRedaction);
|
||||
}
|
||||
|
||||
onDecrypted = () => {
|
||||
@@ -133,6 +135,11 @@ export default class MessageActionBar extends React.PureComponent {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onBeforeRedaction = () => {
|
||||
// When an event is redacted, we can't edit it so update the available actions.
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onFocusChange = (focused) => {
|
||||
if (!this.props.onFocusChange) {
|
||||
return;
|
||||
|
||||
@@ -30,7 +30,7 @@ import { _t } from '../../../languageHandler';
|
||||
import * as ContextMenu from '../../structures/ContextMenu';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {pillifyLinks} from '../../../utils/pillify';
|
||||
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
|
||||
import {toRightOf} from "../../structures/ContextMenu";
|
||||
@@ -92,6 +92,7 @@ export default createReactClass({
|
||||
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
this._pills = [];
|
||||
if (!this.props.editState) {
|
||||
this._applyFormatting();
|
||||
}
|
||||
@@ -103,7 +104,7 @@ export default createReactClass({
|
||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
||||
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
||||
pillifyLinks([this._content.current], this.props.mxEvent);
|
||||
pillifyLinks([this._content.current], this.props.mxEvent, this._pills);
|
||||
HtmlUtils.linkifyElement(this._content.current);
|
||||
this.calculateUrlPreview();
|
||||
|
||||
@@ -146,6 +147,7 @@ export default createReactClass({
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
unmountPills(this._pills);
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
@@ -372,7 +374,9 @@ export default createReactClass({
|
||||
const height = window.screen.height > 800 ? 800 : window.screen.height;
|
||||
const left = (window.screen.width - width) / 2;
|
||||
const top = (window.screen.height - height) / 2;
|
||||
window.open(completeUrl, '_blank', `height=${height}, width=${width}, top=${top}, left=${left},`);
|
||||
const features = `height=${height}, width=${width}, top=${top}, left=${left},`;
|
||||
const wnd = window.open(completeUrl, '_blank', features);
|
||||
wnd.opener = null;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,12 +28,17 @@ export const PendingActionSpinner = ({text}) => {
|
||||
</div>;
|
||||
};
|
||||
|
||||
const EncryptionInfo = ({pending, member, onStartVerification}) => {
|
||||
const EncryptionInfo = ({waitingForOtherParty, waitingForNetwork, member, onStartVerification}) => {
|
||||
let content;
|
||||
if (pending) {
|
||||
const text = _t("Waiting for %(displayName)s to accept…", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
});
|
||||
if (waitingForOtherParty || waitingForNetwork) {
|
||||
let text;
|
||||
if (waitingForOtherParty) {
|
||||
text = _t("Waiting for %(displayName)s to accept…", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
});
|
||||
} else {
|
||||
text = _t("Accepting…");
|
||||
}
|
||||
content = <PendingActionSpinner text={text} />;
|
||||
} else {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
@@ -30,13 +30,32 @@ import {_t} from "../../../languageHandler";
|
||||
// cancellation codes which constitute a key mismatch
|
||||
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
|
||||
|
||||
const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
|
||||
const EncryptionPanel = ({verificationRequest, verificationRequestPromise, member, onClose, layout}) => {
|
||||
const [request, setRequest] = useState(verificationRequest);
|
||||
// state to show a spinner immediately after clicking "start verification",
|
||||
// before we have a request
|
||||
const [isRequesting, setRequesting] = useState(false);
|
||||
const [phase, setPhase] = useState(request && request.phase);
|
||||
useEffect(() => {
|
||||
setRequest(verificationRequest);
|
||||
if (verificationRequest) {
|
||||
setRequesting(false);
|
||||
setPhase(verificationRequest.phase);
|
||||
}
|
||||
}, [verificationRequest]);
|
||||
|
||||
const [phase, setPhase] = useState(request && request.phase);
|
||||
useEffect(() => {
|
||||
async function awaitPromise() {
|
||||
setRequesting(true);
|
||||
const request = await verificationRequestPromise;
|
||||
setRequesting(false);
|
||||
setRequest(request);
|
||||
setPhase(request.phase);
|
||||
}
|
||||
if (verificationRequestPromise) {
|
||||
awaitPromise();
|
||||
}
|
||||
}, [verificationRequestPromise]);
|
||||
const changeHandler = useCallback(() => {
|
||||
// handle transitions -> cancelled for mismatches which fire a modal instead of showing a card
|
||||
if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) {
|
||||
@@ -65,6 +84,7 @@ const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
|
||||
useEventEmitter(request, "change", changeHandler);
|
||||
|
||||
const onStartVerification = useCallback(async () => {
|
||||
setRequesting(true);
|
||||
const cli = MatrixClientPeg.get();
|
||||
const roomId = await ensureDMExists(cli, member.userId);
|
||||
const verificationRequest = await cli.requestVerificationDM(member.userId, roomId);
|
||||
@@ -72,9 +92,16 @@ const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
|
||||
setPhase(verificationRequest.phase);
|
||||
}, [member.userId]);
|
||||
|
||||
const requested = request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined);
|
||||
const requested =
|
||||
(!request && isRequesting) ||
|
||||
(request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined));
|
||||
if (!request || requested) {
|
||||
return <EncryptionInfo onStartVerification={onStartVerification} member={member} pending={requested} />;
|
||||
const initiatedByMe = (!request && isRequesting) || (request && request.initiatedByMe);
|
||||
return <EncryptionInfo
|
||||
onStartVerification={onStartVerification}
|
||||
member={member}
|
||||
waitingForOtherParty={requested && initiatedByMe}
|
||||
waitingForNetwork={requested && !initiatedByMe} />;
|
||||
} else {
|
||||
return (
|
||||
<VerificationPanel
|
||||
|
||||
@@ -25,7 +25,7 @@ import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import createRoom, {findDMForUser} from '../../../createRoom';
|
||||
import createRoom from '../../../createRoom';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
@@ -42,6 +42,8 @@ import {textualPowerLevel} from '../../../Roles';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
|
||||
import EncryptionPanel from "./EncryptionPanel";
|
||||
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
|
||||
import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification';
|
||||
|
||||
const _disambiguateDevices = (devices) => {
|
||||
const names = Object.create(null);
|
||||
@@ -135,54 +137,21 @@ function useIsEncrypted(cli, room) {
|
||||
return isEncrypted;
|
||||
}
|
||||
|
||||
async function verifyDevice(userId, device) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const member = cli.getUser(userId);
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog("Verification warning", "unverified session", QuestionDialog, {
|
||||
headerImage: require("../../../../res/img/e2e/warning.svg"),
|
||||
title: _t("Not Trusted"),
|
||||
description: <div>
|
||||
<p>{_t("%(name)s (%(userId)s) signed in to a new session without verifying it:", {name: member.displayName, userId})}</p>
|
||||
<p>{device.getDisplayName()} ({device.deviceId})</p>
|
||||
<p>{_t("Ask this user to verify their session, or manually verify it below.")}</p>
|
||||
</div>,
|
||||
onFinished: async (doneClicked) => {
|
||||
const manuallyVerifyClicked = !doneClicked;
|
||||
if (!manuallyVerifyClicked) {
|
||||
return;
|
||||
}
|
||||
const cli = MatrixClientPeg.get();
|
||||
const verificationRequest = await cli.requestVerification(
|
||||
userId,
|
||||
[device.deviceId],
|
||||
);
|
||||
dis.dispatch({
|
||||
action: "set_right_panel_phase",
|
||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
refireParams: {member, verificationRequest},
|
||||
});
|
||||
},
|
||||
primaryButton: _t("Done"),
|
||||
cancelButton: _t("Manually Verify"),
|
||||
});
|
||||
}
|
||||
|
||||
function verifyUser(user) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const dmRoom = findDMForUser(cli, user.userId);
|
||||
let existingRequest;
|
||||
if (dmRoom) {
|
||||
existingRequest = cli.findVerificationRequestDMInProgress(dmRoom.roomId);
|
||||
}
|
||||
dis.dispatch({
|
||||
action: "set_right_panel_phase",
|
||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
refireParams: {
|
||||
member: user,
|
||||
verificationRequest: existingRequest,
|
||||
},
|
||||
});
|
||||
function useHasCrossSigningKeys(cli, member, canVerify, setUpdating) {
|
||||
return useAsyncMemo(async () => {
|
||||
if (!canVerify) {
|
||||
return false;
|
||||
}
|
||||
setUpdating(true);
|
||||
try {
|
||||
await cli.downloadKeys([member.userId]);
|
||||
const xsi = cli.getStoredCrossSigningForUser(member.userId);
|
||||
const key = xsi && xsi.getId();
|
||||
return !!key;
|
||||
} finally {
|
||||
setUpdating(false);
|
||||
}
|
||||
}, [cli, member, canVerify], false);
|
||||
}
|
||||
|
||||
function DeviceItem({userId, device}) {
|
||||
@@ -211,7 +180,7 @@ function DeviceItem({userId, device}) {
|
||||
|
||||
const onDeviceClick = () => {
|
||||
if (!isVerified) {
|
||||
verifyDevice(userId, device);
|
||||
verifyDevice(cli.getUser(userId), device);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -916,6 +885,12 @@ const useIsSynapseAdmin = (cli) => {
|
||||
return isAdmin;
|
||||
};
|
||||
|
||||
const useHomeserverSupportsCrossSigning = (cli) => {
|
||||
return useAsyncMemo(async () => {
|
||||
return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||
}, [cli], false);
|
||||
};
|
||||
|
||||
function useRoomPermissions(cli, room, user) {
|
||||
const [roomPermissions, setRoomPermissions] = useState({
|
||||
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
|
||||
@@ -1315,16 +1290,31 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
|
||||
text = _t("Messages in this room are end-to-end encrypted.");
|
||||
}
|
||||
|
||||
const userTrust = cli.checkUserTrust(member.userId);
|
||||
const userVerified = SettingsStore.isFeatureEnabled("feature_cross_signing") ?
|
||||
userTrust.isCrossSigningVerified() :
|
||||
userTrust.isVerified();
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
|
||||
let verifyButton;
|
||||
if (isRoomEncrypted && !userVerified && !isMe) {
|
||||
const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
|
||||
|
||||
const userTrust = cli.checkUserTrust(member.userId);
|
||||
const userVerified = userTrust.isCrossSigningVerified();
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
const canVerify = SettingsStore.isFeatureEnabled("feature_cross_signing") &&
|
||||
homeserverSupportsCrossSigning &&
|
||||
isRoomEncrypted && !userVerified && !isMe;
|
||||
|
||||
const setUpdating = (updating) => {
|
||||
setPendingUpdateCount(count => count + (updating ? 1 : -1));
|
||||
};
|
||||
const hasCrossSigningKeys =
|
||||
useHasCrossSigningKeys(cli, member, canVerify, setUpdating );
|
||||
|
||||
if (canVerify) {
|
||||
verifyButton = (
|
||||
<AccessibleButton className="mx_UserInfo_field" onClick={() => verifyUser(member)}>
|
||||
<AccessibleButton className="mx_UserInfo_field" onClick={() => {
|
||||
if (hasCrossSigningKeys) {
|
||||
verifyUser(member);
|
||||
} else {
|
||||
legacyVerifyUser(member);
|
||||
}
|
||||
}}>
|
||||
{_t("Verify")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
@@ -270,6 +270,8 @@ export default class VerificationPanel extends React.PureComponent {
|
||||
};
|
||||
|
||||
_onVerifierShowSas = (sasEvent) => {
|
||||
const {request} = this.props;
|
||||
request.verifier.off('show_sas', this._onVerifierShowSas);
|
||||
this.setState({sasEvent});
|
||||
};
|
||||
|
||||
@@ -278,7 +280,7 @@ export default class VerificationPanel extends React.PureComponent {
|
||||
const hadVerifier = this._hasVerifier;
|
||||
this._hasVerifier = !!request.verifier;
|
||||
if (!hadVerifier && this._hasVerifier) {
|
||||
request.verifier.once('show_sas', this._onVerifierShowSas);
|
||||
request.verifier.on('show_sas', this._onVerifierShowSas);
|
||||
try {
|
||||
// on the requester side, this is also awaited in _startSAS,
|
||||
// but that's ok as verify should return the same promise.
|
||||
@@ -299,6 +301,10 @@ export default class VerificationPanel extends React.PureComponent {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.request.off("change", this._onRequestChange);
|
||||
const {request} = this.props;
|
||||
if (request.verifier) {
|
||||
request.verifier.off('show_sas', this._onVerifierShowSas);
|
||||
}
|
||||
request.off("change", this._onRequestChange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import Field from "../elements/Field";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import RoomPublishSetting from "./RoomPublishSetting";
|
||||
|
||||
class EditableAliasesList extends EditableItemList {
|
||||
constructor(props) {
|
||||
@@ -46,6 +47,11 @@ class EditableAliasesList extends EditableItemList {
|
||||
};
|
||||
|
||||
_renderNewItemField() {
|
||||
// if we don't need the RoomAliasField,
|
||||
// we don't need to overriden version of _renderNewItemField
|
||||
if (!this.props.domain) {
|
||||
return super._renderNewItemField();
|
||||
}
|
||||
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
|
||||
const onChange = (alias) => this._onNewItemChanged({target: {value: alias}});
|
||||
return (
|
||||
@@ -87,91 +93,64 @@ export default class AliasSettings extends React.Component {
|
||||
super(props);
|
||||
|
||||
const state = {
|
||||
domainToAliases: {}, // { domain.com => [#alias1:domain.com, #alias2:domain.com] }
|
||||
remoteDomains: [], // [ domain.com, foobar.com ]
|
||||
canonicalAlias: null, // #canonical:domain.com
|
||||
altAliases: [], // [ #alias:domain.tld, ... ]
|
||||
localAliases: [], // [ #alias:my-hs.tld, ... ]
|
||||
canonicalAlias: null, // #canonical:domain.tld
|
||||
updatingCanonicalAlias: false,
|
||||
localAliasesLoading: true,
|
||||
localAliasesLoading: false,
|
||||
detailsOpen: false,
|
||||
};
|
||||
|
||||
if (props.canonicalAliasEvent) {
|
||||
state.canonicalAlias = props.canonicalAliasEvent.getContent().alias;
|
||||
const content = props.canonicalAliasEvent.getContent();
|
||||
const altAliases = content.alt_aliases;
|
||||
if (Array.isArray(altAliases)) {
|
||||
state.altAliases = altAliases.slice();
|
||||
}
|
||||
state.canonicalAlias = content.alias;
|
||||
}
|
||||
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
async componentWillMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
componentDidMount() {
|
||||
if (this.props.canSetCanonicalAlias) {
|
||||
// load local aliases for providing recommendations
|
||||
// for the canonical alias and alt_aliases
|
||||
this.loadLocalAliases();
|
||||
}
|
||||
}
|
||||
|
||||
async loadLocalAliases() {
|
||||
this.setState({ localAliasesLoading: true });
|
||||
try {
|
||||
const cli = MatrixClientPeg.get();
|
||||
let localAliases = [];
|
||||
if (await cli.doesServerSupportUnstableFeature("org.matrix.msc2432")) {
|
||||
const response = await cli.unstableGetLocalAliases(this.props.roomId);
|
||||
const localAliases = response.aliases;
|
||||
const localDomain = cli.getDomain();
|
||||
const domainToAliases = Object.assign(
|
||||
{},
|
||||
// FIXME, any localhost alt_aliases will be ignored as they are overwritten by localAliases
|
||||
this.aliasesToDictionary(this._getAltAliases()),
|
||||
{[localDomain]: localAliases || []},
|
||||
);
|
||||
const remoteDomains = Object.keys(domainToAliases).filter((domain) => {
|
||||
return domain !== localDomain && domainToAliases[domain].length > 0;
|
||||
});
|
||||
this.setState({ domainToAliases, remoteDomains });
|
||||
} else {
|
||||
const state = {};
|
||||
const localDomain = cli.getDomain();
|
||||
state.domainToAliases = this.aliasEventsToDictionary(this.props.aliasEvents || []);
|
||||
state.remoteDomains = Object.keys(state.domainToAliases).filter((domain) => {
|
||||
return domain !== localDomain && state.domainToAliases[domain].length > 0;
|
||||
});
|
||||
this.setState(state);
|
||||
if (Array.isArray(response.aliases)) {
|
||||
localAliases = response.aliases;
|
||||
}
|
||||
}
|
||||
this.setState({ localAliases });
|
||||
} finally {
|
||||
this.setState({localAliasesLoading: false});
|
||||
this.setState({ localAliasesLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
aliasesToDictionary(aliases) {
|
||||
return aliases.reduce((dict, alias) => {
|
||||
const domain = alias.split(":")[1];
|
||||
dict[domain] = dict[domain] || [];
|
||||
dict[domain].push(alias);
|
||||
return dict;
|
||||
}, {});
|
||||
}
|
||||
|
||||
aliasEventsToDictionary(aliasEvents) { // m.room.alias events
|
||||
const dict = {};
|
||||
aliasEvents.forEach((event) => {
|
||||
dict[event.getStateKey()] = (
|
||||
(event.getContent().aliases || []).slice() // shallow-copy
|
||||
);
|
||||
});
|
||||
return dict;
|
||||
}
|
||||
|
||||
_getAltAliases() {
|
||||
if (this.props.canonicalAliasEvent) {
|
||||
const altAliases = this.props.canonicalAliasEvent.getContent().alt_aliases;
|
||||
if (Array.isArray(altAliases)) {
|
||||
return altAliases;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
changeCanonicalAlias(alias) {
|
||||
if (!this.props.canSetCanonicalAlias) return;
|
||||
|
||||
const oldAlias = this.state.canonicalAlias;
|
||||
this.setState({
|
||||
canonicalAlias: alias,
|
||||
updatingCanonicalAlias: true,
|
||||
});
|
||||
|
||||
const eventContent = {};
|
||||
const altAliases = this._getAltAliases();
|
||||
if (altAliases) eventContent["alt_aliases"] = altAliases;
|
||||
const eventContent = {
|
||||
alt_aliases: this.state.altAliases,
|
||||
};
|
||||
|
||||
if (alias) eventContent["alias"] = alias;
|
||||
|
||||
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.canonical_alias",
|
||||
@@ -184,6 +163,39 @@ export default class AliasSettings extends React.Component {
|
||||
"or a temporary failure occurred.",
|
||||
),
|
||||
});
|
||||
this.setState({canonicalAlias: oldAlias});
|
||||
}).finally(() => {
|
||||
this.setState({updatingCanonicalAlias: false});
|
||||
});
|
||||
}
|
||||
|
||||
changeAltAliases(altAliases) {
|
||||
if (!this.props.canSetCanonicalAlias) return;
|
||||
|
||||
this.setState({
|
||||
updatingCanonicalAlias: true,
|
||||
altAliases,
|
||||
});
|
||||
|
||||
const eventContent = {};
|
||||
|
||||
if (this.state.canonicalAlias) {
|
||||
eventContent.alias = this.state.canonicalAlias;
|
||||
}
|
||||
if (altAliases) {
|
||||
eventContent["alt_aliases"] = altAliases;
|
||||
}
|
||||
|
||||
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.canonical_alias",
|
||||
eventContent, "").catch((err) => {
|
||||
console.error(err);
|
||||
Modal.createTrackedDialog('Error updating alternative addresses', '', ErrorDialog, {
|
||||
title: _t("Error updating main address"),
|
||||
description: _t(
|
||||
"There was an error updating the room's alternative addresses. " +
|
||||
"It may not be allowed by the server or a temporary failure occurred.",
|
||||
),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({updatingCanonicalAlias: false});
|
||||
});
|
||||
@@ -200,16 +212,10 @@ export default class AliasSettings extends React.Component {
|
||||
if (!alias.includes(':')) alias += ':' + localDomain;
|
||||
|
||||
MatrixClientPeg.get().createAlias(alias, this.props.roomId).then(() => {
|
||||
const localAliases = this.state.domainToAliases[localDomain] || [];
|
||||
const domainAliases = Object.assign({}, this.state.domainToAliases);
|
||||
domainAliases[localDomain] = [...localAliases, alias];
|
||||
|
||||
this.setState({
|
||||
domainToAliases: domainAliases,
|
||||
// Reset the add field
|
||||
newAlias: "",
|
||||
localAliases: this.state.localAliases.concat(alias),
|
||||
newAlias: null,
|
||||
});
|
||||
|
||||
if (!this.state.canonicalAlias) {
|
||||
this.changeCanonicalAlias(alias);
|
||||
}
|
||||
@@ -226,38 +232,78 @@ export default class AliasSettings extends React.Component {
|
||||
};
|
||||
|
||||
onLocalAliasDeleted = (index) => {
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
const alias = this.state.domainToAliases[localDomain][index];
|
||||
|
||||
const alias = this.state.localAliases[index];
|
||||
// TODO: In future, we should probably be making sure that the alias actually belongs
|
||||
// to this room. See https://github.com/vector-im/riot-web/issues/7353
|
||||
MatrixClientPeg.get().deleteAlias(alias).then(() => {
|
||||
const localAliases = this.state.domainToAliases[localDomain].filter((a) => a !== alias);
|
||||
const domainAliases = Object.assign({}, this.state.domainToAliases);
|
||||
domainAliases[localDomain] = localAliases;
|
||||
|
||||
this.setState({domainToAliases: domainAliases});
|
||||
const localAliases = this.state.localAliases.slice();
|
||||
localAliases.splice(index, 1);
|
||||
this.setState({localAliases});
|
||||
|
||||
if (this.state.canonicalAlias === alias) {
|
||||
this.changeCanonicalAlias(null);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
Modal.createTrackedDialog('Error removing alias', '', ErrorDialog, {
|
||||
title: _t("Error removing alias"),
|
||||
description: _t(
|
||||
let description;
|
||||
if (err.errcode === "M_FORBIDDEN") {
|
||||
description = _t("You don't have permission to delete the alias.");
|
||||
} else {
|
||||
description = _t(
|
||||
"There was an error removing that alias. It may no longer exist or a temporary " +
|
||||
"error occurred.",
|
||||
),
|
||||
);
|
||||
}
|
||||
Modal.createTrackedDialog('Error removing alias', '', ErrorDialog, {
|
||||
title: _t("Error removing alias"),
|
||||
description,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onLocalAliasesToggled = (event) => {
|
||||
// expanded
|
||||
if (event.target.open) {
|
||||
// if local aliases haven't been preloaded yet at component mount
|
||||
if (!this.props.canSetCanonicalAlias && this.state.localAliases.length === 0) {
|
||||
this.loadLocalAliases();
|
||||
}
|
||||
}
|
||||
this.setState({detailsOpen: event.target.open});
|
||||
};
|
||||
|
||||
onCanonicalAliasChange = (event) => {
|
||||
this.changeCanonicalAlias(event.target.value);
|
||||
};
|
||||
|
||||
onNewAltAliasChanged = (value) => {
|
||||
this.setState({newAltAlias: value});
|
||||
}
|
||||
|
||||
onAltAliasAdded = (alias) => {
|
||||
const altAliases = this.state.altAliases.slice();
|
||||
if (!altAliases.some(a => a.trim() === alias.trim())) {
|
||||
altAliases.push(alias.trim());
|
||||
this.changeAltAliases(altAliases);
|
||||
this.setState({newAltAlias: ""});
|
||||
}
|
||||
}
|
||||
|
||||
onAltAliasDeleted = (index) => {
|
||||
const altAliases = this.state.altAliases.slice();
|
||||
altAliases.splice(index, 1);
|
||||
this.changeAltAliases(altAliases);
|
||||
}
|
||||
|
||||
_getAliases() {
|
||||
return this.state.altAliases.concat(this._getLocalNonAltAliases());
|
||||
}
|
||||
|
||||
_getLocalNonAltAliases() {
|
||||
const {altAliases} = this.state;
|
||||
return this.state.localAliases.filter(alias => !altAliases.includes(alias));
|
||||
}
|
||||
|
||||
render() {
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
@@ -269,15 +315,13 @@ export default class AliasSettings extends React.Component {
|
||||
element='select' id='canonicalAlias' label={_t('Main address')}>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
Object.keys(this.state.domainToAliases).map((domain, i) => {
|
||||
return this.state.domainToAliases[domain].map((alias, j) => {
|
||||
if (alias === this.state.canonicalAlias) found = true;
|
||||
return (
|
||||
<option value={alias} key={i + "_" + j}>
|
||||
{ alias }
|
||||
</option>
|
||||
);
|
||||
});
|
||||
this._getAliases().map((alias, i) => {
|
||||
if (alias === this.state.canonicalAlias) found = true;
|
||||
return (
|
||||
<option value={alias} key={i}>
|
||||
{ alias }
|
||||
</option>
|
||||
);
|
||||
})
|
||||
}
|
||||
{
|
||||
@@ -289,53 +333,60 @@ export default class AliasSettings extends React.Component {
|
||||
</Field>
|
||||
);
|
||||
|
||||
let remoteAliasesSection;
|
||||
if (this.state.remoteDomains.length) {
|
||||
remoteAliasesSection = (
|
||||
<div>
|
||||
<div>
|
||||
{ _t("Remote addresses for this room:") }
|
||||
</div>
|
||||
<ul>
|
||||
{ this.state.remoteDomains.map((domain, i) => {
|
||||
return this.state.domainToAliases[domain].map((alias, j) => {
|
||||
return <li key={i + "_" + j}>{alias}</li>;
|
||||
});
|
||||
}) }
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let localAliasesList;
|
||||
if (this.state.localAliasesLoading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
localAliasesList = <Spinner />;
|
||||
} else {
|
||||
localAliasesList = <EditableAliasesList
|
||||
localAliasesList = (<EditableAliasesList
|
||||
id="roomAliases"
|
||||
className={"mx_RoomSettings_localAliases"}
|
||||
items={this.state.domainToAliases[localDomain] || []}
|
||||
items={this.state.localAliases}
|
||||
newItem={this.state.newAlias}
|
||||
onNewItemChanged={this.onNewAliasChanged}
|
||||
canRemove={this.props.canSetAliases}
|
||||
canEdit={this.props.canSetAliases}
|
||||
onItemAdded={this.onLocalAliasAdded}
|
||||
onItemRemoved={this.onLocalAliasDeleted}
|
||||
itemsLabel={_t('Local addresses for this room:')}
|
||||
noItemsLabel={_t('This room has no local addresses')}
|
||||
placeholder={_t(
|
||||
'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
|
||||
)}
|
||||
placeholder={_t('Local address')}
|
||||
domain={localDomain}
|
||||
/>;
|
||||
/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx_AliasSettings'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Published Addresses")}</span>
|
||||
<p>{_t("Published addresses can be used by anyone on any server to join your room. " +
|
||||
"To publish an address, it needs to be set as a local address first.")}</p>
|
||||
{canonicalAliasSection}
|
||||
{localAliasesList}
|
||||
{remoteAliasesSection}
|
||||
<RoomPublishSetting roomId={this.props.roomId} canSetCanonicalAlias={this.props.canSetCanonicalAlias} />
|
||||
<datalist id="mx_AliasSettings_altRecommendations">
|
||||
{this._getLocalNonAltAliases().map(alias => {
|
||||
return <option value={alias} key={alias} />;
|
||||
})};
|
||||
</datalist>
|
||||
<EditableAliasesList
|
||||
id="roomAltAliases"
|
||||
className={"mx_RoomSettings_altAliases"}
|
||||
items={this.state.altAliases}
|
||||
newItem={this.state.newAltAlias}
|
||||
onNewItemChanged={this.onNewAltAliasChanged}
|
||||
canRemove={this.props.canSetCanonicalAlias}
|
||||
canEdit={this.props.canSetCanonicalAlias}
|
||||
onItemAdded={this.onAltAliasAdded}
|
||||
onItemRemoved={this.onAltAliasDeleted}
|
||||
suggestionsListId="mx_AliasSettings_altRecommendations"
|
||||
itemsLabel={_t('Other published addresses:')}
|
||||
noItemsLabel={_t('No other published addresses yet, add one below')}
|
||||
placeholder={_t('New published address (e.g. #alias:server)')}
|
||||
/>
|
||||
<span className='mx_SettingsTab_subheading mx_AliasSettings_localAliasHeader'>{_t("Local Addresses")}</span>
|
||||
<p>{_t("Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", {localDomain})}</p>
|
||||
<details onToggle={this.onLocalAliasesToggled}>
|
||||
<summary>{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}</summary>
|
||||
{localAliasesList}
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
60
src/components/views/room_settings/RoomPublishSetting.js
Normal file
60
src/components/views/room_settings/RoomPublishSetting.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
||||
export default class RoomPublishSetting extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {isRoomPublished: false};
|
||||
}
|
||||
|
||||
onRoomPublishChange = (e) => {
|
||||
const valueBefore = this.state.isRoomPublished;
|
||||
const newValue = !valueBefore;
|
||||
this.setState({isRoomPublished: newValue});
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
client.setRoomDirectoryVisibility(
|
||||
this.props.roomId,
|
||||
newValue ? 'public' : 'private',
|
||||
).catch(() => {
|
||||
// Roll back the local echo on the change
|
||||
this.setState({isRoomPublished: valueBefore});
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const client = MatrixClientPeg.get();
|
||||
client.getRoomDirectoryVisibility(this.props.roomId).then((result => {
|
||||
this.setState({isRoomPublished: result.visibility === 'public'});
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
return (<LabelledToggleSwitch value={this.state.isRoomPublished}
|
||||
onChange={this.onRoomPublishChange}
|
||||
disabled={!this.props.canSetCanonicalAlias}
|
||||
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
||||
domain: client.getDomain(),
|
||||
})} />);
|
||||
}
|
||||
}
|
||||
@@ -219,7 +219,7 @@ export default createReactClass({
|
||||
|
||||
if (link) {
|
||||
span = (
|
||||
<a href={link} target="_blank" rel="noopener">
|
||||
<a href={link} target="_blank" rel="noreferrer noopener">
|
||||
{ span }
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -393,6 +393,20 @@ export default class BasicMessageEditor extends React.Component {
|
||||
} else if (event.key === Key.ENTER && (event.shiftKey || (IS_MAC && event.altKey))) {
|
||||
this._insertText("\n");
|
||||
handled = true;
|
||||
// move selection to start of composer
|
||||
} else if (modKey && event.key === Key.HOME && !event.shiftKey) {
|
||||
setSelection(this._editorRef, model, {
|
||||
index: 0,
|
||||
offset: 0,
|
||||
});
|
||||
handled = true;
|
||||
// move selection to end of composer
|
||||
} else if (modKey && event.key === Key.END && !event.shiftKey) {
|
||||
setSelection(this._editorRef, model, {
|
||||
index: model.parts.length - 1,
|
||||
offset: model.parts[model.parts.length - 1].text.length,
|
||||
});
|
||||
handled = true;
|
||||
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
|
||||
} else {
|
||||
const metaOrAltPressed = event.metaKey || event.altKey;
|
||||
@@ -458,10 +472,14 @@ export default class BasicMessageEditor extends React.Component {
|
||||
const addedLen = range.replace([partCreator.pillCandidate(range.text)]);
|
||||
return model.positionForOffset(caret.offset + addedLen, true);
|
||||
});
|
||||
await model.autoComplete.onTab();
|
||||
if (!model.autoComplete.hasSelection()) {
|
||||
this.setState({showVisualBell: true});
|
||||
model.autoComplete.close();
|
||||
|
||||
// Don't try to do things with the autocomplete if there is none shown
|
||||
if (model.autoComplete) {
|
||||
await model.autoComplete.onTab();
|
||||
if (!model.autoComplete.hasSelection()) {
|
||||
this.setState({showVisualBell: true});
|
||||
model.autoComplete.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -491,6 +509,7 @@ export default class BasicMessageEditor extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("selectionchange", this._onSelectionChange);
|
||||
this._editorRef.removeEventListener("input", this._onInput, true);
|
||||
this._editorRef.removeEventListener("compositionstart", this._onCompositionStart, true);
|
||||
this._editorRef.removeEventListener("compositionend", this._onCompositionEnd, true);
|
||||
@@ -545,6 +564,7 @@ export default class BasicMessageEditor extends React.Component {
|
||||
return;
|
||||
}
|
||||
this.historyManager.ensureLastChangesPushed(this.props.model);
|
||||
this._modifiedFlag = true;
|
||||
switch (action) {
|
||||
case "bold":
|
||||
toggleInlineFormat(range, "**");
|
||||
|
||||
@@ -175,7 +175,8 @@ const EntityTile = createReactClass({
|
||||
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
||||
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
||||
const av = this.props.avatarJsx ||
|
||||
<BaseAvatar name={this.props.name} width={36} height={36} aria-hidden="true" />;
|
||||
|
||||
// The wrapping div is required to make the magic mouse listener work, for some reason.
|
||||
return (
|
||||
|
||||
@@ -48,8 +48,6 @@ const eventTileTypes = {
|
||||
|
||||
const stateEventTileTypes = {
|
||||
'm.room.encryption': 'messages.EncryptionEvent',
|
||||
'm.room.aliases': 'messages.TextualEvent',
|
||||
// 'm.room.aliases': 'messages.RoomAliasesEvent', // too complex
|
||||
'm.room.canonical_alias': 'messages.TextualEvent',
|
||||
'm.room.create': 'messages.RoomCreate',
|
||||
'm.room.member': 'messages.TextualEvent',
|
||||
@@ -100,6 +98,17 @@ export function getHandlerTile(ev) {
|
||||
}
|
||||
}
|
||||
|
||||
// sometimes MKeyVerificationConclusion declines to render. Jankily decline to render and
|
||||
// fall back to showing hidden events, if we're viewing hidden events
|
||||
// XXX: This is extremely a hack. Possibly these components should have an interface for
|
||||
// declining to render?
|
||||
if (type === "m.key.verification.cancel" && SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||
const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion");
|
||||
if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ export default class InviteOnlyIcon extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const classes = this.props.collapsedPanel ? "mx_InviteOnlyIcon_small": "mx_InviteOnlyIcon_large";
|
||||
|
||||
if (!SettingsStore.isFeatureEnabled("feature_invite_only_padlocks")) {
|
||||
return null;
|
||||
}
|
||||
@@ -46,7 +48,7 @@ export default class InviteOnlyIcon extends React.Component {
|
||||
if (this.state.hover) {
|
||||
tooltip = <Tooltip className="mx_InviteOnlyIcon_tooltip" label={_t("Invite only")} dir="auto" />;
|
||||
}
|
||||
return (<div className="mx_InviteOnlyIcon"
|
||||
return (<div className={classes}
|
||||
onMouseEnter={this.onHoverStart}
|
||||
onMouseLeave={this.onHoverEnd}
|
||||
>
|
||||
|
||||
@@ -136,7 +136,7 @@ export default createReactClass({
|
||||
<div className="mx_LinkPreviewWidget" >
|
||||
{ img }
|
||||
<div className="mx_LinkPreviewWidget_caption">
|
||||
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noopener">{ p["og:title"] }</a></div>
|
||||
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a></div>
|
||||
<div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
|
||||
<div className="mx_LinkPreviewWidget_description" ref={this._description}>
|
||||
{ description }
|
||||
|
||||
@@ -208,7 +208,7 @@ export default createReactClass({
|
||||
}
|
||||
|
||||
const av = (
|
||||
<MemberAvatar member={member} width={36} height={36} />
|
||||
<MemberAvatar member={member} width={36} height={36} aria-hidden="true" />
|
||||
);
|
||||
|
||||
if (member.user) {
|
||||
|
||||
@@ -178,6 +178,7 @@ export default class MessageComposer extends React.Component {
|
||||
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
|
||||
tombstone: this._getRoomTombstone(),
|
||||
canSendMessages: this.props.room.maySendMessage(),
|
||||
showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -325,10 +326,20 @@ export default class MessageComposer extends React.Component {
|
||||
permalinkCreator={this.props.permalinkCreator} />,
|
||||
<Stickerpicker key='stickerpicker_controls_button' room={this.props.room} />,
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
callInProgress ? <HangupButton key="controls_hangup" roomId={this.props.room.roomId} /> : null,
|
||||
callInProgress ? null : <CallButton key="controls_call" roomId={this.props.room.roomId} />,
|
||||
callInProgress ? null : <VideoCallButton key="controls_videocall" roomId={this.props.room.roomId} />,
|
||||
);
|
||||
|
||||
if (this.state.showCallButtons) {
|
||||
if (callInProgress) {
|
||||
controls.push(
|
||||
<HangupButton key="controls_hangup" roomId={this.props.room.roomId} />,
|
||||
);
|
||||
} else {
|
||||
controls.push(
|
||||
<CallButton key="controls_call" roomId={this.props.room.roomId} />,
|
||||
<VideoCallButton key="controls_videocall" roomId={this.props.room.roomId} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
||||
@@ -341,7 +352,7 @@ export default class MessageComposer extends React.Component {
|
||||
</a>
|
||||
) : '';
|
||||
|
||||
controls.push(<div className="mx_MessageComposer_replaced_wrapper">
|
||||
controls.push(<div className="mx_MessageComposer_replaced_wrapper" key="room_replaced">
|
||||
<div className="mx_MessageComposer_replaced_valign">
|
||||
<img className="mx_MessageComposer_roomReplaced_icon" src={require("../../../../res/img/room_replaced.svg")} />
|
||||
<span className="mx_MessageComposer_roomReplaced_header">
|
||||
|
||||
@@ -28,7 +28,7 @@ import rate_limited_func from "../../../ratelimitedfunc";
|
||||
import * as Rooms from '../../../Rooms';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||
import RoomListStore from '../../../stores/RoomListStore';
|
||||
import RoomListStore, {TAG_DM} from '../../../stores/RoomListStore';
|
||||
import CustomRoomTagStore from '../../../stores/CustomRoomTagStore';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import RoomSubList from '../../structures/RoomSubList';
|
||||
@@ -700,13 +700,11 @@ export default createReactClass({
|
||||
list: [],
|
||||
extraTiles: this._makeGroupInviteTiles(this.props.searchFilter),
|
||||
label: _t('Community Invites'),
|
||||
order: "recent",
|
||||
isInvite: true,
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.invite'],
|
||||
label: _t('Invites'),
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.invite'),
|
||||
isInvite: true,
|
||||
},
|
||||
@@ -714,22 +712,19 @@ export default createReactClass({
|
||||
list: this.state.lists['m.favourite'],
|
||||
label: _t('Favourites'),
|
||||
tagName: "m.favourite",
|
||||
order: "manual",
|
||||
incomingCall: incomingCallIfTaggedAs('m.favourite'),
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.direct'],
|
||||
list: this.state.lists[TAG_DM],
|
||||
label: _t('Direct Messages'),
|
||||
tagName: "im.vector.fake.direct",
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
|
||||
tagName: TAG_DM,
|
||||
incomingCall: incomingCallIfTaggedAs(TAG_DM),
|
||||
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});},
|
||||
addRoomLabel: _t("Start chat"),
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.recent'],
|
||||
label: _t('Rooms'),
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'),
|
||||
onAddRoom: () => {dis.dispatch({action: 'view_create_room'});},
|
||||
},
|
||||
@@ -744,7 +739,6 @@ export default createReactClass({
|
||||
key: tagName,
|
||||
label: labelForTagName(tagName),
|
||||
tagName: tagName,
|
||||
order: "manual",
|
||||
incomingCall: incomingCallIfTaggedAs(tagName),
|
||||
};
|
||||
});
|
||||
@@ -754,13 +748,11 @@ export default createReactClass({
|
||||
list: this.state.lists['m.lowpriority'],
|
||||
label: _t('Low priority'),
|
||||
tagName: "m.lowpriority",
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('m.lowpriority'),
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.archived'],
|
||||
label: _t('Historical'),
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.archived'),
|
||||
startAsHidden: true,
|
||||
showSpinner: this.state.isLoadingLeftRooms,
|
||||
@@ -770,7 +762,6 @@ export default createReactClass({
|
||||
list: this.state.lists['m.server_notice'],
|
||||
label: _t('System Alerts'),
|
||||
tagName: "m.lowpriority",
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('m.server_notice'),
|
||||
},
|
||||
]);
|
||||
@@ -787,6 +778,9 @@ export default createReactClass({
|
||||
className="mx_RoomList"
|
||||
role="tree"
|
||||
aria-label={_t("Rooms")}
|
||||
// Firefox sometimes makes this element focusable due to
|
||||
// overflow:scroll;, so force it out of tab order.
|
||||
tabIndex="-1"
|
||||
onMouseMove={this.onMouseMove}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
|
||||
@@ -509,7 +509,7 @@ export default createReactClass({
|
||||
"<issueLink>submit a bug report</issueLink>.",
|
||||
{ errcode: this.props.error.errcode },
|
||||
{ issueLink: label => <a href="https://github.com/vector-im/riot-web/issues/new/choose"
|
||||
target="_blank" rel="noopener">{ label }</a> },
|
||||
target="_blank" rel="noreferrer noopener">{ label }</a> },
|
||||
),
|
||||
];
|
||||
break;
|
||||
|
||||
@@ -490,16 +490,16 @@ export default createReactClass({
|
||||
height="13"
|
||||
alt="dm"
|
||||
/>;
|
||||
}
|
||||
|
||||
const { room } = this.props;
|
||||
const member = room.getMember(dmUserId);
|
||||
if (
|
||||
member && member.membership === "join" && room.getJoinedMemberCount() === 2 &&
|
||||
SettingsStore.isFeatureEnabled("feature_presence_in_room_list")
|
||||
) {
|
||||
const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
|
||||
dmOnline = <UserOnlineDot userId={dmUserId} />;
|
||||
}
|
||||
const { room } = this.props;
|
||||
const member = room.getMember(dmUserId);
|
||||
if (
|
||||
member && member.membership === "join" && room.getJoinedMemberCount() === 2 &&
|
||||
SettingsStore.isFeatureEnabled("feature_presence_in_room_list")
|
||||
) {
|
||||
const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
|
||||
dmOnline = <UserOnlineDot userId={dmUserId} />;
|
||||
}
|
||||
|
||||
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
|
||||
@@ -528,7 +528,7 @@ export default createReactClass({
|
||||
let privateIcon = null;
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
if (this.state.joinRule == "invite" && !dmUserId) {
|
||||
privateIcon = <InviteOnlyIcon />;
|
||||
privateIcon = <InviteOnlyIcon collapsedPanel={this.props.collapsed} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export default createReactClass({
|
||||
|
||||
propTypes: {
|
||||
onScrollUpClick: PropTypes.func,
|
||||
onCloseClick: PropTypes.func,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
@@ -36,6 +37,10 @@ export default createReactClass({
|
||||
title={_t('Jump to first unread message.')}
|
||||
onClick={this.props.onScrollUpClick}>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_TopUnreadMessagesBar_markAsRead"
|
||||
title={_t('Mark all as read')}
|
||||
onClick={this.props.onCloseClick}>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -119,7 +119,7 @@ export default createReactClass({
|
||||
'In future this will be improved.',
|
||||
) }
|
||||
{' '}
|
||||
<a href="https://github.com/vector-im/riot-web/issues/2671" target="_blank" rel="noopener">
|
||||
<a href="https://github.com/vector-im/riot-web/issues/2671" target="_blank" rel="noreferrer noopener">
|
||||
https://github.com/vector-im/riot-web/issues/2671
|
||||
</a>
|
||||
</div>,
|
||||
|
||||
@@ -72,11 +72,14 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
||||
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
||||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||
const homeserverSupportsCrossSigning =
|
||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||
|
||||
this.setState({
|
||||
crossSigningPublicKeysOnDevice,
|
||||
crossSigningPrivateKeysInStorage,
|
||||
secretStorageKeyInAccount,
|
||||
homeserverSupportsCrossSigning,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,6 +123,7 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||
crossSigningPublicKeysOnDevice,
|
||||
crossSigningPrivateKeysInStorage,
|
||||
secretStorageKeyInAccount,
|
||||
homeserverSupportsCrossSigning,
|
||||
} = this.state;
|
||||
|
||||
let errorSection;
|
||||
@@ -127,14 +131,19 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||
errorSection = <div className="error">{error.toString()}</div>;
|
||||
}
|
||||
|
||||
const enabled = (
|
||||
crossSigningPublicKeysOnDevice &&
|
||||
// Whether the various keys exist on your account (but not necessarily
|
||||
// on this device).
|
||||
const enabledForAccount = (
|
||||
crossSigningPrivateKeysInStorage &&
|
||||
secretStorageKeyInAccount
|
||||
);
|
||||
|
||||
let summarisedStatus;
|
||||
if (enabled) {
|
||||
if (!homeserverSupportsCrossSigning) {
|
||||
summarisedStatus = <p>{_t(
|
||||
"Your homeserver does not support cross-signing.",
|
||||
)}</p>;
|
||||
} else if (enabledForAccount && crossSigningPublicKeysOnDevice) {
|
||||
summarisedStatus = <p>✅ {_t(
|
||||
"Cross-signing and secret storage are enabled.",
|
||||
)}</p>;
|
||||
@@ -149,19 +158,28 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||
)}</p>;
|
||||
}
|
||||
|
||||
let resetButton;
|
||||
if (enabledForAccount) {
|
||||
resetButton = (
|
||||
<div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="danger" onClick={this._destroySecureSecretStorage}>
|
||||
{_t("Reset cross-signing and secret storage")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let bootstrapButton;
|
||||
if (!enabled) {
|
||||
bootstrapButton = <div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._bootstrapSecureSecretStorage}>
|
||||
{_t("Bootstrap cross-signing and secret storage")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
} else {
|
||||
bootstrapButton = <div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="danger" onClick={this._destroySecureSecretStorage}>
|
||||
{_t("Reset cross-signing and secret storage")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
if (
|
||||
(!enabledForAccount || !crossSigningPublicKeysOnDevice) &&
|
||||
homeserverSupportsCrossSigning
|
||||
) {
|
||||
bootstrapButton = (
|
||||
<div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._bootstrapSecureSecretStorage}>
|
||||
{_t("Bootstrap cross-signing and secret storage")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -182,10 +200,15 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||
<td>{_t("Secret storage public key:")}</td>
|
||||
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<tr>
|
||||
<td>{_t("Homeserver feature support:")}</td>
|
||||
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</details>
|
||||
{errorSection}
|
||||
{bootstrapButton}
|
||||
{resetButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ export default class EventIndexPanel extends React.Component {
|
||||
{},
|
||||
{
|
||||
'nativeLink': (sub) => <a href={nativeLink} target="_blank"
|
||||
rel="noopener">{sub}</a>,
|
||||
rel="noreferrer noopener">{sub}</a>,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -188,7 +188,7 @@ export default class EventIndexPanel extends React.Component {
|
||||
{},
|
||||
{
|
||||
'riotLink': (sub) => <a href="https://riot.im/download/desktop"
|
||||
target="_blank" rel="noopener">{sub}</a>,
|
||||
target="_blank" rel="noreferrer noopener">{sub}</a>,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,6 @@ export default class KeyBackupPanel extends React.PureComponent {
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
{
|
||||
secureSecretStorage: SettingsStore.isFeatureEnabled("feature_cross_signing"),
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
},
|
||||
@@ -278,25 +277,25 @@ export default class KeyBackupPanel extends React.PureComponent {
|
||||
"Backup has an <validity>invalid</validity> signature from this session",
|
||||
{}, { validity },
|
||||
);
|
||||
} else if (sig.valid && sig.device.isVerified()) {
|
||||
} else if (sig.valid && sig.deviceTrust.isVerified()) {
|
||||
sigStatus = _t(
|
||||
"Backup has a <validity>valid</validity> signature from " +
|
||||
"<verify>verified</verify> session <device></device>",
|
||||
{}, { validity, verify, device },
|
||||
);
|
||||
} else if (sig.valid && !sig.device.isVerified()) {
|
||||
} else if (sig.valid && !sig.deviceTrust.isVerified()) {
|
||||
sigStatus = _t(
|
||||
"Backup has a <validity>valid</validity> signature from " +
|
||||
"<verify>unverified</verify> session <device></device>",
|
||||
{}, { validity, verify, device },
|
||||
);
|
||||
} else if (!sig.valid && sig.device.isVerified()) {
|
||||
} else if (!sig.valid && sig.deviceTrust.isVerified()) {
|
||||
sigStatus = _t(
|
||||
"Backup has an <validity>invalid</validity> signature from " +
|
||||
"<verify>verified</verify> session <device></device>",
|
||||
{}, { validity, verify, device },
|
||||
);
|
||||
} else if (!sig.valid && !sig.device.isVerified()) {
|
||||
} else if (!sig.valid && !sig.deviceTrust.isVerified()) {
|
||||
sigStatus = _t(
|
||||
"Backup has an <validity>invalid</validity> signature from " +
|
||||
"<verify>unverified</verify> session <device></device>",
|
||||
|
||||
@@ -132,10 +132,10 @@ export default class ProfileSettings extends React.Component {
|
||||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>,
|
||||
},
|
||||
)}
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener">
|
||||
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
|
||||
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||
</a>
|
||||
</span>;
|
||||
|
||||
@@ -68,7 +68,7 @@ export default class BridgeSettingsTab extends React.Component {
|
||||
{
|
||||
// TODO: We don't have this link yet: this will prevent the translators
|
||||
// having to re-translate the string when we do.
|
||||
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noopener">{sub}</a>,
|
||||
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{sub}</a>,
|
||||
},
|
||||
)}</p>
|
||||
<ul className="mx_RoomSettingsDialog_BridgeList">
|
||||
@@ -82,7 +82,7 @@ export default class BridgeSettingsTab extends React.Component {
|
||||
{
|
||||
// TODO: We don't have this link yet: this will prevent the translators
|
||||
// having to re-translate the string when we do.
|
||||
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noopener">{sub}</a>,
|
||||
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{sub}</a>,
|
||||
},
|
||||
)}</p>;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
||||
import * as sdk from "../../../../..";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import dis from "../../../../../dispatcher";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
|
||||
export default class GeneralRoomSettingsTab extends React.Component {
|
||||
@@ -39,26 +38,6 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.context.getRoomDirectoryVisibility(this.props.roomId).then((result => {
|
||||
this.setState({isRoomPublished: result.visibility === 'public'});
|
||||
}));
|
||||
}
|
||||
|
||||
onRoomPublishChange = (e) => {
|
||||
const valueBefore = this.state.isRoomPublished;
|
||||
const newValue = !valueBefore;
|
||||
this.setState({isRoomPublished: newValue});
|
||||
|
||||
this.context.setRoomDirectoryVisibility(
|
||||
this.props.roomId,
|
||||
newValue ? 'public' : 'private',
|
||||
).catch(() => {
|
||||
// Roll back the local echo on the change
|
||||
this.setState({isRoomPublished: valueBefore});
|
||||
});
|
||||
};
|
||||
|
||||
_onLeaveClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
@@ -75,7 +54,6 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
|
||||
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
|
||||
const canActuallySetAliases = room.currentState.mayClientSendStateEvent("m.room.aliases", client);
|
||||
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
|
||||
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
|
||||
const aliasEvents = room.currentState.getStateEvents("m.room.aliases");
|
||||
@@ -90,21 +68,13 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
||||
<RoomProfileSettings roomId={this.props.roomId} />
|
||||
</div>
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Room Addresses")}</span>
|
||||
<div className="mx_SettingsTab_heading">{_t("Room Addresses")}</div>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<AliasSettings roomId={this.props.roomId}
|
||||
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
|
||||
canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
|
||||
</div>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<LabelledToggleSwitch value={this.state.isRoomPublished}
|
||||
onChange={this.onRoomPublishChange}
|
||||
disabled={!canActuallySetAliases}
|
||||
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
||||
domain: client.getDomain(),
|
||||
})} />
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_heading">{_t("Other")}</div>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<RelatedGroupSettings roomId={room.roomId}
|
||||
|
||||
@@ -97,7 +97,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
||||
{},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a rel='noopener' target='_blank'
|
||||
return <a rel='noreferrer noopener' target='_blank'
|
||||
href='https://about.riot.im/help#end-to-end-encryption'>{sub}</a>;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -61,6 +61,8 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||
emails: [],
|
||||
msisdns: [],
|
||||
...this._calculateThemeState(),
|
||||
customThemeUrl: "",
|
||||
customThemeMessage: {isError: false, text: ""},
|
||||
};
|
||||
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
@@ -274,6 +276,41 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
_onAddCustomTheme = async () => {
|
||||
let currentThemes = SettingsStore.getValue("custom_themes");
|
||||
if (!currentThemes) currentThemes = [];
|
||||
currentThemes = currentThemes.map(c => c); // cheap clone
|
||||
|
||||
if (this._themeTimer) {
|
||||
clearTimeout(this._themeTimer);
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await fetch(this.state.customThemeUrl);
|
||||
const themeInfo = await r.json();
|
||||
if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') {
|
||||
this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}});
|
||||
return;
|
||||
}
|
||||
currentThemes.push(themeInfo);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setState({customThemeMessage: {text: _t("Error downloading theme information."), isError: true}});
|
||||
return; // Don't continue on error
|
||||
}
|
||||
|
||||
await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
|
||||
this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}});
|
||||
|
||||
this._themeTimer = setTimeout(() => {
|
||||
this.setState({customThemeMessage: {text: "", isError: false}});
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
_onCustomThemeChange = (e) => {
|
||||
this.setState({customThemeUrl: e.target.value});
|
||||
};
|
||||
|
||||
_renderProfileSection() {
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
@@ -368,6 +405,45 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let customThemeForm;
|
||||
if (SettingsStore.isFeatureEnabled("feature_custom_themes")) {
|
||||
let messageElement = null;
|
||||
if (this.state.customThemeMessage.text) {
|
||||
if (this.state.customThemeMessage.isError) {
|
||||
messageElement = <div className='text-error'>{this.state.customThemeMessage.text}</div>;
|
||||
} else {
|
||||
messageElement = <div className='text-success'>{this.state.customThemeMessage.text}</div>;
|
||||
}
|
||||
}
|
||||
customThemeForm = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<form onSubmit={this._onAddCustomTheme}>
|
||||
<Field
|
||||
label={_t("Custom theme URL")}
|
||||
type='text'
|
||||
id='mx_GeneralUserSettingsTab_customThemeInput'
|
||||
autoComplete="off"
|
||||
onChange={this._onCustomThemeChange}
|
||||
value={this.state.customThemeUrl}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this._onAddCustomTheme}
|
||||
type="submit" kind="primary_sm"
|
||||
disabled={!this.state.customThemeUrl.trim()}
|
||||
>{_t("Add theme")}</AccessibleButton>
|
||||
{messageElement}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const themes = Object.entries(enumerateThemes())
|
||||
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability
|
||||
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
|
||||
const customThemes = themes.filter(p => !builtInThemes.includes(p))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const orderedThemes = [...builtInThemes, ...customThemes];
|
||||
return (
|
||||
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_themeSection">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
|
||||
@@ -376,10 +452,11 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||
value={this.state.theme} onChange={this._onThemeChange}
|
||||
disabled={this.state.useSystemTheme}
|
||||
>
|
||||
{Object.entries(enumerateThemes()).map(([theme, text]) => {
|
||||
return <option key={theme} value={theme}>{text}</option>;
|
||||
{orderedThemes.map(theme => {
|
||||
return <option key={theme.id} value={theme.id}>{theme.name}</option>;
|
||||
})}
|
||||
</Field>
|
||||
{customThemeForm}
|
||||
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,21 +25,6 @@ import Modal from "../../../../../Modal";
|
||||
import * as sdk from "../../../../../";
|
||||
import PlatformPeg from "../../../../../PlatformPeg";
|
||||
|
||||
// Simple method to help prettify GH Release Tags and Commit Hashes.
|
||||
const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
|
||||
const ghVersionLabel = function(repo, token='') {
|
||||
const match = token.match(semVerRegex);
|
||||
let url;
|
||||
if (match && match[1]) { // basic semVer string possibly with commit hash
|
||||
url = (match.length > 1 && match[2])
|
||||
? `https://github.com/${repo}/commit/${match[2]}`
|
||||
: `https://github.com/${repo}/releases/tag/v${match[1]}`;
|
||||
} else {
|
||||
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
|
||||
}
|
||||
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
|
||||
};
|
||||
|
||||
export default class HelpUserSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
closeSettingsFn: PropTypes.func.isRequired,
|
||||
@@ -110,7 +95,7 @@ export default class HelpUserSettingsTab extends React.Component {
|
||||
const legalLinks = [];
|
||||
for (const tocEntry of SdkConfig.get().terms_and_conditions_links) {
|
||||
legalLinks.push(<div key={tocEntry.url}>
|
||||
<a href={tocEntry.url} rel="noopener" target="_blank">{tocEntry.text}</a>
|
||||
<a href={tocEntry.url} rel="noreferrer noopener" target="_blank">{tocEntry.text}</a>
|
||||
</div>);
|
||||
}
|
||||
|
||||
@@ -132,27 +117,27 @@ export default class HelpUserSettingsTab extends React.Component {
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Credits")}</span>
|
||||
<ul>
|
||||
<li>
|
||||
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noopener" target="_blank">
|
||||
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noreferrer noopener" target="_blank">
|
||||
default cover photo</a> is ©
|
||||
<a href="https://www.flickr.com/golan" rel="noopener" target="_blank">Jesús Roncero</a>{' '}
|
||||
<a href="https://www.flickr.com/golan" rel="noreferrer noopener" target="_blank">Jesús Roncero</a>{' '}
|
||||
used under the terms of
|
||||
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noopener" target="_blank">
|
||||
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noreferrer noopener" target="_blank">
|
||||
CC-BY-SA 4.0</a>.
|
||||
</li>
|
||||
<li>
|
||||
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noopener" target="_blank">
|
||||
twemoji-colr</a> font is ©
|
||||
<a href="https://mozilla.org" rel="noopener" target="_blank">Mozilla Foundation</a>{' '}
|
||||
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noreferrer noopener"
|
||||
target="_blank"> twemoji-colr</a> font is ©
|
||||
<a href="https://mozilla.org" rel="noreferrer noopener" target="_blank">Mozilla Foundation</a>{' '}
|
||||
used under the terms of
|
||||
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noopener" target="_blank">
|
||||
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">
|
||||
Apache 2.0</a>.
|
||||
</li>
|
||||
<li>
|
||||
The <a href="https://twemoji.twitter.com/" rel="noopener" target="_blank">
|
||||
The <a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">
|
||||
Twemoji</a> emoji art is ©
|
||||
<a href="https://twemoji.twitter.com/" rel="noopener" target="_blank">Twitter, Inc and other
|
||||
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">Twitter, Inc and other
|
||||
contributors</a> used under the terms of
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noopener" target="_blank">
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noreferrer noopener" target="_blank">
|
||||
CC-BY 4.0</a>.
|
||||
</li>
|
||||
</ul>
|
||||
@@ -162,7 +147,8 @@ export default class HelpUserSettingsTab extends React.Component {
|
||||
|
||||
render() {
|
||||
let faqText = _t('For help with using Riot, click <a>here</a>.', {}, {
|
||||
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noopener' target='_blank'>{sub}</a>,
|
||||
'a': (sub) =>
|
||||
<a href="https://about.riot.im/need-help/" rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
});
|
||||
if (SdkConfig.get().welcomeUserId && getCurrentLanguage().startsWith('en')) {
|
||||
faqText = (
|
||||
@@ -170,7 +156,7 @@ export default class HelpUserSettingsTab extends React.Component {
|
||||
{
|
||||
_t('For help with using Riot, click <a>here</a> or start a chat with our ' +
|
||||
'bot using the button below.', {}, {
|
||||
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noopener'
|
||||
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noreferrer noopener'
|
||||
target='_blank'>{sub}</a>,
|
||||
})
|
||||
}
|
||||
@@ -183,9 +169,7 @@ export default class HelpUserSettingsTab extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
const vectorVersion = this.state.vectorVersion
|
||||
? ghVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
||||
: 'unknown';
|
||||
const vectorVersion = this.state.vectorVersion || 'unknown';
|
||||
|
||||
let olmVersion = MatrixClientPeg.get().olmVersion;
|
||||
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
|
||||
@@ -224,6 +208,15 @@ export default class HelpUserSettingsTab extends React.Component {
|
||||
{_t("Clear cache and reload")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{
|
||||
_t( "To report a Matrix-related security issue, please read the Matrix.org " +
|
||||
"<a>Security Disclosure Policy</a>.", {},
|
||||
{
|
||||
'a': (sub) =>
|
||||
<a href="https://matrix.org/security-disclosure-policy/"
|
||||
rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
|
||||
@@ -55,7 +55,7 @@ export default class LabsUserSettingsTab extends React.Component {
|
||||
'<a>Learn more</a>.', {}, {
|
||||
'a': (sub) => {
|
||||
return <a href="https://github.com/vector-im/riot-web/blob/develop/docs/labs.md"
|
||||
rel='noopener' target='_blank'>{sub}</a>;
|
||||
rel='noreferrer noopener' target='_blank'>{sub}</a>;
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ import * as sdk from "../../../../..";
|
||||
import PlatformPeg from "../../../../../PlatformPeg";
|
||||
|
||||
export default class PreferencesUserSettingsTab extends React.Component {
|
||||
static ROOM_LIST_SETTINGS = [
|
||||
'RoomList.orderAlphabetically',
|
||||
'RoomList.orderByImportance',
|
||||
'breadcrumbs',
|
||||
];
|
||||
|
||||
static COMPOSER_SETTINGS = [
|
||||
'MessageComposerInput.autoReplaceEmoji',
|
||||
'MessageComposerInput.suggestEmoji',
|
||||
@@ -47,11 +53,6 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
||||
'showImages',
|
||||
];
|
||||
|
||||
static ROOM_LIST_SETTINGS = [
|
||||
'RoomList.orderByImportance',
|
||||
'breadcrumbs',
|
||||
];
|
||||
|
||||
static ADVANCED_SETTINGS = [
|
||||
'alwaysShowEncryptionIcons',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
@@ -173,15 +174,21 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
||||
<div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
|
||||
{this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
|
||||
{this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
|
||||
{this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
|
||||
{this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
|
||||
{this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
|
||||
{this._renderGroup(PreferencesUserSettingsTab.ADVANCED_SETTINGS)}
|
||||
{minimizeToTrayOption}
|
||||
|
||||
@@ -77,7 +77,7 @@ export default class InlineTermsAgreement extends React.Component {
|
||||
"Accept <policyLink /> to continue:", {}, {
|
||||
policyLink: () => {
|
||||
return (
|
||||
<a href={policy.url} rel='noopener' target='_blank'>
|
||||
<a href={policy.url} rel='noreferrer noopener' target='_blank'>
|
||||
{policy.name}
|
||||
<span className='mx_InlineTermsAgreement_link' />
|
||||
</a>
|
||||
|
||||
@@ -42,6 +42,12 @@ export default class UnverifiedSessionToast extends React.PureComponent {
|
||||
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
|
||||
userId: MatrixClientPeg.get().getUserId(),
|
||||
device,
|
||||
onFinished: (r) => {
|
||||
if (!r) {
|
||||
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
|
||||
this._onLaterClick();
|
||||
}
|
||||
},
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
};
|
||||
|
||||
|
||||
@@ -78,7 +78,6 @@ export default class VerificationRequestToast extends React.PureComponent {
|
||||
// no room id for to_device requests
|
||||
const cli = MatrixClientPeg.get();
|
||||
try {
|
||||
await request.accept();
|
||||
if (request.channel.roomId) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
@@ -99,6 +98,7 @@ export default class VerificationRequestToast extends React.PureComponent {
|
||||
verificationRequest: request,
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
}
|
||||
await request.accept();
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ export default class VerificationShowSas extends React.Component {
|
||||
this.props.onDone();
|
||||
};
|
||||
|
||||
onDontMatchClick = () => {
|
||||
this.setState({ cancelling: true });
|
||||
this.props.onCancel();
|
||||
};
|
||||
|
||||
render() {
|
||||
let sasDisplay;
|
||||
let sasCaption;
|
||||
@@ -98,9 +103,14 @@ export default class VerificationShowSas extends React.Component {
|
||||
}
|
||||
|
||||
let confirm;
|
||||
if (this.state.pending) {
|
||||
const {displayName} = this.props;
|
||||
const text = _t("Waiting for %(displayName)s to verify…", {displayName});
|
||||
if (this.state.pending || this.state.cancelling) {
|
||||
let text;
|
||||
if (this.state.pending) {
|
||||
const {displayName} = this.props;
|
||||
text = _t("Waiting for %(displayName)s to verify…", {displayName});
|
||||
} else {
|
||||
text = _t("Cancelling…");
|
||||
}
|
||||
confirm = <PendingActionSpinner text={text} />;
|
||||
} else {
|
||||
// FIXME: stop using DialogButtons here once this component is only used in the right panel verification
|
||||
@@ -109,7 +119,7 @@ export default class VerificationShowSas extends React.Component {
|
||||
onPrimaryButtonClick={this.onMatchClick}
|
||||
primaryButtonClass="mx_UserInfo_wideButton"
|
||||
cancelButton={_t("They don't match")}
|
||||
onCancel={this.props.onCancel}
|
||||
onCancel={this.onDontMatchClick}
|
||||
cancelButtonClass="mx_UserInfo_wideButton"
|
||||
/>;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user