Merge branch 'develop' into joriks/fix-filepanel-regression

This commit is contained in:
Jorik Schellekens 2020-05-29 19:14:19 +01:00 committed by GitHub
commit b61f1704d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 2126 additions and 2464 deletions

View file

@ -119,9 +119,12 @@
"@peculiar/webcrypto": "^1.0.22", "@peculiar/webcrypto": "^1.0.22",
"@types/classnames": "^2.2.10", "@types/classnames": "^2.2.10",
"@types/flux": "^3.1.9", "@types/flux": "^3.1.9",
"@types/lodash": "^4.14.152",
"@types/modernizr": "^3.5.3", "@types/modernizr": "^3.5.3",
"@types/node": "^12.12.41",
"@types/qrcode": "^1.3.4", "@types/qrcode": "^1.3.4",
"@types/react": "16.9", "@types/react": "^16.9",
"@types/react-dom": "^16.9.8",
"@types/zxcvbn": "^4.4.0", "@types/zxcvbn": "^4.4.0",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0", "babel-jest": "^24.9.0",

View file

@ -63,7 +63,6 @@
@import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss";
@import "./views/dialogs/_DeviceVerifyDialog.scss"; @import "./views/dialogs/_DeviceVerifyDialog.scss";
@import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss";
@import "./views/dialogs/_EncryptedEventDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_InviteDialog.scss";

View file

@ -55,6 +55,7 @@ limitations under the License.
margin-left: 5px; margin-left: 5px;
width: 20px; width: 20px;
height: 20px; height: 20px;
background-repeat: none;
} }
.mx_ShareDialog_split { .mx_ShareDialog_split {

View file

@ -67,7 +67,3 @@ limitations under the License.
.mx_MatrixToolbar_action { .mx_MatrixToolbar_action {
margin-right: 16px; margin-right: 16px;
} }
.mx_MatrixToolbar_changelog {
white-space: pre;
}

View file

@ -41,7 +41,7 @@ $irc-line-height: $font-18px;
} }
> .mx_EventTile_msgOption { > .mx_EventTile_msgOption {
order: 4; order: 5;
flex-shrink: 0; flex-shrink: 0;
} }
@ -63,6 +63,8 @@ $irc-line-height: $font-18px;
flex-direction: column; flex-direction: column;
order: 3; order: 3;
flex-grow: 1; flex-grow: 1;
flex-shrink: 1;
min-width: 0;
} }
> .mx_EventTile_avatar { > .mx_EventTile_avatar {
@ -90,12 +92,14 @@ $irc-line-height: $font-18px;
text-align: right; text-align: right;
} }
.mx_EventTile_e2eIcon { > .mx_EventTile_e2eIcon {
position: relative; position: relative;
right: unset; right: unset;
left: unset; left: unset;
top: -2px;
padding: 0; padding: 0;
order: 3;
flex-shrink: 0;
flex-grow: 0;
} }
.mx_EventTile_line { .mx_EventTile_line {
@ -113,7 +117,7 @@ $irc-line-height: $font-18px;
} }
.mx_EventTile_reply { .mx_EventTile_reply {
order: 3; order: 4;
} }
.mx_EditMessageComposer_buttons { .mx_EditMessageComposer_buttons {

View file

@ -20,7 +20,7 @@ limitations under the License.
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
height: 32px; height: 34px;
margin: 0; margin: 0;
padding: 0 8px 0 10px; padding: 0 8px 0 10px;
position: relative; position: relative;

View file

@ -17,6 +17,8 @@ limitations under the License.
import * as ModernizrStatic from "modernizr"; import * as ModernizrStatic from "modernizr";
import ContentMessages from "../ContentMessages"; import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg"; import { IMatrixClientPeg } from "../MatrixClientPeg";
import ToastStore from "../stores/ToastStore";
import DeviceListener from "../DeviceListener";
declare global { declare global {
interface Window { interface Window {
@ -27,6 +29,8 @@ declare global {
}; };
mx_ContentMessages: ContentMessages; mx_ContentMessages: ContentMessages;
mx_ToastStore: ToastStore;
mx_DeviceListener: DeviceListener;
} }
// workaround for https://github.com/microsoft/TypeScript/issues/30933 // workaround for https://github.com/microsoft/TypeScript/issues/30933

View file

@ -19,6 +19,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
import DMRoomMap from './utils/DMRoomMap'; import DMRoomMap from './utils/DMRoomMap';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember(member, width, height, resizeMethod) { export function avatarUrlForMember(member, width, height, resizeMethod) {
let url; let url;
if (member && member.getAvatarUrl) { if (member && member.getAvatarUrl) {

View file

@ -180,4 +180,35 @@ export default abstract class BasePlatform {
onKeyDown(ev: KeyboardEvent): boolean { onKeyDown(ev: KeyboardEvent): boolean {
return false; // no shortcuts implemented return false; // no shortcuts implemented
} }
/**
* Get a previously stored pickle key. The pickle key is used for
* encrypting libolm objects.
* @param {string} userId the user ID for the user that the pickle key is for.
* @param {string} userId the device ID that the pickle key is for.
* @returns {string|null} the previously stored pickle key, or null if no
* pickle key has been stored.
*/
async getPickleKey(userId: string, deviceId: string): Promise<string | null> {
return null;
}
/**
* Create and store a pickle key for encrypting libolm objects.
* @param {string} userId the user ID for the user that the pickle key is for.
* @param {string} userId the device ID that the pickle key is for.
* @returns {string|null} the pickle key, or null if the platform does not
* support storing pickle keys.
*/
async createPickleKey(userId: string, deviceId: string): Promise<string | null> {
return null;
}
/**
* Delete a previously stored pickle key from storage.
* @param {string} userId the user ID for the user that the pickle key is for.
* @param {string} userId the device ID that the pickle key is for.
*/
async destroyPickleKey(userId: string, deviceId: string): Promise<void> {
}
} }

View file

@ -14,43 +14,43 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixClientPeg } from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import SettingsStore from './settings/SettingsStore'; import SettingsStore from './settings/SettingsStore';
import * as sdk from './index'; import {
import { _t } from './languageHandler'; hideToast as hideBulkUnverifiedSessionsToast,
import ToastStore from './stores/ToastStore'; showToast as showBulkUnverifiedSessionsToast
} from "./toasts/BulkUnverifiedSessionsToast";
import {
hideToast as hideSetupEncryptionToast,
Kind as SetupKind,
Kind,
showToast as showSetupEncryptionToast
} from "./toasts/SetupEncryptionToast";
import {
hideToast as hideUnverifiedSessionsToast,
showToast as showUnverifiedSessionsToast
} from "./toasts/UnverifiedSessionToast";
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
const THIS_DEVICE_TOAST_KEY = 'setupencryption';
const OTHER_DEVICES_TOAST_KEY = 'reviewsessions';
function toastKey(deviceId) {
return "unverified_session_" + deviceId;
}
export default class DeviceListener { export default class DeviceListener {
// device IDs for which the user has dismissed the verify toast ('Later')
private dismissed = new Set<string>();
// has the user dismissed any of the various nag toasts to setup encryption on this device?
private dismissedThisDeviceToast = false;
// cache of the key backup info
private keyBackupInfo: object = null;
private keyBackupFetchedAt: number = null;
// We keep a list of our own device IDs so we can batch ones that were already
// there the last time the app launched into a single toast, but display new
// ones in their own toasts.
private ourDeviceIdsAtStart: Set<string> = null;
// The set of device IDs we're currently displaying toasts for
private displayingToastsForDeviceIds = new Set<string>();
static sharedInstance() { static sharedInstance() {
if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener(); if (!window.mx_DeviceListener) window.mx_DeviceListener = new DeviceListener();
return global.mx_DeviceListener; return window.mx_DeviceListener;
}
constructor() {
// device IDs for which the user has dismissed the verify toast ('Later')
this._dismissed = new Set();
// has the user dismissed any of the various nag toasts to setup encryption on this device?
this._dismissedThisDeviceToast = false;
// cache of the key backup info
this._keyBackupInfo = null;
this._keyBackupFetchedAt = null;
// We keep a list of our own device IDs so we can batch ones that were already
// there the last time the app launched into a single toast, but display new
// ones in their own toasts.
this._ourDeviceIdsAtStart = null;
// The set of device IDs we're currently displaying toasts for
this._displayingToastsForDeviceIds = new Set();
} }
start() { start() {
@ -74,12 +74,12 @@ export default class DeviceListener {
MatrixClientPeg.get().removeListener('accountData', this._onAccountData); MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
MatrixClientPeg.get().removeListener('sync', this._onSync); MatrixClientPeg.get().removeListener('sync', this._onSync);
} }
this._dismissed.clear(); this.dismissed.clear();
this._dismissedThisDeviceToast = false; this.dismissedThisDeviceToast = false;
this._keyBackupInfo = null; this.keyBackupInfo = null;
this._keyBackupFetchedAt = null; this.keyBackupFetchedAt = null;
this._ourDeviceIdsAtStart = null; this.ourDeviceIdsAtStart = null;
this._displayingToastsForDeviceIds = new Set(); this.displayingToastsForDeviceIds = new Set();
} }
/** /**
@ -87,29 +87,29 @@ export default class DeviceListener {
* *
* @param {String[]} deviceIds List of device IDs to dismiss notifications for * @param {String[]} deviceIds List of device IDs to dismiss notifications for
*/ */
async dismissUnverifiedSessions(deviceIds) { async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
for (const d of deviceIds) { for (const d of deviceIds) {
this._dismissed.add(d); this.dismissed.add(d);
} }
this._recheck(); this._recheck();
} }
dismissEncryptionSetup() { dismissEncryptionSetup() {
this._dismissedThisDeviceToast = true; this.dismissedThisDeviceToast = true;
this._recheck(); this._recheck();
} }
_ensureDeviceIdsAtStartPopulated() { _ensureDeviceIdsAtStartPopulated() {
if (this._ourDeviceIdsAtStart === null) { if (this.ourDeviceIdsAtStart === null) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
this._ourDeviceIdsAtStart = new Set( this.ourDeviceIdsAtStart = new Set(
cli.getStoredDevicesForUser(cli.getUserId()).map(d => d.deviceId), cli.getStoredDevicesForUser(cli.getUserId()).map(d => d.deviceId),
); );
} }
} }
_onWillUpdateDevices = async (users, initialFetch) => { _onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
// If we didn't know about *any* devices before (ie. it's fresh login), // If we didn't know about *any* devices before (ie. it's fresh login),
// then they are all pre-existing devices, so ignore this and set the // then they are all pre-existing devices, so ignore this and set the
// devicesAtStart list to the devices that we see after the fetch. // devicesAtStart list to the devices that we see after the fetch.
@ -122,17 +122,17 @@ export default class DeviceListener {
// before we download any new ones. // before we download any new ones.
} }
_onDevicesUpdated = (users) => { _onDevicesUpdated = (users: string[]) => {
if (!users.includes(MatrixClientPeg.get().getUserId())) return; if (!users.includes(MatrixClientPeg.get().getUserId())) return;
this._recheck(); this._recheck();
} }
_onDeviceVerificationChanged = (userId) => { _onDeviceVerificationChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return; if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck(); this._recheck();
} }
_onUserTrustStatusChanged = (userId, trustLevel) => { _onUserTrustStatusChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return; if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck(); this._recheck();
} }
@ -163,11 +163,11 @@ export default class DeviceListener {
// & cache the result // & cache the result
async _getKeyBackupInfo() { async _getKeyBackupInfo() {
const now = (new Date()).getTime(); const now = (new Date()).getTime();
if (!this._keyBackupInfo || this._keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) { if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
this._keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this._keyBackupFetchedAt = now; this.keyBackupFetchedAt = now;
} }
return this._keyBackupInfo; return this.keyBackupInfo;
} }
async _recheck() { async _recheck() {
@ -186,48 +186,25 @@ export default class DeviceListener {
const crossSigningReady = await cli.isCrossSigningReady(); const crossSigningReady = await cli.isCrossSigningReady();
if (this._dismissedThisDeviceToast) { if (this.dismissedThisDeviceToast || crossSigningReady) {
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); hideSetupEncryptionToast();
} else { } else {
if (!crossSigningReady) { // make sure our keys are finished downloading
// make sure our keys are finished downlaoding await cli.downloadKeys([cli.getUserId()]);
await cli.downloadKeys([cli.getUserId()]); // cross signing isn't enabled - nag to enable it
// cross signing isn't enabled - nag to enable it // There are 3 different toasts for:
// There are 3 different toasts for: if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
if (cli.getStoredCrossSigningForUser(cli.getUserId())) { // Cross-signing on account but this device doesn't trust the master key (verify this session)
// Cross-signing on account but this device doesn't trust the master key (verify this session) showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
ToastStore.sharedInstance().addOrReplaceToast({
key: THIS_DEVICE_TOAST_KEY,
title: _t("Verify this session"),
icon: "verification_warning",
props: {kind: 'verify_this_session'},
component: sdk.getComponent("toasts.SetupEncryptionToast"),
});
} else {
const backupInfo = await this._getKeyBackupInfo();
if (backupInfo) {
// No cross-signing on account but key backup available (upgrade encryption)
ToastStore.sharedInstance().addOrReplaceToast({
key: THIS_DEVICE_TOAST_KEY,
title: _t("Encryption upgrade available"),
icon: "verification_warning",
props: {kind: 'upgrade_encryption'},
component: sdk.getComponent("toasts.SetupEncryptionToast"),
});
} else {
// No cross-signing or key backup on account (set up encryption)
ToastStore.sharedInstance().addOrReplaceToast({
key: THIS_DEVICE_TOAST_KEY,
title: _t("Set up encryption"),
icon: "verification_warning",
props: {kind: 'set_up_encryption'},
component: sdk.getComponent("toasts.SetupEncryptionToast"),
});
}
}
} else { } else {
// cross-signing is ready, and we don't need to upgrade encryption const backupInfo = await this._getKeyBackupInfo();
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); if (backupInfo) {
// No cross-signing on account but key backup available (upgrade encryption)
showSetupEncryptionToast(Kind.UPGRADE_ENCRYPTION);
} else {
// No cross-signing or key backup on account (set up encryption)
showSetupEncryptionToast(Kind.SET_UP_ENCRYPTION);
}
} }
} }
@ -239,20 +216,20 @@ export default class DeviceListener {
// (technically could just be a boolean: we don't actually // (technically could just be a boolean: we don't actually
// need to remember the device IDs, but for the sake of // need to remember the device IDs, but for the sake of
// symmetry...). // symmetry...).
const oldUnverifiedDeviceIds = new Set(); const oldUnverifiedDeviceIds = new Set<string>();
// Unverified devices that have appeared since then // Unverified devices that have appeared since then
const newUnverifiedDeviceIds = new Set(); const newUnverifiedDeviceIds = new Set<string>();
// as long as cross-signing isn't ready, // as long as cross-signing isn't ready,
// you can't see or dismiss any device toasts // you can't see or dismiss any device toasts
if (crossSigningReady) { if (crossSigningReady) {
const devices = cli.getStoredDevicesForUser(cli.getUserId()); const devices = cli.getStoredDevicesForUser(cli.getUserId());
for (const device of devices) { for (const device of devices) {
if (device.deviceId == cli.deviceId) continue; if (device.deviceId === cli.deviceId) continue;
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId); const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
if (!deviceTrust.isCrossSigningVerified() && !this._dismissed.has(device.deviceId)) { if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) {
if (this._ourDeviceIdsAtStart.has(device.deviceId)) { if (this.ourDeviceIdsAtStart.has(device.deviceId)) {
oldUnverifiedDeviceIds.add(device.deviceId); oldUnverifiedDeviceIds.add(device.deviceId);
} else { } else {
newUnverifiedDeviceIds.add(device.deviceId); newUnverifiedDeviceIds.add(device.deviceId);
@ -263,38 +240,23 @@ export default class DeviceListener {
// Display or hide the batch toast for old unverified sessions // Display or hide the batch toast for old unverified sessions
if (oldUnverifiedDeviceIds.size > 0) { if (oldUnverifiedDeviceIds.size > 0) {
ToastStore.sharedInstance().addOrReplaceToast({ showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
key: OTHER_DEVICES_TOAST_KEY,
title: _t("Review where youre logged in"),
icon: "verification_warning",
priority: ToastStore.PRIORITY_LOW,
props: {
deviceIds: oldUnverifiedDeviceIds,
},
component: sdk.getComponent("toasts.BulkUnverifiedSessionsToast"),
});
} else { } else {
ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY); hideBulkUnverifiedSessionsToast();
} }
// Show toasts for new unverified devices if they aren't already there // Show toasts for new unverified devices if they aren't already there
for (const deviceId of newUnverifiedDeviceIds) { for (const deviceId of newUnverifiedDeviceIds) {
ToastStore.sharedInstance().addOrReplaceToast({ showUnverifiedSessionsToast(deviceId);
key: toastKey(deviceId),
title: _t("New login. Was this you?"),
icon: "verification_warning",
props: { deviceId },
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
});
} }
// ...and hide any we don't need any more // ...and hide any we don't need any more
for (const deviceId of this._displayingToastsForDeviceIds) { for (const deviceId of this.displayingToastsForDeviceIds) {
if (!newUnverifiedDeviceIds.has(deviceId)) { if (!newUnverifiedDeviceIds.has(deviceId)) {
ToastStore.sharedInstance().dismissToast(toastKey(deviceId)); hideUnverifiedSessionsToast(deviceId);
} }
} }
this._displayingToastsForDeviceIds = newUnverifiedDeviceIds; this.displayingToastsForDeviceIds = newUnverifiedDeviceIds;
} }
} }

View file

@ -1,158 +0,0 @@
/*
Copyright 2017 Vector Creations 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.
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 * as sdk from './index';
import Modal from './Modal';
import SettingsStore from './settings/SettingsStore';
// TODO: We can remove this once cross-signing is the only way.
// https://github.com/vector-im/riot-web/issues/11908
export default class KeyRequestHandler {
constructor(matrixClient) {
this._matrixClient = matrixClient;
// the user/device for which we currently have a dialog open
this._currentUser = null;
this._currentDevice = null;
// userId -> deviceId -> [keyRequest]
this._pendingKeyRequests = Object.create(null);
}
handleKeyRequest(keyRequest) {
// Ignore own device key requests if cross-signing lab enabled
if (SettingsStore.getValue("feature_cross_signing")) {
return;
}
const userId = keyRequest.userId;
const deviceId = keyRequest.deviceId;
const requestId = keyRequest.requestId;
if (!this._pendingKeyRequests[userId]) {
this._pendingKeyRequests[userId] = Object.create(null);
}
if (!this._pendingKeyRequests[userId][deviceId]) {
this._pendingKeyRequests[userId][deviceId] = [];
}
// check if we already have this request
const requests = this._pendingKeyRequests[userId][deviceId];
if (requests.find((r) => r.requestId === requestId)) {
console.log("Already have this key request, ignoring");
return;
}
requests.push(keyRequest);
if (this._currentUser) {
// ignore for now
console.log("Key request, but we already have a dialog open");
return;
}
this._processNextRequest();
}
handleKeyRequestCancellation(cancellation) {
// Ignore own device key requests if cross-signing lab enabled
if (SettingsStore.getValue("feature_cross_signing")) {
return;
}
// see if we can find the request in the queue
const userId = cancellation.userId;
const deviceId = cancellation.deviceId;
const requestId = cancellation.requestId;
if (userId === this._currentUser && deviceId === this._currentDevice) {
console.log(
"room key request cancellation for the user we currently have a"
+ " dialog open for",
);
// TODO: update the dialog. For now, we just ignore the
// cancellation.
return;
}
if (!this._pendingKeyRequests[userId]) {
return;
}
const requests = this._pendingKeyRequests[userId][deviceId];
if (!requests) {
return;
}
const idx = requests.findIndex((r) => r.requestId === requestId);
if (idx < 0) {
return;
}
console.log("Forgetting room key request");
requests.splice(idx, 1);
if (requests.length === 0) {
delete this._pendingKeyRequests[userId][deviceId];
if (Object.keys(this._pendingKeyRequests[userId]).length === 0) {
delete this._pendingKeyRequests[userId];
}
}
}
_processNextRequest() {
const userId = Object.keys(this._pendingKeyRequests)[0];
if (!userId) {
return;
}
const deviceId = Object.keys(this._pendingKeyRequests[userId])[0];
if (!deviceId) {
return;
}
console.log(`Starting KeyShareDialog for ${userId}:${deviceId}`);
const finished = (r) => {
this._currentUser = null;
this._currentDevice = null;
if (!this._pendingKeyRequests[userId] || !this._pendingKeyRequests[userId][deviceId]) {
// request was removed in the time the dialog was displayed
this._processNextRequest();
return;
}
if (r) {
for (const req of this._pendingKeyRequests[userId][deviceId]) {
req.share();
}
}
delete this._pendingKeyRequests[userId][deviceId];
if (Object.keys(this._pendingKeyRequests[userId]).length === 0) {
delete this._pendingKeyRequests[userId];
}
this._processNextRequest();
};
const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog");
Modal.appendTrackedDialog('Key Share', 'Process Next Request', KeyShareDialog, {
matrixClient: this._matrixClient,
userId: userId,
deviceId: deviceId,
onFinished: finished,
});
this._currentUser = userId;
this._currentDevice = deviceId;
}
}

View file

@ -298,6 +298,8 @@ async function _restoreFromLocalStorage(opts) {
return false; return false;
} }
const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId);
console.log(`Restoring session for ${userId}`); console.log(`Restoring session for ${userId}`);
await _doSetLoggedIn({ await _doSetLoggedIn({
userId: userId, userId: userId,
@ -306,6 +308,7 @@ async function _restoreFromLocalStorage(opts) {
homeserverUrl: hsUrl, homeserverUrl: hsUrl,
identityServerUrl: isUrl, identityServerUrl: isUrl,
guest: isGuest, guest: isGuest,
pickleKey: pickleKey,
}, false); }, false);
return true; return true;
} else { } else {
@ -348,9 +351,13 @@ async function _handleLoadSessionFailure(e) {
* *
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started * @returns {Promise} promise which resolves to the new MatrixClient once it has been started
*/ */
export function setLoggedIn(credentials) { export async function setLoggedIn(credentials) {
stopMatrixClient(); stopMatrixClient();
return _doSetLoggedIn(credentials, true); const pickleKey = credentials.userId && credentials.deviceId
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
: null;
return _doSetLoggedIn(Object.assign({}, credentials, {pickleKey}), true);
} }
/** /**
@ -516,7 +523,9 @@ export function logout() {
} }
_isLoggingOut = true; _isLoggingOut = true;
MatrixClientPeg.get().logout().then(onLoggedOut, const client = MatrixClientPeg.get();
PlatformPeg.get().destroyPickleKey(client.getUserId(), client.getDeviceId());
client.logout().then(onLoggedOut,
(err) => { (err) => {
// Just throwing an error here is going to be very unhelpful // Just throwing an error here is going to be very unhelpful
// if you're trying to log out because your server's down and // if you're trying to log out because your server's down and
@ -575,10 +584,12 @@ async function startMatrixClient(startSyncing=true) {
// to work). // to work).
dis.dispatch({action: 'will_start_client'}, true); dis.dispatch({action: 'will_start_client'}, true);
// reset things first just in case
TypingStore.sharedInstance().reset();
ToastStore.sharedInstance().reset();
Notifier.start(); Notifier.start();
UserActivity.sharedInstance().start(); UserActivity.sharedInstance().start();
TypingStore.sharedInstance().reset(); // just in case
ToastStore.sharedInstance().reset();
DMRoomMap.makeShared().start(); DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching(); IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start(); ActiveWidgetStore.start();

View file

@ -41,6 +41,7 @@ export interface IMatrixClientCreds {
deviceId: string, deviceId: string,
accessToken: string, accessToken: string,
guest: boolean, guest: boolean,
pickleKey?: string,
} }
// TODO: Move this to the js-sdk // TODO: Move this to the js-sdk
@ -197,9 +198,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
// The js-sdk found a crypto DB too new for it to use // The js-sdk found a crypto DB too new for it to use
const CryptoStoreTooNewDialog = const CryptoStoreTooNewDialog =
sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog"); sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog");
Modal.createDialog(CryptoStoreTooNewDialog, { Modal.createDialog(CryptoStoreTooNewDialog);
host: window.location.host,
});
} }
// this can happen for a number of reasons, the most likely being // this can happen for a number of reasons, the most likely being
// that the olm library was missing. It's not fatal. // that the olm library was missing. It's not fatal.
@ -253,6 +252,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
accessToken: creds.accessToken, accessToken: creds.accessToken,
userId: creds.userId, userId: creds.userId,
deviceId: creds.deviceId, deviceId: creds.deviceId,
pickleKey: creds.pickleKey,
timelineSupport: true, timelineSupport: true,
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false), forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'),

View file

@ -26,6 +26,9 @@ import * as sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
import {
hideToast as hideNotificationsToast,
} from "./toasts/DesktopNotificationsToast";
/* /*
* Dispatches: * Dispatches:
@ -278,12 +281,7 @@ const Notifier = {
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden); Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
// XXX: why are we dispatching this here? hideNotificationsToast();
// this is nothing to do with notifier_enabled
dis.dispatch({
action: "notifier_enabled",
value: this.isEnabled(),
});
// update the info to localStorage for persistent settings // update the info to localStorage for persistent settings
if (persistent && global.localStorage) { if (persistent && global.localStorage) {

View file

@ -84,8 +84,14 @@ export default class PasswordReset {
try { try {
await this.client.setPassword({ await this.client.setPassword({
// Note: Though this sounds like a login type for identity servers only, it
// has a dual purpose of being used for homeservers too.
type: "m.login.email.identity", type: "m.login.email.identity",
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/matrix-org/synapse/issues/5665
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds: creds, threepid_creds: creds,
threepidCreds: creds,
}, this.password); }, this.password);
} catch (err) { } catch (err) {
if (err.httpStatus === 401) { if (err.httpStatus === 401) {

View file

@ -1,206 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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 createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {Key} from "../../../Keyboard";
import * as sdk from "../../../index";
// XXX: This component is not cross-signing aware.
// https://github.com/vector-im/riot-web/issues/11752 tracks either updating this
// component or taking it out to pasture.
export default createReactClass({
displayName: 'EncryptedEventDialog',
propTypes: {
event: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {
return { device: null };
},
componentDidMount: function() {
this._unmounted = false;
const client = MatrixClientPeg.get();
// first try to load the device from our store.
//
this.refreshDevice().then((dev) => {
if (dev) {
return dev;
}
// tell the client to try to refresh the device list for this user
return client.downloadKeys([this.props.event.getSender()], true).then(() => {
return this.refreshDevice();
});
}).then((dev) => {
if (this._unmounted) {
return;
}
this.setState({ device: dev });
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
}, (err)=>{
console.log("Error downloading devices", err);
});
},
componentWillUnmount: function() {
this._unmounted = true;
const client = MatrixClientPeg.get();
if (client) {
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
}
},
refreshDevice: function() {
// Promise.resolve to handle transition from static result to promise; can be removed
// in future
return Promise.resolve(MatrixClientPeg.get().getEventSenderDeviceInfo(this.props.event));
},
onDeviceVerificationChanged: function(userId, device) {
if (userId === this.props.event.getSender()) {
this.refreshDevice().then((dev) => {
this.setState({ device: dev });
});
}
},
onKeyDown: function(e) {
if (e.key === Key.ESCAPE) {
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
}
},
_renderDeviceInfo: function() {
const device = this.state.device;
if (!device) {
return (<i>{ _t('unknown device') }</i>);
}
let verificationStatus = (<b>{ _t('NOT verified') }</b>);
if (device.isBlocked()) {
verificationStatus = (<b>{ _t('Blacklisted') }</b>);
} else if (device.isVerified()) {
verificationStatus = _t('verified');
}
return (
<table>
<tbody>
<tr>
<td>{ _t('Name') }</td>
<td>{ device.getDisplayName() }</td>
</tr>
<tr>
<td>{ _t('Device ID') }</td>
<td><code>{ device.deviceId }</code></td>
</tr>
<tr>
<td>{ _t('Verification') }</td>
<td>{ verificationStatus }</td>
</tr>
<tr>
<td>{ _t('Ed25519 fingerprint') }</td>
<td><code>{ device.getFingerprint() }</code></td>
</tr>
</tbody>
</table>
);
},
_renderEventInfo: function() {
const event = this.props.event;
return (
<table>
<tbody>
<tr>
<td>{ _t('User ID') }</td>
<td>{ event.getSender() }</td>
</tr>
<tr>
<td>{ _t('Curve25519 identity key') }</td>
<td><code>{ event.getSenderKey() || <i>{ _t('none') }</i> }</code></td>
</tr>
<tr>
<td>{ _t('Claimed Ed25519 fingerprint key') }</td>
<td><code>{ event.getKeysClaimed().ed25519 || <i>{ _t('none') }</i> }</code></td>
</tr>
<tr>
<td>{ _t('Algorithm') }</td>
<td>{ event.getWireContent().algorithm || <i>{ _t('unencrypted') }</i> }</td>
</tr>
{
event.getContent().msgtype === 'm.bad.encrypted' ? (
<tr>
<td>{ _t('Decryption error') }</td>
<td>{ event.getContent().body }</td>
</tr>
) : null
}
<tr>
<td>{ _t('Session ID') }</td>
<td><code>{ event.getWireContent().session_id || <i>{ _t('none') }</i> }</code></td>
</tr>
</tbody>
</table>
);
},
render: function() {
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
let buttons = null;
if (this.state.device) {
buttons = (
<DeviceVerifyButtons device={this.state.device}
userId={this.props.event.getSender()}
/>
);
}
return (
<div className="mx_EncryptedEventDialog" onKeyDown={this.onKeyDown}>
<div className="mx_Dialog_title">
{ _t('End-to-end encryption information') }
</div>
<div className="mx_Dialog_content">
<h4>{ _t('Event information') }</h4>
{ this._renderEventInfo() }
<h4>{ _t('Sender session information') }</h4>
{ this._renderDeviceInfo() }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}>
{ _t('OK') }
</button>
{ buttons }
</div>
</div>
);
},
});

View file

@ -201,7 +201,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
type: 'm.id.user', type: 'm.id.user',
user: MatrixClientPeg.get().getUserId(), user: MatrixClientPeg.get().getUserId(),
}, },
// https://github.com/matrix-org/synapse/issues/5665 // TODO: Remove `user` once servers support proper UIA
// See https://github.com/matrix-org/synapse/issues/5665
user: MatrixClientPeg.get().getUserId(), user: MatrixClientPeg.get().getUserId(),
password: this.state.accountPassword, password: this.state.accountPassword,
}); });

View file

@ -69,7 +69,7 @@ export default class EmojiProvider extends AutocompleteProvider {
constructor() { constructor() {
super(EMOJI_REGEX); super(EMOJI_REGEX);
this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTNAMES, {
keys: ['emoji.emoticon', 'shortname'], keys: ['emoji.emoticon', 'shortname'],
funcs: [ funcs: [
(o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases (o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases

View file

@ -45,7 +45,7 @@ interface IOptions<T extends {}> {
* @param {function[]} options.funcs List of functions that when called with the * @param {function[]} options.funcs List of functions that when called with the
* object as an arg will return a string to use as an index * object as an arg will return a string to use as an index
*/ */
export default class QueryMatcher<T> { export default class QueryMatcher<T extends Object> {
private _options: IOptions<T>; private _options: IOptions<T>;
private _keys: IOptions<T>["keys"]; private _keys: IOptions<T>["keys"];
private _funcs: Required<IOptions<T>["funcs"]>; private _funcs: Required<IOptions<T>["funcs"]>;
@ -75,7 +75,11 @@ export default class QueryMatcher<T> {
this._items = new Map(); this._items = new Map();
for (const object of objects) { for (const object of objects) {
const keyValues = _at(object, this._keys); // Need to use unsafe coerce here because the objects can have any
// type for their values. We assume that those values who's keys have
// been specified will be string. Also, we cannot infer all the
// types of the keys of the objects at compile.
const keyValues = _at<string>(<any>object, this._keys);
for (const f of this._funcs) { for (const f of this._funcs) {
keyValues.push(f(object)); keyValues.push(f(object));

View file

@ -43,6 +43,15 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy"; import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
import { DefaultTagID } from "../../stores/room-list/models"; import { DefaultTagID } from "../../stores/room-list/models";
import {
showToast as showSetPasswordToast,
hideToast as hideSetPasswordToast
} from "../../toasts/SetPasswordToast";
import {
showToast as showServerLimitToast,
hideToast as hideServerLimitToast
} from "../../toasts/ServerLimitToast";
// We need to fetch each pinned message individually (if we don't already have it) // We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity. // so each pinned message may trigger a request. Limit the number per room for sanity.
// NB. this is just for server notices rather than pinned messages in general. // NB. this is just for server notices rather than pinned messages in general.
@ -65,10 +74,6 @@ interface IProps {
initialEventPixelOffset: number; initialEventPixelOffset: number;
leftDisabled: boolean; leftDisabled: boolean;
rightDisabled: boolean; rightDisabled: boolean;
showCookieBar: boolean;
hasNewVersion: boolean;
userHasGeneratedPassword: boolean;
showNotifierToolbar: boolean;
page_type: string; page_type: string;
autoJoin: boolean; autoJoin: boolean;
thirdPartyInvite?: object; thirdPartyInvite?: object;
@ -86,10 +91,8 @@ interface IProps {
currentUserId?: string; currentUserId?: string;
currentGroupId?: string; currentGroupId?: string;
currentGroupIsNew?: boolean; currentGroupIsNew?: boolean;
version?: string;
newVersion?: string;
newVersionReleaseNotes?: string;
} }
interface IState { interface IState {
mouseDown?: { mouseDown?: {
x: number; x: number;
@ -97,8 +100,6 @@ interface IState {
}; };
syncErrorData: any; syncErrorData: any;
useCompactLayout: boolean; useCompactLayout: boolean;
serverNoticeEvents: MatrixEvent[];
userHasGeneratedPassword: boolean;
} }
/** /**
@ -141,11 +142,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
this.state = { this.state = {
mouseDown: undefined, mouseDown: undefined,
syncErrorData: undefined, syncErrorData: undefined,
userHasGeneratedPassword: false,
// use compact timeline view // use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'), useCompactLayout: SettingsStore.getValue('useCompactLayout'),
// any currently active server notice events
serverNoticeEvents: [],
}; };
// stash the MatrixClient in case we log out before we are unmounted // stash the MatrixClient in case we log out before we are unmounted
@ -182,10 +180,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
// attempt to guess when a banner was opened or closed // attempt to guess when a banner was opened or closed
if ( if (
(prevProps.showCookieBar !== this.props.showCookieBar) || (prevProps.checkingForUpdate !== this.props.checkingForUpdate)
(prevProps.hasNewVersion !== this.props.hasNewVersion) ||
(prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) ||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
) { ) {
this.props.resizeNotifier.notifyBannersChanged(); this.props.resizeNotifier.notifyBannersChanged();
} }
@ -220,9 +215,11 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
}; };
_setStateFromSessionStore = () => { _setStateFromSessionStore = () => {
this.setState({ if (this._sessionStore.getCachedPassword()) {
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), showSetPasswordToast();
}); } else {
hideSetPasswordToast();
}
}; };
_createResizer() { _createResizer() {
@ -294,6 +291,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents(); this._updateServerNoticeEvents();
} else {
this._calculateServerLimitToast(data);
} }
}; };
@ -304,11 +303,24 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
} }
}; };
_calculateServerLimitToast(syncErrorData, usageLimitEventContent?) {
const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
if (error) {
usageLimitEventContent = syncErrorData.error.data;
}
if (usageLimitEventContent) {
showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error);
} else {
hideServerLimitToast();
}
}
_updateServerNoticeEvents = async () => { _updateServerNoticeEvents = async () => {
const roomLists = RoomListStoreTempProxy.getRoomLists(); const roomLists = RoomListStoreTempProxy.getRoomLists();
if (!roomLists[DefaultTagID.ServerNotice]) return []; if (!roomLists[DefaultTagID.ServerNotice]) return [];
const pinnedEvents = []; const events = [];
for (const room of roomLists[DefaultTagID.ServerNotice]) { for (const room of roomLists[DefaultTagID.ServerNotice]) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
@ -318,12 +330,18 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
for (const eventId of pinnedEventIds) { for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
const event = timeline.getEvents().find(ev => ev.getId() === eventId); const event = timeline.getEvents().find(ev => ev.getId() === eventId);
if (event) pinnedEvents.push(event); if (event) events.push(event);
} }
} }
this.setState({
serverNoticeEvents: pinnedEvents, const usageLimitEvent = events.find((e) => {
return (
e && e.getType() === 'm.room.message' &&
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
);
}); });
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEvent && usageLimitEvent.getContent());
}; };
_onPaste = (ev) => { _onPaste = (ev) => {
@ -599,12 +617,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
const GroupView = sdk.getComponent('structures.GroupView'); const GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups'); const MyGroups = sdk.getComponent('structures.MyGroups');
const ToastContainer = sdk.getComponent('structures.ToastContainer'); const ToastContainer = sdk.getComponent('structures.ToastContainer');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
const CookieBar = sdk.getComponent('globals.CookieBar');
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar'); const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar');
let pageElement; let pageElement;
@ -648,40 +661,9 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
break; break;
} }
const usageLimitEvent = this.state.serverNoticeEvents.find((e) => {
return (
e && e.getType() === 'm.room.message' &&
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
);
});
let topBar; let topBar;
if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { if (this.props.checkingForUpdate) {
topBar = <ServerLimitBar kind='hard'
adminContact={this.state.syncErrorData.error.data.admin_contact}
limitType={this.state.syncErrorData.error.data.limit_type}
/>;
} else if (usageLimitEvent) {
topBar = <ServerLimitBar kind='soft'
adminContact={usageLimitEvent.getContent().admin_contact}
limitType={usageLimitEvent.getContent().limit_type}
/>;
} else if (this.props.showCookieBar &&
this.props.config.piwik &&
navigator.doNotTrack !== "1"
) {
const policyUrl = this.props.config.piwik.policyUrl || null;
topBar = <CookieBar policyUrl={policyUrl} />;
} else if (this.props.hasNewVersion) {
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
releaseNotes={this.props.newVersionReleaseNotes}
/>;
} else if (this.props.checkingForUpdate) {
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />; topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
} else if (this.state.userHasGeneratedPassword) {
topBar = <PasswordNagBar />;
} else if (this.props.showNotifierToolbar) {
topBar = <MatrixToolbar />;
} }
let bodyClasses = 'mx_MatrixChat'; let bodyClasses = 'mx_MatrixChat';

View file

@ -49,7 +49,6 @@ import PageTypes from '../../PageTypes';
import { getHomePageUrl } from '../../utils/pages'; import { getHomePageUrl } from '../../utils/pages';
import createRoom from "../../createRoom"; import createRoom from "../../createRoom";
import KeyRequestHandler from '../../KeyRequestHandler';
import { _t, getCurrentLanguage } from '../../languageHandler'; import { _t, getCurrentLanguage } from '../../languageHandler';
import SettingsStore, { SettingLevel } from "../../settings/SettingsStore"; import SettingsStore, { SettingLevel } from "../../settings/SettingsStore";
import ThemeController from "../../settings/controllers/ThemeController"; import ThemeController from "../../settings/controllers/ThemeController";
@ -59,8 +58,8 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
import DMRoomMap from '../../utils/DMRoomMap'; import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs'; import { countRoomsWithNotif } from '../../RoomNotifs';
import { ThemeWatcher } from "../../theme"; import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import { FontWatcher } from '../../FontWatcher'; import { FontWatcher } from '../../settings/watchers/FontWatcher';
import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer, IDeferred } from "../../utils/promise"; import { defer, IDeferred } from "../../utils/promise";
import ToastStore from "../../stores/ToastStore"; import ToastStore from "../../stores/ToastStore";
@ -68,6 +67,11 @@ import * as StorageManager from "../../utils/StorageManager";
import type LoggedInViewType from "./LoggedInView"; import type LoggedInViewType from "./LoggedInView";
import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload"; import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import {
showToast as showAnalyticsToast,
hideToast as hideAnalyticsToast
} from "../../toasts/AnalyticsToast";
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -169,12 +173,7 @@ interface IState {
leftDisabled: boolean; leftDisabled: boolean;
middleDisabled: boolean; middleDisabled: boolean;
// the right panel's disabled state is tracked in its store. // the right panel's disabled state is tracked in its store.
version?: string;
newVersion?: string;
hasNewVersion: boolean;
newVersionReleaseNotes?: string;
checkingForUpdate?: string; // updateCheckStatusEnum checkingForUpdate?: string; // updateCheckStatusEnum
showCookieBar: boolean;
// Parameters used in the registration dance with the IS // Parameters used in the registration dance with the IS
register_client_secret?: string; register_client_secret?: string;
register_session_id?: string; register_session_id?: string;
@ -184,7 +183,6 @@ interface IState {
hideToSRUsers: boolean; hideToSRUsers: boolean;
syncError?: Error; syncError?: Error;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
showNotifierToolbar: boolean;
serverConfig?: ValidatedServerConfig; serverConfig?: ValidatedServerConfig;
ready: boolean; ready: boolean;
thirdPartyInvite?: object; thirdPartyInvite?: object;
@ -228,17 +226,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
leftDisabled: false, leftDisabled: false,
middleDisabled: false, middleDisabled: false,
hasNewVersion: false,
newVersionReleaseNotes: null,
checkingForUpdate: null, checkingForUpdate: null,
showCookieBar: false,
hideToSRUsers: false, hideToSRUsers: false,
syncError: null, // If the current syncing status is ERROR, the error object, otherwise null. syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
resizeNotifier: new ResizeNotifier(), resizeNotifier: new ResizeNotifier(),
showNotifierToolbar: false,
ready: false, ready: false,
}; };
@ -339,12 +332,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
} }
if (SettingsStore.getValue("showCookieBar")) {
this.setState({
showCookieBar: true,
});
}
if (SettingsStore.getValue("analyticsOptIn")) { if (SettingsStore.getValue("analyticsOptIn")) {
Analytics.enable(); Analytics.enable();
} }
@ -686,9 +673,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
dis.dispatch({action: 'view_my_groups'}); dis.dispatch({action: 'view_my_groups'});
} }
break; break;
case 'notifier_enabled':
this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()});
break;
case 'hide_left_panel': case 'hide_left_panel':
this.setState({ this.setState({
collapseLhs: true, collapseLhs: true,
@ -736,12 +720,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'client_started': case 'client_started':
this.onClientStarted(); this.onClientStarted();
break; break;
case 'new_version':
this.onVersion(
payload.currentVersion, payload.newVersion,
payload.releaseNotes,
);
break;
case 'check_updates': case 'check_updates':
this.setState({ checkingForUpdate: payload.value }); this.setState({ checkingForUpdate: payload.value });
break; break;
@ -761,19 +739,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'accept_cookies': case 'accept_cookies':
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true); SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true);
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false); SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
hideAnalyticsToast();
this.setState({
showCookieBar: false,
});
Analytics.enable(); Analytics.enable();
break; break;
case 'reject_cookies': case 'reject_cookies':
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false); SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false);
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false); SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
hideAnalyticsToast();
this.setState({
showCookieBar: false,
});
break; break;
} }
}; };
@ -1262,6 +1234,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
StorageManager.tryPersistStorage(); StorageManager.tryPersistStorage();
if (SettingsStore.getValue("showCookieBar") && this.props.config.piwik && navigator.doNotTrack !== "1") {
showAnalyticsToast(this.props.config.piwik && this.props.config.piwik.policyUrl);
}
} }
private showScreenAfterLogin() { private showScreenAfterLogin() {
@ -1389,10 +1365,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.firstSyncComplete = true; this.firstSyncComplete = true;
this.firstSyncPromise.resolve(); this.firstSyncPromise.resolve();
if (Notifier.shouldShowToolbar()) {
showNotificationsToast();
}
dis.dispatch({action: 'focus_composer'}); dis.dispatch({action: 'focus_composer'});
this.setState({ this.setState({
ready: true, ready: true,
showNotifierToolbar: Notifier.shouldShowToolbar(),
}); });
}); });
cli.on('Call.incoming', function(call) { cli.on('Call.incoming', function(call) {
@ -1471,16 +1450,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
cli.on("Session.logged_out", () => dft.stop()); cli.on("Session.logged_out", () => dft.stop());
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err)); cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err));
// TODO: We can remove this once cross-signing is the only way.
// https://github.com/vector-im/riot-web/issues/11908
const krh = new KeyRequestHandler(cli);
cli.on("crypto.roomKeyRequest", (req) => {
krh.handleKeyRequest(req);
});
cli.on("crypto.roomKeyRequestCancellation", (req) => {
krh.handleKeyRequestCancellation(req);
});
cli.on("Room", (room) => { cli.on("Room", (room) => {
if (MatrixClientPeg.get().isCryptoEnabled()) { if (MatrixClientPeg.get().isCryptoEnabled()) {
const blacklistEnabled = SettingsStore.getValueAt( const blacklistEnabled = SettingsStore.getValueAt(
@ -1570,7 +1539,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
icon: "verification", icon: "verification",
props: {request}, props: {request},
component: sdk.getComponent("toasts.VerificationRequestToast"), component: sdk.getComponent("toasts.VerificationRequestToast"),
priority: ToastStore.PRIORITY_REALTIME, priority: 90,
}); });
} }
}); });
@ -1844,16 +1813,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.showScreen("settings"); this.showScreen("settings");
}; };
onVersion(current: string, latest: string, releaseNotes?: string) {
this.setState({
version: current,
newVersion: latest,
hasNewVersion: current !== latest,
newVersionReleaseNotes: releaseNotes,
checkingForUpdate: null,
});
}
onSendEvent(roomId: string, event: MatrixEvent) { onSendEvent(roomId: string, event: MatrixEvent) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (!cli) { if (!cli) {
@ -2048,7 +2007,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onCloseAllSettings={this.onCloseAllSettings} onCloseAllSettings={this.onCloseAllSettings}
onRegistered={this.onRegistered} onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId} currentRoomId={this.state.currentRoomId}
showCookieBar={this.state.showCookieBar}
/> />
); );
} else { } else {

View file

@ -164,7 +164,10 @@ export default createReactClass({
canReact: false, canReact: false,
canReply: false, canReply: false,
useIRCLayout: SettingsStore.getValue("feature_irc_ui"), useIRCLayout: SettingsStore.getValue("feature_irc_ui"),
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
}; };
}, },
@ -235,7 +238,8 @@ export default createReactClass({
initialEventId: RoomViewStore.getInitialEventId(), initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
forwardingEvent: RoomViewStore.getForwardingEvent(), forwardingEvent: RoomViewStore.getForwardingEvent(),
shouldPeek: RoomViewStore.shouldPeek(), // we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId), showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
}; };
@ -692,6 +696,16 @@ export default createReactClass({
}); });
} }
break; break;
case 'sync_state':
if (!this.state.matrixClientIsReady) {
this.setState({
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
}, () => {
// send another "initial" RVS update to trigger peeking if needed
this._onRoomViewStoreUpdate(true);
});
}
break;
} }
}, },
@ -1674,14 +1688,16 @@ export default createReactClass({
const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary"); const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
if (!this.state.room) { if (!this.state.room) {
const loading = this.state.roomLoading || this.state.peekLoading; const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading;
if (loading) { if (loading) {
// Assume preview loading if we don't have a ready client or a room ID (still resolving the alias)
const previewLoading = !this.state.matrixClientIsReady || !this.state.roomId || this.state.peekLoading;
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<ErrorBoundary> <ErrorBoundary>
<RoomPreviewBar <RoomPreviewBar
canPreview={false} canPreview={false}
previewLoading={this.state.peekLoading} previewLoading={previewLoading && !this.state.roomLoadError}
error={this.state.roomLoadError} error={this.state.roomLoadError}
loading={loading} loading={loading}
joining={this.state.joining} joining={this.state.joining}
@ -1706,7 +1722,8 @@ export default createReactClass({
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<ErrorBoundary> <ErrorBoundary>
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} <RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick} onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked} onRejectClick={this.onRejectThreepidInviteButtonClicked}
canPreview={false} error={this.state.roomLoadError} canPreview={false} error={this.state.roomLoadError}

View file

@ -15,13 +15,21 @@ limitations under the License.
*/ */
import * as React from "react"; import * as React from "react";
import ToastStore from "../../stores/ToastStore"; import ToastStore, {IToast} from "../../stores/ToastStore";
import classNames from "classnames"; import classNames from "classnames";
export default class ToastContainer extends React.Component { interface IState {
constructor() { toasts: IToast<any>[];
super(); countSeen: number;
this.state = {toasts: ToastStore.sharedInstance().getToasts()}; }
export default class ToastContainer extends React.Component<{}, IState> {
constructor(props, context) {
super(props, context);
this.state = {
toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(),
};
// Start listening here rather than in componentDidMount because // Start listening here rather than in componentDidMount because
// toasts may dismiss themselves in their didMount if they find // toasts may dismiss themselves in their didMount if they find
@ -35,7 +43,10 @@ export default class ToastContainer extends React.Component {
} }
_onToastStoreUpdate = () => { _onToastStoreUpdate = () => {
this.setState({toasts: ToastStore.sharedInstance().getToasts()}); this.setState({
toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(),
});
}; };
render() { render() {
@ -51,8 +62,8 @@ export default class ToastContainer extends React.Component {
}); });
let countIndicator; let countIndicator;
if (isStacked) { if (isStacked || this.state.countSeen > 0) {
countIndicator = `(1/${totalCount})`; countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
} }
const toastProps = Object.assign({}, props, { const toastProps = Object.assign({}, props, {

View file

@ -538,6 +538,7 @@ export const MsisdnAuthEntry = createReactClass({
type: MsisdnAuthEntry.LOGIN_TYPE, type: MsisdnAuthEntry.LOGIN_TYPE,
// TODO: Remove `threepid_creds` once servers support proper UIA // TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/vector-im/riot-web/issues/10312 // See https://github.com/vector-im/riot-web/issues/10312
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds: creds, threepid_creds: creds,
threepidCreds: creds, threepidCreds: creds,
}); });

View file

@ -36,7 +36,7 @@ interface IProps {
labelStrongPassword?: string; labelStrongPassword?: string;
labelAllowedButUnsafe?: string; labelAllowedButUnsafe?: string;
onChange(ev: KeyboardEvent); onChange(ev: React.FormEvent<HTMLElement>);
onValidate(result: IValidationResult); onValidate(result: IValidationResult);
} }

View file

@ -238,7 +238,7 @@ export default class PasswordLogin extends React.Component {
type="text" type="text"
label={_t("Phone")} label={_t("Phone")}
value={this.state.phoneNumber} value={this.state.phoneNumber}
prefix={phoneCountry} prefixComponent={phoneCountry}
onChange={this.onPhoneNumberChanged} onChange={this.onPhoneNumberChanged}
onBlur={this.onPhoneNumberBlur} onBlur={this.onPhoneNumberBlur}
disabled={this.props.disableSubmit} disabled={this.props.disableSubmit}

View file

@ -473,7 +473,7 @@ export default createReactClass({
type="text" type="text"
label={phoneLabel} label={phoneLabel}
value={this.state.phoneNumber} value={this.state.phoneNumber}
prefix={phoneCountry} prefixComponent={phoneCountry}
onChange={this.onPhoneNumberChange} onChange={this.onPhoneNumberChange}
onValidate={this.onPhoneNumberValidate} onValidate={this.onPhoneNumberValidate}
/>; />;

View file

@ -18,10 +18,10 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import * as Avatar from '../../../Avatar';
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
export default createReactClass({ export default createReactClass({
displayName: 'MemberAvatar', displayName: 'MemberAvatar',
@ -62,10 +62,14 @@ export default createReactClass({
return { return {
name: props.member.name, name: props.member.name,
title: props.title || props.member.userId, title: props.title || props.member.userId,
imageUrl: Avatar.avatarUrlForMember(props.member, imageUrl: props.member.getAvatarUrl(
props.width, MatrixClientPeg.get().getHomeserverUrl(),
props.height, Math.floor(props.width * window.devicePixelRatio),
props.resizeMethod), Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
false,
),
}; };
} else if (props.fallbackUserId) { } else if (props.fallbackUserId) {
return { return {

View file

@ -116,11 +116,6 @@ export default createReactClass({
this.closeMenu(); this.closeMenu();
}, },
e2eInfoClicked: function() {
this.props.e2eInfoCallback();
this.closeMenu();
},
onReportEventClick: function() { onReportEventClick: function() {
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog"); const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, { Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
@ -465,15 +460,6 @@ export default createReactClass({
); );
} }
let e2eInfo;
if (this.props.e2eInfoCallback) {
e2eInfo = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
{ _t('End-to-end encryption information') }
</MenuItem>
);
}
let reportEventButton; let reportEventButton;
if (mxEvent.getSender() !== me) { if (mxEvent.getSender() !== me) {
reportEventButton = ( reportEventButton = (
@ -500,7 +486,6 @@ export default createReactClass({
{ quoteButton } { quoteButton }
{ externalURLButton } { externalURLButton }
{ collapseReplyThread } { collapseReplyThread }
{ e2eInfo }
{ reportEventButton } { reportEventButton }
</div> </div>
); );

View file

@ -42,11 +42,9 @@ export default (props) => {
}; };
const description = const description =
_t("You've previously used a newer version of Riot on %(host)s. " + _t("You've previously used a newer version of Riot with this session. " +
"To use this version again with end to end encryption, you will " + "To use this version again with end to end encryption, you will " +
"need to sign out and back in again. ", "need to sign out and back in again.");
{host: props.host},
);
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');

View file

@ -1,178 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
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 Modal from '../../../Modal';
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
// TODO: We can remove this once cross-signing is the only way.
// https://github.com/vector-im/riot-web/issues/11908
/**
* Dialog which asks the user whether they want to share their keys with
* an unverified device.
*
* onFinished is called with `true` if the key should be shared, `false` if it
* should not, and `undefined` if the dialog is cancelled. (In other words:
* truthy: do the key share. falsy: don't share the keys).
*/
export default createReactClass({
propTypes: {
matrixClient: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
deviceId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {
return {
deviceInfo: null,
wasNewDevice: false,
};
},
componentDidMount: function() {
this._unmounted = false;
const userId = this.props.userId;
const deviceId = this.props.deviceId;
// give the client a chance to refresh the device list
this.props.matrixClient.downloadKeys([userId], false).then((r) => {
if (this._unmounted) { return; }
const deviceInfo = r[userId][deviceId];
if (!deviceInfo) {
console.warn(`No details found for session ${userId}:${deviceId}`);
this.props.onFinished(false);
return;
}
const wasNewDevice = !deviceInfo.isKnown();
this.setState({
deviceInfo: deviceInfo,
wasNewDevice: wasNewDevice,
});
// if the device was new before, it's not any more.
if (wasNewDevice) {
this.props.matrixClient.setDeviceKnown(
userId,
deviceId,
true,
);
}
});
},
componentWillUnmount: function() {
this._unmounted = true;
},
_onVerifyClicked: function() {
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
console.log("KeyShareDialog: Starting verify dialog");
Modal.createTrackedDialog('Key Share', 'Starting dialog', DeviceVerifyDialog, {
userId: this.props.userId,
device: this.state.deviceInfo,
onFinished: (verified) => {
if (verified) {
// can automatically share the keys now.
this.props.onFinished(true);
}
},
}, null, /* priority = */ false, /* static = */ true);
},
_onShareClicked: function() {
console.log("KeyShareDialog: User clicked 'share'");
this.props.onFinished(true);
},
_onIgnoreClicked: function() {
console.log("KeyShareDialog: User clicked 'ignore'");
this.props.onFinished(false);
},
_renderContent: function() {
const displayName = this.state.deviceInfo.getDisplayName() ||
this.state.deviceInfo.deviceId;
let text;
if (this.state.wasNewDevice) {
text = _td("You added a new session '%(displayName)s', which is"
+ " requesting encryption keys.");
} else {
text = _td("Your unverified session '%(displayName)s' is requesting"
+ " encryption keys.");
}
text = _t(text, {displayName: displayName});
return (
<div id='mx_Dialog_content'>
<p>{ text }</p>
<div className="mx_Dialog_buttons">
<button onClick={this._onVerifyClicked} autoFocus="true">
{ _t('Start verification') }
</button>
<button onClick={this._onShareClicked}>
{ _t('Share without verifying') }
</button>
<button onClick={this._onIgnoreClicked}>
{ _t('Ignore request') }
</button>
</div>
</div>
);
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('views.elements.Spinner');
let content;
if (this.state.deviceInfo) {
content = this._renderContent();
} else {
content = (
<div id='mx_Dialog_content'>
<p>{ _t('Loading session info...') }</p>
<Spinner />
</div>
);
}
return (
<BaseDialog className='mx_KeyShareRequestDialog'
onFinished={this.props.onFinished}
title={_t('Encryption key request')}
contentId='mx_Dialog_content'
>
{ content }
</BaseDialog>
);
},
});

View file

@ -15,10 +15,10 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import {IFieldState, IValidationResult} from "../elements/Validation";
// Invoke validation from user input (when typing, etc.) at most once every N ms. // Invoke validation from user input (when typing, etc.) at most once every N ms.
const VALIDATION_THROTTLE_MS = 200; const VALIDATION_THROTTLE_MS = 200;
@ -29,58 +29,93 @@ function getId() {
return `${BASE_ID}_${count++}`; return `${BASE_ID}_${count++}`;
} }
export default class Field extends React.PureComponent { interface IProps extends React.InputHTMLAttributes<HTMLSelectElement | HTMLInputElement> {
static propTypes = { // The field's ID, which binds the input and label together. Immutable.
// The field's ID, which binds the input and label together. Immutable. id?: string,
id: PropTypes.string, // The element to create. Defaults to "input".
// The element to create. Defaults to "input". // To define options for a select, use <Field><option ... /></Field>
// To define options for a select, use <Field><option ... /></Field> element?: "input" | "select" | "textarea",
element: PropTypes.oneOf(["input", "select", "textarea"]), // The field's type (when used as an <input>). Defaults to "text".
// The field's type (when used as an <input>). Defaults to "text". type?: string,
type: PropTypes.string, // id of a <datalist> element for suggestions
// id of a <datalist> element for suggestions list?: string,
list: PropTypes.string, // The field's label string.
// The field's label string. label?: string,
label: PropTypes.string, // The field's placeholder string. Defaults to the label.
// The field's placeholder string. Defaults to the label. placeholder?: string,
placeholder: PropTypes.string, // The field's value.
// The field's value. // This is a controlled component, so the value is required.
// This is a controlled component, so the value is required. value: string,
value: PropTypes.string.isRequired, // Optional component to include inside the field before the input.
// Optional component to include inside the field before the input. prefixComponent?: React.ReactNode,
prefix: PropTypes.node, // Optional component to include inside the field after the input.
// Optional component to include inside the field after the input. postfixComponent?: React.ReactNode,
postfix: PropTypes.node, // The callback called whenever the contents of the field
// The callback called whenever the contents of the field // changes. Returns an object with `valid` boolean field
// changes. Returns an object with `valid` boolean field // and a `feedback` react component field to provide feedback
// and a `feedback` react component field to provide feedback // to the user.
// to the user. onValidate?: (input: IFieldState) => Promise<IValidationResult>,
onValidate: PropTypes.func, // If specified, overrides the value returned by onValidate.
// If specified, overrides the value returned by onValidate. flagInvalid?: boolean,
flagInvalid: PropTypes.bool, // If specified, contents will appear as a tooltip on the element and
// If specified, contents will appear as a tooltip on the element and // validation feedback tooltips will be suppressed.
// validation feedback tooltips will be suppressed. tooltipContent?: React.ReactNode,
tooltipContent: PropTypes.node, // If specified alongside tooltipContent, the class name to apply to the
// If specified alongside tooltipContent, the class name to apply to the // tooltip itself.
// tooltip itself. tooltipClassName?: string,
tooltipClassName: PropTypes.string, // If specified, an additional class name to apply to the field container
// If specified, an additional class name to apply to the field container className?: string,
className: PropTypes.string, // All other props pass through to the <input>.
// All other props pass through to the <input>. }
};
interface IState {
valid: boolean,
feedback: React.ReactNode,
feedbackVisible: boolean,
focused: boolean,
}
export default class Field extends React.PureComponent<IProps, IState> {
private id: string;
private input: HTMLInputElement;
private static defaultProps = {
element: "input",
type: "text",
}
/*
* This was changed from throttle to debounce: this is more traditional for
* form validation since it means that the validation doesn't happen at all
* until the user stops typing for a bit (debounce defaults to not running on
* the leading edge). If we're doing an HTTP hit on each validation, we have more
* incentive to prevent validating input that's very unlikely to be valid.
* We may find that we actually want different behaviour for registration
* fields, in which case we can add some options to control it.
*/
private validateOnChange = debounce(() => {
this.validate({
focused: true,
});
}, VALIDATION_THROTTLE_MS);
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
valid: undefined, valid: undefined,
feedback: undefined, feedback: undefined,
feedbackVisible: false,
focused: false, focused: false,
}; };
this.id = this.props.id || getId(); this.id = this.props.id || getId();
} }
onFocus = (ev) => { public focus() {
this.input.focus();
}
private onFocus = (ev) => {
this.setState({ this.setState({
focused: true, focused: true,
}); });
@ -93,7 +128,7 @@ export default class Field extends React.PureComponent {
} }
}; };
onChange = (ev) => { private onChange = (ev) => {
this.validateOnChange(); this.validateOnChange();
// Parent component may have supplied its own `onChange` as well // Parent component may have supplied its own `onChange` as well
if (this.props.onChange) { if (this.props.onChange) {
@ -101,7 +136,7 @@ export default class Field extends React.PureComponent {
} }
}; };
onBlur = (ev) => { private onBlur = (ev) => {
this.setState({ this.setState({
focused: false, focused: false,
}); });
@ -114,11 +149,7 @@ export default class Field extends React.PureComponent {
} }
}; };
focus() { private async validate({ focused, allowEmpty = true }: {focused: boolean, allowEmpty?: boolean}) {
this.input.focus();
}
async validate({ focused, allowEmpty = true }) {
if (!this.props.onValidate) { if (!this.props.onValidate) {
return; return;
} }
@ -149,56 +180,42 @@ export default class Field extends React.PureComponent {
} }
} }
/*
* This was changed from throttle to debounce: this is more traditional for
* form validation since it means that the validation doesn't happen at all
* until the user stops typing for a bit (debounce defaults to not running on
* the leading edge). If we're doing an HTTP hit on each validation, we have more
* incentive to prevent validating input that's very unlikely to be valid.
* We may find that we actually want different behaviour for registration
* fields, in which case we can add some options to control it.
*/
validateOnChange = debounce(() => {
this.validate({
focused: true,
});
}, VALIDATION_THROTTLE_MS);
render() {
public render() {
const { const {
element, prefix, postfix, className, onValidate, children, element, prefixComponent, postfixComponent, className, onValidate, children,
tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props; tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props;
const inputElement = element || "input";
// Set some defaults for the <input> element // Set some defaults for the <input> element
inputProps.type = inputProps.type || "text"; const ref = input => this.input = input;
inputProps.ref = input => this.input = input;
inputProps.placeholder = inputProps.placeholder || inputProps.label; inputProps.placeholder = inputProps.placeholder || inputProps.label;
inputProps.id = this.id; // this overwrites the id from props inputProps.id = this.id; // this overwrites the id from props
inputProps.onFocus = this.onFocus; inputProps.onFocus = this.onFocus;
inputProps.onChange = this.onChange; inputProps.onChange = this.onChange;
inputProps.onBlur = this.onBlur; inputProps.onBlur = this.onBlur;
inputProps.list = list;
const fieldInput = React.createElement(inputElement, inputProps, children); // Appease typescript's inference
const inputProps_ = {...inputProps, ref, list};
const fieldInput = React.createElement(this.props.element, inputProps_, children);
let prefixContainer = null; let prefixContainer = null;
if (prefix) { if (prefixComponent) {
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>; prefixContainer = <span className="mx_Field_prefix">{prefixComponent}</span>;
} }
let postfixContainer = null; let postfixContainer = null;
if (postfix) { if (postfixComponent) {
postfixContainer = <span className="mx_Field_postfix">{postfix}</span>; postfixContainer = <span className="mx_Field_postfix">{postfixComponent}</span>;
} }
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined; const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, className, { const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, {
// If we have a prefix element, leave the label always at the top left and // If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do // don't animate it, as it looks a bit clunky and would add complexity to do
// properly. // properly.
mx_Field_labelAlwaysTopLeft: prefix, mx_Field_labelAlwaysTopLeft: prefixComponent,
mx_Field_valid: onValidate && this.state.valid === true, mx_Field_valid: onValidate && this.state.valid === true,
mx_Field_invalid: hasValidationFlag mx_Field_invalid: hasValidationFlag
? flagInvalid ? flagInvalid

View file

@ -47,8 +47,8 @@ export default class RoomAliasField extends React.PureComponent {
<Field <Field
label={_t("Room address")} label={_t("Room address")}
className="mx_RoomAliasField" className="mx_RoomAliasField"
prefix={poundSign} prefixComponent={poundSign}
postfix={domain} postfixComponent={domain}
ref={ref => this._fieldRef = ref} ref={ref => this._fieldRef = ref}
onValidate={this._onValidate} onValidate={this._onValidate}
placeholder={_t("e.g. my-room")} placeholder={_t("e.g. my-room")}

View file

@ -2,7 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -18,67 +18,68 @@ limitations under the License.
*/ */
import React from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import classNames from 'classnames'; import classNames from 'classnames';
import { ViewTooltipPayload } from '../../../dispatcher/payloads/ViewTooltipPayload';
import { Action } from '../../../dispatcher/actions';
const MIN_TOOLTIP_HEIGHT = 25; const MIN_TOOLTIP_HEIGHT = 25;
export default createReactClass({ interface IProps {
displayName: 'Tooltip',
propTypes: {
// Class applied to the element used to position the tooltip // Class applied to the element used to position the tooltip
className: PropTypes.string, className: string,
// Class applied to the tooltip itself // Class applied to the tooltip itself
tooltipClassName: PropTypes.string, tooltipClassName?: string,
// Whether the tooltip is visible or hidden. // Whether the tooltip is visible or hidden.
// The hidden state allows animating the tooltip away via CSS. // The hidden state allows animating the tooltip away via CSS.
// Defaults to visible if unset. // Defaults to visible if unset.
visible: PropTypes.bool, visible?: boolean,
// the react element to put into the tooltip // the react element to put into the tooltip
label: PropTypes.node, label: React.ReactNode,
}, }
getDefaultProps() { export default class Tooltip extends React.Component<IProps> {
return { private tooltipContainer: HTMLElement;
visible: true, private tooltip: void | Element | Component<Element, any, any>;
}; private parent: Element;
},
public static readonly defaultProps = {
visible: true,
};
// Create a wrapper for the tooltip outside the parent and attach it to the body element // Create a wrapper for the tooltip outside the parent and attach it to the body element
componentDidMount: function() { public componentDidMount() {
this.tooltipContainer = document.createElement("div"); this.tooltipContainer = document.createElement("div");
this.tooltipContainer.className = "mx_Tooltip_wrapper"; this.tooltipContainer.className = "mx_Tooltip_wrapper";
document.body.appendChild(this.tooltipContainer); document.body.appendChild(this.tooltipContainer);
window.addEventListener('scroll', this._renderTooltip, true); window.addEventListener('scroll', this.renderTooltip, true);
this.parent = ReactDOM.findDOMNode(this).parentNode; this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
this._renderTooltip(); this.renderTooltip();
}, }
componentDidUpdate: function() { public componentDidUpdate() {
this._renderTooltip(); this.renderTooltip();
}, }
// Remove the wrapper element, as the tooltip has finished using it // Remove the wrapper element, as the tooltip has finished using it
componentWillUnmount: function() { public componentWillUnmount() {
dis.dispatch({ dis.dispatch<ViewTooltipPayload>({
action: 'view_tooltip', action: Action.ViewTooltip,
tooltip: null, tooltip: null,
parent: null, parent: null,
}); });
ReactDOM.unmountComponentAtNode(this.tooltipContainer); ReactDOM.unmountComponentAtNode(this.tooltipContainer);
document.body.removeChild(this.tooltipContainer); document.body.removeChild(this.tooltipContainer);
window.removeEventListener('scroll', this._renderTooltip, true); window.removeEventListener('scroll', this.renderTooltip, true);
}, }
_updatePosition(style) { private updatePosition(style: {[key: string]: any}) {
const parentBox = this.parent.getBoundingClientRect(); const parentBox = this.parent.getBoundingClientRect();
let offset = 0; let offset = 0;
if (parentBox.height > MIN_TOOLTIP_HEIGHT) { if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
@ -91,16 +92,15 @@ export default createReactClass({
style.top = (parentBox.top - 2) + window.pageYOffset + offset; style.top = (parentBox.top - 2) + window.pageYOffset + offset;
style.left = 6 + parentBox.right + window.pageXOffset; style.left = 6 + parentBox.right + window.pageXOffset;
return style; return style;
}, }
_renderTooltip: function() { private renderTooltip() {
// Add the parent's position to the tooltips, so it's correctly // Add the parent's position to the tooltips, so it's correctly
// positioned, also taking into account any window zoom // positioned, also taking into account any window zoom
// NOTE: The additional 6 pixels for the left position, is to take account of the // NOTE: The additional 6 pixels for the left position, is to take account of the
// tooltips chevron // tooltips chevron
const parent = ReactDOM.findDOMNode(this).parentNode; const parent = ReactDOM.findDOMNode(this).parentNode as Element;
let style = {}; const style = this.updatePosition({});
style = this._updatePosition(style);
// Hide the entire container when not visible. This prevents flashing of the tooltip // Hide the entire container when not visible. This prevents flashing of the tooltip
// if it is not meant to be visible on first mount. // if it is not meant to be visible on first mount.
style.display = this.props.visible ? "block" : "none"; style.display = this.props.visible ? "block" : "none";
@ -118,21 +118,21 @@ export default createReactClass({
); );
// Render the tooltip manually, as we wish it not to be rendered within the parent // Render the tooltip manually, as we wish it not to be rendered within the parent
this.tooltip = ReactDOM.render(tooltip, this.tooltipContainer); this.tooltip = ReactDOM.render<Element>(tooltip, this.tooltipContainer);
// Tell the roomlist about us so it can manipulate us if it wishes // Tell the roomlist about us so it can manipulate us if it wishes
dis.dispatch({ dis.dispatch<ViewTooltipPayload>({
action: 'view_tooltip', action: Action.ViewTooltip,
tooltip: this.tooltip, tooltip: this.tooltip,
parent: parent, parent: parent,
}); });
}, }
render: function() { public render() {
// Render a placeholder // Render a placeholder
return ( return (
<div className={this.props.className} > <div className={this.props.className} >
</div> </div>
); );
}, }
}); }

View file

@ -1,103 +0,0 @@
/*
Copyright 2018 New Vector Ltd.
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 dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import Analytics from '../../../Analytics';
export default class CookieBar extends React.Component {
static propTypes = {
policyUrl: PropTypes.string,
}
constructor() {
super();
}
onUsageDataClicked(e) {
e.stopPropagation();
e.preventDefault();
Analytics.showDetailsModal();
}
onAccept() {
dis.dispatch({
action: 'accept_cookies',
});
}
onReject() {
dis.dispatch({
action: 'reject_cookies',
});
}
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const toolbarClasses = "mx_MatrixToolbar";
return (
<div className={toolbarClasses}>
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
<div className="mx_MatrixToolbar_content">
{ this.props.policyUrl ? _t(
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
"This will use a cookie " +
"(please see our <PolicyLink>Cookie Policy</PolicyLink>).",
{},
{
'UsageDataLink': (sub) => <a
className="mx_MatrixToolbar_link"
onClick={this.onUsageDataClicked}
>
{ sub }
</a>,
// XXX: We need to link to the page that explains our cookies
'PolicyLink': (sub) => <a
className="mx_MatrixToolbar_link"
target="_blank"
href={this.props.policyUrl}
>
{ sub }
</a>
,
},
) : _t(
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
"This will use a cookie.",
{},
{
'UsageDataLink': (sub) => <a
className="mx_MatrixToolbar_link"
onClick={this.onUsageDataClicked}
>
{ sub }
</a>,
},
) }
</div>
<AccessibleButton element='button' className="mx_MatrixToolbar_action" onClick={this.onAccept}>
{ _t("Yes, I want to help!") }
</AccessibleButton>
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.onReject}>
<img src={require("../../../../res/img/cancel.svg")} width="18" height="18" alt={_t('Close')} />
</AccessibleButton>
</div>
);
}
}

View file

@ -1,45 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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 createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import Notifier from '../../../Notifier';
import AccessibleButton from '../../../components/views/elements/AccessibleButton';
export default createReactClass({
displayName: 'MatrixToolbar',
hideToolbar: function() {
Notifier.setToolbarHidden(true);
},
onClick: function() {
Notifier.setEnabled(true);
},
render: function() {
return (
<div className="mx_MatrixToolbar">
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
<div className="mx_MatrixToolbar_content">
{ _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a>
</div>
<AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src={require("../../../../res/img/cancel.svg")} width="18" height="18" alt={_t('Close')} /></AccessibleButton>
</div>
);
},
});

View file

@ -1,108 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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 createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import PlatformPeg from '../../../PlatformPeg';
import { _t } from '../../../languageHandler';
/**
* Check a version string is compatible with the Changelog
* dialog ([vectorversion]-react-[react-sdk-version]-js-[js-sdk-version])
*/
function checkVersion(ver) {
const parts = ver.split('-');
return parts.length == 5 && parts[1] == 'react' && parts[3] == 'js';
}
export default createReactClass({
propTypes: {
version: PropTypes.string.isRequired,
newVersion: PropTypes.string.isRequired,
releaseNotes: PropTypes.string,
},
displayReleaseNotes: function(releaseNotes) {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
Modal.createTrackedDialog('Display release notes', '', QuestionDialog, {
title: _t("What's New"),
description: <div className="mx_MatrixToolbar_changelog">{releaseNotes}</div>,
button: _t("Update"),
onFinished: (update) => {
if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate();
}
},
});
},
displayChangelog: function() {
const ChangelogDialog = sdk.getComponent('dialogs.ChangelogDialog');
Modal.createTrackedDialog('Display Changelog', '', ChangelogDialog, {
version: this.props.version,
newVersion: this.props.newVersion,
onFinished: (update) => {
if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate();
}
},
});
},
onUpdateClicked: function() {
PlatformPeg.get().installUpdate();
},
render: function() {
let action_button;
// If we have release notes to display, we display them. Otherwise,
// we display the Changelog Dialog which takes two versions and
// automatically tells you what's changed (provided the versions
// are in the right format)
if (this.props.releaseNotes) {
action_button = (
<button className="mx_MatrixToolbar_action" onClick={this.displayReleaseNotes}>
{ _t("What's new?") }
</button>
);
} else if (checkVersion(this.props.version) && checkVersion(this.props.newVersion)) {
action_button = (
<button className="mx_MatrixToolbar_action" onClick={this.displayChangelog}>
{ _t("What's new?") }
</button>
);
} else if (PlatformPeg.get()) {
action_button = (
<button className="mx_MatrixToolbar_action" onClick={this.onUpdateClicked}>
{ _t("Update") }
</button>
);
}
return (
<div className="mx_MatrixToolbar">
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
<div className="mx_MatrixToolbar_content">
{_t("A new version of Riot is available.")}
</div>
{action_button}
</div>
);
},
});

View file

@ -1,53 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
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 createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
export default createReactClass({
onUpdateClicked: function() {
const SetPasswordDialog = sdk.getComponent('dialogs.SetPasswordDialog');
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog);
},
render: function() {
const toolbarClasses = "mx_MatrixToolbar mx_MatrixToolbar_clickable";
return (
<div className={toolbarClasses} onClick={this.onUpdateClicked}>
<img className="mx_MatrixToolbar_warning"
src={require("../../../../res/img/warning.svg")}
width="24"
height="23"
alt=""
/>
<div className="mx_MatrixToolbar_content">
{ _t(
"To return to your account in future you need to <u>set a password</u>",
{},
{ 'u': (sub) => <u>{ sub }</u> },
) }
</div>
<button className="mx_MatrixToolbar_action">
{ _t("Set Password") }
</button>
</div>
);
},
});

View file

@ -1,99 +0,0 @@
/*
Copyright 2018 New Vector Ltd
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 createReactClass from 'create-react-class';
import classNames from 'classnames';
import { _td } from '../../../languageHandler';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
export default createReactClass({
propTypes: {
// 'hard' if the logged in user has been locked out, 'soft' if they haven't
kind: PropTypes.string,
adminContact: PropTypes.string,
// The type of limit that has been hit.
limitType: PropTypes.string.isRequired,
},
getDefaultProps: function() {
return {
kind: 'hard',
};
},
render: function() {
const toolbarClasses = {
'mx_MatrixToolbar': true,
};
let adminContact;
let limitError;
if (this.props.kind === 'hard') {
toolbarClasses['mx_MatrixToolbar_error'] = true;
adminContact = messageForResourceLimitError(
this.props.limitType,
this.props.adminContact,
{
'': _td("Please <a>contact your service administrator</a> to continue using the service."),
},
);
limitError = messageForResourceLimitError(
this.props.limitType,
this.props.adminContact,
{
'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
'': _td("This homeserver has exceeded one of its resource limits."),
},
);
} else {
toolbarClasses['mx_MatrixToolbar_info'] = true;
adminContact = messageForResourceLimitError(
this.props.limitType,
this.props.adminContact,
{
'': _td("Please <a>contact your service administrator</a> to get this limit increased."),
},
);
limitError = messageForResourceLimitError(
this.props.limitType,
this.props.adminContact,
{
'monthly_active_user': _td(
"This homeserver has hit its Monthly Active User limit so " +
"<b>some users will not be able to log in</b>.",
),
'': _td(
"This homeserver has exceeded one of its resource limits so " +
"<b>some users will not be able to log in</b>.",
),
},
{'b': sub => <b>{sub}</b>},
);
}
return (
<div className={classNames(toolbarClasses)}>
<div className="mx_MatrixToolbar_content">
{limitError}
{' '}
{adminContact}
</div>
</div>
);
},
});

View file

@ -1,7 +1,7 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -22,11 +22,9 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import Modal from '../../../Modal';
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu'; import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
import RoomContext from "../../../contexts/RoomContext"; import RoomContext from "../../../contexts/RoomContext";
import SettingsStore from '../../../settings/SettingsStore';
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@ -41,18 +39,6 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
const tile = getTile && getTile(); const tile = getTile && getTile();
const replyThread = getReplyThread && getReplyThread(); const replyThread = getReplyThread && getReplyThread();
const onCryptoClick = () => {
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
{event: mxEvent},
);
};
let e2eInfoCallback = null;
if (mxEvent.isEncrypted() && !SettingsStore.getValue("feature_cross_signing")) {
e2eInfoCallback = onCryptoClick;
}
const buttonRect = button.current.getBoundingClientRect(); const buttonRect = button.current.getBoundingClientRect();
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}> contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
<MessageContextMenu <MessageContextMenu
@ -60,7 +46,6 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
permalinkCreator={permalinkCreator} permalinkCreator={permalinkCreator}
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined} eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined} collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
e2eInfoCallback={e2eInfoCallback}
onFinished={closeMenu} onFinished={closeMenu}
/> />
</ContextMenu>; </ContextMenu>;

View file

@ -802,6 +802,8 @@ export default createReactClass({
const groupTimestamp = !this.props.useIRCLayout ? linkedTimestamp : null; const groupTimestamp = !this.props.useIRCLayout ? linkedTimestamp : null;
const ircTimestamp = this.props.useIRCLayout ? linkedTimestamp : null; const ircTimestamp = this.props.useIRCLayout ? linkedTimestamp : null;
const groupPadlock = !this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
const ircPadlock = this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
switch (this.props.tileShape) { switch (this.props.tileShape) {
case 'notif': { case 'notif': {
@ -873,9 +875,10 @@ export default createReactClass({
{ ircTimestamp } { ircTimestamp }
{ avatar } { avatar }
{ sender } { sender }
{ ircPadlock }
<div className="mx_EventTile_reply"> <div className="mx_EventTile_reply">
{ groupTimestamp } { groupTimestamp }
{ !isBubbleMessage && this._renderE2EPadlock() } { groupPadlock }
{ thread } { thread }
<EventTileType ref={this._tile} <EventTileType ref={this._tile}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
@ -904,9 +907,10 @@ export default createReactClass({
{ readAvatars } { readAvatars }
</div> </div>
{ sender } { sender }
{ ircPadlock }
<div className="mx_EventTile_line"> <div className="mx_EventTile_line">
{ groupTimestamp } { groupTimestamp }
{ !isBubbleMessage && this._renderE2EPadlock() } { groupPadlock }
{ thread } { thread }
<EventTileType ref={this._tile} <EventTileType ref={this._tile}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}

View file

@ -30,6 +30,8 @@ import * as RoomNotifs from '../../../RoomNotifs';
import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership"; import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership";
import * as Unread from '../../../Unread'; import * as Unread from '../../../Unread';
import * as FormattingUtils from "../../../utils/FormattingUtils"; import * as FormattingUtils from "../../../utils/FormattingUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *
@ -86,10 +88,22 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
hover: false, hover: false,
notificationState: this.getNotificationState(), notificationState: this.getNotificationState(),
}; };
this.props.room.on("Room.receipt", this.handleRoomEventUpdate);
this.props.room.on("Room.timeline", this.handleRoomEventUpdate);
this.props.room.on("Room.redaction", this.handleRoomEventUpdate);
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
} }
public componentWillUnmount() { public componentWillUnmount() {
// TODO: Listen for changes to the badge count and update as needed if (this.props.room) {
this.props.room.removeListener("Room.receipt", this.handleRoomEventUpdate);
this.props.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
this.props.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
}
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
}
} }
// XXX: This is a bit of an awful-looking hack. We should probably be using state for // XXX: This is a bit of an awful-looking hack. We should probably be using state for
@ -99,7 +113,15 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
return getEffectiveMembership(this.props.room.getMyMembership()) === EffectiveMembership.Invite; return getEffectiveMembership(this.props.room.getMyMembership()) === EffectiveMembership.Invite;
} }
// TODO: Make use of this function when the notification state needs updating. private handleRoomEventUpdate = (event: MatrixEvent) => {
const roomId = event.getRoomId();
// Sanity check: should never happen
if (roomId !== this.props.room.roomId) return;
this.updateNotificationState();
};
private updateNotificationState() { private updateNotificationState() {
this.setState({notificationState: this.getNotificationState()}); this.setState({notificationState: this.getNotificationState()});
} }
@ -214,7 +236,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
let tooltip = null; let tooltip = null;
if (false) { // isCollapsed if (false) { // isCollapsed
if (this.state.hover) { if (this.state.hover) {
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto"/> tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} />
} }
} }

View file

@ -141,6 +141,12 @@ export default createReactClass({
_changePassword: function(cli, oldPassword, newPassword) { _changePassword: function(cli, oldPassword, newPassword) {
const authDict = { const authDict = {
type: 'm.login.password', type: 'm.login.password',
identifier: {
type: 'm.id.user',
user: cli.credentials.userId,
},
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/matrix-org/synapse/issues/5665
user: cli.credentials.userId, user: cli.credentials.userId,
password: oldPassword, password: oldPassword,
}; };

View file

@ -1,75 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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 createReactClass from 'create-react-class';
import Notifier from "../../../Notifier";
import dis from "../../../dispatcher/dispatcher";
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'EnableNotificationsButton',
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onAction: function(payload) {
if (payload.action !== "notifier_enabled") {
return;
}
this.forceUpdate();
},
enabled: function() {
return Notifier.isEnabled();
},
onClick: function() {
const self = this;
if (!Notifier.supportsDesktopNotifications()) {
return;
}
if (!Notifier.isEnabled()) {
Notifier.setEnabled(true, function() {
self.forceUpdate();
});
} else {
Notifier.setEnabled(false);
}
this.forceUpdate();
},
render: function() {
if (this.enabled()) {
return (
<button className="mx_EnableNotificationsButton" onClick={this.onClick}>
{ _t("Disable Notifications") }
</button>
);
} else {
return (
<button className="mx_EnableNotificationsButton" onClick={this.onClick}>
{ _t("Enable Notifications") }
</button>
);
}
},
});

View file

@ -267,7 +267,7 @@ export default class PhoneNumbers extends React.Component {
label={_t("Phone Number")} label={_t("Phone Number")}
autoComplete="off" autoComplete="off"
disabled={this.state.verifying} disabled={this.state.verifying}
prefix={phoneCountry} prefixComponent={phoneCountry}
value={this.state.newPhoneNumber} value={this.state.newPhoneNumber}
onChange={this._onChangeNewPhoneNumber} onChange={this._onChangeNewPhoneNumber}
/> />

View file

@ -20,34 +20,64 @@ import React from 'react';
import {_t} from "../../../../../languageHandler"; import {_t} from "../../../../../languageHandler";
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore"; import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
import * as sdk from "../../../../../index"; import * as sdk from "../../../../../index";
import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; import { enumerateThemes } from "../../../../../theme";
import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher";
import Field from "../../../elements/Field"; import Field from "../../../elements/Field";
import Slider from "../../../elements/Slider"; import Slider from "../../../elements/Slider";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import dis from "../../../../../dispatcher/dispatcher"; import dis from "../../../../../dispatcher/dispatcher";
import { FontWatcher } from "../../../../../FontWatcher"; import { FontWatcher } from "../../../../../settings/watchers/FontWatcher";
import { RecheckThemePayload } from '../../../../../dispatcher/payloads/RecheckThemePayload';
import { Action } from '../../../../../dispatcher/actions';
import { IValidationResult, IFieldState } from '../../../elements/Validation';
export default class AppearanceUserSettingsTab extends React.Component { interface IProps {
constructor() { }
super();
interface IThemeState {
theme: string,
useSystemTheme: boolean,
}
export interface CustomThemeMessage {
isError: boolean,
text: string
};
interface IState extends IThemeState {
// String displaying the current selected fontSize.
// Needs to be string for things like '17.' without
// trailing 0s.
fontSize: string,
customThemeUrl: string,
customThemeMessage: CustomThemeMessage,
useCustomFontSize: boolean,
}
export default class AppearanceUserSettingsTab extends React.Component<IProps, IState> {
private themeTimer: NodeJS.Timeout;
constructor(props: IProps) {
super(props);
this.state = { this.state = {
fontSize: SettingsStore.getValue("fontSize", null), fontSize: SettingsStore.getValue("fontSize", null).toString(),
...this._calculateThemeState(), ...this.calculateThemeState(),
customThemeUrl: "", customThemeUrl: "",
customThemeMessage: {isError: false, text: ""}, customThemeMessage: {isError: false, text: ""},
useCustomFontSize: SettingsStore.getValue("useCustomFontSize"), useCustomFontSize: SettingsStore.getValue("useCustomFontSize"),
}; };
} }
_calculateThemeState() { private calculateThemeState(): IThemeState {
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we // We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
// show the right values for things. // show the right values for things.
const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"); const themeChoice: string = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
const systemThemeExplicit = SettingsStore.getValueAt( const systemThemeExplicit: boolean = SettingsStore.getValueAt(
SettingLevel.DEVICE, "use_system_theme", null, false, true); SettingLevel.DEVICE, "use_system_theme", null, false, true);
const themeExplicit = SettingsStore.getValueAt( const themeExplicit: string = SettingsStore.getValueAt(
SettingLevel.DEVICE, "theme", null, false, true); SettingLevel.DEVICE, "theme", null, false, true);
// If the user has enabled system theme matching, use that. // If the user has enabled system theme matching, use that.
@ -73,15 +103,15 @@ export default class AppearanceUserSettingsTab extends React.Component {
}; };
} }
_onThemeChange = (e) => { private onThemeChange(e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>): void {
const newTheme = e.target.value; const newTheme = e.target.value;
if (this.state.theme === newTheme) return; if (this.state.theme === newTheme) return;
// doing getValue in the .catch will still return the value we failed to set, // doing getValue in the .catch will still return the value we failed to set,
// so remember what the value was before we tried to set it so we can revert // so remember what the value was before we tried to set it so we can revert
const oldTheme = SettingsStore.getValue('theme'); const oldTheme: string = SettingsStore.getValue('theme');
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => { SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => {
dis.dispatch({action: 'recheck_theme'}); dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme});
this.setState({theme: oldTheme}); this.setState({theme: oldTheme});
}); });
this.setState({theme: newTheme}); this.setState({theme: newTheme});
@ -91,23 +121,21 @@ export default class AppearanceUserSettingsTab extends React.Component {
// XXX: The local echoed value appears to be unreliable, in particular // XXX: The local echoed value appears to be unreliable, in particular
// when settings custom themes(!) so adding forceTheme to override // when settings custom themes(!) so adding forceTheme to override
// the value from settings. // the value from settings.
dis.dispatch({action: 'recheck_theme', forceTheme: newTheme}); dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme, forceTheme: newTheme});
}; };
_onUseSystemThemeChanged = (checked) => { private onUseSystemThemeChanged(checked: boolean) {
this.setState({useSystemTheme: checked}); this.setState({useSystemTheme: checked});
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked);
dis.dispatch({action: 'recheck_theme'}); dis.dispatch<RecheckThemePayload>({action: Action.RecheckTheme});
}; };
_onFontSizeChanged = (size) => { private onFontSizeChanged(size: number) {
this.setState({fontSize: size}); this.setState({fontSize: size.toString()});
SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size); SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size);
}; };
_onValidateFontSize = ({value}) => { private async onValidateFontSize({value}: Pick<IFieldState, "value">): Promise<IValidationResult> {
console.log({value});
const parsedSize = parseFloat(value); const parsedSize = parseFloat(value);
const min = FontWatcher.MIN_SIZE; const min = FontWatcher.MIN_SIZE;
const max = FontWatcher.MAX_SIZE; const max = FontWatcher.MAX_SIZE;
@ -127,17 +155,18 @@ export default class AppearanceUserSettingsTab extends React.Component {
return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})}; return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})};
} }
_onAddCustomTheme = async () => { private async onAddCustomTheme() {
let currentThemes = SettingsStore.getValue("custom_themes"); let currentThemes: string[] = SettingsStore.getValue("custom_themes");
if (!currentThemes) currentThemes = []; if (!currentThemes) currentThemes = [];
currentThemes = currentThemes.map(c => c); // cheap clone currentThemes = currentThemes.map(c => c); // cheap clone
if (this._themeTimer) { if (this.themeTimer) {
clearTimeout(this._themeTimer); clearTimeout(this.themeTimer);
} }
try { try {
const r = await fetch(this.state.customThemeUrl); const r = await fetch(this.state.customThemeUrl);
// XXX: need some schema for this
const themeInfo = await r.json(); const themeInfo = await r.json();
if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') { if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') {
this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}}); this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}});
@ -153,42 +182,32 @@ export default class AppearanceUserSettingsTab extends React.Component {
await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}}); this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}});
this._themeTimer = setTimeout(() => { this.themeTimer = setTimeout(() => {
this.setState({customThemeMessage: {text: "", isError: false}}); this.setState({customThemeMessage: {text: "", isError: false}});
}, 3000); }, 3000);
}; };
_onCustomThemeChange = (e) => { private onCustomThemeChange(e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) {
this.setState({customThemeUrl: e.target.value}); this.setState({customThemeUrl: e.target.value});
}; };
render() { private renderThemeSection() {
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Appearance")}</div>
{this._renderThemeSection()}
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this._renderFontSection() : null}
</div>
);
}
_renderThemeSection() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch"); const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch");
const themeWatcher = new ThemeWatcher(); const themeWatcher = new ThemeWatcher();
let systemThemeSection; let systemThemeSection: JSX.Element;
if (themeWatcher.isSystemThemeSupported()) { if (themeWatcher.isSystemThemeSupported()) {
systemThemeSection = <div> systemThemeSection = <div>
<LabelledToggleSwitch <LabelledToggleSwitch
value={this.state.useSystemTheme} value={this.state.useSystemTheme}
label={SettingsStore.getDisplayName("use_system_theme")} label={SettingsStore.getDisplayName("use_system_theme")}
onChange={this._onUseSystemThemeChanged} onChange={this.onUseSystemThemeChanged}
/> />
</div>; </div>;
} }
let customThemeForm; let customThemeForm: JSX.Element;
if (SettingsStore.isFeatureEnabled("feature_custom_themes")) { if (SettingsStore.isFeatureEnabled("feature_custom_themes")) {
let messageElement = null; let messageElement = null;
if (this.state.customThemeMessage.text) { if (this.state.customThemeMessage.text) {
@ -200,17 +219,17 @@ export default class AppearanceUserSettingsTab extends React.Component {
} }
customThemeForm = ( customThemeForm = (
<div className='mx_SettingsTab_section'> <div className='mx_SettingsTab_section'>
<form onSubmit={this._onAddCustomTheme}> <form onSubmit={this.onAddCustomTheme}>
<Field <Field
label={_t("Custom theme URL")} label={_t("Custom theme URL")}
type='text' type='text'
id='mx_GeneralUserSettingsTab_customThemeInput' id='mx_GeneralUserSettingsTab_customThemeInput'
autoComplete="off" autoComplete="off"
onChange={this._onCustomThemeChange} onChange={this.onCustomThemeChange}
value={this.state.customThemeUrl} value={this.state.customThemeUrl}
/> />
<AccessibleButton <AccessibleButton
onClick={this._onAddCustomTheme} onClick={this.onAddCustomTheme}
type="submit" kind="primary_sm" type="submit" kind="primary_sm"
disabled={!this.state.customThemeUrl.trim()} disabled={!this.state.customThemeUrl.trim()}
>{_t("Add theme")}</AccessibleButton> >{_t("Add theme")}</AccessibleButton>
@ -220,7 +239,8 @@ export default class AppearanceUserSettingsTab extends React.Component {
); );
} }
const themes = Object.entries(enumerateThemes()) // XXX: replace any type here
const themes = Object.entries<any>(enumerateThemes())
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability .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 builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
const customThemes = themes.filter(p => !builtInThemes.includes(p)) const customThemes = themes.filter(p => !builtInThemes.includes(p))
@ -232,7 +252,7 @@ export default class AppearanceUserSettingsTab extends React.Component {
{systemThemeSection} {systemThemeSection}
<Field <Field
id="theme" label={_t("Theme")} element="select" id="theme" label={_t("Theme")} element="select"
value={this.state.theme} onChange={this._onThemeChange} value={this.state.theme} onChange={this.onThemeChange}
disabled={this.state.useSystemTheme} disabled={this.state.useSystemTheme}
> >
{orderedThemes.map(theme => { {orderedThemes.map(theme => {
@ -245,7 +265,7 @@ export default class AppearanceUserSettingsTab extends React.Component {
); );
} }
_renderFontSection() { private renderFontSection() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_fontScaling"> return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_fontScaling">
<span className="mx_SettingsTab_subheading">{_t("Font size")}</span> <span className="mx_SettingsTab_subheading">{_t("Font size")}</span>
@ -253,9 +273,9 @@ export default class AppearanceUserSettingsTab extends React.Component {
<div className="mx_AppearanceUserSettingsTab_fontSlider_smallText">Aa</div> <div className="mx_AppearanceUserSettingsTab_fontSlider_smallText">Aa</div>
<Slider <Slider
values={[13, 15, 16, 18, 20]} values={[13, 15, 16, 18, 20]}
value={this.state.fontSize} value={parseInt(this.state.fontSize, 10)}
onSelectionChange={this._onFontSizeChanged} onSelectionChange={this.onFontSizeChanged}
displayFunc={value => {}} displayFunc={value => ""}
disabled={this.state.useCustomFontSize} disabled={this.state.useCustomFontSize}
/> />
<div className="mx_AppearanceUserSettingsTab_fontSlider_largeText">Aa</div> <div className="mx_AppearanceUserSettingsTab_fontSlider_largeText">Aa</div>
@ -263,7 +283,7 @@ export default class AppearanceUserSettingsTab extends React.Component {
<SettingsFlag <SettingsFlag
name="useCustomFontSize" name="useCustomFontSize"
level={SettingLevel.ACCOUNT} level={SettingLevel.ACCOUNT}
onChange={(checked)=> this.setState({useCustomFontSize: checked})} onChange={(checked) => this.setState({useCustomFontSize: checked})}
/> />
<Field <Field
type="text" type="text"
@ -272,10 +292,20 @@ export default class AppearanceUserSettingsTab extends React.Component {
placeholder={this.state.fontSize.toString()} placeholder={this.state.fontSize.toString()}
value={this.state.fontSize.toString()} value={this.state.fontSize.toString()}
id="font_size_field" id="font_size_field"
onValidate={this._onValidateFontSize} onValidate={this.onValidateFontSize}
onChange={(value) => this.setState({fontSize: value.target.value})} onChange={(value) => this.setState({fontSize: value.target.value})}
disabled={!this.state.useCustomFontSize} disabled={!this.state.useCustomFontSize}
/> />
</div>; </div>;
} }
render() {
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Appearance")}</div>
{this.renderThemeSection()}
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null}
</div>
);
}
} }

View file

@ -1,56 +0,0 @@
/*
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 { _t } from '../../../languageHandler';
import dis from "../../../dispatcher/dispatcher";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import DeviceListener from '../../../DeviceListener';
import FormButton from '../elements/FormButton';
import { replaceableComponent } from '../../../utils/replaceableComponent';
@replaceableComponent("views.toasts.BulkUnverifiedSessionsToast")
export default class BulkUnverifiedSessionsToast extends React.PureComponent {
static propTypes = {
deviceIds: PropTypes.array,
}
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(this.props.deviceIds);
};
_onReviewClick = async () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(this.props.deviceIds);
dis.dispatch({
action: 'view_user_info',
userId: MatrixClientPeg.get().getUserId(),
});
};
render() {
return (<div>
<div className="mx_Toast_description">
{_t("Verify all your sessions to ensure your account & messages are safe")}
</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
<FormButton label={_t("Review")} onClick={this._onReviewClick} />
</div>
</div>);
}
}

View file

@ -0,0 +1,42 @@
/*
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, {ReactChild} from "react";
import FormButton from "../elements/FormButton";
interface IProps {
description: ReactChild;
acceptLabel: string;
rejectLabel?: string;
onAccept();
onReject?();
}
const GenericToast: React.FC<IProps> = ({description, acceptLabel, rejectLabel, onAccept, onReject}) => {
return <div>
<div className="mx_Toast_description">
{ description }
</div>
<div className="mx_Toast_buttons" aria-live="off">
{onReject && rejectLabel && <FormButton label={rejectLabel} kind="danger" onClick={onReject} /> }
<FormButton label={acceptLabel} onClick={onAccept} />
</div>
</div>;
};
export default GenericToast;

View file

@ -1,88 +0,0 @@
/*
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 Modal from '../../../Modal';
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import DeviceListener from '../../../DeviceListener';
import SetupEncryptionDialog from "../dialogs/SetupEncryptionDialog";
import { accessSecretStorage } from '../../../CrossSigningManager';
export default class SetupEncryptionToast extends React.PureComponent {
static propTypes = {
toastKey: PropTypes.string.isRequired,
kind: PropTypes.oneOf([
'set_up_encryption',
'verify_this_session',
'upgrade_encryption',
]).isRequired,
};
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissEncryptionSetup();
};
_onSetupClick = async () => {
if (this.props.kind === "verify_this_session") {
Modal.createTrackedDialog('Verify session', 'Verify session', SetupEncryptionDialog,
{}, null, /* priority = */ false, /* static = */ true);
} else {
const Spinner = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(
Spinner, null, 'mx_Dialog_spinner', /* priority */ false, /* static */ true,
);
try {
await accessSecretStorage();
} finally {
modal.close();
}
}
};
getDescription() {
switch (this.props.kind) {
case 'set_up_encryption':
case 'upgrade_encryption':
return _t('Verify yourself & others to keep your chats safe');
case 'verify_this_session':
return _t('Other users may not trust it');
}
}
getSetupCaption() {
switch (this.props.kind) {
case 'set_up_encryption':
return _t('Set up');
case 'upgrade_encryption':
return _t('Upgrade');
case 'verify_this_session':
return _t('Verify');
}
}
render() {
const FormButton = sdk.getComponent("elements.FormButton");
return (<div>
<div className="mx_Toast_description">{this.getDescription()}</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
<FormButton label={this.getSetupCaption()} onClick={this._onSetupClick} />
</div>
</div>);
}
}

View file

@ -1,66 +0,0 @@
/*
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 { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import DeviceListener from '../../../DeviceListener';
import NewSessionReviewDialog from '../dialogs/NewSessionReviewDialog';
import FormButton from '../elements/FormButton';
import { replaceableComponent } from '../../../utils/replaceableComponent';
@replaceableComponent("views.toasts.UnverifiedSessionToast")
export default class UnverifiedSessionToast extends React.PureComponent {
static propTypes = {
deviceId: PropTypes.string,
}
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions([this.props.deviceId]);
};
_onReviewClick = async () => {
const cli = MatrixClientPeg.get();
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
userId: cli.getUserId(),
device: cli.getStoredDevice(cli.getUserId(), this.props.deviceId),
onFinished: (r) => {
if (!r) {
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
DeviceListener.sharedInstance().dismissUnverifiedSessions([this.props.deviceId]);
}
},
}, null, /* priority = */ false, /* static = */ true);
};
render() {
const cli = MatrixClientPeg.get();
const device = cli.getStoredDevice(cli.getUserId(), this.props.deviceId);
return (<div>
<div className="mx_Toast_description">
{_t(
"Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()})}
</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
<FormButton label={_t("Verify")} onClick={this._onReviewClick} />
</div>
</div>);
}
}

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from "react";
import PropTypes from 'prop-types';
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
@ -24,8 +24,23 @@ import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import ToastStore from "../../../stores/ToastStore"; import ToastStore from "../../../stores/ToastStore";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import GenericToast from "./GenericToast";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {DeviceInfo} from "matrix-js-sdk/src/crypto/deviceinfo";
interface IProps {
toastKey: string;
request: VerificationRequest;
}
interface IState {
counter: number;
device?: DeviceInfo;
}
export default class VerificationRequestToast extends React.PureComponent<IProps, IState> {
private intervalHandle: NodeJS.Timeout;
export default class VerificationRequestToast extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {counter: Math.ceil(props.request.timeout / 1000)}; this.state = {counter: Math.ceil(props.request.timeout / 1000)};
@ -34,7 +49,7 @@ export default class VerificationRequestToast extends React.PureComponent {
async componentDidMount() { async componentDidMount() {
const {request} = this.props; const {request} = this.props;
if (request.timeout && request.timeout > 0) { if (request.timeout && request.timeout > 0) {
this._intervalHandle = setInterval(() => { this.intervalHandle = setInterval(() => {
let {counter} = this.state; let {counter} = this.state;
counter = Math.max(0, counter - 1); counter = Math.max(0, counter - 1);
this.setState({counter}); this.setState({counter});
@ -56,7 +71,7 @@ export default class VerificationRequestToast extends React.PureComponent {
} }
componentWillUnmount() { componentWillUnmount() {
clearInterval(this._intervalHandle); clearInterval(this.intervalHandle);
const {request} = this.props; const {request} = this.props;
request.off("change", this._checkRequestIsPending); request.off("change", this._checkRequestIsPending);
} }
@ -110,7 +125,6 @@ export default class VerificationRequestToast extends React.PureComponent {
}; };
render() { render() {
const FormButton = sdk.getComponent("elements.FormButton");
const {request} = this.props; const {request} = this.props;
let nameLabel; let nameLabel;
if (request.isSelfVerification) { if (request.isSelfVerification) {
@ -133,20 +147,16 @@ export default class VerificationRequestToast extends React.PureComponent {
} }
} }
} }
const declineLabel = this.state.counter == 0 ? const declineLabel = this.state.counter === 0 ?
_t("Decline") : _t("Decline") :
_t("Decline (%(counter)s)", {counter: this.state.counter}); _t("Decline (%(counter)s)", {counter: this.state.counter});
return (<div>
<div className="mx_Toast_description">{nameLabel}</div> return <GenericToast
<div className="mx_Toast_buttons" aria-live="off"> description={nameLabel}
<FormButton label={declineLabel} kind="danger" onClick={this.cancel} /> acceptLabel={_t("Accept")}
<FormButton label={_t("Accept")} onClick={this.accept} /> onAccept={this.accept}
</div> rejectLabel={declineLabel}
</div>); onReject={this.cancel}
/>;
} }
} }
VerificationRequestToast.propTypes = {
request: PropTypes.object.isRequired,
toastKey: PropTypes.string.isRequired,
};

View file

@ -38,5 +38,15 @@ export enum Action {
* Open the user settings. No additional payload information required. * Open the user settings. No additional payload information required.
*/ */
ViewUserSettings = "view_user_settings", ViewUserSettings = "view_user_settings",
/**
* Sets the current tooltip. Should be use with ViewTooltipPayload.
*/
ViewTooltip = "view_tooltip",
/**
* Forces the theme to reload. No additional payload information required.
*/
RecheckTheme = "recheck_theme",
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,21 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_EncryptedEventDialog .mx_DeviceVerifyButtons { import { ActionPayload } from "../payloads";
float: right; import { Action } from "../actions";
padding: 0px;
margin-right: 42px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.mx_EncryptedEventDialog .mx_MemberDeviceInfo_textButton { export interface RecheckThemePayload extends ActionPayload {
@mixin mx_DialogButton; action: Action.RecheckTheme,
background-color: $primary-bg-color;
color: $accent-color;
}
.mx_EncryptedEventDialog button { /**
margin-top: 0px; * Optionally specify the exact theme which is to be loaded.
*/
forceTheme?: string;
} }

View file

@ -0,0 +1,35 @@
/*
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 { ActionPayload } from "../payloads";
import { Action } from "../actions";
import { Component } from "react";
export interface ViewTooltipPayload extends ActionPayload {
action: Action.ViewTooltip,
/*
* The tooltip to render. If it's null the tooltip will not be rendered
* We need the void type because of typescript headaches.
*/
tooltip: null | void | Element | Component<Element, any, any>;
/*
* The parent under which to render the tooltip. Can be null to remove
* the parent type.
*/
parent: null | Element
}

View file

@ -102,11 +102,6 @@
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s", "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
"Verify this session": "Verify this session",
"Encryption upgrade available": "Encryption upgrade available",
"Set up encryption": "Set up encryption",
"Review where youre logged in": "Review where youre logged in",
"New login. Was this you?": "New login. Was this you?",
"Who would you like to add to this community?": "Who would you like to add to this community?", "Who would you like to add to this community?": "Who would you like to add to this community?",
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID", "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID",
"Invite new community members": "Invite new community members", "Invite new community members": "Invite new community members",
@ -396,6 +391,42 @@
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess", "Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
"Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess", "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess",
"Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess", "Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess",
"Help us improve Riot": "Help us improve Riot",
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve Riot. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve Riot. This will use a <PolicyLink>cookie</PolicyLink>.",
"I want to help": "I want to help",
"No": "No",
"Review where youre logged in": "Review where youre logged in",
"Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe",
"Review": "Review",
"Later": "Later",
"Notifications": "Notifications",
"You are not receiving desktop notifications": "You are not receiving desktop notifications",
"Enable them now": "Enable them now",
"Close": "Close",
"Your homeserver has exceeded its user limit.": "Your homeserver has exceeded its user limit.",
"Your homeserver has exceeded one of its resource limits.": "Your homeserver has exceeded one of its resource limits.",
"Contact your <a>server admin</a>.": "Contact your <a>server admin</a>.",
"Warning": "Warning",
"Ok": "Ok",
"Set password": "Set password",
"To return to your account in future you need to set a password": "To return to your account in future you need to set a password",
"Set Password": "Set Password",
"Set up encryption": "Set up encryption",
"Encryption upgrade available": "Encryption upgrade available",
"Verify this session": "Verify this session",
"Set up": "Set up",
"Upgrade": "Upgrade",
"Verify": "Verify",
"Verify yourself & others to keep your chats safe": "Verify yourself & others to keep your chats safe",
"Other users may not trust it": "Other users may not trust it",
"New login. Was this you?": "New login. Was this you?",
"Verify the new login accessing your account: %(name)s": "Verify the new login accessing your account: %(name)s",
"What's new?": "What's new?",
"What's New": "What's New",
"Update": "Update",
"Restart": "Restart",
"Upgrade your Riot": "Upgrade your Riot",
"A new version of Riot is available!": "A new version of Riot is available!",
"There was an error joining the room": "There was an error joining the room", "There was an error joining the room": "There was an error joining the room",
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
@ -570,15 +601,6 @@
"Headphones": "Headphones", "Headphones": "Headphones",
"Folder": "Folder", "Folder": "Folder",
"Pin": "Pin", "Pin": "Pin",
"Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe",
"Later": "Later",
"Review": "Review",
"Verify yourself & others to keep your chats safe": "Verify yourself & others to keep your chats safe",
"Other users may not trust it": "Other users may not trust it",
"Set up": "Set up",
"Upgrade": "Upgrade",
"Verify": "Verify",
"Verify the new login accessing your account: %(name)s": "Verify the new login accessing your account: %(name)s",
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)", "From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
"Decline (%(counter)s)": "Decline (%(counter)s)", "Decline (%(counter)s)": "Decline (%(counter)s)",
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:", "Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
@ -643,8 +665,6 @@
"Last seen": "Last seen", "Last seen": "Last seen",
"Failed to set display name": "Failed to set display name", "Failed to set display name": "Failed to set display name",
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
"Disable Notifications": "Disable Notifications",
"Enable Notifications": "Enable Notifications",
"Securely cache encrypted messages locally for them to appear in search results, using ": "Securely cache encrypted messages locally for them to appear in search results, using ", "Securely cache encrypted messages locally for them to appear in search results, using ": "Securely cache encrypted messages locally for them to appear in search results, using ",
" to store messages from ": " to store messages from ", " to store messages from ": " to store messages from ",
"rooms.": "rooms.", "rooms.": "rooms.",
@ -758,10 +778,10 @@
"Invalid theme schema.": "Invalid theme schema.", "Invalid theme schema.": "Invalid theme schema.",
"Error downloading theme information.": "Error downloading theme information.", "Error downloading theme information.": "Error downloading theme information.",
"Theme added!": "Theme added!", "Theme added!": "Theme added!",
"Appearance": "Appearance",
"Custom theme URL": "Custom theme URL", "Custom theme URL": "Custom theme URL",
"Add theme": "Add theme", "Add theme": "Add theme",
"Theme": "Theme", "Theme": "Theme",
"Appearance": "Appearance",
"Flair": "Flair", "Flair": "Flair",
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
"Success": "Success", "Success": "Success",
@ -776,7 +796,6 @@
"Account management": "Account management", "Account management": "Account management",
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
"Deactivate Account": "Deactivate Account", "Deactivate Account": "Deactivate Account",
"Warning": "Warning",
"General": "General", "General": "General",
"Discovery": "Discovery", "Discovery": "Discovery",
"Deactivate account": "Deactivate account", "Deactivate account": "Deactivate account",
@ -815,7 +834,6 @@
"Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s", "Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s",
"Server rules": "Server rules", "Server rules": "Server rules",
"User rules": "User rules", "User rules": "User rules",
"Close": "Close",
"You have not ignored anyone.": "You have not ignored anyone.", "You have not ignored anyone.": "You have not ignored anyone.",
"You are currently ignoring:": "You are currently ignoring:", "You are currently ignoring:": "You are currently ignoring:",
"You are not subscribed to any lists": "You are not subscribed to any lists", "You are not subscribed to any lists": "You are not subscribed to any lists",
@ -836,7 +854,6 @@
"If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.",
"Room ID or address of ban list": "Room ID or address of ban list", "Room ID or address of ban list": "Room ID or address of ban list",
"Subscribe": "Subscribe", "Subscribe": "Subscribe",
"Notifications": "Notifications",
"Start automatically after system login": "Start automatically after system login", "Start automatically after system login": "Start automatically after system login",
"Always show the window menu bar": "Always show the window menu bar", "Always show the window menu bar": "Always show the window menu bar",
"Show tray icon and minimize window to it on close": "Show tray icon and minimize window to it on close", "Show tray icon and minimize window to it on close": "Show tray icon and minimize window to it on close",
@ -1287,7 +1304,6 @@
"Verify by emoji": "Verify by emoji", "Verify by emoji": "Verify by emoji",
"Almost there! Is your other session showing the same shield?": "Almost there! Is your other session showing the same shield?", "Almost there! Is your other session showing the same shield?": "Almost there! Is your other session showing the same shield?",
"Almost there! Is %(displayName)s showing the same shield?": "Almost there! Is %(displayName)s showing the same shield?", "Almost there! Is %(displayName)s showing the same shield?": "Almost there! Is %(displayName)s showing the same shield?",
"No": "No",
"Yes": "Yes", "Yes": "Yes",
"Verify all users in a room to ensure it's secure.": "Verify all users in a room to ensure it's secure.", "Verify all users in a room to ensure it's secure.": "Verify all users in a room to ensure it's secure.",
"In encrypted rooms, verify all users to ensure its secure.": "In encrypted rooms, verify all users to ensure its secure.", "In encrypted rooms, verify all users to ensure its secure.": "In encrypted rooms, verify all users to ensure its secure.",
@ -1381,20 +1397,6 @@
"Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.", "Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.",
"Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.", "Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.",
"You're not currently a member of any communities.": "You're not currently a member of any communities.", "You're not currently a member of any communities.": "You're not currently a member of any communities.",
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie (please see our <PolicyLink>Cookie Policy</PolicyLink>).": "Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie (please see our <PolicyLink>Cookie Policy</PolicyLink>).",
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie.": "Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie.",
"Yes, I want to help!": "Yes, I want to help!",
"You are not receiving desktop notifications": "You are not receiving desktop notifications",
"Enable them now": "Enable them now",
"What's New": "What's New",
"Update": "Update",
"What's new?": "What's new?",
"A new version of Riot is available.": "A new version of Riot is available.",
"To return to your account in future you need to <u>set a password</u>": "To return to your account in future you need to <u>set a password</u>",
"Set Password": "Set Password",
"Please <a>contact your service administrator</a> to get this limit increased.": "Please <a>contact your service administrator</a> to get this limit increased.",
"This homeserver has hit its Monthly Active User limit so <b>some users will not be able to log in</b>.": "This homeserver has hit its Monthly Active User limit so <b>some users will not be able to log in</b>.",
"This homeserver has exceeded one of its resource limits so <b>some users will not be able to log in</b>.": "This homeserver has exceeded one of its resource limits so <b>some users will not be able to log in</b>.",
"Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).", "Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).",
"Checking for an update...": "Checking for an update...", "Checking for an update...": "Checking for an update...",
"No update available.": "No update available.", "No update available.": "No update available.",
@ -1598,7 +1600,7 @@
"Create Room": "Create Room", "Create Room": "Create Room",
"Sign out": "Sign out", "Sign out": "Sign out",
"To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this", "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this",
"You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ", "You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "You've previously used a newer version of Riot with this session. To use this version again with end to end encryption, you will need to sign out and back in again.",
"Incompatible Database": "Incompatible Database", "Incompatible Database": "Incompatible Database",
"Continue With Encryption Disabled": "Continue With Encryption Disabled", "Continue With Encryption Disabled": "Continue With Encryption Disabled",
"Confirm your account deactivation by using Single Sign On to prove your identity.": "Confirm your account deactivation by using Single Sign On to prove your identity.", "Confirm your account deactivation by using Single Sign On to prove your identity.": "Confirm your account deactivation by using Single Sign On to prove your identity.",
@ -1669,13 +1671,6 @@
"Start a conversation with someone using their name, username (like <userId/>) or email address.": "Start a conversation with someone using their name, username (like <userId/>) or email address.", "Start a conversation with someone using their name, username (like <userId/>) or email address.": "Start a conversation with someone using their name, username (like <userId/>) or email address.",
"Go": "Go", "Go": "Go",
"Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.", "Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.",
"You added a new session '%(displayName)s', which is requesting encryption keys.": "You added a new session '%(displayName)s', which is requesting encryption keys.",
"Your unverified session '%(displayName)s' is requesting encryption keys.": "Your unverified session '%(displayName)s' is requesting encryption keys.",
"Start verification": "Start verification",
"Share without verifying": "Share without verifying",
"Ignore request": "Ignore request",
"Loading session info...": "Loading session info...",
"Encryption key request": "Encryption key request",
"a new master key signature": "a new master key signature", "a new master key signature": "a new master key signature",
"a new cross-signing key signature": "a new cross-signing key signature", "a new cross-signing key signature": "a new cross-signing key signature",
"a device cross-signing signature": "a device cross-signing signature", "a device cross-signing signature": "a device cross-signing signature",
@ -1856,7 +1851,6 @@
"Share Message": "Share Message", "Share Message": "Share Message",
"Source URL": "Source URL", "Source URL": "Source URL",
"Collapse Reply Thread": "Collapse Reply Thread", "Collapse Reply Thread": "Collapse Reply Thread",
"End-to-end encryption information": "End-to-end encryption information",
"Report Content": "Report Content", "Report Content": "Report Content",
"Failed to set Direct Message status of room": "Failed to set Direct Message status of room", "Failed to set Direct Message status of room": "Failed to set Direct Message status of room",
"Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s", "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
@ -2176,22 +2170,6 @@
"Room Autocomplete": "Room Autocomplete", "Room Autocomplete": "Room Autocomplete",
"Users": "Users", "Users": "Users",
"User Autocomplete": "User Autocomplete", "User Autocomplete": "User Autocomplete",
"unknown device": "unknown device",
"NOT verified": "NOT verified",
"Blacklisted": "Blacklisted",
"verified": "verified",
"Device ID": "Device ID",
"Verification": "Verification",
"Ed25519 fingerprint": "Ed25519 fingerprint",
"User ID": "User ID",
"Curve25519 identity key": "Curve25519 identity key",
"none": "none",
"Claimed Ed25519 fingerprint key": "Claimed Ed25519 fingerprint key",
"Algorithm": "Algorithm",
"unencrypted": "unencrypted",
"Decryption error": "Decryption error",
"Event information": "Event information",
"Sender session information": "Sender session information",
"Passphrases must match": "Passphrases must match", "Passphrases must match": "Passphrases must match",
"Passphrase must not be empty": "Passphrase must not be empty", "Passphrase must not be empty": "Passphrase must not be empty",
"Export room keys": "Export room keys", "Export room keys": "Export room keys",

View file

@ -405,7 +405,7 @@ export default class EventIndex extends EventEmitter {
continue; continue;
} }
console.log("EventIndex: Error crawling events:", e); console.log("EventIndex: Error crawling using checkpoint:", checkpoint, ",", e);
this.crawlerCheckpoints.push(checkpoint); this.crawlerCheckpoints.push(checkpoint);
continue; continue;
} }
@ -507,7 +507,13 @@ export default class EventIndex extends EventEmitter {
try { try {
for (let i = 0; i < redactionEvents.length; i++) { for (let i = 0; i < redactionEvents.length; i++) {
const ev = redactionEvents[i]; const ev = redactionEvents[i];
await indexManager.deleteEvent(ev.getAssociatedId()); const eventId = ev.getAssociatedId();
if (eventId) {
await indexManager.deleteEvent(eventId);
} else {
console.warn("EventIndex: Redaction event doesn't contain a valid associated event id", ev);
}
} }
const eventsAlreadyAdded = await indexManager.addHistoricEvents( const eventsAlreadyAdded = await indexManager.addHistoricEvents(

View file

@ -180,7 +180,7 @@ export const SETTINGS = {
"fontSize": { "fontSize": {
displayName: _td("Font size"), displayName: _td("Font size"),
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: 16, default: 15,
controller: new FontSizeController(), controller: new FontSizeController(),
}, },
"useCustomFontSize": { "useCustomFontSize": {

View file

@ -14,38 +14,42 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import dis from './dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import SettingsStore, {SettingLevel} from './settings/SettingsStore'; import SettingsStore, {SettingLevel} from '../SettingsStore';
import IWatcher from "./Watcher";
import { toPx } from '../../utils/units';
export class FontWatcher { export class FontWatcher implements IWatcher {
static MIN_SIZE = 13; public static readonly MIN_SIZE = 13;
static MAX_SIZE = 20; public static readonly MAX_SIZE = 20;
private dispatcherRef: string;
constructor() { constructor() {
this._dispatcherRef = null; this.dispatcherRef = null;
} }
start() { public start() {
this._setRootFontSize(SettingsStore.getValue("fontSize")); this.setRootFontSize(SettingsStore.getValue("fontSize"));
this._dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this.onAction);
} }
stop() { public stop() {
dis.unregister(this._dispatcherRef); dis.unregister(this.dispatcherRef);
} }
_onAction = (payload) => { private onAction = (payload) => {
if (payload.action === 'update-font-size') { if (payload.action === 'update-font-size') {
this._setRootFontSize(payload.size); this.setRootFontSize(payload.size);
} }
}; };
_setRootFontSize = (size) => { private setRootFontSize = (size) => {
const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE); const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE);
if (fontSize != size) { if (fontSize !== size) {
SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize); SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize);
} }
document.querySelector(":root").style.fontSize = fontSize + "px"; (<HTMLElement>document.querySelector(":root")).style.fontSize = toPx(fontSize);
}; };
} }

View file

@ -0,0 +1,138 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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.
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 SettingsStore, { SettingLevel } from '../SettingsStore';
import dis from '../../dispatcher/dispatcher';
import { Action } from '../../dispatcher/actions';
import ThemeController from "../controllers/ThemeController";
import { setTheme } from "../../theme";
import { ActionPayload } from '../../dispatcher/payloads';
export default class ThemeWatcher {
// XXX: I think this is unused.
static _instance = null;
private themeWatchRef: string;
private systemThemeWatchRef: string;
private dispatcherRef: string;
private preferDark: MediaQueryList;
private preferLight: MediaQueryList;
private currentTheme: string;
constructor() {
this.themeWatchRef = null;
this.systemThemeWatchRef = null;
this.dispatcherRef = null;
// we have both here as each may either match or not match, so by having both
// we can get the tristate of dark/light/unsupported
this.preferDark = (<any>global).matchMedia("(prefers-color-scheme: dark)");
this.preferLight = (<any>global).matchMedia("(prefers-color-scheme: light)");
this.currentTheme = this.getEffectiveTheme();
}
public start() {
this.themeWatchRef = SettingsStore.watchSetting("theme", null, this.onChange);
this.systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this.onChange);
if (this.preferDark.addEventListener) {
this.preferDark.addEventListener('change', this.onChange);
this.preferLight.addEventListener('change', this.onChange);
}
this.dispatcherRef = dis.register(this.onAction);
}
public stop() {
if (this.preferDark.addEventListener) {
this.preferDark.removeEventListener('change', this.onChange);
this.preferLight.removeEventListener('change', this.onChange);
}
SettingsStore.unwatchSetting(this.systemThemeWatchRef);
SettingsStore.unwatchSetting(this.themeWatchRef);
dis.unregister(this.dispatcherRef);
}
private onChange = () => {
this.recheck();
};
private onAction = (payload: ActionPayload) => {
if (payload.action === Action.RecheckTheme) {
// XXX forceTheme
this.recheck(payload.forceTheme);
}
};
// XXX: forceTheme param added here as local echo appears to be unreliable
// https://github.com/vector-im/riot-web/issues/11443
public recheck(forceTheme?: string) {
const oldTheme = this.currentTheme;
this.currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme;
if (oldTheme !== this.currentTheme) {
setTheme(this.currentTheme);
}
}
public getEffectiveTheme(): string {
// Dev note: Much of this logic is replicated in the AppearanceUserSettingsTab
// XXX: checking the isLight flag here makes checking it in the ThemeController
// itself completely redundant since we just override the result here and we're
// now effectively just using the ThemeController as a place to store the static
// variable. The system theme setting probably ought to have an equivalent
// controller that honours the same flag, although probablt better would be to
// have the theme logic in one place rather than split between however many
// different places.
if (ThemeController.isLogin) return 'light';
// If the user has specifically enabled the system matching option (excluding default),
// then use that over anything else. We pick the lowest possible level for the setting
// to ensure the ordering otherwise works.
const systemThemeExplicit = SettingsStore.getValueAt(
SettingLevel.DEVICE, "use_system_theme", null, false, true);
if (systemThemeExplicit) {
console.log("returning explicit system theme");
if (this.preferDark.matches) return 'dark';
if (this.preferLight.matches) return 'light';
}
// If the user has specifically enabled the theme (without the system matching option being
// enabled specifically and excluding the default), use that theme. We pick the lowest possible
// level for the setting to ensure the ordering otherwise works.
const themeExplicit = SettingsStore.getValueAt(
SettingLevel.DEVICE, "theme", null, false, true);
if (themeExplicit) {
console.log("returning explicit theme: " + themeExplicit);
return themeExplicit;
}
// If the user hasn't really made a preference in either direction, assume the defaults of the
// settings and use those.
if (SettingsStore.getValue('use_system_theme')) {
if (this.preferDark.matches) return 'dark';
if (this.preferLight.matches) return 'light';
}
console.log("returning theme value");
return SettingsStore.getValue('theme');
}
public isSystemThemeSupported() {
return this.preferDark.matches || this.preferLight.matches;
}
}

View file

@ -0,0 +1,20 @@
/*
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.
*/
export default interface IWatcher {
start(): void
stop(): void
}

View file

@ -46,7 +46,6 @@ const INITIAL_STATE = {
forwardingEvent: null, forwardingEvent: null,
quotingEvent: null, quotingEvent: null,
matrixClientIsReady: false,
}; };
/** /**
@ -60,9 +59,6 @@ class RoomViewStore extends Store {
// Initialise state // Initialise state
this._state = INITIAL_STATE; this._state = INITIAL_STATE;
if (MatrixClientPeg.get()) {
this._state.matrixClientIsReady = MatrixClientPeg.get().isInitialSyncComplete();
}
} }
_setState(newState) { _setState(newState) {
@ -157,11 +153,6 @@ class RoomViewStore extends Store {
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
break; break;
} }
case 'sync_state':
this._setState({
matrixClientIsReady: MatrixClientPeg.get() && MatrixClientPeg.get().isInitialSyncComplete(),
});
break;
} }
} }
@ -224,6 +215,7 @@ class RoomViewStore extends Store {
storeRoomAliasInCache(payload.room_alias, result.room_id); storeRoomAliasInCache(payload.room_alias, result.room_id);
roomId = result.room_id; roomId = result.room_id;
} catch (err) { } catch (err) {
console.error("RVS failed to get room id for alias: ", err);
dis.dispatch({ dis.dispatch({
action: 'view_room_error', action: 'view_room_error',
room_id: null, room_id: null,
@ -272,9 +264,8 @@ class RoomViewStore extends Store {
err: err, err: err,
}); });
let msg = err.message ? err.message : JSON.stringify(err); let msg = err.message ? err.message : JSON.stringify(err);
// XXX: We are relying on the error message returned by browsers here. console.log("Failed to join room:", msg);
// This isn't great, but it does generalize the error being shown to users. if (err.name === "ConnectionError") {
if (msg && msg.startsWith("CORS request rejected")) {
msg = _t("There was an error joining the room"); msg = _t("There was an error joining the room");
} }
if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') { if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
@ -375,7 +366,7 @@ class RoomViewStore extends Store {
} }
shouldPeek() { shouldPeek() {
return this._state.shouldPeek && this._state.matrixClientIsReady; return this._state.shouldPeek;
} }
} }

View file

@ -1,73 +0,0 @@
/*
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 EventEmitter from 'events';
/**
* Holds the active toasts
*/
export default class ToastStore extends EventEmitter {
static PRIORITY_REALTIME = 0;
static PRIORITY_DEFAULT = 1;
static PRIORITY_LOW = 2;
static sharedInstance() {
if (!global.mx_ToastStore) global.mx_ToastStore = new ToastStore();
return global.mx_ToastStore;
}
constructor() {
super();
this._dispatcherRef = null;
this._toasts = [];
}
reset() {
this._toasts = [];
}
/**
* Add or replace a toast
* If a toast with the same toastKey already exists, the given toast will replace it
* Toasts are always added underneath any toasts of the same priority, so existing
* toasts stay at the top unless a higher priority one arrives (better to not change the
* toast unless necessary).
*
* @param {boject} newToast The new toast
*/
addOrReplaceToast(newToast) {
if (newToast.priority === undefined) newToast.priority = ToastStore.PRIORITY_DEFAULT;
const oldIndex = this._toasts.findIndex(t => t.key === newToast.key);
if (oldIndex === -1) {
let newIndex = this._toasts.length;
while (newIndex > 0 && this._toasts[newIndex - 1].priority > newToast.priority) --newIndex;
this._toasts.splice(newIndex, 0, newToast);
} else {
this._toasts[oldIndex] = newToast;
}
this.emit('update');
}
dismissToast(key) {
this._toasts = this._toasts.filter(t => t.key !== key);
this.emit('update');
}
getToasts() {
return this._toasts;
}
}

93
src/stores/ToastStore.ts Normal file
View file

@ -0,0 +1,93 @@
/*
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 EventEmitter from "events";
import React, {JSXElementConstructor} from "react";
export interface IToast<C extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> {
key: string;
// higher priority number will be shown on top of lower priority
priority: number;
title: string;
icon?: string;
component: C;
props?: React.ComponentProps<C>;
}
/**
* Holds the active toasts
*/
export default class ToastStore extends EventEmitter {
private toasts: IToast<any>[] = [];
// The count of toasts which have been seen & dealt with in this stack
// where the count resets when the stack of toasts clears.
private countSeen = 0;
static sharedInstance() {
if (!window.mx_ToastStore) window.mx_ToastStore = new ToastStore();
return window.mx_ToastStore;
}
reset() {
this.toasts = [];
this.countSeen = 0;
}
/**
* Add or replace a toast
* If a toast with the same toastKey already exists, the given toast will replace it
* Toasts are always added underneath any toasts of the same priority, so existing
* toasts stay at the top unless a higher priority one arrives (better to not change the
* toast unless necessary).
*
* @param {object} newToast The new toast
*/
addOrReplaceToast<C extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>>(newToast: IToast<C>) {
const oldIndex = this.toasts.findIndex(t => t.key === newToast.key);
if (oldIndex === -1) {
let newIndex = this.toasts.length;
while (newIndex > 0 && this.toasts[newIndex - 1].priority < newToast.priority) --newIndex;
this.toasts.splice(newIndex, 0, newToast);
} else {
this.toasts[oldIndex] = newToast;
}
this.emit('update');
}
dismissToast(key) {
if (this.toasts[0] && this.toasts[0].key === key) {
this.countSeen++;
}
const length = this.toasts.length;
this.toasts = this.toasts.filter(t => t.key !== key);
if (length !== this.toasts.length) {
if (this.toasts.length === 0) {
this.countSeen = 0;
}
this.emit('update');
}
}
getToasts() {
return this.toasts;
}
getCountSeen() {
return this.countSeen;
}
}

View file

@ -26,6 +26,7 @@ import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorit
import { getListAlgorithmInstance } from "./algorithms/list-ordering"; import { getListAlgorithmInstance } from "./algorithms/list-ordering";
import { ActionPayload } from "../../dispatcher/payloads"; import { ActionPayload } from "../../dispatcher/payloads";
import defaultDispatcher from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
interface IState { interface IState {
tagsEnabled?: boolean; tagsEnabled?: boolean;
@ -135,15 +136,10 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
if (payload.action === 'MatrixActions.Room.receipt') { if (payload.action === 'MatrixActions.Room.receipt') {
// First see if the receipt event is for our own user. If it was, trigger // First see if the receipt event is for our own user. If it was, trigger
// a room update (we probably read the room on a different device). // a room update (we probably read the room on a different device).
// noinspection JSObjectNullOrUndefined - this.matrixClient can't be null by this point in the lifecycle if (readReceiptChangeIsFor(payload.event, this.matrixClient)) {
const myUserId = this.matrixClient.getUserId(); // TODO: Update room now that it's been read
for (const eventId of Object.keys(payload.event.getContent())) { console.log(payload);
const receiptUsers = Object.keys(payload.event.getContent()[eventId]['m.read'] || {}); return;
if (receiptUsers.includes(myUserId)) {
// TODO: Update room now that it's been read
console.log(payload);
return;
}
} }
} else if (payload.action === 'MatrixActions.Room.tags') { } else if (payload.action === 'MatrixActions.Room.tags') {
// TODO: Update room from tags // TODO: Update room from tags

View file

@ -19,114 +19,8 @@ import {_t} from "./languageHandler";
export const DEFAULT_THEME = "light"; export const DEFAULT_THEME = "light";
import Tinter from "./Tinter"; import Tinter from "./Tinter";
import dis from "./dispatcher/dispatcher"; import SettingsStore from "./settings/SettingsStore";
import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; import ThemeWatcher from "./settings/watchers/ThemeWatcher";
import ThemeController from "./settings/controllers/ThemeController";
export class ThemeWatcher {
static _instance = null;
constructor() {
this._themeWatchRef = null;
this._systemThemeWatchRef = null;
this._dispatcherRef = null;
// we have both here as each may either match or not match, so by having both
// we can get the tristate of dark/light/unsupported
this._preferDark = global.matchMedia("(prefers-color-scheme: dark)");
this._preferLight = global.matchMedia("(prefers-color-scheme: light)");
this._currentTheme = this.getEffectiveTheme();
}
start() {
this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onChange);
this._systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this._onChange);
if (this._preferDark.addEventListener) {
this._preferDark.addEventListener('change', this._onChange);
this._preferLight.addEventListener('change', this._onChange);
}
this._dispatcherRef = dis.register(this._onAction);
}
stop() {
if (this._preferDark.addEventListener) {
this._preferDark.removeEventListener('change', this._onChange);
this._preferLight.removeEventListener('change', this._onChange);
}
SettingsStore.unwatchSetting(this._systemThemeWatchRef);
SettingsStore.unwatchSetting(this._themeWatchRef);
dis.unregister(this._dispatcherRef);
}
_onChange = () => {
this.recheck();
};
_onAction = (payload) => {
if (payload.action === 'recheck_theme') {
// XXX forceTheme
this.recheck(payload.forceTheme);
}
};
// XXX: forceTheme param added here as local echo appears to be unreliable
// https://github.com/vector-im/riot-web/issues/11443
recheck(forceTheme) {
const oldTheme = this._currentTheme;
this._currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme;
if (oldTheme !== this._currentTheme) {
setTheme(this._currentTheme);
}
}
getEffectiveTheme() {
// Dev note: Much of this logic is replicated in the AppearanceUserSettingsTab
// XXX: checking the isLight flag here makes checking it in the ThemeController
// itself completely redundant since we just override the result here and we're
// now effectively just using the ThemeController as a place to store the static
// variable. The system theme setting probably ought to have an equivalent
// controller that honours the same flag, although probablt better would be to
// have the theme logic in one place rather than split between however many
// different places.
if (ThemeController.isLogin) return 'light';
// If the user has specifically enabled the system matching option (excluding default),
// then use that over anything else. We pick the lowest possible level for the setting
// to ensure the ordering otherwise works.
const systemThemeExplicit = SettingsStore.getValueAt(
SettingLevel.DEVICE, "use_system_theme", null, false, true);
if (systemThemeExplicit) {
console.log("returning explicit system theme");
if (this._preferDark.matches) return 'dark';
if (this._preferLight.matches) return 'light';
}
// If the user has specifically enabled the theme (without the system matching option being
// enabled specifically and excluding the default), use that theme. We pick the lowest possible
// level for the setting to ensure the ordering otherwise works.
const themeExplicit = SettingsStore.getValueAt(
SettingLevel.DEVICE, "theme", null, false, true);
if (themeExplicit) {
console.log("returning explicit theme: " + themeExplicit);
return themeExplicit;
}
// If the user hasn't really made a preference in either direction, assume the defaults of the
// settings and use those.
if (SettingsStore.getValue('use_system_theme')) {
if (this._preferDark.matches) return 'dark';
if (this._preferLight.matches) return 'light';
}
console.log("returning theme value");
return SettingsStore.getValue('theme');
}
isSystemThemeSupported() {
return this._preferDark.matches || this._preferLight.matches;
}
}
export function enumerateThemes() { export function enumerateThemes() {
const BUILTIN_THEMES = { const BUILTIN_THEMES = {

View file

@ -0,0 +1,77 @@
/*
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 { _t } from "../languageHandler";
import dis from "../dispatcher/dispatcher";
import Analytics from "../Analytics";
import AccessibleButton from "../components/views/elements/AccessibleButton";
import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
const onAccept = () => {
console.log("DEBUG onAccept AnalyticsToast");
dis.dispatch({
action: 'accept_cookies',
});
};
const onReject = () => {
console.log("DEBUG onReject AnalyticsToast");
dis.dispatch({
action: "reject_cookies",
});
};
const onUsageDataClicked = () => {
Analytics.showDetailsModal();
};
const TOAST_KEY = "analytics";
export const showToast = (policyUrl?: string) => {
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Help us improve Riot"),
props: {
description: _t(
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve Riot. " +
"This will use a <PolicyLink>cookie</PolicyLink>.",
{},
{
"UsageDataLink": (sub) => (
<AccessibleButton kind="link" onClick={onUsageDataClicked}>{ sub }</AccessibleButton>
),
// XXX: We need to link to the page that explains our cookies
"PolicyLink": (sub) => policyUrl ? (
<a target="_blank" href={policyUrl}>{ sub }</a>
) : sub,
},
),
acceptLabel: _t("I want to help"),
onAccept,
rejectLabel: _t("No"),
onReject,
},
component: GenericToast,
priority: 10,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View file

@ -0,0 +1,58 @@
/*
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 { _t } from '../languageHandler';
import dis from "../dispatcher/dispatcher";
import { MatrixClientPeg } from '../MatrixClientPeg';
import DeviceListener from '../DeviceListener';
import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
const TOAST_KEY = "reviewsessions";
export const showToast = (deviceIds: Set<string>) => {
const onAccept = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds);
dis.dispatch({
action: 'view_user_info',
userId: MatrixClientPeg.get().getUserId(),
});
};
const onReject = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds);
};
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Review where youre logged in"),
icon: "verification_warning",
props: {
description: _t("Verify all your sessions to ensure your account & messages are safe"),
acceptLabel: _t("Review"),
onAccept,
rejectLabel: _t("Later"),
onReject,
},
component: GenericToast,
priority: 50,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View file

@ -0,0 +1,50 @@
/*
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 { _t } from "../languageHandler";
import Notifier from "../Notifier";
import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
const onAccept = () => {
Notifier.setEnabled(true);
};
const onReject = () => {
Notifier.setToolbarHidden(true);
};
const TOAST_KEY = "desktopnotifications";
export const showToast = () => {
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Notifications"),
props: {
description: _t("You are not receiving desktop notifications"),
acceptLabel: _t("Enable them now"),
onAccept,
rejectLabel: _t("Close"),
onReject,
},
component: GenericToast,
priority: 30,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View file

@ -0,0 +1,50 @@
/*
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 { _t, _td } from "../languageHandler";
import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
import {messageForResourceLimitError} from "../utils/ErrorUtils";
const TOAST_KEY = "serverlimit";
export const showToast = (limitType: string, adminContact?: string, syncError?: boolean) => {
const errorText = messageForResourceLimitError(limitType, adminContact, {
'monthly_active_user': _td("Your homeserver has exceeded its user limit."),
'': _td("Your homeserver has exceeded one of its resource limits."),
});
const contactText = messageForResourceLimitError(limitType, adminContact, {
'': _td("Contact your <a>server admin</a>."),
});
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Warning"),
props: {
description: <React.Fragment>{errorText} {contactText}</React.Fragment>,
acceptLabel: _t("Ok"),
onAccept: hideToast,
},
component: GenericToast,
priority: 70,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View file

@ -0,0 +1,47 @@
/*
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 { _t } from "../languageHandler";
import Modal from "../Modal";
import SetPasswordDialog from "../components/views/dialogs/SetPasswordDialog";
import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
const onAccept = () => {
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog);
};
const TOAST_KEY = "setpassword";
export const showToast = () => {
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Set password"),
props: {
description: _t("To return to your account in future you need to set a password"),
acceptLabel: _t("Set Password"),
onAccept,
rejectLabel: _t("Later"),
onReject: hideToast, // it'll return on reload
},
component: GenericToast,
priority: 60,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View file

@ -0,0 +1,106 @@
/*
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 Modal from "../Modal";
import * as sdk from "../index";
import { _t } from "../languageHandler";
import DeviceListener from "../DeviceListener";
import SetupEncryptionDialog from "../components/views/dialogs/SetupEncryptionDialog";
import { accessSecretStorage } from "../CrossSigningManager";
import ToastStore from "../stores/ToastStore";
import GenericToast from "../components/views/toasts/GenericToast";
const TOAST_KEY = "setupencryption";
const getTitle = (kind: Kind) => {
switch (kind) {
case Kind.SET_UP_ENCRYPTION:
return _t("Set up encryption");
case Kind.UPGRADE_ENCRYPTION:
return _t("Encryption upgrade available");
case Kind.VERIFY_THIS_SESSION:
return _t("Verify this session");
}
};
const getSetupCaption = (kind: Kind) => {
switch (kind) {
case Kind.SET_UP_ENCRYPTION:
return _t("Set up");
case Kind.UPGRADE_ENCRYPTION:
return _t("Upgrade");
case Kind.VERIFY_THIS_SESSION:
return _t("Verify");
}
};
const getDescription = (kind: Kind) => {
switch (kind) {
case Kind.SET_UP_ENCRYPTION:
case Kind.UPGRADE_ENCRYPTION:
return _t("Verify yourself & others to keep your chats safe");
case Kind.VERIFY_THIS_SESSION:
return _t("Other users may not trust it");
}
};
export enum Kind {
SET_UP_ENCRYPTION = "set_up_encryption",
UPGRADE_ENCRYPTION = "upgrade_encryption",
VERIFY_THIS_SESSION = "verify_this_session",
}
const onReject = () => {
DeviceListener.sharedInstance().dismissEncryptionSetup();
};
export const showToast = (kind: Kind) => {
const onAccept = async () => {
if (kind === Kind.VERIFY_THIS_SESSION) {
Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog,
{}, null, /* priority = */ false, /* static = */ true);
} else {
const Spinner = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(
Spinner, null, "mx_Dialog_spinner", /* priority */ false, /* static */ true,
);
try {
await accessSecretStorage();
} finally {
modal.close();
}
}
};
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: getTitle(kind),
icon: "verification_warning",
props: {
description: getDescription(kind),
acceptLabel: getSetupCaption(kind),
onAccept,
rejectLabel: _t("Later"),
onReject,
},
component: GenericToast,
priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View file

@ -0,0 +1,70 @@
/*
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 { _t } from '../languageHandler';
import { MatrixClientPeg } from '../MatrixClientPeg';
import Modal from '../Modal';
import DeviceListener from '../DeviceListener';
import NewSessionReviewDialog from '../components/views/dialogs/NewSessionReviewDialog';
import ToastStore from "../stores/ToastStore";
import GenericToast from "../components/views/toasts/GenericToast";
function toastKey(deviceId: string) {
return "unverified_session_" + deviceId;
}
export const showToast = (deviceId: string) => {
const cli = MatrixClientPeg.get();
const onAccept = () => {
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
userId: cli.getUserId(),
device: cli.getStoredDevice(cli.getUserId(), deviceId),
onFinished: (r) => {
if (!r) {
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
}
},
}, null, /* priority = */ false, /* static = */ true);
};
const onReject = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
};
const device = cli.getStoredDevice(cli.getUserId(), deviceId);
ToastStore.sharedInstance().addOrReplaceToast({
key: toastKey(deviceId),
title: _t("New login. Was this you?"),
icon: "verification_warning",
props: {
description: _t(
"Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()}),
acceptLabel: _t("Verify"),
onAccept,
rejectLabel: _t("Later"),
onReject,
},
component: GenericToast,
priority: 80,
});
};
export const hideToast = (deviceId: string) => {
ToastStore.sharedInstance().dismissToast(deviceId);
};

View file

@ -0,0 +1,90 @@
/*
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 { _t } from "../languageHandler";
import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
import QuestionDialog from "../components/views/dialogs/QuestionDialog";
import ChangelogDialog from "../components/views/dialogs/ChangelogDialog";
import PlatformPeg from "../PlatformPeg";
import Modal from "../Modal";
const TOAST_KEY = "update";
/*
* Check a version string is compatible with the Changelog
* dialog ([riot-version]-react-[react-sdk-version]-js-[js-sdk-version])
*/
function checkVersion(ver) {
const parts = ver.split('-');
return parts.length === 5 && parts[1] === 'react' && parts[3] === 'js';
}
function installUpdate() {
PlatformPeg.get().installUpdate();
}
export const showToast = (version: string, newVersion: string, releaseNotes?: string) => {
let onAccept;
let acceptLabel = _t("What's new?");
if (releaseNotes) {
onAccept = () => {
Modal.createTrackedDialog('Display release notes', '', QuestionDialog, {
title: _t("What's New"),
description: <pre>{releaseNotes}</pre>,
button: _t("Update"),
onFinished: (update) => {
if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate();
}
},
});
};
} else if (checkVersion(version) && checkVersion(newVersion)) {
onAccept = () => {
Modal.createTrackedDialog('Display Changelog', '', ChangelogDialog, {
version,
newVersion,
onFinished: (update) => {
if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate();
}
},
});
};
} else {
onAccept = installUpdate;
acceptLabel = _t("Restart");
}
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Upgrade your Riot"),
props: {
description: _t("A new version of Riot is available!"),
acceptLabel,
onAccept,
},
component: GenericToast,
priority: 20,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View file

@ -0,0 +1,34 @@
/*
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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MatrixClient } from "matrix-js-sdk/src/client";
/**
* Determines if a read receipt update event includes the client's own user.
* @param event The event to check.
* @param client The client to check against.
* @returns True if the read receipt update includes the client, false otherwise.
*/
export function readReceiptChangeIsFor(event: MatrixEvent, client: MatrixClient): boolean {
const myUserId = client.getUserId();
for (const eventId of Object.keys(event.getContent())) {
const receiptUsers = Object.keys(event.getContent()[eventId]['m.read'] || {});
if (receiptUsers.includes(myUserId)) {
return true;
}
}
}

View file

@ -17,6 +17,7 @@ limitations under the License.
const {range} = require('./util'); const {range} = require('./util');
const signup = require('./usecases/signup'); const signup = require('./usecases/signup');
const toastScenarios = require('./scenarios/toast');
const roomDirectoryScenarios = require('./scenarios/directory'); const roomDirectoryScenarios = require('./scenarios/directory');
const lazyLoadingScenarios = require('./scenarios/lazy-loading'); const lazyLoadingScenarios = require('./scenarios/lazy-loading');
const e2eEncryptionScenarios = require('./scenarios/e2e-encryption'); const e2eEncryptionScenarios = require('./scenarios/e2e-encryption');
@ -37,6 +38,7 @@ module.exports = async function scenario(createSession, restCreator) {
const alice = await createUser("alice"); const alice = await createUser("alice");
const bob = await createUser("bob"); const bob = await createUser("bob");
await toastScenarios(alice, bob);
await roomDirectoryScenarios(alice, bob); await roomDirectoryScenarios(alice, bob);
await e2eEncryptionScenarios(alice, bob); await e2eEncryptionScenarios(alice, bob);
console.log("create REST users:"); console.log("create REST users:");

View file

@ -0,0 +1,49 @@
/*
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.
*/
const {assertNoToasts, acceptToast, rejectToast} = require("../usecases/toasts");
module.exports = async function toastScenarios(alice, bob) {
console.log(" checking and clearing toasts:");
alice.log.startGroup(`clears toasts`);
alice.log.step(`reject desktop notifications toast`);
await rejectToast(alice, "Notifications");
alice.log.done();
alice.log.step(`accepts analytics toast`);
await acceptToast(alice, "Help us improve Riot");
alice.log.done();
alice.log.step(`checks no remaining toasts`);
await assertNoToasts(alice);
alice.log.done();
alice.log.endGroup();
bob.log.startGroup(`clears toasts`);
bob.log.step(`reject desktop notifications toast`);
await rejectToast(bob, "Notifications");
bob.log.done();
bob.log.step(`reject analytics toast`);
await rejectToast(bob, "Help us improve Riot");
bob.log.done();
bob.log.step(`checks no remaining toasts`);
await assertNoToasts(bob);
bob.log.done();
bob.log.endGroup();
};

View file

@ -122,8 +122,8 @@ module.exports = class RiotSession {
await input.type(text); await input.type(text);
} }
query(selector, timeout = DEFAULT_TIMEOUT) { query(selector, timeout = DEFAULT_TIMEOUT, hidden = false) {
return this.page.waitForSelector(selector, {visible: true, timeout}); return this.page.waitForSelector(selector, {visible: true, timeout, hidden});
} }
async queryAll(selector) { async queryAll(selector) {

View file

@ -20,7 +20,7 @@ const assert = require('assert');
async function assertDialog(session, expectedTitle) { async function assertDialog(session, expectedTitle) {
const titleElement = await session.query(".mx_Dialog .mx_Dialog_title"); const titleElement = await session.query(".mx_Dialog .mx_Dialog_title");
const dialogHeader = await session.innerText(titleElement); const dialogHeader = await session.innerText(titleElement);
assert(dialogHeader, expectedTitle); assert.equal(dialogHeader, expectedTitle);
} }
async function acceptDialog(session, expectedTitle) { async function acceptDialog(session, expectedTitle) {

View file

@ -0,0 +1,47 @@
/*
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.
*/
const assert = require('assert');
async function assertNoToasts(session) {
try {
await session.query('.mx_Toast_toast', 1000, true);
} catch (e) {
const h2Element = await session.query('.mx_Toast_title h2', 1000);
const toastTitle = await session.innerText(h2Element);
throw new Error(`"${toastTitle}" toast found when none expected`);
}
}
async function assertToast(session, expectedTitle) {
const h2Element = await session.query('.mx_Toast_title h2');
const toastTitle = await session.innerText(h2Element);
assert.equal(toastTitle, expectedTitle);
}
async function acceptToast(session, expectedTitle) {
await assertToast(session, expectedTitle);
const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_primary');
await btn.click();
}
async function rejectToast(session, expectedTitle) {
await assertToast(session, expectedTitle);
const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_danger');
await btn.click();
}
module.exports = {assertNoToasts, assertToast, acceptToast, rejectToast};

855
yarn.lock

File diff suppressed because it is too large Load diff