mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 05:04:57 +08:00
Merge branch 'develop' into bwindels/verification-right-panel
This commit is contained in:
commit
5556cb5749
@ -123,7 +123,7 @@
|
||||
"@peculiar/webcrypto": "^1.0.22",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"chokidar": "^2.1.2",
|
||||
"chokidar": "^3.3.1",
|
||||
"concurrently": "^4.0.1",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.15.1",
|
||||
|
@ -57,13 +57,13 @@
|
||||
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
||||
@import "./views/dialogs/_CreateGroupDialog.scss";
|
||||
@import "./views/dialogs/_CreateRoomDialog.scss";
|
||||
@import "./views/dialogs/_DMInviteDialog.scss";
|
||||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||
@import "./views/dialogs/_DeviceVerifyDialog.scss";
|
||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||
@import "./views/dialogs/_EncryptedEventDialog.scss";
|
||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||
@import "./views/dialogs/_InviteDialog.scss";
|
||||
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
||||
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
||||
@import "./views/dialogs/_RoomUpgradeDialog.scss";
|
||||
|
@ -51,7 +51,7 @@ limitations under the License.
|
||||
&.mx_Toast_hasIcon {
|
||||
&::after {
|
||||
content: "";
|
||||
width: 20px;
|
||||
width: 21px;
|
||||
height: 20px;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
@ -64,6 +64,10 @@ limitations under the License.
|
||||
background-color: $primary-fg-color;
|
||||
}
|
||||
|
||||
&.mx_Toast_icon_verification_warning::after {
|
||||
background-image: url("$(res)/img/e2e/warning.svg");
|
||||
}
|
||||
|
||||
h2, .mx_Toast_body {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_DMInviteDialog_addressBar {
|
||||
.mx_InviteDialog_addressBar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.mx_DMInviteDialog_editor {
|
||||
.mx_InviteDialog_editor {
|
||||
flex: 1;
|
||||
width: 100%; // Needed to make the Field inside grow
|
||||
background-color: $user-tile-hover-bg-color;
|
||||
@ -28,7 +28,7 @@ limitations under the License.
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
.mx_DMInviteDialog_userTile {
|
||||
.mx_InviteDialog_userTile {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
position: relative;
|
||||
@ -61,15 +61,26 @@ limitations under the License.
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_goButton {
|
||||
.mx_InviteDialog_goButton {
|
||||
width: 48px;
|
||||
margin-left: 10px;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_buttonAndSpinner {
|
||||
.mx_Spinner {
|
||||
// Width and height are required to trick the layout engine.
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 5px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_section {
|
||||
.mx_InviteDialog_section {
|
||||
padding-bottom: 10px;
|
||||
|
||||
h3 {
|
||||
@ -80,7 +91,7 @@ limitations under the License.
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_roomTile {
|
||||
.mx_InviteDialog_roomTile {
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
|
||||
@ -93,7 +104,7 @@ limitations under the License.
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_roomTile_avatarStack {
|
||||
.mx_InviteDialog_roomTile_avatarStack {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 36px;
|
||||
@ -106,7 +117,7 @@ limitations under the License.
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_roomTile_selected {
|
||||
.mx_InviteDialog_roomTile_selected {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 36px;
|
||||
@ -130,20 +141,20 @@ limitations under the License.
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_roomTile_name {
|
||||
.mx_InviteDialog_roomTile_name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: $primary-fg-color;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_roomTile_userId {
|
||||
.mx_InviteDialog_roomTile_userId {
|
||||
font-size: 12px;
|
||||
color: $muted-fg-color;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_roomTile_time {
|
||||
.mx_InviteDialog_roomTile_time {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: $muted-fg-color;
|
||||
@ -151,16 +162,16 @@ limitations under the License.
|
||||
line-height: 36px; // Height of the avatar to keep the time vertically aligned
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_roomTile_highlight {
|
||||
.mx_InviteDialog_roomTile_highlight {
|
||||
font-weight: 900;
|
||||
}
|
||||
}
|
||||
|
||||
// Many of these styles are stolen from mx_UserPill, but adjusted for the invite dialog.
|
||||
.mx_DMInviteDialog_userTile {
|
||||
.mx_InviteDialog_userTile {
|
||||
margin-right: 8px;
|
||||
|
||||
.mx_DMInviteDialog_userTile_pill {
|
||||
.mx_InviteDialog_userTile_pill {
|
||||
background-color: $username-variant1-color;
|
||||
border-radius: 12px;
|
||||
display: inline-block;
|
||||
@ -170,27 +181,27 @@ limitations under the License.
|
||||
padding-right: 8px;
|
||||
color: #ffffff; // this is fine without a var because it's for both themes
|
||||
|
||||
.mx_DMInviteDialog_userTile_avatar {
|
||||
.mx_InviteDialog_userTile_avatar {
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
left: -5px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
img.mx_DMInviteDialog_userTile_avatar {
|
||||
img.mx_InviteDialog_userTile_avatar {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_userTile_name {
|
||||
.mx_InviteDialog_userTile_name {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_userTile_threepidAvatar {
|
||||
.mx_InviteDialog_userTile_threepidAvatar {
|
||||
background-color: #ffffff; // this is fine without a var because it's for both themes
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_userTile_remove {
|
||||
.mx_InviteDialog_userTile_remove {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
}
|
92
src/DeviceListener.js
Normal file
92
src/DeviceListener.js
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import ToastStore from './stores/ToastStore';
|
||||
|
||||
function toastKey(device) {
|
||||
return 'newsession_' + device.deviceId;
|
||||
}
|
||||
|
||||
export default class DeviceListener {
|
||||
static sharedInstance() {
|
||||
if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener();
|
||||
return global.mx_DeviceListener;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// device IDs for which the user has dismissed the verify toast ('Later')
|
||||
this._dismissed = new Set();
|
||||
}
|
||||
|
||||
start() {
|
||||
MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated);
|
||||
MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
||||
this.recheck();
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated);
|
||||
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
||||
}
|
||||
this._dismissed.clear();
|
||||
}
|
||||
|
||||
dismissVerification(deviceId) {
|
||||
this._dismissed.add(deviceId);
|
||||
this.recheck();
|
||||
}
|
||||
|
||||
_onDevicesUpdated = (users) => {
|
||||
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
||||
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) return;
|
||||
this.recheck();
|
||||
}
|
||||
|
||||
_onDeviceVerificationChanged = (users) => {
|
||||
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
||||
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) return;
|
||||
this.recheck();
|
||||
}
|
||||
|
||||
async recheck() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
if (!cli.isCryptoEnabled()) return false;
|
||||
|
||||
const devices = await cli.getStoredDevicesForUser(cli.getUserId());
|
||||
for (const device of devices) {
|
||||
if (device.deviceId == cli.deviceId) continue;
|
||||
|
||||
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
|
||||
if (deviceTrust.isVerified() || this._dismissed.has(device.deviceId)) {
|
||||
ToastStore.sharedInstance().dismissToast(toastKey(device));
|
||||
} else {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: toastKey(device),
|
||||
title: _t("New Session"),
|
||||
icon: "verification_warning",
|
||||
props: {deviceId: device.deviceId},
|
||||
component: sdk.getComponent("toasts.NewSessionToast"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -16,7 +17,10 @@ limitations under the License.
|
||||
|
||||
import * as sdk from './index';
|
||||
import Modal from './Modal';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
|
||||
// TODO: We can remove this once cross-signing is the only way.
|
||||
// https://github.com/vector-im/riot-web/issues/11908
|
||||
export default class KeyRequestHandler {
|
||||
constructor(matrixClient) {
|
||||
this._matrixClient = matrixClient;
|
||||
@ -30,6 +34,11 @@ export default class KeyRequestHandler {
|
||||
}
|
||||
|
||||
handleKeyRequest(keyRequest) {
|
||||
// Ignore own device key requests if cross-signing lab enabled
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = keyRequest.userId;
|
||||
const deviceId = keyRequest.deviceId;
|
||||
const requestId = keyRequest.requestId;
|
||||
@ -60,6 +69,11 @@ export default class KeyRequestHandler {
|
||||
}
|
||||
|
||||
handleKeyRequestCancellation(cancellation) {
|
||||
// Ignore own device key requests if cross-signing lab enabled
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// see if we can find the request in the queue
|
||||
const userId = cancellation.userId;
|
||||
const deviceId = cancellation.deviceId;
|
||||
|
@ -2,6 +2,7 @@
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
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.
|
||||
@ -35,8 +36,10 @@ import { sendLoginRequest } from "./Login";
|
||||
import * as StorageManager from './utils/StorageManager';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import TypingStore from "./stores/TypingStore";
|
||||
import ToastStore from "./stores/ToastStore";
|
||||
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
||||
import {Mjolnir} from "./mjolnir/Mjolnir";
|
||||
import DeviceListener from "./DeviceListener";
|
||||
|
||||
/**
|
||||
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
||||
@ -575,6 +578,7 @@ async function startMatrixClient(startSyncing=true) {
|
||||
Notifier.start();
|
||||
UserActivity.sharedInstance().start();
|
||||
TypingStore.sharedInstance().reset(); // just in case
|
||||
ToastStore.sharedInstance().reset();
|
||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||
Presence.start();
|
||||
}
|
||||
@ -595,6 +599,9 @@ async function startMatrixClient(startSyncing=true) {
|
||||
await MatrixClientPeg.assign();
|
||||
}
|
||||
|
||||
// This needs to be started after crypto is set up
|
||||
DeviceListener.sharedInstance().start();
|
||||
|
||||
// dispatch that we finished starting up to wire up any other bits
|
||||
// of the matrix client that cannot be set prior to starting up.
|
||||
dis.dispatch({action: 'client_started'});
|
||||
@ -651,6 +658,7 @@ export function stopMatrixClient(unsetClient=true) {
|
||||
ActiveWidgetStore.stop();
|
||||
IntegrationManagers.sharedInstance().stopWatching();
|
||||
Mjolnir.sharedInstance().stop();
|
||||
DeviceListener.sharedInstance().stop();
|
||||
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
|
||||
EventIndexPeg.stop();
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -26,6 +27,7 @@ import dis from './dispatcher';
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import { _t } from './languageHandler';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
|
||||
|
||||
/**
|
||||
* Invites multiple addresses to a room
|
||||
@ -36,21 +38,19 @@ import SettingsStore from "./settings/SettingsStore";
|
||||
* @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids.
|
||||
* @returns {Promise} Promise
|
||||
*/
|
||||
function inviteMultipleToRoom(roomId, addrs) {
|
||||
export function inviteMultipleToRoom(roomId, addrs) {
|
||||
const inviter = new MultiInviter(roomId);
|
||||
return inviter.invite(addrs).then(states => Promise.resolve({states, inviter}));
|
||||
}
|
||||
|
||||
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);
|
||||
// This new dialog handles the room creation internally - we don't need to worry about it.
|
||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Start DM', '', InviteDialog, {kind: KIND_DM},
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -74,6 +74,16 @@ export function showStartChatInviteDialog() {
|
||||
}
|
||||
|
||||
export function showRoomInviteDialog(roomId) {
|
||||
if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) {
|
||||
// This new dialog handles the room creation internally - we don't need to worry about it.
|
||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId},
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||
|
||||
Modal.createTrackedDialog('Chat Invite', '', AddressPickerDialog, {
|
||||
|
@ -63,6 +63,7 @@ import { countRoomsWithNotif } from '../../RoomNotifs';
|
||||
import { ThemeWatcher } from "../../theme";
|
||||
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
||||
import { defer } from "../../utils/promise";
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export const VIEWS = {
|
||||
@ -1381,6 +1382,8 @@ export default createReactClass({
|
||||
cli.on("Session.logged_out", () => dft.stop());
|
||||
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err));
|
||||
|
||||
// TODO: We can remove this once cross-signing is the only way.
|
||||
// https://github.com/vector-im/riot-web/issues/11908
|
||||
const krh = new KeyRequestHandler(cli);
|
||||
cli.on("crypto.roomKeyRequest", (req) => {
|
||||
krh.handleKeyRequest(req);
|
||||
@ -1453,15 +1456,12 @@ export default createReactClass({
|
||||
console.log(`MatrixChat got a .request ${request.channel.transactionId}`, request.event.getRoomId());
|
||||
if (request.pending) {
|
||||
console.log(`emitting toast for verification request with txnid ${request.channel.transactionId}`, request.event && request.event.getId());
|
||||
dis.dispatch({
|
||||
action: "show_toast",
|
||||
toast: {
|
||||
key: request.channel.transactionId,
|
||||
title: _t("Verification Request"),
|
||||
icon: "verification",
|
||||
props: {request},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
},
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: 'verifreq_' + request.channel.transactionId,
|
||||
title: _t("Verification Request"),
|
||||
icon: "verification",
|
||||
props: {request},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -173,6 +173,7 @@ export default createReactClass({
|
||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
// Start listening for RoomViewStore updates
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
@ -492,6 +493,7 @@ export default createReactClass({
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
}
|
||||
|
||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
@ -762,6 +764,14 @@ export default createReactClass({
|
||||
this._updateE2EStatus(room);
|
||||
},
|
||||
|
||||
onUserVerificationChanged: function(userId, _trustStatus) {
|
||||
const room = this.state.room;
|
||||
if (!room.currentState.getMember(userId)) {
|
||||
return;
|
||||
}
|
||||
this._updateE2EStatus(room);
|
||||
},
|
||||
|
||||
_updateE2EStatus: async function(room) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli.isRoomEncrypted(room.roomId)) {
|
||||
@ -782,32 +792,41 @@ export default createReactClass({
|
||||
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
|
||||
});
|
||||
});
|
||||
debuglog("e2e check is warning/verified only as cross-signing is off");
|
||||
return;
|
||||
}
|
||||
|
||||
/* At this point, the user has encryption on and cross-signing on */
|
||||
const e2eMembers = await room.getEncryptionTargetMembers();
|
||||
for (const member of e2eMembers) {
|
||||
const { userId } = member;
|
||||
const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified();
|
||||
if (!userVerified) {
|
||||
this.setState({
|
||||
e2eStatus: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const verified = [];
|
||||
const unverified = [];
|
||||
e2eMembers.map(({userId}) => userId)
|
||||
.filter((userId) => userId !== cli.getUserId())
|
||||
.forEach((userId) => {
|
||||
(cli.checkUserTrust(userId).isCrossSigningVerified() ?
|
||||
verified : unverified).push(userId)
|
||||
});
|
||||
|
||||
debuglog("e2e verified", verified, "unverified", unverified);
|
||||
|
||||
/* Check all verified user devices. */
|
||||
for (const userId of verified) {
|
||||
const devices = await cli.getStoredDevicesForUser(userId);
|
||||
const allDevicesVerified = devices.every(device => {
|
||||
const { deviceId } = device;
|
||||
return cli.checkDeviceTrust(userId, deviceId).isCrossSigningVerified();
|
||||
const allDevicesVerified = devices.every(({deviceId}) => {
|
||||
return cli.checkDeviceTrust(userId, deviceId).isVerified();
|
||||
});
|
||||
if (!allDevicesVerified) {
|
||||
this.setState({
|
||||
e2eStatus: "warning",
|
||||
});
|
||||
debuglog("e2e status set to warning as not all users trust all of their devices." +
|
||||
" Aborted on user", userId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
e2eStatus: "verified",
|
||||
e2eStatus: unverified.length === 0 ? "verified" : "normal",
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -15,38 +15,26 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import dis from "../../dispatcher";
|
||||
import { _t } from '../../languageHandler';
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
import classNames from "classnames";
|
||||
|
||||
export default class ToastContainer extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {toasts: []};
|
||||
this.state = {toasts: ToastStore.sharedInstance().getToasts()};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
console.log("ToastContainer mounted");
|
||||
this._dispatcherRef = dis.register(this.onAction);
|
||||
ToastStore.sharedInstance().on('update', this._onToastStoreUpdate);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
ToastStore.sharedInstance().removeListener('update', this._onToastStoreUpdate);
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
if (payload.action === "show_toast") {
|
||||
this._addToast(payload.toast);
|
||||
}
|
||||
};
|
||||
|
||||
_addToast(toast) {
|
||||
this.setState({toasts: this.state.toasts.concat(toast)});
|
||||
}
|
||||
|
||||
dismissTopToast = () => {
|
||||
const [, ...remaining] = this.state.toasts;
|
||||
this.setState({toasts: remaining});
|
||||
_onToastStoreUpdate = () => {
|
||||
this.setState({toasts: ToastStore.sharedInstance().getToasts()});
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -63,8 +51,8 @@ export default class ToastContainer extends React.Component {
|
||||
const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
|
||||
|
||||
const toastProps = Object.assign({}, props, {
|
||||
dismiss: this.dismissTopToast,
|
||||
key,
|
||||
toastKey: key,
|
||||
});
|
||||
toast = (<div className={toastClasses}>
|
||||
<h2>{title}{countIndicator}</h2>
|
||||
|
@ -63,7 +63,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
||||
}
|
||||
|
||||
componentWillUmount() {
|
||||
componentWillUnmount() {
|
||||
const { user } = this.props.member;
|
||||
if (!user) {
|
||||
return;
|
||||
|
@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {RoomMember} from "matrix-js-sdk/src/matrix";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
@ -31,8 +31,11 @@ import dis from "../../../dispatcher";
|
||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import createRoom from "../../../createRoom";
|
||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||
|
||||
// TODO: [TravisR] Make this generic for all kinds of invites
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
|
||||
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
|
||||
@ -138,11 +141,11 @@ class DMUserTile extends React.PureComponent {
|
||||
const avatarSize = 20;
|
||||
const avatar = this.props.member.isEmail
|
||||
? <img
|
||||
className='mx_DMInviteDialog_userTile_avatar mx_DMInviteDialog_userTile_threepidAvatar'
|
||||
className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar'
|
||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||
width={avatarSize} height={avatarSize} />
|
||||
: <BaseAvatar
|
||||
className='mx_DMInviteDialog_userTile_avatar'
|
||||
className='mx_InviteDialog_userTile_avatar'
|
||||
url={getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
||||
avatarSize, avatarSize, "crop")}
|
||||
@ -152,13 +155,13 @@ class DMUserTile extends React.PureComponent {
|
||||
height={avatarSize} />;
|
||||
|
||||
return (
|
||||
<span className='mx_DMInviteDialog_userTile'>
|
||||
<span className='mx_DMInviteDialog_userTile_pill'>
|
||||
<span className='mx_InviteDialog_userTile'>
|
||||
<span className='mx_InviteDialog_userTile_pill'>
|
||||
{avatar}
|
||||
<span className='mx_DMInviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||
<span className='mx_InviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||
</span>
|
||||
<AccessibleButton
|
||||
className='mx_DMInviteDialog_userTile_remove'
|
||||
className='mx_InviteDialog_userTile_remove'
|
||||
onClick={this._onRemove}
|
||||
>
|
||||
<img src={require("../../../../res/img/icon-pill-remove.svg")} alt={_t('Remove')} width={8} height={8} />
|
||||
@ -209,7 +212,7 @@ class DMRoomTile extends React.PureComponent {
|
||||
|
||||
// Highlight the word the user entered
|
||||
const substr = str.substring(i, filterStr.length + i);
|
||||
result.push(<span className='mx_DMInviteDialog_roomTile_highlight' key={i + 'bold'}>{substr}</span>);
|
||||
result.push(<span className='mx_InviteDialog_roomTile_highlight' key={i + 'bold'}>{substr}</span>);
|
||||
i += substr.length;
|
||||
}
|
||||
|
||||
@ -227,7 +230,7 @@ class DMRoomTile extends React.PureComponent {
|
||||
let timestamp = null;
|
||||
if (this.props.lastActiveTs) {
|
||||
const humanTs = humanizeTime(this.props.lastActiveTs);
|
||||
timestamp = <span className='mx_DMInviteDialog_roomTile_time'>{humanTs}</span>;
|
||||
timestamp = <span className='mx_InviteDialog_roomTile_time'>{humanTs}</span>;
|
||||
}
|
||||
|
||||
const avatarSize = 36;
|
||||
@ -247,61 +250,95 @@ class DMRoomTile extends React.PureComponent {
|
||||
let checkmark = null;
|
||||
if (this.props.isSelected) {
|
||||
// To reduce flickering we put the 'selected' room tile above the real avatar
|
||||
checkmark = <div className='mx_DMInviteDialog_roomTile_selected' />;
|
||||
checkmark = <div className='mx_InviteDialog_roomTile_selected' />;
|
||||
}
|
||||
|
||||
// To reduce flickering we put the checkmark on top of the actual avatar (prevents
|
||||
// the browser from reloading the image source when the avatar remounts).
|
||||
const stackedAvatar = (
|
||||
<span className='mx_DMInviteDialog_roomTile_avatarStack'>
|
||||
<span className='mx_InviteDialog_roomTile_avatarStack'>
|
||||
{avatar}
|
||||
{checkmark}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='mx_DMInviteDialog_roomTile' onClick={this._onClick}>
|
||||
<div className='mx_InviteDialog_roomTile' onClick={this._onClick}>
|
||||
{stackedAvatar}
|
||||
<span className='mx_DMInviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</span>
|
||||
<span className='mx_DMInviteDialog_roomTile_userId'>{this._highlightName(this.props.member.userId)}</span>
|
||||
<span className='mx_InviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</span>
|
||||
<span className='mx_InviteDialog_roomTile_userId'>{this._highlightName(this.props.member.userId)}</span>
|
||||
{timestamp}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class DMInviteDialog extends React.PureComponent {
|
||||
export default class InviteDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// Takes an array of user IDs/emails to invite.
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
// The kind of invite being performed. Assumed to be KIND_DM if
|
||||
// not provided.
|
||||
kind: PropTypes.string,
|
||||
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
||||
roomId: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
kind: KIND_DM,
|
||||
};
|
||||
|
||||
_debounceTimer: number = null;
|
||||
_editorRef: any = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props.kind === KIND_INVITE && !props.roomId) {
|
||||
throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
|
||||
}
|
||||
|
||||
let alreadyInvited = [];
|
||||
if (props.roomId) {
|
||||
const room = MatrixClientPeg.get().getRoom(props.roomId);
|
||||
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
|
||||
alreadyInvited = [
|
||||
...room.getMembersWithMembership('invite'),
|
||||
...room.getMembersWithMembership('join'),
|
||||
...room.getMembersWithMembership('ban'), // so we don't try to invite them
|
||||
].map(m => m.userId);
|
||||
}
|
||||
|
||||
|
||||
this.state = {
|
||||
targets: [], // array of Member objects (see interface above)
|
||||
filterText: "",
|
||||
recents: this._buildRecents(),
|
||||
recents: this._buildRecents(alreadyInvited),
|
||||
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||
suggestions: this._buildSuggestions(),
|
||||
suggestions: this._buildSuggestions(alreadyInvited),
|
||||
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
||||
serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions
|
||||
threepidResultsMixin: [], // { user: ThreepidMember, userId: string}[], like recents and suggestions
|
||||
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
|
||||
tryingIdentityServer: false,
|
||||
|
||||
// These two flags are used for the 'Go' button to communicate what is going on.
|
||||
busy: false,
|
||||
errorText: null,
|
||||
};
|
||||
|
||||
this._editorRef = createRef();
|
||||
}
|
||||
|
||||
_buildRecents(): {userId: string, user: RoomMember, lastActive: number} {
|
||||
_buildRecents(excludedTargetIds: string[]): {userId: string, user: RoomMember, lastActive: number} {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals();
|
||||
const recents = [];
|
||||
for (const userId in rooms) {
|
||||
// Filter out user IDs that are already in the room / should be excluded
|
||||
if (excludedTargetIds.includes(userId)) continue;
|
||||
|
||||
const room = rooms[userId];
|
||||
const member = room.getMember(userId);
|
||||
if (!member) continue; // just skip people who don't have memberships for some reason
|
||||
@ -320,7 +357,7 @@ export default class DMInviteDialog extends React.PureComponent {
|
||||
return recents;
|
||||
}
|
||||
|
||||
_buildSuggestions(): {userId: string, user: RoomMember} {
|
||||
_buildSuggestions(excludedTargetIds: string[]): {userId: string, user: RoomMember} {
|
||||
const maxConsideredMembers = 200;
|
||||
const client = MatrixClientPeg.get();
|
||||
const excludedUserIds = [client.getUserId(), SdkConfig.get()['welcomeUserId']];
|
||||
@ -337,6 +374,11 @@ export default class DMInviteDialog extends React.PureComponent {
|
||||
|
||||
const joinedMembers = room.getJoinedMembers().filter(u => !excludedUserIds.includes(u.userId));
|
||||
for (const member of joinedMembers) {
|
||||
// Filter out user IDs that are already in the room / should be excluded
|
||||
if (excludedTargetIds.includes(member.userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!members[member.userId]) {
|
||||
members[member.userId] = {
|
||||
member: member,
|
||||
@ -384,12 +426,101 @@ export default class DMInviteDialog extends React.PureComponent {
|
||||
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
||||
}
|
||||
|
||||
_shouldAbortAfterInviteError(result): boolean {
|
||||
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
|
||||
if (failedUsers.length > 0) {
|
||||
console.log("Failed to invite users: ", result);
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("Failed to invite the following users to chat: %(csvUsers)s", {
|
||||
csvUsers: failedUsers.join(", "),
|
||||
}),
|
||||
});
|
||||
return true; // abort
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_startDm = () => {
|
||||
this.props.onFinished(this.state.targets.map(t => t.userId));
|
||||
this.setState({busy: true});
|
||||
const targetIds = this.state.targets.map(t => t.userId);
|
||||
|
||||
// Check if there is already a DM with these people and reuse it if possible.
|
||||
const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
|
||||
if (existingRoom) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: existingRoom.roomId,
|
||||
should_peek: false,
|
||||
joining: false,
|
||||
});
|
||||
this.props.onFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a traditional DM and create the room if required.
|
||||
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
|
||||
let createRoomPromise = Promise.resolve();
|
||||
if (targetIds.length === 1) {
|
||||
createRoomPromise = createRoom({dmUserId: targetIds[0]});
|
||||
} else {
|
||||
// Create a boring room and try to invite the targets manually.
|
||||
createRoomPromise = createRoom().then(roomId => {
|
||||
return inviteMultipleToRoom(roomId, targetIds);
|
||||
}).then(result => {
|
||||
if (this._shouldAbortAfterInviteError(result)) {
|
||||
return true; // abort
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// the createRoom call will show the room for us, so we don't need to worry about that.
|
||||
createRoomPromise.then(abort => {
|
||||
if (abort === true) return; // only abort on true booleans, not roomIds or something
|
||||
this.props.onFinished();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("We couldn't create your DM. Please check the users you want to invite and try again."),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
_inviteUsers = () => {
|
||||
this.setState({busy: true});
|
||||
const targetIds = this.state.targets.map(t => t.userId);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
if (!room) {
|
||||
console.error("Failed to find the room to invite users to");
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("Something went wrong trying to invite the users."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
inviteMultipleToRoom(this.props.roomId, targetIds).then(result => {
|
||||
if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too
|
||||
this.props.onFinished();
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t(
|
||||
"We couldn't invite those users. Please check the users you want to invite and try again.",
|
||||
),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
_cancel = () => {
|
||||
this.props.onFinished([]);
|
||||
// We do not want the user to close the dialog while an action is in progress
|
||||
if (this.state.busy) return;
|
||||
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_updateFilter = (e) => {
|
||||
@ -599,7 +730,11 @@ export default class DMInviteDialog extends React.PureComponent {
|
||||
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
||||
const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this);
|
||||
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
||||
const sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
||||
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
||||
|
||||
if (this.props.kind === KIND_INVITE) {
|
||||
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
|
||||
}
|
||||
|
||||
// Mix in the server results if we have any, but only if we're searching. We track the additional
|
||||
// members separately because we want to filter sourceMembers but trust the mixin arrays to have
|
||||
@ -631,7 +766,7 @@ export default class DMInviteDialog extends React.PureComponent {
|
||||
|
||||
if (sourceMembers.length === 0 && additionalMembers.length === 0) {
|
||||
return (
|
||||
<div className='mx_DMInviteDialog_section'>
|
||||
<div className='mx_InviteDialog_section'>
|
||||
<h3>{sectionName}</h3>
|
||||
<p>{_t("No results")}</p>
|
||||
</div>
|
||||
@ -672,7 +807,7 @@ export default class DMInviteDialog extends React.PureComponent {
|
||||
/>
|
||||
));
|
||||
return (
|
||||
<div className='mx_DMInviteDialog_section'>
|
||||
<div className='mx_InviteDialog_section'>
|
||||
<h3>{sectionName}</h3>
|
||||
{tiles}
|
||||
{showMore}
|
||||
@ -695,7 +830,7 @@ export default class DMInviteDialog extends React.PureComponent {
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className='mx_DMInviteDialog_editor' onClick={this._onClickInputArea}>
|
||||
<div className='mx_InviteDialog_editor' onClick={this._onClickInputArea}>
|
||||
{targets}
|
||||
{input}
|
||||
</div>
|
||||
@ -739,35 +874,67 @@ export default class DMInviteDialog extends React.PureComponent {
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
let spinner = null;
|
||||
if (this.state.busy) {
|
||||
spinner = <Spinner w={20} h={20} />;
|
||||
}
|
||||
|
||||
|
||||
let title;
|
||||
let helpText;
|
||||
let buttonText;
|
||||
let goButtonFn;
|
||||
|
||||
if (this.props.kind === KIND_DM) {
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
|
||||
title = _t("Direct Messages");
|
||||
helpText = _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>},
|
||||
);
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
} else { // KIND_INVITE
|
||||
title = _t("Invite to this room");
|
||||
helpText = _t(
|
||||
"If you can't find someone, ask them for their username (e.g. @user:server.com) or " +
|
||||
"<a>share this room</a>.", {},
|
||||
{a: (sub) => <a href={makeRoomPermalink(this.props.roomId)} rel="noopener" target="_blank">{sub}</a>},
|
||||
);
|
||||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
}
|
||||
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_DMInviteDialog'
|
||||
className='mx_InviteDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this._cancel}
|
||||
title={_t("Direct Messages")}
|
||||
title={title}
|
||||
>
|
||||
<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>
|
||||
<div className='mx_DMInviteDialog_addressBar'>
|
||||
<div className='mx_InviteDialog_content'>
|
||||
<p>{helpText}</p>
|
||||
<div className='mx_InviteDialog_addressBar'>
|
||||
{this._renderEditor()}
|
||||
{this._renderIdentityServerWarning()}
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this._startDm}
|
||||
className='mx_DMInviteDialog_goButton'
|
||||
>
|
||||
{_t("Go")}
|
||||
</AccessibleButton>
|
||||
<div className='mx_InviteDialog_buttonAndSpinner'>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={goButtonFn}
|
||||
className='mx_InviteDialog_goButton'
|
||||
disabled={this.state.busy}
|
||||
>
|
||||
{buttonText}
|
||||
</AccessibleButton>
|
||||
{spinner}
|
||||
</div>
|
||||
</div>
|
||||
{this._renderIdentityServerWarning()}
|
||||
<div className='error'>{this.state.errorText}</div>
|
||||
{this._renderSection('recents')}
|
||||
{this._renderSection('suggestions')}
|
||||
</div>
|
@ -22,6 +22,9 @@ import * as sdk from '../../../index';
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
||||
// TODO: We can remove this once cross-signing is the only way.
|
||||
// https://github.com/vector-im/riot-web/issues/11908
|
||||
|
||||
/**
|
||||
* Dialog which asks the user whether they want to share their keys with
|
||||
* an unverified device.
|
||||
|
@ -210,8 +210,8 @@ export default class BasicMessageEditor extends React.Component {
|
||||
const selectedParts = range.parts.map(p => p.serialize());
|
||||
event.clipboardData.setData("application/x-riot-composer", JSON.stringify(selectedParts));
|
||||
if (type === "cut") {
|
||||
selection.deleteFromDocument();
|
||||
range.replace([]);
|
||||
// Remove the text, updating the model as appropriate
|
||||
replaceRangeAndMoveCaret(range, []);
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ export default class BridgeSettingsTab extends React.Component {
|
||||
const client = MatrixClientPeg.get();
|
||||
const roomState = (client.getRoom(roomId)).currentState;
|
||||
|
||||
const bridgeEvents = Array.concat(...BRIDGE_EVENT_TYPES.map((typeName) =>
|
||||
const bridgeEvents = [].concat(...BRIDGE_EVENT_TYPES.map((typeName) =>
|
||||
Object.values(roomState.events[typeName] || {}),
|
||||
));
|
||||
|
||||
|
57
src/components/views/toasts/NewSessionToast.js
Normal file
57
src/components/views/toasts/NewSessionToast.js
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from "../../../index";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from "../../../Modal";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import DeviceListener from '../../../DeviceListener';
|
||||
|
||||
export default class VerifySessionToast extends React.PureComponent {
|
||||
static propTypes = {
|
||||
toastKey: PropTypes.string.isRequired,
|
||||
deviceId: PropTypes.string,
|
||||
};
|
||||
|
||||
_onLaterClick = () => {
|
||||
DeviceListener.sharedInstance().dismissVerification(this.props.deviceId);
|
||||
};
|
||||
|
||||
_onVerifyClick = async () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||
|
||||
const device = await cli.getStoredDevice(cli.getUserId(), this.props.deviceId);
|
||||
|
||||
Modal.createTrackedDialog('New Session Verify', 'Starting dialog', DeviceVerifyDialog, {
|
||||
userId: MatrixClientPeg.get().getUserId(),
|
||||
device,
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
};
|
||||
|
||||
render() {
|
||||
const FormButton = sdk.getComponent("elements.FormButton");
|
||||
return (<div>
|
||||
<div className="mx_Toast_description">{_t("Other users may not trust it")}</div>
|
||||
<div className="mx_Toast_buttons" aria-live="off">
|
||||
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
|
||||
<FormButton label={_t("Verify")} onClick={this._onVerifyClick} />
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
|
||||
import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver";
|
||||
import dis from "../../../dispatcher";
|
||||
import ToastStore from "../../../stores/ToastStore";
|
||||
|
||||
export default class VerificationRequestToast extends React.PureComponent {
|
||||
constructor(props) {
|
||||
@ -48,12 +49,12 @@ export default class VerificationRequestToast extends React.PureComponent {
|
||||
_checkRequestIsPending = () => {
|
||||
const {request} = this.props;
|
||||
if (request.ready || request.started || request.done || request.cancelled || request.observeOnly) {
|
||||
this.props.dismiss();
|
||||
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
||||
}
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
this.props.dismiss();
|
||||
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
||||
try {
|
||||
this.props.request.cancel();
|
||||
} catch (err) {
|
||||
@ -62,7 +63,7 @@ export default class VerificationRequestToast extends React.PureComponent {
|
||||
}
|
||||
|
||||
accept = async () => {
|
||||
this.props.dismiss();
|
||||
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
||||
const {request} = this.props;
|
||||
const {event} = request;
|
||||
// no room id for to_device requests
|
||||
@ -111,6 +112,6 @@ export default class VerificationRequestToast extends React.PureComponent {
|
||||
}
|
||||
|
||||
VerificationRequestToast.propTypes = {
|
||||
dismiss: PropTypes.func.isRequired,
|
||||
request: PropTypes.object.isRequired,
|
||||
toastKey: PropTypes.string.isRequired,
|
||||
};
|
||||
|
@ -117,7 +117,7 @@ export default class DocumentPosition {
|
||||
}
|
||||
offset += this.offset;
|
||||
const lastPart = model.parts[this.index];
|
||||
const atEnd = offset >= lastPart.text.length;
|
||||
const atEnd = !lastPart || offset >= lastPart.text.length; // if no last part, we're at the end
|
||||
return new DocumentOffset(offset, atEnd);
|
||||
}
|
||||
|
||||
|
@ -85,6 +85,7 @@
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
|
||||
"New Session": "New Session",
|
||||
"Who would you like to add to this community?": "Who would you like to add to this community?",
|
||||
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID",
|
||||
"Invite new community members": "Invite new community members",
|
||||
@ -372,7 +373,7 @@
|
||||
"Render simple counters in room header": "Render simple counters in room header",
|
||||
"Multiple integration managers": "Multiple integration managers",
|
||||
"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)",
|
||||
"New invite dialog": "New invite dialog",
|
||||
"Show a presence dot next to DMs in the room list": "Show a presence dot next to DMs in the room list",
|
||||
"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)",
|
||||
@ -513,6 +514,9 @@
|
||||
"Headphones": "Headphones",
|
||||
"Folder": "Folder",
|
||||
"Pin": "Pin",
|
||||
"Other users may not trust it": "Other users may not trust it",
|
||||
"Later": "Later",
|
||||
"Verify": "Verify",
|
||||
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
||||
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
||||
"Upload": "Upload",
|
||||
@ -1134,7 +1138,6 @@
|
||||
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
|
||||
"Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.",
|
||||
"Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.",
|
||||
"Verify": "Verify",
|
||||
"Security": "Security",
|
||||
"Sunday": "Sunday",
|
||||
"Monday": "Monday",
|
||||
@ -1442,14 +1445,6 @@
|
||||
"View Servers in Room": "View Servers in Room",
|
||||
"Toolbox": "Toolbox",
|
||||
"Developer Tools": "Developer Tools",
|
||||
"Failed to find the following users": "Failed to find the following users",
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s",
|
||||
"Recent Conversations": "Recent Conversations",
|
||||
"Suggestions": "Suggestions",
|
||||
"Show more": "Show more",
|
||||
"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.",
|
||||
"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.",
|
||||
@ -1459,6 +1454,20 @@
|
||||
"Enable 'Manage Integrations' in Settings to do this.": "Enable 'Manage Integrations' in Settings to do this.",
|
||||
"Integrations not allowed": "Integrations not allowed",
|
||||
"Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.",
|
||||
"Failed to invite the following users to chat: %(csvUsers)s": "Failed to invite the following users to chat: %(csvUsers)s",
|
||||
"We couldn't create your DM. Please check the users you want to invite and try again.": "We couldn't create your DM. Please check the users you want to invite and try again.",
|
||||
"Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.",
|
||||
"We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.",
|
||||
"Failed to find the following users": "Failed to find the following users",
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s",
|
||||
"Recent Conversations": "Recent Conversations",
|
||||
"Suggestions": "Suggestions",
|
||||
"Recently Direct Messaged": "Recently Direct Messaged",
|
||||
"Show more": "Show more",
|
||||
"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",
|
||||
"If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.": "If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.",
|
||||
"You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.",
|
||||
"Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.",
|
||||
"Start verification": "Start verification",
|
||||
|
@ -83,6 +83,7 @@ export class IntegrationManagers {
|
||||
}
|
||||
|
||||
async _setupHomeserverManagers() {
|
||||
if (!MatrixClientPeg.get()) return;
|
||||
try {
|
||||
console.log("Updating homeserver-configured integration managers...");
|
||||
const homeserverDomain = MatrixClientPeg.getHomeserverName();
|
||||
|
@ -130,7 +130,7 @@ export const SETTINGS = {
|
||||
},
|
||||
"feature_ftue_dms": {
|
||||
isFeature: true,
|
||||
displayName: _td("New DM invite dialog (under development)"),
|
||||
displayName: _td("New invite dialog"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
|
56
src/stores/ToastStore.js
Normal file
56
src/stores/ToastStore.js
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from 'events';
|
||||
|
||||
/**
|
||||
* Holds the active toasts
|
||||
*/
|
||||
export default class ToastStore extends EventEmitter {
|
||||
static sharedInstance() {
|
||||
if (!global.mx_ToastStore) global.mx_ToastStore = new ToastStore();
|
||||
return global.mx_ToastStore;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._dispatcherRef = null;
|
||||
this._toasts = [];
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._toasts = [];
|
||||
}
|
||||
|
||||
addOrReplaceToast(newToast) {
|
||||
const oldIndex = this._toasts.findIndex(t => t.key === newToast.key);
|
||||
if (oldIndex === -1) {
|
||||
this._toasts.push(newToast);
|
||||
} else {
|
||||
this._toasts[oldIndex] = newToast;
|
||||
}
|
||||
this.emit('update');
|
||||
}
|
||||
|
||||
dismissToast(key) {
|
||||
this._toasts = this._toasts.filter(t => t.key !== key);
|
||||
this.emit('update');
|
||||
}
|
||||
|
||||
getToasts() {
|
||||
return this._toasts;
|
||||
}
|
||||
}
|
@ -124,6 +124,27 @@ export default class DMRoomMap {
|
||||
return this._getUserToRooms()[userId] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the DM room which the given IDs share, if any.
|
||||
* @param {string[]} ids The identifiers (user IDs and email addresses) to look for.
|
||||
* @returns {Room} The DM room which all IDs given share, or falsey if no common room.
|
||||
*/
|
||||
getDMRoomForIdentifiers(ids) {
|
||||
// TODO: [Canonical DMs] Handle lookups for email addresses.
|
||||
// For now we'll pretend we only get user IDs and end up returning nothing for email addresses
|
||||
|
||||
let commonRooms = this.getDMRoomsForUserId(ids[0]);
|
||||
for (let i = 1; i < ids.length; i++) {
|
||||
const userRooms = this.getDMRoomsForUserId(ids[i]);
|
||||
commonRooms = commonRooms.filter(r => userRooms.includes(r));
|
||||
}
|
||||
|
||||
const joinedRooms = commonRooms.map(r => MatrixClientPeg.get().getRoom(r))
|
||||
.filter(r => r && r.getMyMembership() === 'join');
|
||||
|
||||
return joinedRooms[0];
|
||||
}
|
||||
|
||||
getUserIdForRoomId(roomId) {
|
||||
if (this.roomToUser == null) {
|
||||
// we lazily populate roomToUser so you can use
|
||||
|
91
yarn.lock
91
yarn.lock
@ -1570,6 +1570,14 @@ anymatch@^2.0.0:
|
||||
micromatch "^3.1.4"
|
||||
normalize-path "^2.1.1"
|
||||
|
||||
anymatch@~3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
|
||||
integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==
|
||||
dependencies:
|
||||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
aproba@^1.0.3, aproba@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
||||
@ -1884,6 +1892,11 @@ binary-extensions@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
|
||||
integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
|
||||
integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
|
||||
|
||||
bluebird@^3.5.0, bluebird@^3.5.5:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
@ -1928,6 +1941,13 @@ braces@^2.3.1, braces@^2.3.2:
|
||||
split-string "^3.0.2"
|
||||
to-regex "^3.0.1"
|
||||
|
||||
braces@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
|
||||
brorand@^1.0.1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||
@ -2232,7 +2252,7 @@ cheerio@^1.0.0-rc.3:
|
||||
lodash "^4.15.0"
|
||||
parse5 "^3.0.1"
|
||||
|
||||
chokidar@^2.0.2, chokidar@^2.1.2, chokidar@^2.1.8:
|
||||
chokidar@^2.0.2, chokidar@^2.1.8:
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
|
||||
integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==
|
||||
@ -2251,6 +2271,21 @@ chokidar@^2.0.2, chokidar@^2.1.2, chokidar@^2.1.8:
|
||||
optionalDependencies:
|
||||
fsevents "^1.2.7"
|
||||
|
||||
chokidar@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450"
|
||||
integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==
|
||||
dependencies:
|
||||
anymatch "~3.1.1"
|
||||
braces "~3.0.2"
|
||||
glob-parent "~5.1.0"
|
||||
is-binary-path "~2.1.0"
|
||||
is-glob "~4.0.1"
|
||||
normalize-path "~3.0.0"
|
||||
readdirp "~3.3.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.1.2"
|
||||
|
||||
chownr@^1.1.1:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142"
|
||||
@ -3654,6 +3689,13 @@ fill-range@^4.0.0:
|
||||
repeat-string "^1.6.1"
|
||||
to-regex-range "^2.1.0"
|
||||
|
||||
fill-range@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
find-cache-dir@^2.0.0, find-cache-dir@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7"
|
||||
@ -3815,6 +3857,11 @@ fsevents@^1.2.7:
|
||||
nan "^2.12.1"
|
||||
node-pre-gyp "^0.12.0"
|
||||
|
||||
fsevents@~2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805"
|
||||
integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==
|
||||
|
||||
function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
@ -3909,6 +3956,13 @@ glob-parent@^3.1.0:
|
||||
is-glob "^3.1.0"
|
||||
path-dirname "^1.0.0"
|
||||
|
||||
glob-parent@~5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2"
|
||||
integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob-to-regexp@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
|
||||
@ -4463,6 +4517,13 @@ is-binary-path@^1.0.0:
|
||||
dependencies:
|
||||
binary-extensions "^1.0.0"
|
||||
|
||||
is-binary-path@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
||||
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
|
||||
dependencies:
|
||||
binary-extensions "^2.0.0"
|
||||
|
||||
is-boolean-object@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93"
|
||||
@ -4622,7 +4683,7 @@ is-glob@^3.1.0:
|
||||
dependencies:
|
||||
is-extglob "^2.1.0"
|
||||
|
||||
is-glob@^4.0.0, is-glob@^4.0.1:
|
||||
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
|
||||
integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
|
||||
@ -4663,6 +4724,11 @@ is-number@^3.0.0:
|
||||
dependencies:
|
||||
kind-of "^3.0.2"
|
||||
|
||||
is-number@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||
|
||||
is-obj@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
|
||||
@ -6104,7 +6170,7 @@ normalize-path@^2.1.1:
|
||||
dependencies:
|
||||
remove-trailing-separator "^1.0.1"
|
||||
|
||||
normalize-path@^3.0.0:
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||
@ -6578,6 +6644,11 @@ performance-now@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.0.7:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a"
|
||||
integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==
|
||||
|
||||
pify@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
|
||||
@ -7169,6 +7240,13 @@ readdirp@^2.2.1:
|
||||
micromatch "^3.1.10"
|
||||
readable-stream "^2.0.2"
|
||||
|
||||
readdirp@~3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17"
|
||||
integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==
|
||||
dependencies:
|
||||
picomatch "^2.0.7"
|
||||
|
||||
realpath-native@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c"
|
||||
@ -8375,6 +8453,13 @@ to-regex-range@^2.1.0:
|
||||
is-number "^3.0.0"
|
||||
repeat-string "^1.6.1"
|
||||
|
||||
to-regex-range@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
||||
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
to-regex@^3.0.1, to-regex@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
|
||||
|
Loading…
Reference in New Issue
Block a user