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.json
pull/21833/head
Michael Telatynski 2020-01-05 22:23:03 +00:00
commit 4bea87f00d
31 changed files with 560 additions and 157 deletions

View File

@ -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",

View File

@ -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";

View File

@ -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
}
}

View File

@ -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;

View File

@ -40,4 +40,5 @@ limitations under the License.
.mx_RoomRecoveryReminder_secondary { .mx_RoomRecoveryReminder_secondary {
font-size: 90%; font-size: 90%;
margin-top: 1em;
} }

View File

@ -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 {

View File

@ -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 {

View File

@ -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/

View File

@ -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.

View File

@ -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();

View File

@ -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;

View File

@ -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());

View File

@ -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',

View File

@ -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,

View File

@ -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() {

View File

@ -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,
); );
} }

View File

@ -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

View File

@ -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>
);
}
}

View File

@ -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,
); );
} }

View File

@ -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);

View File

@ -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',

View File

@ -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>
); );

View File

@ -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});
}; };

View File

@ -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(),

View File

@ -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>
); );

View File

@ -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>;

View File

@ -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.",

View File

@ -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();

View File

@ -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: [],

View File

@ -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;

View File

@ -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"