mirror of https://github.com/vector-im/riot-web
Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/roving
commit
397e116efb
|
@ -1,16 +1,17 @@
|
|||
const en = require("../src/i18n/strings/en_EN");
|
||||
|
||||
module.exports = jest.fn((opts, cb) => {
|
||||
if (opts.url.endsWith("languages.json")) {
|
||||
const url = opts.url || opts.uri;
|
||||
if (url && url.endsWith("languages.json")) {
|
||||
cb(undefined, {status: 200}, JSON.stringify({
|
||||
"en": {
|
||||
"fileName": "en_EN.json",
|
||||
"label": "English",
|
||||
},
|
||||
}));
|
||||
} else if (opts.url.endsWith("en_EN.json")) {
|
||||
} else if (url && url.endsWith("en_EN.json")) {
|
||||
cb(undefined, {status: 200}, JSON.stringify(en));
|
||||
} else {
|
||||
cb(undefined, {status: 404}, "");
|
||||
cb(true, {status: 404}, "");
|
||||
}
|
||||
});
|
||||
|
|
16
package.json
16
package.json
|
@ -31,6 +31,7 @@
|
|||
"typings": "./lib/index.d.ts",
|
||||
"matrix_src_main": "./src/index.js",
|
||||
"scripts": {
|
||||
"prepublish": "yarn build",
|
||||
"i18n": "matrix-gen-i18n",
|
||||
"prunei18n": "matrix-prune-i18n",
|
||||
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
|
||||
|
@ -72,23 +73,18 @@
|
|||
"fuse.js": "^2.2.0",
|
||||
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566",
|
||||
"gfm.css": "^1.1.1",
|
||||
"glob": "^5.0.14",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"highlight.js": "^9.15.8",
|
||||
"html-entities": "^1.2.1",
|
||||
"is-ip": "^2.0.0",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"linkifyjs": "^2.1.6",
|
||||
"lodash": "^4.17.14",
|
||||
"lolex": "4.2",
|
||||
"matrix-js-sdk": "3.0.0",
|
||||
"optimist": "^0.6.1",
|
||||
"pako": "^1.0.5",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"prop-types": "^15.5.8",
|
||||
"qrcode-react": "^0.1.16",
|
||||
"qs": "^6.6.0",
|
||||
"querystring": "^0.2.0",
|
||||
"react": "^16.9.0",
|
||||
"react-addons-css-transition-group": "15.6.2",
|
||||
"react-beautiful-dnd": "^4.0.1",
|
||||
|
@ -101,7 +97,6 @@
|
|||
"url": "^0.11.0",
|
||||
"velocity-animate": "^1.5.2",
|
||||
"what-input": "^5.2.6",
|
||||
"whatwg-fetch": "^1.1.1",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -123,7 +118,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",
|
||||
|
@ -137,11 +132,12 @@
|
|||
"estree-walker": "^0.5.0",
|
||||
"file-loader": "^3.0.1",
|
||||
"flow-parser": "^0.57.3",
|
||||
"glob": "^5.0.14",
|
||||
"jest": "^24.9.0",
|
||||
"lolex": "^5.1.2",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"matrix-react-test-utils": "^0.2.2",
|
||||
"react-test-renderer": "^16.9.0",
|
||||
"require-json": "0.0.1",
|
||||
"rimraf": "^2.4.3",
|
||||
"source-map-loader": "^0.2.3",
|
||||
"stylelint": "^9.10.1",
|
||||
|
@ -157,7 +153,9 @@
|
|||
"testMatch": [
|
||||
"<rootDir>/test/**/*-test.js"
|
||||
],
|
||||
"setupTestFrameworkScriptFile": "<rootDir>/test/setupTests.js",
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/test/setupTests.js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.(gif|png|svg|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
||||
"\\$webapp/i18n/languages.json": "<rootDir>/__mocks__/languages.json"
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
@import "./structures/_TopLeftMenuButton.scss";
|
||||
@import "./structures/_UploadBar.scss";
|
||||
@import "./structures/_ViewSource.scss";
|
||||
@import "./structures/auth/_CompleteSecurity.scss";
|
||||
@import "./structures/auth/_Login.scss";
|
||||
@import "./views/auth/_AuthBody.scss";
|
||||
@import "./views/auth/_AuthButtons.scss";
|
||||
|
@ -56,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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_CompleteSecurity_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_CompleteSecurity_headerIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mx_CompleteSecurity_heroIcon {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.mx_CompleteSecurity_body {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.mx_CompleteSecurity_actionRow {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
margin-inline-start: 18px;
|
||||
|
||||
&.warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,28 +181,33 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InviteDialog {
|
||||
// Prevent the dialog from jumping around randomly when elements change.
|
||||
height: 590px;
|
||||
}
|
|
@ -107,7 +107,7 @@ export async function accessSecretStorage(func = async () => { }) {
|
|||
cachingAllowed = true;
|
||||
|
||||
try {
|
||||
if (!cli.hasSecretStorageKey()) {
|
||||
if (!await cli.hasSecretStorageKey()) {
|
||||
// This dialog calls bootstrap itself after guiding the user through
|
||||
// passphrase creation.
|
||||
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
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;
|
||||
this.recheck();
|
||||
}
|
||||
|
||||
_onDeviceVerificationChanged = (users) => {
|
||||
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
||||
this.recheck();
|
||||
}
|
||||
|
||||
async recheck() {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) return;
|
||||
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();
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
Copyright 2017, 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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -223,9 +223,10 @@ class _MatrixClientPeg {
|
|||
};
|
||||
|
||||
opts.cryptoCallbacks = {};
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks);
|
||||
}
|
||||
// These are always installed regardless of the labs flag so that
|
||||
// cross-signing features can toggle on without reloading and also be
|
||||
// accessed immediately after login.
|
||||
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks);
|
||||
|
||||
this.matrixClient = createMatrixClient(opts);
|
||||
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -143,10 +143,14 @@ class Tinter {
|
|||
* over time then the best bet is to register a single callback for the
|
||||
* entire set.
|
||||
*
|
||||
* To ensure the tintable work happens at least once, it is also called as
|
||||
* part of registration.
|
||||
*
|
||||
* @param {Function} tintable Function to call when the tint changes.
|
||||
*/
|
||||
registerTintable(tintable) {
|
||||
this.tintables.push(tintable);
|
||||
tintable();
|
||||
}
|
||||
|
||||
getKeyRgb() {
|
||||
|
|
|
@ -313,7 +313,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
<p>{_t(
|
||||
"Secret Storage will be set up using your existing key backup details. " +
|
||||
"Your secret storage passphrase and recovery key will be the same as " +
|
||||
" they were for your key backup",
|
||||
"they were for your key backup.",
|
||||
)}</p>
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
onPrimaryButtonClick={this._onMigrateNextClick}
|
||||
|
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import {TextualCompletion} from './Components';
|
||||
import type {SelectionRange} from "./Autocompleter";
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017-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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -21,6 +21,7 @@ import React from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as Matrix from "matrix-js-sdk";
|
||||
import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto';
|
||||
|
||||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
||||
import 'focus-visible';
|
||||
|
@ -62,7 +63,7 @@ import { countRoomsWithNotif } from '../../RoomNotifs';
|
|||
import { ThemeWatcher } from "../../theme";
|
||||
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
||||
import { defer } from "../../utils/promise";
|
||||
import KeyVerificationStateObserver from '../../utils/KeyVerificationStateObserver';
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export const VIEWS = {
|
||||
|
@ -79,18 +80,14 @@ export const VIEWS = {
|
|||
// we are showing the registration view
|
||||
REGISTER: 3,
|
||||
|
||||
// completeing the registration flow
|
||||
// completing the registration flow
|
||||
POST_REGISTRATION: 4,
|
||||
|
||||
// showing the 'forgot password' view
|
||||
FORGOT_PASSWORD: 5,
|
||||
|
||||
// we have valid matrix credentials (either via an explicit login, via the
|
||||
// initial re-animation/guest registration, or via a registration), and are
|
||||
// now setting up a matrixclient to talk to it. This isn't an instant
|
||||
// process because we need to clear out indexeddb. While it is going on we
|
||||
// show a big spinner.
|
||||
LOGGING_IN: 6,
|
||||
// showing flow to trust this new device with cross-signing
|
||||
COMPLETE_SECURITY: 6,
|
||||
|
||||
// we are logged in with an active matrix client.
|
||||
LOGGED_IN: 7,
|
||||
|
@ -656,16 +653,12 @@ export default createReactClass({
|
|||
});
|
||||
break;
|
||||
}
|
||||
case 'on_logging_in':
|
||||
// We are now logging in, so set the state to reflect that
|
||||
// NB. This does not touch 'ready' since if our dispatches
|
||||
// are delayed, the sync could already have completed
|
||||
this.setStateForNewView({
|
||||
view: VIEWS.LOGGING_IN,
|
||||
});
|
||||
break;
|
||||
case 'on_logged_in':
|
||||
if (!Lifecycle.isSoftLogout()) {
|
||||
if (
|
||||
!Lifecycle.isSoftLogout() &&
|
||||
this.state.view !== VIEWS.LOGIN &&
|
||||
this.state.view !== VIEWS.COMPLETE_SECURITY
|
||||
) {
|
||||
this._onLoggedIn();
|
||||
}
|
||||
break;
|
||||
|
@ -1169,7 +1162,7 @@ export default createReactClass({
|
|||
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
||||
const welcomeUserRoom = await this._startWelcomeUserChat();
|
||||
if (welcomeUserRoom === null) {
|
||||
// We didn't rediret to the welcome user room, so show
|
||||
// We didn't redirect to the welcome user room, so show
|
||||
// the homepage.
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
}
|
||||
|
@ -1389,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);
|
||||
|
@ -1458,22 +1453,14 @@ export default createReactClass({
|
|||
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
cli.on("crypto.verification.request", request => {
|
||||
let requestObserver;
|
||||
if (request.event.getRoomId()) {
|
||||
requestObserver = new KeyVerificationStateObserver(
|
||||
request.event, MatrixClientPeg.get());
|
||||
}
|
||||
|
||||
if (!requestObserver || requestObserver.pending) {
|
||||
dis.dispatch({
|
||||
action: "show_toast",
|
||||
toast: {
|
||||
key: request.event.getId(),
|
||||
title: _t("Verification Request"),
|
||||
icon: "verification",
|
||||
props: {request, requestObserver},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
},
|
||||
console.log(`MatrixChat got a .request ${request.channel.transactionId}`, request.event.getRoomId());
|
||||
if (request.pending) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: 'verifreq_' + request.channel.transactionId,
|
||||
title: _t("Verification Request"),
|
||||
icon: "verification",
|
||||
props: {request},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1573,6 +1560,10 @@ export default createReactClass({
|
|||
dis.dispatch({
|
||||
action: 'view_my_groups',
|
||||
});
|
||||
} else if (screen === 'complete_security') {
|
||||
dis.dispatch({
|
||||
action: 'start_complete_security',
|
||||
});
|
||||
} else if (screen == 'post_registration') {
|
||||
dis.dispatch({
|
||||
action: 'start_post_registration',
|
||||
|
@ -1822,21 +1813,69 @@ export default createReactClass({
|
|||
this._loggedInView = ref;
|
||||
},
|
||||
|
||||
async onUserCompletedLoginFlow(credentials) {
|
||||
// Wait for the client to be logged in (but not started)
|
||||
// which is enough to ask the server about account data.
|
||||
const loggedIn = new Promise(resolve => {
|
||||
const actionHandlerRef = dis.register(payload => {
|
||||
if (payload.action !== "on_logged_in") {
|
||||
return;
|
||||
}
|
||||
dis.unregister(actionHandlerRef);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Create and start the client in the background
|
||||
Lifecycle.setLoggedIn(credentials);
|
||||
await loggedIn;
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
// We're checking `isCryptoAvailable` here instead of `isCryptoEnabled`
|
||||
// because the client hasn't been started yet.
|
||||
if (!isCryptoAvailable()) {
|
||||
this._onLoggedIn();
|
||||
}
|
||||
|
||||
// Test for the master cross-signing key in SSSS as a quick proxy for
|
||||
// whether cross-signing has been set up on the account.
|
||||
let masterKeyInStorage = false;
|
||||
try {
|
||||
masterKeyInStorage = !!await cli.getAccountDataFromServer("m.cross_signing.master");
|
||||
} catch (e) {
|
||||
if (e.errcode !== "M_NOT_FOUND") throw e;
|
||||
}
|
||||
|
||||
if (masterKeyInStorage) {
|
||||
this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY });
|
||||
} else {
|
||||
this._onLoggedIn();
|
||||
}
|
||||
},
|
||||
|
||||
onCompleteSecurityFinished() {
|
||||
this._onLoggedIn();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
|
||||
|
||||
let view;
|
||||
|
||||
if (
|
||||
this.state.view === VIEWS.LOADING ||
|
||||
this.state.view === VIEWS.LOGGING_IN
|
||||
) {
|
||||
if (this.state.view === VIEWS.LOADING) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
view = (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.view === VIEWS.COMPLETE_SECURITY) {
|
||||
const CompleteSecurity = sdk.getComponent('structures.auth.CompleteSecurity');
|
||||
view = (
|
||||
<CompleteSecurity
|
||||
onFinished={this.onCompleteSecurityFinished}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.view === VIEWS.POST_REGISTRATION) {
|
||||
// needs to be before normal PageTypes as you are logged in technically
|
||||
const PostRegistration = sdk.getComponent('structures.auth.PostRegistration');
|
||||
|
@ -1921,7 +1960,7 @@ export default createReactClass({
|
|||
const Login = sdk.getComponent('structures.auth.Login');
|
||||
view = (
|
||||
<Login
|
||||
onLoggedIn={Lifecycle.setLoggedIn}
|
||||
onLoggedIn={this.onUserCompletedLoginFlow}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
fallbackHsUrl={this.getFallbackHsUrl()}
|
||||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||
|
|
|
@ -160,6 +160,7 @@ export default class RightPanel extends React.Component {
|
|||
groupId: payload.groupId,
|
||||
member: payload.member,
|
||||
event: payload.event,
|
||||
verificationRequest: payload.verificationRequest,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +169,7 @@ export default class RightPanel extends React.Component {
|
|||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
||||
const EncryptionPanel = sdk.getComponent('right_panel.EncryptionPanel');
|
||||
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
|
||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
||||
|
@ -235,6 +237,8 @@ export default class RightPanel extends React.Component {
|
|||
panel = <NotificationPanel />;
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) {
|
||||
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel) {
|
||||
panel = <EncryptionPanel member={this.state.member} verificationRequest={this.state.verificationRequest} />;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_RightPanel", "mx_fadable", {
|
||||
|
|
|
@ -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",
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -1134,9 +1134,11 @@ const TimelinePanel = createReactClass({
|
|||
const allowPartial = opts.allowPartial || false;
|
||||
|
||||
const messagePanel = this._messagePanel.current;
|
||||
if (messagePanel === undefined) return null;
|
||||
if (!messagePanel) return null;
|
||||
|
||||
const wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
||||
const messagePanelNode = ReactDOM.findDOMNode(messagePanel);
|
||||
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
|
||||
const wrapperRect = messagePanelNode.getBoundingClientRect();
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
const isNodeInView = (node) => {
|
||||
|
|
|
@ -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,37 +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() {
|
||||
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() {
|
||||
|
@ -62,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>
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { accessSecretStorage } from '../../../CrossSigningManager';
|
||||
|
||||
const PHASE_INTRO = 0;
|
||||
const PHASE_DONE = 1;
|
||||
const PHASE_CONFIRM_SKIP = 2;
|
||||
|
||||
export default class CompleteSecurity extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
phase: PHASE_INTRO,
|
||||
};
|
||||
}
|
||||
|
||||
onStartClick = async () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
try {
|
||||
await accessSecretStorage(async () => {
|
||||
await cli.checkOwnCrossSigningTrust();
|
||||
});
|
||||
this.setState({
|
||||
phase: PHASE_DONE,
|
||||
});
|
||||
} catch (e) {
|
||||
// this will throw if the user hits cancel, so ignore
|
||||
}
|
||||
}
|
||||
|
||||
onSkipClick = () => {
|
||||
this.setState({
|
||||
phase: PHASE_CONFIRM_SKIP,
|
||||
});
|
||||
}
|
||||
|
||||
onSkipConfirmClick = () => {
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
onSkipBackClick = () => {
|
||||
this.setState({
|
||||
phase: PHASE_INTRO,
|
||||
});
|
||||
}
|
||||
|
||||
onDoneClick = () => {
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
render() {
|
||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const {
|
||||
phase,
|
||||
} = this.state;
|
||||
|
||||
let icon;
|
||||
let title;
|
||||
let body;
|
||||
if (phase === PHASE_INTRO) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||
title = _t("Complete security");
|
||||
body = (
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Verify this session to grant it access to encrypted messages.",
|
||||
)}</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
onClick={this.onSkipClick}
|
||||
>
|
||||
{_t("Skip")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.onStartClick}
|
||||
>
|
||||
{_t("Start")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === PHASE_DONE) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified"></span>;
|
||||
title = _t("Session verified");
|
||||
body = (
|
||||
<div>
|
||||
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified"></div>
|
||||
<p>{_t(
|
||||
"Your new session is now verified. It has access to your " +
|
||||
"encrypted messages, and other users will see it as trusted.",
|
||||
)}</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.onDoneClick}
|
||||
>
|
||||
{_t("Done")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === PHASE_CONFIRM_SKIP) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||
title = _t("Are you sure?");
|
||||
body = (
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Without completing security on this device, it won’t have " +
|
||||
"access to encrypted messages.",
|
||||
)}</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
className="warning"
|
||||
kind="secondary"
|
||||
onClick={this.onSkipConfirmClick}
|
||||
>
|
||||
{_t("Skip")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
onClick={this.onSkipBackClick}
|
||||
>
|
||||
{_t("Go Back")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
throw new Error(`Unknown phase ${phase}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<AuthHeader />
|
||||
<AuthBody>
|
||||
<h2 className="mx_CompleteSecurity_header">
|
||||
{icon}
|
||||
{title}
|
||||
</h2>
|
||||
<div className="mx_CompleteSecurity_body">
|
||||
{body}
|
||||
</div>
|
||||
</AuthBody>
|
||||
</AuthPage>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -66,7 +66,7 @@ export default class SoftLogout extends React.Component {
|
|||
componentDidMount(): void {
|
||||
// We've ended up here when we don't need to - navigate to login
|
||||
if (!Lifecycle.isSoftLogout()) {
|
||||
dis.dispatch({action: "on_logged_in"});
|
||||
dis.dispatch({action: "start_login"});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ export default createReactClass({
|
|||
|
||||
if (viewUserOnClick) {
|
||||
onClick = () => {
|
||||
dispatcher.dispatch({
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
member: this.props.member,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -23,6 +23,7 @@ import * as sdk from '../../../index';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sendBugReport from '../../../rageshake/submit-rageshake';
|
||||
|
||||
export default class BugReportDialog extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -67,32 +68,30 @@ export default class BugReportDialog extends React.Component {
|
|||
this.setState({ busy: true, progress: null, err: null });
|
||||
this._sendProgressCallback(_t("Preparing to send logs"));
|
||||
|
||||
require(['../../../rageshake/submit-rageshake'], (s) => {
|
||||
s(SdkConfig.get().bug_report_endpoint_url, {
|
||||
userText,
|
||||
sendLogs: true,
|
||||
progressCallback: this._sendProgressCallback,
|
||||
label: this.props.label,
|
||||
}).then(() => {
|
||||
if (!this._unmounted) {
|
||||
this.props.onFinished(false);
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
// N.B. first param is passed to piwik and so doesn't want i18n
|
||||
Modal.createTrackedDialog('Bug report sent', '', QuestionDialog, {
|
||||
title: _t('Logs sent'),
|
||||
description: _t('Thank you!'),
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}
|
||||
}, (err) => {
|
||||
if (!this._unmounted) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
progress: null,
|
||||
err: _t("Failed to send logs: ") + `${err.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
|
||||
userText,
|
||||
sendLogs: true,
|
||||
progressCallback: this._sendProgressCallback,
|
||||
label: this.props.label,
|
||||
}).then(() => {
|
||||
if (!this._unmounted) {
|
||||
this.props.onFinished(false);
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
// N.B. first param is passed to piwik and so doesn't want i18n
|
||||
Modal.createTrackedDialog('Bug report sent', '', QuestionDialog, {
|
||||
title: _t('Logs sent'),
|
||||
description: _t('Thank you!'),
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}
|
||||
}, (err) => {
|
||||
if (!this._unmounted) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
progress: null,
|
||||
err: _t("Failed to send logs: ") + `${err.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -24,8 +24,7 @@ import * as sdk from '../../../index';
|
|||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import createRoom from "../../../createRoom";
|
||||
import {ensureDMExists} from "../../../createRoom";
|
||||
import dis from "../../../dispatcher";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
|
||||
|
@ -100,9 +99,15 @@ export default class DeviceVerifyDialog extends React.Component {
|
|||
if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
|
||||
const roomId = await ensureDMExistsAndOpen(this.props.userId);
|
||||
// throws upon cancellation before having started
|
||||
this._verifier = await client.requestVerificationDM(
|
||||
const request = await client.requestVerificationDM(
|
||||
this.props.userId, roomId, [verificationMethods.SAS],
|
||||
);
|
||||
await request.waitFor(r => r.ready || r.started);
|
||||
if (request.ready) {
|
||||
this._verifier = request.beginKeyVerification(verificationMethods.SAS);
|
||||
} else {
|
||||
this._verifier = request.verifier;
|
||||
}
|
||||
} else {
|
||||
this._verifier = client.beginKeyVerification(
|
||||
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
|
||||
|
@ -316,23 +321,7 @@ export default class DeviceVerifyDialog extends React.Component {
|
|||
}
|
||||
|
||||
async function ensureDMExistsAndOpen(userId) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||
const rooms = roomIds.map(id => client.getRoom(id));
|
||||
const suitableDMRooms = rooms.filter(r => {
|
||||
if (r && r.getMyMembership() === "join") {
|
||||
const member = r.getMember(userId);
|
||||
return member && (member.membership === "invite" || member.membership === "join");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
let roomId;
|
||||
if (suitableDMRooms.length) {
|
||||
const room = suitableDMRooms[0];
|
||||
roomId = room.roomId;
|
||||
} else {
|
||||
roomId = await createRoom({dmUserId: userId, spinner: false, andView: false});
|
||||
}
|
||||
const roomId = ensureDMExists(MatrixClientPeg.get(), userId);
|
||||
// don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen,
|
||||
// we causes us to loose the verifier and restart, and we end up having two verification requests
|
||||
dis.dispatch({
|
||||
|
|
|
@ -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,
|
||||
|
@ -369,6 +411,58 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||
return scores;
|
||||
}, {});
|
||||
|
||||
// Now that we have scores for being in rooms, boost those people who have sent messages
|
||||
// recently, as a way to improve the quality of suggestions. We do this by checking every
|
||||
// room to see who has sent a message in the last few hours, and giving them a score
|
||||
// which correlates to the freshness of their message. In theory, this results in suggestions
|
||||
// which are closer to "continue this conversation" rather than "this person exists".
|
||||
const trueJoinedRooms = client.getRooms().filter(r => r.getMyMembership() === 'join');
|
||||
const now = (new Date()).getTime();
|
||||
const earliestAgeConsidered = now - (60 * 60 * 1000); // 1 hour ago
|
||||
const maxMessagesConsidered = 50; // so we don't iterate over a huge amount of traffic
|
||||
const lastSpoke = {}; // userId: timestamp
|
||||
const lastSpokeMembers = {}; // userId: room member
|
||||
for (const room of trueJoinedRooms) {
|
||||
// Skip low priority rooms and DMs
|
||||
const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
if (Object.keys(room.tags).includes("m.lowpriority") || isDm) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
|
||||
for (let i = events.length - 1; i >= Math.max(0, events.length - maxMessagesConsidered); i--) {
|
||||
const ev = events[i];
|
||||
if (excludedUserIds.includes(ev.getSender())) {
|
||||
continue;
|
||||
}
|
||||
if (ev.getTs() <= earliestAgeConsidered) {
|
||||
break; // give up: all events from here on out are too old
|
||||
}
|
||||
|
||||
if (!lastSpoke[ev.getSender()] || lastSpoke[ev.getSender()] < ev.getTs()) {
|
||||
lastSpoke[ev.getSender()] = ev.getTs();
|
||||
lastSpokeMembers[ev.getSender()] = room.getMember(ev.getSender());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const userId in lastSpoke) {
|
||||
const ts = lastSpoke[userId];
|
||||
const member = lastSpokeMembers[userId];
|
||||
if (!member) continue; // skip people we somehow don't have profiles for
|
||||
|
||||
// Scores from being in a room give a 'good' score of about 1.0-1.5, so for our
|
||||
// boost we'll try and award at least +1.0 for making the list, with +4.0 being
|
||||
// an approximate maximum for being selected.
|
||||
const distanceFromNow = Math.abs(now - ts); // abs to account for slight future messages
|
||||
const inverseTime = (now - earliestAgeConsidered) - distanceFromNow;
|
||||
const scoreBoost = Math.max(1, inverseTime / (15 * 60 * 1000)); // 15min segments to keep scores sane
|
||||
|
||||
let record = memberScores[userId];
|
||||
if (!record) record = memberScores[userId] = {score: 0};
|
||||
record.member = member;
|
||||
record.score += scoreBoost;
|
||||
}
|
||||
|
||||
const members = Object.values(memberScores);
|
||||
members.sort((a, b) => {
|
||||
if (a.score === b.score) {
|
||||
|
@ -384,12 +478,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 +782,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 +818,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 +859,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 +882,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 +926,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.
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import url from 'url';
|
||||
import qs from 'querystring';
|
||||
import qs from 'qs';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
|
|
|
@ -19,102 +19,83 @@ import classNames from 'classnames';
|
|||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import KeyVerificationStateObserver, {getNameForEventRoom, userLabelForEventRoom}
|
||||
import {getNameForEventRoom, userLabelForEventRoom}
|
||||
from '../../../utils/KeyVerificationStateObserver';
|
||||
|
||||
export default class MKeyVerificationConclusion extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.keyVerificationState = null;
|
||||
this.state = {
|
||||
done: false,
|
||||
cancelled: false,
|
||||
otherPartyUserId: null,
|
||||
cancelPartyUserId: null,
|
||||
};
|
||||
const rel = this.props.mxEvent.getRelation();
|
||||
if (rel) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.mxEvent.getRoomId());
|
||||
const requestEvent = room.findEventById(rel.event_id);
|
||||
if (requestEvent) {
|
||||
this._createStateObserver(requestEvent, client);
|
||||
this.state = this._copyState();
|
||||
} else {
|
||||
const findEvent = event => {
|
||||
if (event.getId() === rel.event_id) {
|
||||
this._createStateObserver(event, client);
|
||||
this.setState(this._copyState());
|
||||
room.removeListener("Room.timeline", findEvent);
|
||||
}
|
||||
};
|
||||
room.on("Room.timeline", findEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_createStateObserver(requestEvent, client) {
|
||||
this.keyVerificationState = new KeyVerificationStateObserver(requestEvent, client, () => {
|
||||
this.setState(this._copyState());
|
||||
});
|
||||
}
|
||||
|
||||
_copyState() {
|
||||
const {done, cancelled, otherPartyUserId, cancelPartyUserId} = this.keyVerificationState;
|
||||
return {done, cancelled, otherPartyUserId, cancelPartyUserId};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.keyVerificationState) {
|
||||
this.keyVerificationState.attach();
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
request.on("change", this._onRequestChanged);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.keyVerificationState) {
|
||||
this.keyVerificationState.detach();
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
request.off("change", this._onRequestChanged);
|
||||
}
|
||||
}
|
||||
|
||||
_getName(userId) {
|
||||
const roomId = this.props.mxEvent.getRoomId();
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
const member = room.getMember(userId);
|
||||
return member ? member.name : userId;
|
||||
}
|
||||
_onRequestChanged = () => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
_userLabel(userId) {
|
||||
const name = this._getName(userId);
|
||||
if (name !== userId) {
|
||||
return _t("%(name)s (%(userId)s)", {name, userId});
|
||||
} else {
|
||||
return userId;
|
||||
_shouldRender(mxEvent, request) {
|
||||
// normally should not happen
|
||||
if (!request) {
|
||||
return false;
|
||||
}
|
||||
// .cancel event that was sent after the verification finished, ignore
|
||||
if (mxEvent.getType() === "m.key.verification.cancel" && !request.cancelled) {
|
||||
return false;
|
||||
}
|
||||
// .done event that was sent after the verification cancelled, ignore
|
||||
if (mxEvent.getType() === "m.key.verification.done" && !request.done) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// request hasn't concluded yet
|
||||
if (request.pending) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {mxEvent} = this.props;
|
||||
const request = mxEvent.verificationRequest;
|
||||
|
||||
if (!this._shouldRender(mxEvent, request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const myUserId = client.getUserId();
|
||||
|
||||
let title;
|
||||
|
||||
if (this.state.done) {
|
||||
title = _t("You verified %(name)s", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)});
|
||||
} else if (this.state.cancelled) {
|
||||
if (mxEvent.getSender() === myUserId) {
|
||||
if (request.done) {
|
||||
title = _t("You verified %(name)s", {name: getNameForEventRoom(request.otherUserId, mxEvent)});
|
||||
} else if (request.cancelled) {
|
||||
const userId = request.cancellingUserId;
|
||||
if (userId === myUserId) {
|
||||
title = _t("You cancelled verifying %(name)s",
|
||||
{name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)});
|
||||
} else if (mxEvent.getSender() === this.state.otherPartyUserId) {
|
||||
{name: getNameForEventRoom(request.otherUserId, mxEvent)});
|
||||
} else {
|
||||
title = _t("%(name)s cancelled verifying",
|
||||
{name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)});
|
||||
{name: getNameForEventRoom(userId, mxEvent)});
|
||||
}
|
||||
}
|
||||
|
||||
if (title) {
|
||||
const subtitle = userLabelForEventRoom(this.state.otherPartyUserId, mxEvent);
|
||||
const subtitle = userLabelForEventRoom(request.otherUserId, mxEvent);
|
||||
const classes = classNames("mx_EventTile_bubble", "mx_KeyVerification", "mx_KeyVerification_icon", {
|
||||
mx_KeyVerification_icon_verified: this.state.done,
|
||||
mx_KeyVerification_icon_verified: request.done,
|
||||
});
|
||||
return (<div className={classes}>
|
||||
<div className="mx_KeyVerification_title">{title}</div>
|
||||
|
|
|
@ -17,48 +17,66 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from "../../../Modal";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import KeyVerificationStateObserver, {getNameForEventRoom, userLabelForEventRoom}
|
||||
import {getNameForEventRoom, userLabelForEventRoom}
|
||||
from '../../../utils/KeyVerificationStateObserver';
|
||||
import dis from "../../../dispatcher";
|
||||
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
|
||||
|
||||
export default class MKeyVerificationRequest extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.keyVerificationState = new KeyVerificationStateObserver(this.props.mxEvent, MatrixClientPeg.get(), () => {
|
||||
this.setState(this._copyState());
|
||||
});
|
||||
this.state = this._copyState();
|
||||
}
|
||||
|
||||
_copyState() {
|
||||
const {accepted, done, cancelled, cancelPartyUserId, otherPartyUserId} = this.keyVerificationState;
|
||||
return {accepted, done, cancelled, cancelPartyUserId, otherPartyUserId};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.keyVerificationState.attach();
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
request.on("change", this._onRequestChanged);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.keyVerificationState.detach();
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
request.off("change", this._onRequestChanged);
|
||||
}
|
||||
}
|
||||
|
||||
_onAcceptClicked = () => {
|
||||
const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog');
|
||||
// todo: validate event, for example if it has sas in the methods.
|
||||
const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS);
|
||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
||||
verifier,
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
_openRequest = () => {
|
||||
const {verificationRequest} = this.props.mxEvent;
|
||||
dis.dispatch({
|
||||
action: "set_right_panel_phase",
|
||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
refireParams: {verificationRequest},
|
||||
});
|
||||
};
|
||||
|
||||
_onRejectClicked = () => {
|
||||
// todo: validate event, for example if it has sas in the methods.
|
||||
const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS);
|
||||
verifier.cancel("User declined");
|
||||
_onRequestChanged = () => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
_onAcceptClicked = async () => {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
try {
|
||||
await request.accept();
|
||||
this._openRequest();
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onRejectClicked = async () => {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
try {
|
||||
await request.cancel();
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_acceptedLabel(userId) {
|
||||
|
@ -82,46 +100,49 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
const FormButton = sdk.getComponent("elements.FormButton");
|
||||
|
||||
const {mxEvent} = this.props;
|
||||
const fromUserId = mxEvent.getSender();
|
||||
const content = mxEvent.getContent();
|
||||
const toUserId = content.to;
|
||||
const client = MatrixClientPeg.get();
|
||||
const myUserId = client.getUserId();
|
||||
const isOwn = fromUserId === myUserId;
|
||||
const request = mxEvent.verificationRequest;
|
||||
|
||||
if (!request || request.invalid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let title;
|
||||
let subtitle;
|
||||
let stateNode;
|
||||
|
||||
if (this.state.accepted || this.state.cancelled) {
|
||||
const accepted = request.ready || request.started || request.done;
|
||||
if (accepted || request.cancelled) {
|
||||
let stateLabel;
|
||||
if (this.state.accepted) {
|
||||
stateLabel = this._acceptedLabel(toUserId);
|
||||
} else if (this.state.cancelled) {
|
||||
stateLabel = this._cancelledLabel(this.state.cancelPartyUserId);
|
||||
if (accepted) {
|
||||
stateLabel = (<AccessibleButton onClick={this._openRequest}>
|
||||
{this._acceptedLabel(request.receivingUserId)}
|
||||
</AccessibleButton>);
|
||||
} else {
|
||||
stateLabel = this._cancelledLabel(request.cancellingUserId);
|
||||
}
|
||||
stateNode = (<div className="mx_KeyVerification_state">{stateLabel}</div>);
|
||||
}
|
||||
|
||||
if (toUserId === myUserId) { // request sent to us
|
||||
if (!request.initiatedByMe) {
|
||||
title = (<div className="mx_KeyVerification_title">{
|
||||
_t("%(name)s wants to verify", {name: getNameForEventRoom(fromUserId, mxEvent)})}</div>);
|
||||
_t("%(name)s wants to verify", {name: getNameForEventRoom(request.requestingUserId, mxEvent)})}</div>);
|
||||
subtitle = (<div className="mx_KeyVerification_subtitle">{
|
||||
userLabelForEventRoom(fromUserId, mxEvent)}</div>);
|
||||
const isResolved = !(this.state.accepted || this.state.cancelled || this.state.done);
|
||||
if (isResolved) {
|
||||
const FormButton = sdk.getComponent("elements.FormButton");
|
||||
userLabelForEventRoom(request.requestingUserId, mxEvent)}</div>);
|
||||
if (request.requested && !request.observeOnly) {
|
||||
stateNode = (<div className="mx_KeyVerification_buttons">
|
||||
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
|
||||
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />
|
||||
</div>);
|
||||
}
|
||||
} else if (isOwn) { // request sent by us
|
||||
} else { // request sent by us
|
||||
title = (<div className="mx_KeyVerification_title">{
|
||||
_t("You sent a verification request")}</div>);
|
||||
subtitle = (<div className="mx_KeyVerification_subtitle">{
|
||||
userLabelForEventRoom(this.state.otherPartyUserId, mxEvent)}</div>);
|
||||
userLabelForEventRoom(request.receivingUserId, mxEvent)}</div>);
|
||||
}
|
||||
|
||||
if (title) {
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
Copyright 2019 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 * as sdk from '../../../index';
|
||||
import {_t} from "../../../languageHandler";
|
||||
|
||||
export default class EncryptionInfo extends React.PureComponent {
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (<div className="mx_UserInfo"><div className="mx_UserInfo_container">
|
||||
<h3>{_t("Verify User")}</h3>
|
||||
<p>{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}</p>
|
||||
<p>{_t("For maximum security, do this in person.")}</p>
|
||||
<AccessibleButton kind="primary" onClick={this.props.onStartVerification}>{_t("Start Verification")}</AccessibleButton>
|
||||
</div></div>);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Copyright 2019 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 EncryptionInfo from "./EncryptionInfo";
|
||||
import VerificationPanel from "./VerificationPanel";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {ensureDMExists} from "../../../createRoom";
|
||||
|
||||
export default class EncryptionPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
const request = this.props.verificationRequest || this.state.verificationRequest;
|
||||
const {member} = this.props;
|
||||
if (request) {
|
||||
return <VerificationPanel request={request} key={request.channel.transactionId} />;
|
||||
} else if (member) {
|
||||
return <EncryptionInfo onStartVerification={this._onStartVerification} member={member} />;
|
||||
} else {
|
||||
return <p>Not a member nor request, not sure what to render</p>;
|
||||
}
|
||||
}
|
||||
|
||||
_onStartVerification = async () => {
|
||||
const client = MatrixClientPeg.get();
|
||||
const {member} = this.props;
|
||||
const roomId = await ensureDMExists(client, member.userId);
|
||||
const verificationRequest = await client.requestVerificationDM(member.userId, roomId);
|
||||
this.setState({verificationRequest});
|
||||
};
|
||||
}
|
|
@ -66,8 +66,13 @@ export default class GroupHeaderButtons extends HeaderButtons {
|
|||
}
|
||||
|
||||
_onMembersClicked() {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
|
||||
if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) {
|
||||
// send the active phase to trigger a toggle
|
||||
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberInfo);
|
||||
} else {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
|
||||
}
|
||||
}
|
||||
|
||||
_onRoomsClicked() {
|
||||
|
|
|
@ -23,10 +23,12 @@ import { _t } from '../../../languageHandler';
|
|||
import HeaderButton from './HeaderButton';
|
||||
import HeaderButtons, {HEADER_KIND_ROOM} from './HeaderButtons';
|
||||
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../../stores/RightPanelStore";
|
||||
|
||||
const MEMBER_PHASES = [
|
||||
RIGHT_PANEL_PHASES.RoomMemberList,
|
||||
RIGHT_PANEL_PHASES.RoomMemberInfo,
|
||||
RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
RIGHT_PANEL_PHASES.Room3pidMemberInfo,
|
||||
];
|
||||
|
||||
|
@ -56,8 +58,13 @@ export default class RoomHeaderButtons extends HeaderButtons {
|
|||
}
|
||||
|
||||
_onMembersClicked() {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList);
|
||||
if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) {
|
||||
// send the active phase to trigger a toggle
|
||||
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, RightPanelStore.getSharedInstance().roomPanelPhaseParams);
|
||||
} else {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList);
|
||||
}
|
||||
}
|
||||
|
||||
_onFilesClicked() {
|
||||
|
|
|
@ -40,6 +40,7 @@ import E2EIcon from "../rooms/E2EIcon";
|
|||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {textualPowerLevel} from '../../../Roles';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
|
||||
|
||||
const _disambiguateDevices = (devices) => {
|
||||
const names = Object.create(null);
|
||||
|
@ -117,6 +118,14 @@ function verifyDevice(userId, device) {
|
|||
}, null, /* priority = */ false, /* static = */ true);
|
||||
}
|
||||
|
||||
function verifyUser(user) {
|
||||
dis.dispatch({
|
||||
action: "set_right_panel_phase",
|
||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
refireParams: {member: user},
|
||||
});
|
||||
}
|
||||
|
||||
function DeviceItem({userId, device}) {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId);
|
||||
|
@ -1225,15 +1234,13 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
|||
setDevices(null);
|
||||
}
|
||||
}
|
||||
if (isRoomEncrypted) {
|
||||
_downloadDeviceList();
|
||||
}
|
||||
_downloadDeviceList();
|
||||
|
||||
// Handle being unmounted
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [cli, user.userId, isRoomEncrypted]);
|
||||
}, [cli, user.userId]);
|
||||
|
||||
// Listen to changes
|
||||
useEffect(() => {
|
||||
|
@ -1249,18 +1256,13 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isRoomEncrypted) {
|
||||
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
}
|
||||
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
// Handle being unmounted
|
||||
return () => {
|
||||
cancel = true;
|
||||
if (isRoomEncrypted) {
|
||||
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
}
|
||||
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
};
|
||||
}, [cli, user.userId, isRoomEncrypted]);
|
||||
}, [cli, user.userId]);
|
||||
|
||||
let text;
|
||||
if (!isRoomEncrypted) {
|
||||
|
@ -1275,22 +1277,24 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
|||
text = _t("Messages in this room are end-to-end encrypted.");
|
||||
}
|
||||
|
||||
const devicesSection = isRoomEncrypted ?
|
||||
(<DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />) : null;
|
||||
|
||||
const userVerified = cli.checkUserTrust(user.userId).isVerified();
|
||||
const isMe = user.userId === cli.getUserId();
|
||||
let verifyButton;
|
||||
if (!userVerified) {
|
||||
verifyButton = <AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyDevice(user.userId, null)}>
|
||||
if (!userVerified && !isMe) {
|
||||
verifyButton = <AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyUser(user)}>
|
||||
{_t("Verify")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const devicesSection = <DevicesSection
|
||||
loading={devices === undefined}
|
||||
devices={devices} userId={user.userId} />;
|
||||
|
||||
const securitySection = (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Security") }</h3>
|
||||
<p>{ text }</p>
|
||||
{verifyButton}
|
||||
{ verifyButton }
|
||||
{ devicesSection }
|
||||
</div>
|
||||
);
|
||||
|
@ -1308,7 +1312,7 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
|||
|
||||
<div className="mx_UserInfo_container">
|
||||
<div className="mx_UserInfo_profile">
|
||||
<div >
|
||||
<div>
|
||||
<h2 aria-label={displayName}>
|
||||
{ e2eIcon }
|
||||
{ displayName }
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
Copyright 2019 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 * as sdk from '../../../index';
|
||||
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
||||
|
||||
export default class VerificationPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this._hasVerifier = !!props.request.verifier;
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="mx_UserInfo">
|
||||
<div className="mx_UserInfo_container">
|
||||
{ this.renderStatus() }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
renderStatus() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const {request} = this.props;
|
||||
|
||||
if (request.requested) {
|
||||
return (<p>Waiting for {request.otherUserId} to accept ... <Spinner /></p>);
|
||||
} else if (request.ready) {
|
||||
const verifyButton = <AccessibleButton kind="primary" onClick={this._startSAS}>
|
||||
Verify by emoji
|
||||
</AccessibleButton>;
|
||||
return (<p>{request.otherUserId} is ready, start {verifyButton}</p>);
|
||||
} else if (request.started) {
|
||||
if (this.state.sasWaitingForOtherParty) {
|
||||
return <p>Waiting for {request.otherUserId} to confirm ...</p>;
|
||||
} else if (this.state.sasEvent) {
|
||||
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
|
||||
return (<div>
|
||||
<VerificationShowSas
|
||||
sas={this.state.sasEvent.sas}
|
||||
onCancel={this._onSasMismatchesClick}
|
||||
onDone={this._onSasMatchesClick}
|
||||
/>
|
||||
</div>);
|
||||
} else {
|
||||
return (<p>Setting up SAS verification...</p>);
|
||||
}
|
||||
} else if (request.done) {
|
||||
return <p>verified {request.otherUserId}!!</p>;
|
||||
} else if (request.cancelled) {
|
||||
return <p>cancelled by {request.cancellingUserId}!</p>;
|
||||
}
|
||||
}
|
||||
|
||||
_startSAS = async () => {
|
||||
const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS);
|
||||
try {
|
||||
await verifier.verify();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
this.setState({sasEvent: null});
|
||||
}
|
||||
};
|
||||
|
||||
_onSasMatchesClick = () => {
|
||||
this.setState({sasWaitingForOtherParty: true});
|
||||
this.state.sasEvent.confirm();
|
||||
};
|
||||
|
||||
_onSasMismatchesClick = () => {
|
||||
this.state.sasEvent.cancel();
|
||||
};
|
||||
|
||||
_onVerifierShowSas = (sasEvent) => {
|
||||
this.setState({sasEvent});
|
||||
};
|
||||
|
||||
_onRequestChange = async () => {
|
||||
const {request} = this.props;
|
||||
if (!this._hasVerifier && !!request.verifier) {
|
||||
request.verifier.on('show_sas', this._onVerifierShowSas);
|
||||
try {
|
||||
// on the requester side, this is also awaited in _startSAS,
|
||||
// but that's ok as verify should return the same promise.
|
||||
await request.verifier.verify();
|
||||
} catch (err) {
|
||||
console.error("error verify", err);
|
||||
}
|
||||
} else if (this._hasVerifier && !request.verifier) {
|
||||
request.verifier.removeListener('show_sas', this._onVerifierShowSas);
|
||||
}
|
||||
this._hasVerifier = !!request.verifier;
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.request.on("change", this._onRequestChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.request.off("change", this._onRequestChange);
|
||||
}
|
||||
}
|
|
@ -209,8 +209,9 @@ 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
|
||||
this._modifiedFlag = true;
|
||||
replaceRangeAndMoveCaret(range, []);
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
@ -259,8 +260,8 @@ export default class BasicMessageEditor extends React.Component {
|
|||
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
|
||||
const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset);
|
||||
caret.offset += textToInsert.length;
|
||||
this.props.model.update(newText, inputType, caret);
|
||||
this._modifiedFlag = true;
|
||||
this.props.model.update(newText, inputType, caret);
|
||||
}
|
||||
|
||||
// this is used later to see if we need to recalculate the caret
|
||||
|
|
|
@ -60,14 +60,14 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
this.setState(this._getUpdatedStatus());
|
||||
};
|
||||
|
||||
_getUpdatedStatus() {
|
||||
async _getUpdatedStatus() {
|
||||
// XXX: Add public accessors if we keep this around in production
|
||||
const cli = MatrixClientPeg.get();
|
||||
const crossSigning = cli._crypto._crossSigningInfo;
|
||||
const secretStorage = cli._crypto._secretStorage;
|
||||
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
||||
const crossSigningPrivateKeysInStorage = crossSigning.isStoredInSecretStorage(secretStorage);
|
||||
const secretStorageKeyInAccount = secretStorage.hasKey();
|
||||
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
||||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||
|
||||
return {
|
||||
crossSigningPublicKeysOnDevice,
|
||||
|
|
|
@ -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] || {}),
|
||||
));
|
||||
|
||||
|
|
|
@ -160,8 +160,8 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
// for free. So we might as well use that for our own purposes.
|
||||
const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl();
|
||||
const authClient = new IdentityAuthClient();
|
||||
const idAccessToken = await authClient.getAccessToken({ check: false });
|
||||
try {
|
||||
const idAccessToken = await authClient.getAccessToken({ check: false });
|
||||
await startTermsFlow([new Service(
|
||||
SERVICE_TYPES.IS,
|
||||
idServerUrl,
|
||||
|
|
|
@ -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>);
|
||||
}
|
||||
}
|
|
@ -18,57 +18,43 @@ 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 {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
||||
import KeyVerificationStateObserver, {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver";
|
||||
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) {
|
||||
super(props);
|
||||
const {event, timeout} = props.request;
|
||||
// to_device requests don't have a timestamp, so consider them age=0
|
||||
const age = event.getTs() ? event.getLocalAge() : 0;
|
||||
const remaining = Math.max(0, timeout - age);
|
||||
const counter = Math.ceil(remaining / 1000);
|
||||
this.state = {counter};
|
||||
if (this.props.requestObserver) {
|
||||
this.props.requestObserver.setCallback(this._checkRequestIsPending);
|
||||
}
|
||||
this.state = {counter: Math.ceil(props.request.timeout / 1000)};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.requestObserver) {
|
||||
this.props.requestObserver.attach();
|
||||
this._checkRequestIsPending();
|
||||
}
|
||||
const {request} = this.props;
|
||||
this._intervalHandle = setInterval(() => {
|
||||
let {counter} = this.state;
|
||||
counter -= 1;
|
||||
if (counter <= 0) {
|
||||
this.cancel();
|
||||
} else {
|
||||
this.setState({counter});
|
||||
}
|
||||
counter = Math.max(0, counter - 1);
|
||||
this.setState({counter});
|
||||
}, 1000);
|
||||
request.on("change", this._checkRequestIsPending);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this._intervalHandle);
|
||||
if (this.props.requestObserver) {
|
||||
this.props.requestObserver.detach();
|
||||
}
|
||||
const {request} = this.props;
|
||||
request.off("change", this._checkRequestIsPending);
|
||||
}
|
||||
|
||||
_checkRequestIsPending = () => {
|
||||
if (!this.props.requestObserver.pending) {
|
||||
this.props.dismiss();
|
||||
const {request} = this.props;
|
||||
if (request.ready || request.started || request.done || request.cancelled || request.observeOnly) {
|
||||
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
this.props.dismiss();
|
||||
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
||||
try {
|
||||
this.props.request.cancel();
|
||||
} catch (err) {
|
||||
|
@ -76,9 +62,10 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
accept = () => {
|
||||
this.props.dismiss();
|
||||
const {event} = this.props.request;
|
||||
accept = async () => {
|
||||
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
||||
const {request} = this.props;
|
||||
const {event} = request;
|
||||
// no room id for to_device requests
|
||||
if (event.getRoomId()) {
|
||||
dis.dispatch({
|
||||
|
@ -87,18 +74,23 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
should_peek: false,
|
||||
});
|
||||
}
|
||||
|
||||
const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS);
|
||||
const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog');
|
||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
||||
verifier,
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
try {
|
||||
await request.accept();
|
||||
dis.dispatch({
|
||||
action: "set_right_panel_phase",
|
||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
refireParams: {verificationRequest: request},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const FormButton = sdk.getComponent("elements.FormButton");
|
||||
const {event} = this.props.request;
|
||||
const userId = event.getSender();
|
||||
const {request} = this.props;
|
||||
const {event} = request;
|
||||
const userId = request.otherUserId;
|
||||
let nameLabel = event.getRoomId() ? userLabelForEventRoom(userId, event) : userId;
|
||||
// for legacy to_device verification requests
|
||||
if (nameLabel === userId) {
|
||||
|
@ -119,7 +111,6 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
}
|
||||
|
||||
VerificationRequestToast.propTypes = {
|
||||
dismiss: PropTypes.func.isRequired,
|
||||
request: PropTypes.object.isRequired,
|
||||
requestObserver: PropTypes.instanceOf(KeyVerificationStateObserver),
|
||||
toastKey: PropTypes.string.isRequired,
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 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.
|
||||
|
@ -20,7 +21,7 @@ import * as sdk from './index';
|
|||
import { _t } from './languageHandler';
|
||||
import dis from "./dispatcher";
|
||||
import * as Rooms from "./Rooms";
|
||||
|
||||
import DMRoomMap from "./utils/DMRoomMap";
|
||||
import {getAddressType} from "./UserAddress";
|
||||
|
||||
/**
|
||||
|
@ -139,3 +140,23 @@ export default function createRoom(opts) {
|
|||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
export async function ensureDMExists(client, userId) {
|
||||
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||
const rooms = roomIds.map(id => client.getRoom(id));
|
||||
const suitableDMRooms = rooms.filter(r => {
|
||||
if (r && r.getMyMembership() === "join") {
|
||||
const member = r.getMember(userId);
|
||||
return member && (member.membership === "invite" || member.membership === "join");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
let roomId;
|
||||
if (suitableDMRooms.length) {
|
||||
const room = suitableDMRooms[0];
|
||||
roomId = room.roomId;
|
||||
} else {
|
||||
roomId = await createRoom({dmUserId: userId, spinner: false, andView: false});
|
||||
}
|
||||
return roomId;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
@ -1113,6 +1117,10 @@
|
|||
"URL previews are disabled by default for participants in this room.": "URL previews are disabled by default for participants in this room.",
|
||||
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.",
|
||||
"When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.",
|
||||
"Verify User": "Verify User",
|
||||
"For extra security, verify this user by checking a one-time code on both of your devices.": "For extra security, verify this user by checking a one-time code on both of your devices.",
|
||||
"For maximum security, do this in person.": "For maximum security, do this in person.",
|
||||
"Start Verification": "Start Verification",
|
||||
"Members": "Members",
|
||||
"Files": "Files",
|
||||
"Trusted": "Trusted",
|
||||
|
@ -1130,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",
|
||||
|
@ -1438,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.",
|
||||
|
@ -1455,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",
|
||||
|
@ -1848,6 +1861,14 @@
|
|||
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
|
||||
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
||||
"Could not load user profile": "Could not load user profile",
|
||||
"Complete security": "Complete security",
|
||||
"Verify this session to grant it access to encrypted messages.": "Verify this session to grant it access to encrypted messages.",
|
||||
"Start": "Start",
|
||||
"Session verified": "Session verified",
|
||||
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
|
||||
"Done": "Done",
|
||||
"Without completing security on this device, it won’t have access to encrypted messages.": "Without completing security on this device, it won’t have access to encrypted messages.",
|
||||
"Go Back": "Go Back",
|
||||
"Failed to send email": "Failed to send email",
|
||||
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
|
||||
"A new password must be entered.": "A new password must be entered.",
|
||||
|
@ -1952,7 +1973,7 @@
|
|||
"Import": "Import",
|
||||
"Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.": "Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.",
|
||||
"Restore": "Restore",
|
||||
"Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup": "Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup",
|
||||
"Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup.": "Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup.",
|
||||
"Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.",
|
||||
"<b>Warning</b>: You should only set up secret storage from a trusted computer.": "<b>Warning</b>: You should only set up secret storage from a trusted computer.",
|
||||
"We'll use secret storage to optionally store an encrypted copy of your cross-signing identity for verifying other devices and message keys on our server. Protect your access to encrypted messages with a passphrase to keep it secure.": "We'll use secret storage to optionally store an encrypted copy of your cross-signing identity for verifying other devices and message keys on our server. Protect your access to encrypted messages with a passphrase to keep it secure.",
|
||||
|
|
|
@ -2035,5 +2035,44 @@
|
|||
"Go": "Joan",
|
||||
"Suggestions": "Proposamenak",
|
||||
"Failed to find the following users": "Ezin izan dira honako erabiltzaile hauek aurkitu",
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "Honako erabiltzaile hauek agian ez dira existitzen edo baliogabeak dira, eta ezin dira gonbidatu: %(csvNames)s"
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "Honako erabiltzaile hauek agian ez dira existitzen edo baliogabeak dira, eta ezin dira gonbidatu: %(csvNames)s",
|
||||
"Show a presence dot next to DMs in the room list": "Erakutsi presentzia puntua mezu zuzenen ondoan gelen zerrendan",
|
||||
"Lock": "Blokeatu",
|
||||
"Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.": "Gakoen babes-kopia gaituta dago zure kontuan baina ez da saio honetarako ezarri. Biltegi sekretua ezartzeko, berrezarri zure gakoen babes-kopia.",
|
||||
"Restore": "Berrezarri",
|
||||
"Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup": "Biltegi sekretua ezarriko da zure oraingo gakoen babes-kopiaren xehetasunak erabiliz. Zure biltegi sekretuaren pasa-esaldia eta berreskuratze gakoa lehen gakoen babes-kopiarako ziren berberak izango dira",
|
||||
"Restore your Key Backup": "Berrezarri zure gakoen babes-kopia",
|
||||
"a few seconds ago": "duela segundo batzuk",
|
||||
"about a minute ago": "duela minutu bat inguru",
|
||||
"%(num)s minutes ago": "duela %(num)s minutu",
|
||||
"about an hour ago": "duela ordubete inguru",
|
||||
"%(num)s hours ago": "duela %(num)s ordu",
|
||||
"about a day ago": "duela egun bat inguru",
|
||||
"%(num)s days ago": "duela %(num)s egun",
|
||||
"a few seconds from now": "hemendik segundo batzuetara",
|
||||
"about a minute from now": "hemendik minutu batera",
|
||||
"%(num)s minutes from now": "hemendik %(num)s minututara",
|
||||
"about an hour from now": "hemendik ordubetera",
|
||||
"%(num)s hours from now": "hemendik %(num)s ordutara",
|
||||
"about a day from now": "hemendik egun batera",
|
||||
"%(num)s days from now": "hemendik %(num)s egunetara",
|
||||
"New Session": "Saio berria",
|
||||
"New invite dialog": "Gonbidapen elkarrizketa-koadro berria",
|
||||
"Other users may not trust it": "Beste erabiltzaile batzuk ez fidagarritzat jo lezakete",
|
||||
"Later": "Geroago",
|
||||
"Failed to invite the following users to chat: %(csvUsers)s": "Ezin izan dira honako erabiltzaile hauek gonbidatu txatera: %(csvUsers)s",
|
||||
"We couldn't create your DM. Please check the users you want to invite and try again.": "Ezin izan dugu zure mezu zuena sortu. Egiaztatu gonbidatu nahi dituzun erabiltzaileak eta saiatu berriro.",
|
||||
"Something went wrong trying to invite the users.": "Okerren bat egon da erabiltzaileak gonbidatzen saiatzean.",
|
||||
"We couldn't invite those users. Please check the users you want to invite and try again.": "Ezin izan ditugu erabiltzaile horiek gonbidatu. Egiaztatu gonbidatu nahi dituzun erabiltzaileak eta saiatu berriro.",
|
||||
"Recently Direct Messaged": "Berriki mezu zuzena bidalita",
|
||||
"If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.": "Ez baduzu baten bat aurkitzen, eskatu bere erabiltzaile-izena (adib. @erabiltzailea:zerbitzaria.eus) edo <a>partekatu gela hau</a>.",
|
||||
"Complete security": "Segurtasun osoa",
|
||||
"Verify this session to grant it access to encrypted messages.": "Egiaztatu saio hau zifratutako mezuetara sarbidea emateko.",
|
||||
"Start": "Hasi",
|
||||
"Session verified": "Saioa egiaztatuta",
|
||||
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Zure saio berria orain egiaztatuta dago. Zure zifratutako mezuetara sarbidea du, eta beste erabiltzaileek fidagarri gisa ikusiko zaituzte.",
|
||||
"Done": "Egina",
|
||||
"Without completing security on this device, it won’t have access to encrypted messages.": "Gailu honetan segurtasuna osatu ezean, ez du zifratutako mezuetara sarbiderik izango.",
|
||||
"Go Back": "Joan atzera",
|
||||
"Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup.": "Biltegi sekretua oraingo gakoen babeskopiaren xehetasunak erabiliz ezarriko da. Zure biltegi sekretuaren pasa-esaldia eta berreskuratze gakoa zure gakoen babes-kopiarako zenerabiltzanak izango dira."
|
||||
}
|
||||
|
|
|
@ -2025,5 +2025,6 @@
|
|||
"Recent Conversations": "Viimeaikaiset keskustelut",
|
||||
"Direct Messages": "Yksityisviestit",
|
||||
"If you can't find someone, ask them for their username, or share your username (%(userId)s) or <a>profile link</a>.": "Jos et löydä jotakuta, kysy hänen käyttäjätunnusta, tai anna oma käyttäjätunnuksesi (%(userId)s) tai <a>linkin profiiliisi</a> hänelle.",
|
||||
"Go": "Mene"
|
||||
"Go": "Mene",
|
||||
"Lock": "Lukko"
|
||||
}
|
||||
|
|
|
@ -2036,5 +2036,44 @@
|
|||
"Below is a list of bridges connected to this room.": "Vous trouverez ci-dessous la liste des passerelles connectées à ce salon.",
|
||||
"Suggestions": "Suggestions",
|
||||
"Failed to find the following users": "Impossible de trouver les utilisateurs suivants",
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "Les utilisateurs suivant n’existent peut-être pas ou ne sont pas valides, et ne peuvent pas être invités : %(csvNames)s"
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "Les utilisateurs suivant n’existent peut-être pas ou ne sont pas valides, et ne peuvent pas être invités : %(csvNames)s",
|
||||
"Show a presence dot next to DMs in the room list": "Afficher une pastille de présence à côté des messages directs dans la liste des salons",
|
||||
"Lock": "Cadenas",
|
||||
"Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.": "La sauvegarde de clés est activée pour votre compte mais n’a pas été configurée pour cette session. Pour configurer le coffre secret, restaurez votre sauvegarde de clés.",
|
||||
"Restore": "Restaurer",
|
||||
"Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup": "Le coffre secret sera configuré en utilisant les paramètres existants de la sauvegarde de clés. Votre phrase de passe et votre clé de récupération seront les mêmes que celles de votre sauvegarde de clés",
|
||||
"Restore your Key Backup": "Restaurer votre sauvegarde de clés",
|
||||
"a few seconds ago": "il y a quelques secondes",
|
||||
"about a minute ago": "il y a environ une minute",
|
||||
"%(num)s minutes ago": "il y a %(num)s minutes",
|
||||
"about an hour ago": "il y a environ une heure",
|
||||
"%(num)s hours ago": "il y a %(num)s heures",
|
||||
"about a day ago": "il y a environ un jour",
|
||||
"%(num)s days ago": "il y a %(num)s jours",
|
||||
"a few seconds from now": "dans quelques secondes",
|
||||
"about a minute from now": "dans une minute environ",
|
||||
"%(num)s minutes from now": "dans %(num)s minutes",
|
||||
"about an hour from now": "dans une heure environ",
|
||||
"%(num)s hours from now": "dans %(num)s heures",
|
||||
"about a day from now": "dans un jour environ",
|
||||
"%(num)s days from now": "dans %(num)s jours",
|
||||
"New invite dialog": "Nouvelle boîte de dialogue d’invitation",
|
||||
"Failed to invite the following users to chat: %(csvUsers)s": "Échec de l’invitation des utilisateurs suivants à discuter : %(csvUsers)s",
|
||||
"We couldn't create your DM. Please check the users you want to invite and try again.": "Impossible de créer votre Message direct. Vérifiez les utilisateurs que vous souhaitez inviter et réessayez.",
|
||||
"Something went wrong trying to invite the users.": "Une erreur est survenue en essayant d’inviter les utilisateurs.",
|
||||
"We couldn't invite those users. Please check the users you want to invite and try again.": "Impossible d’inviter ces utilisateurs. Vérifiez les utilisateurs que vous souhaitez inviter et réessayez.",
|
||||
"Recently Direct Messaged": "Messages directs récents",
|
||||
"If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.": "S’il y a quelqu’un que vous n’arrivez pas à trouver, demandez-lui son nom d’utilisateur (par ex. @utilisateur:serveur.com) ou <a>partagez ce salon</a>.",
|
||||
"Complete security": "Compléter la sécurité",
|
||||
"Verify this session to grant it access to encrypted messages.": "Vérifiez cette session pour l’autoriser à accéder à vos messages chiffrés.",
|
||||
"Start": "Commencer",
|
||||
"Session verified": "Session vérifiée",
|
||||
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Votre nouvelle session est maintenant vérifiée. Elle a accès à vos messages chiffrés et les autres utilisateurs la verront comme fiable.",
|
||||
"Done": "Terminé",
|
||||
"Without completing security on this device, it won’t have access to encrypted messages.": "Si vous ne complétez pas la sécurité sur cet appareil, vous n’aurez pas accès aux messages chiffrés.",
|
||||
"Go Back": "Retourner en arrière",
|
||||
"Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup.": "Le coffre secret sera configuré en utilisant les détails existants de votre sauvegarde de clés. Votre phrase de passe et votre clé de récupération seront les mêmes que celles de votre sauvegarde de clés.",
|
||||
"New Session": "Nouvelle session",
|
||||
"Other users may not trust it": "D’autres utilisateurs pourraient ne pas lui faire confiance",
|
||||
"Later": "Plus tard"
|
||||
}
|
||||
|
|
|
@ -269,7 +269,7 @@
|
|||
"Show Text Formatting Toolbar": "Szöveg formázási eszköztár megjelenítése",
|
||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Az időbélyegek 12 órás formátumban mutatása (pl.: 2:30pm)",
|
||||
"Signed Out": "Kijelentkezett",
|
||||
"Sign in": "Bejelentkezett",
|
||||
"Sign in": "Bejelentkezés",
|
||||
"Sign out": "Kijelentkezés",
|
||||
"%(count)s of your messages have not been sent.|other": "Néhány üzeneted nem lett elküldve.",
|
||||
"Someone": "Valaki",
|
||||
|
@ -2037,5 +2037,44 @@
|
|||
"Below is a list of bridges connected to this room.": "Alább látható a lista a szobához kapcsolódó hidakról.",
|
||||
"Suggestions": "Javaslatok",
|
||||
"Failed to find the following users": "Az alábbi felhasználók nem találhatók",
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "Az alábbi felhasználók nem léteznek vagy hibásan vannak megadva és nem lehet őket meghívni: %(csvNames)s"
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "Az alábbi felhasználók nem léteznek vagy hibásan vannak megadva és nem lehet őket meghívni: %(csvNames)s",
|
||||
"Show a presence dot next to DMs in the room list": "Jelenlét pötty mutatása a Közvetlen beszélgetések mellett a szobák listájában",
|
||||
"Lock": "Zár",
|
||||
"a few seconds ago": "néhány másodperce",
|
||||
"about a minute ago": "percekkel ezelőtt",
|
||||
"%(num)s minutes ago": "%(num)s perccel ezelőtt",
|
||||
"about an hour ago": "egy órája",
|
||||
"%(num)s hours ago": "%(num)s órával ezelőtt",
|
||||
"about a day ago": "egy napja",
|
||||
"%(num)s days ago": "%(num)s nappal ezelőtt",
|
||||
"a few seconds from now": "másodpercek múlva",
|
||||
"about a minute from now": "percek múlva",
|
||||
"%(num)s minutes from now": "%(num)s perc múlva",
|
||||
"about an hour from now": "egy óra múlva",
|
||||
"%(num)s hours from now": "%(num)s óra múlva",
|
||||
"about a day from now": "egy nap múlva",
|
||||
"%(num)s days from now": "%(num)s nap múlva",
|
||||
"Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.": "A Kulcs Mentés a fiókhoz igen de ehhez a munkamenethez nincs beállítva. A Biztonsági tároló beállításához állítsd vissza a kulcs mentést.",
|
||||
"Restore": "Visszaállít",
|
||||
"Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup": "A Biztonsági Tároló a meglévő kulcs mentés adatai alapján lesz beállítva. A biztonsági tárolóhoz tartozó jelmondat és a visszaállítási kulcs azonos lesz ahogy azok a kulcs mentéshez voltak",
|
||||
"Restore your Key Backup": "Kulcs Mentés visszaállítása",
|
||||
"Complete security": "Biztonság beállítása",
|
||||
"Verify this session to grant it access to encrypted messages.": "A titkosított üzenetekhez való hozzáféréshez hitelesítsd ezt a munkamenetet.",
|
||||
"Start": "Indít",
|
||||
"Session verified": "Munkamenet hitelesítve",
|
||||
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Ez a munkameneted hitelesítve van. A titkosított üzenetekhez hozzáférése van és más felhasználók megbízhatónak látják.",
|
||||
"Done": "Kész",
|
||||
"Without completing security on this device, it won’t have access to encrypted messages.": "Az eszköz biztonságának beállítása nélkül nem férhet hozzá a titkosított üzenetekhez.",
|
||||
"Go Back": "Vissza",
|
||||
"Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup.": "A Biztonsági Tároló a kulcs mentés adatainak felhasználásával lesz beállítva. A biztonsági tároló jelmondata és a visszaállítási kulcs ugyanaz lesz mint a kulcs mentéshez használt.",
|
||||
"New invite dialog": "Új meghívó párbeszédablak",
|
||||
"New Session": "Új Munkamenet",
|
||||
"Other users may not trust it": "Más felhasználók lehet, hogy nem bíznak benne",
|
||||
"Later": "Később",
|
||||
"Failed to invite the following users to chat: %(csvUsers)s": "Az alábbi felhasználókat nem sikerült meghívni a beszélgetésbe: %(csvUsers)s",
|
||||
"We couldn't create your DM. Please check the users you want to invite and try again.": "A közvetlen üzenetedet nem sikerült elkészíteni. Ellenőrizd azokat a felhasználókat akiket meg szeretnél hívni és próbáld újra.",
|
||||
"Something went wrong trying to invite the users.": "Valami nem sikerült a felhasználók meghívásával.",
|
||||
"We couldn't invite those users. Please check the users you want to invite and try again.": "Ezeket a felhasználókat nem tudtuk meghívni. Ellenőrizd azokat a felhasználókat akiket meg szeretnél hívni és próbáld újra.",
|
||||
"Recently Direct Messaged": "Nemrég küldött Közvetlen Üzenetek",
|
||||
"If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.": "Ha nem találsz valakit, akkor kérdezd meg a felhasználói nevét (pl.: @felhasználó:szerver.com) vagy <a>oszd meg ezt a szobát</a>."
|
||||
}
|
||||
|
|
|
@ -2032,5 +2032,28 @@
|
|||
"Show more": "Mostra altro",
|
||||
"Direct Messages": "Messaggi diretti",
|
||||
"If you can't find someone, ask them for their username, or share your username (%(userId)s) or <a>profile link</a>.": "Se non riesci a trovare qualcuno, chiedigli il nome utente o condividi il tuo (%(userId)s) o il <a>link al profilo</a>.",
|
||||
"Go": "Vai"
|
||||
"Go": "Vai",
|
||||
"a few seconds ago": "pochi secondi fa",
|
||||
"about a minute ago": "circa un minuto fa",
|
||||
"%(num)s minutes ago": "%(num)s minuti fa",
|
||||
"about an hour ago": "circa un'ora fa",
|
||||
"%(num)s hours ago": "%(num)s ore fa",
|
||||
"about a day ago": "circa un giorno fa",
|
||||
"%(num)s days ago": "%(num)s giorni fa",
|
||||
"a few seconds from now": "pochi secondi da adesso",
|
||||
"about a minute from now": "circa un minuto da adesso",
|
||||
"%(num)s minutes from now": "%(num)s minuti da adesso",
|
||||
"about an hour from now": "circa un'ora da adesso",
|
||||
"%(num)s hours from now": "%(num)s ore da adesso",
|
||||
"about a day from now": "circa un giorno da adesso",
|
||||
"%(num)s days from now": "%(num)s giorni da adesso",
|
||||
"Show a presence dot next to DMs in the room list": "Mostra un punto di presenza accanto ai mess. diretti nell'elenco stanza",
|
||||
"Lock": "Lucchetto",
|
||||
"Bootstrap cross-signing and secret storage": "Inizializza firma incrociata e archivio segreto",
|
||||
"Failed to find the following users": "Impossibile trovare i seguenti utenti",
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "I seguenti utenti potrebbero non esistere o non sono validi, perciò non possono essere invitati: %(csvNames)s",
|
||||
"Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.": "Il Backup Chiavi è attivo sul tuo account ma non è stato impostato da questa sessione. Per impostare un archivio segreto, ripristina il tuo backup chiavi.",
|
||||
"Restore": "Ripristina",
|
||||
"Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup": "L'archivio segreto verrà impostato usando i dettagli esistenti del backup chiavi. La password dell'archivio segreto e la chiave di recupero saranno le stesse del backup chiavi",
|
||||
"Restore your Key Backup": "Ripristina il tuo Backup Chiavi"
|
||||
}
|
||||
|
|
|
@ -357,7 +357,7 @@
|
|||
"(could not connect media)": "(メディアを接続できませんでした)",
|
||||
"(no answer)": "(応答なし)",
|
||||
"(unknown failure: %(reason)s)": "(不明なエラー: %(reason)s)",
|
||||
"%(senderName)s ended the call.": "%(senderName)s は通話を終了しました。",
|
||||
"%(senderName)s ended the call.": "%(senderName)s が通話を終了しました。",
|
||||
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s は部屋に加わるよう %(targetDisplayName)s に招待状を送りました。",
|
||||
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s は、部屋のメンバー全員に招待された時点からの部屋履歴を参照できるようにしました。",
|
||||
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s は、部屋のメンバー全員に参加した時点からの部屋履歴を参照できるようにしました。",
|
||||
|
@ -575,8 +575,8 @@
|
|||
"New address (e.g. #foo:%(localDomain)s)": "新しいアドレス (例 #foo:%(localDomain)s)",
|
||||
"Invalid community ID": "無効なコミュニティID",
|
||||
"'%(groupId)s' is not a valid community ID": "'%(groupId)s' は有効なコミュニティIDではありません",
|
||||
"Showing flair for these communities:": "これらのコミュニティの特色を示す:",
|
||||
"This room is not showing flair for any communities": "この部屋はどんなコミュニティに対しても特色を表示していません",
|
||||
"Showing flair for these communities:": "次のコミュニティのバッジを表示:",
|
||||
"This room is not showing flair for any communities": "この部屋はどんなコミュニティに対してもバッジを表示していません",
|
||||
"New community ID (e.g. +foo:%(localDomain)s)": "新しいコミュニティID (例 +foo:%(localDomain)s)",
|
||||
"You have <a>enabled</a> URL previews by default.": "デフォルトでURLプレビューが<a>有効</a>です。",
|
||||
"You have <a>disabled</a> URL previews by default.": "デフォルトでURLプレビューが<a>無効</a>です。",
|
||||
|
@ -638,7 +638,7 @@
|
|||
"Only visible to community members": "コミュニティメンバーにのみ表示されます",
|
||||
"Filter community rooms": "コミュニティルームを絞り込む",
|
||||
"Something went wrong when trying to get your communities.": "コミュニティに参加しようとすると何かがうまくいかなかった。",
|
||||
"Display your community flair in rooms configured to show it.": "それを表示するように構成された部屋にコミュニティの特色を表示します。",
|
||||
"Display your community flair in rooms configured to show it.": "表示するよう設定した部屋であなたのコミュニティ バッジを表示",
|
||||
"Show developer tools": "開発者ツールを表示",
|
||||
"You're not currently a member of any communities.": "あなたは現在、どのコミュニティのメンバーでもありません。",
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie (please see our <PolicyLink>Cookie Policy</PolicyLink>).": "<UsageDataLink>匿名利用データ</UsageDataLink>を送信して、Riot.imの改善を支援してください。 これはCookieを使用します (<PolicyLink>クッキーポリシー</PolicyLink>をご覧ください)>。",
|
||||
|
@ -658,7 +658,7 @@
|
|||
"Popout widget": "ウィジェットをポップアウト",
|
||||
"Unblacklist": "ブラックリスト解除",
|
||||
"Blacklist": "ブラックリスト",
|
||||
"Verify...": "検証中...",
|
||||
"Verify...": "検証する...",
|
||||
"No results": "結果がありません",
|
||||
"Communities": "コミュニティ",
|
||||
"Home": "ホーム",
|
||||
|
@ -713,8 +713,8 @@
|
|||
"%(items)s and %(count)s others|other": "%(items)s と 他 %(count)s 回",
|
||||
"%(items)s and %(count)s others|one": "%(items)s と他の1つ",
|
||||
"%(items)s and %(lastItem)s": "%(items)s と %(lastItem)s",
|
||||
"collapse": "崩壊",
|
||||
"expand": "拡大する",
|
||||
"collapse": "折りたたむ",
|
||||
"expand": "展開",
|
||||
"Custom level": "カスタムレベル",
|
||||
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "返信されたイベントを読み込めません。存在しないか、表示する権限がありません。",
|
||||
"<a>In reply to</a> <pill>": "<a>返信</a> <pill>",
|
||||
|
@ -744,7 +744,7 @@
|
|||
"This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "これにより、あなたのアカウントは永久に使用できなくなります。ログインできなくなり、誰も同じユーザーIDを再登録できなくなります。これにより、参加しているすべてのルームから退室し、 IDサーバからあなたのアカウントの詳細が削除されます。<b>この操作は元に戻すことができません。</b>",
|
||||
"Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "アカウントを無効にしても、<b>送信されたメッセージはデフォルトではなくなりません。</b>メッセージを忘れてしまった場合は、下のボックスにチェックを入れてください。",
|
||||
"Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Matrixのメッセージの可視性は電子メールと似ています。メッセージを忘れると、新規または未登録のユーザーと共有することができませんが、既にこれらのメッセージにアクセスしている登録ユーザーは、依然としてそのコピーにアクセスできます。",
|
||||
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "アカウントを無効にしたときに送信したすべてのメッセージを忘れてください (<b>警告:</b>これにより、今後のユーザーは会話履歴の全文を見ることができなくなります)",
|
||||
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "アカウントを無効する際、送信したすべてのメッセージを削除(<b>警告:</b>これにより、今後のユーザーは会話履歴の全文を見ることができなくなります)",
|
||||
"To continue, please enter your password:": "続行するには、パスワードを入力してください:",
|
||||
"To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "この端末が信頼できることを確認するには、他の方法 (個人や電話など) で所有者に連絡し、端末のユーザー設定で表示される鍵が以下のキーと一致するかどうかを尋ねてください:",
|
||||
"Device name": "端末名",
|
||||
|
@ -947,9 +947,9 @@
|
|||
"click to reveal": "クリックすると表示されます",
|
||||
"Homeserver is": "ホームサーバーは",
|
||||
"Identity Server is": "アイデンティティ・サーバー",
|
||||
"matrix-react-sdk version:": "matrix-react-sdk version:",
|
||||
"riot-web version:": "riot-web version:",
|
||||
"olm version:": "olm version:",
|
||||
"matrix-react-sdk version:": "matrix-react-sdk のバージョン:",
|
||||
"riot-web version:": "riot-web のバージョン:",
|
||||
"olm version:": "olm のバージョン:",
|
||||
"Failed to send email": "メールを送信できませんでした",
|
||||
"The email address linked to your account must be entered.": "あなたのアカウントにリンクされているメールアドレスを入力する必要があります。",
|
||||
"A new password must be entered.": "新しいパスワードを入力する必要があります。",
|
||||
|
@ -1017,7 +1017,7 @@
|
|||
"bulleted-list": "bulleted-list",
|
||||
"numbered-list": "numbered-list",
|
||||
"People": "人々",
|
||||
"Flair": "特色",
|
||||
"Flair": "バッジ",
|
||||
"Fill screen": "フィルスクリーン",
|
||||
"Light theme": "明るいテーマ",
|
||||
"Dark theme": "暗いテーマ",
|
||||
|
@ -1124,7 +1124,7 @@
|
|||
"Enable Community Filter Panel": "コミュニティーフィルターパネルを有効にする",
|
||||
"Show recently visited rooms above the room list": "最近訪問した部屋をリストの上位に表示する",
|
||||
"Low bandwidth mode": "低帯域通信モード",
|
||||
"Public Name": "パブリック名",
|
||||
"Public Name": "公開名",
|
||||
"Upload profile picture": "プロフィール画像をアップロード",
|
||||
"<a>Upgrade</a> to your own domain": "あなた自身のドメインに<a>アップグレード</a>",
|
||||
"Phone numbers": "電話番号",
|
||||
|
@ -1134,7 +1134,7 @@
|
|||
"General": "一般",
|
||||
"Preferences": "環境設定",
|
||||
"Security & Privacy": "セキュリティとプライバシー",
|
||||
"A device's public name is visible to people you communicate with": "デバイスのパブリック名はあなたと会話するすべての人が閲覧できます",
|
||||
"A device's public name is visible to people you communicate with": "デバイスの公開名はあなたと会話するすべての人が閲覧できます",
|
||||
"Room information": "部屋の情報",
|
||||
"Internal room ID:": "内部 部屋ID:",
|
||||
"Room version": "部屋のバージョン",
|
||||
|
@ -1195,5 +1195,64 @@
|
|||
"Other servers": "他のサーバー",
|
||||
"Sign in to your Matrix account on %(serverName)s": "%(serverName)s上のMatrixアカウントにサインインします",
|
||||
"Sign in to your Matrix account on <underlinedServerName />": "<underlinedServerName />上のMatrixアカウントにサインインします",
|
||||
"Create account": "アカウントを作成"
|
||||
"Create account": "アカウントを作成",
|
||||
"Error upgrading room": "部屋のアップグレード中にエラーが発生しました",
|
||||
"%(senderName)s placed a voice call.": "%(senderName)s が音声通話を行いました。",
|
||||
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s が音声通話を行いました。 (このブラウザではサポートされていません)",
|
||||
"%(senderName)s placed a video call.": "%(senderName)s がビデオ通話を行いました。",
|
||||
"%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s がビデオ通話を行いました。 (このブラウザではサポートされていません)",
|
||||
"Match system theme": "システムテーマに合わせる",
|
||||
"Allow Peer-to-Peer for 1:1 calls": "1対1通話でP2P(ピアツーピア)を許可する",
|
||||
"Delete Backup": "バックアップを削除",
|
||||
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "暗号化されたメッセージは、エンドツーエンドの暗号化によって保護されています。これらの暗号化されたメッセージを読むための鍵を持っているのは、あなたと参加者だけです。",
|
||||
"Restore from Backup": "バックアップから復元",
|
||||
"This device is backing up your keys. ": "このデバイスは鍵をバックアップしています。 ",
|
||||
"Enable desktop notifications for this device": "このデバイスでデスクトップ通知を有効にする",
|
||||
"Credits": "クレジット",
|
||||
"For help with using Riot, click <a>here</a>.": "Riotの使用方法に関するヘルプは<a>こちら</a>をご覧ください。",
|
||||
"Help & About": "ヘルプと概要",
|
||||
"Bug reporting": "バグの報告",
|
||||
"FAQ": "よくある質問",
|
||||
"Versions": "バージョン",
|
||||
"Key backup": "キーのバックアップ",
|
||||
"Voice & Video": "音声とビデオ",
|
||||
"Remove recent messages": "最近のメッセージを削除する",
|
||||
"%(creator)s created and configured the room.": "%(creator)s が部屋を作成して構成しました。",
|
||||
"Add room": "部屋を追加",
|
||||
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)sがこの部屋に%(groups)sのバッジを追加しました。",
|
||||
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)sがこの部屋から%(groups)sのバッジを削除しました。",
|
||||
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)sが%(newGroups)sのバッジを追加し、%(oldGroups)sのバッジを削除しました。",
|
||||
"Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "キーが正常にバックアップされていない場合、暗号化されたメッセージにアクセスできなくなります。本当によろしいですか?",
|
||||
"not stored": "保存されていません",
|
||||
"All keys backed up": "すべてのキーがバックアップされました",
|
||||
"Backup version: ": "バックアップのバージョン: ",
|
||||
"Algorithm: ": "アルゴリズム: ",
|
||||
"Backup key stored: ": "バックアップキーの保存: ",
|
||||
"Back up your keys before signing out to avoid losing them.": "暗号化キーを失くさないために、サインアウトする前にキーをバックアップしてください。",
|
||||
"Start using Key Backup": "キーのバックアップをはじめる",
|
||||
"Error updating flair": "バッジの更新でエラーが発生しました。",
|
||||
"There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "この部屋のバッジの更新でエラーが発生しました。サーバーが許可していないか、一時的なエラーが発生しました。",
|
||||
"Edited at %(date)s. Click to view edits.": "%(date)sに編集。クリックして編集を表示。",
|
||||
"edited": "編集済",
|
||||
"I don't want my encrypted messages": "暗号化されたメッセージは必要ありません",
|
||||
"Manually export keys": "手動でキーをエクスポート",
|
||||
"You'll lose access to your encrypted messages": "暗号化されたメッセージにアクセスできなくなります",
|
||||
"You'll upgrade this room from <oldVersion /> to <newVersion />.": "このルームを<oldVersion />から<newVersion />にアップグレードします。",
|
||||
"<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>警告</b>: 信頼できるコンピュータからのみキーのバックアップをセットアップしてください。",
|
||||
"For maximum security, this should be different from your account password.": "セキュリティの効果を高めるために、アカウントのパスワードと別のものを設定するべきです。",
|
||||
"Enter a passphrase...": "パスワードを入力...",
|
||||
"That matches!": "同じです!",
|
||||
"Please enter your passphrase a second time to confirm.": "確認のために、パスワードをもう一度入力してください。",
|
||||
"Repeat your passphrase...": "パスワードを再度入力...",
|
||||
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.": "リカバリキーは安全網です - パスワードを忘れた際は、リカバリキーを使って復元できます。",
|
||||
"Copy to clipboard": "クリップボードにコピー",
|
||||
"Download": "ダウンロード",
|
||||
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:": "リカバリキーが<b>クリップボードにコピーされました</b>。ペーストして:",
|
||||
"<b>Print it</b> and store it somewhere safe": "<b>印刷して</b>安全な場所に保管しましょう",
|
||||
"<b>Save it</b> on a USB key or backup drive": "USB メモリーやバックアップ用のドライブに<b>保存</b>しましょう",
|
||||
"<b>Copy it</b> to your personal cloud storage": "個人のクラウドストレージに<b>コピー</b>しましょう",
|
||||
"Confirm your passphrase": "パスワードを確認",
|
||||
"Recovery key": "リカバリキー",
|
||||
"We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "キーの暗号化されたコピーがサーバーに保存されます。バックアップを保護するために、パスワードを設定してください。",
|
||||
"Secure your backup with a passphrase": "バックアップをパスワードで保護"
|
||||
}
|
||||
|
|
|
@ -1440,7 +1440,7 @@
|
|||
"You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "Ранее вы использовали Riot на %(host)s с отложенной загрузкой участников. В этой версии отложенная загрузка отключена. Поскольку локальный кеш не совместим между этими двумя настройками, Riot необходимо повторно синхронизировать вашу учётную запись.",
|
||||
"If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Если другая версия Riot все еще открыта на другой вкладке, закройте ее, так как использование Riot на том же хосте с включенной и отключенной ленивой загрузкой одновременно вызовет проблемы.",
|
||||
"Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot теперь использует в 3-5 раз меньше памяти, загружая информацию только о других пользователях, когда это необходимо. Пожалуйста, подождите, пока мы ресинхронизируемся с сервером!",
|
||||
"I don't want my encrypted messages": "Мне не нужны мои зашифрованные сообщения",
|
||||
"I don't want my encrypted messages": "Продолжить выход, мне не нужны мои зашифрованные сообщения",
|
||||
"Manually export keys": "Ручной экспорт ключей",
|
||||
"If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "Если вы столкнулись с какими-либо ошибками или хотите оставить отзыв, которым хотите поделиться, сообщите нам об этом на GitHub.",
|
||||
"To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "Чтобы избежать повторяющихся проблем, сначала <existingIssuesLink> просмотрите существующие проблемы </existingIssuesLink> (и добавьте +1), либо <newIssueLink> создайте новую проблему </newIssueLink>, если вы не можете ее найти.",
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"Add": "Ekle",
|
||||
"Add a topic": "Bir konu(topic) ekle",
|
||||
"Admin": "Admin",
|
||||
"Admin Tools": "Admin araçları",
|
||||
"Admin Tools": "Admin Araçları",
|
||||
"No Microphones detected": "Hiçbir Mikrofon bulunamadı",
|
||||
"No Webcams detected": "Hiçbir Web kamerası bulunamadı",
|
||||
"No media permissions": "Medya izinleri yok",
|
||||
|
@ -344,7 +344,7 @@
|
|||
"You cannot place VoIP calls in this browser.": "Bu tarayıcıda VoIP çağrısı yapamazsınız.",
|
||||
"You do not have permission to post to this room": "Bu odaya göndermeye izniniz yok",
|
||||
"You have <a>disabled</a> URL previews by default.": "URL önizlemelerini varsayılan olarak <a> devre dışı </a> bıraktınız.",
|
||||
"You have <a>enabled</a> URL previews by default.": "URL önizlemelerini varsayılan olarak <a> etkinleştirdiniz </a> .",
|
||||
"You have <a>enabled</a> URL previews by default.": "URL önizlemelerini varsayılan olarak <a>etkinleştirdiniz</a>.",
|
||||
"You have no visible notifications": "Hiçbir görünür bildiriminiz yok",
|
||||
"You must <a>register</a> to use this functionality": "Bu işlevi kullanmak için <a> Kayıt Olun </a>",
|
||||
"You need to be able to invite users to do that.": "Bunu yapmak için kullanıcıları davet etmeye ihtiyacınız var.",
|
||||
|
@ -1297,5 +1297,130 @@
|
|||
"Copied!": "Kopyalandı!",
|
||||
"Failed to copy": "Kopyalama başarısız",
|
||||
"edited": "düzenlendi",
|
||||
"Message removed by %(userId)s": "Mesaj %(userId)s tarafından silindi"
|
||||
"Message removed by %(userId)s": "Mesaj %(userId)s tarafından silindi",
|
||||
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "Kimlik sunucusu <idserver /> üzerinde hala <b>kişisel veri paylaşımı</b> yapıyorsunuz.",
|
||||
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Kimlik sunucusundan bağlantıyı kesmeden önce telefon numaranızı ve e-posta adreslerinizi silmenizi tavsiye ederiz.",
|
||||
"Set a new account password...": "Yeni bir hesap şifresi belirle...",
|
||||
"Deactivating your account is a permanent action - be careful!": "Hesabınızı pasifleştirmek bir kalıcı eylemdir - dikkat edin!",
|
||||
"Deactivate account": "Hesabı pasifleştir",
|
||||
"For help with using Riot, click <a>here</a>.": "Riot kullanarak yardım etmek için, <a>buraya</a> tıklayın.",
|
||||
"Chat with Riot Bot": "Riot Bot ile Sohbet Et",
|
||||
"Submit debug logs": "Hata ayıklama kayıtlarını gönder",
|
||||
"Something went wrong. Please try again or view your console for hints.": "Bir şeyler hatalı gitti. Lütfen yeniden deneyin veya ipuçları için konsolunuza bakın.",
|
||||
"Please verify the room ID or alias and try again.": "Lütfen odanın ID si veya lakabı doğrulayın ve yeniden deneyin.",
|
||||
"Please try again or view your console for hints.": "Lütfen yeniden deneyin veya ipuçları için konsolunuza bakın.",
|
||||
"None": "Yok",
|
||||
"Ban list rules - %(roomName)s": "Yasak Liste Kuralları - %(roomName)s",
|
||||
"You have not ignored anyone.": "Kimseyi yok saymamışsınız.",
|
||||
"You are currently ignoring:": "Halihazırda yoksaydıklarınız:",
|
||||
"Unsubscribe": "Abonelikten Çık",
|
||||
"You are currently subscribed to:": "Halizhazırdaki abonelikleriniz:",
|
||||
"Ignored users": "Yoksayılan kullanıcılar",
|
||||
"Personal ban list": "Kişisel yasak listesi",
|
||||
"Server or user ID to ignore": "Yoksaymak için sunucu veya kullanıcı ID",
|
||||
"eg: @bot:* or example.org": "örn: @bot:* veya example.org",
|
||||
"Ignore": "Yoksay",
|
||||
"Subscribed lists": "Abone olunmuş listeler",
|
||||
"If this isn't what you want, please use a different tool to ignore users.": "Eğer istediğiniz bu değilse, kullanıcıları yoksaymak için lütfen farklı bir araç kullanın.",
|
||||
"Room ID or alias of ban list": "Yasak listesinin Oda ID veya lakabı",
|
||||
"Subscribe": "Abone ol",
|
||||
"Always show the window menu bar": "Pencerenin menü çubuğunu her zaman göster",
|
||||
"Bulk options": "Toplu işlem seçenekleri",
|
||||
"Accept all %(invitedRooms)s invites": "Bütün %(invitedRooms)s davetlerini kabul et",
|
||||
"Request media permissions": "Medya izinleri talebi",
|
||||
"Upgrade this room to the recommended room version": "Bu odayı önerilen oda sürümüne yükselt",
|
||||
"View older messages in %(roomName)s.": "%(roomName)s odasında daha eski mesajları göster.",
|
||||
"This bridge is managed by <user />.": "Bu köprü <user /> tarafından yönetiliyor.",
|
||||
"Connected via %(protocolName)s": "%(protocolName)s yoluyla bağlandı",
|
||||
"Bridge Info": "Köprü Bilgisi",
|
||||
"Set a new custom sound": "Özel bir ses ayarla",
|
||||
"Change main address for the room": "Oda için ana adresi değiştir",
|
||||
"Error changing power level requirement": "Güç düzey gereksinimi değiştirmede hata",
|
||||
"Error changing power level": "Güç düzeyi değiştirme hatası",
|
||||
"Send %(eventType)s events": "%(eventType)s olaylarını gönder",
|
||||
"To link to this room, please add an alias.": "Bu odaya bağlanmak için, lütfen bir lakap ekle.",
|
||||
"This user has not verified all of their devices.": "Bu kullanıcı tüm cihazlarda doğrulanmadı.",
|
||||
"You have verified this user. This user has verified all of their devices.": "Bu kullanıcıyı doğruladınız. Bu kullanıcı tüm cihazlarında doğrulandı.",
|
||||
"This event could not be displayed": "Bu olay görüntülenemedi",
|
||||
"Demote yourself?": "Kendinin rütbeni düşür?",
|
||||
"Demote": "Rütbe Düşür",
|
||||
"Mention": "Bahsetme",
|
||||
"Remove recent messages": "Son mesajları sil",
|
||||
"The conversation continues here.": "Sohbet buradan devam ediyor.",
|
||||
"You can only join it with a working invite.": "Sadece çalışan bir davet ile katılınabilir.",
|
||||
"Try to join anyway": "Katılmak için yinede deneyin",
|
||||
"Preferences": "Tercihler",
|
||||
"Timeline": "Zaman Çizelgesi",
|
||||
"You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "%(user)s tarafından gönderilen 1 mesajı silmek üzeresiniz. Bu işlem geri alınamaz. Devam etmek istediğinize emin misiniz?",
|
||||
"This room has been replaced and is no longer active.": "Bu oda değiştirildi ve artık aktif değil.",
|
||||
"Idle for %(duration)s": "%(duration)s süresince boşta",
|
||||
"You were kicked from %(roomName)s by %(memberName)s": "%(memberName)s tarafından %(roomName)s dan kovuldunuz",
|
||||
"You were banned from %(roomName)s by %(memberName)s": "%(memberName)s tarafından %(roomName)s odası size yasaklandı",
|
||||
"Something went wrong with your invite to %(roomName)s": "%(roomName)s odasına davet işleminizde birşeyler yanlış gitti",
|
||||
"This invite to %(roomName)s was sent to %(email)s": "%(roomName)s odası daveti %(email)s adresine gönderildi",
|
||||
"You're previewing %(roomName)s. Want to join it?": "%(roomName)s odasını inceliyorsunuz. Katılmak ister misiniz?",
|
||||
"You don't currently have any stickerpacks enabled": "Açılmış herhangi bir çıkartma paketine sahip değilsiniz",
|
||||
"Error creating alias": "Lakap oluştururken hata",
|
||||
"Room Topic": "Oda Başlığı",
|
||||
"Verify this session to grant it access to encrypted messages.": "Şifrelenmiş mesajlara erişmek için bu oturumu doğrula.",
|
||||
"Start": "Başlat",
|
||||
"Session verified": "Oturum doğrulandı",
|
||||
"Done": "Bitti",
|
||||
"Go Back": "Geri dön",
|
||||
"New Session": "Yeni Oturum",
|
||||
"Gets or sets the room topic": "Oda başlığını getirir yada ayarlar",
|
||||
"Unbans user with given ID": "Verilen ID ile kullanıcı yasağını kaldırır",
|
||||
"Ignores a user, hiding their messages from you": "Mesajlarını senden gizleyerek, bir kullanıcıyı yok sayar",
|
||||
"Ignored user": "Yoksayılan kullanıcı",
|
||||
"You are now ignoring %(userId)s": "Şimdi %(userId)s yı yoksayıyorsunuz",
|
||||
"Stops ignoring a user, showing their messages going forward": "Sonraki mesajlarını göstererek, bir kullanıcıyı yoksaymaktan vazgeç",
|
||||
"Adds a custom widget by URL to the room": "URL ile odaya özel bir görsel bileşen ekle",
|
||||
"Verifies a user, device, and pubkey tuple": "Bir kullanıcıyı, cihazı ve açık anahtar ikilisini doğrular",
|
||||
"%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s misafirlerin odaya katılmasına izin verdi.",
|
||||
"%(senderName)s updated an invalid ban rule": "%(senderName)s bir geçersiz yasaklama kuralını güncelledi",
|
||||
"%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s, %(reason)s nedeniyle %(glob)s ile eşleşen yasaklama kuralını güncelledi",
|
||||
"%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s, %(reason)s nedeniyle %(glob)s ile eşleşen kullanıcıları yasaklama kuralı oluşturdu",
|
||||
"%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s, %(reason)s nedeniyle %(glob)s ile eşleşen bir oda yasaklama kuralı oluşturdu",
|
||||
"%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s, %(reason)s nedeniyle %(glob)s ile eşleşen bir sunucular yasaklama kuralı oluşturdu",
|
||||
"%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s, %(reason)s nedeniyle %(glob)s ile eşleşen bir yasak kuralı oluşturdu",
|
||||
"Ensure you have a stable internet connection, or get in touch with the server admin": "Kararlı bir internet bağlantısına sahip olduğunuzdan emin olun yada sunucu yöneticisi ile iletişime geçin",
|
||||
"Please <a>contact your service administrator</a> to continue using the service.": "Hizmeti kullanmaya devam etmek için, lütfen <a>hizmet yöneticiniz ile bağlantıya geçin</a>.",
|
||||
"a few seconds ago": "bir kaç saniye önce",
|
||||
"about a minute ago": "yaklaşık bir dakika önce",
|
||||
"%(num)s minutes ago": "%(num)s dakika önce",
|
||||
"about an hour ago": "yaklaşık bir saat önce",
|
||||
"%(num)s hours ago": "%(num)s saat önce",
|
||||
"about a day ago": "yaklaşık bir gün önce",
|
||||
"%(num)s days ago": "%(num)s gün önce",
|
||||
"The user's homeserver does not support the version of the room.": "Kullanıcının ana sunucusu odanın sürümünü desteklemiyor.",
|
||||
"Unknown server error": "Bilinmeyen sunucu hatası",
|
||||
"Use a few words, avoid common phrases": "Bir kaç kelime kullanın ve genel ifadelerden kaçının",
|
||||
"No need for symbols, digits, or uppercase letters": "Semboller, sayılar yada büyük harflere gerek yok",
|
||||
"Avoid repeated words and characters": "Tekrarlanan kelimeler ve karakterlerden kaçının",
|
||||
"Avoid sequences": "Sekanslardan kaçının",
|
||||
"Avoid recent years": "Son yıllardan kaçının",
|
||||
"Avoid years that are associated with you": "Sizle ilişkili yıllardan kaçının",
|
||||
"Avoid dates and years that are associated with you": "Sizle ilişkili tarihler ve yıllardan kaçının",
|
||||
"Capitalization doesn't help very much": "Baş harfi büyük yapmak size pek yardımcı olmaz",
|
||||
"All-uppercase is almost as easy to guess as all-lowercase": "Bütün harflerin büyük olmasıyla tümünün küçük olması tahmin için hemen hemen aynı kolaylıktadır",
|
||||
"Reversed words aren't much harder to guess": "Ters kelimeler tahmin için çok zor değil",
|
||||
"Repeats like \"aaa\" are easy to guess": "“aaa” gibi tekrarlar tahmin için oldukça kolay",
|
||||
"Dates are often easy to guess": "Tarihler sıklıkla tahmin için daha kolaydır",
|
||||
"This is a top-10 common password": "Bu bir top-10 yaygın parola",
|
||||
"This is a top-100 common password": "Bu bir top-100 yaygın parola",
|
||||
"This is a very common password": "Bu oldukça yaygın parola",
|
||||
"This is similar to a commonly used password": "Bu yaygınca kullanılan bir parolaya benziyor",
|
||||
"Names and surnames by themselves are easy to guess": "Adlar ve soyadlar kendi kendilerine tahmin için kolaydır",
|
||||
"There was an error joining the room": "Odaya katılırken bir hata oluştu",
|
||||
"Custom user status messages": "Özel kullanıcı durum mesajları",
|
||||
"Group & filter rooms by custom tags (refresh to apply changes)": "Özel etiketler ile odaları grupla & filtrele ( değişiklikleri uygulamak için yenile)",
|
||||
"Render simple counters in room header": "Oda başlığında basit sayaçları görüntüle",
|
||||
"Try out new ways to ignore people (experimental)": "Kişileri yoksaymak için yeni yöntemleri dene (deneysel)",
|
||||
"New invite dialog": "Yeni davet diyalogu",
|
||||
"Enable local event indexing and E2EE search (requires restart)": "E2EE arama ve yerel olay indeksini aç (yeniden başlatma gerekli)",
|
||||
"Mirror local video feed": "Yerel video beslemesi yansısı",
|
||||
"Enable Community Filter Panel": "Toluluk Filtre Panelini Aç",
|
||||
"Match system theme": "Sistem temasıyla eşle",
|
||||
"Allow Peer-to-Peer for 1:1 calls": "1:1 çağrılar için eşten-eşe izin ver",
|
||||
"Missing media permissions, click the button below to request.": "Medya izinleri eksik, alttaki butona tıkayarak talep edin."
|
||||
}
|
||||
|
|
|
@ -2036,5 +2036,44 @@
|
|||
"Below is a list of bridges connected to this room.": "以下是連線到此聊天室的橋接列表。",
|
||||
"Suggestions": "建議",
|
||||
"Failed to find the following users": "找不到以下使用者",
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "以下使用者可能不存在或無效,且無法被邀請:%(csvNames)s"
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "以下使用者可能不存在或無效,且無法被邀請:%(csvNames)s",
|
||||
"Show a presence dot next to DMs in the room list": "在聊天室清單中的直接訊息旁顯示上線狀態點",
|
||||
"Lock": "鎖定",
|
||||
"Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.": "您的帳號已啟用金鑰備份,但並在此工作階段中設定。要設定秘密儲存空間,請還原金鑰備份。",
|
||||
"Restore": "還原",
|
||||
"Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup": "秘密儲存空間將會使用您既有的金鑰備份詳細資訊設定。您的秘密儲存空間通關密語與復原金鑰會與您的金鑰備份相同",
|
||||
"Restore your Key Backup": "復原您的金鑰備份",
|
||||
"a few seconds ago": "數秒前",
|
||||
"about a minute ago": "大約一分鐘前",
|
||||
"%(num)s minutes ago": "%(num)s 分鐘前",
|
||||
"about an hour ago": "大約一小時前",
|
||||
"%(num)s hours ago": "%(num)s 小時前",
|
||||
"about a day ago": "大約一天前",
|
||||
"%(num)s days ago": "%(num)s 天前",
|
||||
"a few seconds from now": "從現在開始數秒鐘",
|
||||
"about a minute from now": "從現在開始大約一分鐘",
|
||||
"%(num)s minutes from now": "從現在開始 %(num)s 分鐘",
|
||||
"about an hour from now": "從現在開始大約一小時",
|
||||
"%(num)s hours from now": "從現在開始 %(num)s 小時",
|
||||
"about a day from now": "從現在開始大約一天",
|
||||
"%(num)s days from now": "從現在開始 %(num)s 天",
|
||||
"Failed to invite the following users to chat: %(csvUsers)s": "邀請使用者加入聊天失敗:%(csvUsers)s",
|
||||
"We couldn't create your DM. Please check the users you want to invite and try again.": "我們無法建立您的直接對話。請檢查您想要邀請的使用者並再試一次。",
|
||||
"Complete security": "完全安全",
|
||||
"Verify this session to grant it access to encrypted messages.": "驗證此工作階段以取得對已加密訊息的存取權限。",
|
||||
"Start": "開始",
|
||||
"Session verified": "工作階段已驗證",
|
||||
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "您的新工作階段已驗證。其對您的已加密訊息有存取權,其他使用者也將會看到其受信任。",
|
||||
"Done": "完成",
|
||||
"Without completing security on this device, it won’t have access to encrypted messages.": "此裝置上沒有完全安全性,其對已加密訊息沒有存取權。",
|
||||
"Go Back": "返回",
|
||||
"Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup.": "秘密儲存空間將會使用您既有的金鑰備份詳細資訊設定。您的秘密儲存空間通關密語與復原金鑰將會與您的金鑰備份相同。",
|
||||
"New Session": "新工作階段",
|
||||
"New invite dialog": "新邀請對話框",
|
||||
"Other users may not trust it": "其他使用者可能不會信任它",
|
||||
"Later": "稍後",
|
||||
"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.": "我們無法邀請那些使用者。請檢查您想要邀請的使用者並再試一次。",
|
||||
"Recently Direct Messaged": "最近傳送過直接訊息",
|
||||
"If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.": "如果您找不到某人,請詢問他們的使用者名稱(範例:@user:server.com)或<a>分享此聊天室</a>。"
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -153,7 +153,6 @@ export const SETTINGS = {
|
|||
displayName: _td("Enable cross-signing to verify per-user instead of per-device (in development)"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
controller: new ReloadOnChangeController(),
|
||||
},
|
||||
"feature_event_indexing": {
|
||||
isFeature: true,
|
||||
|
|
|
@ -123,7 +123,11 @@ export default class RightPanelStore extends Store {
|
|||
|
||||
if (payload.action === 'view_room' || payload.action === 'view_group') {
|
||||
// Reset to the member list if we're viewing member info
|
||||
const memberInfoPhases = [RIGHT_PANEL_PHASES.RoomMemberInfo, RIGHT_PANEL_PHASES.Room3pidMemberInfo];
|
||||
const memberInfoPhases = [
|
||||
RIGHT_PANEL_PHASES.RoomMemberInfo,
|
||||
RIGHT_PANEL_PHASES.Room3pidMemberInfo,
|
||||
RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
];
|
||||
if (memberInfoPhases.includes(this._state.lastRoomPhase)) {
|
||||
this._setState({lastRoomPhase: RIGHT_PANEL_PHASES.RoomMemberList, lastRoomPhaseParams: {}});
|
||||
}
|
||||
|
|
|
@ -21,8 +21,9 @@ export const RIGHT_PANEL_PHASES = Object.freeze({
|
|||
FilePanel: 'FilePanel',
|
||||
NotificationPanel: 'NotificationPanel',
|
||||
RoomMemberInfo: 'RoomMemberInfo',
|
||||
Room3pidMemberInfo: 'Room3pidMemberInfo',
|
||||
EncryptionPanel: 'EncryptionPanel',
|
||||
|
||||
Room3pidMemberInfo: 'Room3pidMemberInfo',
|
||||
// Group stuff
|
||||
GroupMemberList: 'GroupMemberList',
|
||||
GroupRoomList: 'GroupRoomList',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,8 +17,6 @@ limitations under the License.
|
|||
|
||||
// Pull in the encryption lib so that we can decrypt attachments.
|
||||
import encrypt from 'browser-encrypt-attachment';
|
||||
// Pull in a fetch polyfill so we can download encrypted attachments.
|
||||
import 'isomorphic-fetch';
|
||||
// Grab the client so that we can turn mxc:// URLs into https:// URLS.
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
|
||||
|
|
|
@ -17,153 +17,6 @@ limitations under the License.
|
|||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import { _t } from '../languageHandler';
|
||||
|
||||
const SUB_EVENT_TYPES_OF_INTEREST = ["start", "cancel", "done"];
|
||||
|
||||
export default class KeyVerificationStateObserver {
|
||||
constructor(requestEvent, client, updateCallback) {
|
||||
this._requestEvent = requestEvent;
|
||||
this._client = client;
|
||||
this._updateCallback = updateCallback;
|
||||
this.accepted = false;
|
||||
this.done = false;
|
||||
this.cancelled = false;
|
||||
this._updateVerificationState();
|
||||
}
|
||||
|
||||
get concluded() {
|
||||
return this.accepted || this.done || this.cancelled;
|
||||
}
|
||||
|
||||
get pending() {
|
||||
return !this.concluded;
|
||||
}
|
||||
|
||||
setCallback(callback) {
|
||||
this._updateCallback = callback;
|
||||
}
|
||||
|
||||
attach() {
|
||||
this._requestEvent.on("Event.relationsCreated", this._onRelationsCreated);
|
||||
for (const phaseName of SUB_EVENT_TYPES_OF_INTEREST) {
|
||||
this._tryListenOnRelationsForType(`m.key.verification.${phaseName}`);
|
||||
}
|
||||
}
|
||||
|
||||
detach() {
|
||||
const roomId = this._requestEvent.getRoomId();
|
||||
const room = this._client.getRoom(roomId);
|
||||
|
||||
for (const phaseName of SUB_EVENT_TYPES_OF_INTEREST) {
|
||||
const relations = room.getUnfilteredTimelineSet()
|
||||
.getRelationsForEvent(this._requestEvent.getId(), "m.reference", `m.key.verification.${phaseName}`);
|
||||
if (relations) {
|
||||
relations.removeListener("Relations.add", this._onRelationsUpdated);
|
||||
relations.removeListener("Relations.remove", this._onRelationsUpdated);
|
||||
relations.removeListener("Relations.redaction", this._onRelationsUpdated);
|
||||
}
|
||||
}
|
||||
this._requestEvent.removeListener("Event.relationsCreated", this._onRelationsCreated);
|
||||
}
|
||||
|
||||
_onRelationsCreated = (relationType, eventType) => {
|
||||
if (relationType !== "m.reference") {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
eventType !== "m.key.verification.start" &&
|
||||
eventType !== "m.key.verification.cancel" &&
|
||||
eventType !== "m.key.verification.done"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._tryListenOnRelationsForType(eventType);
|
||||
this._updateVerificationState();
|
||||
this._updateCallback();
|
||||
};
|
||||
|
||||
_tryListenOnRelationsForType(eventType) {
|
||||
const roomId = this._requestEvent.getRoomId();
|
||||
const room = this._client.getRoom(roomId);
|
||||
const relations = room.getUnfilteredTimelineSet()
|
||||
.getRelationsForEvent(this._requestEvent.getId(), "m.reference", eventType);
|
||||
if (relations) {
|
||||
relations.on("Relations.add", this._onRelationsUpdated);
|
||||
relations.on("Relations.remove", this._onRelationsUpdated);
|
||||
relations.on("Relations.redaction", this._onRelationsUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
_onRelationsUpdated = (event) => {
|
||||
this._updateVerificationState();
|
||||
this._updateCallback && this._updateCallback();
|
||||
};
|
||||
|
||||
_updateVerificationState() {
|
||||
const roomId = this._requestEvent.getRoomId();
|
||||
const room = this._client.getRoom(roomId);
|
||||
const timelineSet = room.getUnfilteredTimelineSet();
|
||||
const fromUserId = this._requestEvent.getSender();
|
||||
const content = this._requestEvent.getContent();
|
||||
const toUserId = content.to;
|
||||
|
||||
this.cancelled = false;
|
||||
this.done = false;
|
||||
this.accepted = false;
|
||||
this.otherPartyUserId = null;
|
||||
this.cancelPartyUserId = null;
|
||||
|
||||
const startRelations = timelineSet.getRelationsForEvent(
|
||||
this._requestEvent.getId(), "m.reference", "m.key.verification.start");
|
||||
if (startRelations) {
|
||||
for (const startEvent of startRelations.getRelations()) {
|
||||
if (startEvent.getSender() === toUserId) {
|
||||
this.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const doneRelations = timelineSet.getRelationsForEvent(
|
||||
this._requestEvent.getId(), "m.reference", "m.key.verification.done");
|
||||
if (doneRelations) {
|
||||
let senderDone = false;
|
||||
let receiverDone = false;
|
||||
for (const doneEvent of doneRelations.getRelations()) {
|
||||
if (doneEvent.getSender() === toUserId) {
|
||||
receiverDone = true;
|
||||
} else if (doneEvent.getSender() === fromUserId) {
|
||||
senderDone = true;
|
||||
}
|
||||
}
|
||||
if (senderDone && receiverDone) {
|
||||
this.done = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.done) {
|
||||
const cancelRelations = timelineSet.getRelationsForEvent(
|
||||
this._requestEvent.getId(), "m.reference", "m.key.verification.cancel");
|
||||
|
||||
if (cancelRelations) {
|
||||
let earliestCancelEvent;
|
||||
for (const cancelEvent of cancelRelations.getRelations()) {
|
||||
// only accept cancellation from the users involved
|
||||
if (cancelEvent.getSender() === toUserId || cancelEvent.getSender() === fromUserId) {
|
||||
this.cancelled = true;
|
||||
if (!earliestCancelEvent || cancelEvent.getTs() < earliestCancelEvent.getTs()) {
|
||||
earliestCancelEvent = cancelEvent;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (earliestCancelEvent) {
|
||||
this.cancelPartyUserId = earliestCancelEvent.getSender();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.otherPartyUserId = fromUserId === this._client.getUserId() ? toUserId : fromUserId;
|
||||
}
|
||||
}
|
||||
|
||||
export function getNameForEventRoom(userId, mxEvent) {
|
||||
const roomId = mxEvent.getRoomId();
|
||||
const client = MatrixClientPeg.get();
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import lolex from 'lolex';
|
||||
import jest from 'jest-mock';
|
||||
import EventEmitter from 'events';
|
||||
import UserActivity from '../src/UserActivity';
|
||||
|
||||
|
@ -36,8 +35,8 @@ describe('UserActivity', function() {
|
|||
let clock;
|
||||
|
||||
beforeEach(function() {
|
||||
fakeWindow = new FakeDomEventEmitter(),
|
||||
fakeDocument = new FakeDomEventEmitter(),
|
||||
fakeWindow = new FakeDomEventEmitter();
|
||||
fakeDocument = new FakeDomEventEmitter();
|
||||
userActivity = new UserActivity(fakeWindow, fakeDocument);
|
||||
userActivity.start();
|
||||
clock = lolex.install();
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import expect from 'expect';
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import { configure, mount } from "enzyme";
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import ReactTestUtils from 'react-dom/test-utils';
|
||||
import ReactDOM from 'react-dom';
|
||||
import lolex from 'lolex';
|
||||
|
||||
import * as TestUtils from '../../../test-utils';
|
||||
|
||||
|
@ -27,7 +26,6 @@ describe('MemberList', () => {
|
|||
let parentDiv = null;
|
||||
let client = null;
|
||||
let root = null;
|
||||
let clock = null;
|
||||
let memberListRoom;
|
||||
let memberList = null;
|
||||
|
||||
|
@ -40,8 +38,6 @@ describe('MemberList', () => {
|
|||
client = MatrixClientPeg.get();
|
||||
client.hasLazyLoadMembersEnabled = () => false;
|
||||
|
||||
clock = lolex.install();
|
||||
|
||||
parentDiv = document.createElement('div');
|
||||
document.body.appendChild(parentDiv);
|
||||
|
||||
|
@ -114,8 +110,6 @@ describe('MemberList', () => {
|
|||
parentDiv = null;
|
||||
}
|
||||
|
||||
clock.uninstall();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// TODO: Rewrite room settings tests for dialog support
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import jest from 'jest-mock';
|
||||
import * as testUtils from '../../../test-utils';
|
||||
import sdk from '../../../skinned-sdk';
|
||||
import {MatrixClientPeg} from '../../../../src/MatrixClientPeg';
|
||||
|
|
127
yarn.lock
127
yarn.lock
|
@ -1137,6 +1137,13 @@
|
|||
tslib "^1.10.0"
|
||||
webcrypto-core "^1.0.17"
|
||||
|
||||
"@sinonjs/commons@^1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.0.tgz#f90ffc52a2e519f018b13b6c4da03cbff36ebed6"
|
||||
integrity sha512-qbk9AP+cZUsKdW1GJsBpxPKFmCJ0T8swwzVje3qFd+AkQb74Q/tiuzrdfFg8AD2g5HH/XbE/I8Uc1KYHVYWfhg==
|
||||
dependencies:
|
||||
type-detect "4.0.8"
|
||||
|
||||
"@types/babel__core@^7.1.0":
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30"
|
||||
|
@ -1570,6 +1577,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 +1899,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 +1948,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 +2259,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 +2278,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 +3696,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 +3864,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 +3963,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 +4524,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 +4690,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 +4731,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"
|
||||
|
@ -4803,7 +4876,7 @@ isobject@^3.0.0, isobject@^3.0.1:
|
|||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
||||
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
|
||||
|
||||
isomorphic-fetch@^2.1.1, isomorphic-fetch@^2.2.1:
|
||||
isomorphic-fetch@^2.1.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
|
||||
integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
|
||||
|
@ -5544,10 +5617,12 @@ loglevel@^1.6.4:
|
|||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.6.tgz#0ee6300cc058db6b3551fa1c4bf73b83bb771312"
|
||||
integrity sha512-Sgr5lbboAUBo3eXCSPL4/KoVz3ROKquOjcctxmHIt+vol2DrqTQe3SwkKKuYhEiWB5kYa13YyopJ69deJ1irzQ==
|
||||
|
||||
lolex@4.2:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7"
|
||||
integrity sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==
|
||||
lolex@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367"
|
||||
integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^1.7.0"
|
||||
|
||||
longest-streak@^2.0.1:
|
||||
version "2.0.3"
|
||||
|
@ -6104,7 +6179,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 +6653,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"
|
||||
|
@ -6924,7 +7004,7 @@ querystring-es3@^0.2.0:
|
|||
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
|
||||
integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
|
||||
|
||||
querystring@0.2.0, querystring@^0.2.0:
|
||||
querystring@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
|
||||
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
|
||||
|
@ -7169,6 +7249,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"
|
||||
|
@ -7395,11 +7482,6 @@ require-directory@^2.1.1:
|
|||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
|
||||
|
||||
require-json@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/require-json/-/require-json-0.0.1.tgz#3c8914f93d7442de8cbf4e681ac62a72aa3367fe"
|
||||
integrity sha1-PIkU+T10Qt6Mv05oGsYqcqozZ/4=
|
||||
|
||||
require-main-filename@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
|
||||
|
@ -8375,6 +8457,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"
|
||||
|
@ -8495,6 +8584,11 @@ type-check@~0.3.2:
|
|||
dependencies:
|
||||
prelude-ls "~1.1.2"
|
||||
|
||||
type-detect@4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
|
||||
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
|
||||
|
||||
typedarray@^0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
|
@ -8929,11 +9023,6 @@ whatwg-fetch@^0.9.0:
|
|||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz#0e3684c6cb9995b43efc9df03e4c365d95fd9cc0"
|
||||
integrity sha1-DjaExsuZlbQ+/J3wPkw2XZX9nMA=
|
||||
|
||||
whatwg-fetch@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-1.1.1.tgz#ac3c9d39f320c6dce5339969d054ef43dd333319"
|
||||
integrity sha1-rDydOfMgxtzlM5lp0FTvQ90zMxk=
|
||||
|
||||
whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
|
||||
|
|
Loading…
Reference in New Issue