Merge branch 'develop' into foldleft/better-errors

This commit is contained in:
Zoe 2020-04-17 14:11:05 +01:00
commit e3d784816d
135 changed files with 4076 additions and 1774 deletions

View file

@ -64,8 +64,8 @@
"create-react-class": "^15.6.0", "create-react-class": "^15.6.0",
"diff-dom": "^4.1.3", "diff-dom": "^4.1.3",
"diff-match-patch": "^1.0.4", "diff-match-patch": "^1.0.4",
"emojibase-data": "^4.0.2", "emojibase-data": "^5.0.1",
"emojibase-regex": "^3.0.0", "emojibase-regex": "^4.0.1",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"file-saver": "^1.3.3", "file-saver": "^1.3.3",
"filesize": "3.5.6", "filesize": "3.5.6",
@ -89,7 +89,6 @@
"qrcode-react": "^0.1.16", "qrcode-react": "^0.1.16",
"qs": "^6.6.0", "qs": "^6.6.0",
"react": "^16.9.0", "react": "^16.9.0",
"react-addons-css-transition-group": "15.6.2",
"react-beautiful-dnd": "^4.0.1", "react-beautiful-dnd": "^4.0.1",
"react-dom": "^16.9.0", "react-dom": "^16.9.0",
"react-focus-lock": "^2.2.1", "react-focus-lock": "^2.2.1",

View file

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

View file

@ -23,3 +23,84 @@ limitations under the License.
margin-left: auto; margin-left: auto;
margin-right: 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 { .mx_CompleteSecurity_actionRow {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-top: $font-28px;
.mx_AccessibleButton { .mx_AccessibleButton {
margin-inline-start: 18px; margin-inline-start: 18px;

View file

@ -89,3 +89,13 @@ limitations under the License.
.mx_Login_underlinedServerName { .mx_Login_underlinedServerName {
border-bottom: 1px dashed $accent-color; 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; 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 { .mx_AuthBody_changeFlow {
display: block; display: block;
text-align: center; text-align: center;

View file

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

View file

@ -15,6 +15,8 @@ limitations under the License.
*/ */
.mx_CreateRoomDialog_details { .mx_CreateRoomDialog_details {
margin-top: 15px;
.mx_CreateRoomDialog_details_summary { .mx_CreateRoomDialog_details_summary {
outline: none; outline: none;
list-style: none; list-style: none;
@ -71,11 +73,19 @@ limitations under the License.
} }
.mx_CreateRoomDialog { .mx_CreateRoomDialog {
&.mx_Dialog_fixedWidth { &.mx_Dialog_fixedWidth {
width: 450px; width: 450px;
} }
.mx_Dialog_content {
margin-bottom: 40px;
}
p,
.mx_Field_input label {
color: $muted-fg-color;
}
.mx_SettingsFlag { .mx_SettingsFlag {
display: flex; display: flex;
} }
@ -90,5 +100,18 @@ limitations under the License.
flex: 0 0 auto; flex: 0 0 auto;
margin-left: 30px; 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-radius: 4px;
border: 1px solid $dialog-close-fg-color; border: 1px solid $dialog-close-fg-color;
background-color: $primary-bg-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 { .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; user-select: none;
} }
.mx_Dropdown_input.mx_AccessibleButton_disabled {
cursor: not-allowed;
}
.mx_Dropdown_input:focus { .mx_Dropdown_input:focus {
border-color: $input-focused-border-color; border-color: $input-focused-border-color;
} }

View file

@ -14,8 +14,11 @@
} }
a.mx_Pill { a.mx_Pill {
word-break: break-all; text-overflow: ellipsis;
display: inline; white-space: nowrap;
overflow: hidden;
vertical-align: text-bottom;
max-width: calc(100% - 1ch);
} }
/* More specific to override `.markdown-body a` text-decoration */ /* 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; background-color: $reaction-row-button-selected-bg-color;
border-color: $reaction-row-button-selected-border-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
max-width: 100px; * {
overflow: hidden; pointer-events: none;
white-space: nowrap; }
text-overflow: ellipsis;
padding-right: 4px; .mx_ReactionsRowButton_content {
max-width: 100px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding-right: 4px;
}
} }

View file

@ -44,27 +44,29 @@ limitations under the License.
outline: none; outline: none;
overflow-x: hidden; overflow-x: hidden;
span.mx_UserPill, span.mx_RoomPill { &.mx_BasicMessageComposer_input_shouldShowPillAvatar {
padding-left: 21px; span.mx_UserPill, span.mx_RoomPill {
position: relative; padding-left: 21px;
position: relative;
// avatar psuedo element // avatar psuedo element
&::before { &::before {
position: absolute; position: absolute;
left: 2px; left: 2px;
top: 2px; top: 2px;
content: var(--avatar-letter); content: var(--avatar-letter);
width: 16px; width: 16px;
height: 16px; height: 16px;
background: var(--avatar-background), $avatar-bg-color; background: var(--avatar-background), $avatar-bg-color;
color: $avatar-initial-color; color: $avatar-initial-color;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 16px; background-size: 16px;
border-radius: 8px; border-radius: 8px;
text-align: center; text-align: center;
font-weight: normal; font-weight: normal;
line-height: $font-16px; line-height: $font-16px;
font-size: $font-10-4px; font-size: $font-10-4px;
}
} }
} }
} }

View file

@ -111,6 +111,7 @@ limitations under the License.
} }
.mx_EventTile_line, .mx_EventTile_reply { .mx_EventTile_line, .mx_EventTile_reply {
clear: both;
position: relative; position: relative;
padding-left: 65px; /* left gutter */ padding-left: 65px; /* left gutter */
padding-top: 4px; padding-top: 4px;

View file

@ -24,6 +24,20 @@ limitations under the License.
margin: 0; margin: 0;
padding: 0 8px 0 10px; padding: 0 8px 0 10px;
position: relative; position: relative;
.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 { .mx_RoomTile:focus {
@ -31,15 +45,6 @@ limitations under the License.
background-color: $roomtile-focused-bg-color; background-color: $roomtile-focused-bg-color;
} }
.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_RoomTile_tooltip { .mx_RoomTile_tooltip {
display: inline-block; display: inline-block;
position: relative; position: relative;
@ -151,7 +156,10 @@ limitations under the License.
} }
.mx_RoomTile_menuButton { .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 { .mx_RoomTile_menuButton {
display: block; display: block;
} }
.mx_UserOnlineDot {
display: none;
}
} }
.mx_RoomTile_unreadNotify .mx_RoomTile_badge, .mx_RoomTile_unreadNotify .mx_RoomTile_badge,

View file

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

View file

@ -61,3 +61,21 @@ limitations under the License.
.mx_VerificationShowSas_emojiSas_break { .mx_VerificationShowSas_emojiSas_break {
flex-basis: 100%; 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 trap 'handle_error' ERR
echo "--- Building Riot" echo "--- Building Riot"
scripts/ci/layered-riot-web.sh scripts/ci/layered-riot-web.sh
cd ../riot-web cd ../riot-web

View file

@ -237,7 +237,7 @@ const walkOpts = {
const fullPath = path.join(root, fileStats.name); const fullPath = path.join(root, fileStats.name);
let trs; 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); trs = getTranslationsJs(fullPath);
} else if (fileStats.name.endsWith('.html')) { } else if (fileStats.name.endsWith('.html')) {
trs = getTranslationsOther(fullPath); trs = getTranslationsOther(fullPath);

View file

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

View file

@ -188,4 +188,8 @@ export default class BasePlatform {
const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl()); const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl());
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO 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 { _t } from './languageHandler';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import dis from './dispatcher'; import dis from './dispatcher';
import SdkConfig from './SdkConfig';
import { showUnknownDeviceDialogForCalls } from './cryptodevices'; import { showUnknownDeviceDialogForCalls } from './cryptodevices';
import WidgetUtils from './utils/WidgetUtils'; import WidgetUtils from './utils/WidgetUtils';
import WidgetEchoStore from './stores/WidgetEchoStore'; import WidgetEchoStore from './stores/WidgetEchoStore';
import SettingsStore, { SettingLevel } from './settings/SettingsStore'; import SettingsStore, { SettingLevel } from './settings/SettingsStore';
import {generateHumanReadableId} from "./utils/NamingUtils"; import {generateHumanReadableId} from "./utils/NamingUtils";
import {Jitsi} from "./widgets/Jitsi";
global.mxCalls = { global.mxCalls = {
//room_id: MatrixCall //room_id: MatrixCall
@ -431,7 +431,7 @@ async function _startCallApp(roomId, type) {
} }
const confId = `JitsiConference${generateHumanReadableId()}`; const confId = `JitsiConference${generateHumanReadableId()}`;
const jitsiDomain = SdkConfig.get()['jitsi']['preferredDomain']; const jitsiDomain = Jitsi.getInstance().preferredDomain;
let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl(); let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl();

View file

@ -51,7 +51,7 @@ async function confirmToDismiss(name) {
} else if (name === "m.cross_signing.self_signing") { } else if (name === "m.cross_signing.self_signing") {
description = _t("If you cancel now, you won't complete verifying your other session."); description = _t("If you cancel now, you won't complete verifying your other session.");
} else { } 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"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
@ -185,7 +185,7 @@ export async function promptForBackupPassphrase() {
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
showSummary: false, keyCallback: k => key = k, showSummary: false, keyCallback: k => key = k,
}, null, /* priority = */ false, /* static = */ true); }, null, /* priority = */ false, /* static = */ true);
const success = await finished; const success = await finished;

View file

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

View file

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

View file

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

View file

@ -40,6 +40,7 @@ import ToastStore from "./stores/ToastStore";
import {IntegrationManagers} from "./integrations/IntegrationManagers"; import {IntegrationManagers} from "./integrations/IntegrationManagers";
import {Mjolnir} from "./mjolnir/Mjolnir"; import {Mjolnir} from "./mjolnir/Mjolnir";
import DeviceListener from "./DeviceListener"; import DeviceListener from "./DeviceListener";
import {Jitsi} from "./widgets/Jitsi";
/** /**
* Called at startup, to attempt to build a logged-in Matrix session. It tries * 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(); UserActivity.sharedInstance().start();
TypingStore.sharedInstance().reset(); // just in case TypingStore.sharedInstance().reset(); // just in case
ToastStore.sharedInstance().reset(); ToastStore.sharedInstance().reset();
if (!SettingsStore.getValue("lowBandwidth")) {
Presence.start();
}
DMRoomMap.makeShared().start(); DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching(); IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start(); ActiveWidgetStore.start();
@ -603,6 +601,14 @@ async function startMatrixClient(startSyncing=true) {
// This needs to be started after crypto is set up // This needs to be started after crypto is set up
DeviceListener.sharedInstance().start(); 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 // dispatch that we finished starting up to wire up any other bits
// of the matrix client that cannot be set prior to starting up. // of the matrix client that cannot be set prior to starting up.
@ -637,6 +643,10 @@ async function _clearStorage() {
window.localStorage.clear(); window.localStorage.clear();
} }
if (window.sessionStorage) {
window.sessionStorage.clear();
}
// create a temporary client to clear out the persistent stores. // create a temporary client to clear out the persistent stores.
const cli = createMatrixClient({ const cli = createMatrixClient({
// we'll never make any requests, so can pass a bogus HS URL // we'll never make any requests, so can pass a bogus HS URL

View file

@ -37,6 +37,18 @@ import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
const MAX_PENDING_ENCRYPTED = 20; 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 = { const Notifier = {
notifsByRoom: {}, notifsByRoom: {},
@ -46,6 +58,9 @@ const Notifier = {
pendingEncryptedEventIds: [], pendingEncryptedEventIds: [],
notificationMessageForEvent: function(ev) { notificationMessageForEvent: function(ev) {
if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) {
return typehandlers[ev.getContent().msgtype](ev);
}
return TextForEvent.textForEvent(ev); return TextForEvent.textForEvent(ev);
}, },
@ -69,7 +84,9 @@ const Notifier = {
title = room.name; title = room.name;
// notificationMessageForEvent includes sender, // notificationMessageForEvent includes sender,
// but we already have the sender here // 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') { } else if (ev.getType() === 'm.room.member') {
// context is all in the message here, we don't need // context is all in the message here, we don't need
// to display sender info // to display sender info
@ -78,7 +95,9 @@ const Notifier = {
title = ev.sender.name + " (" + room.name + ")"; title = ev.sender.name + " (" + room.name + ")";
// notificationMessageForEvent includes sender, // notificationMessageForEvent includes sender,
// but we've just out sender in the title // 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()) { if (!this.isBodyEnabled()) {

View file

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

View file

@ -350,7 +350,7 @@ export const Commands = [
return success(cli.setRoomTopic(roomId, args)); return success(cli.setRoomTopic(roomId, args));
} }
const room = cli.getRoom(roomId); 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 topicEvents = room.currentState.getStateEvents('m.room.topic', '');
const topic = topicEvents && topicEvents.getContent().topic; const topic = topicEvents && topicEvents.getContent().topic;
@ -721,9 +721,10 @@ export const Commands = [
if (!isNaN(powerLevel)) { if (!isNaN(powerLevel)) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId); 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', ''); 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)); return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent));
} }
} }
@ -742,9 +743,10 @@ export const Commands = [
if (matches) { if (matches) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId); 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', ''); 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)); return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent));
} }
} }
@ -914,7 +916,7 @@ export const Commands = [
// Command definitions for autocompletion ONLY: // Command definitions for autocompletion ONLY:
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes // /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
new Command({ new Command({
command: 'me', command: "me",
args: '<message>', args: '<message>',
description: _td('Displays action'), description: _td('Displays action'),
category: CommandCategories.messages, category: CommandCategories.messages,
@ -931,16 +933,7 @@ Commands.forEach(cmd => {
}); });
}); });
export function parseCommandString(input) {
/**
* 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) {
// trim any trailing whitespace, as it can confuse the parser for // trim any trailing whitespace, as it can confuse the parser for
// IRC-style commands // IRC-style commands
input = input.replace(/\s+$/, ''); input = input.replace(/\s+$/, '');
@ -956,6 +949,20 @@ export function getCommand(roomId, input) {
cmd = 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)) { if (CommandMap.has(cmd)) {
return () => CommandMap.get(cmd).run(roomId, args, cmd); return () => CommandMap.get(cmd).run(roomId, args, cmd);
} }

View file

@ -603,6 +603,7 @@ const stateHandlers = {
'm.room.guest_access': textForGuestAccessEvent, 'm.room.guest_access': textForGuestAccessEvent,
'm.room.related_groups': textForRelatedGroupsEvent, '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, 'im.vector.modular.widgets': textForWidgetEvent,
}; };

View file

@ -25,6 +25,7 @@ import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager'; import { accessSecretStorage } from '../../../../CrossSigningManager';
import SettingsStore from '../../../../settings/SettingsStore'; import SettingsStore from '../../../../settings/SettingsStore';
import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import {copyNode} from "../../../../utils/strings";
const PHASE_PASSPHRASE = 0; const PHASE_PASSPHRASE = 0;
const PHASE_PASSPHRASE_CONFIRM = 1; 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 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. 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 * Walks the user through the process of creating an e2e key backup
* on the server. * on the server.
@ -77,7 +68,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
async componentDidMount() { async componentDidMount() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const secureSecretStorage = ( const secureSecretStorage = (
SettingsStore.isFeatureEnabled("feature_cross_signing") && SettingsStore.getValue("feature_cross_signing") &&
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing") await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
); );
this.setState({ secureSecretStorage }); this.setState({ secureSecretStorage });
@ -101,8 +92,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
} }
_onCopyClick = () => { _onCopyClick = () => {
selectText(this._recoveryKeyNode); const successful = copyNode(this._recoveryKeyNode);
const successful = document.execCommand('copy');
if (successful) { if (successful) {
this.setState({ this.setState({
copied: true, copied: true,
@ -272,7 +262,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
let helpText; let helpText;
if (this.state.zxcvbnResult) { if (this.state.zxcvbnResult) {
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) { 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 { } else {
const suggestions = []; const suggestions = [];
for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) { 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>
<p>{_t( <p>{_t(
"We'll store an encrypted copy of your keys on our server. " + "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>
<p>{_t("For maximum security, this should be different from your account password.")}</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} onChange={this._onPassPhraseChange}
value={this.state.passPhrase} value={this.state.passPhrase}
className="mx_CreateKeyBackupDialog_passPhraseInput" className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Enter a passphrase...")} placeholder={_t("Enter a recovery passphrase...")}
autoFocus={true} autoFocus={true}
/> />
<div className="mx_CreateKeyBackupDialog_passPhraseHelp"> <div className="mx_CreateKeyBackupDialog_passPhraseHelp">
@ -364,7 +354,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}> return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t( <p>{_t(
"Please enter your passphrase a second time to confirm.", "Please enter your recovery passphrase a second time to confirm.",
)}</p> )}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer"> <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
@ -373,7 +363,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
onChange={this._onPassPhraseConfirmChange} onChange={this._onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm} value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput" className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Repeat your passphrase...")} placeholder={_t("Repeat your recovery passphrase...")}
autoFocus={true} autoFocus={true}
/> />
</div> </div>
@ -393,7 +383,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
return <div> return <div>
<p>{_t( <p>{_t(
"Your recovery key is a safety net - you can use it to restore " + "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>
<p>{_t( <p>{_t(
"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.",
@ -487,9 +477,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_titleForPhase(phase) { _titleForPhase(phase) {
switch (phase) { switch (phase) {
case PHASE_PASSPHRASE: case PHASE_PASSPHRASE:
return _t('Secure your backup with a passphrase'); return _t('Secure your backup with a recovery passphrase');
case PHASE_PASSPHRASE_CONFIRM: case PHASE_PASSPHRASE_CONFIRM:
return _t('Confirm your passphrase'); return _t('Confirm your recovery passphrase');
case PHASE_OPTOUT_CONFIRM: case PHASE_OPTOUT_CONFIRM:
return _t('Warning!'); return _t('Warning!');
case PHASE_SHOWKEY: case PHASE_SHOWKEY:

View file

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

View file

@ -24,6 +24,7 @@ import FileSaver from 'file-saver';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import Modal from '../../../../Modal'; import Modal from '../../../../Modal';
import { promptForBackupPassphrase } from '../../../../CrossSigningManager'; import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
import {copyNode} from "../../../../utils/strings";
const PHASE_LOADING = 0; const PHASE_LOADING = 0;
const PHASE_MIGRATE = 1; 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 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. 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 * Walks the user through the process of creating a passphrase to guard Secure
* Secret Storage in account data. * Secret Storage in account data.
@ -70,6 +61,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this._recoveryKey = null; this._recoveryKey = null;
this._recoveryKeyNode = null; this._recoveryKeyNode = null;
this._setZxcvbnResultTimeout = null; this._setZxcvbnResultTimeout = null;
this._backupKey = null;
this.state = { this.state = {
phase: PHASE_LOADING, phase: PHASE_LOADING,
@ -168,8 +160,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
} }
_onCopyClick = () => { _onCopyClick = () => {
selectText(this._recoveryKeyNode); const successful = copyNode(this._recoveryKeyNode);
const successful = document.execCommand('copy');
if (successful) { if (successful) {
this.setState({ this.setState({
copied: true, copied: true,
@ -243,7 +234,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
createSecretStorageKey: async () => this._recoveryKey, createSecretStorageKey: async () => this._recoveryKey,
keyBackupInfo: this.state.backupInfo, keyBackupInfo: this.state.backupInfo,
setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup, 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({ this.setState({
@ -272,10 +271,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
} }
_restoreBackup = async () => { _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 RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
const { finished } = Modal.createTrackedDialog( const { finished } = Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, {showSummary: false}, null, 'Restore Backup', '', RestoreKeyBackupDialog,
/* priority = */ false, /* static = */ false, {
showSummary: false,
keyCallback,
},
null, /* priority = */ false, /* static = */ false,
); );
await finished; await finished;
@ -455,7 +462,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let helpText; let helpText;
if (this.state.zxcvbnResult) { if (this.state.zxcvbnResult) {
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) { 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 { } else {
// We take the warning from zxcvbn or failing that, the first // We take the warning from zxcvbn or failing that, the first
// suggestion. In practice The first is generally the most relevant // 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}> return <form onSubmit={this._onPassPhraseNextClick}>
<p>{_t( <p>{_t(
"Set up encryption on this session to allow it to verify other sessions, " + "Set a recovery passphrase to secure encrypted information and recover it if you log out. " +
"granting them access to encrypted messages and marking them as trusted for other users.", "This should be different to your account password:",
)}</p>
<p>{_t(
"Secure your encryption keys with a passphrase. For maximum security " +
"this should be different to your account password:",
)}</p> )}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer"> <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
@ -494,7 +497,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
className="mx_CreateSecretStorageDialog_passPhraseField" className="mx_CreateSecretStorageDialog_passPhraseField"
onChange={this._onPassPhraseChange} onChange={this._onPassPhraseChange}
value={this.state.passPhrase} value={this.state.passPhrase}
label={_t("Enter a passphrase")} label={_t("Enter a recovery passphrase")}
autoFocus={true} autoFocus={true}
autoComplete="new-password" autoComplete="new-password"
/> />
@ -505,7 +508,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div> </div>
<LabelledToggleSwitch <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} 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'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}> return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t( <p>{_t(
"Enter your passphrase a second time to confirm it.", "Enter your recovery passphrase a second time to confirm it.",
)}</p> )}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer"> <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field <Field
@ -570,7 +573,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
onChange={this._onPassPhraseConfirmChange} onChange={this._onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm} value={this.state.passPhraseConfirm}
className="mx_CreateSecretStorageDialog_passPhraseField" className="mx_CreateSecretStorageDialog_passPhraseField"
label={_t("Confirm your passphrase")} label={_t("Confirm your recovery passphrase")}
autoFocus={true} autoFocus={true}
autoComplete="new-password" autoComplete="new-password"
/> />
@ -597,7 +600,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
return <div> return <div>
<p>{_t( <p>{_t(
"Your recovery key is a safety net - you can use it to restore " + "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>
<p>{_t( <p>{_t(
"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.",
@ -611,7 +614,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code> <code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code>
</div> </div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons"> <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")} {_t("Copy")}
</AccessibleButton> </AccessibleButton>
<AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onDownloadClick}> <AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onDownloadClick}>
@ -696,7 +703,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
case PHASE_PASSPHRASE: case PHASE_PASSPHRASE:
return _t('Set up encryption'); return _t('Set up encryption');
case PHASE_PASSPHRASE_CONFIRM: case PHASE_PASSPHRASE_CONFIRM:
return _t('Confirm passphrase'); return _t('Confirm recovery passphrase');
case PHASE_CONFIRM_SKIP: case PHASE_CONFIRM_SKIP:
return _t('Are you sure?'); return _t('Are you sure?');
case PHASE_SHOWKEY: 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) // then sort by score (Infinity if matchedString not in shortname)
sorters.push((c) => score(matchedString, c.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: // If the matchedString is not empty, sort by length of shortname. Example:
// matchedString = ":bookmark" // matchedString = ":bookmark"
// completions = [":bookmark:", ":bookmark_tabs:", ...] // 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 contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null;
const padding = 10;
const chevronOffset = {}; const chevronOffset = {};
if (props.chevronFace) { 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 // If we know the dimensions of the context menu, adjust its position
// such that it does not leave the (padded) window. // such that it does not leave the (padded) window.
if (contextMenuRect) { 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; 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 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. limitations under the License.
*/ */
import { MatrixClient } from 'matrix-js-sdk'; import * as React from 'react';
import React, {createRef} from 'react'; import * as PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import PropTypes from 'prop-types'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { DragDropContext } from 'react-beautiful-dnd'; import { DragDropContext } from 'react-beautiful-dnd';
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard'; import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard';
@ -29,10 +29,9 @@ import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import sessionStore from '../../stores/SessionStore'; import sessionStore from '../../stores/SessionStore';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import RoomListStore from "../../stores/RoomListStore"; import RoomListStore from "../../stores/RoomListStore";
import { getHomePageUrl } from '../../utils/pages';
import TagOrderActions from '../../actions/TagOrderActions'; import TagOrderActions from '../../actions/TagOrderActions';
import RoomListActions from '../../actions/RoomListActions'; import RoomListActions from '../../actions/RoomListActions';
@ -40,6 +39,9 @@ import ResizeHandle from '../views/elements/ResizeHandle';
import {Resizer, CollapseDistributor} from '../../resizer'; import {Resizer, CollapseDistributor} from '../../resizer';
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts"; 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) // 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. // 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. // NB. this is just for server notices rather than pinned messages in general.
@ -52,6 +54,52 @@ function canElementReceiveInput(el) {
!!el.getAttribute("contenteditable"); !!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 * This is what our MatrixChat shows when we are logged in. The precise view is
* determined by the page_type property. * 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. * Components mounted below us can access the matrix client via the react context.
*/ */
const LoggedInView = createReactClass({ class LoggedInView extends React.PureComponent<IProps, IState> {
displayName: 'LoggedInView', static displayName = 'LoggedInView';
propTypes: { static propTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
page_type: PropTypes.string.isRequired, page_type: PropTypes.string.isRequired,
onRoomCreated: PropTypes.func, onRoomCreated: PropTypes.func,
@ -77,25 +125,28 @@ const LoggedInView = createReactClass({
viaServers: PropTypes.arrayOf(PropTypes.string), viaServers: PropTypes.arrayOf(PropTypes.string),
// and lots and lots of other stuff. // and lots and lots of other stuff.
}, };
getInitialState: function() { protected readonly _matrixClient: MatrixClient;
return { 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 // use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'), useCompactLayout: SettingsStore.getValue('useCompactLayout'),
// any currently active server notice events // any currently active server notice events
serverNoticeEvents: [], 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 // stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient; this._matrixClient = this.props.matrixClient;
@ -117,22 +168,29 @@ const LoggedInView = createReactClass({
fixupColorFonts(); 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 // attempt to guess when a banner was opened or closed
if ( if (
(prevProps.showCookieBar !== this.props.showCookieBar) || (prevProps.showCookieBar !== this.props.showCookieBar) ||
(prevProps.hasNewVersion !== this.props.hasNewVersion) || (prevProps.hasNewVersion !== this.props.hasNewVersion) ||
(prevProps.userHasGeneratedPassword !== this.props.userHasGeneratedPassword) || (prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) ||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar) (prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
) { ) {
this.props.resizeNotifier.notifyBannersChanged(); this.props.resizeNotifier.notifyBannersChanged();
} }
}, }
componentWillUnmount: function() { componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false); document.removeEventListener('keydown', this._onNativeKeyDown, false);
this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync); this._matrixClient.removeListener("sync", this.onSync);
@ -141,7 +199,7 @@ const LoggedInView = createReactClass({
this._sessionStoreToken.remove(); this._sessionStoreToken.remove();
} }
this.resizer.detach(); this.resizer.detach();
}, }
// Child components assume that the client peg will not be null, so give them some // 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. // 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 // 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 // updates after the client peg has been made null (during logout), then it will
// attempt to re-render and the children will throw errors. // attempt to re-render and the children will throw errors.
shouldComponentUpdate: function() { shouldComponentUpdate() {
return Boolean(MatrixClientPeg.get()); return Boolean(MatrixClientPeg.get());
}, }
canResetTimelineInRoom: function(roomId) { canResetTimelineInRoom = (roomId) => {
if (!this._roomView.current) { if (!this._roomView.current) {
return true; return true;
} }
return this._roomView.current.canResetTimeline(); return this._roomView.current.canResetTimeline();
}, };
_setStateFromSessionStore() { _setStateFromSessionStore = () => {
this.setState({ this.setState({
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
}); });
}, };
_createResizer() { _createResizer() {
const classNames = { const classNames = {
@ -188,24 +246,22 @@ const LoggedInView = createReactClass({
}, },
}; };
const resizer = new Resizer( const resizer = new Resizer(
this.resizeContainer, this._resizeContainer.current,
CollapseDistributor, CollapseDistributor,
collapseConfig); collapseConfig);
resizer.setClassNames(classNames); resizer.setClassNames(classNames);
return resizer; return resizer;
}, }
_loadResizerPreferences() { _loadResizerPreferences() {
let lhsSize = window.localStorage.getItem("mx_lhs_size"); let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
if (lhsSize !== null) { if (isNaN(lhsSize)) {
lhsSize = parseInt(lhsSize, 10);
} else {
lhsSize = 350; lhsSize = 350;
} }
this.resizer.forHandleAt(0).resize(lhsSize); this.resizer.forHandleAt(0).resize(lhsSize);
}, }
onAccountData: function(event) { onAccountData = (event) => {
if (event.getType() === "im.vector.web.settings") { if (event.getType() === "im.vector.web.settings") {
this.setState({ this.setState({
useCompactLayout: event.getContent().useCompactLayout, useCompactLayout: event.getContent().useCompactLayout,
@ -214,9 +270,9 @@ const LoggedInView = createReactClass({
if (event.getType() === "m.ignored_user_list") { if (event.getType() === "m.ignored_user_list") {
dis.dispatch({action: "ignore_state_changed"}); dis.dispatch({action: "ignore_state_changed"});
} }
}, };
onSync: function(syncState, oldSyncState, data) { onSync = (syncState, oldSyncState, data) => {
const oldErrCode = ( const oldErrCode = (
this.state.syncErrorData && this.state.syncErrorData &&
this.state.syncErrorData.error && this.state.syncErrorData.error &&
@ -238,16 +294,16 @@ const LoggedInView = createReactClass({
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents(); this._updateServerNoticeEvents();
} }
}, };
onRoomStateEvents: function(ev, state) { onRoomStateEvents = (ev, state) => {
const roomLists = RoomListStore.getRoomLists(); const roomLists = RoomListStore.getRoomLists();
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) { if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents(); this._updateServerNoticeEvents();
} }
}, };
_updateServerNoticeEvents: async function() { _updateServerNoticeEvents = async () => {
const roomLists = RoomListStore.getRoomLists(); const roomLists = RoomListStore.getRoomLists();
if (!roomLists['m.server_notice']) return []; 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); const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
for (const eventId of pinnedEventIds) { for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
const ev = timeline.getEvents().find(ev => ev.getId() === eventId); const event = timeline.getEvents().find(ev => ev.getId() === eventId);
if (ev) pinnedEvents.push(ev); if (event) pinnedEvents.push(event);
} }
} }
this.setState({ this.setState({
serverNoticeEvents: pinnedEvents, serverNoticeEvents: pinnedEvents,
}); });
}, };
_onPaste: function(ev) { _onPaste = (ev) => {
let canReceiveInput = false; let canReceiveInput = false;
let element = ev.target; let element = ev.target;
// test for all parents because the target can be a child of a contenteditable element // 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 // so dispatch synchronously before paste happens
dis.dispatch({action: 'focus_composer'}, true); dis.dispatch({action: 'focus_composer'}, true);
} }
}, };
/* /*
SOME HACKERY BELOW: 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. 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. 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 // events caught while bubbling up on the root element
// of this component, so something must be focused. // of this component, so something must be focused.
this._onKeyDown(ev); this._onKeyDown(ev);
}, };
_onNativeKeyDown: function(ev) { _onNativeKeyDown = (ev) => {
// only pass this if there is no focused element. // only pass this if there is no focused element.
// if there is, _onKeyDown will be called by the // if there is, _onKeyDown will be called by the
// react keydown handler that respects the react bubbling order. // react keydown handler that respects the react bubbling order.
if (ev.target === document.body) { if (ev.target === document.body) {
this._onKeyDown(ev); 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 // 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. // Will need to find a better meta key if anyone actually cares about using this.
@ -407,6 +463,11 @@ const LoggedInView = createReactClass({
}); });
handled = true; 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) { if (handled) {
@ -432,19 +493,19 @@ const LoggedInView = createReactClass({
// that would prevent typing in the now-focussed composer // that would prevent typing in the now-focussed composer
} }
} }
}, };
/** /**
* dispatch a page-up/page-down/etc to the appropriate component * dispatch a page-up/page-down/etc to the appropriate component
* @param {Object} ev The key event * @param {Object} ev The key event
*/ */
_onScrollKeyPressed: function(ev) { _onScrollKeyPressed = (ev) => {
if (this._roomView.current) { if (this._roomView.current) {
this._roomView.current.handleScrollKey(ev); this._roomView.current.handleScrollKey(ev);
} }
}, };
_onDragEnd: function(result) { _onDragEnd = (result) => {
// Dragged to an invalid destination, not onto a droppable // Dragged to an invalid destination, not onto a droppable
if (!result.destination) { if (!result.destination) {
return; return;
@ -467,9 +528,9 @@ const LoggedInView = createReactClass({
} else if (dest.startsWith('room-sub-list-droppable_')) { } else if (dest.startsWith('room-sub-list-droppable_')) {
this._onRoomTileEndDrag(result); this._onRoomTileEndDrag(result);
} }
}, };
_onRoomTileEndDrag: function(result) { _onRoomTileEndDrag = (result) => {
let newTag = result.destination.droppableId.split('_')[1]; let newTag = result.destination.droppableId.split('_')[1];
let prevTag = result.source.droppableId.split('_')[1]; let prevTag = result.source.droppableId.split('_')[1];
if (newTag === 'undefined') newTag = undefined; if (newTag === 'undefined') newTag = undefined;
@ -486,9 +547,9 @@ const LoggedInView = createReactClass({
prevTag, newTag, prevTag, newTag,
oldIndex, newIndex, oldIndex, newIndex,
), true); ), true);
}, };
_onMouseDown: function(ev) { _onMouseDown = (ev) => {
// When the panels are disabled, clicking on them results in a mouse event // 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 // which bubbles to certain elements in the tree. When this happens, close
// any settings page that is currently open (user/room/group). // 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; if (!this.state.mouseDown) return;
const deltaX = ev.pageX - this.state.mouseDown.x; 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 // Always clear the mouseDown state to ensure we don't accidentally
// use stale values due to the mouseDown checks. // use stale values due to the mouseDown checks.
this.setState({mouseDown: null}); this.setState({mouseDown: null});
}, };
_setResizeContainerRef(div) { render() {
this.resizeContainer = div;
},
render: function() {
const LeftPanel = sdk.getComponent('structures.LeftPanel'); const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RoomView = sdk.getComponent('structures.RoomView'); const RoomView = sdk.getComponent('structures.RoomView');
const UserView = sdk.getComponent('structures.UserView'); const UserView = sdk.getComponent('structures.UserView');
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
const GroupView = sdk.getComponent('structures.GroupView'); const GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups'); const MyGroups = sdk.getComponent('structures.MyGroups');
const ToastContainer = sdk.getComponent('structures.ToastContainer'); const ToastContainer = sdk.getComponent('structures.ToastContainer');
@ -577,13 +633,7 @@ const LoggedInView = createReactClass({
break; break;
case PageTypes.HomePage: case PageTypes.HomePage:
{ pageElement = <HomePage />;
const pageUrl = getHomePageUrl(this.props.config);
pageElement = <EmbeddedPage className="mx_HomePage"
url={pageUrl}
scrollbar={true}
/>;
}
break; break;
case PageTypes.UserView: case PageTypes.UserView:
@ -654,7 +704,7 @@ const LoggedInView = createReactClass({
{ topBar } { topBar }
<ToastContainer /> <ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}> <DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._setResizeContainerRef} className={bodyClasses}> <div ref={this._resizeContainer} className={bodyClasses}>
<LeftPanel <LeftPanel
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false} collapsed={this.props.collapseLhs || false}
@ -667,7 +717,7 @@ const LoggedInView = createReactClass({
</div> </div>
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
); );
}, }
}); }
export default LoggedInView; export default LoggedInView;

View file

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

View file

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

View file

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

View file

@ -84,11 +84,13 @@ export default createReactClass({
onServerConfigChange: PropTypes.func.isRequired, onServerConfigChange: PropTypes.func.isRequired,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
isSyncing: PropTypes.bool,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
busy: false, busy: false,
busyLoggingIn: null,
errorText: null, errorText: null,
loginIncorrect: false, loginIncorrect: false,
canTryLogin: true, // can we attempt to log in or are there validation errors? 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); const componentState = AutoDiscoveryUtils.authComponentStateForError(e);
this.setState({ this.setState({
busy: false, busy: false,
busyLoggingIn: false,
...componentState, ...componentState,
}); });
aliveAgain = !componentState.serverErrorIsFatal; aliveAgain = !componentState.serverErrorIsFatal;
@ -182,6 +185,7 @@ export default createReactClass({
this.setState({ this.setState({
busy: true, busy: true,
busyLoggingIn: true,
errorText: null, errorText: null,
loginIncorrect: false, loginIncorrect: false,
}); });
@ -250,6 +254,7 @@ export default createReactClass({
this.setState({ this.setState({
busy: false, busy: false,
busyLoggingIn: false,
errorText: errorText, errorText: errorText,
// 401 would be the sensible status code for 'incorrect password' // 401 would be the sensible status code for 'incorrect password'
// but the login API gives a 403 https://matrix.org/jira/browse/SYN-744 // 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} loginIncorrect={this.state.loginIncorrect}
serverConfig={this.props.serverConfig} serverConfig={this.props.serverConfig}
disableSubmit={this.isBusy()} disableSubmit={this.isBusy()}
busy={this.props.isSyncing || this.state.busyLoggingIn}
/> />
); );
}, },
@ -629,9 +635,11 @@ export default createReactClass({
render: function() { render: function() {
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
const InlineSpinner = sdk.getComponent("elements.InlineSpinner");
const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody"); 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; 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 ( return (
<AuthPage> <AuthPage>
<AuthHeader /> <AuthHeader disableLanguageSelector={this.props.isSyncing || this.state.busyLoggingIn} />
<AuthBody> <AuthBody>
<h2> <h2>
{_t('Sign in')} {_t('Sign in')}
@ -670,9 +697,7 @@ export default createReactClass({
{ serverDeadSection } { serverDeadSection }
{ this.renderServerComponent() } { this.renderServerComponent() }
{ this.renderLoginComponentForStep() } { this.renderLoginComponentForStep() }
<a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#"> { footer }
{ _t('Create account') }
</a>
</AuthBody> </AuthBody>
</AuthPage> </AuthPage>
); );

View file

@ -108,16 +108,15 @@ export default class SetupEncryptionBody extends React.Component {
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)} member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>; />;
} else if (phase === PHASE_INTRO) { } else if (phase === PHASE_INTRO) {
const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); const ButtonPlaceholder = sdk.getComponent("elements.ButtonPlaceholder");
return ( return (
<div> <div>
<p>{_t( <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.", "granting it access to encrypted messages.",
)}</p> )}</p>
<p className="mx_CompleteSecurity_waiting"><InlineSpinner />{_t("Waiting…")}</p>
<p>{_t( <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" button: sub => <AccessibleButton element="span"
className="mx_linkButton" className="mx_linkButton"
@ -133,6 +132,7 @@ export default class SetupEncryptionBody extends React.Component {
> >
{_t("Skip")} {_t("Skip")}
</AccessibleButton> </AccessibleButton>
<ButtonPlaceholder>{_t("Use your other device to continue…")}</ButtonPlaceholder>
</div> </div>
</div> </div>
); );

View file

@ -16,12 +16,17 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
export default createReactClass({ export default createReactClass({
displayName: 'AuthHeader', displayName: 'AuthHeader',
propTypes: {
disableLanguageSelector: PropTypes.bool,
},
render: function() { render: function() {
const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo'); const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo');
const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector'); const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector');
@ -29,7 +34,7 @@ export default createReactClass({
return ( return (
<div className="mx_AuthHeader"> <div className="mx_AuthHeader">
<AuthHeaderLogo /> <AuthHeaderLogo />
<LanguageSelector /> <LanguageSelector disabled={this.props.disableLanguageSelector} />
</div> </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 />; if (SdkConfig.get()['disable_login_language_selector']) return <div />;
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown'); const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
return <LanguageDropdown className="mx_AuthBody_language" return <LanguageDropdown
className="mx_AuthBody_language"
onOptionChange={onChange} onOptionChange={onChange}
value={getCurrentLanguage()} value={getCurrentLanguage()}
disabled={disabled}
/>; />;
} }

View file

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

View file

@ -26,6 +26,7 @@ import { getHostingLink } from '../../../utils/HostingLink';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {MenuItem} from "../../structures/ContextMenu"; import {MenuItem} from "../../structures/ContextMenu";
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import {getHomePageUrl} from "../../../utils/pages";
export default class TopLeftMenu extends React.Component { export default class TopLeftMenu extends React.Component {
static propTypes = { static propTypes = {
@ -47,15 +48,7 @@ export default class TopLeftMenu extends React.Component {
} }
hasHomePage() { hasHomePage() {
const config = SdkConfig.get(); return !!getHomePageUrl(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;
} }
render() { 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 ( return (
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel} <BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
title={_t('Submit debug logs')} title={_t('Submit debug logs')}
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
> >
<div className="mx_Dialog_content" id='mx_Dialog_content'> <div className="mx_Dialog_content" id='mx_Dialog_content'>
{ warning }
<p> <p>
{ _t( { _t(
"Debug logs contain application usage data including your " + "Debug logs contain application usage data including your " +

View file

@ -24,6 +24,7 @@ import withValidation from '../elements/Validation';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
import SettingsStore from "../../../settings/SettingsStore";
export default createReactClass({ export default createReactClass({
displayName: 'CreateRoomDialog', displayName: 'CreateRoomDialog',
@ -35,6 +36,7 @@ export default createReactClass({
const config = SdkConfig.get(); const config = SdkConfig.get();
return { return {
isPublic: false, isPublic: false,
isEncrypted: true,
name: "", name: "",
topic: "", topic: "",
alias: "", alias: "",
@ -62,6 +64,11 @@ export default createReactClass({
if (this.state.noFederate) { if (this.state.noFederate) {
createOpts.creation_content = {'m.federate': false}; createOpts.creation_content = {'m.federate': false};
} }
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
opts.encryption = this.state.isEncrypted;
}
return opts; return opts;
}, },
@ -127,6 +134,10 @@ export default createReactClass({
this.setState({isPublic}); this.setState({isPublic});
}, },
onEncryptedChange(isEncrypted) {
this.setState({isEncrypted});
},
onAliasChange(alias) { onAliasChange(alias) {
this.setState({alias}); this.setState({alias});
}, },
@ -166,11 +177,10 @@ export default createReactClass({
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch'); const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField'); const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
let privateLabel; let publicPrivateLabel;
let publicLabel;
let aliasField; let aliasField;
if (this.state.isPublic) { 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(); const domain = MatrixClientPeg.get().getDomain();
aliasField = ( aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer"> <div className="mx_CreateRoomDialog_aliasContainer">
@ -178,7 +188,20 @@ export default createReactClass({
</div> </div>
); );
} else { } 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'); 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}> <form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
<div className="mx_Dialog_content"> <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 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} /> <LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
{ privateLabel } { publicPrivateLabel }
{ publicLabel } { e2eeSection }
{ aliasField } { aliasField }
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details"> <details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary> <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 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as Lifecycle from '../../../Lifecycle'; import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler'; 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 { export default class DeactivateAccountDialog extends React.Component {
constructor(props) { constructor(props) {
super(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 = { this.state = {
password: "",
busy: false,
shouldErase: false, shouldErase: false,
errStr: null, 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) { MatrixClientPeg.get().deactivateAccount(null, false).then(r => {
this.setState({ // If we got here, oops. The server didn't require any auth.
password: ev.target.value, // 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");
_onEraseFieldChange(ev) { this.setState({errStr: _t("Server did not require any authentication")});
this.setState({ }).catch(e => {
shouldErase: ev.target.checked, if (e && e.httpStatus === 401 && e.data) {
}); // Valid UIA response
} this.setState({authData: e.data});
} else {
async _onOk() { this.setState({errStr: _t("Server did not return valid authentication information.")});
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,
};
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,
}); _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;
}
this.setState({bodyText, continueText, continueKind});
};
_onUIAuthFinished = (success, result, extra) => {
if (success) return; // great! makeRequest() will be called too.
if (result === ERROR_USER_CANCELLED) {
this._onCancel();
return; return;
} }
Analytics.trackEvent('Account', 'Deactivate Account'); console.error("Error during UI Auth:", {result, extra});
Lifecycle.onLoggedOut(); this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
this.props.onFinished(true); };
}
_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() { _onCancel() {
this.props.onFinished(false); this.props.onFinished(false);
@ -95,34 +133,36 @@ export default class DeactivateAccountDialog extends React.Component {
render() { render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Loader = sdk.getComponent("elements.Spinner");
let passwordBoxClass = '';
let error = null; let error = null;
if (this.state.errStr) { if (this.state.errStr) {
error = <div className="error"> error = <div className="error">
{ this.state.errStr } { this.state.errStr }
</div>; </div>;
passwordBoxClass = 'error';
} }
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account'); let auth = <div>{_t("Loading...")}</div>;
const okEnabled = this.state.password && !this.state.busy; if (this.state.authData) {
auth = (
let cancelButton = null; <div>
if (!this.state.busy) { {this.state.bodyText}
cancelButton = <button onClick={this._onCancel} autoFocus={true}> <InteractiveAuth
{ _t("Cancel") } matrixClient={MatrixClientPeg.get()}
</button>; 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 // this is on purpose not a <form /> to prevent Enter triggering submission, to further prevent accidents
return ( return (
<BaseDialog className="mx_DeactivateAccountDialog" <BaseDialog className="mx_DeactivateAccountDialog"
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
titleClass="danger" titleClass="danger"
title={_t("Deactivate Account")} title={_t("Deactivate Account")}
> >
@ -172,28 +212,10 @@ export default class DeactivateAccountDialog extends React.Component {
</label> </label>
</p> </p>
<p>{ _t("To continue, please enter your password:") }</p> {error}
<Field {auth}
type="password"
label={_t('Password')}
onChange={this._onPasswordFieldChange}
value={this.state.password}
className={passwordBoxClass}
/>
</div> </div>
{ error }
</div>
<div className="mx_Dialog_buttons">
<button
className="mx_Dialog_primary danger"
onClick={this._onOk}
disabled={!okEnabled}
>
{ okLabel }
</button>
{ cancelButton }
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -131,7 +131,7 @@ export default class DeviceVerifyDialog extends React.Component {
} else { } else {
this._verifier = request.verifier; 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, [ this._request = await client.requestVerification(this.props.userId, [
verificationMethods.SAS, verificationMethods.SAS,
SHOW_QR_CODE_METHOD, SHOW_QR_CODE_METHOD,

View file

@ -574,7 +574,7 @@ export default class InviteDialog extends React.PureComponent {
const createRoomOptions = {inlineErrors: true}; 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. // Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room. // If so, enable encryption in the new room.
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2018 New Vector Ltd Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. limitations under the License.
*/ */
import React, {createRef} from 'react'; import * as React from 'react';
import PropTypes from 'prop-types'; import * as PropTypes from 'prop-types';
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk'; 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 * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import QRCode from 'qrcode-react'; import QRCode from 'qrcode-react';
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks"; import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import * as ContextMenu from "../../structures/ContextMenu"; import * as ContextMenu from "../../structures/ContextMenu";
import {toRightOf} from "../../structures/ContextMenu"; import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext, selectText} from "../../../utils/strings";
const socials = [ 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 = { static propTypes = {
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
target: PropTypes.oneOfType([ target: PropTypes.oneOfType([
@ -64,55 +81,45 @@ export default class ShareDialog extends React.Component {
]).isRequired, ]).isRequired,
}; };
protected closeCopiedTooltip: () => void;
constructor(props) { constructor(props) {
super(props); super(props);
this.onCopyClick = this.onCopyClick.bind(this); this.onCopyClick = this.onCopyClick.bind(this);
this.onLinkSpecificEventCheckboxClick = this.onLinkSpecificEventCheckboxClick.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 = { this.state = {
// MatrixEvent defaults to share linkSpecificEvent // MatrixEvent defaults to share linkSpecificEvent
linkSpecificEvent: this.props.target instanceof MatrixEvent, 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) { static onLinkClick(e) {
e.preventDefault(); e.preventDefault();
const {target} = e; selectText(e.target);
ShareDialog._selectText(target);
} }
onCopyClick(e) { async onCopyClick(e) {
e.preventDefault(); e.preventDefault();
const target = e.target; // copy target before we go async and React throws it away
ShareDialog._selectText(this._link.current); const successful = await copyPlaintext(this.getUrl());
const buttonRect = target.getBoundingClientRect();
let successful;
try {
successful = document.execCommand('copy');
} catch (err) {
console.error('Failed to copy: ', err);
}
const buttonRect = e.target.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu'); const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, { const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2), ...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'), message: successful ? _t('Copied!') : _t('Failed to copy'),
}); });
// Drop a reference to this close handler for componentWillUnmount // Drop a reference to this close handler for componentWillUnmount
this.closeCopiedTooltip = e.target.onmouseleave = close; this.closeCopiedTooltip = target.onmouseleave = close;
} }
onLinkSpecificEventCheckboxClick() { 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() { componentWillUnmount() {
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close // 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 // the tooltip otherwise, such as pressing Escape or clicking X really quickly
if (this.closeCopiedTooltip) this.closeCopiedTooltip(); if (this.closeCopiedTooltip) this.closeCopiedTooltip();
} }
render() { getUrl() {
let title;
let matrixToUrl; 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; let checkbox;
if (this.props.target instanceof Room) { if (this.props.target instanceof Room) {
@ -156,18 +177,10 @@ export default class ShareDialog extends React.Component {
</label> </label>
</div>; </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) { } else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
title = _t('Share User'); title = _t('Share User');
matrixToUrl = makeUserPermalink(this.props.target.userId);
} else if (this.props.target instanceof Group) { } else if (this.props.target instanceof Group) {
title = _t('Share Community'); title = _t('Share Community');
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
} else if (this.props.target instanceof MatrixEvent) { } else if (this.props.target instanceof MatrixEvent) {
title = _t('Share Room Message'); title = _t('Share Room Message');
checkbox = <div> checkbox = <div>
@ -179,14 +192,9 @@ export default class ShareDialog extends React.Component {
{ _t('Link to selected message') } { _t('Link to selected message') }
</label> </label>
</div>; </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 encodedUrl = encodeURIComponent(matrixToUrl);
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); 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_content">
<div className="mx_ShareDialog_matrixto"> <div className="mx_ShareDialog_matrixto">
<a ref={this._link} <a href={matrixToUrl}
href={matrixToUrl}
onClick={ShareDialog.onLinkClick} onClick={ShareDialog.onLinkClick}
className="mx_ShareDialog_matrixto_link" 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")} /> <QRCode value={matrixToUrl} size={256} logoWidth={48} logo={require("../../../../res/img/matrix-m.svg")} />
</div> </div>
<div className="mx_ShareDialog_social_container"> <div className="mx_ShareDialog_social_container">
{ { socials.map((social) => (
socials.map((social) => <a rel="noreferrer noopener" <a
target="_blank" rel="noreferrer noopener"
key={social.name} target="_blank"
name={social.name} key={social.name}
href={social.url(encodedUrl)} title={social.name}
className="mx_ShareDialog_social_icon" href={social.url(encodedUrl)}
className="mx_ShareDialog_social_icon"
> >
<img src={social.img} alt={social.name} height={64} width={64} /> <img src={social.img} alt={social.name} height={64} width={64} />
</a>) </a>
} )) }
</div> </div>
</div> </div>
</div> </div>

View file

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

View file

@ -283,7 +283,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
title = _t("Recovery key mismatch"); title = _t("Recovery key mismatch");
content = <div> content = <div>
<p>{_t( <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.", "please verify that you entered the correct recovery key.",
)}</p> )}</p>
</div>; </div>;
@ -291,7 +291,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
title = _t("Incorrect recovery passphrase"); title = _t("Incorrect recovery passphrase");
content = <div> content = <div>
<p>{_t( <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.", "please verify that you entered the correct recovery passphrase.",
)}</p> )}</p>
</div>; </div>;

View file

@ -119,14 +119,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
if (hasPassphrase && !this.state.forceRecoveryKey) { if (hasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter secret storage passphrase"); title = _t("Enter recovery passphrase");
let keyStatus; let keyStatus;
if (this.state.keyMatches === false) { if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"> keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t( {"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. Please verify that you " + "Unable to access secret storage. " +
"entered the correct passphrase.", "Please verify that you entered the correct recovery passphrase.",
)} )}
</div>; </div>;
} else { } else {
@ -135,13 +135,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
content = <div> content = <div>
<p>{_t( <p>{_t(
"<b>Warning</b>: You should only access secret storage " + "<b>Warning</b>: You should only do this on a trusted computer.", {},
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> }, { b: sub => <b>{sub}</b> },
)}</p> )}</p>
<p>{_t( <p>{_t(
"Access your secure message history and your cross-signing " + "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> )}</p>
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}> <form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}>
@ -164,7 +163,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
/> />
</form> </form>
{_t( {_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 " + "<button1>use your recovery key</button1> or " +
"<button2>set up new recovery options</button2>." "<button2>set up new recovery options</button2>."
, {}, { , {}, {
@ -183,7 +182,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
})} })}
</div>; </div>;
} else { } else {
title = _t("Enter secret storage recovery key"); title = _t("Enter recovery key");
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@ -193,8 +192,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} else if (this.state.keyMatches === false) { } else if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"> keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t( {"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. Please verify that you " + "Unable to access secret storage. " +
"entered the correct recovery key.", "Please verify that you entered the correct recovery key.",
)} )}
</div>; </div>;
} else if (this.state.recoveryKeyValid) { } else if (this.state.recoveryKeyValid) {
@ -209,8 +208,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
content = <div> content = <div>
<p>{_t( <p>{_t(
"<b>Warning</b>: You should only access secret storage " + "<b>Warning</b>: You should only do this on a trusted computer.", {},
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> }, { b: sub => <b>{sub}</b> },
)}</p> )}</p>
<p>{_t( <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. * If url can not be parsed, it is returned unmodified.
*/ */
_addWurlParams(urlString) { _addWurlParams(urlString) {
const u = url.parse(urlString); try {
if (!u) { const parsed = new URL(urlString);
console.error("_addWurlParams", "Invalid URL", urlString);
return url; // 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() { isMixedContent() {
@ -270,7 +269,9 @@ export default class AppTile extends React.Component {
if (this.props.show && this.state.hasPermissionToLoad) { if (this.props.show && this.state.hasPermissionToLoad) {
this.setScalarToken(); 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. // We assume that persisted widgets are loaded and don't need a spinner.
if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) { if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) {
this.setState({ this.setState({
@ -281,7 +282,9 @@ export default class AppTile extends React.Component {
if (this.state.hasPermissionToLoad) { if (this.state.hasPermissionToLoad) {
this.setScalarToken(); this.setScalarToken();
} }
} else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) { }
if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
this.setState({ this.setState({
widgetPageTitle: nextProps.widgetPageTitle, 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, /* If user has permission to modify widgets, delete the widget,
* otherwise revoke access for the widget to load in the user's browser * 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}); this.setState({deleting: true});
// HACK: This is a really dirty way to ensure that Jitsi cleans up this._endWidgetActions();
// 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';
}
WidgetUtils.setRoomWidget( WidgetUtils.setRoomWidget(
this.props.room.roomId, this.props.room.roomId,
@ -530,6 +544,10 @@ export default class AppTile extends React.Component {
if (this.props.userWidget) { if (this.props.userWidget) {
this._onMinimiseClick(); this._onMinimiseClick();
} else { } else {
if (this.props.show) {
// if we were being shown, end the widget as we're about to be minimized.
this._endWidgetActions();
}
dis.dispatch({ dis.dispatch({
action: 'appsDrawer', action: 'appsDrawer',
show: !this.props.show, 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 // True to put the toggle in front of the label
// Default false. // Default false.
toggleInFront: PropTypes.bool, toggleInFront: PropTypes.bool,
// Additional class names to append to the switch. Optional.
className: PropTypes.string,
}; };
render() { render() {
@ -50,8 +53,9 @@ export default class LabelledToggleSwitch extends React.Component {
secondPart = temp; secondPart = temp;
} }
const classes = `mx_SettingsFlag ${this.props.className || ""}`;
return ( return (
<div className="mx_SettingsFlag"> <div className={classes}>
{firstPart} {firstPart}
{secondPart} {secondPart}
</div> </div>

View file

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

View file

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

View file

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

View file

@ -32,7 +32,7 @@ import {_t} from "../../../languageHandler";
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"]; const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
const EncryptionPanel = (props) => { 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); const [request, setRequest] = useState(verificationRequest);
// state to show a spinner immediately after clicking "start verification", // state to show a spinner immediately after clicking "start verification",
// before we have a request // before we have a request
@ -133,7 +133,7 @@ const EncryptionPanel = (props) => {
isSelfVerification={isSelfVerification} isSelfVerification={isSelfVerification}
waitingForOtherParty={requested && initiatedByMe} waitingForOtherParty={requested && initiatedByMe}
waitingForNetwork={requested && !initiatedByMe} waitingForNetwork={requested && !initiatedByMe}
inDialog={inDialog} /> inDialog={layout === "dialog"} />
</React.Fragment>); </React.Fragment>);
} else { } else {
return (<React.Fragment> return (<React.Fragment>
@ -145,7 +145,7 @@ const EncryptionPanel = (props) => {
member={member} member={member}
request={request} request={request}
key={request.channel.transactionId} key={request.channel.transactionId}
inDialog={inDialog} inDialog={layout === "dialog"}
phase={phase} phase={phase}
device={device} /> device={device} />
</React.Fragment>); </React.Fragment>);

View file

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

View file

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

View file

@ -81,12 +81,14 @@ export default createReactClass({
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer'; const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
switch (action.action) { switch (action.action) {
case 'appsDrawer': 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) { if (action.show) {
localStorage.removeItem(hideWidgetKey); localStorage.setItem(hideWidgetKey, "false");
} else { } else {
// Store hidden state of widget // Store hidden state of widget
// Don't show if previously hidden // Don't show if previously hidden
localStorage.setItem(hideWidgetKey, true); localStorage.setItem(hideWidgetKey, "true");
} }
break; break;

View file

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

View file

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

View file

@ -59,6 +59,7 @@ const stateEventTileTypes = {
'm.room.power_levels': 'messages.TextualEvent', 'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events': 'messages.TextualEvent', 'm.room.pinned_events': 'messages.TextualEvent',
'm.room.server_acl': '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', 'im.vector.modular.widgets': 'messages.TextualEvent',
'm.room.tombstone': 'messages.TextualEvent', 'm.room.tombstone': 'messages.TextualEvent',
'm.room.join_rules': '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 // 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 // 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({ this.setState({
verified: E2E_STATE.WARNING, verified: E2E_STATE.WARNING,
}, this.props.onHeightChanged); }, 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; const { roomId } = this.props.member;
if (roomId) { if (roomId) {
const isRoomEncrypted = cli.isRoomEncrypted(roomId); const isRoomEncrypted = cli.isRoomEncrypted(roomId);

View file

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

View file

@ -18,7 +18,7 @@ limitations under the License.
import React, {createRef} from 'react'; import React, {createRef} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import('../../../VelocityBounce'); import '../../../VelocityBounce';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {formatDate} from '../../../DateUtils'; import {formatDate} from '../../../DateUtils';
import Velociraptor from "../../../Velociraptor"; import Velociraptor from "../../../Velociraptor";

View file

@ -363,17 +363,6 @@ export default class RoomBreadcrumbs extends React.Component {
badge = <div className={badgeClasses}>{r.formattedCount}</div>; 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 ( return (
<AccessibleButton <AccessibleButton
className={classes} className={classes}
@ -385,7 +374,6 @@ export default class RoomBreadcrumbs extends React.Component {
> >
<RoomAvatar room={r.room} width={32} height={32} /> <RoomAvatar room={r.room} width={32} height={32} />
{badge} {badge}
{dmIndicator}
{tooltip} {tooltip}
</AccessibleButton> </AccessibleButton>
); );

View file

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

View file

@ -130,6 +130,10 @@ export default createReactClass({
this._updateE2eStatus(); this._updateE2eStatus();
}, },
onCrossSigningKeysChanged: function() {
this._updateE2eStatus();
},
onRoomTimeline: function(ev, room) { onRoomTimeline: function(ev, room) {
if (!room) return; if (!room) return;
if (room.roomId != this.props.room.roomId) return; if (room.roomId != this.props.room.roomId) return;
@ -142,7 +146,7 @@ export default createReactClass({
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember); cli.on("RoomState.members", this.onRoomStateMember);
cli.on("userTrustStatusChanged", this.onUserVerificationChanged); cli.on("userTrustStatusChanged", this.onUserVerificationChanged);
cli.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
this._updateE2eStatus(); this._updateE2eStatus();
}, },
@ -151,7 +155,7 @@ export default createReactClass({
if (!cli.isRoomEncrypted(this.props.room.roomId)) { if (!cli.isRoomEncrypted(this.props.room.roomId)) {
return; return;
} }
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) { if (!SettingsStore.getValue("feature_cross_signing")) {
return; return;
} }
@ -267,6 +271,7 @@ export default createReactClass({
cli.removeListener("RoomState.events", this.onJoinRule); cli.removeListener("RoomState.events", this.onJoinRule);
cli.removeListener("RoomState.members", this.onRoomStateMember); cli.removeListener("RoomState.members", this.onRoomStateMember);
cli.removeListener("userTrustStatusChanged", this.onUserVerificationChanged); cli.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
cli.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
cli.removeListener("Room.timeline", this.onRoomTimeline); cli.removeListener("Room.timeline", this.onRoomTimeline);
} }
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange); ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
@ -479,26 +484,10 @@ export default createReactClass({
let ariaLabel = name; let ariaLabel = name;
let dmIndicator;
let dmOnline; 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 { room } = this.props;
const member = room.getMember(dmUserId); const member = room.getMember(dmUserId);
if ( if (member && member.membership === "join" && room.getJoinedMemberCount() === 2) {
member && member.membership === "join" && room.getJoinedMemberCount() === 2 &&
SettingsStore.isFeatureEnabled("feature_presence_in_room_list")
) {
const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot'); const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
dmOnline = <UserOnlineDot userId={dmUserId} />; dmOnline = <UserOnlineDot userId={dmUserId} />;
} }
@ -527,7 +516,7 @@ export default createReactClass({
} }
let privateIcon = null; let privateIcon = null;
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { if (SettingsStore.getValue("feature_cross_signing")) {
if (this.state.joinRule == "invite" && !dmUserId) { if (this.state.joinRule == "invite" && !dmUserId) {
privateIcon = <InviteOnlyIcon collapsedPanel={this.props.collapsed} />; privateIcon = <InviteOnlyIcon collapsedPanel={this.props.collapsed} />;
} }
@ -557,7 +546,6 @@ export default createReactClass({
<div className={avatarClasses}> <div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container"> <div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} /> <RoomAvatar room={this.props.room} width={24} height={24} />
{ dmIndicator }
{ e2eIcon } { e2eIcon }
</div> </div>
</div> </div>

View file

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

View file

@ -81,7 +81,9 @@ export default class CrossSigningPanel extends React.PureComponent {
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage); const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
const selfSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing")); const selfSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing"));
const userSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("user_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 secretStorageKeyInAccount = await secretStorage.hasKey();
const homeserverSupportsCrossSigning = const homeserverSupportsCrossSigning =
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
@ -94,6 +96,7 @@ export default class CrossSigningPanel extends React.PureComponent {
selfSigningPrivateKeyCached, selfSigningPrivateKeyCached,
userSigningPrivateKeyCached, userSigningPrivateKeyCached,
sessionBackupKeyCached, sessionBackupKeyCached,
sessionBackupKeyWellFormed,
secretStorageKeyInAccount, secretStorageKeyInAccount,
homeserverSupportsCrossSigning, homeserverSupportsCrossSigning,
crossSigningReady, crossSigningReady,
@ -143,6 +146,7 @@ export default class CrossSigningPanel extends React.PureComponent {
selfSigningPrivateKeyCached, selfSigningPrivateKeyCached,
userSigningPrivateKeyCached, userSigningPrivateKeyCached,
sessionBackupKeyCached, sessionBackupKeyCached,
sessionBackupKeyWellFormed,
secretStorageKeyInAccount, secretStorageKeyInAccount,
homeserverSupportsCrossSigning, homeserverSupportsCrossSigning,
crossSigningReady, 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 ( return (
<div> <div>
{summarisedStatus} {summarisedStatus}
@ -232,7 +246,10 @@ export default class CrossSigningPanel extends React.PureComponent {
</tr> </tr>
<tr> <tr>
<td>{_t("Session backup key:")}</td> <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>
<tr> <tr>
<td>{_t("Secret storage public key:")}</td> <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 // pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
const numDevices = this.state.selectedDevices.length;
const dialogAesthetics = { const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: { [SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"), 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"), continueText: _t("Single Sign On"),
continueKind: "primary", continueKind: "primary",
}, },
[SSOAuthEntry.PHASE_POSTAUTH]: { [SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("Confirm deleting these sessions"), title: _t("Confirm deleting these sessions"),
body: _t("Click the button below to confirm deleting these sessions."), body: _t("Click the button below to confirm deleting these sessions.", {
continueText: _t("Delete sessions"), count: numDevices,
}),
continueText: _t("Delete sessions", {count: numDevices}),
continueKind: "danger", continueKind: "danger",
}, },
}; };

View file

@ -75,7 +75,7 @@ export default class KeyBackupPanel extends React.PureComponent {
async _checkKeyBackupStatus() { async _checkKeyBackupStatus() {
try { try {
const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup(); const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup();
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored(); const backupKeyStored = Boolean(await MatrixClientPeg.get().isKeyBackupKeyStored());
this.setState({ this.setState({
backupInfo, backupInfo,
backupSigStatus: trustInfo, backupSigStatus: trustInfo,
@ -326,7 +326,7 @@ export default class KeyBackupPanel extends React.PureComponent {
</AccessibleButton> </AccessibleButton>
</div> </div>
); );
if (this.state.backupKeyStored && !SettingsStore.isFeatureEnabled("feature_cross_signing")) { if (this.state.backupKeyStored && !SettingsStore.getValue("feature_cross_signing")) {
buttonRow = <p> {_t( buttonRow = <p> {_t(
"Backup key stored in secret storage, but this feature is not " + "Backup key stored in secret storage, but this feature is not " +
"enabled on this session. Please enable cross-signing in Labs to " + "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.tombstone": _td("Upgrade the room"),
"m.room.encryption": _td("Enable room encryption"), "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"), "im.vector.modular.widgets": _td("Modify widgets"),
}; };
@ -47,6 +48,7 @@ const plEventsToShow = {
"m.room.tombstone": {isState: true}, "m.room.tombstone": {isState: true},
"m.room.encryption": {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}, "im.vector.modular.widgets": {isState: true},
}; };

View file

@ -270,7 +270,7 @@ export default class SecurityUserSettingsTab extends React.Component {
// can remove this. // can remove this.
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel'); const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
let crossSigning; let crossSigning;
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { if (SettingsStore.getValue("feature_cross_signing")) {
crossSigning = ( crossSigning = (
<div className='mx_SettingsTab_section'> <div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span> <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); {}, null, /* priority = */ false, /* static = */ true);
} else { } else {
const Spinner = sdk.getComponent("elements.Spinner"); 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 { try {
await accessSecretStorage(); await accessSecretStorage();
await this._waitForCompletion(); await this._waitForCompletion();

View file

@ -20,8 +20,8 @@ import { _t } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import VerificationQRCode from "../elements/crypto/VerificationQRCode"; import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
@replaceableComponent("views.verification.VerificationQREmojiOptions") @replaceableComponent("views.verification.VerificationQREmojiOptions")
export default class VerificationQREmojiOptions extends React.Component { export default class VerificationQREmojiOptions extends React.Component {
@ -31,31 +31,17 @@ export default class VerificationQREmojiOptions extends React.Component {
onStartEmoji: PropTypes.func.isRequired, 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() { render() {
let qrCode = <div className='mx_VerificationQREmojiOptions_noQR'><Spinner /></div>; const {request} = this.props;
if (this.state.qrProps) { const showQR = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
qrCode = <VerificationQRCode {...this.state.qrProps} />;
let qrCode;
if (showQR) {
qrCode = <VerificationQRCode qrCodeData={request.qrCodeData} />;
} else {
qrCode = <div className='mx_VerificationQREmojiOptions_noQR'><Spinner /></div>;
} }
return ( return (
<div> <div>
{_t("Verify this session by completing one of the following:")} {_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 {PendingActionSpinner} from "../right_panel/EncryptionInfo";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import { fixupColorFonts } from '../../../utils/FontManager';
function capFirst(s) { function capFirst(s) {
return s.charAt(0).toUpperCase() + s.slice(1); 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 = () => { onMatchClick = () => {
this.setState({ pending: true }); this.setState({ pending: true });
this.props.onDone(); this.props.onDone();
@ -125,19 +133,19 @@ export default class VerificationShowSas extends React.Component {
confirm = <DialogButtons confirm = <DialogButtons
primaryButton={_t("They match")} primaryButton={_t("They match")}
onPrimaryButtonClick={this.onMatchClick} onPrimaryButtonClick={this.onMatchClick}
primaryButtonClass="mx_UserInfo_wideButton" primaryButtonClass="mx_UserInfo_wideButton mx_VerificationShowSas_matchButton"
cancelButton={_t("They don't match")} cancelButton={_t("They don't match")}
onCancel={this.onDontMatchClick} onCancel={this.onDontMatchClick}
cancelButtonClass="mx_UserInfo_wideButton" cancelButtonClass="mx_UserInfo_wideButton mx_VerificationShowSas_noMatchButton"
/>; />;
} else { } else {
confirm = <React.Fragment> confirm = <React.Fragment>
<AccessibleButton onClick={this.onMatchClick} kind="primary">
{ _t("They match") }
</AccessibleButton>
<AccessibleButton onClick={this.onDontMatchClick} kind="danger"> <AccessibleButton onClick={this.onDontMatchClick} kind="danger">
{ _t("They don't match") } { _t("They don't match") }
</AccessibleButton> </AccessibleButton>
<AccessibleButton onClick={this.onMatchClick} kind="primary">
{ _t("They match") }
</AccessibleButton>
</React.Fragment>; </React.Fragment>;
} }

View file

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

View file

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

View file

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

View file

@ -1892,5 +1892,74 @@
"The information being sent to us to help make Riot better includes:": "Информацията, която се изпраща за да ни помогне да подобрим Riot включва:", "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.": "В тази стая има непознати сесии: ако продължите без да ги потвърдите, ще е възможно някой да подслуша обаждането ви.", "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": "Прегледай сесиите", "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", "Attachment": "Příloha",
"Autoplay GIFs and videos": "Automaticky přehrávat GIFy a videa", "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.", "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ů", "Anyone who knows the room's link, including guests": "Kdokoliv s odkazem na místnost, včetně hostů",
"Banned users": "Vykázaní uživatelé", "Banned users": "Vykázaní uživatelé",
"Ban": "Vykázat", "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>.", "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í...", "Waiting for %(userId)s to confirm...": "Čekáme až to %(userId)s potvrdí...",
"Use two-way text verification": "Použít oboustranné ověření", "Use two-way text verification": "Použít oboustranné ověření",
"Security & Privacy": "Bezpečnost a soukromí", "Security & Privacy": "Zabezpečení",
"Encryption": "Šifrování", "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í", "Encrypted": "Šifrování",
"General": "Obecné", "General": "Obecné",
"General failure": "Nějaká chyba", "General failure": "Nějaká chyba",
@ -1546,8 +1546,8 @@
"Create a private room": "Vytvořit neveřejnou místnost", "Create a private room": "Vytvořit neveřejnou místnost",
"Topic (optional)": "Téma (volitelné)", "Topic (optional)": "Téma (volitelné)",
"Make this room public": "Zveřejnit místnost", "Make this room public": "Zveřejnit místnost",
"Hide advanced": "Skrýt pokročilé", "Hide advanced": "Skrýt pokročilé možnosti",
"Show advanced": "Zobrazit pokročilé", "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!)", "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.", "Your homeserver doesn't seem to support this feature.": "Váš domovský server asi tuto funkci nepodporuje.",
"Message edits": "Editování zpráv", "Message edits": "Editování zpráv",
@ -2170,5 +2170,72 @@
"Mark all as read": "Označit vše jako přečtené", "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.", "Not currently indexing messages for any room.": "Aktuálně neindexujeme žádné zprávy.",
"Currently indexing: %(currentRoom)s.": "Aktuálně indexujeme: %(currentRoom)s.", "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.", "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.", "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", "Hangup": "Auflegen",
"Homeserver is": "Heimserver:", "Homeserver is": "Der Heimserver ist",
"Identity Server is": "Identitätsserver:", "Identity Server is": "Der Identitätsserver ist",
"I have verified my email address": "Ich habe meine E-Mail-Adresse verifiziert", "I have verified my email address": "Ich habe meine E-Mail-Adresse verifiziert",
"Import E2E room keys": "E2E-Raum-Schlüssel importieren", "Import E2E room keys": "E2E-Raum-Schlüssel importieren",
"Invalid Email Address": "Ungültige E-Mail-Adresse", "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.", "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.", "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.", "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", "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>", "<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.", "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.", "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", "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.", "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>.", "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", "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.", "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:", "You are currently subscribed to:": "Du abonnierst momentan:",
"⚠ These settings are meant for advanced users.": "⚠ Diese Einstellungen sind für fortgeschrittene Nutzer gedacht.", "⚠ These settings are meant for advanced users.": "⚠ Diese Einstellungen sind für fortgeschrittene Nutzer gedacht.",
"The version of Riot": "Die Version von Riot", "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", "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:", "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.", "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.", "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", "Notification sound": "Benachrichtigungston",
"Set a new custom sound": "Setze einen neuen benutzerdefinierten Sound", "Set a new custom sound": "Setze einen neuen benutzerdefinierten Sound",
"Browse": "Durchsuche", "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?", "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.", "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", "Suggestions": "Vorschläge",
"Recently Direct Messaged": "Kürzlich direkt verschickt", "Recently Direct Messaged": "Kürzlich direkt verschickt",
@ -1625,7 +1625,7 @@
"Create a private room": "Erstelle einen privaten Raum", "Create a private room": "Erstelle einen privaten Raum",
"Topic (optional)": "Thema (optional)", "Topic (optional)": "Thema (optional)",
"Make this room public": "Machen Sie diesen Raum öffentlich", "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!)", "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", "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.", "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", "or": "oder",
"Compare unique emoji": "Vergleiche einzigartige Emojis", "Compare unique emoji": "Vergleiche einzigartige Emojis",
"Start": "Starte", "Start": "Starte",
"Discovery": "Entdeckung", "Discovery": "Kontakte",
"Done": "Erledigt", "Done": "Erledigt",
"Manually Verify": "Manuell verifizieren", "Manually Verify": "Manuell verifizieren",
"Trusted": "Vertrauenswürdig", "Trusted": "Vertrauenswürdig",
@ -1718,8 +1718,8 @@
"Displays information about a user": "Zeigt Informationen über einen Benutzer", "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.", "%(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 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.|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.|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 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 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", "%(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", "Start chatting": "Chat starten",
"Reject & Ignore user": "Ablehnen & Nutzer ignorieren", "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 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", "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 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 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?", "Cancel entering passphrase?": "Cancel entering passphrase?",
"Enter passphrase": "Enter passphrase", "Enter passphrase": "Enter passphrase",
"Cancel": "Cancel", "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.", "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.", "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",
"Trust": "Trust", "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 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", "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", "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 this current room only": "Changes your avatar in this current room only",
"Changes your avatar in all rooms": "Changes your avatar in all rooms", "Changes your avatar in all rooms": "Changes your avatar in all rooms",
"Gets or sets the room topic": "Gets or sets the room topic", "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.", "This room has no topic.": "This room has no topic.",
"Sets the room name": "Sets the room name", "Sets the room name": "Sets the room name",
"Invites user with given id to current room": "Invites user with given id to current room", "Invites user with given id to current room": "Invites user with given id to current room",
@ -195,6 +197,8 @@
"Unignored user": "Unignored user", "Unignored user": "Unignored user",
"You are no longer ignoring %(userId)s": "You are no longer ignoring %(userId)s", "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", "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", "Deops user with given id": "Deops user with given id",
"Opens the Developer Tools dialog": "Opens the Developer Tools dialog", "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", "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", "Render simple counters in room header": "Render simple counters in room header",
"Multiple integration managers": "Multiple integration managers", "Multiple integration managers": "Multiple integration managers",
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "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", "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)", "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 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", "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)", "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", "Show previews/thumbnails for images": "Show previews/thumbnails for images",
"Enable message search in encrypted rooms": "Enable message search in encrypted rooms", "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.", "How fast should messages be downloaded.": "How fast should messages be downloaded.",
"Manually verify all remote sessions": "Manually verify all remote sessions", "Manually verify all remote sessions": "Manually verify all remote sessions",
"Collecting app version information": "Collecting app version information", "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.", "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", "Reset cross-signing and secret storage": "Reset cross-signing and secret storage",
"Bootstrap cross-signing and secret storage": "Bootstrap 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:", "Cross-signing public keys:": "Cross-signing public keys:",
"in memory": "in memory", "in memory": "in memory",
"not found": "not found", "not found": "not found",
@ -610,10 +615,13 @@
"up to date": "up to date", "up to date": "up to date",
"Your homeserver does not support session management.": "Your homeserver does not support session management.", "Your homeserver does not support session management.": "Your homeserver does not support session management.",
"Unable to load session list": "Unable to load session list", "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", "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.", "Click the button below to confirm deleting these sessions.|other": "Click the button below to confirm deleting these sessions.",
"Delete sessions": "Delete 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", "Authentication": "Authentication",
"Delete %(count)s sessions|other": "Delete %(count)s sessions", "Delete %(count)s sessions|other": "Delete %(count)s sessions",
"Delete %(count)s sessions|one": "Delete %(count)s session", "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 %(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", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
"Replying": "Replying", "Replying": "Replying",
"Direct Chat": "Direct Chat",
"Room %(name)s": "Room %(name)s", "Room %(name)s": "Room %(name)s",
"Recent rooms": "Recent rooms", "Recent rooms": "Recent rooms",
"No rooms to show": "No rooms to show", "No rooms to show": "No rooms to show",
@ -1523,6 +1530,7 @@
"Logs sent": "Logs sent", "Logs sent": "Logs sent",
"Thank you!": "Thank you!", "Thank you!": "Thank you!",
"Failed to send logs: ": "Failed to send logs: ", "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.", "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.", "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", "GitHub issue": "GitHub issue",
@ -1554,6 +1562,8 @@
"Please enter a name for the room": "Please enter a name for the room", "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.", "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.", "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 public room": "Create a public room",
"Create a private room": "Create a private room", "Create a private room": "Create a private room",
"Name": "Name", "Name": "Name",
@ -1568,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. ", "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", "Incompatible Database": "Incompatible Database",
"Continue With Encryption Disabled": "Continue With Encryption Disabled", "Continue With Encryption Disabled": "Continue With Encryption Disabled",
"Unknown error": "Unknown error", "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.",
"Incorrect password": "Incorrect password", "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>", "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.", "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.", "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)", "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", "Verify session": "Verify session",
"Use Legacy Verification (for older clients)": "Use Legacy Verification (for older clients)", "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.", "Verify by comparing a short text string.": "Verify by comparing a short text string.",
@ -1759,18 +1773,19 @@
"Upload %(count)s other files|one": "Upload %(count)s other file", "Upload %(count)s other files|one": "Upload %(count)s other file",
"Cancel All": "Cancel All", "Cancel All": "Cancel All",
"Upload Error": "Upload Error", "Upload Error": "Upload Error",
"Verify other session": "Verify other session",
"Verification Request": "Verification Request", "Verification Request": "Verification Request",
"A widget would like to verify your identity": "A widget would like to verify your identity", "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.", "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", "Remember my selection for this widget": "Remember my selection for this widget",
"Allow": "Allow", "Allow": "Allow",
"Deny": "Deny", "Deny": "Deny",
"Enter secret storage passphrase": "Enter secret storage passphrase", "Enter recovery passphrase": "Enter recovery 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.", "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 access secret storage from a trusted computer.": "<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>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 passphrase.": "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 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 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>.", "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 secret storage recovery key": "Enter secret storage recovery key", "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.", "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!", "This looks like a valid recovery key!": "This looks like a valid recovery key!",
"Not a valid recovery key": "Not a valid recovery key", "Not a valid recovery key": "Not a valid recovery key",
@ -1778,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>.", "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", "Unable to load backup status": "Unable to load backup status",
"Recovery key mismatch": "Recovery key mismatch", "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", "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", "Unable to restore backup": "Unable to restore backup",
"No backup found!": "No backup found!", "No backup found!": "No backup found!",
"Backup restored": "Backup restored", "Backup restored": "Backup restored",
"Failed to decrypt %(failedCount)s sessions!": "Failed to decrypt %(failedCount)s sessions!", "Failed to decrypt %(failedCount)s sessions!": "Failed to decrypt %(failedCount)s sessions!",
"Restored %(sessionCount)s session keys": "Restored %(sessionCount)s session keys", "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.", "<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.", "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>", "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.", "<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.", "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>", "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>",
@ -1826,6 +1839,7 @@
"Forget": "Forget", "Forget": "Forget",
"Favourite": "Favourite", "Favourite": "Favourite",
"Low Priority": "Low Priority", "Low Priority": "Low Priority",
"Direct Chat": "Direct Chat",
"Clear status": "Clear status", "Clear status": "Clear status",
"Update status": "Update status", "Update status": "Update status",
"Set status": "Set status", "Set status": "Set status",
@ -1956,6 +1970,11 @@
"Community %(groupId)s not found": "Community %(groupId)s not found", "Community %(groupId)s not found": "Community %(groupId)s not found",
"This homeserver does not support communities": "This homeserver does not support communities", "This homeserver does not support communities": "This homeserver does not support communities",
"Failed to load %(groupId)s": "Failed to load %(groupId)s", "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", "Explore": "Explore",
"Filter": "Filter", "Filter": "Filter",
"Filter rooms…": "Filter rooms…", "Filter rooms…": "Filter rooms…",
@ -1965,6 +1984,7 @@
"Failed to leave room": "Failed to leave room", "Failed to leave room": "Failed to leave room",
"Can't leave Server Notices room": "Can't leave Server Notices 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.", "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", "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.", "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", "Terms and Conditions": "Terms and Conditions",
@ -2045,7 +2065,6 @@
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "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", "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", "Could not load user profile": "Could not load user profile",
"Complete security": "Complete security",
"Session verified": "Session verified", "Session verified": "Session verified",
"Failed to send email": "Failed to send email", "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.", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
@ -2083,6 +2102,9 @@
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", "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 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.", "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", "Create account": "Create account",
"Failed to fetch avatar URL": "Failed to fetch avatar URL", "Failed to fetch avatar URL": "Failed to fetch avatar URL",
"Set a display name:": "Set a display name:", "Set a display name:": "Set a display name:",
@ -2096,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.", "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", "Registration Successful": "Registration Successful",
"Create your account": "Create your account", "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.", "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.",
"Waiting…": "Waiting…", "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>",
"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 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. 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.", "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.", "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", "Go Back": "Go Back",
"Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem", "Failed to re-authenticate 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", "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.", "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.", "Enter your password to sign in and regain access to your account.": "Enter your password to sign in and regain access to your account.",
@ -2159,18 +2182,17 @@
"Restore": "Restore", "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.", "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.", "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.", "Great! This recovery passphrase looks strong enough.": "Great! This recovery 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.", "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:",
"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 recovery passphrase": "Enter a recovery passphrase",
"Enter a passphrase": "Enter a passphrase", "Back up encrypted message keys": "Back up encrypted message keys",
"Back up my encryption keys, securing them with the same passphrase": "Back up my encryption keys, securing them with the same passphrase",
"Set up with a recovery key": "Set up with a recovery key", "Set up with a recovery key": "Set up with a recovery key",
"That matches!": "That matches!", "That matches!": "That matches!",
"That doesn't match.": "That doesn't match.", "That doesn't match.": "That doesn't match.",
"Go back to set it again.": "Go back to set it again.", "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.", "Enter your recovery passphrase a second time to confirm it.": "Enter your recovery passphrase a second time to confirm it.",
"Confirm your passphrase": "Confirm your passphrase", "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 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 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.", "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", "Your recovery key": "Your recovery key",
"Copy": "Copy", "Copy": "Copy",
@ -2182,19 +2204,20 @@
"<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage", "<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.", "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", "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", "Make a copy of your recovery key": "Make a copy of your recovery key",
"You're done!": "You're done!", "You're done!": "You're done!",
"Unable to set up secret storage": "Unable to set up secret storage", "Unable to set up secret storage": "Unable to set up secret storage",
"Retry": "Retry", "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.", "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...", "Enter a recovery passphrase...": "Enter a recovery passphrase...",
"Please enter your passphrase a second time to confirm.": "Please enter your passphrase a second time to confirm.", "Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.",
"Repeat your passphrase...": "Repeat your passphrase...", "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).", "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.", "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", "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...", "Starting backup...": "Starting backup...",
"Success!": "Success!", "Success!": "Success!",
"Create key backup": "Create key backup", "Create key backup": "Create key backup",

View file

@ -2213,7 +2213,7 @@
"If you cant access one, <button>use your recovery key or passphrase.</button>": "Se vi ne povas iun atingi, <button>uzu vian rehavan ŝlosilon aŭ pasfrazon.</button>", "If you cant access one, <button>use your recovery key or passphrase.</button>": "Se vi ne povas iun atingi, <button>uzu vian rehavan ŝlosilon aŭ pasfrazon.</button>",
"Manually Verify by Text": "Permane kontroli tekste", "Manually Verify by Text": "Permane kontroli tekste",
"Interactively verify by Emoji": "Interage kontroli bildosigne", "Interactively verify by Emoji": "Interage kontroli bildosigne",
"Self signing private key:": "Memsubskriba privata ŝlosilo", "Self signing private key:": "Memsubskriba privata ŝlosilo:",
"cached locally": "kaŝmemorita loke", "cached locally": "kaŝmemorita loke",
"not found locally": "ne trovita loke", "not found locally": "ne trovita loke",
"User signing private key:": "Uzantosubskriba privata ŝlosilo:", "User signing private key:": "Uzantosubskriba privata ŝlosilo:",
@ -2233,7 +2233,7 @@
"Signature upload success": "Alŝuto de subskribo sukcesis", "Signature upload success": "Alŝuto de subskribo sukcesis",
"Signature upload failed": "Alŝuto de subskribo malsukcesis", "Signature upload failed": "Alŝuto de subskribo malsukcesis",
"Confirm by comparing the following with the User Settings in your other session:": "Konfirmu per komparo de la sekva kun la agardoj de uzanto en via alia salutaĵo:", "Confirm by comparing the following with the User Settings in your other session:": "Konfirmu per komparo de la sekva kun la agardoj de uzanto en via alia salutaĵo:",
"Confirm this user's session by comparing the following with their User Settings:": "Konfirmu la salutaĵon de ĉi tiu uzanto per komparo de la sekva kun ĝiaj agordoj de uzanto", "Confirm this user's session by comparing the following with their User Settings:": "Konfirmu la salutaĵon de ĉi tiu uzanto per komparo de la sekva kun ĝiaj agordoj de uzanto:",
"If they don't match, the security of your communication may be compromised.": "Se ili ne akordas, la sekureco de via komunikado eble estas rompita.", "If they don't match, the security of your communication may be compromised.": "Se ili ne akordas, la sekureco de via komunikado eble estas rompita.",
"Navigation": "Navigado", "Navigation": "Navigado",
"Calls": "Vokoj", "Calls": "Vokoj",
@ -2274,5 +2274,20 @@
"Esc": "Eskapa klavo", "Esc": "Eskapa klavo",
"Enter": "Eniga klavo", "Enter": "Eniga klavo",
"Space": "Spaco", "Space": "Spaco",
"End": "Finen-klavo" "End": "Finen-klavo",
"Whether you're using Riot as an installed Progressive Web App": "Ĉu vi uzas Rioton kiel Progresan retan aplikaĵon",
"Review Sessions": "Rekontroli salutaĵojn",
"Unverified login. Was this you?": "Nekontrolita salutaĵo. Ĉu tio estis vi?",
"Manually verify all remote sessions": "Permane kontroli ĉiujn forajn salutaĵojn",
"Update your secure storage": "Ĝisdatigi vian sekuran deponejon",
"Session backup key:": "Savkopia ŝlosilo de salutaĵo:",
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Unuope kontroli ĉiun salutaĵon de uzanto por marki ĝin fidata, ne fidante transire subskribitajn aparatojn.",
"Invalid theme schema.": "Nevalida skemo de haŭto.",
"Mod": "Reguligisto",
"In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "En ĉifritaj ĉambroj, viaj mesaĝoj estas sekurigitaj, kaj nur vi kaj la ricevanto havas la unikajn malĉifrajn ŝlosilojn.",
"Verify all users in a room to ensure it's secure.": "Kontroli ĉiujn uzantojn en ĉambro por certigi, ke ĝi sekuras.",
"In encrypted rooms, verify all users to ensure its secure.": "En ĉifritaj ĉambroj, kontroli ĉiujn uzantojn por certigi, ke ili sekuras.",
"Verified": "Kontrolita",
"Verification cancelled": "Kontrolo nuliĝis",
"Compare emoji": "Kompari bildsignojn"
} }

1
src/i18n/strings/et.json Normal file
View file

@ -0,0 +1 @@
{}

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