Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/roving

pull/21833/head
Michael Telatynski 2020-01-20 20:48:11 +00:00
commit 397e116efb
68 changed files with 1948 additions and 626 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

91
src/DeviceListener.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -83,7 +83,7 @@ export default createReactClass({
if (viewUserOnClick) {
onClick = () => {
dispatcher.dispatch({
dis.dispatch({
action: 'view_user',
member: this.props.member,
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,57 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import Modal from "../../../Modal";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import DeviceListener from '../../../DeviceListener';
export default class VerifySessionToast extends React.PureComponent {
static propTypes = {
toastKey: PropTypes.string.isRequired,
deviceId: PropTypes.string,
};
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissVerification(this.props.deviceId);
};
_onVerifyClick = async () => {
const cli = MatrixClientPeg.get();
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
const device = await cli.getStoredDevice(cli.getUserId(), this.props.deviceId);
Modal.createTrackedDialog('New Session Verify', 'Starting dialog', DeviceVerifyDialog, {
userId: MatrixClientPeg.get().getUserId(),
device,
}, null, /* priority = */ false, /* static = */ true);
};
render() {
const FormButton = sdk.getComponent("elements.FormButton");
return (<div>
<div className="mx_Toast_description">{_t("Other users may not trust it")}</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
<FormButton label={_t("Verify")} onClick={this._onVerifyClick} />
</div>
</div>);
}
}

View File

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

View File

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

View File

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

View File

@ -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 wont have access to encrypted messages.": "Without completing security on this device, it wont 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.",

View File

@ -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 wont 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."
}

View File

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

View File

@ -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 nexistent 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 nexistent 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 na 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 dinvitation",
"Failed to invite the following users to chat: %(csvUsers)s": "Échec de linvitation 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 dinviter les utilisateurs.",
"We couldn't invite those users. Please check the users you want to invite and try again.": "Impossible dinviter 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>.": "Sil y a quelquun que vous narrivez pas à trouver, demandez-lui son nom dutilisateur (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 lautoriser à 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 wont have access to encrypted messages.": "Si vous ne complétez pas la sécurité sur cet appareil, vous naurez 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": "Dautres utilisateurs pourraient ne pas lui faire confiance",
"Later": "Plus tard"
}

View File

@ -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 wont 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>."
}

View File

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

View File

@ -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": "バックアップをパスワードで保護"
}

View File

@ -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>, если вы не можете ее найти.",

View File

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

View File

@ -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 wont 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>。"
}

View File

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

View File

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

View File

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

View File

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

56
src/stores/ToastStore.js Normal file
View File

@ -0,0 +1,56 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import EventEmitter from 'events';
/**
* Holds the active toasts
*/
export default class ToastStore extends EventEmitter {
static sharedInstance() {
if (!global.mx_ToastStore) global.mx_ToastStore = new ToastStore();
return global.mx_ToastStore;
}
constructor() {
super();
this._dispatcherRef = null;
this._toasts = [];
}
reset() {
this._toasts = [];
}
addOrReplaceToast(newToast) {
const oldIndex = this._toasts.findIndex(t => t.key === newToast.key);
if (oldIndex === -1) {
this._toasts.push(newToast);
} else {
this._toasts[oldIndex] = newToast;
}
this.emit('update');
}
dismissToast(key) {
this._toasts = this._toasts.filter(t => t.key !== key);
this.emit('update');
}
getToasts() {
return this._toasts;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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