Merge branches 'develop' and 't3chguy/render_html_entities_in_og-description' of github.com:matrix-org/matrix-react-sdk into t3chguy/render_html_entities_in_og-description
Conflicts: package.jsonpull/21833/head
commit
4bea87f00d
|
@ -82,6 +82,7 @@
|
||||||
"glob-to-regexp": "^0.4.1",
|
"glob-to-regexp": "^0.4.1",
|
||||||
"highlight.js": "^9.15.8",
|
"highlight.js": "^9.15.8",
|
||||||
"html-entities": "^1.2.1",
|
"html-entities": "^1.2.1",
|
||||||
|
"humanize": "^0.0.9",
|
||||||
"is-ip": "^2.0.0",
|
"is-ip": "^2.0.0",
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.6",
|
"linkifyjs": "^2.1.6",
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
||||||
@import "./views/dialogs/_CreateGroupDialog.scss";
|
@import "./views/dialogs/_CreateGroupDialog.scss";
|
||||||
@import "./views/dialogs/_CreateRoomDialog.scss";
|
@import "./views/dialogs/_CreateRoomDialog.scss";
|
||||||
|
@import "./views/dialogs/_DMInviteDialog.scss";
|
||||||
@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";
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_addressBar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_editor {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%; // Needed to make the Field inside grow
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Field {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_goButton {
|
||||||
|
width: 48px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_section {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $muted-fg-color;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_roomTile {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $user-tile-hover-bg-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_roomTile_name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_roomTile_userId {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $muted-fg-color;
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_roomTile_time {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $muted-fg-color;
|
||||||
|
float: right;
|
||||||
|
line-height: 36px; // Height of the avatar to keep the time vertically aligned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 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.
|
||||||
|
@ -353,7 +354,6 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
|
||||||
left: 46px;
|
left: 46px;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
cursor: pointer;
|
|
||||||
display: block;
|
display: block;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
|
@ -40,4 +40,5 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_RoomRecoveryReminder_secondary {
|
.mx_RoomRecoveryReminder_secondary {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ $room-highlight-color: #343a46;
|
||||||
// typical text (dark-on-white in light skin)
|
// typical text (dark-on-white in light skin)
|
||||||
$primary-fg-color: $text-primary-color;
|
$primary-fg-color: $text-primary-color;
|
||||||
$primary-bg-color: $bg-color;
|
$primary-bg-color: $bg-color;
|
||||||
|
$muted-fg-color: $header-panel-text-primary-color;
|
||||||
|
|
||||||
// used for dialog box text
|
// used for dialog box text
|
||||||
$light-fg-color: $header-panel-text-secondary-color;
|
$light-fg-color: $header-panel-text-secondary-color;
|
||||||
|
@ -172,6 +173,8 @@ $interactive-tooltip-fg-color: #ffffff;
|
||||||
|
|
||||||
$breadcrumb-placeholder-bg-color: #272c35;
|
$breadcrumb-placeholder-bg-color: #272c35;
|
||||||
|
|
||||||
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -21,6 +21,7 @@ $header-panel-bg-color: #f3f8fd;
|
||||||
// typical text (dark-on-white in light skin)
|
// typical text (dark-on-white in light skin)
|
||||||
$primary-fg-color: #2e2f32;
|
$primary-fg-color: #2e2f32;
|
||||||
$primary-bg-color: #ffffff;
|
$primary-bg-color: #ffffff;
|
||||||
|
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
|
||||||
|
|
||||||
// used for dialog box text
|
// used for dialog box text
|
||||||
$light-fg-color: #747474;
|
$light-fg-color: #747474;
|
||||||
|
@ -293,6 +294,8 @@ $interactive-tooltip-fg-color: #ffffff;
|
||||||
|
|
||||||
$breadcrumb-placeholder-bg-color: #e8eef5;
|
$breadcrumb-placeholder-bg-color: #e8eef5;
|
||||||
|
|
||||||
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -36,7 +36,8 @@ echo "--- Install synapse & other dependencies"
|
||||||
./install.sh
|
./install.sh
|
||||||
# install static webserver to server symlinked local copy of riot
|
# install static webserver to server symlinked local copy of riot
|
||||||
./riot/install-webserver.sh
|
./riot/install-webserver.sh
|
||||||
mkdir logs || rm -r logs/*
|
rm -r logs || true
|
||||||
|
mkdir logs
|
||||||
echo "+++ Running end-to-end tests"
|
echo "+++ Running end-to-end tests"
|
||||||
TESTS_STARTED=1
|
TESTS_STARTED=1
|
||||||
./run.sh --no-sandbox --log-directory logs/
|
./run.sh --no-sandbox --log-directory logs/
|
||||||
|
|
|
@ -97,7 +97,7 @@ export const crossSigningCallbacks = {
|
||||||
*
|
*
|
||||||
* Additionally, the secret storage keys are cached during the scope of this function
|
* Additionally, the secret storage keys are cached during the scope of this function
|
||||||
* to ensure the user is prompted only once for their secret storage
|
* to ensure the user is prompted only once for their secret storage
|
||||||
* passphrase. The cache is then
|
* passphrase. The cache is then cleared once the provided function completes.
|
||||||
*
|
*
|
||||||
* @param {Function} [func] An operation to perform once secret storage has been
|
* @param {Function} [func] An operation to perform once secret storage has been
|
||||||
* bootstrapped. Optional.
|
* bootstrapped. Optional.
|
||||||
|
|
|
@ -111,6 +111,12 @@ export default class KeyRequestHandler {
|
||||||
this._currentUser = null;
|
this._currentUser = null;
|
||||||
this._currentDevice = 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) {
|
if (r) {
|
||||||
for (const req of this._pendingKeyRequests[userId][deviceId]) {
|
for (const req of this._pendingKeyRequests[userId][deviceId]) {
|
||||||
req.share();
|
req.share();
|
||||||
|
|
|
@ -25,6 +25,7 @@ import sdk from './';
|
||||||
import dis from './dispatcher';
|
import dis from './dispatcher';
|
||||||
import DMRoomMap from './utils/DMRoomMap';
|
import DMRoomMap from './utils/DMRoomMap';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invites multiple addresses to a room
|
* Invites multiple addresses to a room
|
||||||
|
@ -41,6 +42,18 @@ function inviteMultipleToRoom(roomId, addrs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showStartChatInviteDialog() {
|
export function showStartChatInviteDialog() {
|
||||||
|
if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) {
|
||||||
|
const DMInviteDialog = sdk.getComponent("dialogs.DMInviteDialog");
|
||||||
|
Modal.createTrackedDialog('Start DM', '', DMInviteDialog, {
|
||||||
|
onFinished: (inviteIds) => {
|
||||||
|
// TODO: Replace _onStartDmFinished with less hacks
|
||||||
|
if (inviteIds.length > 0) _onStartDmFinished(true, inviteIds.map(i => ({address: i})));
|
||||||
|
// else ignore and just do nothing
|
||||||
|
},
|
||||||
|
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
|
|
||||||
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
|
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
|
||||||
|
@ -99,7 +112,7 @@ export function isValid3pidInvite(event) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Immutable DMs replaces this
|
// TODO: Canonical DMs replaces this
|
||||||
function _onStartDmFinished(shouldInvite, addrs) {
|
function _onStartDmFinished(shouldInvite, addrs) {
|
||||||
if (!shouldInvite) return;
|
if (!shouldInvite) return;
|
||||||
|
|
||||||
|
|
|
@ -780,54 +780,52 @@ export const CommandMap = {
|
||||||
const deviceId = matches[2];
|
const deviceId = matches[2];
|
||||||
const fingerprint = matches[3];
|
const fingerprint = matches[3];
|
||||||
|
|
||||||
return success(
|
return success((async () => {
|
||||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
const device = await cli.getStoredDevice(userId, deviceId);
|
||||||
// in future
|
if (!device) {
|
||||||
Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => {
|
throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`);
|
||||||
if (!device) {
|
}
|
||||||
throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`);
|
const deviceTrust = await cli.checkDeviceTrust(userId, deviceId);
|
||||||
}
|
|
||||||
|
|
||||||
if (device.isVerified()) {
|
if (deviceTrust.isVerified()) {
|
||||||
if (device.getFingerprint() === fingerprint) {
|
if (device.getFingerprint() === fingerprint) {
|
||||||
throw new Error(_t('Device already verified!'));
|
throw new Error(_t('Device already verified!'));
|
||||||
} else {
|
} else {
|
||||||
throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!'));
|
throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!'));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (device.getFingerprint() !== fingerprint) {
|
if (device.getFingerprint() !== fingerprint) {
|
||||||
const fprint = device.getFingerprint();
|
const fprint = device.getFingerprint();
|
||||||
throw new Error(
|
throw new Error(
|
||||||
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
||||||
' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
|
' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
|
||||||
'"%(fingerprint)s". This could mean your communications are being intercepted!',
|
'"%(fingerprint)s". This could mean your communications are being intercepted!',
|
||||||
{
|
{
|
||||||
fprint,
|
fprint,
|
||||||
userId,
|
userId,
|
||||||
deviceId,
|
deviceId,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return cli.setDeviceVerified(userId, deviceId, true);
|
await cli.setDeviceVerified(userId, deviceId, true);
|
||||||
}).then(() => {
|
|
||||||
// Tell the user we verified everything
|
// Tell the user we verified everything
|
||||||
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
|
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
|
||||||
Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, {
|
Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, {
|
||||||
title: _t('Verified key'),
|
title: _t('Verified key'),
|
||||||
description: <div>
|
description: <div>
|
||||||
<p>
|
<p>
|
||||||
{
|
{
|
||||||
_t('The signing key you provided matches the signing key you received ' +
|
_t('The signing key you provided matches the signing key you received ' +
|
||||||
'from %(userId)s\'s device %(deviceId)s. Device marked as verified.',
|
'from %(userId)s\'s device %(deviceId)s. Device marked as verified.',
|
||||||
{userId, deviceId})
|
{userId, deviceId})
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>,
|
</div>,
|
||||||
});
|
});
|
||||||
}),
|
})());
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
|
|
@ -23,6 +23,9 @@ import { _t } from '../../../languageHandler';
|
||||||
const sdk = require('../../../index');
|
const sdk = require('../../../index');
|
||||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
|
||||||
|
// 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.
|
||||||
module.exports = createReactClass({
|
module.exports = createReactClass({
|
||||||
displayName: 'EncryptedEventDialog',
|
displayName: 'EncryptedEventDialog',
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
Copyright 2018, 2019 New Vector Ltd
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,11 +17,14 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import sdk from '../../../../index';
|
import sdk from '../../../../index';
|
||||||
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
||||||
import { scorePassword } from '../../../../utils/PasswordScorer';
|
import { scorePassword } from '../../../../utils/PasswordScorer';
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
|
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||||
|
import SettingsStore from '../../../../../lib/settings/SettingsStore';
|
||||||
|
|
||||||
const PHASE_PASSPHRASE = 0;
|
const PHASE_PASSPHRASE = 0;
|
||||||
const PHASE_PASSPHRASE_CONFIRM = 1;
|
const PHASE_PASSPHRASE_CONFIRM = 1;
|
||||||
|
@ -49,10 +52,20 @@ function selectText(target) {
|
||||||
* on the server.
|
* on the server.
|
||||||
*/
|
*/
|
||||||
export default class CreateKeyBackupDialog extends React.PureComponent {
|
export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
secureSecretStorage: PropTypes.bool,
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this._recoveryKeyNode = null;
|
||||||
|
this._keyBackupInfo = null;
|
||||||
|
this._setZxcvbnResultTimeout = null;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
secureSecretStorage: props.secureSecretStorage,
|
||||||
phase: PHASE_PASSPHRASE,
|
phase: PHASE_PASSPHRASE,
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
passPhraseConfirm: '',
|
passPhraseConfirm: '',
|
||||||
|
@ -61,12 +74,25 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
zxcvbnResult: null,
|
zxcvbnResult: null,
|
||||||
setPassPhrase: false,
|
setPassPhrase: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.state.secureSecretStorage === undefined) {
|
||||||
|
this.state.secureSecretStorage =
|
||||||
|
SettingsStore.isFeatureEnabled("feature_cross_signing");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're using secret storage, skip ahead to the backing up step, as
|
||||||
|
// `accessSecretStorage` will handle passphrases as needed.
|
||||||
|
if (this.state.secureSecretStorage) {
|
||||||
|
this.state.phase = PHASE_BACKINGUP;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentDidMount() {
|
||||||
this._recoveryKeyNode = null;
|
// If we're using secret storage, skip ahead to the backing up step, as
|
||||||
this._keyBackupInfo = null;
|
// `accessSecretStorage` will handle passphrases as needed.
|
||||||
this._setZxcvbnResultTimeout = null;
|
if (this.state.secureSecretStorage) {
|
||||||
|
this._createBackup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -103,15 +129,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
_createBackup = async () => {
|
_createBackup = async () => {
|
||||||
|
const { secureSecretStorage } = this.state;
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_BACKINGUP,
|
phase: PHASE_BACKINGUP,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
let info;
|
let info;
|
||||||
try {
|
try {
|
||||||
info = await MatrixClientPeg.get().createKeyBackupVersion(
|
if (secureSecretStorage) {
|
||||||
this._keyBackupInfo,
|
await accessSecretStorage(async () => {
|
||||||
);
|
info = await MatrixClientPeg.get().prepareKeyBackupVersion(
|
||||||
|
null /* random key */,
|
||||||
|
{ secureSecretStorage: true },
|
||||||
|
);
|
||||||
|
info = await MatrixClientPeg.get().createKeyBackupVersion(info);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
info = await MatrixClientPeg.get().createKeyBackupVersion(
|
||||||
|
this._keyBackupInfo,
|
||||||
|
);
|
||||||
|
}
|
||||||
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
|
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_DONE,
|
phase: PHASE_DONE,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018-2019 New Vector Ltd
|
Copyright 2018, 2019 New Vector 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.
|
||||||
|
@ -40,9 +41,11 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
|
||||||
|
|
||||||
onSetupClick = async () => {
|
onSetupClick = async () => {
|
||||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
|
Modal.createTrackedDialog(
|
||||||
onFinished: this.props.onFinished,
|
'Restore Backup', '', RestoreKeyBackupDialog, {
|
||||||
});
|
onFinished: this.props.onFinished,
|
||||||
|
}, null, /* priority = */ false, /* static = */ true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 New Vector 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.
|
||||||
|
@ -35,6 +36,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||||
import("./CreateKeyBackupDialog"),
|
import("./CreateKeyBackupDialog"),
|
||||||
|
null, null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1569,9 +1569,17 @@ export default createReactClass({
|
||||||
action: 'start_post_registration',
|
action: 'start_post_registration',
|
||||||
});
|
});
|
||||||
} else if (screen.indexOf('room/') == 0) {
|
} else if (screen.indexOf('room/') == 0) {
|
||||||
const segments = screen.substring(5).split('/');
|
// Rooms can have the following formats:
|
||||||
const roomString = segments[0];
|
// #room_alias:domain or !opaque_id:domain
|
||||||
let eventId = segments.splice(1).join("/"); // empty string if no event id given
|
const room = screen.substring(5);
|
||||||
|
const domainOffset = room.indexOf(':') + 1; // 0 in case room does not contain a :
|
||||||
|
let eventOffset = room.length;
|
||||||
|
// room aliases can contain slashes only look for slash after domain
|
||||||
|
if (room.substring(domainOffset).indexOf('/') > -1) {
|
||||||
|
eventOffset = domainOffset + room.substring(domainOffset).indexOf('/');
|
||||||
|
}
|
||||||
|
const roomString = room.substring(0, eventOffset);
|
||||||
|
let eventId = room.substring(eventOffset + 1); // empty string if no event id given
|
||||||
|
|
||||||
// Previously we pulled the eventID from the segments in such a way
|
// Previously we pulled the eventID from the segments in such a way
|
||||||
// where if there was no eventId then we'd get undefined. However, we
|
// where if there was no eventId then we'd get undefined. However, we
|
||||||
|
|
|
@ -0,0 +1,217 @@
|
||||||
|
/*
|
||||||
|
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 React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {_t} from "../../../languageHandler";
|
||||||
|
import sdk from "../../../index";
|
||||||
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
|
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||||
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
|
import {RoomMember} from "matrix-js-sdk/lib/matrix";
|
||||||
|
import * as humanize from "humanize";
|
||||||
|
|
||||||
|
// TODO: [TravisR] Make this generic for all kinds of invites
|
||||||
|
|
||||||
|
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||||
|
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||||
|
|
||||||
|
class DMRoomTile extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
member: PropTypes.object.isRequired,
|
||||||
|
lastActiveTs: PropTypes.number,
|
||||||
|
onToggle: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClick = (e) => {
|
||||||
|
// Stop the browser from highlighting text
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
this.props.onToggle(this.props.member.userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||||
|
|
||||||
|
let timestamp = null;
|
||||||
|
if (this.props.lastActiveTs) {
|
||||||
|
// TODO: [TravisR] Figure out how to i18n this
|
||||||
|
// `humanize` wants seconds for a timestamp, so divide by 1000
|
||||||
|
const humanTs = humanize.relativeTime(this.props.lastActiveTs / 1000);
|
||||||
|
timestamp = <span className='mx_DMInviteDialog_roomTile_time'>{humanTs}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mx_DMInviteDialog_roomTile' onClick={this._onClick}>
|
||||||
|
<MemberAvatar member={this.props.member} width={36} height={36} />
|
||||||
|
<span className='mx_DMInviteDialog_roomTile_name'>{this.props.member.name}</span>
|
||||||
|
<span className='mx_DMInviteDialog_roomTile_userId'>{this.props.member.userId}</span>
|
||||||
|
{timestamp}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DMInviteDialog extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
// Takes an array of user IDs/emails to invite.
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
targets: [], // string[] of mxids/email addresses
|
||||||
|
filterText: "",
|
||||||
|
recents: this._buildRecents(),
|
||||||
|
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildRecents(): {userId: string, user: RoomMember, lastActive: number} {
|
||||||
|
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals();
|
||||||
|
const recents = [];
|
||||||
|
for (const userId in rooms) {
|
||||||
|
const room = rooms[userId];
|
||||||
|
const member = room.getMember(userId);
|
||||||
|
if (!member) continue; // just skip people who don't have memberships for some reason
|
||||||
|
|
||||||
|
const lastEventTs = room.timeline && room.timeline.length
|
||||||
|
? room.timeline[room.timeline.length - 1].getTs()
|
||||||
|
: 0;
|
||||||
|
if (!lastEventTs) continue; // something weird is going on with this room
|
||||||
|
|
||||||
|
recents.push({userId, user: member, lastActive: lastEventTs});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the recents by last active to save us time later
|
||||||
|
recents.sort((a, b) => b.lastActive - a.lastActive);
|
||||||
|
|
||||||
|
return recents;
|
||||||
|
}
|
||||||
|
|
||||||
|
_startDm = () => {
|
||||||
|
this.props.onFinished(this.state.targets);
|
||||||
|
};
|
||||||
|
|
||||||
|
_cancel = () => {
|
||||||
|
this.props.onFinished([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
_updateFilter = (e) => {
|
||||||
|
this.setState({filterText: e.target.value});
|
||||||
|
};
|
||||||
|
|
||||||
|
_showMoreRecents = () => {
|
||||||
|
this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN});
|
||||||
|
};
|
||||||
|
|
||||||
|
_toggleMember = (userId) => {
|
||||||
|
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||||
|
const idx = targets.indexOf(userId);
|
||||||
|
if (idx >= 0) targets.splice(idx, 1);
|
||||||
|
else targets.push(userId);
|
||||||
|
this.setState({targets});
|
||||||
|
};
|
||||||
|
|
||||||
|
_renderRecents() {
|
||||||
|
if (!this.state.recents || this.state.recents.length === 0) return null;
|
||||||
|
|
||||||
|
// .slice() will return an incomplete array but won't error on us if we go too far
|
||||||
|
const toRender = this.state.recents.slice(0, this.state.numRecentsShown);
|
||||||
|
const hasMore = toRender.length < this.state.recents.length;
|
||||||
|
|
||||||
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||||
|
let showMore = null;
|
||||||
|
if (hasMore) {
|
||||||
|
showMore = (
|
||||||
|
<AccessibleButton onClick={this._showMoreRecents} kind="link">
|
||||||
|
{_t("Show more")}
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tiles = toRender.map(r => (
|
||||||
|
<DMRoomTile member={r.user} lastActiveTs={r.lastActive} key={r.userId} onToggle={this._toggleMember} />
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<div className='mx_DMInviteDialog_section'>
|
||||||
|
<h3>{_t("Recent Conversations")}</h3>
|
||||||
|
{tiles}
|
||||||
|
{showMore}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const Field = sdk.getComponent("elements.Field");
|
||||||
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||||
|
|
||||||
|
// Dev note: The use of Field is temporary/incomplete pending https://github.com/vector-im/riot-web/issues/11197
|
||||||
|
// For now, we just list who the targets are.
|
||||||
|
const editor = (
|
||||||
|
<div className='mx_DMInviteDialog_editor'>
|
||||||
|
<Field
|
||||||
|
id="inviteTargets"
|
||||||
|
value={this.state.filterText}
|
||||||
|
onChange={this._updateFilter}
|
||||||
|
placeholder="TODO: Implement filtering/searching (vector-im/riot-web#11199)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const targets = this.state.targets.map(t => <div key={t}>{t}</div>);
|
||||||
|
|
||||||
|
const userId = MatrixClientPeg.get().getUserId();
|
||||||
|
return (
|
||||||
|
<BaseDialog
|
||||||
|
className='mx_DMInviteDialog'
|
||||||
|
hasCancel={true}
|
||||||
|
onFinished={this._cancel}
|
||||||
|
title={_t("Direct Messages")}
|
||||||
|
>
|
||||||
|
<div className='mx_DMInviteDialog_content'>
|
||||||
|
<p>
|
||||||
|
{_t(
|
||||||
|
"If you can't find someone, ask them for their username, or share your " +
|
||||||
|
"username (%(userId)s) or <a>profile link</a>.",
|
||||||
|
{userId},
|
||||||
|
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{targets}
|
||||||
|
<div className='mx_DMInviteDialog_addressBar'>
|
||||||
|
{editor}
|
||||||
|
<AccessibleButton
|
||||||
|
kind="primary"
|
||||||
|
onClick={this._startDm}
|
||||||
|
className='mx_DMInviteDialog_goButton'
|
||||||
|
>
|
||||||
|
{_t("Go")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
{this._renderRecents()}
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
Copyright 2018, 2019 New Vector 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.
|
||||||
|
@ -94,10 +95,14 @@ export default class LogoutDialog extends React.Component {
|
||||||
// verified, so restore the backup which will give us the keys from it and
|
// verified, so restore the backup which will give us the keys from it and
|
||||||
// allow us to trust it (ie. upload keys to it)
|
// allow us to trust it (ie. upload keys to it)
|
||||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {});
|
Modal.createTrackedDialog(
|
||||||
|
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||||
|
/* priority = */ false, /* static = */ true,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||||
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||||
|
null, null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
Copyright 2018, 2019 New Vector 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.
|
||||||
|
@ -15,17 +16,18 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
|
||||||
import sdk from '../../../../index';
|
import sdk from '../../../../index';
|
||||||
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
|
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
|
||||||
|
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import {Key} from "../../../../Keyboard";
|
import {Key} from "../../../../Keyboard";
|
||||||
|
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||||
|
|
||||||
const RESTORE_TYPE_PASSPHRASE = 0;
|
const RESTORE_TYPE_PASSPHRASE = 0;
|
||||||
const RESTORE_TYPE_RECOVERYKEY = 1;
|
const RESTORE_TYPE_RECOVERYKEY = 1;
|
||||||
|
const RESTORE_TYPE_SECRET_STORAGE = 2;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Dialog for restoring e2e keys from a backup and the user's recovery key
|
* Dialog for restoring e2e keys from a backup and the user's recovery key
|
||||||
|
@ -35,6 +37,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
backupInfo: null,
|
backupInfo: null,
|
||||||
|
backupKeyStored: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
loadError: null,
|
loadError: null,
|
||||||
restoreError: null,
|
restoreError: null,
|
||||||
|
@ -73,7 +76,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
onFinished: () => {
|
onFinished: () => {
|
||||||
this._loadBackupStatus();
|
this._loadBackupStatus();
|
||||||
},
|
},
|
||||||
},
|
}, null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,6 +151,32 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _restoreWithSecretStorage() {
|
||||||
|
this.setState({
|
||||||
|
loading: true,
|
||||||
|
restoreError: null,
|
||||||
|
restoreType: RESTORE_TYPE_SECRET_STORAGE,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
// `accessSecretStorage` may prompt for storage access as needed.
|
||||||
|
const recoverInfo = await accessSecretStorage(async () => {
|
||||||
|
return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
|
||||||
|
this.state.backupInfo,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
recoverInfo,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Error restoring backup", e);
|
||||||
|
this.setState({
|
||||||
|
restoreError: e,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async _loadBackupStatus() {
|
async _loadBackupStatus() {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: true,
|
loading: true,
|
||||||
|
@ -155,10 +184,20 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||||
|
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored();
|
||||||
|
this.setState({
|
||||||
|
backupInfo,
|
||||||
|
backupKeyStored,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the backup key is stored, we can proceed directly to restore.
|
||||||
|
if (backupKeyStored) {
|
||||||
|
return this._restoreWithSecretStorage();
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loadError: null,
|
loadError: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
backupInfo,
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Error loading backup status", e);
|
console.log("Error loading backup status", e);
|
||||||
|
|
|
@ -22,6 +22,8 @@ import sdk from '../../../index';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
// XXX: This component is *not* cross-signing aware. Once everything is
|
||||||
|
// cross-signing, this component should just go away.
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
displayName: 'DeviceVerifyButtons',
|
displayName: 'DeviceVerifyButtons',
|
||||||
|
|
||||||
|
|
|
@ -74,17 +74,6 @@ const _getE2EStatus = (cli, userId, devices) => {
|
||||||
return "warning";
|
return "warning";
|
||||||
};
|
};
|
||||||
|
|
||||||
async function unverifyUser(matrixClient, userId) {
|
|
||||||
const devices = await matrixClient.getStoredDevicesForUser(userId);
|
|
||||||
for (const device of devices) {
|
|
||||||
if (device.isVerified()) {
|
|
||||||
matrixClient.setDeviceVerified(
|
|
||||||
userId, device.deviceId, false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDMForUser(matrixClient, userId) {
|
function openDMForUser(matrixClient, userId) {
|
||||||
const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||||
const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
|
const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
|
||||||
|
@ -331,14 +320,6 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => {
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let unverifyButton;
|
|
||||||
if (devices && devices.some(device => device.isVerified())) {
|
|
||||||
unverifyButton = (
|
|
||||||
<AccessibleButton onClick={() => unverifyUser(cli, member.userId)} className="mx_UserInfo_field mx_UserInfo_destructive">
|
|
||||||
{ _t('Unverify user') }
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserInfo_container">
|
<div className="mx_UserInfo_container">
|
||||||
|
@ -350,7 +331,6 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => {
|
||||||
{ insertPillButton }
|
{ insertPillButton }
|
||||||
{ inviteUserButton }
|
{ inviteUserButton }
|
||||||
{ ignoreButton }
|
{ ignoreButton }
|
||||||
{ unverifyButton }
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017 New Vector Ltd
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
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");
|
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.
|
||||||
|
@ -24,7 +24,6 @@ import PropTypes from 'prop-types';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
const classNames = require("classnames");
|
const classNames = require("classnames");
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
const Modal = require('../../../Modal');
|
|
||||||
|
|
||||||
const sdk = require('../../../index');
|
const sdk = require('../../../index');
|
||||||
const TextForEvent = require('../../../TextForEvent');
|
const TextForEvent = require('../../../TextForEvent');
|
||||||
|
@ -443,15 +442,6 @@ module.exports = createReactClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onCryptoClick: function(e) {
|
|
||||||
const event = this.props.mxEvent;
|
|
||||||
|
|
||||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
|
||||||
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
|
||||||
{event},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
onRequestKeysClick: function() {
|
onRequestKeysClick: function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
// Indicate in the UI that the keys have been requested (this is expected to
|
// Indicate in the UI that the keys have been requested (this is expected to
|
||||||
|
@ -479,11 +469,10 @@ module.exports = createReactClass({
|
||||||
|
|
||||||
_renderE2EPadlock: function() {
|
_renderE2EPadlock: function() {
|
||||||
const ev = this.props.mxEvent;
|
const ev = this.props.mxEvent;
|
||||||
const props = {onClick: this.onCryptoClick};
|
|
||||||
|
|
||||||
// event could not be decrypted
|
// event could not be decrypted
|
||||||
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
||||||
return <E2ePadlockUndecryptable {...props} />;
|
return <E2ePadlockUndecryptable />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// event is encrypted, display padlock corresponding to whether or not it is verified
|
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||||
|
@ -491,7 +480,7 @@ module.exports = createReactClass({
|
||||||
if (this.state.verified) {
|
if (this.state.verified) {
|
||||||
return; // no icon for verified
|
return; // no icon for verified
|
||||||
} else {
|
} else {
|
||||||
return (<E2ePadlockUnverified {...props} />);
|
return (<E2ePadlockUnverified />);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -508,7 +497,7 @@ module.exports = createReactClass({
|
||||||
return; // we expect this to be unencrypted
|
return; // we expect this to be unencrypted
|
||||||
}
|
}
|
||||||
// if the event is not encrypted, but it's an e2e room, show the open padlock
|
// if the event is not encrypted, but it's an e2e room, show the open padlock
|
||||||
return <E2ePadlockUnencrypted {...props} />;
|
return <E2ePadlockUnencrypted />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// no padlock needed
|
// no padlock needed
|
||||||
|
@ -920,7 +909,6 @@ class E2ePadlock extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
icon: PropTypes.string.isRequired,
|
icon: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
onClick: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -931,10 +919,6 @@ class E2ePadlock extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick = (e) => {
|
|
||||||
if (this.props.onClick) this.props.onClick(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
onHoverStart = () => {
|
onHoverStart = () => {
|
||||||
this.setState({hover: true});
|
this.setState({hover: true});
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,6 +23,8 @@ import classNames from 'classnames';
|
||||||
export default class MemberDeviceInfo extends React.Component {
|
export default class MemberDeviceInfo extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
|
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
|
||||||
|
// XXX: These checks are not cross-signing aware but this component is only used
|
||||||
|
// from the old, pre-cross-signing memberinfopanel
|
||||||
const iconClasses = classNames({
|
const iconClasses = classNames({
|
||||||
mx_MemberDeviceInfo_icon: true,
|
mx_MemberDeviceInfo_icon: true,
|
||||||
mx_MemberDeviceInfo_icon_blacklisted: this.props.device.isBlocked(),
|
mx_MemberDeviceInfo_icon_blacklisted: this.props.device.isBlocked(),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
Copyright 2018, 2019 New Vector 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.
|
||||||
|
@ -70,10 +71,14 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
||||||
// verified, so restore the backup which will give us the keys from it and
|
// verified, so restore the backup which will give us the keys from it and
|
||||||
// allow us to trust it (ie. upload keys to it)
|
// allow us to trust it (ie. upload keys to it)
|
||||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {});
|
Modal.createTrackedDialog(
|
||||||
|
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||||
|
/* priority = */ false, /* static = */ true,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||||
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||||
|
null, null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,14 +155,14 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
||||||
onClick={this.onSetupClick}>
|
onClick={this.onSetupClick}>
|
||||||
{setupCaption}
|
{setupCaption}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
<AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
||||||
onClick={this.onOnNotNowClick}>
|
onClick={this.onOnNotNowClick}>
|
||||||
{ _t("Not now") }
|
{ _t("Not now") }
|
||||||
</AccessibleButton></p>
|
</AccessibleButton>
|
||||||
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
<AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
||||||
onClick={this.onDontAskAgainClick}>
|
onClick={this.onDontAskAgainClick}>
|
||||||
{ _t("Don't ask me again") }
|
{ _t("Don't ask me again") }
|
||||||
</AccessibleButton></p>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 New Vector Ltd
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
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,7 +22,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import SettingsStore from '../../../../lib/settings/SettingsStore';
|
import SettingsStore from '../../../../lib/settings/SettingsStore';
|
||||||
import { accessSecretStorage } from '../../../CrossSigningManager';
|
|
||||||
|
|
||||||
export default class KeyBackupPanel extends React.PureComponent {
|
export default class KeyBackupPanel extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -128,36 +127,24 @@ export default class KeyBackupPanel extends React.PureComponent {
|
||||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||||
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||||
{
|
{
|
||||||
|
secureSecretStorage: false,
|
||||||
onFinished: () => {
|
onFinished: () => {
|
||||||
this._loadBackupStatus();
|
this._loadBackupStatus();
|
||||||
},
|
},
|
||||||
},
|
}, null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_startNewBackupWithSecureSecretStorage = async () => {
|
_startNewBackupWithSecureSecretStorage = async () => {
|
||||||
const cli = MatrixClientPeg.get();
|
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||||
let info;
|
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||||
try {
|
{
|
||||||
await accessSecretStorage(async () => {
|
secureSecretStorage: true,
|
||||||
info = await cli.prepareKeyBackupVersion(
|
onFinished: () => {
|
||||||
null /* random key */,
|
this._loadBackupStatus();
|
||||||
{ secureSecretStorage: true },
|
},
|
||||||
);
|
}, null, /* priority = */ false, /* static = */ true,
|
||||||
info = await cli.createKeyBackupVersion(info);
|
);
|
||||||
});
|
|
||||||
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
|
|
||||||
this._loadBackupStatus();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error creating key backup", e);
|
|
||||||
// TODO: If creating a version succeeds, but backup fails, should we
|
|
||||||
// delete the version, disable backup, or do nothing? If we just
|
|
||||||
// disable without deleting, we'll enable on next app reload since
|
|
||||||
// it is trusted.
|
|
||||||
if (info && info.version) {
|
|
||||||
MatrixClientPeg.get().deleteKeyBackupVersion(info.version);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_deleteBackup = () => {
|
_deleteBackup = () => {
|
||||||
|
@ -181,22 +168,11 @@ export default class KeyBackupPanel extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
_restoreBackup = async () => {
|
_restoreBackup = async () => {
|
||||||
// Use legacy path if backup key not stored in secret storage
|
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||||
if (!this.state.backupKeyStored) {
|
Modal.createTrackedDialog(
|
||||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog);
|
/* priority = */ false, /* static = */ true,
|
||||||
return;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await accessSecretStorage(async () => {
|
|
||||||
await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
|
|
||||||
this.state.backupInfo,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Error restoring backup", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -270,7 +246,7 @@ export default class KeyBackupPanel extends React.PureComponent {
|
||||||
{sub}
|
{sub}
|
||||||
</span>;
|
</span>;
|
||||||
const verify = sub =>
|
const verify = sub =>
|
||||||
<span className={sig.device && sig.device.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
|
<span className={sig.device && sig.deviceTrust.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
|
||||||
{sub}
|
{sub}
|
||||||
</span>;
|
</span>;
|
||||||
const device = sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>;
|
const device = sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>;
|
||||||
|
|
|
@ -358,6 +358,7 @@
|
||||||
"Render simple counters in room header": "Render simple counters in room header",
|
"Render simple counters in room header": "Render simple counters in room header",
|
||||||
"Multiple integration managers": "Multiple integration managers",
|
"Multiple integration managers": "Multiple integration managers",
|
||||||
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
|
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
|
||||||
|
"New DM invite dialog (under development)": "New DM invite dialog (under development)",
|
||||||
"Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)",
|
"Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)",
|
||||||
"Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)",
|
"Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)",
|
||||||
"Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages",
|
"Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages",
|
||||||
|
@ -1114,7 +1115,6 @@
|
||||||
"%(count)s verified sessions|other": "%(count)s verified sessions",
|
"%(count)s verified sessions|other": "%(count)s verified sessions",
|
||||||
"%(count)s verified sessions|one": "1 verified session",
|
"%(count)s verified sessions|one": "1 verified session",
|
||||||
"Direct message": "Direct message",
|
"Direct message": "Direct message",
|
||||||
"Unverify user": "Unverify user",
|
|
||||||
"Remove from community": "Remove from community",
|
"Remove from community": "Remove from community",
|
||||||
"Disinvite this user from community?": "Disinvite this user from community?",
|
"Disinvite this user from community?": "Disinvite this user from community?",
|
||||||
"Remove this user from community?": "Remove this user from community?",
|
"Remove this user from community?": "Remove this user from community?",
|
||||||
|
@ -1431,6 +1431,11 @@
|
||||||
"View Servers in Room": "View Servers in Room",
|
"View Servers in Room": "View Servers in Room",
|
||||||
"Toolbox": "Toolbox",
|
"Toolbox": "Toolbox",
|
||||||
"Developer Tools": "Developer Tools",
|
"Developer Tools": "Developer Tools",
|
||||||
|
"Show more": "Show more",
|
||||||
|
"Recent Conversations": "Recent Conversations",
|
||||||
|
"Direct Messages": "Direct Messages",
|
||||||
|
"If you can't find someone, ask them for their username, or share your username (%(userId)s) or <a>profile link</a>.": "If you can't find someone, ask them for their username, or share your username (%(userId)s) or <a>profile link</a>.",
|
||||||
|
"Go": "Go",
|
||||||
"An error has occurred.": "An error has occurred.",
|
"An error has occurred.": "An error has occurred.",
|
||||||
"Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.",
|
"Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.",
|
||||||
"Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.",
|
"Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.",
|
||||||
|
|
|
@ -27,6 +27,7 @@ import rageshake from './rageshake';
|
||||||
|
|
||||||
// polyfill textencoder if necessary
|
// polyfill textencoder if necessary
|
||||||
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
|
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
let TextEncoder = window.TextEncoder;
|
let TextEncoder = window.TextEncoder;
|
||||||
if (!TextEncoder) {
|
if (!TextEncoder) {
|
||||||
TextEncoder = TextEncodingUtf8.TextEncoder;
|
TextEncoder = TextEncodingUtf8.TextEncoder;
|
||||||
|
@ -85,6 +86,12 @@ export default async function sendBugReport(bugReportEndpoint, opts) {
|
||||||
body.append('label', opts.label);
|
body.append('label', opts.label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add labs options
|
||||||
|
const enabledLabs = SettingsStore.getLabsFeatures().filter(SettingsStore.isFeatureEnabled);
|
||||||
|
if (enabledLabs.length) {
|
||||||
|
body.append('enabled_labs', enabledLabs.join(', '));
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.sendLogs) {
|
if (opts.sendLogs) {
|
||||||
progressCallback(_t("Collecting logs"));
|
progressCallback(_t("Collecting logs"));
|
||||||
const logs = await rageshake.getLogsForReport();
|
const logs = await rageshake.getLogsForReport();
|
||||||
|
|
|
@ -128,6 +128,12 @@ export const SETTINGS = {
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"feature_ftue_dms": {
|
||||||
|
isFeature: true,
|
||||||
|
displayName: _td("New DM invite dialog (under development)"),
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"mjolnirRooms": {
|
"mjolnirRooms": {
|
||||||
supportedLevels: ['account'],
|
supportedLevels: ['account'],
|
||||||
default: [],
|
default: [],
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
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.
|
||||||
|
@ -16,6 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import _uniq from 'lodash/uniq';
|
import _uniq from 'lodash/uniq';
|
||||||
|
import {Room} from "matrix-js-sdk/lib/matrix";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that takes a Matrix Client and flips the m.direct map
|
* Class that takes a Matrix Client and flips the m.direct map
|
||||||
|
@ -144,6 +146,13 @@ export default class DMRoomMap {
|
||||||
return this.roomToUser[roomId];
|
return this.roomToUser[roomId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUniqueRoomsWithIndividuals(): {[userId: string]: Room} {
|
||||||
|
return Object.keys(this.roomToUser)
|
||||||
|
.map(r => ({userId: this.getUserIdForRoomId(r), room: this.matrixClient.getRoom(r)}))
|
||||||
|
.filter(r => r.userId && r.room && r.room.getInvitedAndJoinedMemberCount() === 2)
|
||||||
|
.reduce((obj, r) => (obj[r.userId] = r.room) && obj, {});
|
||||||
|
}
|
||||||
|
|
||||||
_getUserToRooms() {
|
_getUserToRooms() {
|
||||||
if (!this.userToRooms) {
|
if (!this.userToRooms) {
|
||||||
const userToRooms = this.mDirectEvent;
|
const userToRooms = this.mDirectEvent;
|
||||||
|
|
|
@ -4094,6 +4094,11 @@ humanize-ms@^1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.0.0"
|
ms "^2.0.0"
|
||||||
|
|
||||||
|
humanize@^0.0.9:
|
||||||
|
version "0.0.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/humanize/-/humanize-0.0.9.tgz#1994ffaecdfe9c441ed2bdac7452b7bb4c9e41a4"
|
||||||
|
integrity sha1-GZT/rs3+nEQe0r2sdFK3u0yeQaQ=
|
||||||
|
|
||||||
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
|
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
|
||||||
version "0.4.24"
|
version "0.4.24"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||||
|
|
Loading…
Reference in New Issue