mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 11:47:23 +03:00
Merge branch 'develop' into joriks/fix-filepanel-regression
This commit is contained in:
commit
b61f1704d3
80 changed files with 2126 additions and 2464 deletions
|
@ -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",
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
4
src/@types/global.d.ts
vendored
4
src/@types/global.d.ts
vendored
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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> {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 you’re 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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, {
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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
|
|
@ -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")}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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>);
|
|
||||||
}
|
|
||||||
}
|
|
42
src/components/views/toasts/GenericToast.tsx
Normal file
42
src/components/views/toasts/GenericToast.tsx
Normal 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;
|
|
@ -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>);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
};
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
35
src/dispatcher/payloads/ViewTooltipPayload.ts
Normal file
35
src/dispatcher/payloads/ViewTooltipPayload.ts
Normal 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
|
||||||
|
}
|
|
@ -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 you’re logged in": "Review where you’re 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 you’re logged in": "Review where you’re 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 it’s secure.": "In encrypted rooms, verify all users to ensure it’s secure.",
|
"In encrypted rooms, verify all users to ensure it’s secure.": "In encrypted rooms, verify all users to ensure it’s 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",
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
138
src/settings/watchers/ThemeWatcher.ts
Normal file
138
src/settings/watchers/ThemeWatcher.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
20
src/settings/watchers/Watcher.ts
Normal file
20
src/settings/watchers/Watcher.ts
Normal 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
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
93
src/stores/ToastStore.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
110
src/theme.js
110
src/theme.js
|
@ -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 = {
|
||||||
|
|
77
src/toasts/AnalyticsToast.tsx
Normal file
77
src/toasts/AnalyticsToast.tsx
Normal 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);
|
||||||
|
};
|
58
src/toasts/BulkUnverifiedSessionsToast.ts
Normal file
58
src/toasts/BulkUnverifiedSessionsToast.ts
Normal 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 you’re 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);
|
||||||
|
};
|
50
src/toasts/DesktopNotificationsToast.ts
Normal file
50
src/toasts/DesktopNotificationsToast.ts
Normal 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);
|
||||||
|
};
|
50
src/toasts/ServerLimitToast.tsx
Normal file
50
src/toasts/ServerLimitToast.tsx
Normal 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);
|
||||||
|
};
|
47
src/toasts/SetPasswordToast.ts
Normal file
47
src/toasts/SetPasswordToast.ts
Normal 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);
|
||||||
|
};
|
106
src/toasts/SetupEncryptionToast.ts
Normal file
106
src/toasts/SetupEncryptionToast.ts
Normal 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);
|
||||||
|
};
|
70
src/toasts/UnverifiedSessionToast.ts
Normal file
70
src/toasts/UnverifiedSessionToast.ts
Normal 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);
|
||||||
|
};
|
90
src/toasts/UpdateToast.tsx
Normal file
90
src/toasts/UpdateToast.tsx
Normal 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);
|
||||||
|
};
|
34
src/utils/read-receipts.ts
Normal file
34
src/utils/read-receipts.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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:");
|
||||||
|
|
49
test/end-to-end-tests/src/scenarios/toast.js
Normal file
49
test/end-to-end-tests/src/scenarios/toast.js
Normal 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();
|
||||||
|
};
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
47
test/end-to-end-tests/src/usecases/toasts.js
Normal file
47
test/end-to-end-tests/src/usecases/toasts.js
Normal 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};
|
Loading…
Reference in a new issue