Merge branch 'develop' into thobyv/make-filepanel-remember-scroll-position

This commit is contained in:
Travis Ralston 2020-04-17 15:10:12 -06:00
commit d910eb4d57
139 changed files with 4219 additions and 1816 deletions

View file

@ -89,7 +89,6 @@
"qrcode-react": "^0.1.16",
"qs": "^6.6.0",
"react": "^16.9.0",
"react-addons-css-transition-group": "15.6.2",
"react-beautiful-dnd": "^4.0.1",
"react-dom": "^16.9.0",
"react-focus-lock": "^2.2.1",

View file

@ -94,6 +94,7 @@
@import "./views/elements/_AccessibleButton.scss";
@import "./views/elements/_AddressSelector.scss";
@import "./views/elements/_AddressTile.scss";
@import "./views/elements/_ButtonPlaceholder.scss";
@import "./views/elements/_DirectorySearchBox.scss";
@import "./views/elements/_Dropdown.scss";
@import "./views/elements/_EditableItemList.scss";
@ -133,6 +134,7 @@
@import "./views/messages/_MNoticeBody.scss";
@import "./views/messages/_MStickerBody.scss";
@import "./views/messages/_MTextBody.scss";
@import "./views/messages/_MVideoBody.scss";
@import "./views/messages/_MessageActionBar.scss";
@import "./views/messages/_MessageTimestamp.scss";
@import "./views/messages/_MjolnirBody.scss";

View file

@ -23,3 +23,84 @@ limitations under the License.
margin-left: auto;
margin-right: auto;
}
.mx_HomePage_default {
text-align: center;
.mx_HomePage_default_wrapper {
padding: 25vh 0 12px;
}
img {
height: 48px;
}
h1 {
font-weight: 600;
font-size: $font-32px;
line-height: $font-44px;
margin-bottom: 4px;
}
h4 {
margin-top: 4px;
font-weight: 600;
font-size: $font-18px;
line-height: $font-25px;
color: $muted-fg-color;
}
.mx_HomePage_default_buttons {
margin: 80px auto 0;
width: fit-content;
.mx_AccessibleButton {
padding: 73px 8px 15px; // top: 20px top padding + 40px icon + 13px margin
width: 104px; // 120px - 2* 8px
margin: 0 39px; // 55px - 2* 8px
position: relative;
display: inline-block;
border-radius: 8px;
vertical-align: top;
word-break: break-word;
font-weight: 600;
font-size: $font-15px;
line-height: $font-20px;
color: $muted-fg-color;
&:hover {
color: $accent-color;
background: rgba(#03b381, 0.06);
&::before {
background-color: $accent-color;
}
}
&::before {
top: 20px;
left: 40px; // (120px-40px)/2
width: 40px;
height: 40px;
content: '';
position: absolute;
background-color: $muted-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
}
&.mx_HomePage_button_sendDm::before {
mask-image: url('$(res)/img/feather-customised/message-circle.svg');
}
&.mx_HomePage_button_explore::before {
mask-image: url('$(res)/img/feather-customised/explore.svg');
}
&.mx_HomePage_button_createGroup::before {
mask-image: url('$(res)/img/feather-customised/group.svg');
}
}
}
}

View file

@ -44,6 +44,7 @@ limitations under the License.
.mx_CompleteSecurity_actionRow {
display: flex;
justify-content: flex-end;
margin-top: $font-28px;
.mx_AccessibleButton {
margin-inline-start: 18px;

View file

@ -89,3 +89,13 @@ limitations under the License.
.mx_Login_underlinedServerName {
border-bottom: 1px dashed $accent-color;
}
div.mx_AccessibleButton_kind_link.mx_Login_forgot {
// style it as a link
font-size: inherit;
padding: 0;
&.mx_AccessibleButton_disabled {
cursor: not-allowed;
}
}

View file

@ -119,6 +119,24 @@ limitations under the License.
margin-right: 0;
}
.mx_AuthBody_paddedFooter {
height: 80px; // height of the submit button + register link
padding-top: 28px;
text-align: center;
.mx_AuthBody_paddedFooter_title {
margin-top: 16px;
font-size: $font-15px;
line-height: $font-24px;
}
.mx_AuthBody_paddedFooter_subtitle {
margin-top: 8px;
font-size: $font-10px;
line-height: $font-14px;
}
}
.mx_AuthBody_changeFlow {
display: block;
text-align: center;

View file

@ -19,6 +19,7 @@ limitations under the License.
}
.mx_MessageContextMenu_field {
display: block;
padding: 3px 6px 3px 6px;
cursor: pointer;
white-space: nowrap;

View file

@ -15,6 +15,8 @@ limitations under the License.
*/
.mx_CreateRoomDialog_details {
margin-top: 15px;
.mx_CreateRoomDialog_details_summary {
outline: none;
list-style: none;
@ -71,11 +73,19 @@ limitations under the License.
}
.mx_CreateRoomDialog {
&.mx_Dialog_fixedWidth {
width: 450px;
}
.mx_Dialog_content {
margin-bottom: 40px;
}
p,
.mx_Field_input label {
color: $muted-fg-color;
}
.mx_SettingsFlag {
display: flex;
}
@ -90,5 +100,18 @@ limitations under the License.
flex: 0 0 auto;
margin-left: 30px;
}
.mx_CreateRoomDialog_topic {
margin-bottom: 36px;
}
.mx_Dialog_content > .mx_SettingsFlag {
margin-top: 24px;
}
p {
margin: 0 85px 0 0;
font-size: $font-12px;
}
}

View file

@ -35,6 +35,8 @@ limitations under the License.
border-radius: 4px;
border: 1px solid $dialog-close-fg-color;
background-color: $primary-bg-color;
max-height: calc(100vh - 20px); // allow 10px padding on both top and bottom
overflow-y: auto;
}
.mx_NetworkDropdown_menu_network {

View file

@ -0,0 +1,24 @@
/*
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_ButtonPlaceholder {
font-size: $font-14px;
font-weight: 600;
padding: 7px 18px;
display: inline-block;
text-align: center;
color: $authpage-secondary-color;
}

View file

@ -33,6 +33,10 @@ limitations under the License.
user-select: none;
}
.mx_Dropdown_input.mx_AccessibleButton_disabled {
cursor: not-allowed;
}
.mx_Dropdown_input:focus {
border-color: $input-focused-border-color;
}

View file

@ -14,8 +14,11 @@
}
a.mx_Pill {
word-break: break-all;
display: inline;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
vertical-align: text-bottom;
max-width: calc(100% - 1ch);
}
/* More specific to override `.markdown-body a` text-decoration */

View file

@ -0,0 +1,22 @@
/*
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.
*/
span.mx_MVideoBody {
video.mx_MVideoBody {
max-width: 100%;
height: auto;
}
}

View file

@ -34,12 +34,17 @@ limitations under the License.
background-color: $reaction-row-button-selected-bg-color;
border-color: $reaction-row-button-selected-border-color;
}
}
.mx_ReactionsRowButton_content {
// ignore mouse events for all children, treat it as one entire hoverable entity
* {
pointer-events: none;
}
.mx_ReactionsRowButton_content {
max-width: 100px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding-right: 4px;
}
}

View file

@ -44,6 +44,7 @@ limitations under the License.
outline: none;
overflow-x: hidden;
&.mx_BasicMessageComposer_input_shouldShowPillAvatar {
span.mx_UserPill, span.mx_RoomPill {
padding-left: 21px;
position: relative;
@ -68,6 +69,7 @@ limitations under the License.
}
}
}
}
.mx_BasicMessageComposer_AutoCompleteWrapper {
position: relative;

View file

@ -111,6 +111,7 @@ limitations under the License.
}
.mx_EventTile_line, .mx_EventTile_reply {
clear: both;
position: relative;
padding-left: 65px; /* left gutter */
padding-top: 4px;
@ -660,3 +661,23 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
}
}
}
.mx_EventTile_tileError {
color: red;
text-align: center;
// Remove some of the default tile padding so that the error is centered
margin-right: 0;
.mx_EventTile_line {
padding-left: 0;
margin-right: 0;
}
.mx_EventTile_line span {
padding: 4px 8px;
}
a {
margin-left: 1em;
}
}

View file

@ -24,20 +24,25 @@ limitations under the License.
margin: 0;
padding: 0 8px 0 10px;
position: relative;
}
.mx_RoomTile:focus {
filter: none !important;
background-color: $roomtile-focused-bg-color;
}
.mx_RoomTile_menuButton {
.mx_RoomTile_menuButton {
display: none;
flex: 0 0 16px;
height: 16px;
background-image: url('$(res)/img/icon_context.svg');
background-repeat: no-repeat;
background-position: center;
}
.mx_UserOnlineDot {
display: block;
margin-right: 5px;
}
}
.mx_RoomTile:focus {
filter: none !important;
background-color: $roomtile-focused-bg-color;
}
.mx_RoomTile_tooltip {
@ -151,7 +156,10 @@ limitations under the License.
}
.mx_RoomTile_menuButton {
display: none; //no design for this for now
display: none; // no design for this for now
}
.mx_UserOnlineDot {
display: none; // no design for this for now
}
}
@ -164,6 +172,9 @@ limitations under the License.
.mx_RoomTile_menuButton {
display: block;
}
.mx_UserOnlineDot {
display: none;
}
}
.mx_RoomTile_unreadNotify .mx_RoomTile_badge,

View file

@ -17,7 +17,7 @@ limitations under the License.
.mx_UserOnlineDot {
border-radius: 50%;
background-color: $accent-color;
height: 5px;
width: 5px;
height: 6px;
width: 6px;
display: inline-block;
}

View file

@ -61,3 +61,21 @@ limitations under the License.
.mx_VerificationShowSas_emojiSas_break {
flex-basis: 100%;
}
.mx_VerificationShowSas {
.mx_Dialog_buttons {
// this is more specific than the DialogButtons css so gets preference
button.mx_VerificationShowSas_matchButton {
color: $accent-color;
background-color: $accent-bg-color;
border: none;
}
// this is more specific than the DialogButtons css so gets preference
button.mx_VerificationShowSas_noMatchButton {
color: $notice-primary-color;
background-color: $notice-primary-bg-color;
border: none;
}
}
}

View file

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="11.5" stroke="#2E2F32" stroke-linecap="round"/>
<circle cx="12" cy="4" r="0.8" fill="#2E2F32" stroke="#2E2F32" stroke-width="0.4"/>
<circle cx="12" cy="20" r="0.8" fill="#2E2F32" stroke="#2E2F32" stroke-width="0.4"/>
<circle cx="20" cy="12" r="0.8" fill="#2E2F32" stroke="#2E2F32" stroke-width="0.4"/>
<circle cx="4" cy="12" r="0.8" fill="#2E2F32" stroke="#2E2F32" stroke-width="0.4"/>
<path d="M10.7272 10.4443L18.3639 5.63602L13.5556 13.2728L5.636 18.3639L10.7272 10.4443Z" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 672 B

View file

@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.7273 16.8V15.6666C14.7273 14.4148 13.7504 13.4 12.5455 13.4H8.18182C6.97683 13.4 6 14.4148 6 15.6666V16.8" stroke="#27303A" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.3636 11.1333C11.5686 11.1333 12.5455 10.1185 12.5455 8.86664C12.5455 7.6148 11.5686 6.59998 10.3636 6.59998C9.15866 6.59998 8.18182 7.6148 8.18182 8.86664C8.18182 10.1185 9.15866 11.1333 10.3636 11.1333Z" stroke="#27303A" stroke-linecap="round"/>
<path d="M18 16.8V15.6666C17.9993 14.6337 17.3264 13.7319 16.3636 13.4736" stroke="#27303A" stroke-linecap="round"/>
<path d="M14.1818 6.67352C15.1472 6.93032 15.8225 7.83405 15.8225 8.86936C15.8225 9.90466 15.1472 10.8084 14.1818 11.0652" stroke="#27303A" stroke-linecap="round"/>
<circle cx="12" cy="12" r="11.5" stroke="#27303A" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 922 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.5 11.3334L23.5 11.3347C23.5043 13.0157 23.1116 14.6741 22.3537 16.1746L22.3527 16.1765C20.5186 19.8462 16.769 22.1651 12.6665 22.1667L12.6653 22.1667C10.9843 22.1711 9.32594 21.7783 7.82541 21.0204C7.70642 20.9603 7.56835 20.9502 7.44187 20.9923L1.73925 22.8932C1.34837 23.0235 0.976504 22.6516 1.1068 22.2607L3.00767 16.5581C3.04983 16.4317 3.03973 16.2936 2.97963 16.1746C2.22171 14.6741 1.82895 13.0157 1.83333 11.3347L1.83333 11.3335C1.83492 7.23102 4.15379 3.48136 7.82352 1.64729L7.82352 1.64729L7.82541 1.64634C9.32594 0.888416 10.9843 0.495653 12.6653 0.500038H12.6666H13.3194C18.8084 0.809695 23.1903 5.19159 23.5 10.6806L23.5 11.3334Z" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 828 B

View file

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="11px" height="13px" viewBox="0 0 11 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
<title>815EF7DE-169A-4322-AE2A-B65CBE91DCED</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="Left-menu" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Left-menu-option-B" transform="translate(-33.000000, -223.000000)" fill="#FFFFFF" stroke="#76CFA6">
<g id="Left-panel">
<g id="Room-list" transform="translate(0.000000, 69.000000)">
<g id="Group-3" transform="translate(16.000000, 144.000000)">
<g id="icon_person" transform="translate(18.000000, 11.000000)">
<g>
<path d="M5.34291667,5.8555 C5.51425,5.86608333 5.68525,5.88091667 5.85566667,5.90108333 C5.987,5.91666667 6.118,5.93533333 6.24825,5.95783333 C6.96516667,6.08175 7.69391667,6.32633333 8.23175,6.83591667 C8.32116667,6.92058333 8.40433333,7.01166667 8.48041667,7.1085 C8.59608333,7.25566667 8.69475,7.41583333 8.77633333,7.58425 C8.92233333,7.8855 9.0125,8.21083333 9.06841667,8.54008333 C9.13758333,8.9475 9.15758333,9.36266667 9.1635,9.77533333 C9.1685,10.1279167 9.167,10.4805833 9.16725,10.8331667 L8.33333344e-05,10.8331667 C0.000250000001,10.4805833 -0.00125,10.1279167 0.00375,9.77533333 C0.00916666667,9.39616667 0.0268333333,9.01533333 0.083,8.63991667 C0.134833333,8.29291667 0.221666667,7.94891667 0.369333333,7.62966667 C0.44775,7.46033333 0.543,7.29875 0.65525,7.14958333 C0.729,7.05166667 0.809833333,6.95925 0.897,6.87308333 C1.41916667,6.35725 2.13533333,6.10216667 2.84408333,5.97125 C2.97233333,5.94758333 3.10125,5.92775 3.23058333,5.91108333 C3.39841667,5.8895 3.56683333,5.87333333 3.73558333,5.86133333 C3.95191667,5.846 4.16858333,5.8385 4.38533333,5.83458333 C4.48475,5.8335 4.58408333,5.83316667 4.6835,5.8335 C4.9035,5.83583333 5.12333333,5.84183333 5.34291667,5.8555 Z" id="Fill-1" stroke-linejoin="round"></path>
<path d="M4.99558333,0.031 C5.28133333,0.0745833333 5.55966667,0.1645 5.81691667,0.29625 C6.32075,0.554333333 6.7375,0.971 6.9955,1.47483333 C7.11691667,1.712 7.20291667,1.967 7.24975,2.22916667 C7.30216667,2.52283333 7.30583333,2.82525 7.26083333,3.12008333 C7.2205,3.38416667 7.14066667,3.642 7.02475,3.88266667 C6.88325,4.17633333 6.68833333,4.44375 6.45233333,4.66866667 C6.21591667,4.89408333 5.93891667,5.07633333 5.638,5.20358333 C5.30525,5.34433333 4.94491667,5.4165 4.58366667,5.4165 C4.22233333,5.4165 3.86208333,5.34433333 3.52925,5.20358333 C3.22833333,5.07633333 2.95133333,4.89408333 2.71491667,4.66866667 C2.479,4.44375 2.284,4.17633333 2.1425,3.88266667 C2.02658333,3.642 1.94675,3.38416667 1.90641667,3.12008333 C1.86141667,2.82525 1.86508333,2.52291667 1.91758333,2.22925 C1.96433333,1.967 2.05033333,1.712 2.17175,1.47483333 C2.42975,0.971 2.8465,0.554333333 3.35033333,0.29625 C3.60758333,0.1645 3.88591667,0.0745833333 4.17166667,0.031 C4.28525,0.0136666667 4.39916667,0.005 4.51391667,0.000666666667 C4.58391667,-0.000166666667 4.58366667,-0.000166666667 4.65333333,0.000666666667 C4.76808333,0.005 4.882,0.0136666667 4.99558333,0.031 Z" id="Fill-2"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -13,7 +13,6 @@ handle_error() {
trap 'handle_error' ERR
echo "--- Building Riot"
scripts/ci/layered-riot-web.sh
cd ../riot-web

View file

@ -237,7 +237,7 @@ const walkOpts = {
const fullPath = path.join(root, fileStats.name);
let trs;
if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.tsx')) {
if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.ts') || fileStats.name.endsWith('.tsx')) {
trs = getTranslationsJs(fullPath);
} else if (fileStats.name.endsWith('.html')) {
trs = getTranslationsOther(fullPath);

View file

@ -123,8 +123,8 @@ const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts";
function getUid() {
try {
let data = localStorage.getItem(UID_KEY);
if (!data) {
let data = localStorage && localStorage.getItem(UID_KEY);
if (!data && localStorage) {
localStorage.setItem(UID_KEY, data = [...Array(16)].map(() => Math.random().toString(16)[2]).join(''));
}
return data;
@ -145,15 +145,17 @@ class Analytics {
this.firstPage = true;
this._heartbeatIntervalID = null;
this.creationTs = localStorage.getItem(CREATION_TS_KEY);
if (!this.creationTs) {
this.creationTs = localStorage && localStorage.getItem(CREATION_TS_KEY);
if (!this.creationTs && localStorage) {
localStorage.setItem(CREATION_TS_KEY, this.creationTs = new Date().getTime());
}
this.lastVisitTs = localStorage.getItem(LAST_VISIT_TS_KEY);
this.visitCount = localStorage.getItem(VISIT_COUNT_KEY) || 0;
this.lastVisitTs = localStorage && localStorage.getItem(LAST_VISIT_TS_KEY);
this.visitCount = localStorage && localStorage.getItem(VISIT_COUNT_KEY) || 0;
if (localStorage) {
localStorage.setItem(VISIT_COUNT_KEY, parseInt(this.visitCount, 10) + 1);
}
}
get disabled() {
return !this.baseUrl;

View file

@ -188,4 +188,8 @@ export default class BasePlatform {
const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl());
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO
}
onKeyDown(ev: KeyboardEvent): boolean {
return false; // no shortcuts implemented
}
}

View file

@ -60,12 +60,12 @@ import * as sdk from './index';
import { _t } from './languageHandler';
import Matrix from 'matrix-js-sdk';
import dis from './dispatcher';
import SdkConfig from './SdkConfig';
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
import WidgetUtils from './utils/WidgetUtils';
import WidgetEchoStore from './stores/WidgetEchoStore';
import SettingsStore, { SettingLevel } from './settings/SettingsStore';
import {generateHumanReadableId} from "./utils/NamingUtils";
import {Jitsi} from "./widgets/Jitsi";
global.mxCalls = {
//room_id: MatrixCall
@ -431,7 +431,7 @@ async function _startCallApp(roomId, type) {
}
const confId = `JitsiConference${generateHumanReadableId()}`;
const jitsiDomain = SdkConfig.get()['jitsi']['preferredDomain'];
const jitsiDomain = Jitsi.getInstance().preferredDomain;
let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl();

View file

@ -51,7 +51,7 @@ async function confirmToDismiss(name) {
} else if (name === "m.cross_signing.self_signing") {
description = _t("If you cancel now, you won't complete verifying your other session.");
} else {
description = _t("If you cancel now, you won't complete your secret storage operation.");
description = _t("If you cancel now, you won't complete your operation.");
}
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");

View file

@ -50,6 +50,7 @@ export default class DeviceListener {
MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated);
MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged);
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
MatrixClientPeg.get().on('accountData', this._onAccountData);
this._recheck();
}
@ -59,6 +60,7 @@ export default class DeviceListener {
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated);
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged);
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged);
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
}
this._dismissed.clear();
@ -89,9 +91,20 @@ export default class DeviceListener {
this._recheck();
}
_onCrossSingingKeysChanged = () => {
this._recheck();
}
_onAccountData = (ev) => {
// User may have migrated SSSS to symmetric, in which case we can dismiss that toast
if (ev.getType().startsWith('m.secret_storage.key.')) {
// User may have:
// * migrated SSSS to symmetric
// * uploaded keys to secret storage
// * completed secret storage creation
// which result in account data changes affecting checks below.
if (
ev.getType().startsWith('m.secret_storage.') ||
ev.getType().startsWith('m.cross_signing.')
) {
this._recheck();
}
}
@ -111,7 +124,7 @@ export default class DeviceListener {
const cli = MatrixClientPeg.get();
if (
!SettingsStore.isFeatureEnabled("feature_cross_signing") ||
!SettingsStore.getValue("feature_cross_signing") ||
!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
) return;

View file

@ -35,7 +35,7 @@ export default class KeyRequestHandler {
handleKeyRequest(keyRequest) {
// Ignore own device key requests if cross-signing lab enabled
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
return;
}
@ -70,7 +70,7 @@ export default class KeyRequestHandler {
handleKeyRequestCancellation(cancellation) {
// Ignore own device key requests if cross-signing lab enabled
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
return;
}

View file

@ -43,6 +43,8 @@ export const Key = {
BACKTICK: "`",
SPACE: " ",
SLASH: "/",
SQUARE_BRACKET_LEFT: "[",
SQUARE_BRACKET_RIGHT: "]",
A: "a",
B: "b",
C: "c",

View file

@ -40,6 +40,7 @@ import ToastStore from "./stores/ToastStore";
import {IntegrationManagers} from "./integrations/IntegrationManagers";
import {Mjolnir} from "./mjolnir/Mjolnir";
import DeviceListener from "./DeviceListener";
import {Jitsi} from "./widgets/Jitsi";
/**
* Called at startup, to attempt to build a logged-in Matrix session. It tries
@ -578,9 +579,6 @@ async function startMatrixClient(startSyncing=true) {
UserActivity.sharedInstance().start();
TypingStore.sharedInstance().reset(); // just in case
ToastStore.sharedInstance().reset();
if (!SettingsStore.getValue("lowBandwidth")) {
Presence.start();
}
DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start();
@ -603,6 +601,14 @@ async function startMatrixClient(startSyncing=true) {
// This needs to be started after crypto is set up
DeviceListener.sharedInstance().start();
// Similarly, don't start sending presence updates until we've started
// the client
if (!SettingsStore.getValue("lowBandwidth")) {
Presence.start();
}
// Now that we have a MatrixClientPeg, update the Jitsi info
await Jitsi.getInstance().update();
// dispatch that we finished starting up to wire up any other bits
// of the matrix client that cannot be set prior to starting up.

View file

@ -37,6 +37,18 @@ import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
const MAX_PENDING_ENCRYPTED = 20;
/*
Override both the content body and the TextForEvent handler for specific msgtypes, in notifications.
This is useful when the content body contains fallback text that would explain that the client can't handle a particular
type of tile.
*/
const typehandlers = {
"m.key.verification.request": (event) => {
const name = (event.sender || {}).name;
return _t("%(name)s is requesting verification", { name });
},
};
const Notifier = {
notifsByRoom: {},
@ -46,6 +58,9 @@ const Notifier = {
pendingEncryptedEventIds: [],
notificationMessageForEvent: function(ev) {
if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) {
return typehandlers[ev.getContent().msgtype](ev);
}
return TextForEvent.textForEvent(ev);
},
@ -69,7 +84,9 @@ const Notifier = {
title = room.name;
// notificationMessageForEvent includes sender,
// but we already have the sender here
if (ev.getContent().body) msg = ev.getContent().body;
if (ev.getContent().body && !typehandlers.hasOwnProperty(ev.getContent().msgtype)) {
msg = ev.getContent().body;
}
} else if (ev.getType() === 'm.room.member') {
// context is all in the message here, we don't need
// to display sender info
@ -78,7 +95,9 @@ const Notifier = {
title = ev.sender.name + " (" + room.name + ")";
// notificationMessageForEvent includes sender,
// but we've just out sender in the title
if (ev.getContent().body) msg = ev.getContent().body;
if (ev.getContent().body && !typehandlers.hasOwnProperty(ev.getContent().msgtype)) {
msg = ev.getContent().body;
}
}
if (!this.isBodyEnabled()) {

View file

@ -172,6 +172,7 @@ Request:
Response:
[
{
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
type: "im.vector.modular.widgets",
state_key: "wid1",
content: {
@ -190,6 +191,7 @@ Example:
room_id: "!foo:bar",
response: [
{
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
type: "im.vector.modular.widgets",
state_key: "wid1",
content: {

View file

@ -350,7 +350,7 @@ export const Commands = [
return success(cli.setRoomTopic(roomId, args));
}
const room = cli.getRoom(roomId);
if (!room) return reject('Bad room ID: ' + roomId);
if (!room) return reject(_t("Failed to set topic"));
const topicEvents = room.currentState.getStateEvents('m.room.topic', '');
const topic = topicEvents && topicEvents.getContent().topic;
@ -721,9 +721,10 @@ export const Commands = [
if (!isNaN(powerLevel)) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId);
if (!room) return reject('Bad room ID: ' + roomId);
if (!room) return reject(_t("Command failed"));
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
if (!powerLevelEvent.getContent().users[args]) return reject(_t("Could not find user in room"));
return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent));
}
}
@ -742,9 +743,10 @@ export const Commands = [
if (matches) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId);
if (!room) return reject('Bad room ID: ' + roomId);
if (!room) return reject(_t("Command failed"));
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
if (!powerLevelEvent.getContent().users[args]) return reject(_t("Could not find user in room"));
return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent));
}
}
@ -914,7 +916,7 @@ export const Commands = [
// Command definitions for autocompletion ONLY:
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
new Command({
command: 'me',
command: "me",
args: '<message>',
description: _td('Displays action'),
category: CommandCategories.messages,
@ -931,16 +933,7 @@ Commands.forEach(cmd => {
});
});
/**
* Process the given text for /commands and return a bound method to perform them.
* @param {string} roomId The room in which the command was performed.
* @param {string} input The raw text input by the user.
* @return {null|function(): Object} Function returning an object with the property 'error' if there was an error
* processing the command, or 'promise' if a request was sent out.
* Returns null if the input didn't match a command.
*/
export function getCommand(roomId, input) {
export function parseCommandString(input) {
// trim any trailing whitespace, as it can confuse the parser for
// IRC-style commands
input = input.replace(/\s+$/, '');
@ -956,6 +949,20 @@ export function getCommand(roomId, input) {
cmd = input;
}
return {cmd, args};
}
/**
* Process the given text for /commands and return a bound method to perform them.
* @param {string} roomId The room in which the command was performed.
* @param {string} input The raw text input by the user.
* @return {null|function(): Object} Function returning an object with the property 'error' if there was an error
* processing the command, or 'promise' if a request was sent out.
* Returns null if the input didn't match a command.
*/
export function getCommand(roomId, input) {
const {cmd, args} = parseCommandString(input);
if (CommandMap.has(cmd)) {
return () => CommandMap.get(cmd).run(roomId, args, cmd);
}

View file

@ -603,6 +603,7 @@ const stateHandlers = {
'm.room.guest_access': textForGuestAccessEvent,
'm.room.related_groups': textForRelatedGroupsEvent,
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
'im.vector.modular.widgets': textForWidgetEvent,
};

View file

@ -25,6 +25,7 @@ import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager';
import SettingsStore from '../../../../settings/SettingsStore';
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import {copyNode} from "../../../../utils/strings";
const PHASE_PASSPHRASE = 0;
const PHASE_PASSPHRASE_CONFIRM = 1;
@ -37,16 +38,6 @@ const PHASE_OPTOUT_CONFIRM = 6;
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
// XXX: copied from ShareDialog: factor out into utils
function selectText(target) {
const range = document.createRange();
range.selectNodeContents(target);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
/*
* Walks the user through the process of creating an e2e key backup
* on the server.
@ -77,7 +68,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
async componentDidMount() {
const cli = MatrixClientPeg.get();
const secureSecretStorage = (
SettingsStore.isFeatureEnabled("feature_cross_signing") &&
SettingsStore.getValue("feature_cross_signing") &&
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
);
this.setState({ secureSecretStorage });
@ -101,8 +92,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
}
_onCopyClick = () => {
selectText(this._recoveryKeyNode);
const successful = document.execCommand('copy');
const successful = copyNode(this._recoveryKeyNode);
if (successful) {
this.setState({
copied: true,
@ -272,7 +262,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
let helpText;
if (this.state.zxcvbnResult) {
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
helpText = _t("Great! This passphrase looks strong enough.");
helpText = _t("Great! This recovery passphrase looks strong enough.");
} else {
const suggestions = [];
for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) {
@ -297,7 +287,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
)}</p>
<p>{_t(
"We'll store an encrypted copy of your keys on our server. " +
"Protect your backup with a passphrase to keep it secure.",
"Secure your backup with a recovery passphrase.",
)}</p>
<p>{_t("For maximum security, this should be different from your account password.")}</p>
@ -307,7 +297,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
onChange={this._onPassPhraseChange}
value={this.state.passPhrase}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Enter a passphrase...")}
placeholder={_t("Enter a recovery passphrase...")}
autoFocus={true}
/>
<div className="mx_CreateKeyBackupDialog_passPhraseHelp">
@ -364,7 +354,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t(
"Please enter your passphrase a second time to confirm.",
"Please enter your recovery passphrase a second time to confirm.",
)}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
@ -373,7 +363,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
onChange={this._onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Repeat your passphrase...")}
placeholder={_t("Repeat your recovery passphrase...")}
autoFocus={true}
/>
</div>
@ -393,7 +383,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
return <div>
<p>{_t(
"Your recovery key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your passphrase.",
"access to your encrypted messages if you forget your recovery passphrase.",
)}</p>
<p>{_t(
"Keep a copy of it somewhere secure, like a password manager or even a safe.",
@ -487,9 +477,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_titleForPhase(phase) {
switch (phase) {
case PHASE_PASSPHRASE:
return _t('Secure your backup with a passphrase');
return _t('Secure your backup with a recovery passphrase');
case PHASE_PASSPHRASE_CONFIRM:
return _t('Confirm your passphrase');
return _t('Confirm your recovery passphrase');
case PHASE_OPTOUT_CONFIRM:
return _t('Warning!');
case PHASE_SHOWKEY:

View file

@ -57,8 +57,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
</span>;
const newMethodDetected = <p>{_t(
"A new recovery passphrase and key for Secure " +
"Messages have been detected.",
"A new recovery passphrase and key for Secure Messages have been detected.",
)}</p>;
const hackWarning = <p className="warning">{_t(

View file

@ -24,6 +24,7 @@ import FileSaver from 'file-saver';
import { _t } from '../../../../languageHandler';
import Modal from '../../../../Modal';
import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
import {copyNode} from "../../../../utils/strings";
const PHASE_LOADING = 0;
const PHASE_MIGRATE = 1;
@ -38,16 +39,6 @@ const PHASE_CONFIRM_SKIP = 8;
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
// XXX: copied from ShareDialog: factor out into utils
function selectText(target) {
const range = document.createRange();
range.selectNodeContents(target);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
/*
* Walks the user through the process of creating a passphrase to guard Secure
* Secret Storage in account data.
@ -70,6 +61,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this._recoveryKey = null;
this._recoveryKeyNode = null;
this._setZxcvbnResultTimeout = null;
this._backupKey = null;
this.state = {
phase: PHASE_LOADING,
@ -168,8 +160,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
_onCopyClick = () => {
selectText(this._recoveryKeyNode);
const successful = document.execCommand('copy');
const successful = copyNode(this._recoveryKeyNode);
if (successful) {
this.setState({
copied: true,
@ -243,7 +234,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
createSecretStorageKey: async () => this._recoveryKey,
keyBackupInfo: this.state.backupInfo,
setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup,
getKeyBackupPassphrase: promptForBackupPassphrase,
getKeyBackupPassphrase: () => {
// We may already have the backup key if we earlier went
// through the restore backup path, so pass it along
// rather than prompting again.
if (this._backupKey) {
return this._backupKey;
}
return promptForBackupPassphrase();
},
});
}
this.setState({
@ -272,10 +271,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}
_restoreBackup = async () => {
// It's possible we'll need the backup key later on for bootstrapping,
// so let's stash it here, rather than prompting for it twice.
const keyCallback = k => this._backupKey = k;
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
const { finished } = Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, {showSummary: false}, null,
/* priority = */ false, /* static = */ false,
'Restore Backup', '', RestoreKeyBackupDialog,
{
showSummary: false,
keyCallback,
},
null, /* priority = */ false, /* static = */ false,
);
await finished;
@ -455,7 +462,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let helpText;
if (this.state.zxcvbnResult) {
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
helpText = _t("Great! This passphrase looks strong enough.");
helpText = _t("Great! This recovery passphrase looks strong enough.");
} else {
// We take the warning from zxcvbn or failing that, the first
// suggestion. In practice The first is generally the most relevant
@ -480,12 +487,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
return <form onSubmit={this._onPassPhraseNextClick}>
<p>{_t(
"Set up encryption on this session to allow it to verify other sessions, " +
"granting them access to encrypted messages and marking them as trusted for other users.",
)}</p>
<p>{_t(
"Secure your encryption keys with a passphrase. For maximum security " +
"this should be different to your account password:",
"Set a recovery passphrase to secure encrypted information and recover it if you log out. " +
"This should be different to your account password:",
)}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
@ -494,7 +497,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
className="mx_CreateSecretStorageDialog_passPhraseField"
onChange={this._onPassPhraseChange}
value={this.state.passPhrase}
label={_t("Enter a passphrase")}
label={_t("Enter a recovery passphrase")}
autoFocus={true}
autoComplete="new-password"
/>
@ -505,7 +508,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>
<LabelledToggleSwitch
label={ _t("Back up my encryption keys, securing them with the same passphrase")}
label={ _t("Back up encrypted message keys")}
onChange={this._onUseKeyBackupChange} value={this.state.useKeyBackup}
/>
@ -562,7 +565,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t(
"Enter your passphrase a second time to confirm it.",
"Enter your recovery passphrase a second time to confirm it.",
)}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field
@ -570,7 +573,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
onChange={this._onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateSecretStorageDialog_passPhraseField"
label={_t("Confirm your passphrase")}
label={_t("Confirm your recovery passphrase")}
autoFocus={true}
autoComplete="new-password"
/>
@ -597,7 +600,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
return <div>
<p>{_t(
"Your recovery key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your passphrase.",
"access to your encrypted messages if you forget your recovery passphrase.",
)}</p>
<p>{_t(
"Keep a copy of it somewhere secure, like a password manager or even a safe.",
@ -611,7 +614,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code>
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onCopyClick}>
<AccessibleButton
kind='primary'
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
onClick={this._onCopyClick}
>
{_t("Copy")}
</AccessibleButton>
<AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onDownloadClick}>
@ -696,7 +703,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
case PHASE_PASSPHRASE:
return _t('Set up encryption');
case PHASE_PASSPHRASE_CONFIRM:
return _t('Confirm passphrase');
return _t('Confirm recovery passphrase');
case PHASE_CONFIRM_SKIP:
return _t('Are you sure?');
case PHASE_SHOWKEY:

View file

@ -100,6 +100,8 @@ export default class EmojiProvider extends AutocompleteProvider {
// then sort by score (Infinity if matchedString not in shortname)
sorters.push((c) => score(matchedString, c.shortname));
// then sort by max score of all shortcodes, trim off the `:`
sorters.push((c) => Math.min(...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s))));
// If the matchedString is not empty, sort by length of shortname. Example:
// matchedString = ":bookmark"
// completions = [":bookmark:", ":bookmark_tabs:", ...]

View file

@ -245,7 +245,6 @@ export class ContextMenu extends React.Component {
}
const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null;
const padding = 10;
const chevronOffset = {};
if (props.chevronFace) {
@ -264,7 +263,8 @@ export class ContextMenu extends React.Component {
// If we know the dimensions of the context menu, adjust its position
// such that it does not leave the (padded) window.
if (contextMenuRect) {
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding);
const padding = 10;
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height + padding);
}
position.top = adjusted;

View file

@ -0,0 +1,66 @@
/*
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 * as React from "react";
import AutoHideScrollbar from './AutoHideScrollbar';
import { getHomePageUrl } from "../../utils/pages";
import { _t } from "../../languageHandler";
import SdkConfig from "../../SdkConfig";
import * as sdk from "../../index";
import dis from "../../dispatcher";
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'});
const onClickExplore = () => dis.dispatch({action: 'view_room_directory'});
const onClickNewRoom = () => dis.dispatch({action: 'view_create_room'});
const HomePage = () => {
const config = SdkConfig.get();
const pageUrl = getHomePageUrl(config);
if (pageUrl) {
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
return <EmbeddedPage className="mx_HomePage" url={pageUrl} scrollbar={true} />;
}
const brandingConfig = config.branding;
let logoUrl = "themes/riot/img/logos/riot-logo.svg";
if (brandingConfig && brandingConfig.authHeaderLogoUrl) {
logoUrl = brandingConfig.authHeaderLogoUrl;
}
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default">
<div className="mx_HomePage_default_wrapper">
<img src={logoUrl} alt="Riot" />
<h1>{ _t("Welcome to %(appName)s", { appName: config.brand || "Riot" }) }</h1>
<h4>{ _t("Liberate your communication") }</h4>
<div className="mx_HomePage_default_buttons">
<AccessibleButton onClick={onClickSendDm} className="mx_HomePage_button_sendDm">
{ _t("Send a Direct Message") }
</AccessibleButton>
<AccessibleButton onClick={onClickExplore} className="mx_HomePage_button_explore">
{ _t("Explore Public Rooms") }
</AccessibleButton>
<AccessibleButton onClick={onClickNewRoom} className="mx_HomePage_button_createGroup">
{ _t("Create a Group Chat") }
</AccessibleButton>
</div>
</div>
</AutoHideScrollbar>;
};
export default HomePage;

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 New Vector Ltd
Copyright 2017, 2018, 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,10 +16,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from 'matrix-js-sdk';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { DragDropContext } from 'react-beautiful-dnd';
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard';
@ -29,10 +29,9 @@ import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index';
import dis from '../../dispatcher';
import sessionStore from '../../stores/SessionStore';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
import RoomListStore from "../../stores/RoomListStore";
import { getHomePageUrl } from '../../utils/pages';
import TagOrderActions from '../../actions/TagOrderActions';
import RoomListActions from '../../actions/RoomListActions';
@ -40,6 +39,9 @@ import ResizeHandle from '../views/elements/ResizeHandle';
import {Resizer, CollapseDistributor} from '../../resizer';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
import HomePage from "./HomePage";
import ResizeNotifier from "../../utils/ResizeNotifier";
import PlatformPeg from "../../PlatformPeg";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
// NB. this is just for server notices rather than pinned messages in general.
@ -52,6 +54,52 @@ function canElementReceiveInput(el) {
!!el.getAttribute("contenteditable");
}
interface IProps {
matrixClient: MatrixClient;
onRegistered: (credentials: MatrixClientCreds) => Promise<MatrixClient>;
viaServers?: string[];
hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier;
middleDisabled: boolean;
initialEventPixelOffset: number;
leftDisabled: boolean;
rightDisabled: boolean;
showCookieBar: boolean;
hasNewVersion: boolean;
userHasGeneratedPassword: boolean;
showNotifierToolbar: boolean;
page_type: string;
autoJoin: boolean;
thirdPartyInvite?: object;
roomOobData?: object;
currentRoomId: string;
ConferenceHandler?: object;
collapseLhs: boolean;
checkingForUpdate: boolean;
config: {
piwik: {
policyUrl: string;
},
[key: string]: any,
};
currentUserId?: string;
currentGroupId?: string;
currentGroupIsNew?: boolean;
version?: string;
newVersion?: string;
newVersionReleaseNotes?: string;
}
interface IState {
mouseDown?: {
x: number;
y: number;
};
syncErrorData: any;
useCompactLayout: boolean;
serverNoticeEvents: MatrixEvent[];
userHasGeneratedPassword: boolean;
}
/**
* This is what our MatrixChat shows when we are logged in. The precise view is
* determined by the page_type property.
@ -61,10 +109,10 @@ function canElementReceiveInput(el) {
*
* Components mounted below us can access the matrix client via the react context.
*/
const LoggedInView = createReactClass({
displayName: 'LoggedInView',
class LoggedInView extends React.PureComponent<IProps, IState> {
static displayName = 'LoggedInView';
propTypes: {
static propTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
page_type: PropTypes.string.isRequired,
onRoomCreated: PropTypes.func,
@ -77,25 +125,28 @@ const LoggedInView = createReactClass({
viaServers: PropTypes.arrayOf(PropTypes.string),
// and lots and lots of other stuff.
},
};
getInitialState: function() {
return {
protected readonly _matrixClient: MatrixClient;
protected readonly _roomView: React.RefObject<any>;
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
protected readonly _sessionStore: sessionStore;
protected readonly _sessionStoreToken: { remove: () => void };
protected resizer: Resizer;
constructor(props, context) {
super(props, context);
this.state = {
mouseDown: undefined,
syncErrorData: undefined,
userHasGeneratedPassword: false,
// use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
// any currently active server notice events
serverNoticeEvents: [],
};
},
componentDidMount: function() {
this.resizer = this._createResizer();
this.resizer.attach();
this._loadResizerPreferences();
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
// stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient;
@ -117,22 +168,29 @@ const LoggedInView = createReactClass({
fixupColorFonts();
this._roomView = createRef();
},
this._roomView = React.createRef();
this._resizeContainer = React.createRef();
}
componentDidUpdate(prevProps) {
componentDidMount() {
this.resizer = this._createResizer();
this.resizer.attach();
this._loadResizerPreferences();
}
componentDidUpdate(prevProps, prevState) {
// attempt to guess when a banner was opened or closed
if (
(prevProps.showCookieBar !== this.props.showCookieBar) ||
(prevProps.hasNewVersion !== this.props.hasNewVersion) ||
(prevProps.userHasGeneratedPassword !== this.props.userHasGeneratedPassword) ||
(prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) ||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
) {
this.props.resizeNotifier.notifyBannersChanged();
}
},
}
componentWillUnmount: function() {
componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false);
this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync);
@ -141,7 +199,7 @@ const LoggedInView = createReactClass({
this._sessionStoreToken.remove();
}
this.resizer.detach();
},
}
// Child components assume that the client peg will not be null, so give them some
// sort of assurance here by only allowing a re-render if the client is truthy.
@ -149,22 +207,22 @@ const LoggedInView = createReactClass({
// This is required because `LoggedInView` maintains its own state and if this state
// updates after the client peg has been made null (during logout), then it will
// attempt to re-render and the children will throw errors.
shouldComponentUpdate: function() {
shouldComponentUpdate() {
return Boolean(MatrixClientPeg.get());
},
}
canResetTimelineInRoom: function(roomId) {
canResetTimelineInRoom = (roomId) => {
if (!this._roomView.current) {
return true;
}
return this._roomView.current.canResetTimeline();
},
};
_setStateFromSessionStore() {
_setStateFromSessionStore = () => {
this.setState({
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
});
},
};
_createResizer() {
const classNames = {
@ -188,24 +246,22 @@ const LoggedInView = createReactClass({
},
};
const resizer = new Resizer(
this.resizeContainer,
this._resizeContainer.current,
CollapseDistributor,
collapseConfig);
resizer.setClassNames(classNames);
return resizer;
},
}
_loadResizerPreferences() {
let lhsSize = window.localStorage.getItem("mx_lhs_size");
if (lhsSize !== null) {
lhsSize = parseInt(lhsSize, 10);
} else {
let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
if (isNaN(lhsSize)) {
lhsSize = 350;
}
this.resizer.forHandleAt(0).resize(lhsSize);
},
}
onAccountData: function(event) {
onAccountData = (event) => {
if (event.getType() === "im.vector.web.settings") {
this.setState({
useCompactLayout: event.getContent().useCompactLayout,
@ -214,9 +270,9 @@ const LoggedInView = createReactClass({
if (event.getType() === "m.ignored_user_list") {
dis.dispatch({action: "ignore_state_changed"});
}
},
};
onSync: function(syncState, oldSyncState, data) {
onSync = (syncState, oldSyncState, data) => {
const oldErrCode = (
this.state.syncErrorData &&
this.state.syncErrorData.error &&
@ -238,16 +294,16 @@ const LoggedInView = createReactClass({
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents();
}
},
};
onRoomStateEvents: function(ev, state) {
onRoomStateEvents = (ev, state) => {
const roomLists = RoomListStore.getRoomLists();
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents();
}
},
};
_updateServerNoticeEvents: async function() {
_updateServerNoticeEvents = async () => {
const roomLists = RoomListStore.getRoomLists();
if (!roomLists['m.server_notice']) return [];
@ -260,16 +316,16 @@ const LoggedInView = createReactClass({
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
const ev = timeline.getEvents().find(ev => ev.getId() === eventId);
if (ev) pinnedEvents.push(ev);
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
if (event) pinnedEvents.push(event);
}
}
this.setState({
serverNoticeEvents: pinnedEvents,
});
},
};
_onPaste: function(ev) {
_onPaste = (ev) => {
let canReceiveInput = false;
let element = ev.target;
// test for all parents because the target can be a child of a contenteditable element
@ -283,7 +339,7 @@ const LoggedInView = createReactClass({
// so dispatch synchronously before paste happens
dis.dispatch({action: 'focus_composer'}, true);
}
},
};
/*
SOME HACKERY BELOW:
@ -307,22 +363,22 @@ const LoggedInView = createReactClass({
We also listen with a native listener on the document to get keydown events when no element is focused.
Bubbling is irrelevant here as the target is the body element.
*/
_onReactKeyDown: function(ev) {
_onReactKeyDown = (ev) => {
// events caught while bubbling up on the root element
// of this component, so something must be focused.
this._onKeyDown(ev);
},
};
_onNativeKeyDown: function(ev) {
_onNativeKeyDown = (ev) => {
// only pass this if there is no focused element.
// if there is, _onKeyDown will be called by the
// react keydown handler that respects the react bubbling order.
if (ev.target === document.body) {
this._onKeyDown(ev);
}
},
};
_onKeyDown: function(ev) {
_onKeyDown = (ev) => {
/*
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
// Will need to find a better meta key if anyone actually cares about using this.
@ -407,6 +463,11 @@ const LoggedInView = createReactClass({
});
handled = true;
}
break;
default:
// if we do not have a handler for it, pass it to the platform which might
handled = PlatformPeg.get().onKeyDown(ev);
}
if (handled) {
@ -432,19 +493,19 @@ const LoggedInView = createReactClass({
// that would prevent typing in the now-focussed composer
}
}
},
};
/**
* dispatch a page-up/page-down/etc to the appropriate component
* @param {Object} ev The key event
*/
_onScrollKeyPressed: function(ev) {
_onScrollKeyPressed = (ev) => {
if (this._roomView.current) {
this._roomView.current.handleScrollKey(ev);
}
},
};
_onDragEnd: function(result) {
_onDragEnd = (result) => {
// Dragged to an invalid destination, not onto a droppable
if (!result.destination) {
return;
@ -467,9 +528,9 @@ const LoggedInView = createReactClass({
} else if (dest.startsWith('room-sub-list-droppable_')) {
this._onRoomTileEndDrag(result);
}
},
};
_onRoomTileEndDrag: function(result) {
_onRoomTileEndDrag = (result) => {
let newTag = result.destination.droppableId.split('_')[1];
let prevTag = result.source.droppableId.split('_')[1];
if (newTag === 'undefined') newTag = undefined;
@ -486,9 +547,9 @@ const LoggedInView = createReactClass({
prevTag, newTag,
oldIndex, newIndex,
), true);
},
};
_onMouseDown: function(ev) {
_onMouseDown = (ev) => {
// When the panels are disabled, clicking on them results in a mouse event
// which bubbles to certain elements in the tree. When this happens, close
// any settings page that is currently open (user/room/group).
@ -507,9 +568,9 @@ const LoggedInView = createReactClass({
});
}
}
},
};
_onMouseUp: function(ev) {
_onMouseUp = (ev) => {
if (!this.state.mouseDown) return;
const deltaX = ev.pageX - this.state.mouseDown.x;
@ -528,17 +589,12 @@ const LoggedInView = createReactClass({
// Always clear the mouseDown state to ensure we don't accidentally
// use stale values due to the mouseDown checks.
this.setState({mouseDown: null});
},
};
_setResizeContainerRef(div) {
this.resizeContainer = div;
},
render: function() {
render() {
const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RoomView = sdk.getComponent('structures.RoomView');
const UserView = sdk.getComponent('structures.UserView');
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
const GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups');
const ToastContainer = sdk.getComponent('structures.ToastContainer');
@ -577,13 +633,7 @@ const LoggedInView = createReactClass({
break;
case PageTypes.HomePage:
{
const pageUrl = getHomePageUrl(this.props.config);
pageElement = <EmbeddedPage className="mx_HomePage"
url={pageUrl}
scrollbar={true}
/>;
}
pageElement = <HomePage />;
break;
case PageTypes.UserView:
@ -654,7 +704,7 @@ const LoggedInView = createReactClass({
{ topBar }
<ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._setResizeContainerRef} className={bodyClasses}>
<div ref={this._resizeContainer} className={bodyClasses}>
<LeftPanel
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
@ -667,7 +717,7 @@ const LoggedInView = createReactClass({
</div>
</MatrixClientContext.Provider>
);
},
});
}
}
export default LoggedInView;

View file

@ -1506,7 +1506,7 @@ export default createReactClass({
});
cli.on("crypto.verification.request", request => {
const isFlagOn = SettingsStore.isFeatureEnabled("feature_cross_signing");
const isFlagOn = SettingsStore.getValue("feature_cross_signing");
if (!isFlagOn && !request.channel.deviceId) {
request.cancel({code: "m.invalid_message", reason: "This client has cross-signing disabled"});
@ -1556,7 +1556,7 @@ export default createReactClass({
// changing colour. More advanced behaviour will come once
// we implement more settings.
cli.setGlobalErrorOnUnknownDevices(
!SettingsStore.isFeatureEnabled("feature_cross_signing"),
!SettingsStore.getValue("feature_cross_signing"),
);
}
},
@ -1902,28 +1902,29 @@ export default createReactClass({
const cli = MatrixClientPeg.get();
// We're checking `isCryptoAvailable` here instead of `isCryptoEnabled`
// because the client hasn't been started yet.
if (!isCryptoAvailable()) {
const cryptoAvailable = isCryptoAvailable();
if (!cryptoAvailable) {
this._onLoggedIn();
}
this.setState({ pendingInitialSync: true });
await this.firstSyncPromise.promise;
if (!cryptoAvailable) {
this.setState({ pendingInitialSync: false });
return setLoggedInPromise;
}
// Test for the master cross-signing key in SSSS as a quick proxy for
// whether cross-signing has been set up on the account.
let masterKeyInStorage = false;
try {
masterKeyInStorage = !!await cli.getAccountDataFromServer("m.cross_signing.master");
} catch (e) {
if (e.errcode !== "M_NOT_FOUND") {
console.warn("Secret storage account data check failed", e);
}
}
const masterKeyInStorage = !!cli.getAccountData("m.cross_signing.master");
if (masterKeyInStorage) {
// Auto-enable cross-signing for the new session when key found in
// secret storage.
SettingsStore.setFeatureEnabled("feature_cross_signing", true);
SettingsStore.setValue("feature_cross_signing", null, SettingLevel.DEVICE, true);
this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY });
} else if (
SettingsStore.isFeatureEnabled("feature_cross_signing") &&
SettingsStore.getValue("feature_cross_signing") &&
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
) {
// This will only work if the feature is set to 'enable' in the config,
@ -1933,6 +1934,7 @@ export default createReactClass({
} else {
this._onLoggedIn();
}
this.setState({ pendingInitialSync: false });
return setLoggedInPromise;
},
@ -2054,6 +2056,7 @@ export default createReactClass({
const Login = sdk.getComponent('structures.auth.Login');
view = (
<Login
isSyncing={this.state.pendingInitialSync}
onLoggedIn={this.onUserCompletedLoginFlow}
onRegisterClick={this.onRegisterClick}
fallbackHsUrl={this.getFallbackHsUrl()}

View file

@ -503,6 +503,7 @@ export default class MessagePanel extends React.Component {
}
_getTilesForEvent(prevEvent, mxEv, last) {
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const ret = [];
@ -577,6 +578,7 @@ export default class MessagePanel extends React.Component {
ref={this._collectEventNode.bind(this, eventId)}
data-scroll-tokens={scrollToken}
>
<TileErrorBoundary mxEvent={mxEv}>
<EventTile mxEvent={mxEv}
continuation={continuation}
isRedacted={mxEv.isRedacted()}
@ -596,6 +598,7 @@ export default class MessagePanel extends React.Component {
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
/>
</TileErrorBoundary>
</li>,
);
@ -757,6 +760,7 @@ export default class MessagePanel extends React.Component {
}
render() {
const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary');
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile");
const Spinner = sdk.getComponent("elements.Spinner");
@ -789,6 +793,7 @@ export default class MessagePanel extends React.Component {
}
return (
<ErrorBoundary>
<ScrollPanel
ref={this._scrollPanel}
className={className}
@ -805,6 +810,7 @@ export default class MessagePanel extends React.Component {
{ whoIsTyping }
{ bottomSpinner }
</ScrollPanel>
</ErrorBoundary>
);
}
}

View file

@ -219,7 +219,7 @@ export default class RightPanel extends React.Component {
break;
case RIGHT_PANEL_PHASES.RoomMemberInfo:
case RIGHT_PANEL_PHASES.EncryptionPanel:
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
const onClose = () => {
dis.dispatch({
action: "view_user",
@ -246,7 +246,7 @@ export default class RightPanel extends React.Component {
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
break;
case RIGHT_PANEL_PHASES.GroupMemberInfo:
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
const onClose = () => {
dis.dispatch({
action: "view_user",

View file

@ -49,7 +49,6 @@ import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import WidgetUtils from '../../utils/WidgetUtils';
import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/RightPanelStore";
import {haveTileForEvent} from "../views/rooms/EventTile";
@ -182,6 +181,7 @@ export default createReactClass({
this.context.on("crypto.keyBackupStatus", this.onKeyBackupStatus);
this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
// Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate);
@ -405,13 +405,9 @@ export default createReactClass({
const hideWidgetDrawer = localStorage.getItem(
room.roomId + "_hide_widget_drawer");
if (hideWidgetDrawer === "true") {
return false;
}
const widgets = WidgetEchoStore.getEchoedRoomWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
return widgets.length > 0 || WidgetEchoStore.roomHasPendingWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
// This is confusing, but it means to say that we default to the tray being
// hidden unless the user clicked to open it.
return hideWidgetDrawer === "false";
},
componentDidMount: function() {
@ -504,6 +500,7 @@ export default createReactClass({
this.context.removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
}
window.removeEventListener('beforeunload', this.onPageUnload);
@ -805,6 +802,13 @@ export default createReactClass({
this._updateE2EStatus(room);
},
onCrossSigningKeysChanged: function() {
const room = this.state.room;
if (room) {
this._updateE2EStatus(room);
}
},
_updateE2EStatus: async function(room) {
if (!this.context.isRoomEncrypted(room.roomId)) {
return;
@ -818,7 +822,7 @@ export default createReactClass({
});
return;
}
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (!SettingsStore.getValue("feature_cross_signing")) {
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
this.setState({
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
@ -1203,7 +1207,7 @@ export default createReactClass({
});
}, function(error) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Search failed: " + error);
console.error("Search failed", error);
Modal.createTrackedDialog('Search failed', '', ErrorDialog, {
title: _t("Search failed"),
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")),

View file

@ -59,17 +59,17 @@ export default class CompleteSecurity extends React.Component {
let title;
if (phase === PHASE_INTRO) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
title = _t("Complete security");
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this session");
} else if (phase === PHASE_DONE) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified"></span>;
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
title = _t("Session verified");
} else if (phase === PHASE_CONFIRM_SKIP) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Are you sure?");
} else if (phase === PHASE_BUSY) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
title = _t("Complete security");
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this session");
} else {
throw new Error(`Unknown phase ${phase}`);
}

View file

@ -84,11 +84,13 @@ export default createReactClass({
onServerConfigChange: PropTypes.func.isRequired,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
isSyncing: PropTypes.bool,
},
getInitialState: function() {
return {
busy: false,
busyLoggingIn: null,
errorText: null,
loginIncorrect: false,
canTryLogin: true, // can we attempt to log in or are there validation errors?
@ -169,6 +171,7 @@ export default createReactClass({
const componentState = AutoDiscoveryUtils.authComponentStateForError(e);
this.setState({
busy: false,
busyLoggingIn: false,
...componentState,
});
aliveAgain = !componentState.serverErrorIsFatal;
@ -182,6 +185,7 @@ export default createReactClass({
this.setState({
busy: true,
busyLoggingIn: true,
errorText: null,
loginIncorrect: false,
});
@ -250,6 +254,7 @@ export default createReactClass({
this.setState({
busy: false,
busyLoggingIn: false,
errorText: errorText,
// 401 would be the sensible status code for 'incorrect password'
// but the login API gives a 403 https://matrix.org/jira/browse/SYN-744
@ -594,6 +599,7 @@ export default createReactClass({
loginIncorrect={this.state.loginIncorrect}
serverConfig={this.props.serverConfig}
disableSubmit={this.isBusy()}
busy={this.props.isSyncing || this.state.busyLoggingIn}
/>
);
},
@ -629,9 +635,11 @@ export default createReactClass({
render: function() {
const Loader = sdk.getComponent("elements.Spinner");
const InlineSpinner = sdk.getComponent("elements.InlineSpinner");
const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody");
const loader = this.isBusy() ? <div className="mx_Login_loader"><Loader /></div> : null;
const loader = this.isBusy() && !this.state.busyLoggingIn ?
<div className="mx_Login_loader"><Loader /></div> : null;
const errorText = this.state.errorText;
@ -658,9 +666,28 @@ export default createReactClass({
);
}
let footer;
if (this.props.isSyncing || this.state.busyLoggingIn) {
footer = <div className="mx_AuthBody_paddedFooter">
<div className="mx_AuthBody_paddedFooter_title">
<InlineSpinner w={20} h={20} />
{ this.props.isSyncing ? _t("Syncing...") : _t("Signing In...") }
</div>
{ this.props.isSyncing && <div className="mx_AuthBody_paddedFooter_subtitle">
{_t("If you've joined lots of rooms, this might take a while")}
</div> }
</div>;
} else {
footer = (
<a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#">
{ _t('Create account') }
</a>
);
}
return (
<AuthPage>
<AuthHeader />
<AuthHeader disableLanguageSelector={this.props.isSyncing || this.state.busyLoggingIn} />
<AuthBody>
<h2>
{_t('Sign in')}
@ -670,9 +697,7 @@ export default createReactClass({
{ serverDeadSection }
{ this.renderServerComponent() }
{ this.renderLoginComponentForStep() }
<a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#">
{ _t('Create account') }
</a>
{ footer }
</AuthBody>
</AuthPage>
);

View file

@ -108,16 +108,15 @@ export default class SetupEncryptionBody extends React.Component {
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>;
} else if (phase === PHASE_INTRO) {
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
const ButtonPlaceholder = sdk.getComponent("elements.ButtonPlaceholder");
return (
<div>
<p>{_t(
"Open an existing session & use it to verify this one, " +
"Use an existing session 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>",
"If you cant access one, <button>use your recovery key or recovery passphrase.</button>",
{}, {
button: sub => <AccessibleButton element="span"
className="mx_linkButton"
@ -133,6 +132,7 @@ export default class SetupEncryptionBody extends React.Component {
>
{_t("Skip")}
</AccessibleButton>
<ButtonPlaceholder>{_t("Use your other device to continue…")}</ButtonPlaceholder>
</div>
</div>
);

View file

@ -16,12 +16,17 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
export default createReactClass({
displayName: 'AuthHeader',
propTypes: {
disableLanguageSelector: PropTypes.bool,
},
render: function() {
const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo');
const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector');
@ -29,7 +34,7 @@ export default createReactClass({
return (
<div className="mx_AuthHeader">
<AuthHeaderLogo />
<LanguageSelector />
<LanguageSelector disabled={this.props.disableLanguageSelector} />
</div>
);
},

View file

@ -28,12 +28,14 @@ function onChange(newLang) {
}
}
export default function LanguageSelector() {
export default function LanguageSelector({disabled}) {
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
return <LanguageDropdown className="mx_AuthBody_language"
return <LanguageDropdown
className="mx_AuthBody_language"
onOptionChange={onChange}
value={getCurrentLanguage()}
disabled={disabled}
/>;
}

View file

@ -23,6 +23,7 @@ import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import AccessibleButton from "../elements/AccessibleButton";
/**
* A pure UI component which displays a username/password form.
@ -44,6 +45,7 @@ export default class PasswordLogin extends React.Component {
loginIncorrect: PropTypes.bool,
disableSubmit: PropTypes.bool,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
busy: PropTypes.bool,
};
static defaultProps = {
@ -265,12 +267,16 @@ export default class PasswordLogin extends React.Component {
if (this.props.onForgotPasswordClick) {
forgotPasswordJsx = <span>
{_t('Not sure of your password? <a>Set a new one</a>', {}, {
a: sub => <a className="mx_Login_forgot"
a: sub => (
<AccessibleButton
className="mx_Login_forgot"
disabled={this.props.busy}
kind="link"
onClick={this.onForgotPasswordClick}
href="#"
>
{sub}
</a>,
</AccessibleButton>
),
})}
</span>;
}
@ -332,11 +338,11 @@ export default class PasswordLogin extends React.Component {
disabled={this.props.disableSubmit}
/>
{forgotPasswordJsx}
<input className="mx_Login_submit"
{ !this.props.busy && <input className="mx_Login_submit"
type="submit"
value={_t('Sign in')}
disabled={this.props.disableSubmit}
/>
/> }
</form>
</div>
);

View file

@ -26,6 +26,7 @@ import { getHostingLink } from '../../../utils/HostingLink';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {MenuItem} from "../../structures/ContextMenu";
import * as sdk from "../../../index";
import {getHomePageUrl} from "../../../utils/pages";
export default class TopLeftMenu extends React.Component {
static propTypes = {
@ -47,15 +48,7 @@ export default class TopLeftMenu extends React.Component {
}
hasHomePage() {
const config = SdkConfig.get();
const pagesConfig = config.embeddedPages;
if (pagesConfig && pagesConfig.homeUrl) {
return true;
}
// This is a deprecated config option for the home page
// (despite the name, given we also now have a welcome
// page, which is not the same).
return !!config.welcomePageUrl;
return !!getHomePageUrl(SdkConfig.get());
}
render() {

View file

@ -137,12 +137,20 @@ export default class BugReportDialog extends React.Component {
);
}
let warning;
if (window.Modernizr && Object.values(window.Modernizr).some(support => support === false)) {
warning = <p><b>
{ _t("Reminder: Your browser is unsupported, so your experience may be unpredictable.") }
</b></p>;
}
return (
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
title={_t('Submit debug logs')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ warning }
<p>
{ _t(
"Debug logs contain application usage data including your " +

View file

@ -24,6 +24,7 @@ import withValidation from '../elements/Validation';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {Key} from "../../../Keyboard";
import SettingsStore from "../../../settings/SettingsStore";
export default createReactClass({
displayName: 'CreateRoomDialog',
@ -35,6 +36,7 @@ export default createReactClass({
const config = SdkConfig.get();
return {
isPublic: false,
isEncrypted: true,
name: "",
topic: "",
alias: "",
@ -62,6 +64,11 @@ export default createReactClass({
if (this.state.noFederate) {
createOpts.creation_content = {'m.federate': false};
}
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
opts.encryption = this.state.isEncrypted;
}
return opts;
},
@ -127,6 +134,10 @@ export default createReactClass({
this.setState({isPublic});
},
onEncryptedChange(isEncrypted) {
this.setState({isEncrypted});
},
onAliasChange(alias) {
this.setState({alias});
},
@ -166,11 +177,10 @@ export default createReactClass({
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
let privateLabel;
let publicLabel;
let publicPrivateLabel;
let aliasField;
if (this.state.isPublic) {
publicLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
publicPrivateLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
const domain = MatrixClientPeg.get().getDomain();
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">
@ -178,7 +188,20 @@ export default createReactClass({
</div>
);
} else {
privateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
publicPrivateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
}
let e2eeSection;
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
e2eeSection = <React.Fragment>
<LabelledToggleSwitch
label={ _t("Enable end-to-end encryption")}
onChange={this.onEncryptedChange}
value={this.state.isEncrypted}
className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests
/>
<p>{ _t("You cant disable this later. Bridges & most bots wont work yet.") }</p>
</React.Fragment>;
}
const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
@ -189,10 +212,10 @@ export default createReactClass({
<form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
<div className="mx_Dialog_content">
<Field ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} />
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} className="mx_CreateRoomDialog_topic" />
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
{ privateLabel }
{ publicLabel }
{ publicPrivateLabel }
{ e2eeSection }
{ aliasField }
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>

View file

@ -1,6 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -23,71 +23,109 @@ import Analytics from '../../../Analytics';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler';
import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
continueText: _t("Single Sign On"),
continueKind: "danger",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
body: _t("Are you sure you want to deactivate your account? This is irreversible."),
continueText: _t("Confirm account deactivation"),
continueKind: "danger",
},
};
// This is the same as aestheticsForStagePhases in InteractiveAuthDialog minus the `title`
const DEACTIVATE_AESTHETICS = {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
[PasswordAuthEntry.LOGIN_TYPE]: {
[DEFAULT_PHASE]: {
body: _t("To continue, please enter your password:"),
},
},
};
export default class DeactivateAccountDialog extends React.Component {
constructor(props) {
super(props);
this._onOk = this._onOk.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
this.state = {
password: "",
busy: false,
shouldErase: false,
errStr: null,
authData: null, // for UIA
// A few strings that are passed to InteractiveAuth for design or are displayed
// next to the InteractiveAuth component.
bodyText: null,
continueText: null,
continueKind: null,
};
}
_onPasswordFieldChange(ev) {
this.setState({
password: ev.target.value,
MatrixClientPeg.get().deactivateAccount(null, false).then(r => {
// If we got here, oops. The server didn't require any auth.
// Our application lifecycle will catch the error and do the logout bits.
// We'll try to log something in an vain attempt to record what happened (storage
// is also obliterated on logout).
console.warn("User's account got deactivated without confirmation: Server had no auth");
this.setState({errStr: _t("Server did not require any authentication")});
}).catch(e => {
if (e && e.httpStatus === 401 && e.data) {
// Valid UIA response
this.setState({authData: e.data});
} else {
this.setState({errStr: _t("Server did not return valid authentication information.")});
}
});
}
_onEraseFieldChange(ev) {
this.setState({
shouldErase: ev.target.checked,
});
_onStagePhaseChange = (stage, phase) => {
const aesthetics = DEACTIVATE_AESTHETICS[stage];
let bodyText = null;
let continueText = null;
let continueKind = null;
if (aesthetics) {
const phaseAesthetics = aesthetics[phase];
if (phaseAesthetics && phaseAesthetics.body) bodyText = phaseAesthetics.body;
if (phaseAesthetics && phaseAesthetics.continueText) continueText = phaseAesthetics.continueText;
if (phaseAesthetics && phaseAesthetics.continueKind) continueKind = phaseAesthetics.continueKind;
}
async _onOk() {
this.setState({busy: true});
try {
// This assumes that the HS requires password UI auth
// for this endpoint. In reality it could be any UI auth.
const auth = {
type: 'm.login.password',
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/riot-web/issues/10312
user: MatrixClientPeg.get().credentials.userId,
identifier: {
type: "m.id.user",
user: MatrixClientPeg.get().credentials.userId,
},
password: this.state.password,
this.setState({bodyText, continueText, continueKind});
};
await MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase);
} catch (err) {
let errStr = _t('Unknown error');
// https://matrix.org/jira/browse/SYN-744
if (err.httpStatus === 401 || err.httpStatus === 403) {
errStr = _t('Incorrect password');
}
this.setState({
busy: false,
errStr: errStr,
});
_onUIAuthFinished = (success, result, extra) => {
if (success) return; // great! makeRequest() will be called too.
if (result === ERROR_USER_CANCELLED) {
this._onCancel();
return;
}
console.error("Error during UI Auth:", {result, extra});
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
};
_onUIAuthComplete = (auth) => {
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
// Deactivation worked - logout & close this dialog
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
this.props.onFinished(true);
}
}).catch(e => {
console.error(e);
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
});
};
_onEraseFieldChange = (ev) => {
this.setState({
shouldErase: ev.target.checked,
});
};
_onCancel() {
this.props.onFinished(false);
@ -95,34 +133,36 @@ export default class DeactivateAccountDialog extends React.Component {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Loader = sdk.getComponent("elements.Spinner");
let passwordBoxClass = '';
let error = null;
if (this.state.errStr) {
error = <div className="error">
{ this.state.errStr }
</div>;
passwordBoxClass = 'error';
}
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
const okEnabled = this.state.password && !this.state.busy;
let cancelButton = null;
if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
{ _t("Cancel") }
</button>;
let auth = <div>{_t("Loading...")}</div>;
if (this.state.authData) {
auth = (
<div>
{this.state.bodyText}
<InteractiveAuth
matrixClient={MatrixClientPeg.get()}
authData={this.state.authData}
makeRequest={this._onUIAuthComplete}
onAuthFinished={this._onUIAuthFinished}
onStagePhaseChange={this._onStagePhaseChange}
continueText={this.state.continueText}
continueKind={this.state.continueKind}
/>
</div>
);
}
const Field = sdk.getComponent('elements.Field');
// this is on purpose not a <form /> to prevent Enter triggering submission, to further prevent accidents
return (
<BaseDialog className="mx_DeactivateAccountDialog"
onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
titleClass="danger"
title={_t("Deactivate Account")}
>
@ -172,28 +212,10 @@ export default class DeactivateAccountDialog extends React.Component {
</label>
</p>
<p>{ _t("To continue, please enter your password:") }</p>
<Field
type="password"
label={_t('Password')}
onChange={this._onPasswordFieldChange}
value={this.state.password}
className={passwordBoxClass}
/>
{error}
{auth}
</div>
{ error }
</div>
<div className="mx_Dialog_buttons">
<button
className="mx_Dialog_primary danger"
onClick={this._onOk}
disabled={!okEnabled}
>
{ okLabel }
</button>
{ cancelButton }
</div>
</BaseDialog>
);

View file

@ -131,7 +131,7 @@ export default class DeviceVerifyDialog extends React.Component {
} else {
this._verifier = request.verifier;
}
} else if (verifyingOwnDevice && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
} else if (verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
this._request = await client.requestVerification(this.props.userId, [
verificationMethods.SAS,
SHOW_QR_CODE_METHOD,

View file

@ -574,7 +574,7 @@ export default class InviteDialog extends React.PureComponent {
const createRoomOptions = {inlineErrors: true};
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
// Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room.
const client = MatrixClientPeg.get();

View file

@ -85,7 +85,7 @@ export default function KeySignatureUploadFailedDialog({
<span>{_t("Upload completed")}</span> :
cancelled ?
<span>{_t("Cancelled signature upload")}</span> :
<span>{_t("Unabled to upload")}</span>}
<span>{_t("Unable to upload")}</span>}
<DialogButtons
primaryButton={_t("OK")}
hasCancel={false}

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.
@ -14,15 +15,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
import * as React from 'react';
import * as PropTypes from 'prop-types';
import {Room} from "matrix-js-sdk/src/models/room";
import {User} from "matrix-js-sdk/src/models/user";
import {Group} from "matrix-js-sdk/src/models/group";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import QRCode from 'qrcode-react';
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import * as ContextMenu from "../../structures/ContextMenu";
import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext, selectText} from "../../../utils/strings";
const socials = [
{
@ -52,7 +58,18 @@ const socials = [
},
];
export default class ShareDialog extends React.Component {
interface IProps {
onFinished: () => void;
target: Room | User | Group | RoomMember | MatrixEvent;
permalinkCreator: RoomPermalinkCreator;
}
interface IState {
linkSpecificEvent: boolean;
permalinkCreator: RoomPermalinkCreator;
}
export default class ShareDialog extends React.PureComponent<IProps, IState> {
static propTypes = {
onFinished: PropTypes.func.isRequired,
target: PropTypes.oneOfType([
@ -64,55 +81,45 @@ export default class ShareDialog extends React.Component {
]).isRequired,
};
protected closeCopiedTooltip: () => void;
constructor(props) {
super(props);
this.onCopyClick = this.onCopyClick.bind(this);
this.onLinkSpecificEventCheckboxClick = this.onLinkSpecificEventCheckboxClick.bind(this);
let permalinkCreator: RoomPermalinkCreator = null;
if (props.target instanceof Room) {
permalinkCreator = new RoomPermalinkCreator(props.target);
permalinkCreator.load();
}
this.state = {
// MatrixEvent defaults to share linkSpecificEvent
linkSpecificEvent: this.props.target instanceof MatrixEvent,
permalinkCreator,
};
this._link = createRef();
}
static _selectText(target) {
const range = document.createRange();
range.selectNodeContents(target);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
static onLinkClick(e) {
e.preventDefault();
const {target} = e;
ShareDialog._selectText(target);
selectText(e.target);
}
onCopyClick(e) {
async onCopyClick(e) {
e.preventDefault();
const target = e.target; // copy target before we go async and React throws it away
ShareDialog._selectText(this._link.current);
let successful;
try {
successful = document.execCommand('copy');
} catch (err) {
console.error('Failed to copy: ', err);
}
const buttonRect = e.target.getBoundingClientRect();
const successful = await copyPlaintext(this.getUrl());
const buttonRect = target.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
// Drop a reference to this close handler for componentWillUnmount
this.closeCopiedTooltip = e.target.onmouseleave = close;
this.closeCopiedTooltip = target.onmouseleave = close;
}
onLinkSpecificEventCheckboxClick() {
@ -121,24 +128,38 @@ export default class ShareDialog extends React.Component {
});
}
componentDidMount() {
if (this.props.target instanceof Room) {
const permalinkCreator = new RoomPermalinkCreator(this.props.target);
permalinkCreator.load();
this.setState({permalinkCreator});
}
}
componentWillUnmount() {
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
}
render() {
let title;
getUrl() {
let matrixToUrl;
if (this.props.target instanceof Room) {
if (this.state.linkSpecificEvent) {
const events = this.props.target.getLiveTimeline().getEvents();
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
} else {
matrixToUrl = this.state.permalinkCreator.forRoom();
}
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
matrixToUrl = makeUserPermalink(this.props.target.userId);
} else if (this.props.target instanceof Group) {
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
} else if (this.props.target instanceof MatrixEvent) {
if (this.state.linkSpecificEvent) {
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
} else {
matrixToUrl = this.props.permalinkCreator.forRoom();
}
}
return matrixToUrl;
}
render() {
let title;
let checkbox;
if (this.props.target instanceof Room) {
@ -156,18 +177,10 @@ export default class ShareDialog extends React.Component {
</label>
</div>;
}
if (this.state.linkSpecificEvent) {
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
} else {
matrixToUrl = this.state.permalinkCreator.forRoom();
}
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
title = _t('Share User');
matrixToUrl = makeUserPermalink(this.props.target.userId);
} else if (this.props.target instanceof Group) {
title = _t('Share Community');
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
} else if (this.props.target instanceof MatrixEvent) {
title = _t('Share Room Message');
checkbox = <div>
@ -179,14 +192,9 @@ export default class ShareDialog extends React.Component {
{ _t('Link to selected message') }
</label>
</div>;
if (this.state.linkSpecificEvent) {
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
} else {
matrixToUrl = this.props.permalinkCreator.forRoom();
}
}
const matrixToUrl = this.getUrl();
const encodedUrl = encodeURIComponent(matrixToUrl);
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@ -197,8 +205,7 @@ export default class ShareDialog extends React.Component {
>
<div className="mx_ShareDialog_content">
<div className="mx_ShareDialog_matrixto">
<a ref={this._link}
href={matrixToUrl}
<a href={matrixToUrl}
onClick={ShareDialog.onLinkClick}
className="mx_ShareDialog_matrixto_link"
>
@ -217,17 +224,18 @@ export default class ShareDialog extends React.Component {
<QRCode value={matrixToUrl} size={256} logoWidth={48} logo={require("../../../../res/img/matrix-m.svg")} />
</div>
<div className="mx_ShareDialog_social_container">
{
socials.map((social) => <a rel="noreferrer noopener"
{ socials.map((social) => (
<a
rel="noreferrer noopener"
target="_blank"
key={social.name}
name={social.name}
title={social.name}
href={social.url(encodedUrl)}
className="mx_ShareDialog_social_icon"
>
<img src={social.img} alt={social.name} height={64} width={64} />
</a>)
}
</a>
)) }
</div>
</div>
</div>

View file

@ -48,7 +48,7 @@ export default class VerificationRequestDialog extends React.Component {
const member = this.props.member ||
otherUserId && MatrixClientPeg.get().getUser(otherUserId);
const title = request && request.isSelfVerification ?
_t("Verify this session") : _t("Verification Request");
_t("Verify other session") : _t("Verification Request");
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
contentId="mx_Dialog_content"
@ -61,7 +61,6 @@ export default class VerificationRequestDialog extends React.Component {
verificationRequestPromise={this.props.verificationRequestPromise}
onClose={this.props.onFinished}
member={member}
inDialog={true}
/>
</BaseDialog>;
}

View file

@ -283,7 +283,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
title = _t("Recovery key mismatch");
content = <div>
<p>{_t(
"Backup could not be decrypted with this key: " +
"Backup could not be decrypted with this recovery key: " +
"please verify that you entered the correct recovery key.",
)}</p>
</div>;
@ -291,7 +291,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
title = _t("Incorrect recovery passphrase");
content = <div>
<p>{_t(
"Backup could not be decrypted with this passphrase: " +
"Backup could not be decrypted with this recovery passphrase: " +
"please verify that you entered the correct recovery passphrase.",
)}</p>
</div>;

View file

@ -119,14 +119,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
if (hasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter secret storage passphrase");
title = _t("Enter recovery passphrase");
let keyStatus;
if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. Please verify that you " +
"entered the correct passphrase.",
"Unable to access secret storage. " +
"Please verify that you entered the correct recovery passphrase.",
)}
</div>;
} else {
@ -135,13 +135,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
content = <div>
<p>{_t(
"<b>Warning</b>: You should only access secret storage " +
"from a trusted computer.", {},
"<b>Warning</b>: You should only do this on a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other sessions by entering your passphrase.",
"identity for verifying other sessions by entering your recovery passphrase.",
)}</p>
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}>
@ -164,7 +163,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
/>
</form>
{_t(
"If you've forgotten your passphrase you can "+
"If you've forgotten your recovery passphrase you can "+
"<button1>use your recovery key</button1> or " +
"<button2>set up new recovery options</button2>."
, {}, {
@ -183,7 +182,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
})}
</div>;
} else {
title = _t("Enter secret storage recovery key");
title = _t("Enter recovery key");
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@ -193,8 +192,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} else if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. Please verify that you " +
"entered the correct recovery key.",
"Unable to access secret storage. " +
"Please verify that you entered the correct recovery key.",
)}
</div>;
} else if (this.state.recoveryKeyValid) {
@ -209,8 +208,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
content = <div>
<p>{_t(
"<b>Warning</b>: You should only access secret storage " +
"from a trusted computer.", {},
"<b>Warning</b>: You should only do this on a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(

View file

@ -136,22 +136,21 @@ export default class AppTile extends React.Component {
* If url can not be parsed, it is returned unmodified.
*/
_addWurlParams(urlString) {
const u = url.parse(urlString);
if (!u) {
console.error("_addWurlParams", "Invalid URL", urlString);
return url;
try {
const parsed = new URL(urlString);
// TODO: Replace these with proper widget params
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
parsed.searchParams.set('widgetId', this.props.app.id);
parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
// Replace the encoded dollar signs back to dollar signs. They have no special meaning
// in HTTP, but URL parsers encode them anyways.
return parsed.toString().replace(/%24/g, '$');
} catch (e) {
console.error("Failed to add widget URL params:", e);
return urlString;
}
const params = qs.parse(u.query);
// Append widget ID to query parameters
params.widgetId = this.props.app.id;
// Append current / parent URL, minus the hash because that will change when
// we view a different room (ie. may change for persistent widgets)
params.parentUrl = window.location.href.split('#', 2)[0];
u.search = undefined;
u.query = params;
return u.format();
}
isMixedContent() {
@ -270,7 +269,9 @@ export default class AppTile extends React.Component {
if (this.props.show && this.state.hasPermissionToLoad) {
this.setScalarToken();
}
} else if (nextProps.show && !this.props.show) {
}
if (nextProps.show && !this.props.show) {
// We assume that persisted widgets are loaded and don't need a spinner.
if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) {
this.setState({
@ -281,7 +282,9 @@ export default class AppTile extends React.Component {
if (this.state.hasPermissionToLoad) {
this.setScalarToken();
}
} else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
}
if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
this.setState({
widgetPageTitle: nextProps.widgetPageTitle,
});
@ -333,6 +336,28 @@ export default class AppTile extends React.Component {
});
}
/**
* Ends all widget interaction, such as cancelling calls and disabling webcams.
* @private
*/
_endWidgetActions() {
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
if (this._appFrame.current) {
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Riot instance is located.
this._appFrame.current.src = 'about:blank';
}
// Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey);
}
/* If user has permission to modify widgets, delete the widget,
* otherwise revoke access for the widget to load in the user's browser
*/
@ -354,18 +379,7 @@ export default class AppTile extends React.Component {
}
this.setState({deleting: true});
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
if (this._appFrame.current) {
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Riot instance is located.
this._appFrame.current.src = 'about:blank';
}
this._endWidgetActions();
WidgetUtils.setRoomWidget(
this.props.room.roomId,
@ -530,6 +544,10 @@ export default class AppTile extends React.Component {
if (this.props.userWidget) {
this._onMinimiseClick();
} else {
if (this.props.show) {
// if we were being shown, end the widget as we're about to be minimized.
this._endWidgetActions();
}
dis.dispatch({
action: 'appsDrawer',
show: !this.props.show,

View file

@ -0,0 +1,19 @@
/*
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.
*/
export default function ButtonPlaceholder(props) {
return <div class="mx_ButtonPlaceholder">{props.children}</div>;
}

View file

@ -35,6 +35,9 @@ export default class LabelledToggleSwitch extends React.Component {
// True to put the toggle in front of the label
// Default false.
toggleInFront: PropTypes.bool,
// Additional class names to append to the switch. Optional.
className: PropTypes.string,
};
render() {
@ -50,8 +53,9 @@ export default class LabelledToggleSwitch extends React.Component {
secondPart = temp;
}
const classes = `mx_SettingsFlag ${this.props.className || ""}`;
return (
<div className="mx_SettingsFlag">
<div className={classes}>
{firstPart}
{secondPart}
</div>

View file

@ -114,6 +114,7 @@ export default class LanguageDropdown extends React.Component {
searchEnabled={true}
value={value}
label={_t("Language Dropdown")}
disabled={this.props.disabled}
>
{ options }
</Dropdown>;

View file

@ -26,6 +26,7 @@ import Modal from '../../../Modal';
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
import RoomContext from "../../../contexts/RoomContext";
import SettingsStore from '../../../settings/SettingsStore';
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@ -48,7 +49,7 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
};
let e2eInfoCallback = null;
if (mxEvent.isEncrypted()) {
if (mxEvent.isEncrypted() && !SettingsStore.getValue("feature_cross_signing")) {
e2eInfoCallback = onCryptoClick;
}

View file

@ -34,6 +34,7 @@ import {pillifyLinks, unmountPills} from '../../../utils/pillify';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext} from "../../../utils/strings";
export default createReactClass({
displayName: 'TextualBody',
@ -69,23 +70,6 @@ export default createReactClass({
};
},
copyToClipboard: function(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
let successful = false;
try {
successful = document.execCommand('copy');
} catch (err) {
console.log('Unable to copy');
}
document.body.removeChild(textArea);
return successful;
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._content = createRef();
@ -277,17 +261,17 @@ export default createReactClass({
Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
const button = document.createElement("span");
button.className = "mx_EventTile_copyButton";
button.onclick = (e) => {
const copyCode = button.parentNode.getElementsByTagName("code")[0];
const successful = this.copyToClipboard(copyCode.textContent);
button.onclick = async () => {
const copyCode = button.parentNode.getElementsByTagName("pre")[0];
const successful = await copyPlaintext(copyCode.textContent);
const buttonRect = e.target.getBoundingClientRect();
const buttonRect = button.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
e.target.onmouseleave = close;
button.onmouseleave = close;
};
// Wrap a div around <pre> so that the copy button can be correctly positioned

View file

@ -0,0 +1,72 @@
/*
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 classNames from 'classnames';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
export default class TileErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
};
}
static getDerivedStateFromError(error) {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
_onBugReport = () => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) {
return;
}
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash-tile',
});
};
render() {
if (this.state.error) {
const { mxEvent } = this.props;
const classes = {
mx_EventTile: true,
mx_EventTile_info: true,
mx_EventTile_content: true,
mx_EventTile_tileError: true,
};
return (<div className={classNames(classes)}>
<div className="mx_EventTile_line">
<span>
{_t("Can't load this message")}
{ mxEvent && ` (${mxEvent.getType()})` }
<a onClick={this._onBugReport} href="#">
{_t("Submit logs")}
</a>
</span>
</div>
</div>);
}
return this.props.children;
}
}

View file

@ -32,7 +32,7 @@ import {_t} from "../../../languageHandler";
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
const EncryptionPanel = (props) => {
const {verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted, inDialog} = 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
@ -133,7 +133,7 @@ const EncryptionPanel = (props) => {
isSelfVerification={isSelfVerification}
waitingForOtherParty={requested && initiatedByMe}
waitingForNetwork={requested && !initiatedByMe}
inDialog={inDialog} />
inDialog={layout === "dialog"} />
</React.Fragment>);
} else {
return (<React.Fragment>
@ -145,7 +145,7 @@ const EncryptionPanel = (props) => {
member={member}
request={request}
key={request.channel.transactionId}
inDialog={inDialog}
inDialog={layout === "dialog"}
phase={phase}
device={device} />
</React.Fragment>);

View file

@ -63,7 +63,7 @@ const _disambiguateDevices = (devices) => {
};
export const getE2EStatus = (cli, userId, devices) => {
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (!SettingsStore.getValue("feature_cross_signing")) {
const hasUnverifiedDevice = devices.some((device) => device.isUnverified());
return hasUnverifiedDevice ? "warning" : "verified";
}
@ -111,7 +111,7 @@ async function openDMForUser(matrixClient, userId) {
dmUserId: userId,
};
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
// Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room.
const usersToDevicesMap = await matrixClient.downloadKeys([userId]);
@ -166,7 +166,7 @@ function DeviceItem({userId, device}) {
// cross-signing so that other users can then safely trust you.
// For other people's devices, the more general verified check that
// includes locally verified devices can be used.
const isVerified = (isMe && SettingsStore.isFeatureEnabled("feature_cross_signing")) ?
const isVerified = (isMe && SettingsStore.getValue("feature_cross_signing")) ?
deviceTrust.isCrossSigningVerified() :
deviceTrust.isVerified();
@ -237,7 +237,7 @@ function DevicesSection({devices, userId, loading}) {
// cross-signing so that other users can then safely trust you.
// For other people's devices, the more general verified check that
// includes locally verified devices can be used.
const isVerified = (isMe && SettingsStore.isFeatureEnabled("feature_cross_signing")) ?
const isVerified = (isMe && SettingsStore.getValue("feature_cross_signing")) ?
deviceTrust.isCrossSigningVerified() :
deviceTrust.isVerified();
@ -1298,7 +1298,7 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
const userTrust = cli.checkUserTrust(member.userId);
const userVerified = userTrust.isCrossSigningVerified();
const isMe = member.userId === cli.getUserId();
const canVerify = SettingsStore.isFeatureEnabled("feature_cross_signing") &&
const canVerify = SettingsStore.getValue("feature_cross_signing") &&
homeserverSupportsCrossSigning && !userVerified && !isMe;
const setUpdating = (updating) => {

View file

@ -18,7 +18,9 @@ import React from 'react';
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import {_t} from "../../../languageHandler";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.room_settings.RoomPublishSetting")
export default class RoomPublishSetting extends React.PureComponent {
constructor(props) {
super(props);

View file

@ -81,12 +81,14 @@ export default createReactClass({
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
switch (action.action) {
case 'appsDrawer':
// Note: these booleans are awkward because localstorage is fundamentally
// string-based. We also do exact equality on the strings later on.
if (action.show) {
localStorage.removeItem(hideWidgetKey);
localStorage.setItem(hideWidgetKey, "false");
} else {
// Store hidden state of widget
// Don't show if previously hidden
localStorage.setItem(hideWidgetKey, true);
localStorage.setItem(hideWidgetKey, "true");
}
break;

View file

@ -39,6 +39,7 @@ import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import * as sdk from '../../../index';
import {Key} from "../../../Keyboard";
import {EMOTICON_TO_EMOJI} from "../../../emoji";
import {CommandCategories, CommandMap, parseCommandString} from "../../../SlashCommands";
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
@ -84,6 +85,7 @@ export default class BasicMessageEditor extends React.Component {
super(props);
this.state = {
autoComplete: null,
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
};
this._editorRef = null;
this._autocompleteRef = null;
@ -92,10 +94,10 @@ export default class BasicMessageEditor extends React.Component {
this._isIMEComposing = false;
this._hasTextSelected = false;
this._emoticonSettingHandle = null;
this._shouldShowPillAvatarSettingHandle = null;
}
// TODO: [REACT-WARNING] Move into better lifecycle position
UNSAFE_componentWillUpdate(prevProps) { // eslint-disable-line camelcase
componentDidUpdate(prevProps) {
if (this.props.placeholder !== prevProps.placeholder && this.props.placeholder) {
const {isEmpty} = this.props.model;
if (isEmpty) {
@ -163,7 +165,16 @@ export default class BasicMessageEditor extends React.Component {
}
this.setState({autoComplete: this.props.model.autoComplete});
this.historyManager.tryPush(this.props.model, selection, inputType, diff);
TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, !this.props.model.isEmpty);
let isTyping = !this.props.model.isEmpty;
// If the user is entering a command, only consider them typing if it is one which sends a message into the room
if (isTyping && this.props.model.parts[0].type === "command") {
const {cmd} = parseCommandString(this.props.model.parts[0].text);
if (!CommandMap.has(cmd) || CommandMap.get(cmd).category !== CommandCategories.messages) {
isTyping = false;
}
}
TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, isTyping);
if (this.props.onChange) {
this.props.onChange();
@ -509,10 +520,15 @@ export default class BasicMessageEditor extends React.Component {
this.setState({completionIndex});
}
_configureEmoticonAutoReplace() {
_configureEmoticonAutoReplace = () => {
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
this.props.model.setTransformCallback(shouldReplace ? this._replaceEmoticon : null);
}
};
_configureShouldShowPillAvatar = () => {
const showPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar");
this.setState({ showPillAvatar });
};
componentWillUnmount() {
document.removeEventListener("selectionchange", this._onSelectionChange);
@ -520,15 +536,17 @@ export default class BasicMessageEditor extends React.Component {
this._editorRef.removeEventListener("compositionstart", this._onCompositionStart, true);
this._editorRef.removeEventListener("compositionend", this._onCompositionEnd, true);
SettingsStore.unwatchSetting(this._emoticonSettingHandle);
SettingsStore.unwatchSetting(this._shouldShowPillAvatarSettingHandle);
}
componentDidMount() {
const model = this.props.model;
model.setUpdateCallback(this._updateEditorState);
this._emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null, () => {
this._configureEmoticonAutoReplace();
});
this._emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
this._configureEmoticonAutoReplace);
this._configureEmoticonAutoReplace();
this._shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
this._configureShouldShowPillAvatar);
const partCreator = model.partCreator;
// TODO: does this allow us to get rid of EditorStateTransfer?
// not really, but we could not serialize the parts, and just change the autoCompleter
@ -606,9 +624,12 @@ export default class BasicMessageEditor extends React.Component {
/>
</div>);
}
const classes = classNames("mx_BasicMessageComposer", {
const wrapperClasses = classNames("mx_BasicMessageComposer", {
"mx_BasicMessageComposer_input_error": this.state.showVisualBell,
});
const classes = classNames("mx_BasicMessageComposer_input", {
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,
});
const MessageComposerFormatBar = sdk.getComponent('rooms.MessageComposerFormatBar');
const shortcuts = {
@ -619,11 +640,11 @@ export default class BasicMessageEditor extends React.Component {
const {completionIndex} = this.state;
return (<div className={classes}>
return (<div className={wrapperClasses}>
{ autoComplete }
<MessageComposerFormatBar ref={ref => this._formatBarRef = ref} onAction={this._onFormatAction} shortcuts={shortcuts} />
<div
className="mx_BasicMessageComposer_input"
className={classes}
contentEditable="true"
tabIndex="0"
onBlur={this._onBlur}

View file

@ -20,7 +20,7 @@ import PropTypes from "prop-types";
import classNames from 'classnames';
import {_t, _td} from '../../../languageHandler';
import {useFeatureEnabled} from "../../../hooks/useSettings";
import {useSettingValue} from "../../../hooks/useSettings";
import AccessibleButton from "../elements/AccessibleButton";
import Tooltip from "../elements/Tooltip";
@ -62,7 +62,7 @@ const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => {
}, className);
let e2eTitle;
const crossSigning = useFeatureEnabled("feature_cross_signing");
const crossSigning = useSettingValue("feature_cross_signing");
if (crossSigning && isUser) {
e2eTitle = crossSigningUserTitles[status];
} else if (crossSigning && !isUser) {

View file

@ -59,6 +59,7 @@ const stateEventTileTypes = {
'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events': 'messages.TextualEvent',
'm.room.server_acl': 'messages.TextualEvent',
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
'im.vector.modular.widgets': 'messages.TextualEvent',
'm.room.tombstone': 'messages.TextualEvent',
'm.room.join_rules': 'messages.TextualEvent',
@ -322,7 +323,7 @@ export default createReactClass({
// If cross-signing is off, the old behaviour is to scream at the user
// as if they've done something wrong, which they haven't
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (!SettingsStore.getValue("feature_cross_signing")) {
this.setState({
verified: E2E_STATE.WARNING,
}, this.props.onHeightChanged);

View file

@ -56,7 +56,7 @@ export default createReactClass({
}
}
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
const { roomId } = this.props.member;
if (roomId) {
const isRoomEncrypted = cli.isRoomEncrypted(roomId);

View file

@ -270,7 +270,7 @@ export default class MessageComposer extends React.Component {
}
renderPlaceholderText() {
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
if (this.state.isQuoting) {
if (this.props.e2eStatus) {
return _t('Send an encrypted reply…');

View file

@ -363,17 +363,6 @@ export default class RoomBreadcrumbs extends React.Component {
badge = <div className={badgeClasses}>{r.formattedCount}</div>;
}
let dmIndicator;
if (this._isDmRoom(r.room) && !SettingsStore.isFeatureEnabled("feature_cross_signing")) {
dmIndicator = <img
src={require("../../../../res/img/icon_person.svg")}
className="mx_RoomBreadcrumbs_dmIndicator"
width="13"
height="15"
alt={_t("Direct Chat")}
/>;
}
return (
<AccessibleButton
className={classes}
@ -385,7 +374,6 @@ export default class RoomBreadcrumbs extends React.Component {
>
<RoomAvatar room={r.room} width={32} height={32} />
{badge}
{dmIndicator}
{tooltip}
</AccessibleButton>
);

View file

@ -168,7 +168,7 @@ export default createReactClass({
const joinRule = joinRules && joinRules.getContent().join_rule;
let privateIcon;
// Don't show an invite-only icon for DMs. Users know they're invite-only.
if (!dmUserId && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (!dmUserId && SettingsStore.getValue("feature_cross_signing")) {
if (joinRule == "invite") {
privateIcon = <InviteOnlyIcon />;
}

View file

@ -130,6 +130,10 @@ export default createReactClass({
this._updateE2eStatus();
},
onCrossSigningKeysChanged: function() {
this._updateE2eStatus();
},
onRoomTimeline: function(ev, room) {
if (!room) return;
if (room.roomId != this.props.room.roomId) return;
@ -142,7 +146,7 @@ export default createReactClass({
const cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
cli.on("userTrustStatusChanged", this.onUserVerificationChanged);
cli.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
this._updateE2eStatus();
},
@ -151,7 +155,7 @@ export default createReactClass({
if (!cli.isRoomEncrypted(this.props.room.roomId)) {
return;
}
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (!SettingsStore.getValue("feature_cross_signing")) {
return;
}
@ -267,6 +271,7 @@ export default createReactClass({
cli.removeListener("RoomState.events", this.onJoinRule);
cli.removeListener("RoomState.members", this.onRoomStateMember);
cli.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
cli.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
cli.removeListener("Room.timeline", this.onRoomTimeline);
}
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
@ -479,26 +484,10 @@ export default createReactClass({
let ariaLabel = name;
let dmIndicator;
let dmOnline;
/* Post-cross-signing we don't show DM indicators at all, instead relying on user
context to let them know when that is. */
if (dmUserId && !SettingsStore.isFeatureEnabled("feature_cross_signing")) {
dmIndicator = <img
src={require("../../../../res/img/icon_person.svg")}
className="mx_RoomTile_dm"
width="11"
height="13"
alt="dm"
/>;
}
const { room } = this.props;
const member = room.getMember(dmUserId);
if (
member && member.membership === "join" && room.getJoinedMemberCount() === 2 &&
SettingsStore.isFeatureEnabled("feature_presence_in_room_list")
) {
if (member && member.membership === "join" && room.getJoinedMemberCount() === 2) {
const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
dmOnline = <UserOnlineDot userId={dmUserId} />;
}
@ -527,7 +516,7 @@ export default createReactClass({
}
let privateIcon = null;
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
if (this.state.joinRule == "invite" && !dmUserId) {
privateIcon = <InviteOnlyIcon collapsedPanel={this.props.collapsed} />;
}
@ -557,7 +546,6 @@ export default createReactClass({
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} />
{ dmIndicator }
{ e2eIcon }
</div>
</div>

View file

@ -246,6 +246,7 @@ export default class Stickerpicker extends React.Component {
url: stickerpickerWidget.content.url,
name: stickerpickerWidget.content.name,
type: stickerpickerWidget.content.type,
data: stickerpickerWidget.content.data,
};
stickersContent = (

View file

@ -81,7 +81,9 @@ export default class CrossSigningPanel extends React.PureComponent {
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 sessionBackupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey();
const sessionBackupKeyCached = !!(sessionBackupKeyFromCache);
const sessionBackupKeyWellFormed = sessionBackupKeyFromCache instanceof Uint8Array;
const secretStorageKeyInAccount = await secretStorage.hasKey();
const homeserverSupportsCrossSigning =
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
@ -94,6 +96,7 @@ export default class CrossSigningPanel extends React.PureComponent {
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
sessionBackupKeyCached,
sessionBackupKeyWellFormed,
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
@ -143,6 +146,7 @@ export default class CrossSigningPanel extends React.PureComponent {
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
sessionBackupKeyCached,
sessionBackupKeyWellFormed,
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
@ -208,6 +212,16 @@ export default class CrossSigningPanel extends React.PureComponent {
);
}
let sessionBackupKeyWellFormedText = "";
if (sessionBackupKeyCached) {
sessionBackupKeyWellFormedText = ", ";
if (sessionBackupKeyWellFormed) {
sessionBackupKeyWellFormedText += _t("well formed");
} else {
sessionBackupKeyWellFormedText += _t("unexpected type");
}
}
return (
<div>
{summarisedStatus}
@ -232,7 +246,10 @@ export default class CrossSigningPanel extends React.PureComponent {
</tr>
<tr>
<td>{_t("Session backup key:")}</td>
<td>{sessionBackupKeyCached ? _t("cached locally") : _t("not found locally")}</td>
<td>
{sessionBackupKeyCached ? _t("cached locally") : _t("not found locally")}
{sessionBackupKeyWellFormedText}
</td>
</tr>
<tr>
<td>{_t("Secret storage public key:")}</td>

View file

@ -124,17 +124,22 @@ export default class DevicesPanel extends React.Component {
// pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
const numDevices = this.state.selectedDevices.length;
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
body: _t("Confirm deleting these sessions by using Single Sign On to prove your identity."),
body: _t("Confirm deleting these sessions by using Single Sign On to prove your identity.", {
count: numDevices,
}),
continueText: _t("Single Sign On"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("Confirm deleting these sessions"),
body: _t("Click the button below to confirm deleting these sessions."),
continueText: _t("Delete sessions"),
body: _t("Click the button below to confirm deleting these sessions.", {
count: numDevices,
}),
continueText: _t("Delete sessions", {count: numDevices}),
continueKind: "danger",
},
};

View file

@ -75,7 +75,7 @@ export default class KeyBackupPanel extends React.PureComponent {
async _checkKeyBackupStatus() {
try {
const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup();
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored();
const backupKeyStored = Boolean(await MatrixClientPeg.get().isKeyBackupKeyStored());
this.setState({
backupInfo,
backupSigStatus: trustInfo,
@ -326,7 +326,7 @@ export default class KeyBackupPanel extends React.PureComponent {
</AccessibleButton>
</div>
);
if (this.state.backupKeyStored && !SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (this.state.backupKeyStored && !SettingsStore.getValue("feature_cross_signing")) {
buttonRow = <p> {_t(
"Backup key stored in secret storage, but this feature is not " +
"enabled on this session. Please enable cross-signing in Labs to " +

View file

@ -33,6 +33,7 @@ const plEventsToLabels = {
"m.room.tombstone": _td("Upgrade the room"),
"m.room.encryption": _td("Enable room encryption"),
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
"im.vector.modular.widgets": _td("Modify widgets"),
};
@ -47,6 +48,7 @@ const plEventsToShow = {
"m.room.tombstone": {isState: true},
"m.room.encryption": {isState: true},
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
"im.vector.modular.widgets": {isState: true},
};

View file

@ -270,7 +270,7 @@ export default class SecurityUserSettingsTab extends React.Component {
// can remove this.
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
let crossSigning;
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
crossSigning = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>

View file

@ -63,7 +63,9 @@ export default class SetupEncryptionToast extends React.PureComponent {
{}, null, /* priority = */ false, /* static = */ true);
} else {
const Spinner = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
const modal = Modal.createDialog(
Spinner, null, 'mx_Dialog_spinner', /* priority */ false, /* static */ true,
);
try {
await accessSecretStorage();
await this._waitForCompletion();

View file

@ -20,8 +20,8 @@ import { _t } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import Spinner from "../elements/Spinner";
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
@replaceableComponent("views.verification.VerificationQREmojiOptions")
export default class VerificationQREmojiOptions extends React.Component {
@ -31,31 +31,17 @@ export default class VerificationQREmojiOptions extends React.Component {
onStartEmoji: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
qrProps: null,
};
this._prepareQrCode(props.request);
}
async _prepareQrCode(request: VerificationRequest) {
try {
const props = await VerificationQRCode.getPropsForRequest(request);
this.setState({qrProps: props});
} catch (e) {
console.error(e);
// We just won't show a QR code
}
}
render() {
let qrCode = <div className='mx_VerificationQREmojiOptions_noQR'><Spinner /></div>;
if (this.state.qrProps) {
qrCode = <VerificationQRCode {...this.state.qrProps} />;
const {request} = this.props;
const showQR = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
let qrCode;
if (showQR) {
qrCode = <VerificationQRCode qrCodeData={request.qrCodeData} />;
} else {
qrCode = <div className='mx_VerificationQREmojiOptions_noQR'><Spinner /></div>;
}
return (
<div>
{_t("Verify this session by completing one of the following:")}

View file

@ -20,6 +20,7 @@ import { _t, _td } from '../../../languageHandler';
import {PendingActionSpinner} from "../right_panel/EncryptionInfo";
import AccessibleButton from "../elements/AccessibleButton";
import DialogButtons from "../elements/DialogButtons";
import { fixupColorFonts } from '../../../utils/FontManager';
function capFirst(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
@ -44,6 +45,13 @@ export default class VerificationShowSas extends React.Component {
};
}
componentWillMount() {
// As this component is also used before login (during complete security),
// also make sure we have a working emoji font to display the SAS emojis here.
// This is also done from LoggedInView.
fixupColorFonts();
}
onMatchClick = () => {
this.setState({ pending: true });
this.props.onDone();
@ -125,19 +133,19 @@ export default class VerificationShowSas extends React.Component {
confirm = <DialogButtons
primaryButton={_t("They match")}
onPrimaryButtonClick={this.onMatchClick}
primaryButtonClass="mx_UserInfo_wideButton"
primaryButtonClass="mx_UserInfo_wideButton mx_VerificationShowSas_matchButton"
cancelButton={_t("They don't match")}
onCancel={this.onDontMatchClick}
cancelButtonClass="mx_UserInfo_wideButton"
cancelButtonClass="mx_UserInfo_wideButton mx_VerificationShowSas_noMatchButton"
/>;
} else {
confirm = <React.Fragment>
<AccessibleButton onClick={this.onMatchClick} kind="primary">
{ _t("They match") }
</AccessibleButton>
<AccessibleButton onClick={this.onDontMatchClick} kind="danger">
{ _t("They don't match") }
</AccessibleButton>
<AccessibleButton onClick={this.onMatchClick} kind="primary">
{ _t("They match") }
</AccessibleButton>
</React.Fragment>;
}

View file

@ -227,7 +227,7 @@ export async function ensureDMExists(client, userId) {
roomId = existingDMRoom.roomId;
} else {
let encryption;
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
encryption = canEncryptToAllUsers(client, [userId]);
}
roomId = await createRoom({encryption, dmUserId: userId, spinner: false, andView: false});

View file

@ -1,6 +1,6 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,11 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { walkDOMDepthFirst } from "./dom";
import { checkBlockNode } from "../HtmlUtils";
import {getPrimaryPermalinkEntity} from "../utils/permalinks/Permalinks";
import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
import { PartCreator } from "./parts";
function parseAtRoomMentions(text, partCreator) {
function parseAtRoomMentions(text: string, partCreator: PartCreator) {
const ATROOM = "@room";
const parts = [];
text.split(ATROOM).forEach((textPart, i, arr) => {
@ -37,7 +40,7 @@ function parseAtRoomMentions(text, partCreator) {
return parts;
}
function parseLink(a, partCreator) {
function parseLink(a: HTMLAnchorElement, partCreator: PartCreator) {
const {href} = a;
const resourceId = getPrimaryPermalinkEntity(href); // The room/user ID
const prefix = resourceId ? resourceId[0] : undefined; // First character of ID
@ -50,17 +53,17 @@ function parseLink(a, partCreator) {
if (href === a.textContent) {
return partCreator.plain(a.textContent);
} else {
return partCreator.plain(`[${a.textContent}](${href})`);
return partCreator.plain(`[${a.textContent.replace(/[[\\\]]/g, c => "\\" + c)}](${href})`);
}
}
}
}
function parseCodeBlock(n, partCreator) {
function parseCodeBlock(n: HTMLElement, partCreator: PartCreator) {
const parts = [];
let language = "";
if (n.firstChild && n.firstChild.nodeName === "CODE") {
for (const className of n.firstChild.classList) {
for (const className of (<HTMLElement>n.firstChild).classList) {
if (className.startsWith("language-")) {
language = className.substr("language-".length);
break;
@ -77,12 +80,17 @@ function parseCodeBlock(n, partCreator) {
return parts;
}
function parseHeader(el, partCreator) {
function parseHeader(el: HTMLElement, partCreator: PartCreator) {
const depth = parseInt(el.nodeName.substr(1), 10);
return partCreator.plain("#".repeat(depth) + " ");
}
function parseElement(n, partCreator, lastNode, state) {
interface IState {
listIndex: number[];
listDepth?: number;
}
function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLElement | undefined, state: IState) {
switch (n.nodeName) {
case "H1":
case "H2":
@ -92,7 +100,7 @@ function parseElement(n, partCreator, lastNode, state) {
case "H6":
return parseHeader(n, partCreator);
case "A":
return parseLink(n, partCreator);
return parseLink(<HTMLAnchorElement>n, partCreator);
case "BR":
return partCreator.newline();
case "EM":
@ -123,11 +131,11 @@ function parseElement(n, partCreator, lastNode, state) {
break;
}
case "OL":
state.listIndex.push(n.start || 1);
// fallthrough
state.listIndex.push((<HTMLOListElement>n).start || 1);
/* falls through */
case "UL":
state.listDepth = (state.listDepth || 0) + 1;
// fallthrough
/* falls through */
default:
// don't textify block nodes we'll descend into
if (!checkDescendInto(n)) {
@ -174,7 +182,7 @@ function prefixQuoteLines(isFirstNode, parts, partCreator) {
}
}
function parseHtmlMessage(html, partCreator, isQuotedMessage) {
function parseHtmlMessage(html: string, partCreator: PartCreator, isQuotedMessage: boolean) {
// no nodes from parsing here should be inserted in the document,
// as scripts in event handlers, etc would be executed then.
// we're only taking text, so that is fine
@ -182,7 +190,7 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) {
const parts = [];
let lastNode;
let inQuote = isQuotedMessage;
const state = {
const state: IState = {
listIndex: [],
};
@ -236,7 +244,7 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) {
break;
case "OL":
state.listIndex.pop();
// fallthrough
/* falls through */
case "UL":
state.listDepth -= 1;
break;
@ -249,9 +257,9 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) {
return parts;
}
export function parsePlainTextMessage(body, partCreator, isQuotedMessage) {
export function parsePlainTextMessage(body: string, partCreator: PartCreator, isQuotedMessage: boolean) {
const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n, collapses \r\n
const parts = lines.reduce((parts, line, i) => {
return lines.reduce((parts, line, i) => {
if (isQuotedMessage) {
parts.push(partCreator.plain(QUOTE_LINE_PREFIX));
}
@ -262,10 +270,9 @@ export function parsePlainTextMessage(body, partCreator, isQuotedMessage) {
}
return parts;
}, []);
return parts;
}
export function parseEvent(event, partCreator, {isQuotedMessage = false} = {}) {
export function parseEvent(event: MatrixEvent, partCreator: PartCreator, {isQuotedMessage = false} = {}) {
const content = event.getContent();
let parts;
if (content.format === "org.matrix.custom.html") {

View file

@ -1,6 +1,6 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -17,8 +17,9 @@ limitations under the License.
import Markdown from '../Markdown';
import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
import EditorModel from "./model";
export function mdSerialize(model) {
export function mdSerialize(model: EditorModel) {
return model.parts.reduce((html, part) => {
switch (part.type) {
case "newline":
@ -30,12 +31,12 @@ export function mdSerialize(model) {
return html + part.text;
case "room-pill":
case "user-pill":
return html + `[${part.text}](${makeGenericPermalink(part.resourceId)})`;
return html + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;
}
}, "");
}
export function htmlSerializeIfNeeded(model, {forceHTML = false} = {}) {
export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
const md = mdSerialize(model);
const parser = new Markdown(md);
if (!parser.isPlainText() || forceHTML) {
@ -43,7 +44,7 @@ export function htmlSerializeIfNeeded(model, {forceHTML = false} = {}) {
}
}
export function textSerialize(model) {
export function textSerialize(model: EditorModel) {
return model.parts.reduce((text, part) => {
switch (part.type) {
case "newline":
@ -60,11 +61,11 @@ export function textSerialize(model) {
}, "");
}
export function containsEmote(model) {
export function containsEmote(model: EditorModel) {
return startsWith(model, "/me ");
}
export function startsWith(model, prefix) {
export function startsWith(model: EditorModel, prefix: string) {
const firstPart = model.parts[0];
// part type will be "plain" while editing,
// and "command" while composing a message.
@ -73,18 +74,18 @@ export function startsWith(model, prefix) {
firstPart.text.startsWith(prefix);
}
export function stripEmoteCommand(model) {
export function stripEmoteCommand(model: EditorModel) {
// trim "/me "
return stripPrefix(model, "/me ");
}
export function stripPrefix(model, prefix) {
export function stripPrefix(model: EditorModel, prefix: string) {
model = model.clone();
model.removeText({index: 0, offset: 0}, prefix.length);
return model;
}
export function unescapeMessage(model) {
export function unescapeMessage(model: EditorModel) {
const {parts} = model;
if (parts.length) {
const firstPart = parts[0];

View file

@ -1892,5 +1892,74 @@
"The information being sent to us to help make Riot better includes:": "Информацията, която се изпраща за да ни помогне да подобрим Riot включва:",
"There are unknown sessions in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "В тази стая има непознати сесии: ако продължите без да ги потвърдите, ще е възможно някой да подслуша обаждането ви.",
"Review Sessions": "Прегледай сесиите",
"If you cancel now, you won't complete verifying the other user.": "Ако се откажете сега, няма да завършите верификацията на другия потребител."
"If you cancel now, you won't complete verifying the other user.": "Ако се откажете сега, няма да завършите верификацията на другия потребител.",
"Use Single Sign On to continue": "Използвайте Single Sign On за да продължите",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Потвърдете добавянето на този имейл адрес като потвърдите идентичността си чрез Single Sign On.",
"Single Sign On": "Single Sign On",
"Confirm adding email": "Потвърдете добавянето на имейл",
"Click the button below to confirm adding this email address.": "Кликнете бутона по-долу за да потвърдите добавянето на имейл адреса.",
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Потвърдете добавянето на този телефонен номер като докажете идентичността си чрез използване на Single Sign On.",
"Confirm adding phone number": "Потвърдете добавянето на телефонен номер",
"Click the button below to confirm adding this phone number.": "Кликнете бутона по-долу за да потвърдите добавянето на телефонния номер.",
"If you cancel now, you won't complete verifying your other session.": "Ако се откажете сега, няма да завършите потвърждаването на другата ви сесия.",
"If you cancel now, you won't complete your secret storage operation.": "Ако се откажете сега, няма да завърши операцията по секретно складиране.",
"Cancel entering passphrase?": "Откажете въвеждането на парола?",
"Setting up keys": "Настройка на ключове",
"Verify this session": "Потвърди тази сесия",
"Encryption upgrade available": "Има обновление на шифроването",
"Set up encryption": "Настрой шифроване",
"Unverified login. Was this you?": "Непотвърден вход. Вие ли бяхте?",
"Sign In or Create Account": "Влезте или Създайте профил",
"Use your account or create a new one to continue.": "Използвайте профила си или създайте нов за да продължите.",
"Create Account": "Създай профил",
"Sends a message as html, without interpreting it as markdown": "Изпраща съобщението като HTML, без да го интерпретира като Markdown",
"Verifies a user, session, and pubkey tuple": "Потвърждава потребител, сесия и двойка ключове",
"Unknown (user, session) pair:": "Непозната двойка (потребител, сесия):",
"Session already verified!": "Сесията вече е потвърдена!",
"WARNING: Session already verified, but keys do NOT MATCH!": "ВНИМАНИЕ: Сесията вече е потвърдена, но ключовете НЕ СЪВПАДАТ!",
"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!": "ВНИМАНИЕ: ПОТВЪРЖДАВАНЕТО НА КЛЮЧОВЕТЕ Е НЕУСПЕШНО! Подписващия ключ за %(userId)s и сесия %(deviceId)s е \"%(fprint)s\", което не съвпада с предоставения ключ \"%(fingerprint)s\". Това може би означава, че комуникацията ви бива прихваната!",
"The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "Предоставения от вас ключ за подписване съвпада с ключа за подписване получен от сесия %(deviceId)s на %(userId)s. Сесията е маркирана като потвърдена.",
"Displays information about a user": "Показва информация за потребителя",
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s промени името на стаята от %(oldRoomName)s на %(newRoomName)s.",
"%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s добави алтернативните адреси %(addresses)s към стаята.",
"%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s добави алтернативният адрес %(addresses)s към стаята.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s премахна алтернативните адреси %(addresses)s от стаята.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s премахна алтернативният адрес %(addresses)s от стаята.",
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s промени алтернативните адреси на стаята.",
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s промени основният и алтернативните адреси на стаята.",
"%(senderName)s changed the addresses for this room.": "%(senderName)s промени адресите на стаята.",
"Not Trusted": "Недоверено",
"%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) влезе в нова сесия без да я потвърди:",
"Ask this user to verify their session, or manually verify it below.": "Поискайте от този потребител да потвърди сесията си, или я потвърдете ръчно по-долу.",
"Manually Verify by Text": "Ръчно потвърждаване чрез текст",
"Interactively verify by Emoji": "Потвърдете интерактивно с Емоджи",
"Done": "Готово",
"a few seconds ago": "преди няколко секунди",
"about a minute ago": "преди около минута",
"%(num)s minutes ago": "преди %(num)s минути",
"about an hour ago": "преди около час",
"%(num)s hours ago": "преди %(num)s часа",
"about a day ago": "преди около ден",
"%(num)s days ago": "преди %(num)s дни",
"a few seconds from now": "след няколко секунди",
"about a minute from now": "след около минута",
"%(num)s minutes from now": "след %(num)s минути",
"about an hour from now": "след около час",
"%(num)s hours from now": "след %(num)s часа",
"about a day from now": "след около ден",
"%(num)s days from now": "след %(num)s дни",
"Show a presence dot next to DMs in the room list": "Показвай точка за присъствие до директните съобщения в списъка със стаи",
"Support adding custom themes": "Включи поддръжка за добавяне на собствени теми",
"Enable cross-signing to verify per-user instead of per-session (in development)": "Включи кръстосано-подписване за потвърждаване на потребител, вместо на отделни сесии (в процес на разработка)",
"Show padlocks on invite only rooms": "Показвай катинари на стаите изискващи покана",
"Show typing notifications": "Показвай уведомления за писане",
"Never send encrypted messages to unverified sessions from this session": "Никога не изпращай шифровани съобщения към непотвърдени сесии от тази сесия",
"Never send encrypted messages to unverified sessions in this room from this session": "Никога не изпращай шифровани съобщения към непотвърдени сесии в тази стая от тази сесия",
"Order rooms by name": "Подреждай стаите по име",
"Show rooms with unread notifications first": "Показвай първи стаите с непрочетени уведомления",
"Show shortcuts to recently viewed rooms above the room list": "Показвай преки пътища до скоро-прегледаните стаи над списъка със стаи",
"Enable message search in encrypted rooms": "Включи търсенето на съобщения в шифровани стаи",
"Keep secret storage passphrase in memory for this session": "Съхрани паролата за секретното складиране в паметта за тази сесия",
"How fast should messages be downloaded.": "Колко бързо да се изтеглят съобщенията.",
"Manually verify all remote sessions": "Ръчно потвърждаване на всички отдалечени сесии"
}

View file

@ -80,7 +80,7 @@
"Attachment": "Příloha",
"Autoplay GIFs and videos": "Automaticky přehrávat GIFy a videa",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Nelze se připojit k domovskému serveru zkontrolujte prosím své připojení, prověřte, zda je <a>SSL certifikát</a> vašeho domovského serveru důvěryhodný, a že některé z rozšíření prohlížeče neblokuje komunikaci.",
"Anyone who knows the room's link, apart from guests": "Kdokoliv s odkazem na místnost, kromě hostů",
"Anyone who knows the room's link, apart from guests": "Kdokoliv s odkazem na místnost, ale pouze registrovaní uživatelé",
"Anyone who knows the room's link, including guests": "Kdokoliv s odkazem na místnost, včetně hostů",
"Banned users": "Vykázaní uživatelé",
"Ban": "Vykázat",
@ -941,9 +941,9 @@
"Nothing appearing? Not all clients support interactive verification yet. <button>Use legacy verification</button>.": "Nic se neděje? Ne všichni klienti klienti podporují interaktivní ověření. <button>Použít starší verzi</button>.",
"Waiting for %(userId)s to confirm...": "Čekáme až to %(userId)s potvrdí...",
"Use two-way text verification": "Použít oboustranné ověření",
"Security & Privacy": "Bezpečnost a soukromí",
"Security & Privacy": "Zabezpečení",
"Encryption": "Šifrování",
"Once enabled, encryption cannot be disabled.": "Když se šifrování zapne, už nepůjde vypnout.",
"Once enabled, encryption cannot be disabled.": "Po zapnutí, už nepůjde šifrování vypnout.",
"Encrypted": "Šifrování",
"General": "Obecné",
"General failure": "Nějaká chyba",
@ -1546,8 +1546,8 @@
"Create a private room": "Vytvořit neveřejnou místnost",
"Topic (optional)": "Téma (volitelné)",
"Make this room public": "Zveřejnit místnost",
"Hide advanced": "Skrýt pokročilé",
"Show advanced": "Zobrazit pokročilé",
"Hide advanced": "Skrýt pokročilé možnosti",
"Show advanced": "Zobrazit pokročilé možnosti",
"Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Zamezit uživatelům jiných domovských serverů, aby do místnosti vstoupili (nelze později změnit!)",
"Your homeserver doesn't seem to support this feature.": "Váš domovský server asi tuto funkci nepodporuje.",
"Message edits": "Editování zpráv",
@ -2170,5 +2170,72 @@
"Mark all as read": "Označit vše jako přečtené",
"Not currently indexing messages for any room.": "Aktuálně neindexujeme žádné zprávy.",
"Currently indexing: %(currentRoom)s.": "Aktuálně indexujeme: %(currentRoom)s.",
"%(doneRooms)s out of %(totalRooms)s": "%(doneRooms)s z %(totalRooms)s"
"%(doneRooms)s out of %(totalRooms)s": "%(doneRooms)s z %(totalRooms)s",
"Review Sessions": "Prověřit relace",
"Unverified login. Was this you?": "Neověřené přihlášeni. Jste to vy?",
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s změnil/a jméno místnosti z %(oldRoomName)s na %(newRoomName)s.",
"%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s přidal/a této místnosti alternativní adresy %(addresses)s.",
"%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s přidal/a této místnosti alternativní adresu %(addresses)s.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s odebral/a této místnosti alternativní adresy %(addresses)s.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s odebral/a této místnosti alternativní adresu %(addresses)s.",
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s změnil/a této místnosti alternativní adresy.",
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s změnil/a této místnosti hlavní a alternativní adresy.",
"%(senderName)s changed the addresses for this room.": "%(senderName)s změnil/a této místnosti adresy.",
"Manually Verify by Text": "Manuální textové ověření",
"Interactively verify by Emoji": "Interaktivní ověření s emotikonami",
"Support adding custom themes": "Umožnit přidání vlastního vzhledu",
"Manually verify all remote sessions": "Manuálně ověřit všechny relace",
"Update your secure storage": "Aktualizovat vaše bezpečné úložistě",
"cached locally": "uložen lokálně",
"not found locally": "nenalezen lolálně",
"Secret Storage key format:": "Formát klíče Bezpečného Úložistě:",
"outdated": "zastaralý",
"up to date": "aktuální",
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individuálně ověřit každou uživatelovu relaci a označit jí za důvěryhodnou, bez důvěry v cross-signing.",
"Invalid theme schema.": "Neplatné schéma vzhledu.",
"Error downloading theme information.": "Nepovedlo se stáhnout informace o vzhledu.",
"Theme added!": "Motiv vzhledu přidán!",
"Custom theme URL": "URL adresa vlastního vzhledu",
"Add theme": "Přidat motiv vzhledu",
"Keyboard Shortcuts": "Klávesové zkratky",
"Scroll to most recent messages": "Přejít na poslední zprávy",
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "Nepovedlo se změnit alternativní adresy místnosti. Možná to server neumožňuje a nebo je to dočasná chyba.",
"You don't have permission to delete the alias.": "Nemáte oprávnění odebrat alias.",
"Local address": "Lokální adresa",
"Published Addresses": "Publikovaná adresa",
"Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Publikovaná adresa může být použíta kýmkoli na libovolném serveru pro přidání se do místnosti. Abyste mohli adresu publikovat, musí být nejdříve nastavená jako lokální.",
"Other published addresses:": "Další publikované adresy:",
"No other published addresses yet, add one below": "Zatím žádné další publikované adresy, přidejte nějakou níže",
"New published address (e.g. #alias:server)": "Nové publikované adresy (například #alias:server)",
"Local Addresses": "Lokální Adresy",
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Nastavit adresy pro tuto místnost, aby uživatelé mohli místnost najít zkrze váš domovský server (%(localDomain)s)",
"In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "V šifrovaných místnostech jsou vaše zprávy bezpečné a pouze vy a příjemce má klíče k jejich rozšifrování.",
"Verify all users in a room to ensure it's secure.": "Ověřit všechny uživatele v místnosti, abyste se přesvědčili o bezpečnosti.",
"In encrypted rooms, verify all users to ensure its secure.": "V šifrovaných místnostech ověřit všechny uživatele, abyste se přesvědčili o bezpečnosti.",
"Verified": "Oveřený",
"Verification cancelled": "Oveření bylo zrušeno",
"Compare emoji": "Porovnejte emotikony",
"Enter a server name": "Zadejte jméno serveru",
"Use Single Sign On to continue": "Pokračovat pomocí Jednotného přihlášení",
"Confirm adding this email address by using Single Sign On to prove your identity.": "Potvrďte přidání této adresy pomocí Jednotného přihlášení.",
"Single Sign On": "Jednotné přihlášení",
"Confirm adding email": "Potvrdit přidání emailu",
"Click the button below to confirm adding this email address.": "Kliknutím na tlačítko potvrdíte přidání emailové adresy.",
"Confirm adding this phone number by using Single Sign On to prove your identity.": "Potvrďte přidání telefonního čísla pomocí Jednotného přihlášení.",
"Confirm adding phone number": "Potrvrdit přidání telefonního čísla",
"Click the button below to confirm adding this phone number.": "Kliknutím na tlačítko potvrdíte přidání telefonního čísla.",
"Sends a message as html, without interpreting it as markdown": "Pošle zprávu jako HTML a nebude jí interpretovat jako Markdown",
"Confirm the emoji below are displayed on both sessions, in the same order:": "Potvrďte, že následující emotikony se zobrazují ve stejném pořadí na obou zařízeních:",
"Verify this session by confirming the following number appears on its screen.": "Ověřtě tuto relaci potrvrzením, že se následující čísla objevily na její obrazovce.",
"Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "Čekám na ověření od relace %(deviceName)s (%(deviceId)s)…",
"From %(deviceName)s (%(deviceId)s)": "Od %(deviceName)s (%(deviceId)s)",
"Confirm deleting these sessions by using Single Sign On to prove your identity.": "Potvrďte odstranění těchto relací pomocí Jednotného přihlášení.",
"Confirm deleting these sessions": "Potvrdit odstranění těchto relací",
"Click the button below to confirm deleting these sessions.": "Kliknutím na tlačítko potvrdíte odstranění těchto relací.",
"Delete sessions": "Odstranit relace",
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Pro hlášení bezpečnostních problémů s Matrixem si prosím přečtěte <a>naší Bezpečnostní politiku</a> (anglicky).",
"Waiting for you to accept on your other session…": "Čekáme na vaše přijetí v druhé relaci…",
"Almost there! Is your other session showing the same shield?": "Téměř hotovo! Je vaše druhá relace také ověřená?",
"Almost there! Is %(displayName)s showing the same shield?": "Téměř hotovo! Je relace %(displayName)s také ověřená?",
"You've successfully verified %(deviceName)s (%(deviceId)s)!": "Ověřili jste %(deviceName)s (%(deviceId)s)!"
}

View file

@ -64,8 +64,8 @@
"For security, this session has been signed out. Please sign in again.": "Aus Sicherheitsgründen wurde diese Sitzung beendet. Bitte melde dich erneut an.",
"Guests cannot join this room even if explicitly invited.": "Gäste können diesem Raum nicht beitreten, auch wenn sie explizit eingeladen wurden.",
"Hangup": "Auflegen",
"Homeserver is": "Heimserver:",
"Identity Server is": "Identitätsserver:",
"Homeserver is": "Der Heimserver ist",
"Identity Server is": "Der Identitätsserver ist",
"I have verified my email address": "Ich habe meine E-Mail-Adresse verifiziert",
"Import E2E room keys": "E2E-Raum-Schlüssel importieren",
"Invalid Email Address": "Ungültige E-Mail-Adresse",
@ -692,7 +692,7 @@
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Du wirst nicht in der Lage sein, die Änderung zurückzusetzen, da du dich degradierst. Wenn du der letze Nutzer mit Berechtigungen bist, wird es unmöglich sein die Privilegien zurückzubekommen.",
"Community IDs cannot be empty.": "Community-IDs können nicht leer sein.",
"Learn more about how we use analytics.": "Lerne mehr darüber, wie wir die Analysedaten nutzen.",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Wenn diese Seite identifizierbare Informationen wie Raum, Nutzer oder Gruppen-ID enthalten, werden diese Daten entfernt bevor sie an den Server gesendet werden.",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Wenn diese Seite identifizierbare Informationen wie Raum-, Nutzer- oder Gruppen-ID enthält, werden diese Daten entfernt bevor sie an den Server gesendet werden.",
"Which officially provided instance you are using, if any": "Welche offiziell angebotene Instanz du nutzt, wenn es der Fall ist",
"<a>In reply to</a> <pill>": "<a>Als Antwort auf</a> <pill>",
"This room is not public. You will not be able to rejoin without an invite.": "Dies ist kein öffentlicher Raum. Du wirst diesen nicht ohne Einladung wieder beitreten können.",
@ -1526,7 +1526,7 @@
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Verwende einen Integrationsmanager um Bots, Widgets und Sticker Packs zu verwalten.",
"Manage integrations": "Integrationen verwalten",
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Stimme den Nutzungsbedingungen des Identitätsservers %(serverName)s zu, um dich per Email Adresse und Telefonnummer auffindbar zu machen.",
"Clear cache and reload": "Cache löschen und neu laden",
"Clear cache and reload": "Zwischenspeicher löschen und neu laden",
"Customise your experience with experimental labs features. <a>Learn more</a>.": "Passe deine Erfahrung mit experimentellen Lab Funktionen an. <a>Mehr erfahren</a>.",
"Ignored/Blocked": "Ignoriert/Blockiert",
"Something went wrong. Please try again or view your console for hints.": "Etwas ist schief gelaufen. Bitte versuche es erneut oder sieh für weitere Hinweise in deiner Konsole nach.",
@ -1544,9 +1544,9 @@
"You are currently subscribed to:": "Du abonnierst momentan:",
"⚠ These settings are meant for advanced users.": "⚠ Diese Einstellungen sind für fortgeschrittene Nutzer gedacht.",
"The version of Riot": "Die Version von Riot",
"Whether you're using Riot on a device where touch is the primary input mechanism": "Ob Sie Riot auf einem Gerät verwenden, bei dem Berührung der primäre Eingabemechanismus ist",
"Whether you're using Riot on a device where touch is the primary input mechanism": "Ob du Riot auf einem Gerät verwendest, bei dem Berührung der primäre Eingabemechanismus ist",
"Whether you're using Riot as an installed Progressive Web App": "Ob Sie Riot als installierte progressive Web-App verwenden",
"Your user agent": "Ihr User-Agent",
"Your user agent": "Dein User-Agent",
"The information being sent to us to help make Riot better includes:": "Zu den Informationen, die uns zugesandt werden, um zu helfen, Riot besser zu machen, gehören:",
"There are unknown sessions in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Es sind unbekannte Sitzungen in diesem Raum: Wenn du ohne Verifizierung fortfährst, wird es für jemanden möglich sein, deinen Anruf zu belauschen.",
"If you cancel now, you won't complete verifying the other user.": "Wenn Sie jetzt abbrechen, werden Sie die Verifizierung des anderen Nutzers nicht beenden können.",
@ -1571,9 +1571,9 @@
"Notification sound": "Benachrichtigungston",
"Set a new custom sound": "Setze einen neuen benutzerdefinierten Sound",
"Browse": "Durchsuche",
"Direct Messages": "Direkte Nachrichten",
"Direct Messages": "Direktnachrichten",
"You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "Sie können <code>/help</code> benutzen, um verfügbare Befehle aufzulisten. Wollten Sie dies als Nachricht senden?",
"Direct message": "Direkte Nachricht",
"Direct message": "Direktnachricht",
"Set a room alias to easily share your room with other people.": "Setze ein Raum-Alias, um deinen Raum einfach mit anderen Personen zu teilen.",
"Suggestions": "Vorschläge",
"Recently Direct Messaged": "Kürzlich direkt verschickt",
@ -1625,7 +1625,7 @@
"Create a private room": "Erstelle einen privaten Raum",
"Topic (optional)": "Thema (optional)",
"Make this room public": "Machen Sie diesen Raum öffentlich",
"Hide advanced": "Fortgeschrittenes ausblenden",
"Hide advanced": "Weitere Einstellungen ausblenden",
"Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Hindere Benutzer auf anderen Matrix-Homeservern daran, diesem Raum beizutreten (Diese Einstellung kann später nicht geändert werden!)",
"Session name": "Name der Sitzung",
"This will allow you to return to your account after signing out, and sign in on other sessions.": "So können Sie nach der Abmeldung zu Ihrem Konto zurückkehren und sich bei anderen Sitzungen anmelden.",
@ -1657,7 +1657,7 @@
"or": "oder",
"Compare unique emoji": "Vergleiche einzigartige Emojis",
"Start": "Starte",
"Discovery": "Entdeckung",
"Discovery": "Kontakte",
"Done": "Erledigt",
"Manually Verify": "Manuell verifizieren",
"Trusted": "Vertrauenswürdig",
@ -1718,8 +1718,8 @@
"Displays information about a user": "Zeigt Informationen über einen Benutzer",
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s hat den Raumnamen von %(oldRoomName)s zu %(newRoomName)s geändert.",
"%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s hat die alternative Adresse %(addresses)s für diesen Raum hinzugefügt.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s hat die alternativen Adressen %(addresses)s für diesen Raum entfernt",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s hat die alternative Adresse %(addresses)s für diesen Raum entfernt",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s hat die alternativen Adressen %(addresses)s für diesen Raum entfernt.",
"%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s hat die alternative Adresse %(addresses)s für diesen Raum entfernt.",
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s hat die alternative Adresse für diesen Raum geändert.",
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s hat die Haupt- und Alternativadresse für diesen Raum geändert.",
"%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s entfernte die Ausschluss-Regel für Nutzer, die %(glob)s entsprechen",
@ -1739,5 +1739,53 @@
"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"
"%(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",
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Erlaube den Standard-Server zur Anrufunterstützung (turn.matrix.org) zu verwenden wenn dein Heimserver keinen eigenen anbietet (deine IP Adresse wird bei dem Anruf übermittelt)",
"Show more": "mehr",
"This session is <b>not backing up your keys</b>, but you do have an existing backup you can restore from and add to going forward.": "Diese Sitzung <b>speichert deine Schlüssel nicht</b>, du kannst sie aber an die Schlüsselsicherung anschließen.",
"Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "Verbinde diese Sitzung mit deiner Schlüsselsicherung bevor du dich abmeldest, um den Verlust von Schlüsseln zu vermeiden.",
"This backup is trusted because it has been restored on this session": "Dieser Sicherung wird vertraut, da sie während dieser Sitzung wiederhergestellt wurde",
"Enable desktop notifications for this session": "Desktop-Benachrichtigungen für diese Sitzung aktivieren",
"Enable audible notifications for this session": "Aktiviere die akustischen Benachrichtigungen für diese Sitzung",
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrationsserver können für dich Widgets einstellen, Raum-Einladungen verschicken oder deine Berechtigungen setzen.",
"Read Marker lifetime (ms)": "Gültigkeitsdauer der Gelesen-Markierung (ms)",
"Read Marker off-screen lifetime (ms)": "Gültigkeitsdauer der Gelesen-Markierung außerhalb des Bildschirms (ms)",
"Session key:": "Sitzungsschlüssel:",
"A session's public name is visible to people you communicate with": "Der Sitzungsname ist sichtbar für die Personen mit denen du kommunizierst",
"Sounds": "Töne",
"Upgrade the room": "Raum hochstufen",
"Enable room encryption": "Verschlüsselung aktivieren",
"This message cannot be decrypted": "Diese Nachricht konnte nicht entschlüsselt werden",
"Encrypted by an unverified session": "Verschlüsselt von einer unbekannten Sitzung",
"Unencrypted": "Unverschlüsselt",
"Encrypted by a deleted session": "Verschlüsselt von einer gelöschten Sitzung",
"The encryption used by this room isn't supported.": "Die Verschlüsselung, die dieser Raum verwendet, wird nicht unterstützt.",
"React": "Reaktion hinzufügen",
"e.g. my-room": "z.B. mein-raum",
"Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.": "Verwende einen Identitätsserver um mit einer E-Mail-Adresse einzuladen. <default>Benutzer den Standard-Identitätsserver (%(defaultIdentityServerName)s)</default> oder konfiguriere einen in den <settings>Einstellungen</settings>.",
"Use an identity server to invite by email. Manage in <settings>Settings</settings>.": "Verwende einen Identitätsserver um mit einer E-Mail-Adresse einzuladen. Diese können in den <settings>Einstellungen</settings> konfiguriert werden.",
"Create a public room": "Erstelle einen öffentlichen Raum",
"Show advanced": "Weitere Einstellungen anzeigen",
"Verify session": "Sitzung verifizieren",
"To verify that this session can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this session matches the key below:": "Um diese Sitzung zu verifizieren kontaktiere bitte den Benutzer über einen anderen Kanal (z.B. persönlich oder mit einem Telefonanruf) und frage ihn ob der Sitzungsschlüssel in seinen Benutzereinstellungen mit dem hier angezeigten übereinstimmt:",
"Session key": "Sitzungsschlüssel",
"If it matches, press the verify button below. If it doesn't, then someone else is intercepting this session and you probably want to press the blacklist button instead.": "Wenn die Sitzungsschlüssel übereinstimmen, drücke den Knopf zur Bestätigung. Stimmen sie nicht überein versucht jemand diese Sitzung abzufangen und du solltest diese Sitzung blockieren.",
"Recent Conversations": "Letzte Unterhaltungen",
"Report Content to Your Homeserver Administrator": "Inhalte an den Administrator deines Heimservers melden",
"Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Wenn du diese Nachricht meldest wird dessen einzigartige 'event ID' an den Administrator deines Heimservers übermittelt. Wenn die Nachrichten in diesem Raum verschlüsselt sind wird dein Administrator nicht in der Lage sein den Text zu lesen oder Medien einzusehen.",
"Send report": "Bericht senden",
"Enter recovery passphrase": "Gib die Wiederherstellungspassphrase ein",
"Enter recovery key": "Wiederherstellungspassphrase eingeben",
"Report Content": "Inhalte melden",
"Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Gib eine E-Mail-Adresse an um dein Konto wiederherstellen zu können. Die E-Mail-Adresse kann auch genutzt werden um deinen Kontakt zu finden.",
"Enter your custom homeserver URL <a>What does this mean?</a>": "Gib eine andere Heimserver-Adresse an <a>Was bedeutet das?</a>",
"%(creator)s created and configured the room.": "%(creator)s hat den Raum erstellt und konfiguriert.",
"Sender session information": "Absender Sitzungsinformationen",
"Set up with a recovery key": "Mit einem Wiederherstellungsschlüssel einrichten",
"Keep a copy of it somewhere secure, like a password manager or even a safe.": "Bewahre ihn sicher auf, wie in einem Passwort-Manager oder einem Safe.",
"Your recovery key": "Dein Wiederherstellungsschlüssel",
"Copy": "In Zwischenablage kopieren",
"Make a copy of your recovery key": "Speichere deinen Wiederherstellungsschlüssel",
"Unverified login. Was this you?": "Nicht verifzierte Anmeldung. Bist du es gewesen?",
"Sends a message as html, without interpreting it as markdown": "Verschickt eine Nachricht im html-Format, ohne sie in Markdown zu formatieren"
}

View file

@ -70,7 +70,7 @@
"Failure to create room": "Failure to create room",
"If you cancel now, you won't complete verifying the other user.": "If you cancel now, you won't complete verifying the other user.",
"If you cancel now, you won't complete verifying your other session.": "If you cancel now, you won't complete verifying your other session.",
"If you cancel now, you won't complete your secret storage operation.": "If you cancel now, you won't complete your secret storage operation.",
"If you cancel now, you won't complete your operation.": "If you cancel now, you won't complete your operation.",
"Cancel entering passphrase?": "Cancel entering passphrase?",
"Enter passphrase": "Enter passphrase",
"Cancel": "Cancel",
@ -125,6 +125,7 @@
"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.": "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.",
"Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",
"Trust": "Trust",
"%(name)s is requesting verification": "%(name)s is requesting verification",
"Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings",
"Riot was not given permission to send notifications - please try again": "Riot was not given permission to send notifications - please try again",
"Unable to enable Notifications": "Unable to enable Notifications",
@ -176,6 +177,7 @@
"Changes your avatar in this current room only": "Changes your avatar in this current room only",
"Changes your avatar in all rooms": "Changes your avatar in all rooms",
"Gets or sets the room topic": "Gets or sets the room topic",
"Failed to set topic": "Failed to set topic",
"This room has no topic.": "This room has no topic.",
"Sets the room name": "Sets the room name",
"Invites user with given id to current room": "Invites user with given id to current room",
@ -195,6 +197,8 @@
"Unignored user": "Unignored user",
"You are no longer ignoring %(userId)s": "You are no longer ignoring %(userId)s",
"Define the power level of a user": "Define the power level of a user",
"Command failed": "Command failed",
"Could not find user in room": "Could not find user in room",
"Deops user with given id": "Deops user with given id",
"Opens the Developer Tools dialog": "Opens the Developer Tools dialog",
"Adds a custom widget by URL to the room": "Adds a custom widget by URL to the room",
@ -393,9 +397,8 @@
"Render simple counters in room header": "Render simple counters in room header",
"Multiple integration managers": "Multiple integration managers",
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
"Show a presence dot next to DMs in the room list": "Show a presence dot next to DMs in the room list",
"Support adding custom themes": "Support adding custom themes",
"Enable cross-signing to verify per-user instead of per-session (in development)": "Enable cross-signing to verify per-user instead of per-session (in development)",
"Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session",
"Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)",
"Show info about bridges in room settings": "Show info about bridges in room settings",
"Show padlocks on invite only rooms": "Show padlocks on invite only rooms",
@ -440,7 +443,7 @@
"Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)",
"Show previews/thumbnails for images": "Show previews/thumbnails for images",
"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",
"Keep recovery passphrase in memory for this session": "Keep recovery 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",
@ -591,6 +594,8 @@
"Cross-signing and secret storage are not yet set up.": "Cross-signing and secret storage are not yet set up.",
"Reset cross-signing and secret storage": "Reset cross-signing and secret storage",
"Bootstrap cross-signing and secret storage": "Bootstrap cross-signing and secret storage",
"well formed": "well formed",
"unexpected type": "unexpected type",
"Cross-signing public keys:": "Cross-signing public keys:",
"in memory": "in memory",
"not found": "not found",
@ -610,10 +615,13 @@
"up to date": "up to date",
"Your homeserver does not support session management.": "Your homeserver does not support session management.",
"Unable to load session list": "Unable to load session list",
"Confirm deleting these sessions by using Single Sign On to prove your identity.": "Confirm deleting these sessions by using Single Sign On to prove your identity.",
"Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Confirm deleting these sessions by using Single Sign On to prove your identity.",
"Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Confirm deleting this session by using Single Sign On to prove your identity.",
"Confirm deleting these sessions": "Confirm deleting these sessions",
"Click the button below to confirm deleting these sessions.": "Click the button below to confirm deleting these sessions.",
"Delete sessions": "Delete sessions",
"Click the button below to confirm deleting these sessions.|other": "Click the button below to confirm deleting these sessions.",
"Click the button below to confirm deleting these sessions.|one": "Click the button below to confirm deleting this session.",
"Delete sessions|other": "Delete sessions",
"Delete sessions|one": "Delete session",
"Authentication": "Authentication",
"Delete %(count)s sessions|other": "Delete %(count)s sessions",
"Delete %(count)s sessions|one": "Delete %(count)s session",
@ -1077,7 +1085,6 @@
"Seen by %(userName)s at %(dateTime)s": "Seen by %(userName)s at %(dateTime)s",
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
"Replying": "Replying",
"Direct Chat": "Direct Chat",
"Room %(name)s": "Room %(name)s",
"Recent rooms": "Recent rooms",
"No rooms to show": "No rooms to show",
@ -1329,6 +1336,8 @@
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?",
"Edited at %(date)s. Click to view edits.": "Edited at %(date)s. Click to view edits.",
"edited": "edited",
"Can't load this message": "Can't load this message",
"Submit logs": "Submit logs",
"Removed or unknown message type": "Removed or unknown message type",
"Message removed by %(userId)s": "Message removed by %(userId)s",
"Message removed": "Message removed",
@ -1521,6 +1530,7 @@
"Logs sent": "Logs sent",
"Thank you!": "Thank you!",
"Failed to send logs: ": "Failed to send logs: ",
"Reminder: Your browser is unsupported, so your experience may be unpredictable.": "Reminder: Your browser is unsupported, so your experience may be unpredictable.",
"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.": "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.",
"Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.",
"GitHub issue": "GitHub issue",
@ -1552,6 +1562,8 @@
"Please enter a name for the room": "Please enter a name for the room",
"Set a room alias to easily share your room with other people.": "Set a room alias to easily share your room with other people.",
"This room is private, and can only be joined by invitation.": "This room is private, and can only be joined by invitation.",
"Enable end-to-end encryption": "Enable end-to-end encryption",
"You cant disable this later. Bridges & most bots wont work yet.": "You cant disable this later. Bridges & most bots wont work yet.",
"Create a public room": "Create a public room",
"Create a private room": "Create a private room",
"Name": "Name",
@ -1566,13 +1578,17 @@
"You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ",
"Incompatible Database": "Incompatible Database",
"Continue With Encryption Disabled": "Continue With Encryption Disabled",
"Unknown error": "Unknown error",
"Incorrect password": "Incorrect password",
"Confirm your account deactivation by using Single Sign On to prove your identity.": "Confirm your account deactivation by using Single Sign On to prove your identity.",
"Are you sure you want to deactivate your account? This is irreversible.": "Are you sure you want to deactivate your account? This is irreversible.",
"Confirm account deactivation": "Confirm account deactivation",
"To continue, please enter your password:": "To continue, please enter your password:",
"Server did not require any authentication": "Server did not require any authentication",
"Server did not return valid authentication information.": "Server did not return valid authentication information.",
"There was a problem communicating with the server. Please try again.": "There was a problem communicating with the server. Please try again.",
"This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>",
"Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.",
"Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.",
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)",
"To continue, please enter your password:": "To continue, please enter your password:",
"Verify session": "Verify session",
"Use Legacy Verification (for older clients)": "Use Legacy Verification (for older clients)",
"Verify by comparing a short text string.": "Verify by comparing a short text string.",
@ -1641,7 +1657,7 @@
"Riot encountered an error during upload of:": "Riot encountered an error during upload of:",
"Upload completed": "Upload completed",
"Cancelled signature upload": "Cancelled signature upload",
"Unabled to upload": "Unabled to upload",
"Unable to upload": "Unable to upload",
"Signature upload success": "Signature upload success",
"Signature upload failed": "Signature upload failed",
"You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.",
@ -1757,18 +1773,19 @@
"Upload %(count)s other files|one": "Upload %(count)s other file",
"Cancel All": "Cancel All",
"Upload Error": "Upload Error",
"Verify other session": "Verify other session",
"Verification Request": "Verification Request",
"A widget would like to verify your identity": "A widget would like to verify your identity",
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.",
"Remember my selection for this widget": "Remember my selection for this widget",
"Allow": "Allow",
"Deny": "Deny",
"Enter secret storage passphrase": "Enter secret storage passphrase",
"Unable to access secret storage. Please verify that you entered the correct passphrase.": "Unable to access secret storage. Please verify that you entered the correct passphrase.",
"<b>Warning</b>: You should only access secret storage from a trusted computer.": "<b>Warning</b>: You should only access secret storage from a trusted computer.",
"Access your secure message history and your cross-signing identity for verifying other sessions by entering your passphrase.": "Access your secure message history and your cross-signing identity for verifying other sessions by entering your passphrase.",
"If you've forgotten your passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>.": "If you've forgotten your passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>.",
"Enter secret storage recovery key": "Enter secret storage recovery key",
"Enter recovery passphrase": "Enter recovery passphrase",
"Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "Unable to access secret storage. Please verify that you entered the correct recovery passphrase.",
"<b>Warning</b>: You should only do this on a trusted computer.": "<b>Warning</b>: You should only do this on a trusted computer.",
"Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery passphrase.": "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery passphrase.",
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>.": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>.",
"Enter recovery key": "Enter recovery key",
"Unable to access secret storage. Please verify that you entered the correct recovery key.": "Unable to access secret storage. Please verify that you entered the correct recovery key.",
"This looks like a valid recovery key!": "This looks like a valid recovery key!",
"Not a valid recovery key": "Not a valid recovery key",
@ -1776,19 +1793,17 @@
"If you've forgotten your recovery key you can <button>set up new recovery options</button>.": "If you've forgotten your recovery key you can <button>set up new recovery options</button>.",
"Unable to load backup status": "Unable to load backup status",
"Recovery key mismatch": "Recovery key mismatch",
"Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.",
"Backup could not be decrypted with this recovery key: please verify that you entered the correct recovery key.": "Backup could not be decrypted with this recovery key: please verify that you entered the correct recovery key.",
"Incorrect recovery passphrase": "Incorrect recovery passphrase",
"Backup could not be decrypted with this passphrase: please verify that you entered the correct recovery passphrase.": "Backup could not be decrypted with this passphrase: please verify that you entered the correct recovery passphrase.",
"Backup could not be decrypted with this recovery passphrase: please verify that you entered the correct recovery passphrase.": "Backup could not be decrypted with this recovery passphrase: please verify that you entered the correct recovery passphrase.",
"Unable to restore backup": "Unable to restore backup",
"No backup found!": "No backup found!",
"Backup restored": "Backup restored",
"Failed to decrypt %(failedCount)s sessions!": "Failed to decrypt %(failedCount)s sessions!",
"Restored %(sessionCount)s session keys": "Restored %(sessionCount)s session keys",
"Enter recovery passphrase": "Enter recovery passphrase",
"<b>Warning</b>: you should only set up key backup from a trusted computer.": "<b>Warning</b>: you should only set up key backup from a trusted computer.",
"Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.",
"If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>",
"Enter recovery key": "Enter recovery key",
"<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>Warning</b>: You should only set up key backup from a trusted computer.",
"Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.",
"If you've forgotten your recovery key you can <button>set up new recovery options</button>": "If you've forgotten your recovery key you can <button>set up new recovery options</button>",
@ -1824,6 +1839,7 @@
"Forget": "Forget",
"Favourite": "Favourite",
"Low Priority": "Low Priority",
"Direct Chat": "Direct Chat",
"Clear status": "Clear status",
"Update status": "Update status",
"Set status": "Set status",
@ -1954,6 +1970,11 @@
"Community %(groupId)s not found": "Community %(groupId)s not found",
"This homeserver does not support communities": "This homeserver does not support communities",
"Failed to load %(groupId)s": "Failed to load %(groupId)s",
"Welcome to %(appName)s": "Welcome to %(appName)s",
"Liberate your communication": "Liberate your communication",
"Send a Direct Message": "Send a Direct Message",
"Explore Public Rooms": "Explore Public Rooms",
"Create a Group Chat": "Create a Group Chat",
"Explore": "Explore",
"Filter": "Filter",
"Filter rooms…": "Filter rooms…",
@ -1963,6 +1984,7 @@
"Failed to leave room": "Failed to leave room",
"Can't leave Server Notices room": "Can't leave Server Notices room",
"This room is used for important messages from the Homeserver, so you cannot leave it.": "This room is used for important messages from the Homeserver, so you cannot leave it.",
"Unknown error": "Unknown error",
"Signed Out": "Signed Out",
"For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.",
"Terms and Conditions": "Terms and Conditions",
@ -2043,7 +2065,6 @@
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
"Could not load user profile": "Could not load user profile",
"Complete security": "Complete security",
"Session verified": "Session verified",
"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.",
@ -2081,6 +2102,9 @@
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
"Syncing...": "Syncing...",
"Signing In...": "Signing In...",
"If you've joined lots of rooms, this might take a while": "If you've joined lots of rooms, this might take a while",
"Create account": "Create account",
"Failed to fetch avatar URL": "Failed to fetch avatar URL",
"Set a display name:": "Set a display name:",
@ -2094,14 +2118,15 @@
"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>",
"Use an existing session to verify this one, granting it access to encrypted messages.": "Use an existing session to verify this one, granting it access to encrypted messages.",
"If you cant access one, <button>use your recovery key or recovery passphrase.</button>": "If you cant access one, <button>use your recovery key or recovery passphrase.</button>",
"Use your other device to continue…": "Use your other device to continue…",
"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",
"Incorrect password": "Incorrect password",
"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.",
"Enter your password to sign in and regain access to your account.": "Enter your password to sign in and regain access to your account.",
@ -2157,18 +2182,17 @@
"Restore": "Restore",
"You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.",
"Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.",
"Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.",
"Set up encryption on this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Set up encryption on this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.",
"Secure your encryption keys with a passphrase. For maximum security this should be different to your account password:": "Secure your encryption keys with a passphrase. For maximum security this should be different to your account password:",
"Enter a passphrase": "Enter a passphrase",
"Back up my encryption keys, securing them with the same passphrase": "Back up my encryption keys, securing them with the same passphrase",
"Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.",
"Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:": "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:",
"Enter a recovery passphrase": "Enter a recovery passphrase",
"Back up encrypted message keys": "Back up encrypted message keys",
"Set up with a recovery key": "Set up with a recovery key",
"That matches!": "That matches!",
"That doesn't match.": "That doesn't match.",
"Go back to set it again.": "Go back to set it again.",
"Enter your passphrase a second time to confirm it.": "Enter your passphrase a second time to confirm it.",
"Confirm your passphrase": "Confirm your passphrase",
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.",
"Enter your recovery passphrase a second time to confirm it.": "Enter your recovery passphrase a second time to confirm it.",
"Confirm your recovery passphrase": "Confirm your recovery passphrase",
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.",
"Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.",
"Your recovery key": "Your recovery key",
"Copy": "Copy",
@ -2180,19 +2204,20 @@
"<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage",
"You can now verify your other devices, and other users to keep your chats safe.": "You can now verify your other devices, and other users to keep your chats safe.",
"Upgrade your encryption": "Upgrade your encryption",
"Confirm recovery passphrase": "Confirm recovery passphrase",
"Make a copy of your recovery key": "Make a copy of your recovery key",
"You're done!": "You're done!",
"Unable to set up secret storage": "Unable to set up secret storage",
"Retry": "Retry",
"We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.",
"For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
"Enter a passphrase...": "Enter a passphrase...",
"Please enter your passphrase a second time to confirm.": "Please enter your passphrase a second time to confirm.",
"Repeat your passphrase...": "Repeat your passphrase...",
"Enter a recovery passphrase...": "Enter a recovery passphrase...",
"Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.",
"Repeat your recovery passphrase...": "Repeat your recovery passphrase...",
"Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).",
"Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.",
"Set up Secure Message Recovery": "Set up Secure Message Recovery",
"Secure your backup with a passphrase": "Secure your backup with a passphrase",
"Secure your backup with a recovery passphrase": "Secure your backup with a recovery passphrase",
"Starting backup...": "Starting backup...",
"Success!": "Success!",
"Create key backup": "Create key backup",

Some files were not shown because too many files have changed in this diff Show more