Merge branch 'develop' into foldleft/reset-refactor

This commit is contained in:
Zoe 2020-03-27 10:50:05 +00:00
commit a02731f632
49 changed files with 1116 additions and 489 deletions

View file

@ -186,6 +186,7 @@
@import "./views/settings/_AvatarSetting.scss";
@import "./views/settings/_CrossSigningPanel.scss";
@import "./views/settings/_DevicesPanel.scss";
@import "./views/settings/_E2eAdvancedPanel.scss";
@import "./views/settings/_EmailAddresses.scss";
@import "./views/settings/_IntegrationManager.scss";
@import "./views/settings/_KeyBackupPanel.scss";

View file

@ -14,76 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* This file has CSS for both native and non-native scrollbars in an order
* that's fairly logical to read but duplicates a selector to separate the
* hiding/showing from the sizing.
*/
/* stylelint-disable no-duplicate-selectors */
/*
1. for browsers that support native overlay auto-hiding scrollbars
*/
.mx_AutoHideScrollbar {
overflow-x: hidden;
overflow-y: auto;
overflow-y: overlay; // where supported
-ms-overflow-style: -ms-autohiding-scrollbar;
}
/*
2. webkit also supports overflow:overlay where the scrollbars don't take any space
in the layout but they don't autohide, so do that only on hover
*/
body.mx_scrollbar_overlay_noautohide .mx_AutoHideScrollbar {
overflow-y: hidden;
}
body.mx_scrollbar_overlay_noautohide .mx_AutoHideScrollbar:hover {
overflow-y: overlay;
}
/*
3. as a last fallback, compensate for the scrollbar taking up space in the layout
by having giving the child element (.mx_AutoHideScrollbar_offset) a
negative right margin of the width of the scrollbar when the container
is overflowing. This is what Firefox ends up using. Overflow is detected
in javascript, and adds the mx_AutoHideScrollbar_overflow class to the container.
This only works in Firefox, which should be fine as this fallback is only needed there.
*/
body.mx_scrollbar_nooverlay {
.mx_AutoHideScrollbar {
overflow-y: hidden;
&::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: transparent;
}
.mx_AutoHideScrollbar:hover {
overflow-y: auto;
&::-webkit-scrollbar-thumb {
border-radius: 3px;
background-color: transparent;
}
/*
offset scrollbar width with negative margin-right
include before and after psuedo-elements here so they can
be used to do something interesting like scroll-indicating
gradients (see IndicatorScrollBar)
*/
.mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow > .mx_AutoHideScrollbar_offset,
.mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow::before,
.mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow::after {
margin-right: calc(-1 * var(--scrollbar-width));
}
}
// style the native scrollbars ...
// ... standard css scrollbars (firefox at time of writing)
.mx_AutoHideScrollbar {
scrollbar-color: $scrollbar-thumb-color $scrollbar-track-color;
scrollbar-color: transparent transparent;
scrollbar-width: thin;
}
// or fallback for webkit browsers
::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: $scrollbar-track-color;
}
::-webkit-scrollbar-thumb {
background-color: $scrollbar-thumb-color;
border-radius: 3px;
.mx_AutoHideScrollbar:hover {
&::-webkit-scrollbar {
background-color: $scrollbar-track-color;
}
&::-webkit-scrollbar-thumb {
background-color: $scrollbar-thumb-color;
}
scrollbar-color: $scrollbar-thumb-color $scrollbar-track-color;
}

View file

@ -337,7 +337,7 @@ limitations under the License.
display: none;
}
.mx_GroupView_body .mx_AutoHideScrollbar_offset > * {
.mx_GroupView_body .mx_AutoHideScrollbar > * {
margin: 11px 50px 50px 68px;
}
@ -366,7 +366,7 @@ limitations under the License.
padding: 40px 20px;
}
.mx_GroupView .mx_MemberInfo .mx_AutoHideScrollbar_offset > :not(.mx_MemberInfo_avatar) {
.mx_GroupView .mx_MemberInfo .mx_AutoHideScrollbar > :not(.mx_MemberInfo_avatar) {
padding-left: 16px;
padding-right: 16px;
}

View file

@ -166,41 +166,22 @@ limitations under the License.
// overflow indicators
.mx_RoomSubList:not(.resized-all) > .mx_RoomSubList_scroll {
&.mx_IndicatorScrollbar_topOverflow::before,
&.mx_IndicatorScrollbar_bottomOverflow::after {
&.mx_IndicatorScrollbar_topOverflow::before {
position: sticky;
content: "";
top: 0;
left: 0;
right: 0;
height: 8px;
content: "";
display: block;
z-index: 100;
display: block;
pointer-events: none;
}
&.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset {
margin-top: -8px;
}
&.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset {
margin-bottom: -8px;
}
&.mx_IndicatorScrollbar_topOverflow::before {
top: 0;
transition: background-image 0.1s ease-in;
background: linear-gradient(to top, $panel-gradient);
}
/*
// for now, we remove the bottomOverflow entirely as we don't want to
// lose the screen real-estate due to a bg-colored gradient, but we also
// don't want to use drop shadows and risk a confusing hierarchy of cards.
// so, instead, we hard-clip at the bottom but soft-clip at the top.
&.mx_IndicatorScrollbar_bottomOverflow::after {
bottom: 0;
transition: background-image 0.1s ease-in;
margin: 0px -8px;
background: linear-gradient(to bottom, rgba(0,0,0,0.1), rgba(0,0,0,0.0));
&.mx_IndicatorScrollbar_topOverflow {
margin-top: -8px;
}
*/
}

View file

@ -15,6 +15,21 @@ limitations under the License.
*/
.mx_UserInfo {
.mx_EncryptionPanel_cancel {
mask: url('$(res)/img/feather-customised/cancel.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
width: 14px;
height: 14px;
background-color: $settings-subsection-fg-color;
cursor: pointer;
position: absolute;
z-index: 100;
top: 14px;
right: 14px;
}
.mx_VerificationPanel_verified_section .mx_E2EIcon {
// Override general user info margin
margin: 0 auto !important;

View file

@ -41,7 +41,7 @@ limitations under the License.
overflow-x: visible;
}
.mx_AutoHideScrollbar_offset {
.mx_AutoHideScrollbar {
display: flex;
flex-direction: row;
height: 100%;

View file

@ -0,0 +1,20 @@
/*
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_E2eAdvancedPanel_settingLongDescription {
margin-right: 150px;
}

View file

@ -145,13 +145,34 @@ const onSecretRequested = async function({
console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`);
return;
}
const callbacks = client.getCrossSigningCacheCallbacks();
if (!callbacks.getCrossSigningKeyCache) return;
if (name === "m.cross_signing.self_signing") {
const key = await callbacks.getCrossSigningKeyCache("self_signing");
return key && encodeBase64(key);
} else if (name === "m.cross_signing.user_signing") {
const key = await callbacks.getCrossSigningKeyCache("user_signing");
if (name.startsWith("m.cross_signing")) {
const callbacks = client.getCrossSigningCacheCallbacks();
if (!callbacks.getCrossSigningKeyCache) return;
/* Explicit enumeration here is deliberate never share the master key! */
if (name === "m.cross_signing.self_signing") {
const key = await callbacks.getCrossSigningKeyCache("self_signing");
if (!key) {
console.log(
`self_signing requested by ${deviceId}, but not found in cache`,
);
}
return key && encodeBase64(key);
} else if (name === "m.cross_signing.user_signing") {
const key = await callbacks.getCrossSigningKeyCache("user_signing");
if (!key) {
console.log(
`user_signing requested by ${deviceId}, but not found in cache`,
);
}
return key && encodeBase64(key);
}
} else if (name === "m.megolm_backup.v1") {
const key = await client._crypto.getSessionBackupPrivateKey();
if (!key) {
console.log(
`session backup key requested by ${deviceId}, but not found in cache`,
);
}
return key && encodeBase64(key);
}
console.warn("onSecretRequested didn't recognise the secret named ", name);

View file

@ -169,7 +169,7 @@ export default class DeviceListener {
key: THIS_DEVICE_TOAST_KEY,
title: _t("Encryption upgrade available"),
icon: "verification_warning",
props: {kind: 'upgrade_encryption'},
props: {kind: 'upgrade_ssss'},
component: sdk.getComponent("toasts.SetupEncryptionToast"),
});
} else {
@ -189,7 +189,7 @@ export default class DeviceListener {
this._activeNagToasts.add(device.deviceId);
ToastStore.sharedInstance().addOrReplaceToast({
key: toastKey(device.deviceId),
title: _t("Unverified session"),
title: _t("Unverified login. Was this you?"),
icon: "verification_warning",
props: { device },
component: sdk.getComponent("toasts.UnverifiedSessionToast"),

View file

@ -24,6 +24,8 @@ import {MatrixClientPeg} from "./MatrixClientPeg";
import RoomViewStore from "./stores/RoomViewStore";
import {IntegrationManagers} from "./integrations/IntegrationManagers";
import SettingsStore from "./settings/SettingsStore";
import {Capability, KnownWidgetActions} from "./widgets/WidgetApi";
import SdkConfig from "./SdkConfig";
const WIDGET_API_VERSION = '0.0.2'; // Current API version
const SUPPORTED_WIDGET_API_VERSIONS = [
@ -213,11 +215,18 @@ export default class FromWidgetPostMessageApi {
const data = event.data.data;
const val = data.value;
if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) {
if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
}
} else if (action === 'get_openid') {
// Handled by caller
} else if (action === KnownWidgetActions.GetRiotWebConfig) {
if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.GetRiotWebConfig)) {
this.sendResponse(event, {
api: INBOUND_API_NAME,
config: SdkConfig.get(),
});
}
} else {
console.warn('Widget postMessage event unhandled');
this.sendError(event, {message: 'The postMessage was unhandled'});

View file

@ -22,6 +22,7 @@ export const Key = {
PAGE_UP: "PageUp",
PAGE_DOWN: "PageDown",
BACKSPACE: "Backspace",
DELETE: "Delete",
ARROW_UP: "ArrowUp",
ARROW_DOWN: "ArrowDown",
ARROW_LEFT: "ArrowLeft",

View file

@ -148,6 +148,9 @@ class _MatrixClientPeg {
// check that we have a version of the js-sdk which includes initCrypto
if (!SettingsStore.getValue("lowBandwidth") && this.matrixClient.initCrypto) {
await this.matrixClient.initCrypto();
this.matrixClient.setCryptoTrustCrossSignedDevices(
!SettingsStore.getValue('e2ee.manuallyVerifyAllSessions'),
);
StorageManager.setCryptoInitialised(true);
}
} catch (e) {

View file

@ -27,6 +27,7 @@ import {MatrixClientPeg} from "./MatrixClientPeg";
import SettingsStore from "./settings/SettingsStore";
import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
import WidgetUtils from "./utils/WidgetUtils";
import {KnownWidgetActions} from "./widgets/WidgetApi";
if (!global.mxFromWidgetMessaging) {
global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
@ -75,6 +76,17 @@ export default class WidgetMessaging {
});
}
/**
* Tells the widget that the client is ready to handle further widget requests.
* @returns {Promise<*>} Resolves after the widget has acknowledged the ready message.
*/
flagReadyToContinue() {
return this.messageToWidget({
api: OUTBOUND_API_NAME,
action: KnownWidgetActions.ClientReady,
});
}
/**
* Request a screenshot from a widget
* @return {Promise} To be resolved with screenshot data when it has been generated

View file

@ -1,5 +1,6 @@
/*
Copyright 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.
@ -16,93 +17,10 @@ limitations under the License.
import React from "react";
// derived from code from github.com/noeldelgado/gemini-scrollbar
// Copyright (c) Noel Delgado <pixelia.me@gmail.com> (pixelia.me)
function getScrollbarWidth(alternativeOverflow) {
const div = document.createElement('div');
div.className = 'mx_AutoHideScrollbar'; //to get width of css scrollbar
div.style.position = 'absolute';
div.style.top = '-9999px';
div.style.width = '100px';
div.style.height = '100px';
div.style.overflow = "scroll";
if (alternativeOverflow) {
div.style.overflow = alternativeOverflow;
}
div.style.msOverflowStyle = '-ms-autohiding-scrollbar';
document.body.appendChild(div);
const scrollbarWidth = (div.offsetWidth - div.clientWidth);
document.body.removeChild(div);
return scrollbarWidth;
}
function install() {
const scrollbarWidth = getScrollbarWidth();
if (scrollbarWidth !== 0) {
const hasForcedOverlayScrollbar = getScrollbarWidth('overlay') === 0;
// overflow: overlay on webkit doesn't auto hide the scrollbar
if (hasForcedOverlayScrollbar) {
document.body.classList.add("mx_scrollbar_overlay_noautohide");
} else {
document.body.classList.add("mx_scrollbar_nooverlay");
const style = document.createElement('style');
style.type = 'text/css';
style.innerText =
`body.mx_scrollbar_nooverlay { --scrollbar-width: ${scrollbarWidth}px; }`;
document.head.appendChild(style);
}
}
}
const installBodyClassesIfNeeded = (function() {
let installed = false;
return function() {
if (!installed) {
install();
installed = true;
}
};
})();
export default class AutoHideScrollbar extends React.Component {
constructor(props) {
super(props);
this.onOverflow = this.onOverflow.bind(this);
this.onUnderflow = this.onUnderflow.bind(this);
this._collectContainerRef = this._collectContainerRef.bind(this);
this._needsOverflowListener = null;
}
onOverflow() {
this.containerRef.classList.add("mx_AutoHideScrollbar_overflow");
this.containerRef.classList.remove("mx_AutoHideScrollbar_underflow");
}
onUnderflow() {
this.containerRef.classList.remove("mx_AutoHideScrollbar_overflow");
this.containerRef.classList.add("mx_AutoHideScrollbar_underflow");
}
checkOverflow() {
if (!this._needsOverflowListener) {
return;
}
if (this.containerRef.scrollHeight > this.containerRef.clientHeight) {
this.onOverflow();
} else {
this.onUnderflow();
}
}
componentDidUpdate() {
this.checkOverflow();
}
componentDidMount() {
installBodyClassesIfNeeded();
this._needsOverflowListener =
document.body.classList.contains("mx_scrollbar_nooverlay");
this.checkOverflow();
}
_collectContainerRef(ref) {
@ -126,9 +44,7 @@ export default class AutoHideScrollbar extends React.Component {
onScroll={this.props.onScroll}
onWheel={this.props.onWheel}
>
<div className="mx_AutoHideScrollbar_offset">
{ this.props.children }
</div>
{ this.props.children }
</div>);
}
}

View file

@ -66,6 +66,22 @@ export default class IndicatorScrollbar extends React.Component {
this._autoHideScrollbar = autoHideScrollbar;
}
componentDidUpdate(prevProps) {
const prevLen = prevProps && prevProps.children && prevProps.children.length || 0;
const curLen = this.props.children && this.props.children.length || 0;
// check overflow only if amount of children changes.
// if we don't guard here, we end up with an infinite
// render > componentDidUpdate > checkOverflow > setState > render loop
if (prevLen !== curLen) {
this.checkOverflow();
}
}
componentDidMount() {
this.checkOverflow();
}
checkOverflow() {
const hasTopOverflow = this._scrollElement.scrollTop > 0;
const hasBottomOverflow = this._scrollElement.scrollHeight >
@ -95,10 +111,6 @@ export default class IndicatorScrollbar extends React.Component {
this._scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
}
if (this._autoHideScrollbar) {
this._autoHideScrollbar.checkOverflow();
}
if (this.props.trackHorizontalOverflow) {
this.setState({
// Offset from absolute position of the container

View file

@ -18,13 +18,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { accessSecretStorage, AccessCancelledError } from '../../../CrossSigningManager';
const PHASE_INTRO = 0;
const PHASE_BUSY = 1;
const PHASE_DONE = 2;
const PHASE_CONFIRM_SKIP = 3;
import {
SetupEncryptionStore,
PHASE_INTRO,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
} from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from "./SetupEncryptionBody";
export default class CompleteSecurity extends React.Component {
static propTypes = {
@ -33,232 +34,42 @@ export default class CompleteSecurity extends React.Component {
constructor() {
super();
this.state = {
phase: PHASE_INTRO,
// this serves dual purpose as the object for the request logic and
// the presence of it insidicating that we're in 'verify mode'.
// Because of the latter, it lives in the state.
verificationRequest: null,
backupInfo: null,
};
MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest);
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this._onStoreUpdate);
store.start();
this.state = {phase: store.phase};
}
_onStoreUpdate = () => {
const store = SetupEncryptionStore.sharedInstance();
this.setState({phase: store.phase});
};
componentWillUnmount() {
if (this.state.verificationRequest) {
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
}
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest);
}
}
_onUsePassphraseClick = async () => {
this.setState({
phase: PHASE_BUSY,
});
const cli = MatrixClientPeg.get();
try {
const backupInfo = await cli.getKeyBackupVersion();
this.setState({backupInfo});
// The control flow is fairly twisted here...
// For the purposes of completing security, we only wait on getting
// as far as the trust check and then show a green shield.
// We also begin the key backup restore as well, which we're
// awaiting inside `accessSecretStorage` only so that it keeps your
// passphase cached for that work. This dialog itself will only wait
// on the first trust check, and the key backup restore will happen
// in the background.
await new Promise((resolve, reject) => {
try {
accessSecretStorage(async () => {
await cli.checkOwnCrossSigningTrust();
resolve();
if (backupInfo) {
// A complete restore can take many minutes for large
// accounts / slow servers, so we allow the dialog
// to advance before this.
await cli.restoreKeyBackupWithSecretStorage(backupInfo);
}
});
} catch (e) {
console.error(e);
reject(e);
}
});
if (cli.getCrossSigningId()) {
this.setState({
phase: PHASE_DONE,
});
}
} catch (e) {
if (!(e instanceof AccessCancelledError)) {
console.log(e);
}
// this will throw if the user hits cancel, so ignore
this.setState({
phase: PHASE_INTRO,
});
}
}
onVerificationRequest = async (request) => {
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
if (this.state.verificationRequest) {
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
}
await request.accept();
request.on("change", this.onVerificationRequestChange);
this.setState({
verificationRequest: request,
});
}
onVerificationRequestChange = () => {
if (this.state.verificationRequest.cancelled) {
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
this.setState({
verificationRequest: null,
});
}
}
onSkipClick = () => {
this.setState({
phase: PHASE_CONFIRM_SKIP,
});
}
onSkipConfirmClick = () => {
this.props.onFinished();
}
onSkipBackClick = () => {
this.setState({
phase: PHASE_INTRO,
});
}
onDoneClick = () => {
this.props.onFinished();
const store = SetupEncryptionStore.sharedInstance();
store.off("update", this._onStoreUpdate);
store.stop();
}
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {
phase,
} = this.state;
const {phase} = this.state;
let icon;
let title;
let body;
if (this.state.verificationRequest) {
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
body = <EncryptionPanel
layout="dialog"
verificationRequest={this.state.verificationRequest}
onClose={this.props.onFinished}
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>;
} else if (phase === PHASE_INTRO) {
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
if (phase === PHASE_INTRO) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
title = _t("Complete security");
body = (
<div>
<p>{_t(
"Open an existing session & use it to verify this one, " +
"granting it access to encrypted messages.",
)}</p>
<p className="mx_CompleteSecurity_waiting"><InlineSpinner />{_t("Waiting…")}</p>
<p>{_t(
"If you cant access one, <button>use your recovery key or passphrase.</button>",
{}, {
button: sub => <AccessibleButton element="span"
className="mx_linkButton"
onClick={this._onUsePassphraseClick}
>
{sub}
</AccessibleButton>,
})}</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
kind="danger"
onClick={this.onSkipClick}
>
{_t("Skip")}
</AccessibleButton>
</div>
</div>
);
} else if (phase === PHASE_DONE) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified"></span>;
title = _t("Session verified");
let message;
if (this.state.backupInfo) {
message = <p>{_t(
"Your new session is now verified. It has access to your " +
"encrypted messages, and other users will see it as trusted.",
)}</p>;
} else {
message = <p>{_t(
"Your new session is now verified. Other users will see it as trusted.",
)}</p>;
}
body = (
<div>
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified"></div>
{message}
<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 session, 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 if (phase === PHASE_BUSY) {
const Spinner = sdk.getComponent('views.elements.Spinner');
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
title = _t("Complete security");
body = <Spinner />;
} else {
throw new Error(`Unknown phase ${phase}`);
}
@ -271,7 +82,7 @@ export default class CompleteSecurity extends React.Component {
{title}
</h2>
<div className="mx_CompleteSecurity_body">
{body}
<SetupEncryptionBody onFinished={this.props.onFinished} />
</div>
</CompleteSecurityBody>
</AuthPage>

View file

@ -0,0 +1,196 @@
/*
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 { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import {
SetupEncryptionStore,
PHASE_INTRO,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
PHASE_FINISHED,
} from '../../../stores/SetupEncryptionStore';
export default class SetupEncryptionBody extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this._onStoreUpdate);
store.start();
this.state = {
phase: store.phase,
// this serves dual purpose as the object for the request logic and
// the presence of it indicating that we're in 'verify mode'.
// Because of the latter, it lives in the state.
verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo,
};
}
_onStoreUpdate = () => {
const store = SetupEncryptionStore.sharedInstance();
if (store.phase === PHASE_FINISHED) {
this.props.onFinished();
return;
}
this.setState({
phase: store.phase,
verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo,
});
};
componentWillUnmount() {
const store = SetupEncryptionStore.sharedInstance();
store.off("update", this._onStoreUpdate);
store.stop();
}
_onUsePassphraseClick = async () => {
const store = SetupEncryptionStore.sharedInstance();
store.usePassPhrase();
}
onSkipClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skip();
}
onSkipConfirmClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skipConfirm();
}
onSkipBackClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.returnAfterSkip();
}
onDoneClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.done();
}
render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {
phase,
} = this.state;
if (this.state.verificationRequest) {
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
return <EncryptionPanel
layout="dialog"
verificationRequest={this.state.verificationRequest}
onClose={this.props.onFinished}
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>;
} else if (phase === PHASE_INTRO) {
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
return (
<div>
<p>{_t(
"Open an existing session & use it to verify this one, " +
"granting it access to encrypted messages.",
)}</p>
<p className="mx_CompleteSecurity_waiting"><InlineSpinner />{_t("Waiting…")}</p>
<p>{_t(
"If you cant access one, <button>use your recovery key or passphrase.</button>",
{}, {
button: sub => <AccessibleButton element="span"
className="mx_linkButton"
onClick={this._onUsePassphraseClick}
>
{sub}
</AccessibleButton>,
})}</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
kind="danger"
onClick={this.onSkipClick}
>
{_t("Skip")}
</AccessibleButton>
</div>
</div>
);
} else if (phase === PHASE_DONE) {
let message;
if (this.state.backupInfo) {
message = <p>{_t(
"Your new session is now verified. It has access to your " +
"encrypted messages, and other users will see it as trusted.",
)}</p>;
} else {
message = <p>{_t(
"Your new session is now verified. Other users will see it as trusted.",
)}</p>;
}
return (
<div>
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified"></div>
{message}
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
kind="primary"
onClick={this.onDoneClick}
>
{_t("Done")}
</AccessibleButton>
</div>
</div>
);
} else if (phase === PHASE_CONFIRM_SKIP) {
return (
<div>
<p>{_t(
"Without completing security on this session, 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 if (phase === PHASE_BUSY) {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <Spinner />;
} else {
console.log(`SetupEncryptionBody: Unknown phase ${phase}`);
}
}
}

View file

@ -0,0 +1,29 @@
/*
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 SetupEncryptionBody from '../../structures/auth/SetupEncryptionBody';
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
export default function SetupEncryptionDialog({onFinished}) {
return <BaseDialog
headerImage={require("../../../../res/img/e2e/warning.svg")}
onFinished={onFinished}
title={_t("Verify this session")}
>
<SetupEncryptionBody onFinished={onFinished} />
</BaseDialog>;
}

View file

@ -419,6 +419,12 @@ export default class AppTile extends React.Component {
if (this.props.onCapabilityRequest) {
this.props.onCapabilityRequest(requestedCapabilities);
}
// We only tell Jitsi widgets that we're ready because they're realistically the only ones
// using this custom extension to the widget API.
if (this.props.type === 'jitsi') {
widgetMessaging.flagReadyToContinue();
}
}).catch((err) => {
console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err);
});

View file

@ -28,7 +28,7 @@ export const PendingActionSpinner = ({text}) => {
</div>;
};
const EncryptionInfo = ({waitingForOtherParty, waitingForNetwork, member, onStartVerification}) => {
const EncryptionInfo = ({waitingForOtherParty, waitingForNetwork, member, onStartVerification, isRoomEncrypted}) => {
let content;
if (waitingForOtherParty || waitingForNetwork) {
let text;
@ -49,13 +49,27 @@ const EncryptionInfo = ({waitingForOtherParty, waitingForNetwork, member, onStar
);
}
return <React.Fragment>
<div className="mx_UserInfo_container">
<h3>{_t("Encryption")}</h3>
let description;
if (isRoomEncrypted) {
description = (
<div>
<p>{_t("Messages in this room are end-to-end encrypted.")}</p>
<p>{_t("Your messages are secured and only you and the recipient have the unique keys to unlock them.")}</p>
</div>
);
} else {
description = (
<div>
<p>{_t("Messages in this room are not end-to-end encrypted.")}</p>
<p>{_t("In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.")}</p>
</div>
);
}
return <React.Fragment>
<div className="mx_UserInfo_container">
<h3>{_t("Encryption")}</h3>
{ description }
</div>
<div className="mx_UserInfo_container">
<h3>{_t("Verify User")}</h3>

View file

@ -30,7 +30,8 @@ import {_t} from "../../../languageHandler";
// cancellation codes which constitute a key mismatch
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
const EncryptionPanel = ({verificationRequest, verificationRequestPromise, member, onClose, layout}) => {
const EncryptionPanel = (props) => {
const {verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted} = props;
const [request, setRequest] = useState(verificationRequest);
// state to show a spinner immediately after clicking "start verification",
// before we have a request
@ -83,6 +84,22 @@ const EncryptionPanel = ({verificationRequest, verificationRequestPromise, membe
}, [onClose, request]);
useEventEmitter(request, "change", changeHandler);
const onCancel = useCallback(function() {
if (request) {
request.cancel();
}
}, [request]);
let cancelButton;
if (layout !== "dialog" && request && request.pending) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
cancelButton = (<AccessibleButton
className="mx_EncryptionPanel_cancel"
onClick={onCancel}
title={_t('Cancel')}
></AccessibleButton>);
}
const onStartVerification = useCallback(async () => {
setRequesting(true);
const cli = MatrixClientPeg.get();
@ -97,21 +114,27 @@ const EncryptionPanel = ({verificationRequest, verificationRequestPromise, membe
(request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined));
if (!request || requested) {
const initiatedByMe = (!request && isRequesting) || (request && request.initiatedByMe);
return <EncryptionInfo
onStartVerification={onStartVerification}
member={member}
waitingForOtherParty={requested && initiatedByMe}
waitingForNetwork={requested && !initiatedByMe} />;
return (<React.Fragment>
{cancelButton}
<EncryptionInfo
isRoomEncrypted={isRoomEncrypted}
onStartVerification={onStartVerification}
member={member}
waitingForOtherParty={requested && initiatedByMe}
waitingForNetwork={requested && !initiatedByMe} />
</React.Fragment>);
} else {
return (
return (<React.Fragment>
{cancelButton}
<VerificationPanel
isRoomEncrypted={isRoomEncrypted}
layout={layout}
onClose={onClose}
member={member}
request={request}
key={request.channel.transactionId}
phase={phase} />
);
</React.Fragment>);
}
};
EncryptionPanel.propTypes = {

View file

@ -1297,8 +1297,7 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
const userVerified = userTrust.isCrossSigningVerified();
const isMe = member.userId === cli.getUserId();
const canVerify = SettingsStore.isFeatureEnabled("feature_cross_signing") &&
homeserverSupportsCrossSigning &&
isRoomEncrypted && !userVerified && !isMe;
homeserverSupportsCrossSigning && !userVerified && !isMe;
const setUpdating = (updating) => {
setPendingUpdateCount(count => count + (updating ? 1 : -1));
@ -1320,20 +1319,15 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
);
}
let devicesSection;
if (isRoomEncrypted) {
devicesSection = <DevicesSection
loading={devices === undefined}
devices={devices}
userId={member.userId} />;
}
const securitySection = (
<div className="mx_UserInfo_container">
<h3>{ _t("Security") }</h3>
<p>{ text }</p>
{ verifyButton }
{ devicesSection }
<DevicesSection
loading={devices === undefined}
devices={devices}
userId={member.userId} />
</div>
);
@ -1388,6 +1382,7 @@ const UserInfoHeader = ({onClose, member, e2eStatus}) => {
<div>
<div>
<MemberAvatar
key={member.userId} // to instantly blank the avatar when UserInfo changes members
member={member}
width={2 * 0.3 * window.innerHeight} // 2x@30vh
height={2 * 0.3 * window.innerHeight} // 2x@30vh
@ -1496,7 +1491,7 @@ const UserInfo = ({user, groupId, roomId, onClose, phase=RIGHT_PANEL_PHASES.Room
case RIGHT_PANEL_PHASES.EncryptionPanel:
classes.push("mx_UserInfo_smallAvatar");
content = (
<EncryptionPanel {...props} member={member} onClose={onClose} />
<EncryptionPanel {...props} member={member} onClose={onClose} isRoomEncrypted={isRoomEncrypted} />
);
break;
}

View file

@ -48,6 +48,7 @@ export default class VerificationPanel extends React.PureComponent {
PHASE_DONE,
]).isRequired,
onClose: PropTypes.func.isRequired,
isRoomEncrypted: PropTypes.bool,
};
constructor(props) {
@ -174,15 +175,22 @@ export default class VerificationPanel extends React.PureComponent {
renderVerifiedPhase() {
const {member} = this.props;
let text;
if (this.props.isRoomEncrypted) {
text = _t("Verify all users in a room to ensure it's secure.");
} else {
text = _t("In encrypted rooms, verify all users to ensure its secure.");
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
<h3>Verified</h3>
<h3>{_t("Verified")}</h3>
<p>{_t("You've successfully verified %(displayName)s!", {
displayName: member.displayName || member.name || member.userId,
})}</p>
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
<p>Verify all users in a room to ensure it's secure.</p>
<p>{ text }</p>
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
{_t("Got it")}
@ -209,7 +217,7 @@ export default class VerificationPanel extends React.PureComponent {
return (
<div className="mx_UserInfo_container">
<h3>Verification cancelled</h3>
<h3>{_t("Verification cancelled")}</h3>
<p>{ text }</p>
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
@ -231,7 +239,7 @@ export default class VerificationPanel extends React.PureComponent {
if (this.state.sasEvent) {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return <div className="mx_UserInfo_container">
<h3>Compare emoji</h3>
<h3>{_t("Compare emoji")}</h3>
<VerificationShowSas
displayName={displayName}
sas={this.state.sasEvent.sas}

View file

@ -447,6 +447,8 @@ export default class BasicMessageEditor extends React.Component {
} else if (event.key === Key.TAB) {
this._tabCompleteName();
handled = true;
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
this._formatBarRef.hide();
}
}
if (handled) {

View file

@ -65,6 +65,7 @@ export default createReactClass({
});
if (isRoomEncrypted) {
cli.on("userTrustStatusChanged", this.onUserTrustStatusChanged);
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.updateE2EStatus();
} else {
// Listen for room to become encrypted
@ -88,6 +89,7 @@ export default createReactClass({
if (cli) {
cli.removeListener("RoomState.events", this.onRoomStateEvents);
cli.removeListener("userTrustStatusChanged", this.onUserTrustStatusChanged);
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
}
},
@ -110,6 +112,11 @@ export default createReactClass({
this.updateE2EStatus();
},
onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
if (userId !== this.props.member.userId) return;
this.updateE2EStatus();
},
updateE2EStatus: async function() {
const cli = MatrixClientPeg.get();
const { userId } = this.props.member;

View file

@ -32,6 +32,9 @@ export default class CrossSigningPanel extends React.PureComponent {
error: null,
crossSigningPublicKeysOnDevice: false,
crossSigningPrivateKeysInStorage: false,
selfSigningPrivateKeyCached: false,
userSigningPrivateKeyCached: false,
sessionBackupKeyCached: false,
secretStorageKeyInAccount: false,
secretStorageKeyNeedsUpgrade: null,
};
@ -71,10 +74,14 @@ export default class CrossSigningPanel extends React.PureComponent {
async _getUpdatedStatus() {
const cli = MatrixClientPeg.get();
const pkCache = cli.getCrossSigningCacheCallbacks();
const crossSigning = cli._crypto._crossSigningInfo;
const secretStorage = cli._crypto._secretStorage;
const crossSigningPublicKeysOnDevice = crossSigning.getId();
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
const selfSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing"));
const userSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing"));
const sessionBackupKeyCached = !!(await cli._crypto.getSessionBackupPrivateKey());
const secretStorageKeyInAccount = await secretStorage.hasKey();
const homeserverSupportsCrossSigning =
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
@ -84,6 +91,9 @@ export default class CrossSigningPanel extends React.PureComponent {
this.setState({
crossSigningPublicKeysOnDevice,
crossSigningPrivateKeysInStorage,
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
sessionBackupKeyCached,
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
@ -130,6 +140,9 @@ export default class CrossSigningPanel extends React.PureComponent {
error,
crossSigningPublicKeysOnDevice,
crossSigningPrivateKeysInStorage,
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
sessionBackupKeyCached,
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
@ -209,6 +222,18 @@ export default class CrossSigningPanel extends React.PureComponent {
<td>{_t("Cross-signing private keys:")}</td>
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}</td>
</tr>
<tr>
<td>{_t("Self signing private key:")}</td>
<td>{selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
</tr>
<tr>
<td>{_t("User signing private key:")}</td>
<td>{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
</tr>
<tr>
<td>{_t("Session backup key:")}</td>
<td>{sessionBackupKeyCached ? _t("cached locally") : _t("not found locally")}</td>
</tr>
<tr>
<td>{_t("Secret storage public key:")}</td>
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>

View file

@ -0,0 +1,39 @@
/*
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 * as sdk from '../../../index';
import {_t} from "../../../languageHandler";
import {SettingLevel} from "../../../settings/SettingsStore";
const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
const E2eAdvancedPanel = props => {
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
return <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS}
level={SettingLevel.DEVICE}
/>
<div className="mx_E2eAdvancedPanel_settingLongDescription">{_t(
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
)}</div>
</div>;
};
export default E2eAdvancedPanel;

View file

@ -281,6 +281,8 @@ export default class SecurityUserSettingsTab extends React.Component {
);
}
const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel');
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
@ -311,6 +313,7 @@ export default class SecurityUserSettingsTab extends React.Component {
</div>
{this._renderIgnoredUsers()}
{this._renderManageInvites()}
<E2eAdvancedPanel />
</div>
);
}

View file

@ -16,23 +16,61 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import Modal from '../../../Modal';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import DeviceListener from '../../../DeviceListener';
import SetupEncryptionDialog from "../dialogs/SetupEncryptionDialog";
import { accessSecretStorage } from '../../../CrossSigningManager';
export default class SetupEncryptionToast extends React.PureComponent {
static propTypes = {
toastKey: PropTypes.string.isRequired,
kind: PropTypes.oneOf(['set_up_encryption', 'verify_this_session', 'upgrade_encryption']).isRequired,
kind: PropTypes.oneOf([
'set_up_encryption',
'verify_this_session',
'upgrade_encryption',
'upgrade_ssss',
]).isRequired,
};
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissEncryptionSetup();
};
async _waitForCompletion() {
if (this.props.kind === 'upgrade_ssss') {
return new Promise(resolve => {
const recheck = async () => {
const needsUpgrade = await MatrixClientPeg.get().secretStorageKeyNeedsUpgrade();
if (!needsUpgrade) {
MatrixClientPeg.get().removeListener('accountData', recheck);
resolve();
}
};
MatrixClientPeg.get().on('accountData', recheck);
recheck();
});
} else {
return;
}
}
_onSetupClick = async () => {
accessSecretStorage();
if (this.props.kind === "verify_this_session") {
Modal.createTrackedDialog('Verify session', 'Verify session', SetupEncryptionDialog,
{}, null, /* priority = */ false, /* static = */ true);
} else {
const Spinner = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
try {
await accessSecretStorage();
await this._waitForCompletion();
} finally {
modal.close();
}
}
};
getDescription() {
@ -42,6 +80,8 @@ export default class SetupEncryptionToast extends React.PureComponent {
return _t('Verify yourself & others to keep your chats safe');
case 'verify_this_session':
return _t('Other users may not trust it');
case 'upgrade_ssss':
return _t('Update your secure storage');
}
}
@ -49,6 +89,7 @@ export default class SetupEncryptionToast extends React.PureComponent {
switch (this.props.kind) {
case 'set_up_encryption':
case 'upgrade_encryption':
case 'upgrade_ssss':
return _t('Upgrade');
case 'verify_this_session':
return _t('Verify');

View file

@ -174,6 +174,9 @@ export function findDMForUser(client, userId) {
return member && (member.membership === "invite" || member.membership === "join");
}
return false;
}).sort((r1, r2) => {
return r2.getLastActiveTimestamp() -
r1.getLastActiveTimestamp();
});
if (suitableDMRooms.length) {
return suitableDMRooms[0];

View file

@ -701,7 +701,7 @@
"Failed to remove tag %(tagName)s from room": "Fehler beim Entfernen des \"%(tagName)s\"-Tags von dem Raum",
"Failed to add tag %(tagName)s to room": "Fehler beim Hinzufügen des \"%(tagName)s\"-Tags an dem Raum",
"Did you know: you can use communities to filter your Riot.im experience!": "Wusstest du: Du kannst Communities nutzen um deine Riot.im-Erfahrung zu filtern!",
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Um einen Filter zu setzen, siehe einen Community-Bild auf das Filter-Panel ganz links. Du kannst jederzeit auf einen Avatar im Filter-Panel klicken um nur die Räume und Personen aus der Community zu sehen.",
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Um einen Filter zu setzen, ziehe ein Community-Bild auf das Filter-Panel ganz links. Du kannst jederzeit auf einen Avatar im Filter-Panel klicken um nur die Räume und Personen aus der Community zu sehen.",
"Clear filter": "Filter zurücksetzen",
"Key request sent.": "Schlüssel-Anfragen gesendet.",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Wenn du einen Fehler via GitHub gemeldet hast, können Fehlerberichte uns helfen um das Problem zu finden. Sie enthalten Anwendungsdaten wie deinen Nutzernamen, Raum- und Gruppen-ID's und Aliase die du besucht hast und Nutzernamen anderer Nutzer. Sie enthalten keine Nachrichten.",
@ -1733,5 +1733,11 @@
"%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s erstellte eine Ausschluss-Regel für Nutzer, die wegen %(reason)s %(glob)s entspricht",
"%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s erstellte eine Ausschluss-Regel für Räume, die wegen %(reason)s %(glob)s entspricht",
"%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s erstellte eine Ausschluss-Regel für Server, die aufgrund von %(reason)s %(glob)s entsprechen",
"%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s erstellt eine Ausschluss-Regel, die aufgrund von %(reason)s %(glob)s entsprechen"
"%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s erstellt eine Ausschluss-Regel, die aufgrund von %(reason)s %(glob)s entsprechen",
"Do you want to chat with %(user)s?": "Möchtest du mit %(user)s chatten?",
"<userName/> wants to chat": "<userName/> möchte mit dir chatten",
"Start chatting": "Chat starten",
"Reject & Ignore user": "Ablehnen & Nutzer ignorieren",
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s ändert eine Ausschluss-Regel von %(oldGlob)s nach %(newGlob)s, wegen %(reason)s",
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s ändert eine Ausschluss-Regel für Räume von %(oldGlob)s nach %(newGlob)s, wegen %(reason)s"
}

View file

@ -96,7 +96,7 @@
"Verify this session": "Verify this session",
"Encryption upgrade available": "Encryption upgrade available",
"Set up encryption": "Set up encryption",
"Unverified session": "Unverified session",
"Unverified login. Was this you?": "Unverified login. Was this you?",
"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",
@ -432,6 +432,7 @@
"Enable message search in encrypted rooms": "Enable message search in encrypted rooms",
"Keep secret storage passphrase in memory for this session": "Keep secret storage passphrase in memory for this session",
"How fast should messages be downloaded.": "How fast should messages be downloaded.",
"Manually verify all remote sessions": "Manually verify all remote sessions",
"Collecting app version information": "Collecting app version information",
"Collecting logs": "Collecting logs",
"Uploading report": "Uploading report",
@ -543,6 +544,7 @@
"Pin": "Pin",
"Verify yourself & others to keep your chats safe": "Verify yourself & others to keep your chats safe",
"Other users may not trust it": "Other users may not trust it",
"Update your secure storage": "Update your secure storage",
"Upgrade": "Upgrade",
"Verify": "Verify",
"Later": "Later",
@ -582,6 +584,11 @@
"not found": "not found",
"Cross-signing private keys:": "Cross-signing private keys:",
"in secret storage": "in secret storage",
"Self signing private key:": "Self signing private key:",
"cached locally": "cached locally",
"not found locally": "not found locally",
"User signing private key:": "User signing private key:",
"Session backup key:": "Session backup key:",
"Secret storage public key:": "Secret storage public key:",
"in account data": "in account data",
"Homeserver feature support:": "Homeserver feature support:",
@ -598,6 +605,7 @@
"Public Name": "Public Name",
"Last seen": "Last seen",
"Failed to set display name": "Failed to set display name",
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
"Disable Notifications": "Disable Notifications",
"Enable Notifications": "Enable Notifications",
"Securely cache encrypted messages locally for them to appear in search results, using ": "Securely cache encrypted messages locally for them to appear in search results, using ",
@ -1195,6 +1203,8 @@
"Start Verification": "Start Verification",
"Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.",
"Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Your messages are secured and only you and the recipient have the unique keys to unlock them.",
"Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.",
"In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.",
"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.",
"Your messages are not secure": "Your messages are not secure",
@ -1221,7 +1231,6 @@
"Failed to remove user from community": "Failed to remove user from community",
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> in %(roomName)s",
"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.",
"Security": "Security",
"The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.": "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.",
"Verify by scanning": "Verify by scanning",
@ -1229,11 +1238,16 @@
"Verify by emoji": "Verify by emoji",
"If you can't scan the code above, verify by comparing unique emoji.": "If you can't scan the code above, verify by comparing unique emoji.",
"Verify by comparing unique emoji.": "Verify by comparing unique emoji.",
"Verify all users in a room to ensure it's secure.": "Verify all users in a room to ensure it's secure.",
"In encrypted rooms, verify all users to ensure its secure.": "In encrypted rooms, verify all users to ensure its secure.",
"Verified": "Verified",
"You've successfully verified %(displayName)s!": "You've successfully verified %(displayName)s!",
"Got it": "Got it",
"Verification timed out. Start verification again from their profile.": "Verification timed out. Start verification again from their profile.",
"%(displayName)s cancelled verification. Start verification again from their profile.": "%(displayName)s cancelled verification. Start verification again from their profile.",
"You cancelled verification. Start verification again from their profile.": "You cancelled verification. Start verification again from their profile.",
"Verification cancelled": "Verification cancelled",
"Compare emoji": "Compare emoji",
"Sunday": "Sunday",
"Monday": "Monday",
"Tuesday": "Tuesday",
@ -2006,14 +2020,7 @@
"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",
"Open an existing session & use it to verify this one, granting it access to encrypted messages.": "Open an existing session & use it to verify this one, granting it access to encrypted messages.",
"Waiting…": "Waiting…",
"If you cant access one, <button>use your recovery key or passphrase.</button>": "If you cant access one, <button>use your recovery key or passphrase.</button>",
"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.",
"Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
"Without completing security on this session, it wont have access to encrypted messages.": "Without completing security on this session, 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.",
@ -2063,6 +2070,13 @@
"You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
"Registration Successful": "Registration Successful",
"Create your account": "Create your account",
"Open an existing session & use it to verify this one, granting it access to encrypted messages.": "Open an existing session & use it to verify this one, granting it access to encrypted messages.",
"Waiting…": "Waiting…",
"If you cant access one, <button>use your recovery key or passphrase.</button>": "If you cant access one, <button>use your recovery key or passphrase.</button>",
"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.",
"Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
"Without completing security on this session, it wont have access to encrypted messages.": "Without completing security on this session, it wont have access to encrypted messages.",
"Go Back": "Go Back",
"Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem",
"Failed to re-authenticate": "Failed to re-authenticate",
"Regain access to your account and recover encryption keys stored in this session. Without them, you wont be able to read all of your secure messages in any session.": "Regain access to your account and recover encryption keys stored in this session. Without them, you wont be able to read all of your secure messages in any session.",

View file

@ -111,7 +111,7 @@
"Someone": "Iu",
"(not supported by this browser)": "(nesubtenata de tiu ĉi foliumilo)",
"%(senderName)s answered the call.": "%(senderName)s akceptis la vokon.",
"(could not connect media)": "(aŭdvidaĵoj ne kunigeblis)",
"(could not connect media)": "(ne povis kunigi aŭdovidaĵojn)",
"(no answer)": "(sen respondo)",
"(unknown failure: %(reason)s)": "(nekonata eraro: %(reason)s)",
"%(senderName)s ended the call.": "%(senderName)s finis la vokon.",
@ -2210,5 +2210,69 @@
"Matrix rooms": "Ĉambroj de Matrix",
"Open an existing session & use it to verify this one, granting it access to encrypted messages.": "Malfermi jaman salutaĵon kaj kontroli ĉi tiun per ĝi, permesante al ĝi aliron al ĉifritaj mesaĝoj.",
"Waiting…": "Atendante…",
"If you cant access one, <button>use your recovery key or passphrase.</button>": "Se vi ne povas iun atingi, <button>uzu vian rehavan ŝlosilon aŭ pasfrazon.</button>"
"If you cant access one, <button>use your recovery key or passphrase.</button>": "Se vi ne povas iun atingi, <button>uzu vian rehavan ŝlosilon aŭ pasfrazon.</button>",
"Manually Verify by Text": "Permane kontroli tekste",
"Interactively verify by Emoji": "Interage kontroli bildosigne",
"Self signing private key:": "Memsubskriba privata ŝlosilo",
"cached locally": "kaŝmemorita loke",
"not found locally": "ne trovita loke",
"User signing private key:": "Uzantosubskriba privata ŝlosilo:",
"Secret Storage key format:": "Ŝlosila formo de sekreta deponejo:",
"outdated": "eksdata",
"up to date": "ĝisdata",
"Keyboard Shortcuts": "Klavkombinoj",
"Start a conversation with someone using their name, username (like <userId/>) or email address.": "Komencu interparolon kun iu per ĝia nomo, uzantonomo (kiel <userId/>), aŭ retpoŝtadreso.",
"a new master key signature": "nova ĉefŝlosila subskribo",
"a new cross-signing key signature": "nova transire subskriba ŝlosila subskribo",
"a device cross-signing signature": "aparata transire subskriba ŝlosila subskribo",
"a key signature": "ŝlosila subskribo",
"Riot encountered an error during upload of:": "Riot eraris dum alŝuto de:",
"Upload completed": "Alŝuto finiĝis",
"Cancelled signature upload": "Alŝuto de subskribo nuliĝis",
"Unabled to upload": "Ne povas alŝuti",
"Signature upload success": "Alŝuto de subskribo sukcesis",
"Signature upload failed": "Alŝuto de subskribo malsukcesis",
"Confirm by comparing the following with the User Settings in your other session:": "Konfirmu per komparo de la sekva kun la agardoj de uzanto en via alia salutaĵo:",
"Confirm this user's session by comparing the following with their User Settings:": "Konfirmu la salutaĵon de ĉi tiu uzanto per komparo de la sekva kun ĝiaj agordoj de uzanto",
"If they don't match, the security of your communication may be compromised.": "Se ili ne akordas, la sekureco de via komunikado eble estas rompita.",
"Navigation": "Navigado",
"Calls": "Vokoj",
"Room List": "Listo de ĉambroj",
"Autocomplete": "Memkompletigo",
"Alt": "Alt-klavo",
"Alt Gr": "Alt-Gr-klavo",
"Shift": "Majuskliga klavo",
"Super": "Super-klavo",
"Ctrl": "Stir-klavo",
"Toggle Bold": "Ŝalti grason",
"Toggle Italics": "Ŝalti kursivon",
"Toggle Quote": "Ŝalti citaĵon",
"New line": "Nova linio",
"Navigate recent messages to edit": "Navigi freŝajn mesaĝojn redaktotajn",
"Jump to start/end of the composer": "Salti al komenco/fino de la komponilo",
"Navigate composer history": "Navigi historion de la komponilo",
"Toggle microphone mute": "Baskuligi silentigon de mikrofono",
"Toggle video on/off": "Baskuligi filmojn",
"Jump to room search": "Salti al serĉo de ĉambroj",
"Navigate up/down in the room list": "Navigi supren/malsupren en la listo de ĉambroj",
"Select room from the room list": "Elekti ĉambron el la listo de ĉambroj",
"Collapse room list section": "Maletendi parton kun listo de ĉambroj",
"Expand room list section": "Etendi parton kun listo de ĉambroj",
"Clear room list filter field": "Vakigi filtrilon de la listo de ĉambroj",
"Scroll up/down in the timeline": "Rulumi supren/suben en la historio",
"Previous/next unread room or DM": "Antaŭa/sekva nelegita ĉambro",
"Previous/next room or DM": "Antaŭa/sekva ĉambro",
"Toggle the top left menu": "Baskuligi la supran maldekstran menuon",
"Close dialog or context menu": "Fermi interagujon aŭ kuntekstan menuon",
"Activate selected button": "Aktivigi la elektitan butonon",
"Toggle right panel": "Baskuligi la dekstran panelon",
"Toggle this dialog": "Baskuligi ĉi tiun interagujon",
"Move autocomplete selection up/down": "Movi memkompletigan elekton supren/suben",
"Cancel autocomplete": "Nuligi memkompletigon",
"Page Up": "Paĝosupren-klavo",
"Page Down": "Paĝosuben-klavo",
"Esc": "Eskapa klavo",
"Enter": "Eniga klavo",
"Space": "Spaco",
"End": "Finen-klavo"
}

View file

@ -2288,5 +2288,8 @@
"Navigate composer history": "Explorer lhistorique du compositeur",
"Previous/next unread room or DM": "Salon ou message direct non lu précédent/suivant",
"Previous/next room or DM": "Salon ou message direct précédent/suivant",
"Toggle right panel": "Afficher/masquer le panneau de droite"
"Toggle right panel": "Afficher/masquer le panneau de droite",
"Secret Storage key format:": "Format de clé du coffre secret :",
"outdated": "obsolète",
"up to date": "à jour"
}

View file

@ -2199,5 +2199,32 @@
"Error downloading theme information.": "A téma információk letöltése sikertelen.",
"Theme added!": "Téma hozzáadva!",
"Custom theme URL": "Egyedi téma URL",
"Add theme": "Téma hozzáadása"
"Add theme": "Téma hozzáadása",
"Review Sessions": "Munkamenetek átnézése",
"Manually Verify by Text": "Manuális szöveges ellenőrzés",
"Interactively verify by Emoji": "Közös ellenőrzés Emodzsival",
"Self signing private key:": "Titkos önaláíró kulcs:",
"cached locally": "helyben gyorsítótárazott",
"not found locally": "helyben nem található",
"User signing private key:": "Titkos felhasználó aláíró kulcs:",
"Secret Storage key format:": "Biztonsági tároló kulcs formátum:",
"outdated": "lejárt",
"up to date": "friss",
"Keyboard Shortcuts": "Billentyűzet kombinációk",
"Scroll to most recent messages": "A legfrissebb üzenethez görget",
"Local address": "Helyi cím",
"Published Addresses": "Nyilvánosságra hozott cím",
"Other published addresses:": "Másik nyilvánosságra hozott cím:",
"No other published addresses yet, add one below": "Nincs másik nyilvánosságra hozott cím, alább adj hozzá egyet",
"New published address (e.g. #alias:server)": "Új nyilvános cím (pl.: #becenév:szerver)",
"Local Addresses": "Helyi címek",
"Enter a server name": "Add meg a szerver nevét",
"Looks good": "Jól néz ki",
"Can't find this server or its room list": "A szerver vagy a szoba listája nem található",
"All rooms": "Minden szoba",
"Your server": "Matrix szervered",
"Are you sure you want to remove <b>%(serverName)s</b>": "Biztos, hogy törölni szeretnéd: <b>%(serverName)s</b>",
"Remove server": "Szerver törlése",
"Matrix": "Matrix",
"Add a new server": "Új szerver hozzáadása"
}

View file

@ -2276,5 +2276,17 @@
"Esc": "Esc",
"Enter": "Invio",
"Space": "Barra spaziatrice",
"End": "Fine"
"End": "Fine",
"Manually Verify by Text": "Verifica manualmente con testo",
"Interactively verify by Emoji": "Verifica interattivamente con emoji",
"Secret Storage key format:": "Formato chiave di archivio segreto:",
"outdated": "non aggiornato",
"up to date": "aggiornato",
"Confirm by comparing the following with the User Settings in your other session:": "Conferma confrontando il seguente con le impostazioni utente nell'altra sessione:",
"Confirm this user's session by comparing the following with their User Settings:": "Conferma questa sessione confrontando il seguente con le sue impostazioni utente:",
"If they don't match, the security of your communication may be compromised.": "Se non corrispondono, la sicurezza delle tue comunicazioni potrebbe essere compromessa.",
"Navigate composer history": "Naviga cronologia compositore",
"Previous/next unread room or DM": "Stanza o msg non letti successivi/precedenti",
"Previous/next room or DM": "Stanza o msg successivi/precedenti",
"Toggle right panel": "Apri/chiudi pannello a destra"
}

View file

@ -2265,5 +2265,20 @@
"Esc": "Esc",
"Enter": "Enter",
"Space": "Space",
"End": "End"
"End": "End",
"Manually Verify by Text": "Verifikojeni Dorazi përmes Teksti",
"Interactively verify by Emoji": "Verifikojeni në mënyrë ndërvepruese përmes Emoji-sh",
"Secret Storage key format:": "Format kyçesh Depozite të Fshehtë:",
"outdated": "e vjetruar",
"up to date": "e përditësuar",
"Start a conversation with someone using their name, username (like <userId/>) or email address.": "Nisni një bisedë me dikë duke përdorur emrin e tij, emrin e përdoruesit për të (bie fjala, <userId/>) ose adresë email.",
"Confirm by comparing the following with the User Settings in your other session:": "Ripohojeni duke krahasuar sa vijon me Rregullimet e Përdoruesit te sesioni juaj tjetër:",
"Confirm this user's session by comparing the following with their User Settings:": "Ripohojeni këtë sesion përdoruesi duke krahasuar sa vijon me Rregullimet e tij të Përdoruesit:",
"If they don't match, the security of your communication may be compromised.": "Nëse spërputhen, siguria e komunikimeve tuaja mund të jetë komprometuar.",
"Super": "Super",
"Navigate composer history": "Lëvizni nëpër historikun e hartuesit",
"Toggle video on/off": "Aktivizoni/çaktivizoni videon",
"Previous/next unread room or DM": "Dhoma ose MD i palexuar i mëparshëm/pasues",
"Previous/next room or DM": "Dhoma ose MD i mëparshëm/pasues",
"Toggle right panel": "Hap/mbyll panelin djathtas"
}

View file

@ -81,7 +81,7 @@
"Direct chats": "Doğrudan Sohbetler",
"Disable Notifications": "Bildirimleri Devre Dışı Bırak",
"Disinvite": "Daveti İptal Et",
"Displays action": "Görünür eylem",
"Displays action": "Eylemi görüntüler",
"Download %(text)s": "%(text)s metnini indir",
"Drop File Here": "Dosyayı Buraya Bırak",
"Ed25519 fingerprint": "Ed25519 parmak izi",
@ -117,7 +117,7 @@
"Failed to toggle moderator status": "Moderatör durumunu değiştirmek başarısız oldu",
"Failed to unban": "Yasağı kaldırmak başarısız oldu",
"Failed to upload profile picture!": "Profil resmi yükleme başarısız oldu!",
"Failed to verify email address: make sure you clicked the link in the email": "E-posta adresini doğrulama başarısız : e-postadaki bağlantıya tıkladığınızdan emin olun",
"Failed to verify email address: make sure you clicked the link in the email": "Eposta adresini doğrulamadı: epostadaki bağlantıya tıkladığınızdan emin olun",
"Failure to create room": "Oda oluşturulamadı",
"Favourite": "Favori",
"Favourites": "Favoriler",
@ -249,7 +249,7 @@
"Submit": "Gönder",
"Success": "Başarılı",
"The phone number entered looks invalid": "Girilen telefon numarası geçersiz görünüyor",
"This email address is already in use": "Bu e-posta adresi zaten kullanımda",
"This email address is already in use": "Bu eposta adresi zaten kullanımda",
"This email address was not found": "Bu e-posta adresi bulunamadı",
"The email address linked to your account must be entered.": "Hesabınıza bağlı e-posta adresi girilmelidir.",
"The remote side failed to pick up": "Uzak taraf toplanamadı(alınamadı)",
@ -538,15 +538,15 @@
"View Source": "Kaynağı Görüntüle",
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Geçerli tarayıcınız ile birlikte , uygulamanın görünüş ve kullanım hissi tamamen hatalı olabilir ve bazı ya da tüm özellikler çalışmayabilir. Yine de denemek isterseniz devam edebilirsiniz ancak karşılaşabileceğiniz sorunlar karşısında kendi başınasınız !",
"There are advanced notifications which are not shown here": "Burada gösterilmeyen gelişmiş bildirimler var",
"The platform you're on": "Bulunduğun platform",
"The platform you're on": "Platformunuz",
"The version of Riot.im": "Riot.im'in sürümü",
"Your language of choice": "Dil seçeneği",
"Which officially provided instance you are using, if any": "Hangi resmi destekli örneği(eğer varsa) kullanmaktasınız",
"Add Email Address": "E-posta Adresi Ekle",
"Your language of choice": "Dil seçiminiz",
"Which officially provided instance you are using, if any": "Hangi resmi destekli platformu kullanmaktasınız (eğer varsa)",
"Add Email Address": "Eposta Adresi Ekle",
"Add Phone Number": "Telefon Numarası Ekle",
"Your identity server's URL": "Kimlik sunucunuzun linki",
"e.g. %(exampleValue)s": "örn.%(exampleValue)s",
"Every page you use in the app": "uygulamadaki kullandığınız tüm sayfalar",
"Every page you use in the app": "Uygulamadaki kullandığınız tüm sayfalar",
"e.g. <CurrentPageURL>": "örn. <CurrentPageURL>",
"Your User Agent": "Kullanıcı Ajanınız",
"Your device resolution": "Cihazınızın çözünürlüğü",
@ -602,7 +602,7 @@
"%(names)s and %(count)s others are typing …|one": "%(names)s ve bir diğeri yazıyor…",
"%(names)s and %(lastPerson)s are typing …": "%(names)s ve %(lastPerson)s yazıyor…",
"Cannot reach homeserver": "Ana sunucuya erişilemiyor",
"Your Riot is misconfigured": "Rioutunuz hatalı yapılandırılmış",
"Your Riot is misconfigured": "Riot hatalı ayarlanmış",
"Cannot reach identity server": "Kimlik sunucu erişilemiyor",
"No homeserver URL provided": "Ana sunucu adresi belirtilmemiş",
"Unexpected error resolving homeserver configuration": "Ana sunucu yapılandırması çözümlenirken beklenmeyen hata",
@ -643,7 +643,7 @@
"Power level": "Güç düzeyi",
"e.g. my-room": "örn. odam",
"Some characters not allowed": "Bazı karakterlere izin verilmiyor",
"Matrix ID": "Matrix ID",
"Matrix ID": "Matrix Kimliği",
"Matrix Room ID": "Matrix Oda ID",
"email address": "e-posta adresi",
"That doesn't look like a valid email address": "Geçerli bir e-posta adresi gibi gözükmüyor",
@ -839,7 +839,7 @@
"Set a new password": "Yeni bir şifre belirle",
"General failure": "Genel başarısızlık",
"This homeserver does not support login using email address.": "Bu ana sunucu e-posta adresiyle oturum açmayı desteklemiyor.",
"This account has been deactivated.": "Bu hesap pasifleştirilmiş.",
"This account has been deactivated.": "Hesap devre dışı bırakıldı.",
"Create account": "Yeni hesap",
"Unable to query for supported registration methods.": "Desteklenen kayıt yöntemleri için sorgulama yapılamıyor.",
"Continue with previous account": "Önceki hesapla devam et",
@ -848,8 +848,8 @@
"Create your account": "Hesabınızı oluşturun",
"Forgotten your password?": "Parolanızı mı unuttunuz?",
"Sign in and regain access to your account.": "Oturum açın ve yeniden hesabınıza ulaşın.",
"Whether or not you're logged in (we don't record your username)": "İster oturum açın yasa açmayın (biz kullanıcı adınızı kaydetmiyoruz)",
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Zengin Metin Düzenleyicinin Zengin metin modunu kullanıyor ya da kullanmıyorsunuz",
"Whether or not you're logged in (we don't record your username)": "İster oturum açın yada açmayın (biz kullanıcı adınızı kaydetmiyoruz)",
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Zengin Metin Düzenleyicisinin Zengin metin modunu kullanıyor ya da kullanmıyorsunuz",
"Your homeserver's URL": "Ana sunucunuzun URLi",
"The information being sent to us to help make Riot.im better includes:": "Riot.im i daha iyi yapmamıza yardımcı olacak bize gönderdiğiniz bilgilerin içeriği:",
"Try using turn.matrix.org": "turn.matrix.org i kullanarak dene",
@ -983,7 +983,7 @@
"Cat": "Kedi",
"Lion": "Aslan",
"Horse": "At",
"Unicorn": "Tek boynuzlu at",
"Unicorn": "Midilli",
"Pig": "Domuz",
"Elephant": "Fil",
"Rabbit": "Tavşan",
@ -1230,7 +1230,7 @@
"Failed to copy": "Kopyalama başarısız",
"edited": "düzenlendi",
"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.",
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "Kimlik sunucusu üzerinde hala <b>kişisel veri paylaşımı</b> yapıyorsunuz \n<idserver />.",
"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 parolası belirle...",
"Deactivating your account is a permanent action - be careful!": "Hesabınızı pasifleştirmek bir kalıcı eylemdir - dikkat edin!",
@ -1363,7 +1363,7 @@
"Members only (since the point in time of selecting this option)": "Sadece üyeler ( bu seçeneği seçtiğinizden itibaren)",
"Unable to revoke sharing for email address": "E-posta adresi paylaşımı kaldırılamadı",
"Unable to revoke sharing for phone number": "Telefon numarası paylaşımı kaldırılamıyor",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Bu sayfadaki oda, kullanıcı veya grup ID si gibi betimleyici bilgiler sunucuya gönderilmeden önce silindi.",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Bu sayfadaki oda, kullanıcı veya grup kimliği gibi betimleyici bilgiler sunucuya gönderilmeden önce silindi.",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Çağrıların sağlıklı bir şekide yapılabilmesi için lütfen anasunucunuzun (<code>%(homeserverDomain)s</code>) yöneticisinden bir TURN sunucusu yapılandırmasını isteyin.",
"%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s kullanıcıları isimlerini %(count)s kez değiştirdiler",
"%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s ismini %(count)s kez değiştirdi",
@ -1538,7 +1538,7 @@
"Some sessions for this user are not trusted": "Bu kullanıcı için bazı oturumlar güvenilir değil",
"All sessions for this user are trusted": "Bu kullanıcı için tüm oturumlar güvenilir",
"The version of Riot": "Riot sürümü",
"Your user agent": "Kullanıcı ajanınız",
"Your user agent": "Kullanıcı aracınız",
"If you cancel now, you won't complete verifying the other user.": "Şimdi iptal ederseniz, diğer kullanıcıyı doğrulamayı tamamlamış olmayacaksınız.",
"If you cancel now, you won't complete verifying your other session.": "Şimdi iptal ederseniz, diğer oturumu doğrulamış olmayacaksınız.",
"Setting up keys": "Anahtarları ayarla",
@ -1752,5 +1752,79 @@
"Failed to re-authenticate due to a homeserver problem": "Anasunucu problemi yüzünden yeniden kimlik doğrulama başarısız",
"Failed to re-authenticate": "Yeniden kimlik doğrulama başarısız",
"A new recovery passphrase and key for Secure Messages have been detected.": "Yeni bir kurtarma parolası ve Güvenli Mesajlar için anahtar tespit edildi.",
"Not currently indexing messages for any room.": "Şu an hiç bir odada mesaj indeksleme yapılmıyor."
"Not currently indexing messages for any room.": "Şu an hiç bir odada mesaj indeksleme yapılmıyor.",
"Whether you're using Riot on a device where touch is the primary input mechanism": "Riot'u ana giriş yöntemi dokunma olan bir cihazda kullanıyor olsanızda",
"Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "'Breadcrumbs' özelliğini kullanıp kullanmadığınız (oda listesi üzerinde avatarlar)",
"Whether you're using Riot as an installed Progressive Web App": "Riot'u gelişmiş web uygulaması olarak yükleyip yüklemediğinizi",
"The information being sent to us to help make Riot better includes:": "Riot'u geliştirmemizde bize yardım etmek için gönderdiğiniz bilgiler şunları içeriyor:",
"A call is currently being placed!": "Bir çağrı şu anda başlatılıyor!",
"At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Şu anda dosya ile birlikte mesaj yollamak mümkün değil. Dosyayı mesajsız yüklemek ister misiniz?",
"PM": "24:00",
"AM": "12:00",
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Bu eylem, bir e-posta adresini veya telefon numarasını doğrulamak için varsayılan kimlik sunucusuna <server /> erişilmesini gerektirir, ancak sunucunun herhangi bir hizmet şartı yoktur.",
"Sends a message as plain text, without interpreting it as markdown": "Mesajı markdown kullanmadan basit metin olarak iletir",
"Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "E-posta ile davet etmek için kimlik sunucusu kullan. Varsayılan kimlik sunucusunu (%(defaultIdentityServerName)s) kullanmak için devam edin ya da ayarlardan değiştirin.",
"Unignored user": "Reddedilmemiş kullanıcı",
"WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "UYARI: ANAHTAR DOĞRULAMASI BAŞARISIZ! %(userld)s'nin/nın %(deviceld)s oturumu için imza anahtarı \"%(fprint)s\" verilen anahtar ile uyumsuz \"%(fingerprint)s\". Bu iletişiminizin engellendiği anlamına gelebilir!",
"The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "Verilen imza anahtarı %(userld)s'nin/nın %(deviceld)s oturumundan gelen anahtar ile uyumlu. Oturum doğrulanmış olarak işaretlendi.",
"Forces the current outbound group session in an encrypted room to be discarded": "Şifreli bir odadaki geçerli giden grup oturumunun atılmasını zorlar",
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s oda ismini %(oldRoomName)s bununla değiştirdi %(newRoomName)s.",
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s bu odada %(groups)s için etiketleri etkinleştirdi.",
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s bu odada %(groups)s için etiketleri devre dışı bıraktı.",
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s bu odada etiketleri %(newGroups)s için etkinleştirdi ve %(oldGroups)s için devre dışı bıraktı.",
"%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s bu oda için alternatif adres %(addresses)s ekledi.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s bu oda için alternatif adresleri %(addresses)s sildi.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s bu oda için alternatif adresi %(addresses)s sildi.",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s, %(targetDisplayName)s'nin odaya katılması için daveti iptal etti.",
"%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s %(glob)s ile eşleşen kullanıcıları banlama kuralını kaldırdı",
"%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s %(glob)s ile eşleşen odaları banlama kuralını kaldırdı",
"%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s %(glob)s ile eşleşen sunucuları banlama kuralını kaldırdı",
"%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s %(glob)s ile eşleşen banlama kuralını kaldırdı",
"%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s %(glob)s ile eşleşen kullanıcıları banlama kuralını bu sebepten dolayı güncelledi %(reason)s",
"%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s %(glob)s ile eşleşen odaları banlama kuralını bu sebepten dolayı güncelledi %(reason)s",
"%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s %(glob)s ile eşleşen sunucuları banlama kuralını bu sebepten dolayı güncelledi %(reason)s",
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s %(oldGlob)s ile eşleşen kullanıcıları banlama kuralını %(newGlob)s ile eşleşen olarak değiştirdi sebebi %(reason)s",
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s %(oldGlob)s ile eşleşen odaları banlama kuralını %(newGlob)s ile eşleşen olarak değiştirdi sebebi %(reason)s",
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s %(oldGlob)s ile eşleşen sunucuları banlama kuralını %(newGlob)s ile eşleşen olarak değiştirdi sebebi %(reason)s",
"%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s %(oldGlob)s ile eşleşen banlama kuralını %(newGlob)s ile eşleşen olarak değiştirdi sebebi %(reason)s",
"%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) yeni oturuma doğrulamadan giriş yaptı:",
"Ask this user to verify their session, or manually verify it below.": "Kullanıcıya oturumunu doğrulamasını söyle, ya da aşağıdan doğrula.",
"Manually Verify by Text": "Metin ile Doğrula",
"Interactively verify by Emoji": "Emoji ile etkileşimli olarak doğrula",
"Use a longer keyboard pattern with more turns": "Daha karmaşık ve uzun bir klavye deseni kullan",
"Predictable substitutions like '@' instead of 'a' don't help very much": "Tahmin edilebilir harf değişimleri örneğin 'a' yerine '@' pek yardımcı olmuyor",
"A word by itself is easy to guess": "Kelime zaten kolay tahmin edilir",
"Straight rows of keys are easy to guess": "Aynı klavye satırındaki ardışık tuşlar kolay tahmin edilir",
"Short keyboard patterns are easy to guess": "Kısa klavye desenleri kolay tahmin edilir",
"Show a presence dot next to DMs in the room list": "Oda listesinde DM'lerin yanında varlık noktası göster",
"Support adding custom themes": "Özel tema eklemeyi destekle",
"Enable cross-signing to verify per-user instead of per-session (in development)": "Oturum başına doğrulamak yerine kullanıcı başına doğrulama yapmak için çapraz giriş yapmayı etkinleştir (geliştirmede)",
"Show padlocks on invite only rooms": "Sadece davetle girilen odalarda kilit işareti göster",
"Show read receipts sent by other users": "Diğer kullanıcılar tarafından gönderilen okundu bilgisini göster",
"Show a reminder to enable Secure Message Recovery in encrypted rooms": "Şifrelenmiş odalarda güvenli mesaj kurtarmayı etkinleştirmek için hatırlatıcı göster",
"Enable automatic language detection for syntax highlighting": "Sözdizimi vurgularken otomatik dil algılamayı etkinleştir",
"Show avatars in user and room mentions": "Kullanıcı veya odadan bahsedilirken avatarlarını göster",
"Automatically replace plain text Emoji": "Düz metini otomatik olarak emoji ile değiştir",
"Never send encrypted messages to unverified sessions from this session": "Şifreli mesajları asla bu oturumdaki doğrulanmamış oturumlara iletme",
"Never send encrypted messages to unverified sessions in this room from this session": "Şifreli mesajları asla oturumdaki bu odadaki doğrulanmamış oturumlara iletme",
"Prompt before sending invites to potentially invalid matrix IDs": "Potansiyel olarak geçersiz matrix kimliği olanlara davet gönderirken uyarı ver",
"Show shortcuts to recently viewed rooms above the room list": "Oda listesinin üzerinde en son kullanılan odaları göster",
"Secret Storage key format:": "Sır Depolama anahtar biçemi:",
"Error downloading theme information.": "Tema bilgisi indirilirken hata.",
"Theme added!": "Tema eklendi!",
"Add theme": "Tema ekle",
"Keyboard Shortcuts": "Klavye Kısayolları",
"%(count)s unread messages including mentions.|other": "anmalar dahil okunmayan %(count)s mesaj.",
"Local address": "Yerel adres",
"Local Addresses": "Yerel Adresler",
"Yours, or the other users session": "Sizin yada diğer kullanıcıların oturumları",
"Trusted": "Güvenilir",
"Not trusted": "Güvenilir değil",
"Hide sessions": "Oturumları gizle",
"Verify by scanning": "Taramayla doğrula",
"Verify by emoji": "Emojiyle doğrula",
"Verify by comparing unique emoji.": "Eşsiz emoji eşleştirme ile doğrulama.",
"Edited at %(date)s. Click to view edits.": "%(date)s tarihinde düzenlendi. Düzenlemeleri görmek için tıkla.",
"Failed to load group members": "Grup üyeleri yüklenirken başarısız",
"Visibility in Room List": "Oda Listesindeki Görünürlük"
}

View file

@ -2287,5 +2287,12 @@
"Navigate composer history": "瀏覽編輯區歷史紀錄",
"Previous/next unread room or DM": "上一下/下一個未讀聊天室或直接訊息",
"Previous/next room or DM": "上一個/下一個聊天室或直接訊息",
"Toggle right panel": "切換右側面板"
"Toggle right panel": "切換右側面板",
"Secret Storage key format:": "秘密儲存金鑰格式:",
"outdated": "太舊了",
"up to date": "已為最新",
"Self signing private key:": "自行簽章私鑰:",
"cached locally": "本機快取",
"not found locally": "在本機找不到",
"User signing private key:": "使用者簽章私鑰:"
}

View file

@ -200,13 +200,17 @@ matrixLinkify.options = {
switch (type) {
case "url": {
// intercept local permalinks to users and show them like userids (in userinfo of current room)
const permalink = parsePermalink(href);
if (permalink && permalink.userId) {
return {
click: function(e) {
matrixLinkify.onUserClick(e, permalink.userId);
},
};
try {
const permalink = parsePermalink(href);
if (permalink && permalink.userId) {
return {
click: function(e) {
matrixLinkify.onUserClick(e, permalink.userId);
},
};
}
} catch (e) {
// OK fine, it's not actually a permalink
}
break;
}

View file

@ -118,6 +118,10 @@ export default async function sendBugReport(bugReportEndpoint, opts) {
try {
body.append("storageManager_persisted", await navigator.storage.persisted());
} catch (e) {}
} else if (document.hasStorageAccess) { // Safari
try {
body.append("storageManager_persisted", await document.hasStorageAccess());
} catch (e) {}
}
if (navigator.storage && navigator.storage.estimate) {
try {

View file

@ -16,6 +16,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {MatrixClient} from 'matrix-js-sdk';
import {_td} from '../languageHandler';
import {
AudioNotificationsEnabledController,
@ -24,6 +26,7 @@ import {
} from "./controllers/NotificationControllers";
import CustomStatusController from "./controllers/CustomStatusController";
import ThemeController from './controllers/ThemeController';
import PushToMatrixClientController from './controllers/PushToMatrixClientController';
import ReloadOnChangeController from "./controllers/ReloadOnChangeController";
import {RIGHT_PANEL_PHASES} from "../stores/RightPanelStorePhases";
@ -525,4 +528,12 @@ export const SETTINGS = {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
default: true,
},
"e2ee.manuallyVerifyAllSessions": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td("Manually verify all remote sessions"),
default: false,
controller: new PushToMatrixClientController(
MatrixClient.prototype.setCryptoTrustCrossSignedDevices, true,
),
},
};

View file

@ -0,0 +1,37 @@
/*
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';
/**
* When the value changes, call a setter function on the matrix client with the new value
*/
export default class PushToMatrixClientController {
constructor(setter, inverse) {
this._setter = setter;
this._inverse = inverse;
}
getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) {
return null; // no override
}
onChange(level, roomId, newValue) {
// XXX does this work? This surely isn't necessarily the effective value,
// but it's what NotificationsEnabledController does...
this._setter.call(MatrixClientPeg.get(), this._inverse ? !newValue : newValue);
}
}

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import dis from '../dispatcher';
import {pendingVerificationRequestForUser} from '../verification';
import {Store} from 'flux/utils';
import SettingsStore, {SettingLevel} from "../settings/SettingsStore";
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "./RightPanelStorePhases";
@ -135,7 +136,20 @@ export default class RightPanelStore extends Store {
break;
case 'set_right_panel_phase': {
const targetPhase = payload.phase;
let targetPhase = payload.phase;
let refireParams = payload.refireParams;
// redirect to EncryptionPanel if there is an ongoing verification request
if (targetPhase === RIGHT_PANEL_PHASES.RoomMemberInfo) {
const {member} = payload.refireParams;
const pendingRequest = pendingVerificationRequestForUser(member);
if (pendingRequest) {
targetPhase = RIGHT_PANEL_PHASES.EncryptionPanel;
refireParams = {
verificationRequest: pendingRequest,
member,
};
}
}
if (!RIGHT_PANEL_PHASES[targetPhase]) {
console.warn(`Tried to switch right panel to unknown phase: ${targetPhase}`);
return;
@ -153,7 +167,7 @@ export default class RightPanelStore extends Store {
});
}
} else {
if (targetPhase === this._state.lastRoomPhase && !payload.refireParams) {
if (targetPhase === this._state.lastRoomPhase && !refireParams) {
this._setState({
showRoomPanel: !this._state.showRoomPanel,
});
@ -161,7 +175,7 @@ export default class RightPanelStore extends Store {
this._setState({
lastRoomPhase: targetPhase,
showRoomPanel: true,
lastRoomPhaseParams: payload.refireParams || {},
lastRoomPhaseParams: refireParams || {},
});
}
}
@ -170,7 +184,7 @@ export default class RightPanelStore extends Store {
dis.dispatch({
action: 'after_right_panel_phase_change',
phase: targetPhase,
...(payload.refireParams || {}),
...(refireParams || {}),
});
break;
}

View file

@ -0,0 +1,144 @@
/*
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';
import { MatrixClientPeg } from '../MatrixClientPeg';
import { accessSecretStorage, AccessCancelledError } from '../CrossSigningManager';
export const PHASE_INTRO = 0;
export const PHASE_BUSY = 1;
export const PHASE_DONE = 2; //final done stage, but still showing UX
export const PHASE_CONFIRM_SKIP = 3;
export const PHASE_FINISHED = 4; //UX can be closed
export class SetupEncryptionStore extends EventEmitter {
static sharedInstance() {
if (!global.mx_SetupEncryptionStore) global.mx_SetupEncryptionStore = new SetupEncryptionStore();
return global.mx_SetupEncryptionStore;
}
start() {
if (this._started) {
return;
}
this._started = true;
this.phase = PHASE_INTRO;
this.verificationRequest = null;
this.backupInfo = null;
MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest);
}
stop() {
if (!this._started) {
return;
}
this._started = false;
if (this.verificationRequest) {
this.verificationRequest.off("change", this.onVerificationRequestChange);
}
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest);
}
}
async usePassPhrase() {
this.phase = PHASE_BUSY;
this.emit("update");
const cli = MatrixClientPeg.get();
try {
const backupInfo = await cli.getKeyBackupVersion();
this.backupInfo = backupInfo;
this.emit("update");
// The control flow is fairly twisted here...
// For the purposes of completing security, we only wait on getting
// as far as the trust check and then show a green shield.
// We also begin the key backup restore as well, which we're
// awaiting inside `accessSecretStorage` only so that it keeps your
// passphase cached for that work. This dialog itself will only wait
// on the first trust check, and the key backup restore will happen
// in the background.
await new Promise((resolve, reject) => {
try {
accessSecretStorage(async () => {
await cli.checkOwnCrossSigningTrust();
resolve();
if (backupInfo) {
// A complete restore can take many minutes for large
// accounts / slow servers, so we allow the dialog
// to advance before this.
await cli.restoreKeyBackupWithSecretStorage(backupInfo);
}
}).catch(reject);
} catch (e) {
console.error(e);
reject(e);
}
});
if (cli.getCrossSigningId()) {
this.phase = PHASE_DONE;
this.emit("update");
}
} catch (e) {
if (!(e instanceof AccessCancelledError)) {
console.log(e);
}
// this will throw if the user hits cancel, so ignore
this.phase = PHASE_INTRO;
this.emit("update");
}
}
onVerificationRequest = async (request) => {
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
if (this.verificationRequest) {
this.verificationRequest.off("change", this.onVerificationRequestChange);
}
this.verificationRequest = request;
await request.accept();
request.on("change", this.onVerificationRequestChange);
this.emit("update");
}
onVerificationRequestChange = () => {
if (this.verificationRequest.cancelled) {
this.verificationRequest.off("change", this.onVerificationRequestChange);
this.verificationRequest = null;
this.emit("update");
}
}
skip() {
this.phase = PHASE_CONFIRM_SKIP;
this.emit("update");
}
skipConfirm() {
this.phase = PHASE_FINISHED;
this.emit("update");
}
returnAfterSkip() {
this.phase = PHASE_INTRO;
this.emit("update");
}
done() {
this.phase = PHASE_FINISHED;
this.emit("update");
}
}

View file

@ -48,6 +48,11 @@ export function tryPersistStorage() {
navigator.storage.persist().then(persistent => {
console.log("StorageManager: Persistent?", persistent);
});
} else if (document.requestStorageAccess) { // Safari
document.requestStorageAccess().then(
() => console.log("StorageManager: Persistent?", true),
() => console.log("StorageManager: Persistent?", false),
);
} else {
console.log("StorageManager: Persistence unsupported");
}

View file

@ -28,6 +28,7 @@ const WIDGET_WAIT_TIME = 20000;
import SettingsStore from "../settings/SettingsStore";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
import {IntegrationManagers} from "../integrations/IntegrationManagers";
import {Capability} from "../widgets/WidgetApi";
/**
* Encodes a URI according to a set of template variables. Variables will be
@ -454,12 +455,15 @@ export default class WidgetUtils {
static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
const capWhitelist = enableScreenshots ? ["m.capability.screenshot"] : [];
const capWhitelist = enableScreenshots ? [Capability.Screenshot] : [];
// Obviously anyone that can add a widget can claim it's a jitsi widget,
// so this doesn't really offer much over the set of domains we load
// widgets from at all, but it probably makes sense for sanity.
if (appType == 'jitsi') capWhitelist.push("m.always_on_screen");
if (appType === 'jitsi') {
capWhitelist.push(Capability.AlwaysOnScreen);
capWhitelist.push(Capability.GetRiotWebConfig);
}
return capWhitelist;
}

View file

@ -111,12 +111,7 @@ export async function verifyUser(user) {
if (!await enable4SIfNeeded()) {
return;
}
const cli = MatrixClientPeg.get();
const dmRoom = findDMForUser(cli, user.userId);
let existingRequest;
if (dmRoom) {
existingRequest = cli.findVerificationRequestDMInProgress(dmRoom.roomId);
}
const existingRequest = pendingVerificationRequestForUser(user);
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
@ -126,3 +121,11 @@ export async function verifyUser(user) {
},
});
}
export function pendingVerificationRequestForUser(user) {
const cli = MatrixClientPeg.get();
const dmRoom = findDMForUser(cli, user.userId);
if (dmRoom) {
return cli.findVerificationRequestDMInProgress(dmRoom.roomId);
}
}

View file

@ -23,6 +23,7 @@ export enum Capability {
Screenshot = "m.capability.screenshot",
Sticker = "m.sticker",
AlwaysOnScreen = "m.always_on_screen",
GetRiotWebConfig = "im.vector.web.riot_config",
}
export enum KnownWidgetActions {
@ -33,7 +34,10 @@ export enum KnownWidgetActions {
UpdateVisibility = "visibility",
ReceiveOpenIDCredentials = "openid_credentials",
SetAlwaysOnScreen = "set_always_on_screen",
GetRiotWebConfig = "im.vector.web.riot_config",
ClientReady = "im.vector.ready",
}
export type WidgetAction = KnownWidgetActions | string;
export enum WidgetApiType {
@ -63,10 +67,15 @@ export interface FromWidgetRequest extends WidgetRequest {
*/
export class WidgetApi {
private origin: string;
private inFlightRequests: {[requestId: string]: (reply: FromWidgetRequest) => void} = {};
private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
private readyPromise: Promise<any>;
private readyPromiseResolve: () => void;
/**
* Set this to true if your widget is expecting a ready message from the client. False otherwise (default).
*/
public expectingExplicitReady = false;
constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) {
this.origin = new URL(currentUrl).origin;
@ -83,7 +92,14 @@ export class WidgetApi {
if (payload.action === KnownWidgetActions.GetCapabilities) {
this.onCapabilitiesRequest(<ToWidgetRequest>payload);
if (!this.expectingExplicitReady) {
this.readyPromiseResolve();
}
} else if (payload.action === KnownWidgetActions.ClientReady) {
this.readyPromiseResolve();
// Automatically acknowledge so we can move on
this.replyToRequest(<ToWidgetRequest>payload, {});
} else {
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
}
@ -126,7 +142,10 @@ export class WidgetApi {
data: payload,
response: {}, // Not used at this layer - it's used when the client responds
};
this.inFlightRequests[request.requestId] = callback;
if (callback) {
this.inFlightRequests[request.requestId] = callback;
}
console.log(`[WidgetAPI] Sending request: `, request);
window.parent.postMessage(request, "*");
@ -134,7 +153,16 @@ export class WidgetApi {
public setAlwaysOnScreen(onScreen: boolean): Promise<any> {
return new Promise<any>(resolve => {
this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, resolve);
this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, null);
resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change.
});
}
public getRiotConfig(): Promise<any> {
return new Promise<any>(resolve => {
this.callAction(KnownWidgetActions.GetRiotWebConfig, {}, response => {
resolve(response.response.config);
});
});
}
}