Merge remote-tracking branch 'upstream/develop' into compact-reply-rendering

This commit is contained in:
Tulir Asokan 2020-06-07 15:12:30 +03:00
commit 4521e9feb1
234 changed files with 6906 additions and 6817 deletions

View file

@ -9,21 +9,17 @@ src/components/structures/UploadBar.js
src/components/views/avatars/MemberAvatar.js
src/components/views/create_room/RoomAlias.js
src/components/views/dialogs/SetPasswordDialog.js
src/components/views/dialogs/UnknownDeviceDialog.js
src/components/views/elements/AddressSelector.js
src/components/views/elements/DirectorySearchBox.js
src/components/views/elements/MemberEventListSummary.js
src/components/views/elements/UserSelector.js
src/components/views/globals/MatrixToolbar.js
src/components/views/globals/NewVersionBar.js
src/components/views/globals/UpdateCheckBar.js
src/components/views/messages/MFileBody.js
src/components/views/messages/TextualBody.js
src/components/views/room_settings/ColorSettings.js
src/components/views/rooms/Autocomplete.js
src/components/views/rooms/AuxPanel.js
src/components/views/rooms/LinkPreviewWidget.js
src/components/views/rooms/MemberDeviceInfo.js
src/components/views/rooms/MemberInfo.js
src/components/views/rooms/MemberList.js
src/components/views/rooms/RoomList.js

View file

@ -1,3 +1,198 @@
Changes in [2.7.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.1) (2020-06-05)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0...v2.7.1)
* Upgrade to JS SDK 6.2.1
* Fix exceptions from Tooltip
[\#4716](https://github.com/matrix-org/matrix-react-sdk/pull/4716)
* Fix not being able to dismiss new login toasts
[\#4715](https://github.com/matrix-org/matrix-react-sdk/pull/4715)
* Fix compact layout regression
[\#4714](https://github.com/matrix-org/matrix-react-sdk/pull/4714)
Changes in [2.7.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0) (2020-06-04)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0-rc.2...v2.7.0)
* Upgrade to JS SDK 6.2.0
* Prevent (double) 4S bootstrap from RestoreKeyBackupDialog
[\#4703](https://github.com/matrix-org/matrix-react-sdk/pull/4703)
* Fix checkbox bleed
[\#4702](https://github.com/matrix-org/matrix-react-sdk/pull/4702)
* Fix login loop where the sso flow returns to `#/login` to release
[\#4693](https://github.com/matrix-org/matrix-react-sdk/pull/4693)
Changes in [2.7.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0-rc.2) (2020-06-02)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0-rc.1...v2.7.0-rc.2)
* Rewire the Sticker button to be an Emoji Picker
[\#3747](https://github.com/matrix-org/matrix-react-sdk/pull/3747)
Changes in [2.7.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0-rc.1) (2020-06-02)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.1...v2.7.0-rc.1)
* Upgrade to JS SDK 6.2.0-rc.1
* Update from Weblate
[\#4683](https://github.com/matrix-org/matrix-react-sdk/pull/4683)
* Make auth argument in the register request compliant with r0.6.0
[\#4347](https://github.com/matrix-org/matrix-react-sdk/pull/4347)
* Revert "Prevent PersistedElements overflowing scrolled areas"
[\#4682](https://github.com/matrix-org/matrix-react-sdk/pull/4682)
* Remove unused TagPanelButtons
[\#4680](https://github.com/matrix-org/matrix-react-sdk/pull/4680)
* Pass roomId to IRCTimelineProfileResizer
[\#4679](https://github.com/matrix-org/matrix-react-sdk/pull/4679)
* Remove logging to console for irc name resize
[\#4678](https://github.com/matrix-org/matrix-react-sdk/pull/4678)
* Use arrow functions instead of binding `this`
[\#4677](https://github.com/matrix-org/matrix-react-sdk/pull/4677)
* Increase specificity of compact layout selectors
[\#4675](https://github.com/matrix-org/matrix-react-sdk/pull/4675)
* Create and use stylised checkboxes
[\#4665](https://github.com/matrix-org/matrix-react-sdk/pull/4665)
* useIRCLayout moved to props
[\#4676](https://github.com/matrix-org/matrix-react-sdk/pull/4676)
* Fix paste image to upload
[\#4674](https://github.com/matrix-org/matrix-react-sdk/pull/4674)
* Fix FilePanel and NotificationsPanel regression
[\#4647](https://github.com/matrix-org/matrix-react-sdk/pull/4647)
* Allow deferring of Update Toast until the next morning
[\#4669](https://github.com/matrix-org/matrix-react-sdk/pull/4669)
* Give contextual feedback for manual update check instead of banner
[\#4668](https://github.com/matrix-org/matrix-react-sdk/pull/4668)
* Dialog wrap title instead of taking same space as the close/cancel button
[\#4659](https://github.com/matrix-org/matrix-react-sdk/pull/4659)
* Update Modular hosting link
[\#4627](https://github.com/matrix-org/matrix-react-sdk/pull/4627)
* Fix field placeholder regression
[\#4663](https://github.com/matrix-org/matrix-react-sdk/pull/4663)
* Fix/document a number of UIA oddities
[\#4667](https://github.com/matrix-org/matrix-react-sdk/pull/4667)
* Stop copy icon repeating weirdly
[\#4662](https://github.com/matrix-org/matrix-react-sdk/pull/4662)
* Try and fix the Notifier race
[\#4661](https://github.com/matrix-org/matrix-react-sdk/pull/4661)
* set the client's pickle key if the platform can store one
[\#4657](https://github.com/matrix-org/matrix-react-sdk/pull/4657)
* Migrate Banners to Toasts
[\#4624](https://github.com/matrix-org/matrix-react-sdk/pull/4624)
* Move Appearance tab to ts
[\#4658](https://github.com/matrix-org/matrix-react-sdk/pull/4658)
* Fix room alias lookup vs peeking race condition
[\#4606](https://github.com/matrix-org/matrix-react-sdk/pull/4606)
* Fix encryption icon miss-alignment
[\#4651](https://github.com/matrix-org/matrix-react-sdk/pull/4651)
* Fix sublist sizing regression
[\#4649](https://github.com/matrix-org/matrix-react-sdk/pull/4649)
* Fix lines overflowing room list width
[\#4650](https://github.com/matrix-org/matrix-react-sdk/pull/4650)
* Remove the keyshare dialog
[\#4648](https://github.com/matrix-org/matrix-react-sdk/pull/4648)
* Update badge counts in new room list as needed
[\#4654](https://github.com/matrix-org/matrix-react-sdk/pull/4654)
* EventIndex: Handle invalid m.room.redaction events correctly.
[\#4653](https://github.com/matrix-org/matrix-react-sdk/pull/4653)
* EventIndex: Print out the checkpoint if there was an error during a crawl
[\#4652](https://github.com/matrix-org/matrix-react-sdk/pull/4652)
* Move Field to Typescript
[\#4635](https://github.com/matrix-org/matrix-react-sdk/pull/4635)
* Use connection error to detect network problem
[\#4646](https://github.com/matrix-org/matrix-react-sdk/pull/4646)
* Revert default font size to 15px
[\#4641](https://github.com/matrix-org/matrix-react-sdk/pull/4641)
* Add logging when room join fails
[\#4645](https://github.com/matrix-org/matrix-react-sdk/pull/4645)
* Remove EncryptedEventDialog
[\#4644](https://github.com/matrix-org/matrix-react-sdk/pull/4644)
* Migrate Toasts to Typescript and to granular priority system
[\#4618](https://github.com/matrix-org/matrix-react-sdk/pull/4618)
* Update Crypto Store Too New copy
[\#4632](https://github.com/matrix-org/matrix-react-sdk/pull/4632)
* MemberAvatar should not have its own letter fallback, it should use
BaseAvatar
[\#4643](https://github.com/matrix-org/matrix-react-sdk/pull/4643)
* Fix media upload issues with abort and status bar
[\#4630](https://github.com/matrix-org/matrix-react-sdk/pull/4630)
* fix viewGroup to actually show the group if possible
[\#4633](https://github.com/matrix-org/matrix-react-sdk/pull/4633)
* Update confirm passphrase copy
[\#4634](https://github.com/matrix-org/matrix-react-sdk/pull/4634)
* Improve accessibility of the emoji picker
[\#4636](https://github.com/matrix-org/matrix-react-sdk/pull/4636)
* Fix Emoji Picker footer being too small if text overflows
[\#4631](https://github.com/matrix-org/matrix-react-sdk/pull/4631)
* Improve style of toasts to match Figma
[\#4613](https://github.com/matrix-org/matrix-react-sdk/pull/4613)
* Iterate toast count indicator more logically
[\#4620](https://github.com/matrix-org/matrix-react-sdk/pull/4620)
* Fix reacting to redactions
[\#4626](https://github.com/matrix-org/matrix-react-sdk/pull/4626)
* Fix sentMessageAndIsAlone by dispatching `message_sent` more consistently
[\#4628](https://github.com/matrix-org/matrix-react-sdk/pull/4628)
* Update from Weblate
[\#4640](https://github.com/matrix-org/matrix-react-sdk/pull/4640)
* Replace `alias` with `address` in copy for consistency
[\#4402](https://github.com/matrix-org/matrix-react-sdk/pull/4402)
* Convert MatrixClientPeg to TypeScript
[\#4638](https://github.com/matrix-org/matrix-react-sdk/pull/4638)
* Fix BaseAvatar wrongly retrying urls
[\#4629](https://github.com/matrix-org/matrix-react-sdk/pull/4629)
* Fix event highlights not being updated to reflect edits
[\#4637](https://github.com/matrix-org/matrix-react-sdk/pull/4637)
* Calculate badges in the new room list more reliably
[\#4625](https://github.com/matrix-org/matrix-react-sdk/pull/4625)
* Transition BaseAvatar to hooks
[\#4101](https://github.com/matrix-org/matrix-react-sdk/pull/4101)
* Convert BasePlatform and BaseEventIndexManager to Typescript
[\#4614](https://github.com/matrix-org/matrix-react-sdk/pull/4614)
* Fix: Tag_DM is not defined
[\#4619](https://github.com/matrix-org/matrix-react-sdk/pull/4619)
* Fix visibility of message timestamps
[\#4615](https://github.com/matrix-org/matrix-react-sdk/pull/4615)
* Rewrite the room list store
[\#4253](https://github.com/matrix-org/matrix-react-sdk/pull/4253)
* Update code style to mention switch statements
[\#4610](https://github.com/matrix-org/matrix-react-sdk/pull/4610)
* Fix key backup restore with SSSS
[\#4612](https://github.com/matrix-org/matrix-react-sdk/pull/4612)
* Handle null tokens in the crawler loop.
[\#4608](https://github.com/matrix-org/matrix-react-sdk/pull/4608)
* Font scaling settings and slider
[\#4424](https://github.com/matrix-org/matrix-react-sdk/pull/4424)
* Prevent PersistedElements overflowing scrolled areas
[\#4494](https://github.com/matrix-org/matrix-react-sdk/pull/4494)
* IRC ui layout
[\#4531](https://github.com/matrix-org/matrix-react-sdk/pull/4531)
* Remove SSSS key upgrade check from rageshake
[\#4607](https://github.com/matrix-org/matrix-react-sdk/pull/4607)
* Label the create room button better than "Add room"
[\#4603](https://github.com/matrix-org/matrix-react-sdk/pull/4603)
* Convert the dispatcher to TypeScript
[\#4593](https://github.com/matrix-org/matrix-react-sdk/pull/4593)
* Consolidate password/passphrase fields into a component & add dynamic colour
to progress
[\#4599](https://github.com/matrix-org/matrix-react-sdk/pull/4599)
* UserView, show Welcome page in the mid panel instead of empty space
[\#4590](https://github.com/matrix-org/matrix-react-sdk/pull/4590)
* Update from Weblate
[\#4601](https://github.com/matrix-org/matrix-react-sdk/pull/4601)
* Make email auth component fail better if server claims email isn't validated
[\#4600](https://github.com/matrix-org/matrix-react-sdk/pull/4600)
* Add new keyboard shortcuts for jump to unread and upload file
[\#4588](https://github.com/matrix-org/matrix-react-sdk/pull/4588)
* accept and linkify local domains like those from mDNS
[\#4594](https://github.com/matrix-org/matrix-react-sdk/pull/4594)
* Revert "ImageView make clicking off it easier"
[\#4586](https://github.com/matrix-org/matrix-react-sdk/pull/4586)
* wrap node-qrcode in a React FC and use it for ShareDialog
[\#4394](https://github.com/matrix-org/matrix-react-sdk/pull/4394)
* Pass screenAfterLogin through SSO in the callback url
[\#4585](https://github.com/matrix-org/matrix-react-sdk/pull/4585)
* Remove debugging that causes email addresses to load forever
[\#4597](https://github.com/matrix-org/matrix-react-sdk/pull/4597)
Changes in [2.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.1) (2020-05-22)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.0...v2.6.1)

View file

@ -4,7 +4,7 @@ Matrix JavaScript/ECMAScript Style Guide
The intention of this guide is to make Matrix's JavaScript codebase clean,
consistent with other popular JavaScript styles and consistent with the rest of
the Matrix codebase. For reference, the Matrix Python style guide can be found
at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst
at https://github.com/matrix-org/synapse/blob/master/docs/code_style.md
This document reflects how we would like Matrix JavaScript code to look, with
acknowledgement that a significant amount of code is written to older
@ -17,7 +17,7 @@ writing in modern ECMAScript and using a transpile step to generate the file
that applications can then include. There are significant benefits in being
able to use modern ECMAScript, although the tooling for doing so can be awkward
for library code, especially with regard to translating source maps and line
number throgh from the original code to the final application.
number through from the original code to the final application.
General Style
-------------

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "2.6.1",
"version": "2.7.1",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -65,8 +65,8 @@
"create-react-class": "^15.6.0",
"diff-dom": "^4.1.3",
"diff-match-patch": "^1.0.4",
"emojibase-data": "^4.0.2",
"emojibase-regex": "^3.0.0",
"emojibase-data": "^5.0.1",
"emojibase-regex": "^4.0.1",
"escape-html": "^1.0.3",
"file-saver": "^1.3.3",
"filesize": "3.5.6",
@ -93,6 +93,7 @@
"react-beautiful-dnd": "^4.0.1",
"react-dom": "^16.9.0",
"react-focus-lock": "^2.2.1",
"react-resizable": "^1.10.1",
"resize-observer-polyfill": "^1.5.0",
"sanitize-html": "^1.18.4",
"text-encoding-utf-8": "^1.0.1",
@ -119,9 +120,12 @@
"@peculiar/webcrypto": "^1.0.22",
"@types/classnames": "^2.2.10",
"@types/flux": "^3.1.9",
"@types/lodash": "^4.14.152",
"@types/modernizr": "^3.5.3",
"@types/node": "^12.12.41",
"@types/qrcode": "^1.3.4",
"@types/react": "16.9",
"@types/react": "^16.9",
"@types/react-dom": "^16.9.8",
"@types/zxcvbn": "^4.4.0",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
@ -161,7 +165,9 @@
"testMatch": [
"<rootDir>/test/**/*-test.js"
],
"setupFiles": ["jest-canvas-mock"],
"setupFiles": [
"jest-canvas-mock"
],
"setupFilesAfterEnv": [
"<rootDir>/test/setupTests.js"
],

View file

@ -335,6 +335,9 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
.mx_Dialog_header.mx_Dialog_headerWithButton > .mx_Dialog_title {
text-align: center;
}
.mx_Dialog_header.mx_Dialog_headerWithCancel > .mx_Dialog_title {
margin-right: 20px; // leave space for the 'X' cancel button
}
.mx_Dialog_title.danger {
color: $warning-color;

View file

@ -61,9 +61,7 @@
@import "./views/dialogs/_CreateGroupDialog.scss";
@import "./views/dialogs/_CreateRoomDialog.scss";
@import "./views/dialogs/_DeactivateAccountDialog.scss";
@import "./views/dialogs/_DeviceVerifyDialog.scss";
@import "./views/dialogs/_DevtoolsDialog.scss";
@import "./views/dialogs/_EncryptedEventDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_InviteDialog.scss";
@ -82,7 +80,6 @@
@import "./views/dialogs/_SlashCommandHelpDialog.scss";
@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
@import "./views/dialogs/_TermsDialog.scss";
@import "./views/dialogs/_UnknownDeviceDialog.scss";
@import "./views/dialogs/_UploadConfirmDialog.scss";
@import "./views/dialogs/_UserSettingsDialog.scss";
@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
@ -117,6 +114,7 @@
@import "./views/elements/_RoomAliasField.scss";
@import "./views/elements/_Slider.scss";
@import "./views/elements/_Spinner.scss";
@import "./views/elements/_StyledCheckbox.scss";
@import "./views/elements/_SyntaxHighlight.scss";
@import "./views/elements/_TextWithTooltip.scss";
@import "./views/elements/_ToggleSwitch.scss";
@ -124,7 +122,6 @@
@import "./views/elements/_TooltipButton.scss";
@import "./views/elements/_Validation.scss";
@import "./views/emojipicker/_EmojiPicker.scss";
@import "./views/globals/_MatrixToolbar.scss";
@import "./views/groups/_GroupPublicityToggle.scss";
@import "./views/groups/_GroupRoomList.scss";
@import "./views/groups/_GroupUserSettings.scss";
@ -169,7 +166,6 @@
@import "./views/rooms/_InviteOnlyIcon.scss";
@import "./views/rooms/_JumpToBottomButton.scss";
@import "./views/rooms/_LinkPreviewWidget.scss";
@import "./views/rooms/_MemberDeviceInfo.scss";
@import "./views/rooms/_MemberInfo.scss";
@import "./views/rooms/_MemberList.scss";
@import "./views/rooms/_MessageComposer.scss";
@ -185,6 +181,7 @@
@import "./views/rooms/_RoomList.scss";
@import "./views/rooms/_RoomPreviewBar.scss";
@import "./views/rooms/_RoomRecoveryReminder.scss";
@import "./views/rooms/_RoomSublist2.scss";
@import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
@import "./views/rooms/_SearchBar.scss";
@ -205,6 +202,7 @@
@import "./views/settings/_ProfileSettings.scss";
@import "./views/settings/_SetIdServer.scss";
@import "./views/settings/_SetIntegrationManager.scss";
@import "./views/settings/_UpdateCheckButton.scss";
@import "./views/settings/tabs/_SettingsTab.scss";
@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";
@import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss";

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
$font-1px: 0.067rem;
$font-1-5px: 0.100rem;
$font-2px: 0.133rem;
$font-3px: 0.200rem;
$font-4px: 0.267rem;

View file

@ -19,9 +19,18 @@ limitations under the License.
display: flex;
/* LeftPanel 260px */
min-width: 260px;
max-width: 50%;
flex: 0 0 auto;
}
// TODO: Remove temporary indicator of new room list implementation.
// This border is meant to visually distinguish between the two components when the
// user has turned on the new room list implementation, at least until the designs
// themselves give it away.
.mx_LeftPanel2 .mx_LeftPanel {
border-left: 5px #e26dff solid;
}
.mx_LeftPanel_container.collapsed {
min-width: unset;
/* Collapsed LeftPanel 50px */

View file

@ -41,10 +41,6 @@ limitations under the License.
height: 40px;
}
.mx_MatrixChat_toolbarShowing {
height: auto;
}
.mx_MatrixChat {
width: 100%;
height: 100%;

View file

@ -63,6 +63,10 @@ limitations under the License.
padding-left: 32px;
padding-top: 8px;
position: relative;
a {
display: flex;
}
}
.mx_NotificationPanel .mx_EventTile_roomName a,

View file

@ -20,6 +20,7 @@ limitations under the License.
flex: 0 0 auto;
position: relative;
min-width: 264px;
max-width: 50%;
display: flex;
flex-direction: column;
}
@ -67,22 +68,27 @@ limitations under the License.
.mx_RightPanel_membersButton::before {
mask-image: url('$(res)/img/feather-customised/user.svg');
mask-position: center;
}
.mx_RightPanel_filesButton::before {
mask-image: url('$(res)/img/feather-customised/files.svg');
mask-position: center;
}
.mx_RightPanel_notifsButton::before {
mask-image: url('$(res)/img/feather-customised/notifications.svg');
mask-position: center;
}
.mx_RightPanel_groupMembersButton::before {
mask-image: url('$(res)/img/icons-people.svg');
mask-position: center;
}
.mx_RightPanel_roomsButton::before {
mask-image: url('$(res)/img/icons-room-nobg.svg');
mask-position: center;
}
.mx_RightPanel_headerButton_highlight::after {

View file

@ -28,8 +28,8 @@ limitations under the License.
margin: 0 4px;
grid-row: 2 / 4;
grid-column: 1;
background-color: white;
box-shadow: 0px 4px 12px $menu-box-shadow-color;
background-color: $dark-panel-bg-color;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
border-radius: 8px;
}
@ -37,16 +37,15 @@ limitations under the License.
grid-row: 1 / 3;
grid-column: 1;
color: $primary-fg-color;
background-color: $primary-bg-color;
box-shadow: 0px 4px 12px $menu-box-shadow-color;
background-color: $dark-panel-bg-color;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
border-radius: 8px;
overflow: hidden;
display: grid;
grid-template-columns: 20px 1fr;
column-gap: 10px;
grid-template-columns: 22px 1fr;
column-gap: 8px;
row-gap: 4px;
padding: 8px;
padding-right: 16px;
&.mx_Toast_hasIcon {
&::after {
@ -68,10 +67,26 @@ limitations under the License.
background-image: url("$(res)/img/e2e/warning.svg");
}
h2, .mx_Toast_body {
.mx_Toast_title, .mx_Toast_body {
grid-column: 2;
}
}
&:not(.mx_Toast_hasIcon) {
padding-left: 12px;
.mx_Toast_title {
grid-column: 1 / -1;
}
}
.mx_Toast_title,
.mx_Toast_description {
padding-right: 8px;
}
.mx_Toast_title {
width: 100%;
box-sizing: border-box;
h2 {
grid-column: 1 / 3;
@ -79,6 +94,18 @@ limitations under the License.
margin: 0;
font-size: $font-15px;
font-weight: 600;
display: inline;
width: auto;
vertical-align: middle;
}
span {
padding-left: 8px;
float: right;
font-size: $font-12px;
line-height: $font-22px;
color: $muted-fg-color;
}
}
.mx_Toast_body {
@ -87,7 +114,13 @@ limitations under the License.
}
.mx_Toast_buttons {
float: right;
display: flex;
.mx_FormButton {
min-width: 96px;
box-sizing: border-box;
}
}
.mx_Toast_description {
@ -96,6 +129,15 @@ limitations under the License.
text-overflow: ellipsis;
margin: 4px 0 11px 0;
font-size: $font-12px;
.mx_AccessibleButton_kind_link {
font-size: inherit;
padding: 0;
}
a {
text-decoration: none;
}
}
.mx_Toast_deviceID {

View file

@ -18,8 +18,3 @@ limitations under the License.
margin-top: 10px;
display: flex;
}
.mx_GroupAddressPicker_checkboxContainer input[type="checkbox"] {
/* Stop flex from shrinking the checkbox */
width: 20px;
}

View file

@ -55,6 +55,7 @@ limitations under the License.
margin-left: 5px;
width: 20px;
height: 20px;
background-repeat: none;
}
.mx_ShareDialog_split {

View file

@ -1,48 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
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_UnknownDeviceDialog {
height: 100%;
display: flex;
flex-direction: column;
}
.mx_UnknownDeviceDialog ul {
list-style: none;
padding: 0;
}
// userid
.mx_UnknownDeviceDialog p {
font-weight: bold;
font-size: $font-16px;
}
.mx_UnknownDeviceDialog .mx_DeviceVerifyButtons {
flex-direction: row !important;
}
.mx_UnknownDeviceDialog .mx_Dialog_content {
margin-bottom: 24px;
overflow-y: scroll;
}
.mx_UnknownDeviceDialog_deviceList > li {
padding: 4px;
}
.mx_UnknownDeviceDialog_deviceList > li > * {
padding-bottom: 0;
}

View file

@ -23,6 +23,7 @@ limitations under the License.
border-radius: 3px;
border: solid 1px $accent-color;
cursor: pointer;
z-index: 1;
}
.mx_AddressSelector.mx_AddressSelector_empty {

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.
*/
.mx_Checkbox {
$size: $font-16px;
$border-size: $font-1-5px;
$border-radius: $font-4px;
display: flex;
align-items: flex-start;
input[type=checkbox] {
display: none;
& + label {
display: flex;
align-items: center;
flex-grow: 1;
}
& + label > .mx_Checkbox_background {
display: inline-flex;
position: relative;
flex-shrink: 0;
height: $size;
width: $size;
size: 0.5rem;
border: $border-size solid rgba($muted-fg-color, 0.5);
box-sizing: border-box;
border-radius: $border-radius;
img {
height: 100%;
width: 100%;
filter: invert(100%);
}
}
&:checked + label > .mx_Checkbox_background {
background: $accent-color;
border-color: $accent-color;
}
& + label > *:not(.mx_Checkbox_background) {
margin-left: 10px;
}
}
}

View file

@ -190,7 +190,7 @@ limitations under the License.
.mx_EmojiPicker_footer {
border-top: 1px solid $message-action-bar-border-color;
height: 72px;
min-height: 72px;
display: flex;
align-items: center;

View file

@ -1,73 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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_MatrixToolbar {
background-color: $accent-color;
color: $accent-fg-color;
display: flex;
align-items: center;
}
.mx_MatrixToolbar_warning {
margin-left: 16px;
margin-right: 8px;
margin-top: -2px;
}
.mx_MatrixToolbar_info {
padding-left: 16px;
padding-right: 8px;
background-color: $info-bg-color;
}
.mx_MatrixToolbar_error {
padding-left: 16px;
padding-right: 8px;
background-color: $warning-bg-color;
}
.mx_MatrixToolbar_content {
flex: 1;
}
.mx_MatrixToolbar_link {
color: $accent-fg-color !important;
text-decoration: underline !important;
cursor: pointer;
}
.mx_MatrixToolbar_clickable {
cursor: pointer;
}
.mx_MatrixToolbar_close {
cursor: pointer;
}
.mx_MatrixToolbar_close img {
display: block;
float: right;
margin-right: 10px;
}
.mx_MatrixToolbar_action {
margin-right: 16px;
}
.mx_MatrixToolbar_changelog {
white-space: pre;
}

View file

@ -96,10 +96,6 @@ $AppsDrawerBodyHeight: 273px;
height: $AppsDrawerBodyHeight;
}
.mx_AppTile_persistedWrapper > div {
height: 100%;
}
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
height: 114px;
}

View file

@ -18,7 +18,6 @@ limitations under the License.
$left-gutter: 65px;
.mx_GroupLayout {
.mx_EventTile {
> .mx_SenderProfile {
line-height: $font-17px;
@ -55,9 +54,13 @@ $left-gutter: 65px;
.mx_MatrixChat_useCompactLayout {
.mx_EventTile {
padding-top: 4px;
.mx_EventTile_line, .mx_EventTile_reply {
padding-top: 0;
padding-bottom: 0;
}
.mx_EventTile.mx_EventTile_info {
&.mx_EventTile_info {
// same as the padding for non-compact .mx_EventTile.mx_EventTile_info
padding-top: 0px;
font-size: $font-13px;
@ -69,11 +72,11 @@ $left-gutter: 65px;
}
}
.mx_EventTile .mx_SenderProfile {
.mx_SenderProfile {
font-size: $font-13px;
}
.mx_EventTile.mx_EventTile_emote {
&.mx_EventTile_emote {
// add a bit more space for emotes so that avatars don't collide
padding-top: 8px;
.mx_EventTile_avatar {
@ -85,7 +88,7 @@ $left-gutter: 65px;
}
}
.mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation {
&.mx_EventTile_emote.mx_EventTile_continuation {
padding-top: 0;
.mx_EventTile_line, .mx_EventTile_reply {
padding-top: 0px;
@ -93,11 +96,6 @@ $left-gutter: 65px;
}
}
.mx_EventTile_line, .mx_EventTile_reply {
padding-top: 0px;
padding-bottom: 0px;
}
.mx_EventTile_avatar {
top: 2px;
}
@ -119,13 +117,14 @@ $left-gutter: 65px;
top: 4px;
}
.mx_RoomView_MessageList h2 {
margin-top: 6px;
}
.mx_EventTile_content .markdown-body {
p, ul, ol, dl, blockquote, pre, table {
margin-bottom: 4px; // 1/4 of the non-compact margin-bottom
}
}
}
.mx_RoomView_MessageList h2 {
margin-top: 6px;
}
}

View file

@ -41,7 +41,7 @@ $irc-line-height: $font-18px;
}
> .mx_EventTile_msgOption {
order: 4;
order: 5;
flex-shrink: 0;
}
@ -63,6 +63,8 @@ $irc-line-height: $font-18px;
flex-direction: column;
order: 3;
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
}
> .mx_EventTile_avatar {
@ -90,12 +92,14 @@ $irc-line-height: $font-18px;
text-align: right;
}
.mx_EventTile_e2eIcon {
> .mx_EventTile_e2eIcon {
position: relative;
right: unset;
left: unset;
top: -2px;
padding: 0;
order: 3;
flex-shrink: 0;
flex-grow: 0;
}
.mx_EventTile_line {
@ -113,7 +117,7 @@ $irc-line-height: $font-18px;
}
.mx_EventTile_reply {
order: 3;
order: 4;
}
.mx_EditMessageComposer_buttons {

View file

@ -1,95 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
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_MemberDeviceInfo {
display: flex;
padding-bottom: 10px;
align-items: flex-start;
}
.mx_MemberDeviceInfo_icon {
margin-top: 4px;
width: 12px;
height: 12px;
mask-repeat: no-repeat;
mask-size: 100%;
}
.mx_MemberDeviceInfo_icon_blacklisted {
mask-image: url('$(res)/img/e2e/blacklisted.svg');
background-color: $warning-color;
}
.mx_MemberDeviceInfo_icon_verified {
mask-image: url('$(res)/img/e2e/verified.svg');
background-color: $accent-color;
}
.mx_MemberDeviceInfo_icon_unverified {
mask-image: url('$(res)/img/e2e/warning.svg');
background-color: $warning-color;
}
.mx_MemberDeviceInfo > .mx_DeviceVerifyButtons {
display: flex;
flex-direction: column;
flex: 0 1 auto;
align-items: stretch;
}
.mx_MemberDeviceInfo_textButton {
@mixin mx_DialogButton_small;
margin: 2px;
flex: 1;
}
.mx_MemberDeviceInfo_textButton:hover {
@mixin mx_DialogButton_hover;
}
.mx_MemberDeviceInfo_deviceId {
word-break: break-word;
font-size: $font-13px;
}
.mx_MemberDeviceInfo_deviceInfo {
margin: 0 5px 5px 8px;
flex: 1;
}
/* "Unblacklist" is too long for a regular button: make it wider and
reduce the padding. */
.mx_EncryptedEventDialog .mx_MemberDeviceInfo_blacklist,
.mx_EncryptedEventDialog .mx_MemberDeviceInfo_unblacklist {
padding-left: 1em;
padding-right: 1em;
}
.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_verified,
.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_unverified,
.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_blacklisted {
float: right;
padding-left: 1em;
}
.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_verified {
color: $e2e-verified-color;
}
.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_unverified {
color: $e2e-unverified-color;
}
.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_blacklisted {
color: $e2e-warning-color;
}

View file

@ -214,8 +214,12 @@ limitations under the License.
mask-image: url('$(res)/img/feather-customised/video.svg');
}
.mx_MessageComposer_emoji::before {
mask-image: url('$(res)/img/feather-customised/emoji3.custom.svg');
}
.mx_MessageComposer_stickers::before {
mask-image: url('$(res)/img/feather-customised/face.svg');
mask-image: url('$(res)/img/feather-customised/sticker.custom.svg');
}
.mx_MessageComposer_formatting {

View file

@ -15,6 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomList.mx_RoomList2 {
overflow-y: auto;
}
.mx_RoomList {
/* take up remaining space below TopLeftMenu */
flex: 1;

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,16 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_DeviceVerifyDialog_cryptoSection ul {
display: table;
}
@import "../../../../node_modules/react-resizable/css/styles.css";
.mx_DeviceVerifyDialog_cryptoSection li {
display: table-row;
}
.mx_DeviceVerifyDialog_cryptoSection label,
.mx_DeviceVerifyDialog_cryptoSection span {
display: table-cell;
padding-right: 1em;
.mx_RoomList2 .mx_RoomSubList_labelContainer {
z-index: 12;
}

View file

@ -20,7 +20,7 @@ limitations under the License.
flex-direction: row;
align-items: center;
cursor: pointer;
height: 32px;
height: 34px;
margin: 0;
padding: 0 8px 0 10px;
position: relative;

View file

@ -0,0 +1,23 @@
/*
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_UpdateCheckButton_summary {
margin-left: 16px;
.mx_AccessibleButton_kind_link {
padding: 0;
}
}

View file

@ -63,4 +63,25 @@ limitations under the License.
font-size: inherit;
}
}
.mx_SecurityUserSettingsTab_warning {
color: $notice-primary-color;
position: relative;
padding-left: 40px;
margin-top: 30px;
&::before {
mask-repeat: no-repeat;
mask-position: 0 center;
mask-size: $font-24px;
position: absolute;
width: $font-24px;
height: $font-24px;
content: "";
top: 0;
left: 0;
background-color: $notice-primary-color;
mask-image: url('$(res)/img/feather-customised/alert-triangle.svg');
}
}
}

View file

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.29 3.86002L1.82002 18C1.46466 18.6154 1.46254 19.3732 1.81445 19.9905C2.16635 20.6079 2.81943 20.9922 3.53002 21H20.47C21.1806 20.9922 21.8337 20.6079 22.1856 19.9905C22.5375 19.3732 22.5354 18.6154 22.18 18L13.71 3.86002C13.3475 3.2623 12.6991 2.89728 12 2.89728C11.3009 2.89728 10.6526 3.2623 10.29 3.86002Z" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 9V13" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="17" r="1" fill="#2E2F32"/>
</svg>

After

Width:  |  Height:  |  Size: 665 B

View file

@ -0,0 +1,6 @@
<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"/>
<path d="M6.95508 14.75C8.02332 16.4046 9.88349 17.5 11.9995 17.5C14.1155 17.5 15.9757 16.4046 17.0439 14.75" stroke="#2E2F32" stroke-linecap="round"/>
<path d="M8.8 9.5C8.8 9.88024 8.69689 10.2154 8.5407 10.4497C8.38357 10.6854 8.18847 10.8 8 10.8C7.81153 10.8 7.61643 10.6854 7.4593 10.4497C7.30311 10.2154 7.2 9.88024 7.2 9.5C7.2 9.11976 7.30311 8.78457 7.4593 8.55028C7.61643 8.31459 7.81153 8.2 8 8.2C8.18847 8.2 8.38357 8.31459 8.5407 8.55028C8.69689 8.78457 8.8 9.11976 8.8 9.5Z" fill="#2E2F32" stroke="#2E2F32" stroke-width="0.4"/>
<path d="M16.8 9.5C16.8 9.88024 16.6969 10.2154 16.5407 10.4497C16.3836 10.6854 16.1885 10.8 16 10.8C15.8115 10.8 15.6164 10.6854 15.4593 10.4497C15.3031 10.2154 15.2 9.88024 15.2 9.5C15.2 9.11976 15.3031 8.78457 15.4593 8.55028C15.6164 8.31459 15.8115 8.2 16 8.2C16.1885 8.2 16.3836 8.31459 16.5407 8.55028C16.6969 8.78457 16.8 9.11976 16.8 9.5Z" fill="#2E2F32" stroke="#2E2F32" stroke-width="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.7947 23.4982C5.53814 23.3887 0.5 18.2827 0.5 12C0.5 5.64873 5.64873 0.5 12 0.5C18.2827 0.5 23.3887 5.53814 23.4982 11.7947L11.7947 23.4982Z" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.7137 23.496C10.6345 20.1166 11.3182 16.403 13.8244 13.875C16.3306 11.347 20.0122 10.6574 23.3625 11.7459" stroke="#2E2F32" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 503 B

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="17px" height="22px" viewBox="0 0 17 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: bin/sketchtool 1.4 (305) - http://www.bohemiancoding.com/sketch -->
<title>icons_browse_files</title>
<desc>Created with bin/sketchtool.</desc>
<defs></defs>
<g id="02-Chat" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="02_13-Chat-member-profile" sketch:type="MSArtboardGroup" transform="translate(-1025.000000, -33.000000)">
<g id="icons_browse_files" sketch:type="MSLayerGroup" transform="translate(1025.000000, 32.000000)">
<g id="Rectangle-5-+-Rectangle-6-Copy" transform="translate(0.000000, 1.000000)" sketch:type="MSShapeGroup">
<path d="M0,4.00955791 C0,1.79514022 1.78163126,0 3.99825563,0 L9.59161955,0 C9.59161955,0 16.3225806,6.49234232 16.3225806,6.49234232 L16.3225806,18.0063928 C16.3225806,20.2120012 14.5290874,22 12.3296282,22 L3.99295243,22 C1.7877057,22 0,20.1996477 0,17.9904421 L0,4.00955791 Z" id="Rectangle-5" stroke="#76CFA6"></path>
<path d="M15.6804916,7.49527496 L11.5273266,7.49527496 C10.3308881,7.49527496 9.3609831,6.52527676 9.3609831,5.3289315 L9.3609831,1.88544393 L15.6804916,7.49527496 Z" id="Rectangle-6-Copy" fill="#FFFFFF"></path>
<path d="M16.3225806,7.09677419 L11.4129801,7.09677419 C10.2050375,7.09677419 9.22580645,6.11744908 9.22580645,4.90960051 L9.22580645,0" id="Rectangle-6" stroke="#76CFA6"></path>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

19
src/@types/common.ts Normal file
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.
*/
// Based on https://stackoverflow.com/a/53229857/3532235
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>] ? : never}
export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;

View file

@ -15,13 +15,24 @@ limitations under the License.
*/
import * as ModernizrStatic from "modernizr";
import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg";
import ToastStore from "../stores/ToastStore";
import DeviceListener from "../DeviceListener";
import { RoomListStore2 } from "../stores/room-list/RoomListStore2";
declare global {
interface Window {
Modernizr: ModernizrStatic;
mxMatrixClientPeg: IMatrixClientPeg;
Olm: {
init: () => Promise<void>;
};
mx_ContentMessages: ContentMessages;
mx_ToastStore: ToastStore;
mx_DeviceListener: DeviceListener;
mx_RoomListStore2: RoomListStore2;
}
// workaround for https://github.com/microsoft/TypeScript/issues/30933

View file

@ -19,6 +19,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
import DMRoomMap from './utils/DMRoomMap';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember(member, width, height, resizeMethod) {
let url;
if (member && member.getAvatarUrl) {

View file

@ -21,6 +21,22 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
import dis from './dispatcher/dispatcher';
import BaseEventIndexManager from './indexing/BaseEventIndexManager';
import {ActionPayload} from "./dispatcher/payloads";
import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload";
import {Action} from "./dispatcher/actions";
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
export const HOMESERVER_URL_KEY = "mx_hs_url";
export const ID_SERVER_URL_KEY = "mx_is_url";
export enum UpdateCheckStatus {
Checking = "CHECKING",
Error = "ERROR",
NotAvailable = "NOTAVAILABLE",
Downloading = "DOWNLOADING",
Ready = "READY",
}
const UPDATE_DEFER_KEY = "mx_defer_update";
/**
* Base class for classes that provide platform-specific functionality
@ -34,6 +50,7 @@ export default abstract class BasePlatform {
constructor() {
dis.register(this.onAction);
this.startUpdateCheck = this.startUpdateCheck.bind(this);
}
protected onAction = (payload: ActionPayload) => {
@ -56,6 +73,53 @@ export default abstract class BasePlatform {
this.errorDidOccur = errorDidOccur;
}
/**
* Whether we can call checkForUpdate on this platform build
*/
async canSelfUpdate(): Promise<boolean> {
return false;
}
startUpdateCheck() {
hideUpdateToast();
localStorage.removeItem(UPDATE_DEFER_KEY);
dis.dispatch<CheckUpdatesPayload>({
action: Action.CheckUpdates,
status: UpdateCheckStatus.Checking,
});
}
/**
* Update the currently running app to the latest available version
* and replace this instance of the app with the new version.
*/
installUpdate() {
}
/**
* Check if the version update has been deferred and that deferment is still in effect
* @param newVersion the version string to check
*/
protected shouldShowUpdate(newVersion: string): boolean {
try {
const [version, deferUntil] = JSON.parse(localStorage.getItem(UPDATE_DEFER_KEY));
return newVersion !== version || Date.now() > deferUntil;
} catch (e) {
return true;
}
}
/**
* Ignore the pending update and don't prompt about this version
* until the next morning (8am).
*/
deferUpdate(newVersion: string) {
const date = new Date(Date.now() + 24 * 60 * 60 * 1000);
date.setHours(8, 0, 0, 0); // set to next 8am
localStorage.setItem(UPDATE_DEFER_KEY, JSON.stringify([newVersion, date.getTime()]));
hideUpdateToast();
}
/**
* Returns true if the platform supports displaying
* notifications, otherwise false.
@ -157,11 +221,9 @@ export default abstract class BasePlatform {
setLanguage(preferredLangs: string[]) {}
getSSOCallbackUrl(hsUrl: string, isUrl: string, fragmentAfterLogin: string): URL {
getSSOCallbackUrl(fragmentAfterLogin: string): URL {
const url = new URL(window.location.href);
url.hash = fragmentAfterLogin || "";
url.searchParams.set("homeserver", hsUrl);
url.searchParams.set("identityServer", isUrl);
return url;
}
@ -172,12 +234,47 @@ export default abstract class BasePlatform {
* @param {string} fragmentAfterLogin the hash to pass to the app during sso callback.
*/
startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) {
const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl(),
fragmentAfterLogin);
// persist hs url and is url for when the user is returned to the app with the login token
localStorage.setItem(HOMESERVER_URL_KEY, mxClient.getHomeserverUrl());
if (mxClient.getIdentityServerUrl()) {
localStorage.setItem(ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl());
}
const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin);
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO
}
onKeyDown(ev: KeyboardEvent): boolean {
return false; // no shortcuts implemented
}
/**
* Get a previously stored pickle key. The pickle key is used for
* encrypting libolm objects.
* @param {string} userId the user ID for the user that the pickle key is for.
* @param {string} userId the device ID that the pickle key is for.
* @returns {string|null} the previously stored pickle key, or null if no
* pickle key has been stored.
*/
async getPickleKey(userId: string, deviceId: string): Promise<string | null> {
return null;
}
/**
* Create and store a pickle key for encrypting libolm objects.
* @param {string} userId the user ID for the user that the pickle key is for.
* @param {string} userId the device ID that the pickle key is for.
* @returns {string|null} the pickle key, or null if the platform does not
* support storing pickle keys.
*/
async createPickleKey(userId: string, deviceId: string): Promise<string | null> {
return null;
}
/**
* Delete a previously stored pickle key from storage.
* @param {string} userId the user ID for the user that the pickle key is for.
* @param {string} userId the device ID that the pickle key is for.
*/
async destroyPickleKey(userId: string, deviceId: string): Promise<void> {
}
}

View file

@ -60,7 +60,6 @@ import * as sdk from './index';
import { _t } from './languageHandler';
import Matrix from 'matrix-js-sdk';
import dis from './dispatcher/dispatcher';
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
import WidgetUtils from './utils/WidgetUtils';
import WidgetEchoStore from './stores/WidgetEchoStore';
import SettingsStore, { SettingLevel } from './settings/SettingsStore';
@ -119,48 +118,9 @@ function pause(audioId) {
}
}
function _reAttemptCall(call) {
if (call.direction === 'outbound') {
dis.dispatch({
action: 'place_call',
room_id: call.roomId,
type: call.type,
});
} else {
call.answer();
}
}
function _setCallListeners(call) {
call.on("error", function(err) {
console.error("Call error:", err);
if (err.code === 'unknown_devices') {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Call Failed', '', QuestionDialog, {
title: _t('Call Failed'),
description: _t(
"There are unknown sessions in this room: "+
"if you proceed without verifying them, it will be "+
"possible for someone to eavesdrop on your call.",
),
button: _t('Review Sessions'),
onFinished: function(confirmed) {
if (confirmed) {
const room = MatrixClientPeg.get().getRoom(call.roomId);
showUnknownDeviceDialogForCalls(
MatrixClientPeg.get(),
room,
() => {
_reAttemptCall(call);
},
call.direction === 'outbound' ? _t("Call Anyway") : _t("Answer Anyway"),
call.direction === 'outbound' ? _t("Call") : _t("Answer"),
);
}
},
});
} else {
if (
MatrixClientPeg.get().getTurnServers().length === 0 &&
SettingsStore.getValue("fallbackICEServerAllowed") === null
@ -174,7 +134,6 @@ function _setCallListeners(call) {
title: _t('Call Failed'),
description: err.message,
});
}
});
call.on("hangup", function() {
_setCallState(undefined, call.roomId, "ended");

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,20 +16,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from "react";
import extend from './extend';
import dis from './dispatcher/dispatcher';
import {MatrixClientPeg} from './MatrixClientPeg';
import {MatrixClient} from "matrix-js-sdk/src/client";
import * as sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal';
import RoomViewStore from './stores/RoomViewStore';
import encrypt from "browser-encrypt-attachment";
import extractPngChunks from "png-chunks-extract";
import Spinner from "./components/views/elements/Spinner";
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
import "blueimp-canvas-to-blob";
import { Action } from "./dispatcher/actions";
const MAX_WIDTH = 800;
const MAX_HEIGHT = 600;
@ -39,6 +42,50 @@ const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
export class UploadCanceledError extends Error {}
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
interface IUpload {
fileName: string;
roomId: string;
total: number;
loaded: number;
promise: Promise<any>;
canceled?: boolean;
}
interface IMediaConfig {
"m.upload.size"?: number;
}
interface IContent {
body: string;
msgtype: string;
info: {
size: number;
mimetype?: string;
};
file?: string;
url?: string;
}
interface IThumbnail {
info: {
thumbnail_info: {
w: number;
h: number;
mimetype: string;
size: number;
};
w: number;
h: number;
};
thumbnail: Blob;
}
interface IAbortablePromise<T> extends Promise<T> {
abort(): void;
}
/**
* Create a thumbnail for a image DOM element.
* The image will be smaller than MAX_WIDTH and MAX_HEIGHT.
@ -51,13 +98,13 @@ export class UploadCanceledError extends Error {}
* about the original image and the thumbnail.
*
* @param {HTMLElement} element The element to thumbnail.
* @param {integer} inputWidth The width of the image in the input element.
* @param {integer} inputHeight the width of the image in the input element.
* @param {number} inputWidth The width of the image in the input element.
* @param {number} inputHeight the width of the image in the input element.
* @param {String} mimeType The mimeType to save the blob as.
* @return {Promise} A promise that resolves with an object with an info key
* and a thumbnail key.
*/
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
function createThumbnail(element: ThumbnailableElement, inputWidth: number, inputHeight: number, mimeType: string): Promise<IThumbnail> {
return new Promise((resolve) => {
let targetWidth = inputWidth;
let targetHeight = inputHeight;
@ -98,7 +145,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
* @param {File} imageFile The file to load in an image element.
* @return {Promise} A promise that resolves with the html image element.
*/
async function loadImageElement(imageFile) {
async function loadImageElement(imageFile: File) {
// Load the file into an html element
const img = document.createElement("img");
const objectUrl = URL.createObjectURL(imageFile);
@ -128,8 +175,7 @@ async function loadImageElement(imageFile) {
for (const chunk of chunks) {
if (chunk.name === 'pHYs') {
if (chunk.data.byteLength !== PHYS_HIDPI.length) return;
const hidpi = chunk.data.every((val, i) => val === PHYS_HIDPI[i]);
return hidpi;
return chunk.data.every((val, i) => val === PHYS_HIDPI[i]);
}
}
return false;
@ -152,7 +198,7 @@ async function loadImageElement(imageFile) {
*/
function infoForImageFile(matrixClient, roomId, imageFile) {
let thumbnailType = "image/png";
if (imageFile.type == "image/jpeg") {
if (imageFile.type === "image/jpeg") {
thumbnailType = "image/jpeg";
}
@ -175,15 +221,15 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
* @param {File} videoFile The file to load in an video element.
* @return {Promise} A promise that resolves with the video image element.
*/
function loadVideoElement(videoFile) {
function loadVideoElement(videoFile): Promise<HTMLVideoElement> {
return new Promise((resolve, reject) => {
// Load the file into an html element
const video = document.createElement("video");
const reader = new FileReader();
reader.onload = function(e) {
video.src = e.target.result;
reader.onload = function(ev) {
video.src = ev.target.result as string;
// Once ready, returns its size
// Wait until we have enough data to thumbnail the first frame.
@ -231,11 +277,11 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
* @return {Promise} A promise that resolves with an ArrayBuffer when the file
* is read.
*/
function readFileAsArrayBuffer(file) {
function readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function(e) {
resolve(e.target.result);
resolve(e.target.result as ArrayBuffer);
};
reader.onerror = function(e) {
reject(e);
@ -257,11 +303,11 @@ function readFileAsArrayBuffer(file) {
* If the file is unencrypted then the object will have a "url" key.
* If the file is encrypted then the object will have a "file" key.
*/
function uploadFile(matrixClient, roomId, file, progressHandler) {
function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blob, progressHandler?: any) {
let canceled = false;
if (matrixClient.isRoomEncrypted(roomId)) {
// If the room is encrypted then encrypt the file before uploading it.
// First read the file into memory.
let canceled = false;
let uploadPromise;
let encryptInfo;
const prom = readFileAsArrayBuffer(file).then(function(data) {
@ -278,9 +324,9 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
progressHandler: progressHandler,
includeFilename: false,
});
return uploadPromise;
}).then(function(url) {
if (canceled) throw new UploadCanceledError();
// If the attachment is encrypted then bundle the URL along
// with the information needed to decrypt the attachment and
// add it under a file key.
@ -290,7 +336,7 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
}
return {"file": encryptInfo};
});
prom.abort = () => {
(prom as IAbortablePromise<any>).abort = () => {
canceled = true;
if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise);
};
@ -300,55 +346,23 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
progressHandler: progressHandler,
});
const promise1 = basePromise.then(function(url) {
if (canceled) throw new UploadCanceledError();
// If the attachment isn't encrypted then include the URL directly.
return {"url": url};
});
// XXX: copy over the abort method to the new promise
promise1.abort = basePromise.abort;
promise1.abort = () => {
canceled = true;
MatrixClientPeg.get().cancelUpload(basePromise);
};
return promise1;
}
}
export default class ContentMessages {
constructor() {
this.inprogress = [];
this.nextId = 0;
this._mediaConfig = null;
}
private inprogress: IUpload[] = [];
private mediaConfig: IMediaConfig = null;
static sharedInstance() {
if (global.mx_ContentMessages === undefined) {
global.mx_ContentMessages = new ContentMessages();
}
return global.mx_ContentMessages;
}
_isFileSizeAcceptable(file) {
if (this._mediaConfig !== null &&
this._mediaConfig["m.upload.size"] !== undefined &&
file.size > this._mediaConfig["m.upload.size"]) {
return false;
}
return true;
}
_ensureMediaConfigFetched() {
if (this._mediaConfig !== null) return;
console.log("[Media Config] Fetching");
return MatrixClientPeg.get().getMediaConfig().then((config) => {
console.log("[Media Config] Fetched config:", config);
return config;
}).catch(() => {
// Media repo can't or won't report limits, so provide an empty object (no limits).
console.log("[Media Config] Could not fetch config, so not limiting uploads.");
return {};
}).then((config) => {
this._mediaConfig = config;
});
}
sendStickerContentToRoom(url, roomId, info, text, matrixClient) {
sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) {
return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
throw e;
@ -356,14 +370,14 @@ export default class ContentMessages {
}
getUploadLimit() {
if (this._mediaConfig !== null && this._mediaConfig["m.upload.size"] !== undefined) {
return this._mediaConfig["m.upload.size"];
if (this.mediaConfig !== null && this.mediaConfig["m.upload.size"] !== undefined) {
return this.mediaConfig["m.upload.size"];
} else {
return null;
}
}
async sendContentListToRoom(files, roomId, matrixClient) {
async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient) {
if (matrixClient.isGuest()) {
dis.dispatch({action: 'require_registration'});
return;
@ -372,8 +386,7 @@ export default class ContentMessages {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
if (isQuoting) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const shouldUpload = await new Promise((resolve) => {
Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, {
const {finished} = Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, {
title: _t('Replying With Files'),
description: (
<div>{_t(
@ -383,21 +396,22 @@ export default class ContentMessages {
),
hasCancelButton: true,
button: _t("Continue"),
onFinished: (shouldUpload) => {
resolve(shouldUpload);
},
});
});
const [shouldUpload]: [boolean] = await finished;
if (!shouldUpload) return;
}
await this._ensureMediaConfigFetched();
if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
await this.ensureMediaConfigFetched();
modal.close();
}
const tooBigFiles = [];
const okFiles = [];
for (let i = 0; i < files.length; ++i) {
if (this._isFileSizeAcceptable(files[i])) {
if (this.isFileSizeAcceptable(files[i])) {
okFiles.push(files[i]);
} else {
tooBigFiles.push(files[i]);
@ -406,17 +420,12 @@ export default class ContentMessages {
if (tooBigFiles.length > 0) {
const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog");
const uploadFailureDialogPromise = new Promise((resolve) => {
Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, {
const {finished} = Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, {
badFiles: tooBigFiles,
totalFiles: files.length,
contentMessages: this,
onFinished: (shouldContinue) => {
resolve(shouldContinue);
},
});
});
const shouldContinue = await uploadFailureDialogPromise;
const [shouldContinue]: [boolean] = await finished;
if (!shouldContinue) return;
}
@ -428,31 +437,47 @@ export default class ContentMessages {
for (let i = 0; i < okFiles.length; ++i) {
const file = okFiles[i];
if (!uploadAll) {
const shouldContinue = await new Promise((resolve) => {
Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
const {finished} = Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
file,
currentIndex: i,
totalFiles: okFiles.length,
onFinished: (shouldContinue, shouldUploadAll) => {
});
const [shouldContinue, shouldUploadAll]: [boolean, boolean] = await finished;
if (!shouldContinue) break;
if (shouldUploadAll) {
uploadAll = true;
}
resolve(shouldContinue);
},
});
});
if (!shouldContinue) break;
}
promBefore = this._sendContentToRoom(file, roomId, matrixClient, promBefore);
promBefore = this.sendContentToRoom(file, roomId, matrixClient, promBefore);
}
}
_sendContentToRoom(file, roomId, matrixClient, promBefore) {
const content = {
getCurrentUploads() {
return this.inprogress.filter(u => !u.canceled);
}
cancelUpload(promise: Promise<any>) {
let upload: IUpload;
for (let i = 0; i < this.inprogress.length; ++i) {
if (this.inprogress[i].promise === promise) {
upload = this.inprogress[i];
break;
}
}
if (upload) {
upload.canceled = true;
MatrixClientPeg.get().cancelUpload(upload.promise);
dis.dispatch({action: 'upload_canceled', upload});
}
}
private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise<any>) {
const content: IContent = {
body: file.name || 'Attachment',
info: {
size: file.size,
},
msgtype: "", // set later
};
// if we have a mime type for the file, add it to the message metadata
@ -461,25 +486,25 @@ export default class ContentMessages {
}
const prom = new Promise((resolve) => {
if (file.type.indexOf('image/') == 0) {
if (file.type.indexOf('image/') === 0) {
content.msgtype = 'm.image';
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
extend(content.info, imageInfo);
resolve();
}, (error)=>{
console.error(error);
}, (e) => {
console.error(e);
content.msgtype = 'm.file';
resolve();
});
} else if (file.type.indexOf('audio/') == 0) {
} else if (file.type.indexOf('audio/') === 0) {
content.msgtype = 'm.audio';
resolve();
} else if (file.type.indexOf('video/') == 0) {
} else if (file.type.indexOf('video/') === 0) {
content.msgtype = 'm.video';
infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => {
extend(content.info, videoInfo);
resolve();
}, (error)=>{
}, (e) => {
content.msgtype = 'm.file';
resolve();
});
@ -489,19 +514,23 @@ export default class ContentMessages {
}
});
const upload = {
// create temporary abort handler for before the actual upload gets passed off to js-sdk
(prom as IAbortablePromise<any>).abort = () => {
upload.canceled = true;
};
const upload: IUpload = {
fileName: file.name || 'Attachment',
roomId: roomId,
total: 0,
total: file.size,
loaded: 0,
promise: prom,
};
this.inprogress.push(upload);
dis.dispatch({action: 'upload_started'});
// Focus the composer view
dis.dispatch({action: 'focus_composer'});
let error;
dis.fire(Action.FocusComposer);
function onProgress(ev) {
upload.total = ev.total;
@ -509,7 +538,9 @@ export default class ContentMessages {
dis.dispatch({action: 'upload_progress', upload: upload});
}
let error;
return prom.then(function() {
if (upload.canceled) throw new UploadCanceledError();
// XXX: upload.promise must be the promise that
// is returned by uploadFile as it has an abort()
// method hacked onto it.
@ -520,16 +551,17 @@ export default class ContentMessages {
content.file = result.file;
content.url = result.url;
});
}).then((url) => {
}).then(() => {
// Await previous message being sent into the room
return promBefore;
}).then(function() {
if (upload.canceled) throw new UploadCanceledError();
return matrixClient.sendMessage(roomId, content);
}, function(err) {
error = err;
if (!upload.canceled) {
let desc = _t("The file '%(fileName)s' failed to upload.", {fileName: upload.fileName});
if (err.http_status == 413) {
if (err.http_status === 413) {
desc = _t(
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
{fileName: upload.fileName},
@ -542,11 +574,9 @@ export default class ContentMessages {
});
}
}).finally(() => {
const inprogressKeys = Object.keys(this.inprogress);
for (let i = 0; i < this.inprogress.length; ++i) {
const k = inprogressKeys[i];
if (this.inprogress[k].promise === upload.promise) {
this.inprogress.splice(k, 1);
if (this.inprogress[i].promise === upload.promise) {
this.inprogress.splice(i, 1);
break;
}
}
@ -555,7 +585,7 @@ export default class ContentMessages {
// clear the media size limit so we fetch it again next time
// we try to upload
if (error && error.http_status === 413) {
this._mediaConfig = null;
this.mediaConfig = null;
}
dis.dispatch({action: 'upload_failed', upload, error});
} else {
@ -565,24 +595,35 @@ export default class ContentMessages {
});
}
getCurrentUploads() {
return this.inprogress.filter(u => !u.canceled);
private isFileSizeAcceptable(file: File) {
if (this.mediaConfig !== null &&
this.mediaConfig["m.upload.size"] !== undefined &&
file.size > this.mediaConfig["m.upload.size"]) {
return false;
}
return true;
}
cancelUpload(promise) {
const inprogressKeys = Object.keys(this.inprogress);
let upload;
for (let i = 0; i < this.inprogress.length; ++i) {
const k = inprogressKeys[i];
if (this.inprogress[k].promise === promise) {
upload = this.inprogress[k];
break;
}
}
if (upload) {
upload.canceled = true;
MatrixClientPeg.get().cancelUpload(upload.promise);
dis.dispatch({action: 'upload_canceled', upload});
}
private ensureMediaConfigFetched() {
if (this.mediaConfig !== null) return;
console.log("[Media Config] Fetching");
return MatrixClientPeg.get().getMediaConfig().then((config) => {
console.log("[Media Config] Fetched config:", config);
return config;
}).catch(() => {
// Media repo can't or won't report limits, so provide an empty object (no limits).
console.log("[Media Config] Could not fetch config, so not limiting uploads.");
return {};
}).then((config) => {
this.mediaConfig = config;
});
}
static sharedInstance() {
if (window.mx_ContentMessages === undefined) {
window.mx_ContentMessages = new ContentMessages();
}
return window.mx_ContentMessages;
}
}

View file

@ -15,42 +15,41 @@ limitations under the License.
*/
import {MatrixClientPeg} from './MatrixClientPeg';
import SettingsStore from './settings/SettingsStore';
import * as sdk from './index';
import { _t } from './languageHandler';
import ToastStore from './stores/ToastStore';
import {
hideToast as hideBulkUnverifiedSessionsToast,
showToast as showBulkUnverifiedSessionsToast
} from "./toasts/BulkUnverifiedSessionsToast";
import {
hideToast as hideSetupEncryptionToast,
Kind as SetupKind,
showToast as showSetupEncryptionToast
} from "./toasts/SetupEncryptionToast";
import {
hideToast as hideUnverifiedSessionsToast,
showToast as showUnverifiedSessionsToast
} from "./toasts/UnverifiedSessionToast";
import {privateShouldBeEncrypted} from "./createRoom";
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
const THIS_DEVICE_TOAST_KEY = 'setupencryption';
const OTHER_DEVICES_TOAST_KEY = 'reviewsessions';
function toastKey(deviceId) {
return "unverified_session_" + deviceId;
}
export default class DeviceListener {
static sharedInstance() {
if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener();
return global.mx_DeviceListener;
}
constructor() {
// device IDs for which the user has dismissed the verify toast ('Later')
this._dismissed = new Set();
private dismissed = new Set<string>();
// has the user dismissed any of the various nag toasts to setup encryption on this device?
this._dismissedThisDeviceToast = false;
private dismissedThisDeviceToast = false;
// cache of the key backup info
this._keyBackupInfo = null;
this._keyBackupFetchedAt = null;
private keyBackupInfo: object = null;
private keyBackupFetchedAt: number = null;
// We keep a list of our own device IDs so we can batch ones that were already
// there the last time the app launched into a single toast, but display new
// ones in their own toasts.
this._ourDeviceIdsAtStart = null;
private ourDeviceIdsAtStart: Set<string> = null;
// The set of device IDs we're currently displaying toasts for
this._displayingToastsForDeviceIds = new Set();
private displayingToastsForDeviceIds = new Set<string>();
static sharedInstance() {
if (!window.mx_DeviceListener) window.mx_DeviceListener = new DeviceListener();
return window.mx_DeviceListener;
}
start() {
@ -74,12 +73,12 @@ export default class DeviceListener {
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
MatrixClientPeg.get().removeListener('sync', this._onSync);
}
this._dismissed.clear();
this._dismissedThisDeviceToast = false;
this._keyBackupInfo = null;
this._keyBackupFetchedAt = null;
this._ourDeviceIdsAtStart = null;
this._displayingToastsForDeviceIds = new Set();
this.dismissed.clear();
this.dismissedThisDeviceToast = false;
this.keyBackupInfo = null;
this.keyBackupFetchedAt = null;
this.ourDeviceIdsAtStart = null;
this.displayingToastsForDeviceIds = new Set();
}
/**
@ -87,29 +86,29 @@ export default class DeviceListener {
*
* @param {String[]} deviceIds List of device IDs to dismiss notifications for
*/
async dismissUnverifiedSessions(deviceIds) {
async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
for (const d of deviceIds) {
this._dismissed.add(d);
this.dismissed.add(d);
}
this._recheck();
}
dismissEncryptionSetup() {
this._dismissedThisDeviceToast = true;
this.dismissedThisDeviceToast = true;
this._recheck();
}
_ensureDeviceIdsAtStartPopulated() {
if (this._ourDeviceIdsAtStart === null) {
if (this.ourDeviceIdsAtStart === null) {
const cli = MatrixClientPeg.get();
this._ourDeviceIdsAtStart = new Set(
this.ourDeviceIdsAtStart = new Set(
cli.getStoredDevicesForUser(cli.getUserId()).map(d => d.deviceId),
);
}
}
_onWillUpdateDevices = async (users, initialFetch) => {
_onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
// If we didn't know about *any* devices before (ie. it's fresh login),
// then they are all pre-existing devices, so ignore this and set the
// devicesAtStart list to the devices that we see after the fetch.
@ -122,17 +121,17 @@ export default class DeviceListener {
// before we download any new ones.
}
_onDevicesUpdated = (users) => {
_onDevicesUpdated = (users: string[]) => {
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
this._recheck();
}
_onDeviceVerificationChanged = (userId) => {
_onDeviceVerificationChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck();
}
_onUserTrustStatusChanged = (userId, trustLevel) => {
_onUserTrustStatusChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck();
}
@ -163,20 +162,25 @@ export default class DeviceListener {
// & cache the result
async _getKeyBackupInfo() {
const now = (new Date()).getTime();
if (!this._keyBackupInfo || this._keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
this._keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this._keyBackupFetchedAt = now;
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this.keyBackupFetchedAt = now;
}
return this._keyBackupInfo;
return this.keyBackupInfo;
}
private shouldShowSetupEncryptionToast() {
// In a default configuration, show the toasts. If the well-known config causes e2ee default to be false
// then do not show the toasts until user is in at least one encrypted room.
if (privateShouldBeEncrypted()) return true;
const cli = MatrixClientPeg.get();
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
}
async _recheck() {
const cli = MatrixClientPeg.get();
if (
!SettingsStore.getValue("feature_cross_signing") ||
!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
) return;
if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return;
if (!cli.isCryptoEnabled()) return;
// don't recheck until the initial sync is complete: lots of account data events will fire
@ -186,49 +190,26 @@ export default class DeviceListener {
const crossSigningReady = await cli.isCrossSigningReady();
if (this._dismissedThisDeviceToast) {
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
} else {
if (!crossSigningReady) {
// make sure our keys are finished downlaoding
if (this.dismissedThisDeviceToast || crossSigningReady) {
hideSetupEncryptionToast();
} else if (this.shouldShowSetupEncryptionToast()) {
// make sure our keys are finished downloading
await cli.downloadKeys([cli.getUserId()]);
// cross signing isn't enabled - nag to enable it
// There are 3 different toasts for:
if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
// Cross-signing on account but this device doesn't trust the master key (verify this session)
ToastStore.sharedInstance().addOrReplaceToast({
key: THIS_DEVICE_TOAST_KEY,
title: _t("Verify this session"),
icon: "verification_warning",
props: {kind: 'verify_this_session'},
component: sdk.getComponent("toasts.SetupEncryptionToast"),
});
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
} else {
const backupInfo = await this._getKeyBackupInfo();
if (backupInfo) {
// No cross-signing on account but key backup available (upgrade encryption)
ToastStore.sharedInstance().addOrReplaceToast({
key: THIS_DEVICE_TOAST_KEY,
title: _t("Encryption upgrade available"),
icon: "verification_warning",
props: {kind: 'upgrade_encryption'},
component: sdk.getComponent("toasts.SetupEncryptionToast"),
});
showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION);
} else {
// No cross-signing or key backup on account (set up encryption)
ToastStore.sharedInstance().addOrReplaceToast({
key: THIS_DEVICE_TOAST_KEY,
title: _t("Set up encryption"),
icon: "verification_warning",
props: {kind: 'set_up_encryption'},
component: sdk.getComponent("toasts.SetupEncryptionToast"),
});
showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
}
}
} else {
// cross-signing is ready, and we don't need to upgrade encryption
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
}
}
// This needs to be done after awaiting on downloadKeys() above, so
@ -239,20 +220,20 @@ export default class DeviceListener {
// (technically could just be a boolean: we don't actually
// need to remember the device IDs, but for the sake of
// symmetry...).
const oldUnverifiedDeviceIds = new Set();
const oldUnverifiedDeviceIds = new Set<string>();
// Unverified devices that have appeared since then
const newUnverifiedDeviceIds = new Set();
const newUnverifiedDeviceIds = new Set<string>();
// as long as cross-signing isn't ready,
// you can't see or dismiss any device toasts
if (crossSigningReady) {
const devices = cli.getStoredDevicesForUser(cli.getUserId());
for (const device of devices) {
if (device.deviceId == cli.deviceId) continue;
if (device.deviceId === cli.deviceId) continue;
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
if (!deviceTrust.isCrossSigningVerified() && !this._dismissed.has(device.deviceId)) {
if (this._ourDeviceIdsAtStart.has(device.deviceId)) {
if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) {
if (this.ourDeviceIdsAtStart.has(device.deviceId)) {
oldUnverifiedDeviceIds.add(device.deviceId);
} else {
newUnverifiedDeviceIds.add(device.deviceId);
@ -263,38 +244,23 @@ export default class DeviceListener {
// Display or hide the batch toast for old unverified sessions
if (oldUnverifiedDeviceIds.size > 0) {
ToastStore.sharedInstance().addOrReplaceToast({
key: OTHER_DEVICES_TOAST_KEY,
title: _t("Review where youre logged in"),
icon: "verification_warning",
priority: ToastStore.PRIORITY_LOW,
props: {
deviceIds: oldUnverifiedDeviceIds,
},
component: sdk.getComponent("toasts.BulkUnverifiedSessionsToast"),
});
showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
} else {
ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY);
hideBulkUnverifiedSessionsToast();
}
// Show toasts for new unverified devices if they aren't already there
for (const deviceId of newUnverifiedDeviceIds) {
ToastStore.sharedInstance().addOrReplaceToast({
key: toastKey(deviceId),
title: _t("New login. Was this you?"),
icon: "verification_warning",
props: { deviceId },
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
});
showUnverifiedSessionsToast(deviceId);
}
// ...and hide any we don't need any more
for (const deviceId of this._displayingToastsForDeviceIds) {
for (const deviceId of this.displayingToastsForDeviceIds) {
if (!newUnverifiedDeviceIds.has(deviceId)) {
ToastStore.sharedInstance().dismissToast(toastKey(deviceId));
hideUnverifiedSessionsToast(deviceId);
}
}
this._displayingToastsForDeviceIds = newUnverifiedDeviceIds;
this.displayingToastsForDeviceIds = newUnverifiedDeviceIds;
}
}

View file

@ -22,6 +22,7 @@ import { _t } from './languageHandler';
import {MatrixClientPeg} from './MatrixClientPeg';
import GroupStore from './stores/GroupStore';
import {allSettled} from "./utils/promise";
import StyledCheckbox from './components/views/elements/StyledCheckbox';
export function showGroupInviteDialog(groupId) {
return new Promise((resolve, reject) => {
@ -61,19 +62,19 @@ export function showGroupAddRoomDialog(groupId) {
<div>{ _t("Which rooms would you like to add to this community?") }</div>
</div>;
const checkboxContainer = <label className="mx_GroupAddressPicker_checkboxContainer">
<input type="checkbox" onChange={onCheckboxClicked} />
<div>
const checkboxContainer = <StyledCheckbox
className="mx_GroupAddressPicker_checkboxContainer"
onChange={onCheckboxClicked}
>
{ _t("Show these rooms to non-members on the community page and room list?") }
</div>
</label>;
</StyledCheckbox>;
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
title: _t("Add rooms to the community"),
description: description,
extraNode: checkboxContainer,
placeholder: _t("Room name or alias"),
placeholder: _t("Room name or address"),
button: _t("Add to community"),
pickerType: 'room',
validAddressTypes: ['mx-room-id'],

View file

@ -1,158 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 sdk from './index';
import Modal from './Modal';
import SettingsStore from './settings/SettingsStore';
// TODO: We can remove this once cross-signing is the only way.
// https://github.com/vector-im/riot-web/issues/11908
export default class KeyRequestHandler {
constructor(matrixClient) {
this._matrixClient = matrixClient;
// the user/device for which we currently have a dialog open
this._currentUser = null;
this._currentDevice = null;
// userId -> deviceId -> [keyRequest]
this._pendingKeyRequests = Object.create(null);
}
handleKeyRequest(keyRequest) {
// Ignore own device key requests if cross-signing lab enabled
if (SettingsStore.getValue("feature_cross_signing")) {
return;
}
const userId = keyRequest.userId;
const deviceId = keyRequest.deviceId;
const requestId = keyRequest.requestId;
if (!this._pendingKeyRequests[userId]) {
this._pendingKeyRequests[userId] = Object.create(null);
}
if (!this._pendingKeyRequests[userId][deviceId]) {
this._pendingKeyRequests[userId][deviceId] = [];
}
// check if we already have this request
const requests = this._pendingKeyRequests[userId][deviceId];
if (requests.find((r) => r.requestId === requestId)) {
console.log("Already have this key request, ignoring");
return;
}
requests.push(keyRequest);
if (this._currentUser) {
// ignore for now
console.log("Key request, but we already have a dialog open");
return;
}
this._processNextRequest();
}
handleKeyRequestCancellation(cancellation) {
// Ignore own device key requests if cross-signing lab enabled
if (SettingsStore.getValue("feature_cross_signing")) {
return;
}
// see if we can find the request in the queue
const userId = cancellation.userId;
const deviceId = cancellation.deviceId;
const requestId = cancellation.requestId;
if (userId === this._currentUser && deviceId === this._currentDevice) {
console.log(
"room key request cancellation for the user we currently have a"
+ " dialog open for",
);
// TODO: update the dialog. For now, we just ignore the
// cancellation.
return;
}
if (!this._pendingKeyRequests[userId]) {
return;
}
const requests = this._pendingKeyRequests[userId][deviceId];
if (!requests) {
return;
}
const idx = requests.findIndex((r) => r.requestId === requestId);
if (idx < 0) {
return;
}
console.log("Forgetting room key request");
requests.splice(idx, 1);
if (requests.length === 0) {
delete this._pendingKeyRequests[userId][deviceId];
if (Object.keys(this._pendingKeyRequests[userId]).length === 0) {
delete this._pendingKeyRequests[userId];
}
}
}
_processNextRequest() {
const userId = Object.keys(this._pendingKeyRequests)[0];
if (!userId) {
return;
}
const deviceId = Object.keys(this._pendingKeyRequests[userId])[0];
if (!deviceId) {
return;
}
console.log(`Starting KeyShareDialog for ${userId}:${deviceId}`);
const finished = (r) => {
this._currentUser = null;
this._currentDevice = null;
if (!this._pendingKeyRequests[userId] || !this._pendingKeyRequests[userId][deviceId]) {
// request was removed in the time the dialog was displayed
this._processNextRequest();
return;
}
if (r) {
for (const req of this._pendingKeyRequests[userId][deviceId]) {
req.share();
}
}
delete this._pendingKeyRequests[userId][deviceId];
if (Object.keys(this._pendingKeyRequests[userId]).length === 0) {
delete this._pendingKeyRequests[userId];
}
this._processNextRequest();
};
const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog");
Modal.appendTrackedDialog('Key Share', 'Process Next Request', KeyShareDialog, {
matrixClient: this._matrixClient,
userId: userId,
deviceId: deviceId,
onFinished: finished,
});
this._currentUser = userId;
this._currentDevice = deviceId;
}
}

View file

@ -41,6 +41,7 @@ import {IntegrationManagers} from "./integrations/IntegrationManagers";
import {Mjolnir} from "./mjolnir/Mjolnir";
import DeviceListener from "./DeviceListener";
import {Jitsi} from "./widgets/Jitsi";
import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "./BasePlatform";
/**
* Called at startup, to attempt to build a logged-in Matrix session. It tries
@ -163,14 +164,16 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
return Promise.resolve(false);
}
if (!queryParams.homeserver) {
const homeserver = localStorage.getItem(HOMESERVER_URL_KEY);
const identityServer = localStorage.getItem(ID_SERVER_URL_KEY);
if (!homeserver) {
console.warn("Cannot log in with token: can't determine HS URL to use");
return Promise.resolve(false);
}
return sendLoginRequest(
queryParams.homeserver,
queryParams.identityServer,
homeserver,
identityServer,
"m.login.token", {
token: queryParams.loginToken,
initial_device_display_name: defaultDeviceDisplayName,
@ -256,8 +259,8 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
* @returns {Object} Information about the session - see implementation for variables.
*/
export function getLocalStorageSessionVars() {
const hsUrl = localStorage.getItem("mx_hs_url");
const isUrl = localStorage.getItem("mx_is_url");
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY);
const accessToken = localStorage.getItem("mx_access_token");
const userId = localStorage.getItem("mx_user_id");
const deviceId = localStorage.getItem("mx_device_id");
@ -298,6 +301,8 @@ async function _restoreFromLocalStorage(opts) {
return false;
}
const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId);
console.log(`Restoring session for ${userId}`);
await _doSetLoggedIn({
userId: userId,
@ -306,6 +311,7 @@ async function _restoreFromLocalStorage(opts) {
homeserverUrl: hsUrl,
identityServerUrl: isUrl,
guest: isGuest,
pickleKey: pickleKey,
}, false);
return true;
} else {
@ -348,9 +354,13 @@ async function _handleLoadSessionFailure(e) {
*
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
*/
export function setLoggedIn(credentials) {
export async function setLoggedIn(credentials) {
stopMatrixClient();
return _doSetLoggedIn(credentials, true);
const pickleKey = credentials.userId && credentials.deviceId
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
: null;
return _doSetLoggedIn(Object.assign({}, credentials, {pickleKey}), true);
}
/**
@ -479,9 +489,9 @@ function _showStorageEvictedDialog() {
class AbortLoginAndRebuildStorage extends Error { }
function _persistCredentialsToLocalStorage(credentials) {
localStorage.setItem("mx_hs_url", credentials.homeserverUrl);
localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
if (credentials.identityServerUrl) {
localStorage.setItem("mx_is_url", credentials.identityServerUrl);
localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl);
}
localStorage.setItem("mx_user_id", credentials.userId);
localStorage.setItem("mx_access_token", credentials.accessToken);
@ -516,7 +526,9 @@ export function logout() {
}
_isLoggingOut = true;
MatrixClientPeg.get().logout().then(onLoggedOut,
const client = MatrixClientPeg.get();
PlatformPeg.get().destroyPickleKey(client.getUserId(), client.getDeviceId());
client.logout().then(onLoggedOut,
(err) => {
// Just throwing an error here is going to be very unhelpful
// if you're trying to log out because your server's down and
@ -575,10 +587,12 @@ async function startMatrixClient(startSyncing=true) {
// to work).
dis.dispatch({action: 'will_start_client'}, true);
// reset things first just in case
TypingStore.sharedInstance().reset();
ToastStore.sharedInstance().reset();
Notifier.start();
UserActivity.sharedInstance().start();
TypingStore.sharedInstance().reset(); // just in case
ToastStore.sharedInstance().reset();
DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start();
@ -608,7 +622,7 @@ async function startMatrixClient(startSyncing=true) {
}
// Now that we have a MatrixClientPeg, update the Jitsi info
await Jitsi.getInstance().update();
await Jitsi.getInstance().start();
// dispatch that we finished starting up to wire up any other bits
// of the matrix client that cannot be set prior to starting up.

View file

@ -17,8 +17,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {MatrixClient, MemoryStore} from 'matrix-js-sdk';
import {MatrixClient} from 'matrix-js-sdk/src/client';
import {MemoryStore} from 'matrix-js-sdk/src/store/memory';
import * as utils from 'matrix-js-sdk/src/utils';
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
import {EventTimelineSet} from 'matrix-js-sdk/src/models/event-timeline-set';
@ -34,38 +34,27 @@ import IdentityAuthClient from './IdentityAuthClient';
import { crossSigningCallbacks } from './CrossSigningManager';
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
interface MatrixClientCreds {
export interface IMatrixClientCreds {
homeserverUrl: string,
identityServerUrl: string,
userId: string,
deviceId: string,
accessToken: string,
guest: boolean,
pickleKey?: string,
}
/**
* Wrapper object for handling the js-sdk Matrix Client object in the react-sdk
* Handles the creation/initialisation of client objects.
* This module provides a singleton instance of this class so the 'current'
* Matrix Client object is available easily.
*/
class _MatrixClientPeg {
constructor() {
this.matrixClient = null;
this._justRegisteredUserId = null;
// These are the default options used when when the
// client is started in 'start'. These can be altered
// at any time up to after the 'will_start_client'
// event is finished processing.
this.opts = {
initialSyncLimit: 20,
};
// the credentials used to init the current client object.
// used if we tear it down & recreate it with a different store
this._currentClientCreds = null;
// TODO: Move this to the js-sdk
export interface IOpts {
initialSyncLimit?: number;
pendingEventOrdering?: "detached" | "chronological";
lazyLoadMembers?: boolean;
clientWellKnownPollPeriod?: number;
}
export interface IMatrixClientPeg {
opts: IOpts;
/**
* Sets the script href passed to the IndexedDB web worker
* If set, a separate web worker will be started to run the IndexedDB
@ -73,19 +62,23 @@ class _MatrixClientPeg {
*
* @param {string} script href to the script to be passed to the web worker
*/
setIndexedDbWorkerScript(script) {
createMatrixClient.indexedDbWorkerScript = script;
}
setIndexedDbWorkerScript(script: string): void;
get(): MatrixClient {
return this.matrixClient;
}
/**
* Return the server name of the user's homeserver
* Throws an error if unable to deduce the homeserver name
* (eg. if the user is not logged in)
*
* @returns {string} The homeserver name, if present.
*/
getHomeserverName(): string;
unset() {
this.matrixClient = null;
get(): MatrixClient;
unset(): void;
assign(): Promise<any>;
start(): Promise<any>;
MatrixActionCreators.stop();
}
getCredentials(): IMatrixClientCreds;
/**
* If we've registered a user ID we set this to the ID of the
@ -95,9 +88,7 @@ class _MatrixClientPeg {
*
* @param {string} uid The user ID of the user we've just registered
*/
setJustRegisteredUserId(uid) {
this._justRegisteredUserId = uid;
}
setJustRegisteredUserId(uid: string): void;
/**
* Returns true if the current user has just been registered by this
@ -105,23 +96,73 @@ class _MatrixClientPeg {
*
* @returns {bool} True if user has just been registered
*/
currentUserIsJustRegistered() {
currentUserIsJustRegistered(): boolean;
/**
* Replace this MatrixClientPeg's client with a client instance that has
* homeserver / identity server URLs and active credentials
*
* @param {IMatrixClientCreds} creds The new credentials to use.
*/
replaceUsingCreds(creds: IMatrixClientCreds): void;
}
/**
* Wrapper object for handling the js-sdk Matrix Client object in the react-sdk
* Handles the creation/initialisation of client objects.
* This module provides a singleton instance of this class so the 'current'
* Matrix Client object is available easily.
*/
class _MatrixClientPeg implements IMatrixClientPeg {
// These are the default options used when when the
// client is started in 'start'. These can be altered
// at any time up to after the 'will_start_client'
// event is finished processing.
public opts: IOpts = {
initialSyncLimit: 20,
};
private matrixClient: MatrixClient = null;
private justRegisteredUserId: string;
// the credentials used to init the current client object.
// used if we tear it down & recreate it with a different store
private currentClientCreds: IMatrixClientCreds;
constructor() {
}
public setIndexedDbWorkerScript(script: string): void {
createMatrixClient.indexedDbWorkerScript = script;
}
public get(): MatrixClient {
return this.matrixClient;
}
public unset(): void {
this.matrixClient = null;
MatrixActionCreators.stop();
}
public setJustRegisteredUserId(uid: string): void {
this.justRegisteredUserId = uid;
}
public currentUserIsJustRegistered(): boolean {
return (
this.matrixClient &&
this.matrixClient.credentials.userId === this._justRegisteredUserId
this.matrixClient.credentials.userId === this.justRegisteredUserId
);
}
/*
* Replace this MatrixClientPeg's client with a client instance that has
* homeserver / identity server URLs and active credentials
*/
replaceUsingCreds(creds: MatrixClientCreds) {
this._currentClientCreds = creds;
this._createClient(creds);
public replaceUsingCreds(creds: IMatrixClientCreds): void {
this.currentClientCreds = creds;
this.createClient(creds);
}
async assign() {
public async assign(): Promise<any> {
for (const dbType of ['indexeddb', 'memory']) {
try {
const promise = this.matrixClient.store.startup();
@ -132,7 +173,7 @@ class _MatrixClientPeg {
if (dbType === 'indexeddb') {
console.error('Error starting matrixclient store - falling back to memory store', err);
this.matrixClient.store = new MemoryStore({
localStorage: global.localStorage,
localStorage: localStorage,
});
} else {
console.error('Failed to start memory store!', err);
@ -158,9 +199,7 @@ class _MatrixClientPeg {
// The js-sdk found a crypto DB too new for it to use
const CryptoStoreTooNewDialog =
sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog");
Modal.createDialog(CryptoStoreTooNewDialog, {
host: window.location.host,
});
Modal.createDialog(CryptoStoreTooNewDialog);
}
// this can happen for a number of reasons, the most likely being
// that the olm library was missing. It's not fatal.
@ -171,6 +210,7 @@ class _MatrixClientPeg {
// the react sdk doesn't work without this, so don't allow
opts.pendingEventOrdering = "detached";
opts.lazyLoadMembers = true;
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
// Connect the matrix client to the dispatcher and setting handlers
MatrixActionCreators.start(this.matrixClient);
@ -179,7 +219,7 @@ class _MatrixClientPeg {
return opts;
}
async start() {
public async start(): Promise<any> {
const opts = await this.assign();
console.log(`MatrixClientPeg: really starting MatrixClient`);
@ -187,7 +227,7 @@ class _MatrixClientPeg {
console.log(`MatrixClientPeg: MatrixClient started`);
}
getCredentials(): MatrixClientCreds {
public getCredentials(): IMatrixClientCreds {
return {
homeserverUrl: this.matrixClient.baseUrl,
identityServerUrl: this.matrixClient.idBaseUrl,
@ -198,12 +238,7 @@ class _MatrixClientPeg {
};
}
/*
* Return the server name of the user's homeserver
* Throws an error if unable to deduce the homeserver name
* (eg. if the user is not logged in)
*/
getHomeserverName() {
public getHomeserverName(): string {
const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId);
if (matches === null || matches.length < 1) {
throw new Error("Failed to derive homeserver name from user ID!");
@ -211,13 +246,15 @@ class _MatrixClientPeg {
return matches[1];
}
_createClient(creds: MatrixClientCreds) {
private createClient(creds: IMatrixClientCreds): void {
// TODO: Make these opts typesafe with the js-sdk
const opts = {
baseUrl: creds.homeserverUrl,
idBaseUrl: creds.identityServerUrl,
accessToken: creds.accessToken,
userId: creds.userId,
deviceId: creds.deviceId,
pickleKey: creds.pickleKey,
timelineSupport: true,
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'),
@ -228,9 +265,9 @@ class _MatrixClientPeg {
],
unstableClientRelationAggregation: true,
identityServer: new IdentityAuthClient(),
cryptoCallbacks: {},
};
opts.cryptoCallbacks = {};
// These are always installed regardless of the labs flag so that
// cross-signing features can toggle on without reloading and also be
// accessed immediately after login.
@ -253,8 +290,8 @@ class _MatrixClientPeg {
}
}
if (!global.mxMatrixClientPeg) {
global.mxMatrixClientPeg = new _MatrixClientPeg();
if (!window.mxMatrixClientPeg) {
window.mxMatrixClientPeg = new _MatrixClientPeg();
}
export const MatrixClientPeg = global.mxMatrixClientPeg;
export const MatrixClientPeg = window.mxMatrixClientPeg;

View file

@ -26,6 +26,9 @@ import * as sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
import {
hideToast as hideNotificationsToast,
} from "./toasts/DesktopNotificationsToast";
/*
* Dispatches:
@ -278,12 +281,7 @@ const Notifier = {
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
// XXX: why are we dispatching this here?
// this is nothing to do with notifier_enabled
dis.dispatch({
action: "notifier_enabled",
value: this.isEnabled(),
});
hideNotificationsToast();
// update the info to localStorage for persistent settings
if (persistent && global.localStorage) {

View file

@ -84,8 +84,14 @@ export default class PasswordReset {
try {
await this.client.setPassword({
// Note: Though this sounds like a login type for identity servers only, it
// has a dual purpose of being used for homeservers too.
type: "m.login.email.identity",
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/matrix-org/synapse/issues/5665
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds: creds,
threepidCreds: creds,
}, this.password);
} catch (err) {
if (err.httpStatus === 401) {

View file

@ -450,8 +450,8 @@ export const Commands = [
new Command({
command: 'join',
aliases: ['j', 'goto'],
args: '<room-alias>',
description: _td('Joins room with given alias'),
args: '<room-address>',
description: _td('Joins room with given address'),
runFn: function(_, args) {
if (args) {
// Note: we support 2 versions of this command. The first is
@ -562,7 +562,7 @@ export const Commands = [
}),
new Command({
command: 'part',
args: '[<room-alias>]',
args: '[<room-address>]',
description: _td('Leave room'),
runFn: function(roomId, args) {
const cli = MatrixClientPeg.get();
@ -594,7 +594,7 @@ export const Commands = [
}
if (targetRoomId) break;
}
if (!targetRoomId) return reject(_t('Unrecognised room alias:') + ' ' + roomAlias);
if (!targetRoomId) return reject(_t('Unrecognised room address:') + ' ' + roomAlias);
}
}

View file

@ -1,206 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {Key} from "../../../Keyboard";
import * as sdk from "../../../index";
// XXX: This component is not cross-signing aware.
// https://github.com/vector-im/riot-web/issues/11752 tracks either updating this
// component or taking it out to pasture.
export default createReactClass({
displayName: 'EncryptedEventDialog',
propTypes: {
event: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {
return { device: null };
},
componentDidMount: function() {
this._unmounted = false;
const client = MatrixClientPeg.get();
// first try to load the device from our store.
//
this.refreshDevice().then((dev) => {
if (dev) {
return dev;
}
// tell the client to try to refresh the device list for this user
return client.downloadKeys([this.props.event.getSender()], true).then(() => {
return this.refreshDevice();
});
}).then((dev) => {
if (this._unmounted) {
return;
}
this.setState({ device: dev });
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
}, (err)=>{
console.log("Error downloading devices", err);
});
},
componentWillUnmount: function() {
this._unmounted = true;
const client = MatrixClientPeg.get();
if (client) {
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
}
},
refreshDevice: function() {
// Promise.resolve to handle transition from static result to promise; can be removed
// in future
return Promise.resolve(MatrixClientPeg.get().getEventSenderDeviceInfo(this.props.event));
},
onDeviceVerificationChanged: function(userId, device) {
if (userId === this.props.event.getSender()) {
this.refreshDevice().then((dev) => {
this.setState({ device: dev });
});
}
},
onKeyDown: function(e) {
if (e.key === Key.ESCAPE) {
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
}
},
_renderDeviceInfo: function() {
const device = this.state.device;
if (!device) {
return (<i>{ _t('unknown device') }</i>);
}
let verificationStatus = (<b>{ _t('NOT verified') }</b>);
if (device.isBlocked()) {
verificationStatus = (<b>{ _t('Blacklisted') }</b>);
} else if (device.isVerified()) {
verificationStatus = _t('verified');
}
return (
<table>
<tbody>
<tr>
<td>{ _t('Name') }</td>
<td>{ device.getDisplayName() }</td>
</tr>
<tr>
<td>{ _t('Device ID') }</td>
<td><code>{ device.deviceId }</code></td>
</tr>
<tr>
<td>{ _t('Verification') }</td>
<td>{ verificationStatus }</td>
</tr>
<tr>
<td>{ _t('Ed25519 fingerprint') }</td>
<td><code>{ device.getFingerprint() }</code></td>
</tr>
</tbody>
</table>
);
},
_renderEventInfo: function() {
const event = this.props.event;
return (
<table>
<tbody>
<tr>
<td>{ _t('User ID') }</td>
<td>{ event.getSender() }</td>
</tr>
<tr>
<td>{ _t('Curve25519 identity key') }</td>
<td><code>{ event.getSenderKey() || <i>{ _t('none') }</i> }</code></td>
</tr>
<tr>
<td>{ _t('Claimed Ed25519 fingerprint key') }</td>
<td><code>{ event.getKeysClaimed().ed25519 || <i>{ _t('none') }</i> }</code></td>
</tr>
<tr>
<td>{ _t('Algorithm') }</td>
<td>{ event.getWireContent().algorithm || <i>{ _t('unencrypted') }</i> }</td>
</tr>
{
event.getContent().msgtype === 'm.bad.encrypted' ? (
<tr>
<td>{ _t('Decryption error') }</td>
<td>{ event.getContent().body }</td>
</tr>
) : null
}
<tr>
<td>{ _t('Session ID') }</td>
<td><code>{ event.getWireContent().session_id || <i>{ _t('none') }</i> }</code></td>
</tr>
</tbody>
</table>
);
},
render: function() {
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
let buttons = null;
if (this.state.device) {
buttons = (
<DeviceVerifyButtons device={this.state.device}
userId={this.props.event.getSender()}
/>
);
}
return (
<div className="mx_EncryptedEventDialog" onKeyDown={this.onKeyDown}>
<div className="mx_Dialog_title">
{ _t('End-to-end encryption information') }
</div>
<div className="mx_Dialog_content">
<h4>{ _t('Event information') }</h4>
{ this._renderEventInfo() }
<h4>{ _t('Sender session information') }</h4>
{ this._renderDeviceInfo() }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}>
{ _t('OK') }
</button>
{ buttons }
</div>
</div>
);
},
});

View file

@ -22,7 +22,6 @@ import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import PropTypes from 'prop-types';
import {_t, _td} from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager';
import SettingsStore from '../../../../settings/SettingsStore';
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import {copyNode} from "../../../../utils/strings";
import PassphraseField from "../../../../components/views/auth/PassphraseField";
@ -67,10 +66,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
async componentDidMount() {
const cli = MatrixClientPeg.get();
const secureSecretStorage = (
SettingsStore.getValue("feature_cross_signing") &&
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
);
const secureSecretStorage = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
this.setState({ secureSecretStorage });
// If we're using secret storage, skip ahead to the backing up step, as
@ -284,8 +280,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let matchText;
let changeText;
if (this.state.passPhraseConfirm === this.state.passPhrase) {
matchText = _t("That matches!");
changeText = _t("Use a different passphrase?");
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
// only tell them they're wrong if they've actually gone wrong.
// Security concious readers will note that if you left riot-web unattended
@ -295,6 +293,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
// Note that not having typed anything at all will not hit this clause and
// fall through so empty box === no hint.
matchText = _t("That doesn't match.");
changeText = _t("Go back to set it again.");
}
let passPhraseMatch = null;
@ -303,7 +302,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<div>{matchText}</div>
<div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{_t("Go back to set it again.")}
{changeText}
</AccessibleButton>
</div>
</div>;

View file

@ -201,7 +201,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
type: 'm.id.user',
user: MatrixClientPeg.get().getUserId(),
},
// https://github.com/matrix-org/synapse/issues/5665
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/matrix-org/synapse/issues/5665
user: MatrixClientPeg.get().getUserId(),
password: this.state.accountPassword,
});
@ -538,8 +539,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const Field = sdk.getComponent('views.elements.Field');
let matchText;
let changeText;
if (this.state.passPhraseConfirm === this.state.passPhrase) {
matchText = _t("That matches!");
changeText = _t("Use a different passphrase?");
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
// only tell them they're wrong if they've actually gone wrong.
// Security concious readers will note that if you left riot-web unattended
@ -549,6 +552,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
// Note that not having typed anything at all will not hit this clause and
// fall through so empty box === no hint.
matchText = _t("That doesn't match.");
changeText = _t("Go back to set it again.");
}
let passPhraseMatch = null;
@ -557,7 +561,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<div>{matchText}</div>
<div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{_t("Go back to set it again.")}
{changeText}
</AccessibleButton>
</div>
</div>;

View file

@ -90,11 +90,12 @@ export default class CommunityProvider extends AutocompleteProvider {
type: "community",
href: makeGroupPermalink(groupId),
component: (
<PillCompletion initialComponent={
<PillCompletion title={name} description={groupId}>
<BaseAvatar name={name || groupId}
width={24} height={24}
width={24}
height={24}
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} />
} title={name} description={groupId} />
</PillCompletion>
),
range,
}))

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {forwardRef} from 'react';
import classNames from 'classnames';
/* These were earlier stateless functional components but had to be converted
@ -30,50 +30,37 @@ interface ITextualCompletionProps {
className?: string;
}
export class TextualCompletion extends React.PureComponent<ITextualCompletionProps> {
render() {
const {
title,
subtitle,
description,
className,
...restProps
} = this.props;
export const TextualCompletion = forwardRef<ITextualCompletionProps, any>((props, ref) => {
const {title, subtitle, description, className, ...restProps} = props;
return (
<div className={classNames('mx_Autocomplete_Completion_block', className)} role="option" {...restProps}>
<div {...restProps}
className={classNames('mx_Autocomplete_Completion_block', className)}
role="option"
ref={ref}
>
<span className="mx_Autocomplete_Completion_title">{ title }</span>
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
<span className="mx_Autocomplete_Completion_description">{ description }</span>
</div>
);
}
});
interface IPillCompletionProps extends ITextualCompletionProps {
children?: React.ReactNode,
}
interface IPillCompletionProps {
title?: string;
subtitle?: string;
description?: string;
initialComponent?: React.ReactNode,
className?: string;
}
export class PillCompletion extends React.PureComponent<IPillCompletionProps> {
render() {
const {
title,
subtitle,
description,
initialComponent,
className,
...restProps
} = this.props;
export const PillCompletion = forwardRef<IPillCompletionProps, any>((props, ref) => {
const {title, subtitle, description, className, children, ...restProps} = props;
return (
<div className={classNames('mx_Autocomplete_Completion_pill', className)} role="option" {...restProps}>
{ initialComponent }
<div {...restProps}
className={classNames('mx_Autocomplete_Completion_pill', className)}
role="option"
ref={ref}
>
{ children }
<span className="mx_Autocomplete_Completion_title">{ title }</span>
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
<span className="mx_Autocomplete_Completion_description">{ description }</span>
</div>
);
}
}
});

View file

@ -69,7 +69,7 @@ export default class EmojiProvider extends AutocompleteProvider {
constructor() {
super(EMOJI_REGEX);
this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, {
this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTNAMES, {
keys: ['emoji.emoticon', 'shortname'],
funcs: [
(o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases
@ -121,9 +121,9 @@ export default class EmojiProvider extends AutocompleteProvider {
return {
completion: unicode,
component: (
<PillCompletion title={shortname} aria-label={unicode} initialComponent={
<span style={{maxWidth: '1em'}}>{ unicode }</span>
} />
<PillCompletion title={shortname} aria-label={unicode}>
<span>{ unicode }</span>
</PillCompletion>
),
range,
};

View file

@ -48,7 +48,9 @@ export default class NotifProvider extends AutocompleteProvider {
type: "at-room",
suffix: ' ',
component: (
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={this.room} />} title="@room" description={_t("Notify the whole room")} />
<PillCompletion title="@room" description={_t("Notify the whole room")}>
<RoomAvatar width={24} height={24} room={this.room} />
</PillCompletion>
),
range,
}];

View file

@ -45,7 +45,7 @@ interface IOptions<T extends {}> {
* @param {function[]} options.funcs List of functions that when called with the
* object as an arg will return a string to use as an index
*/
export default class QueryMatcher<T> {
export default class QueryMatcher<T extends Object> {
private _options: IOptions<T>;
private _keys: IOptions<T>["keys"];
private _funcs: Required<IOptions<T>["funcs"]>;
@ -75,7 +75,11 @@ export default class QueryMatcher<T> {
this._items = new Map();
for (const object of objects) {
const keyValues = _at(object, this._keys);
// Need to use unsafe coerce here because the objects can have any
// type for their values. We assume that those values who's keys have
// been specified will be string. Also, we cannot infer all the
// types of the keys of the objects at compile.
const keyValues = _at<string>(<any>object, this._keys);
for (const f of this._funcs) {
keyValues.push(f(object));

View file

@ -103,7 +103,9 @@ export default class RoomProvider extends AutocompleteProvider {
suffix: ' ',
href: makeRoomPermalink(room.displayedAlias),
component: (
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.room.name} description={room.displayedAlias} />
<PillCompletion title={room.room.name} description={room.displayedAlias}>
<RoomAvatar width={24} height={24} room={room.room} />
</PillCompletion>
),
range,
};

View file

@ -125,10 +125,9 @@ export default class UserProvider extends AutocompleteProvider {
suffix: (selection.beginning && range.start === 0) ? ': ' : ' ',
href: makeUserPermalink(user.userId),
component: (
<PillCompletion
initialComponent={<MemberAvatar member={user} width={24} height={24} />}
title={displayName}
description={user.userId} />
<PillCompletion title={displayName} description={user.userId}>
<MemberAvatar member={user} width={24} height={24} />
</PillCompletion>
),
range,
};

View file

@ -92,7 +92,7 @@ const CategoryRoomList = createReactClass({
Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
title: _t('Add rooms to the community summary'),
description: _t("Which rooms would you like to add to this summary?"),
placeholder: _t("Room name or alias"),
placeholder: _t("Room name or address"),
button: _t("Add to summary"),
pickerType: 'room',
validAddressTypes: ['mx-room-id'],

View file

@ -26,7 +26,7 @@ import * as VectorConferenceHandler from '../../VectorConferenceHandler';
import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
import Analytics from "../../Analytics";
import RoomList2 from "../views/rooms/RoomList2";
import {Action} from "../../dispatcher/actions";
const LeftPanel = createReactClass({
@ -198,7 +198,7 @@ const LeftPanel = createReactClass({
onSearchCleared: function(source) {
if (source === "keyboard") {
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
}
this.setState({searchExpanded: false});
},
@ -274,19 +274,7 @@ const LeftPanel = createReactClass({
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
}
let roomList = null;
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
roomList = <RoomList2
onKeyDown={this._onKeyDown}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ref={this.collectRoomList}
onFocus={this._onFocus}
onBlur={this._onBlur}
/>;
} else {
roomList = <RoomList
const roomList = <RoomList
onKeyDown={this._onKeyDown}
onFocus={this._onFocus}
onBlur={this._onBlur}
@ -295,7 +283,6 @@ const LeftPanel = createReactClass({
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />;
}
return (
<div className={containerClasses}>

View file

@ -0,0 +1,154 @@
/*
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 TagPanel from "./TagPanel";
import classNames from "classnames";
import dis from "../../dispatcher/dispatcher";
import AccessibleButton from "../views/elements/AccessibleButton";
import { _t } from "../../languageHandler";
import SearchBox from "./SearchBox";
import RoomList2 from "../views/rooms/RoomList2";
import TopLeftMenuButton from "./TopLeftMenuButton";
import { Action } from "../../dispatcher/actions";
/*******************************************************************
* CAUTION *
*******************************************************************
* This is a work in progress implementation and isn't complete or *
* even useful as a component. Please avoid using it until this *
* warning disappears. *
*******************************************************************/
interface IProps {
// TODO: Support collapsed state
}
interface IState {
searchExpanded: boolean;
searchFilter: string; // TODO: Move search into room list?
}
export default class LeftPanel2 extends React.Component<IProps, IState> {
// TODO: Properly support TagPanel
// TODO: Properly support searching/filtering
// TODO: Properly support breadcrumbs
// TODO: Properly support TopLeftMenu (User Settings)
// TODO: a11y
// TODO: actually make this useful in general (match design proposals)
// TODO: Fadable support (is this still needed?)
constructor(props: IProps) {
super(props);
this.state = {
searchExpanded: false,
searchFilter: "",
};
}
private onSearch = (term: string): void => {
this.setState({searchFilter: term});
};
private onSearchCleared = (source: string): void => {
if (source === "keyboard") {
dis.fire(Action.FocusComposer);
}
this.setState({searchExpanded: false});
}
private onSearchFocus = (): void => {
this.setState({searchExpanded: true});
};
private onSearchBlur = (event: FocusEvent): void => {
const target = event.target as HTMLInputElement;
if (target.value.length === 0) {
this.setState({searchExpanded: false});
}
}
public render(): React.ReactNode {
const tagPanel = (
<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel/>
</div>
);
const exploreButton = (
<div
className={classNames("mx_LeftPanel_explore", {"mx_LeftPanel_explore_hidden": this.state.searchExpanded})}>
<AccessibleButton onClick={() => dis.dispatch({action: 'view_room_directory'})}>
{_t("Explore")}
</AccessibleButton>
</div>
);
const searchBox = (<SearchBox
className="mx_LeftPanel_filterRooms"
enableRoomSearchFocus={true}
blurredPlaceholder={_t('Filter')}
placeholder={_t('Filter rooms…')}
onKeyDown={() => {/*TODO*/}}
onSearch={this.onSearch}
onCleared={this.onSearchCleared}
onFocus={this.onSearchFocus}
onBlur={this.onSearchBlur}
collapsed={false}/>); // TODO: Collapsed support
// TODO: Improve props for RoomList2
const roomList = <RoomList2
onKeyDown={() => {/*TODO*/}}
resizeNotifier={null}
collapsed={false}
searchFilter={this.state.searchFilter}
onFocus={() => {/*TODO*/}}
onBlur={() => {/*TODO*/}}
/>;
// TODO: Breadcrumbs
// TODO: Conference handling / calls
const containerClasses = classNames({
"mx_LeftPanel_container": true,
"mx_fadable": true,
"collapsed": false, // TODO: Collapsed support
"mx_LeftPanel_container_hasTagPanel": true, // TODO: TagPanel support
"mx_fadable_faded": false,
"mx_LeftPanel2": true, // TODO: Remove flag when RoomList2 ships (used as an indicator)
});
return (
<div className={containerClasses}>
{tagPanel}
<aside className="mx_LeftPanel dark-panel">
<TopLeftMenuButton collapsed={false}/>
<div
className="mx_LeftPanel_exploreAndFilterRow"
onKeyDown={() => {/*TODO*/}}
onFocus={() => {/*TODO*/}}
onBlur={() => {/*TODO*/}}
>
{exploreButton}
{searchBox}
</div>
{roomList}
</aside>
</div>
);
}
}

View file

@ -29,7 +29,7 @@ import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import sessionStore from '../../stores/SessionStore';
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
import TagOrderActions from '../../actions/TagOrderActions';
@ -43,6 +43,17 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import PlatformPeg from "../../PlatformPeg";
import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
import { DefaultTagID } from "../../stores/room-list/models";
import {
showToast as showSetPasswordToast,
hideToast as hideSetPasswordToast
} from "../../toasts/SetPasswordToast";
import {
showToast as showServerLimitToast,
hideToast as hideServerLimitToast
} from "../../toasts/ServerLimitToast";
import { Action } from "../../dispatcher/actions";
import LeftPanel2 from "./LeftPanel2";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
// NB. this is just for server notices rather than pinned messages in general.
@ -57,7 +68,7 @@ function canElementReceiveInput(el) {
interface IProps {
matrixClient: MatrixClient;
onRegistered: (credentials: MatrixClientCreds) => Promise<MatrixClient>;
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
viaServers?: string[];
hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier;
@ -65,10 +76,6 @@ interface IProps {
initialEventPixelOffset: number;
leftDisabled: boolean;
rightDisabled: boolean;
showCookieBar: boolean;
hasNewVersion: boolean;
userHasGeneratedPassword: boolean;
showNotifierToolbar: boolean;
page_type: string;
autoJoin: boolean;
thirdPartyInvite?: object;
@ -76,7 +83,6 @@ interface IProps {
currentRoomId: string;
ConferenceHandler?: object;
collapseLhs: boolean;
checkingForUpdate: boolean;
config: {
piwik: {
policyUrl: string;
@ -86,19 +92,26 @@ interface IProps {
currentUserId?: string;
currentGroupId?: string;
currentGroupIsNew?: boolean;
version?: string;
newVersion?: string;
newVersionReleaseNotes?: string;
}
interface IUsageLimit {
limit_type: "monthly_active_user" | string;
admin_contact?: string;
}
interface IState {
mouseDown?: {
x: number;
y: number;
};
syncErrorData: any;
syncErrorData?: {
error: {
data: IUsageLimit;
errcode: string;
};
};
usageLimitEventContent?: IUsageLimit;
useCompactLayout: boolean;
serverNoticeEvents: MatrixEvent[];
userHasGeneratedPassword: boolean;
}
/**
@ -141,11 +154,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
this.state = {
mouseDown: undefined,
syncErrorData: undefined,
userHasGeneratedPassword: false,
// use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
// any currently active server notice events
serverNoticeEvents: [],
};
// stash the MatrixClient in case we log out before we are unmounted
@ -179,18 +189,6 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
this._loadResizerPreferences();
}
componentDidUpdate(prevProps, prevState) {
// attempt to guess when a banner was opened or closed
if (
(prevProps.showCookieBar !== this.props.showCookieBar) ||
(prevProps.hasNewVersion !== this.props.hasNewVersion) ||
(prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) ||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
) {
this.props.resizeNotifier.notifyBannersChanged();
}
}
componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false);
this._matrixClient.removeListener("accountData", this.onAccountData);
@ -220,9 +218,11 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
};
_setStateFromSessionStore = () => {
this.setState({
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
});
if (this._sessionStore.getCachedPassword()) {
showSetPasswordToast();
} else {
hideSetPasswordToast();
}
};
_createResizer() {
@ -294,6 +294,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents();
} else {
this._calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
}
};
@ -304,11 +306,24 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
}
};
_calculateServerLimitToast(syncErrorData: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
if (error) {
usageLimitEventContent = syncErrorData.error.data;
}
if (usageLimitEventContent) {
showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error);
} else {
hideServerLimitToast();
}
}
_updateServerNoticeEvents = async () => {
const roomLists = RoomListStoreTempProxy.getRoomLists();
if (!roomLists[DefaultTagID.ServerNotice]) return [];
const pinnedEvents = [];
const events = [];
for (const room of roomLists[DefaultTagID.ServerNotice]) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
@ -318,12 +333,19 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
if (event) pinnedEvents.push(event);
if (event) events.push(event);
}
}
this.setState({
serverNoticeEvents: pinnedEvents,
const usageLimitEvent = events.find((e) => {
return (
e && e.getType() === 'm.room.message' &&
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
);
});
const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
this.setState({ usageLimitEventContent });
};
_onPaste = (ev) => {
@ -338,7 +360,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
// refocusing during a paste event will make the
// paste end up in the newly focused element,
// so dispatch synchronously before paste happens
dis.dispatch({action: 'focus_composer'}, true);
dis.fire(Action.FocusComposer, true);
}
};
@ -488,7 +510,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
// synchronous dispatch so we focus before key generates input
dis.dispatch({action: 'focus_composer'}, true);
dis.fire(Action.FocusComposer, true);
ev.stopPropagation();
// we should *not* preventDefault() here as
// that would prevent typing in the now-focussed composer
@ -599,12 +621,6 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
const GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups');
const ToastContainer = sdk.getComponent('structures.ToastContainer');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
const CookieBar = sdk.getComponent('globals.CookieBar');
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar');
let pageElement;
@ -648,50 +664,25 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
break;
}
const usageLimitEvent = this.state.serverNoticeEvents.find((e) => {
return (
e && e.getType() === 'm.room.message' &&
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
);
});
let topBar;
if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
topBar = <ServerLimitBar kind='hard'
adminContact={this.state.syncErrorData.error.data.admin_contact}
limitType={this.state.syncErrorData.error.data.limit_type}
/>;
} else if (usageLimitEvent) {
topBar = <ServerLimitBar kind='soft'
adminContact={usageLimitEvent.getContent().admin_contact}
limitType={usageLimitEvent.getContent().limit_type}
/>;
} else if (this.props.showCookieBar &&
this.props.config.piwik &&
navigator.doNotTrack !== "1"
) {
const policyUrl = this.props.config.piwik.policyUrl || null;
topBar = <CookieBar policyUrl={policyUrl} />;
} else if (this.props.hasNewVersion) {
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
releaseNotes={this.props.newVersionReleaseNotes}
/>;
} else if (this.props.checkingForUpdate) {
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
} else if (this.state.userHasGeneratedPassword) {
topBar = <PasswordNagBar />;
} else if (this.props.showNotifierToolbar) {
topBar = <MatrixToolbar />;
}
let bodyClasses = 'mx_MatrixChat';
if (topBar) {
bodyClasses += ' mx_MatrixChat_toolbarShowing';
}
if (this.state.useCompactLayout) {
bodyClasses += ' mx_MatrixChat_useCompactLayout';
}
let leftPanel = (
<LeftPanel
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
);
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
// TODO: Supply props like collapsed and disabled to LeftPanel2
leftPanel = (
<LeftPanel2 />
);
}
return (
<MatrixClientContext.Provider value={this._matrixClient}>
<div
@ -702,15 +693,10 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
onMouseDown={this._onMouseDown}
onMouseUp={this._onMouseUp}
>
{ topBar }
<ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._resizeContainer} className={bodyClasses}>
<LeftPanel
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
{ leftPanel }
<ResizeHandle />
{ pageElement }
</div>

View file

@ -49,7 +49,6 @@ import PageTypes from '../../PageTypes';
import { getHomePageUrl } from '../../utils/pages';
import createRoom from "../../createRoom";
import KeyRequestHandler from '../../KeyRequestHandler';
import { _t, getCurrentLanguage } from '../../languageHandler';
import SettingsStore, { SettingLevel } from "../../settings/SettingsStore";
import ThemeController from "../../settings/controllers/ThemeController";
@ -59,8 +58,8 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs';
import { ThemeWatcher } from "../../theme";
import { FontWatcher } from '../../FontWatcher';
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import { FontWatcher } from '../../settings/watchers/FontWatcher';
import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer, IDeferred } from "../../utils/promise";
import ToastStore from "../../stores/ToastStore";
@ -68,6 +67,11 @@ import * as StorageManager from "../../utils/StorageManager";
import type LoggedInViewType from "./LoggedInView";
import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload";
import { Action } from "../../dispatcher/actions";
import {
showToast as showAnalyticsToast,
hideToast as hideAnalyticsToast
} from "../../toasts/AnalyticsToast";
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
/** constants for MatrixChat.state.view */
export enum Views {
@ -169,12 +173,6 @@ interface IState {
leftDisabled: boolean;
middleDisabled: boolean;
// the right panel's disabled state is tracked in its store.
version?: string;
newVersion?: string;
hasNewVersion: boolean;
newVersionReleaseNotes?: string;
checkingForUpdate?: string; // updateCheckStatusEnum
showCookieBar: boolean;
// Parameters used in the registration dance with the IS
register_client_secret?: string;
register_session_id?: string;
@ -184,7 +182,6 @@ interface IState {
hideToSRUsers: boolean;
syncError?: Error;
resizeNotifier: ResizeNotifier;
showNotifierToolbar: boolean;
serverConfig?: ValidatedServerConfig;
ready: boolean;
thirdPartyInvite?: object;
@ -228,17 +225,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
leftDisabled: false,
middleDisabled: false,
hasNewVersion: false,
newVersionReleaseNotes: null,
checkingForUpdate: null,
showCookieBar: false,
hideToSRUsers: false,
syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
resizeNotifier: new ResizeNotifier(),
showNotifierToolbar: false,
ready: false,
};
@ -339,12 +329,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
if (SettingsStore.getValue("showCookieBar")) {
this.setState({
showCookieBar: true,
});
}
if (SettingsStore.getValue("analyticsOptIn")) {
Analytics.enable();
}
@ -363,7 +347,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Analytics.trackPageChange(durationMs);
}
if (this.focusComposer) {
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
this.focusComposer = false;
}
}
@ -686,9 +670,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
dis.dispatch({action: 'view_my_groups'});
}
break;
case 'notifier_enabled':
this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()});
break;
case 'hide_left_panel':
this.setState({
collapseLhs: true,
@ -736,15 +717,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'client_started':
this.onClientStarted();
break;
case 'new_version':
this.onVersion(
payload.currentVersion, payload.newVersion,
payload.releaseNotes,
);
break;
case 'check_updates':
this.setState({ checkingForUpdate: payload.value });
break;
case 'send_event':
this.onSendEvent(payload.room_id, payload.event);
break;
@ -761,19 +733,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'accept_cookies':
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true);
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
this.setState({
showCookieBar: false,
});
hideAnalyticsToast();
Analytics.enable();
break;
case 'reject_cookies':
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false);
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
this.setState({
showCookieBar: false,
});
hideAnalyticsToast();
break;
}
};
@ -932,9 +898,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
private viewGroup(payload) {
private async viewGroup(payload) {
const groupId = payload.group_id;
// Wait for the first sync to complete
if (!this.firstSyncComplete) {
if (!this.firstSyncPromise) {
console.warn('Cannot view a group before first sync. group_id:', groupId);
return;
}
await this.firstSyncPromise.promise;
}
this.setState({
view: Views.LOGGED_IN,
currentGroupId: groupId,
currentGroupIsNew: payload.group_is_new,
});
@ -1251,6 +1228,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
StorageManager.tryPersistStorage();
if (SettingsStore.getValue("showCookieBar") && this.props.config.piwik && navigator.doNotTrack !== "1") {
showAnalyticsToast(this.props.config.piwik && this.props.config.piwik.policyUrl);
}
}
private showScreenAfterLogin() {
@ -1378,10 +1359,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.firstSyncComplete = true;
this.firstSyncPromise.resolve();
dis.dispatch({action: 'focus_composer'});
if (Notifier.shouldShowToolbar()) {
showNotificationsToast();
}
dis.fire(Action.FocusComposer);
this.setState({
ready: true,
showNotifierToolbar: Notifier.shouldShowToolbar(),
});
});
cli.on('Call.incoming', function(call) {
@ -1460,16 +1444,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
cli.on("Session.logged_out", () => dft.stop());
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err));
// TODO: We can remove this once cross-signing is the only way.
// https://github.com/vector-im/riot-web/issues/11908
const krh = new KeyRequestHandler(cli);
cli.on("crypto.roomKeyRequest", (req) => {
krh.handleKeyRequest(req);
});
cli.on("crypto.roomKeyRequestCancellation", (req) => {
krh.handleKeyRequestCancellation(req);
});
cli.on("Room", (room) => {
if (MatrixClientPeg.get().isCryptoEnabled()) {
const blacklistEnabled = SettingsStore.getValueAt(
@ -1540,13 +1514,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
cli.on("crypto.verification.request", request => {
const isFlagOn = SettingsStore.getValue("feature_cross_signing");
if (!isFlagOn && !request.channel.deviceId) {
request.cancel({code: "m.invalid_message", reason: "This client has cross-signing disabled"});
return;
}
if (request.verifier) {
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
@ -1559,7 +1526,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
icon: "verification",
props: {request},
component: sdk.getComponent("toasts.VerificationRequestToast"),
priority: ToastStore.PRIORITY_REALTIME,
priority: 90,
});
}
});
@ -1589,9 +1556,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// be aware of will be signalled through the room shield
// changing colour. More advanced behaviour will come once
// we implement more settings.
cli.setGlobalErrorOnUnknownDevices(
!SettingsStore.getValue("feature_cross_signing"),
);
cli.setGlobalErrorOnUnknownDevices(false);
}
}
@ -1833,16 +1798,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.showScreen("settings");
};
onVersion(current: string, latest: string, releaseNotes?: string) {
this.setState({
version: current,
newVersion: latest,
hasNewVersion: current !== latest,
newVersionReleaseNotes: releaseNotes,
checkingForUpdate: null,
});
}
onSendEvent(roomId: string, event: MatrixEvent) {
const cli = MatrixClientPeg.get();
if (!cli) {
@ -1949,17 +1904,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// whether cross-signing has been set up on the account.
const masterKeyInStorage = !!cli.getAccountData("m.cross_signing.master");
if (masterKeyInStorage) {
// Auto-enable cross-signing for the new session when key found in
// secret storage.
SettingsStore.setValue("feature_cross_signing", null, SettingLevel.DEVICE, true);
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
} else if (
SettingsStore.getValue("feature_cross_signing") &&
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
) {
// This will only work if the feature is set to 'enable' in the config,
// since it's too early in the lifecycle for users to have turned the
// labs flag on.
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
this.setStateForNewView({ view: Views.E2E_SETUP });
} else {
this.onLoggedIn();
@ -1978,7 +1924,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
let fragmentAfterLogin = "";
if (this.props.initialScreenAfterLogin) {
if (this.props.initialScreenAfterLogin &&
// XXX: workaround for https://github.com/vector-im/riot-web/issues/11643 causing a login-loop
!["welcome", "login", "register"].includes(this.props.initialScreenAfterLogin.screen)
) {
fragmentAfterLogin = `/${this.props.initialScreenAfterLogin.screen}`;
}
@ -2037,7 +1986,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onCloseAllSettings={this.onCloseAllSettings}
onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId}
showCookieBar={this.state.showCookieBar}
/>
);
} else {

View file

@ -34,6 +34,30 @@ import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResiz
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message'];
// check if there is a previous event and it has the same sender as this event
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
function shouldFormContinuation(prevEvent, mxEvent) {
// sanity check inputs
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
// check if within the max continuation period
if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false;
// Some events should appear as continuations from previous events of different types.
if (mxEvent.getType() !== prevEvent.getType() &&
(!continuedTypes.includes(mxEvent.getType()) ||
!continuedTypes.includes(prevEvent.getType()))) return false;
// Check if the sender is the same and hasn't changed their displayname/avatar between these events
if (mxEvent.sender.userId !== prevEvent.sender.userId ||
mxEvent.sender.name !== prevEvent.sender.name ||
mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false;
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
if (!haveTileForEvent(prevEvent)) return false;
return true;
}
const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite';
/* (almost) stateless UI component which builds the event tiles in the room timeline.
@ -108,6 +132,9 @@ export default class MessagePanel extends React.Component {
// whether to show reactions for an event
showReactions: PropTypes.bool,
// whether to use the irc layout
useIRCLayout: PropTypes.bool,
};
// Force props to be loaded for useIRCLayout
@ -119,7 +146,6 @@ export default class MessagePanel extends React.Component {
// display 'ghost' read markers that are animating away
ghostReadMarkers: [],
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")),
};
// opaque readreceipt info for each userId; used by ReadReceiptMarker
@ -172,8 +198,6 @@ export default class MessagePanel extends React.Component {
this._showTypingNotificationsWatcherRef =
SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange);
this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange);
}
componentDidMount() {
@ -183,7 +207,6 @@ export default class MessagePanel extends React.Component {
componentWillUnmount() {
this._isMounted = false;
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
SettingsStore.unwatchSetting(this._layoutWatcherRef);
}
componentDidUpdate(prevProps, prevState) {
@ -202,17 +225,6 @@ export default class MessagePanel extends React.Component {
});
};
onLayoutChange = () => {
this.setState({
useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")),
});
}
useIRCLayout(ircLayoutSelected) {
// if room is null we are not in a normal room list
return ircLayoutSelected && this.props.room;
}
/* get the DOM node representing the given event */
getNodeForEventId(eventId) {
if (!this.eventNodes) {
@ -527,39 +539,6 @@ export default class MessagePanel extends React.Component {
const isEditing = this.props.editState &&
this.props.editState.getEvent().getId() === mxEv.getId();
// is this a continuation of the previous message?
let continuation = false;
// Some events should appear as continuations from previous events of
// different types.
const eventTypeContinues =
prevEvent !== null &&
continuedTypes.includes(mxEv.getType()) &&
continuedTypes.includes(prevEvent.getType());
// if there is a previous event and it has the same sender as this event
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId &&
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
haveTileForEvent(prevEvent) && (mxEv.getType() === prevEvent.getType() || eventTypeContinues) &&
(mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) {
continuation = true;
}
/*
// Work out if this is still a continuation, as we are now showing commands
// and /me messages with their own little avatar. The case of a change of
// event type (commands) is handled above, but we need to handle the /me
// messages seperately as they have a msgtype of 'm.emote' but are classed
// as normal messages
if (prevEvent !== null && prevEvent.sender && mxEv.sender
&& mxEv.sender.userId === prevEvent.sender.userId
&& mxEv.getType() == prevEvent.getType()
&& prevEvent.getContent().msgtype === 'm.emote') {
continuation = false;
}
*/
// local echoes have a fake date, which could even be yesterday. Treat them
// as 'today' for the date separators.
@ -571,12 +550,15 @@ export default class MessagePanel extends React.Component {
}
// do we need a date separator since the last event?
if (this._wantsDateSeparator(prevEvent, eventDate)) {
const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate);
if (wantsDateSeparator) {
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
ret.push(dateSeparator);
continuation = false;
}
// is this a continuation of the previous message?
const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv);
const eventId = mxEv.getId();
const highlight = (eventId === this.props.highlightedEventId);
@ -614,7 +596,7 @@ export default class MessagePanel extends React.Component {
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
useIRCLayout={this.state.useIRCLayout}
useIRCLayout={this.props.useIRCLayout}
/>
</TileErrorBoundary>
</li>,
@ -797,8 +779,6 @@ export default class MessagePanel extends React.Component {
this.props.className,
{
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
"mx_IRCLayout": this.state.useIRCLayout,
"mx_GroupLayout": !this.state.useIRCLayout,
},
);
@ -813,11 +793,11 @@ export default class MessagePanel extends React.Component {
}
let ircResizer = null;
if (this.state.useIRCLayout) {
if (this.props.useIRCLayout) {
ircResizer = <IRCTimelineProfileResizer
minWidth={20}
maxWidth={600}
roomId={this.props.room ? this.props.roomroomId : null}
roomId={this.props.room ? this.props.room.roomId : null}
/>;
}

View file

@ -26,7 +26,6 @@ import dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore';
import SettingsStore from "../../settings/SettingsStore";
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
@ -189,16 +188,37 @@ export default class RightPanel extends React.Component {
}
}
onCloseUserInfo = () => {
// XXX: There are three different ways of 'closing' this panel depending on what state
// things are in... this knows far more than it should do about the state of the rest
// of the app and is generally a bit silly.
if (this.props.user) {
// If we have a user prop then we're displaying a user from the 'user' page type
// in LoggedInView, so need to change the page type to close the panel (we switch
// to the home page which is not obviously the correct thing to do, but I'm not sure
// anything else is - we could hide the close button altogether?)
dis.dispatch({
action: "view_home_page",
});
} else {
// Otherwise we have got our user from RoomViewStore which means we're being shown
// within a room/group, so go back to the member panel if we were in the encryption panel,
// or the member list if we were in the member panel... phew.
dis.dispatch({
action: Action.ViewUser,
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null,
});
}
};
render() {
const MemberList = sdk.getComponent('rooms.MemberList');
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
const UserInfo = sdk.getComponent('right_panel.UserInfo');
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
const FilePanel = sdk.getComponent('structures.FilePanel');
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo');
const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
@ -220,71 +240,25 @@ export default class RightPanel extends React.Component {
break;
case RIGHT_PANEL_PHASES.RoomMemberInfo:
case RIGHT_PANEL_PHASES.EncryptionPanel:
if (SettingsStore.getValue("feature_cross_signing")) {
const onClose = () => {
// XXX: There are three different ways of 'closing' this panel depending on what state
// things are in... this knows far more than it should do about the state of the rest
// of the app and is generally a bit silly.
if (this.props.user) {
// If we have a user prop then we're displaying a user from the 'user' page type
// in LoggedInView, so need to change the page type to close the panel (we switch
// to the home page which is not obviously the correct thing to do, but I'm not sure
// anything else is - we could hide the close button altogether?)
dis.dispatch({
action: "view_home_page",
});
} else {
// Otherwise we have got our user from RoomViewStore which means we're being shown
// within a room, so go back to the member panel if we were in the encryption panel,
// or the member list if we were in the member panel... phew.
dis.dispatch({
action: Action.ViewUser,
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ?
this.state.member : null,
});
}
};
panel = <UserInfo
user={this.state.member}
roomId={this.props.roomId}
key={this.props.roomId || this.state.member.userId}
onClose={onClose}
onClose={this.onCloseUserInfo}
phase={this.state.phase}
verificationRequest={this.state.verificationRequest}
verificationRequestPromise={this.state.verificationRequestPromise}
/>;
} else {
panel = <MemberInfo
member={this.state.member}
key={this.props.roomId || this.state.member.userId}
/>;
}
break;
case RIGHT_PANEL_PHASES.Room3pidMemberInfo:
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
break;
case RIGHT_PANEL_PHASES.GroupMemberInfo:
if (SettingsStore.getValue("feature_cross_signing")) {
const onClose = () => {
dis.dispatch({
action: Action.ViewUser,
member: null,
});
};
panel = <UserInfo
user={this.state.member}
groupId={this.props.groupId}
key={this.state.member.userId}
onClose={onClose} />;
} else {
panel = (
<GroupMemberInfo
groupMember={this.state.member}
groupId={this.props.groupId}
key={this.state.member.user_id}
/>
);
}
onClose={this.onCloseUserInfo} />;
break;
case RIGHT_PANEL_PHASES.GroupRoomInfo:
panel = <GroupRoomInfo

View file

@ -199,7 +199,7 @@ export default createReactClass({
let desc;
if (alias) {
desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
} else {
desc = _t('Remove %(name)s from the directory?', {name: name});
}
@ -216,7 +216,7 @@ export default createReactClass({
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
if (!alias) return;
step = _t('delete the alias.');
step = _t('delete the address.');
return MatrixClientPeg.get().deleteAlias(alias);
}).then(() => {
modal.close();

View file

@ -24,9 +24,9 @@ import { _t, _td } from '../../languageHandler';
import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import Resend from '../../Resend';
import * as cryptodevices from '../../cryptodevices';
import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
import {Action} from "../../dispatcher/actions";
const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1;
@ -126,25 +126,14 @@ export default createReactClass({
});
},
_onSendWithoutVerifyingClick: function() {
cryptodevices.getUnknownDevicesForRoom(MatrixClientPeg.get(), this.props.room).then((devices) => {
cryptodevices.markAllDevicesKnown(MatrixClientPeg.get(), devices);
Resend.resendUnsentEvents(this.props.room);
});
},
_onResendAllClick: function() {
Resend.resendUnsentEvents(this.props.room);
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
_onCancelAllClick: function() {
Resend.cancelUnsentEvents(this.props.room);
dis.dispatch({action: 'focus_composer'});
},
_onShowDevicesClick: function() {
cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
dis.fire(Action.FocusComposer);
},
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {
@ -213,24 +202,7 @@ export default createReactClass({
if (!unsentMessages.length) return null;
let title;
let content;
const hasUDE = unsentMessages.some((m) => {
return m.error && m.error.name === "UnknownDeviceError";
});
if (hasUDE) {
title = _t("Message not sent due to unknown sessions being present");
content = _t(
"<showSessionsText>Show sessions</showSessionsText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.",
{},
{
'showSessionsText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
'sendAnywayText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="sendAnyway" onClick={this._onSendWithoutVerifyingClick}>{ sub }</a>,
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
},
);
} else {
let consentError = null;
let resourceLimitError = null;
for (const m of unsentMessages) {
@ -277,8 +249,9 @@ export default createReactClass({
} else {
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
}
content = _t("%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
"You can also select individual messages to resend or cancel.",
const content = _t("%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> " +
"now. You can also select individual messages to resend or cancel.",
{ count: unsentMessages.length },
{
'resendText': (sub) =>
@ -287,7 +260,6 @@ export default createReactClass({
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
},
);
}
return <div className="mx_RoomStatusBar_connectionLostBar">
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title={_t("Warning")} alt="" />

View file

@ -55,6 +55,7 @@ import {haveTileForEvent} from "../views/rooms/EventTile";
import RoomContext from "../../contexts/RoomContext";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { shieldStatusForRoom } from '../../utils/ShieldUtils';
import {Action} from "../../dispatcher/actions";
const DEBUG = false;
let debuglog = function() {};
@ -164,6 +165,10 @@ export default createReactClass({
canReact: false,
canReply: false,
useIRCLayout: SettingsStore.getValue("feature_irc_ui"),
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
};
},
@ -193,6 +198,8 @@ export default createReactClass({
this._roomView = createRef();
this._searchResultsPanel = createRef();
this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange);
},
_onReadReceiptsChange: function() {
@ -232,7 +239,8 @@ export default createReactClass({
initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
shouldPeek: RoomViewStore.shouldPeek(),
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
};
@ -532,6 +540,14 @@ export default createReactClass({
// no need to do this as Dir & Settings are now overlays. It just burnt CPU.
// console.log("Tinter.tint from RoomView.unmount");
// Tinter.tint(); // reset colourscheme
SettingsStore.unwatchSetting(this._layoutWatcherRef);
},
onLayoutChange: function() {
this.setState({
useIRCLayout: SettingsStore.getValue("feature_irc_ui"),
});
},
_onRightPanelStoreUpdate: function() {
@ -681,6 +697,16 @@ export default createReactClass({
});
}
break;
case 'sync_state':
if (!this.state.matrixClientIsReady) {
this.setState({
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
}, () => {
// send another "initial" RVS update to trigger peeking if needed
this._onRoomViewStoreUpdate(true);
});
}
break;
}
},
@ -854,15 +880,6 @@ export default createReactClass({
});
return;
}
if (!SettingsStore.getValue("feature_cross_signing")) {
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
this.setState({
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
});
});
debuglog("e2e check is warning/verified only as cross-signing is off");
return;
}
/* At this point, the user has encryption on and cross-signing on */
this.setState({
@ -1146,7 +1163,7 @@ export default createReactClass({
ev.dataTransfer.files, this.state.room.roomId, this.context,
);
this.setState({ draggingFile: false });
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
onDragLeaveOrEnd: function(ev) {
@ -1352,7 +1369,7 @@ export default createReactClass({
event: null,
});
}
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
onLeaveClick: function() {
@ -1463,7 +1480,7 @@ export default createReactClass({
// jump down to the bottom of this room, where new events are arriving
jumpToLiveTimeline: function() {
this._messagePanel.jumpToLiveTimeline();
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
// jump up to wherever our read marker is
@ -1663,14 +1680,16 @@ export default createReactClass({
const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
if (!this.state.room) {
const loading = this.state.roomLoading || this.state.peekLoading;
const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading;
if (loading) {
// Assume preview loading if we don't have a ready client or a room ID (still resolving the alias)
const previewLoading = !this.state.matrixClientIsReady || !this.state.roomId || this.state.peekLoading;
return (
<div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar
canPreview={false}
previewLoading={this.state.peekLoading}
previewLoading={previewLoading && !this.state.roomLoadError}
error={this.state.roomLoadError}
loading={loading}
joining={this.state.joining}
@ -1695,7 +1714,8 @@ export default createReactClass({
return (
<div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
<RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked}
canPreview={false} error={this.state.roomLoadError}
@ -1980,6 +2000,13 @@ export default createReactClass({
highlightedEventId = this.state.initialEventId;
}
const messagePanelClassNames = classNames(
"mx_RoomView_messagePanel",
{
"mx_IRCLayout": this.state.useIRCLayout,
"mx_GroupLayout": !this.state.useIRCLayout,
});
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
const messagePanel = (
<TimelinePanel
@ -1995,11 +2022,12 @@ export default createReactClass({
onScroll={this.onMessageListScroll}
onReadMarkerUpdated={this._updateTopUnreadMessagesBar}
showUrlPreview = {this.state.showUrlPreview}
className="mx_RoomView_messagePanel"
className={messagePanelClassNames}
membersLoaded={this.state.membersLoaded}
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
resizeNotifier={this.props.resizeNotifier}
showReactions={true}
useIRCLayout={this.state.useIRCLayout}
/>);
let topUnreadMessagesBar = null;

View file

@ -1,59 +0,0 @@
/*
Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import Modal from '../../Modal';
import { _t } from '../../languageHandler';
const TagPanelButtons = createReactClass({
displayName: 'TagPanelButtons',
componentDidMount: function() {
this._dispatcherRef = dis.register(this._onAction);
},
componentWillUnmount() {
if (this._dispatcherRef) {
dis.unregister(this._dispatcherRef);
this._dispatcherRef = null;
}
},
_onAction(payload) {
if (payload.action === "show_redesign_feedback_dialog") {
const RedesignFeedbackDialog =
sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
}
},
render() {
const GroupsButton = sdk.getComponent('elements.GroupsButton');
const ActionButton = sdk.getComponent("elements.ActionButton");
return (<div className="mx_TagPanelButtons">
<GroupsButton />
<ActionButton
className="mx_TagPanelButtons_report" action="show_redesign_feedback_dialog"
label={_t("Report bugs & give feedback")} tooltip={true} />
</div>);
},
});
export default TagPanelButtons;

View file

@ -112,6 +112,9 @@ const TimelinePanel = createReactClass({
// whether to show reactions for an event
showReactions: PropTypes.bool,
// whether to use the irc layout
useIRCLayout: PropTypes.bool,
},
statics: {
@ -1447,6 +1450,7 @@ const TimelinePanel = createReactClass({
getRelationsForEvent={this.getRelationsForEvent}
editState={this.state.editState}
showReactions={this.props.showReactions}
useIRCLayout={this.props.useIRCLayout}
/>
);
},

View file

@ -15,14 +15,21 @@ limitations under the License.
*/
import * as React from "react";
import { _t } from '../../languageHandler';
import ToastStore from "../../stores/ToastStore";
import ToastStore, {IToast} from "../../stores/ToastStore";
import classNames from "classnames";
export default class ToastContainer extends React.Component {
constructor() {
super();
this.state = {toasts: ToastStore.sharedInstance().getToasts()};
interface IState {
toasts: IToast<any>[];
countSeen: number;
}
export default class ToastContainer extends React.Component<{}, IState> {
constructor(props, context) {
super(props, context);
this.state = {
toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(),
};
// Start listening here rather than in componentDidMount because
// toasts may dismiss themselves in their didMount if they find
@ -36,7 +43,10 @@ export default class ToastContainer extends React.Component {
}
_onToastStoreUpdate = () => {
this.setState({toasts: ToastStore.sharedInstance().getToasts()});
this.setState({
toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(),
});
};
render() {
@ -50,14 +60,21 @@ export default class ToastContainer extends React.Component {
"mx_Toast_hasIcon": icon,
[`mx_Toast_icon_${icon}`]: icon,
});
const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
let countIndicator;
if (isStacked || this.state.countSeen > 0) {
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
}
const toastProps = Object.assign({}, props, {
key,
toastKey: key,
});
toast = (<div className={toastClasses}>
<h2>{title}{countIndicator}</h2>
<div className="mx_Toast_title">
<h2>{title}</h2>
<span>{countIndicator}</span>
</div>
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
</div>);
}

View file

@ -247,9 +247,8 @@ export default createReactClass({
// do SSO instead. If we've already started the UI Auth process though, we don't
// need to.
if (!this.state.doingUIAuth) {
await this._makeRegisterRequest({});
// This should never succeed since we specified an empty
// auth object.
await this._makeRegisterRequest(null);
// This should never succeed since we specified no auth object.
console.log("Expecting 401 from register request but got success!");
}
} catch (e) {

View file

@ -25,6 +25,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {sendLoginRequest} from "../../../Login";
import AuthPage from "../../views/auth/AuthPage";
import SSOButton from "../../views/elements/SSOButton";
import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "../../../BasePlatform";
const LOGIN_VIEW = {
LOADING: 1,
@ -43,7 +44,7 @@ const FLOWS_TO_VIEWS = {
export default class SoftLogout extends React.Component {
static propTypes = {
// Query parameters from MatrixChat
realQueryParams: PropTypes.object, // {homeserver, identityServer, loginToken}
realQueryParams: PropTypes.object, // {loginToken}
// Called when the SSO login completes
onTokenLoginCompleted: PropTypes.func,
@ -90,7 +91,7 @@ export default class SoftLogout extends React.Component {
async _initLogin() {
const queryParams = this.props.realQueryParams;
const hasAllParams = queryParams && queryParams['homeserver'] && queryParams['loginToken'];
const hasAllParams = queryParams && queryParams['loginToken'];
if (hasAllParams) {
this.setState({loginView: LOGIN_VIEW.LOADING});
this.trySsoLogin();
@ -157,8 +158,8 @@ export default class SoftLogout extends React.Component {
async trySsoLogin() {
this.setState({busy: true});
const hsUrl = this.props.realQueryParams['homeserver'];
const isUrl = this.props.realQueryParams['identityServer'] || MatrixClientPeg.get().getIdentityServerUrl();
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY) || MatrixClientPeg.get().getIdentityServerUrl();
const loginType = "m.login.token";
const loginParams = {
token: this.props.realQueryParams['loginToken'],

View file

@ -355,6 +355,7 @@ export const TermsAuthEntry = createReactClass({
allChecked = allChecked && checked;
checkboxes.push(
// XXX: replace with StyledCheckbox
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
@ -538,6 +539,7 @@ export const MsisdnAuthEntry = createReactClass({
type: MsisdnAuthEntry.LOGIN_TYPE,
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/vector-im/riot-web/issues/10312
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds: creds,
threepidCreds: creds,
});

View file

@ -23,7 +23,8 @@ import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import * as ServerType from '../../views/auth/ServerTypeSelector';
import ServerConfig from "./ServerConfig";
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
const MODULAR_URL = 'https://modular.im/services/matrix-hosting-riot' +
'?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
// TODO: TravisR - Can this extend ServerConfig for most things?

View file

@ -36,7 +36,7 @@ interface IProps {
labelStrongPassword?: string;
labelAllowedButUnsafe?: string;
onChange(ev: KeyboardEvent);
onChange(ev: React.FormEvent<HTMLElement>);
onValidate(result: IValidationResult);
}

View file

@ -238,7 +238,7 @@ export default class PasswordLogin extends React.Component {
type="text"
label={_t("Phone")}
value={this.state.phoneNumber}
prefix={phoneCountry}
prefixComponent={phoneCountry}
onChange={this.onPhoneNumberChanged}
onBlur={this.onPhoneNumberBlur}
disabled={this.props.disableSubmit}

View file

@ -473,7 +473,7 @@ export default createReactClass({
type="text"
label={phoneLabel}
value={this.state.phoneNumber}
prefix={phoneCountry}
prefixComponent={phoneCountry}
onChange={this.onPhoneNumberChange}
onValidate={this.onPhoneNumberValidate}
/>;

View file

@ -22,7 +22,8 @@ import classnames from 'classnames';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import {makeType} from "../../../utils/TypeUtils";
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
const MODULAR_URL = 'https://modular.im/services/matrix-hosting-riot' +
'?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
export const FREE = 'Free';
export const PREMIUM = 'Premium';

View file

@ -26,58 +26,48 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {toPx} from "../../../utils/units";
const useImageUrl = ({url, urls, idName, name, defaultToInitialLetter}) => {
const useImageUrl = ({url, urls}) => {
const [imageUrls, setUrls] = useState([]);
const [urlsIndex, setIndex] = useState();
const onError = () => {
const nextIndex = urlsIndex + 1;
if (nextIndex < imageUrls.length) {
// try the next one
setIndex(nextIndex);
}
};
const defaultImageUrl = useMemo(() => AvatarLogic.defaultAvatarUrlForString(idName || name), [idName, name]);
const onError = useCallback(() => {
setIndex(i => i + 1); // try the next one
}, []);
const memoizedUrls = useMemo(() => urls, [JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls, default image ]
// imageUrls: [ props.url, ...props.urls ]
let _urls = [];
if (!SettingsStore.getValue("lowBandwidth")) {
_urls = urls || [];
_urls = memoizedUrls || [];
if (url) {
_urls.unshift(url); // put in urls[0]
}
}
if (defaultToInitialLetter) {
_urls.push(defaultImageUrl); // lowest priority
}
// deduplicate URLs
_urls = Array.from(new Set(_urls));
setIndex(0);
setUrls(_urls);
}, [url, ...(urls || [])]); // eslint-disable-line react-hooks/exhaustive-deps
}, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps
const cli = useContext(MatrixClientContext);
const onClientSync = useCallback((syncState, prevState) => {
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected && urlsIndex > 0 ) { // Did we fall back?
// Start from the highest priority URL again
if (reconnected) {
setIndex(0);
}
}, [urlsIndex]);
}, []);
useEventEmitter(cli, "sync", onClientSync);
const imageUrl = imageUrls[urlsIndex];
return [imageUrl, imageUrl === defaultImageUrl, onError];
return [imageUrl, onError];
};
const BaseAvatar = (props) => {
@ -96,9 +86,9 @@ const BaseAvatar = (props) => {
...otherProps
} = props;
const [imageUrl, isDefault, onError] = useImageUrl({url, urls, idName, name, defaultToInitialLetter});
const [imageUrl, onError] = useImageUrl({url, urls});
if (isDefault) {
if (!imageUrl && defaultToInitialLetter) {
const initialLetter = AvatarLogic.getInitialLetter(name);
const textNode = (
<span
@ -116,7 +106,7 @@ const BaseAvatar = (props) => {
const imgNode = (
<img
className="mx_BaseAvatar_image"
src={imageUrl}
src={AvatarLogic.defaultAvatarUrlForString(idName || name)}
alt=""
title={title}
onError={onError}

View file

@ -18,10 +18,10 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as Avatar from '../../../Avatar';
import * as sdk from "../../../index";
import dis from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
export default createReactClass({
displayName: 'MemberAvatar',
@ -62,10 +62,14 @@ export default createReactClass({
return {
name: props.member.name,
title: props.title || props.member.userId,
imageUrl: Avatar.avatarUrlForMember(props.member,
props.width,
props.height,
props.resizeMethod),
imageUrl: props.member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
false,
),
};
} else if (props.fallbackUserId) {
return {

View file

@ -116,11 +116,6 @@ export default createReactClass({
this.closeMenu();
},
e2eInfoClicked: function() {
this.props.e2eInfoCallback();
this.closeMenu();
},
onReportEventClick: function() {
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
@ -465,15 +460,6 @@ export default createReactClass({
);
}
let e2eInfo;
if (this.props.e2eInfoCallback) {
e2eInfo = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
{ _t('End-to-end encryption information') }
</MenuItem>
);
}
let reportEventButton;
if (mxEvent.getSender() !== me) {
reportEventButton = (
@ -500,7 +486,6 @@ export default createReactClass({
{ quoteButton }
{ externalURLButton }
{ collapseReplyThread }
{ e2eInfo }
{ reportEventButton }
</div>
);

View file

@ -98,7 +98,7 @@ export default createReactClass({
render: function() {
return (
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
<input type="text" className="mx_RoomAlias" placeholder={_t("Address (optional)")}
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
value={this.props.alias} />
);

View file

@ -144,6 +144,7 @@ export default createReactClass({
>
<div className={classNames('mx_Dialog_header', {
'mx_Dialog_headerWithButton': !!this.props.headerButton,
'mx_Dialog_headerWithCancel': !!cancelButton,
})}>
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
{headerImage}

View file

@ -24,7 +24,7 @@ import withValidation from '../elements/Validation';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {Key} from "../../../Keyboard";
import SettingsStore from "../../../settings/SettingsStore";
import {privateShouldBeEncrypted} from "../../../createRoom";
export default createReactClass({
displayName: 'CreateRoomDialog',
@ -37,7 +37,7 @@ export default createReactClass({
const config = SdkConfig.get();
return {
isPublic: this.props.defaultPublic || false,
isEncrypted: true,
isEncrypted: privateShouldBeEncrypted(),
name: "",
topic: "",
alias: "",
@ -66,7 +66,7 @@ export default createReactClass({
createOpts.creation_content = {'m.federate': false};
}
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
if (!this.state.isPublic) {
opts.encryption = this.state.isEncrypted;
}
@ -181,7 +181,7 @@ export default createReactClass({
let publicPrivateLabel;
let aliasField;
if (this.state.isPublic) {
publicPrivateLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
publicPrivateLabel = (<p>{_t("Set a room address to easily share your room with other people.")}</p>);
const domain = MatrixClientPeg.get().getDomain();
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">
@ -193,7 +193,14 @@ export default createReactClass({
}
let e2eeSection;
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
if (!this.state.isPublic) {
let microcopy;
if (privateShouldBeEncrypted()) {
microcopy = _t("You cant disable this later. Bridges & most bots wont work yet.");
} else {
microcopy = _t("Your server admin has disabled end-to-end encryption by default " +
"in private rooms & Direct Messages.");
}
e2eeSection = <React.Fragment>
<LabelledToggleSwitch
label={ _t("Enable end-to-end encryption")}
@ -201,7 +208,7 @@ export default createReactClass({
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>
<p>{ microcopy }</p>
</React.Fragment>;
}

View file

@ -42,11 +42,9 @@ export default (props) => {
};
const description =
_t("You've previously used a newer version of Riot on %(host)s. " +
_t("You've previously used a newer version of Riot with this session. " +
"To use this version again with end to end encryption, you will " +
"need to sign out and back in again. ",
{host: props.host},
);
"need to sign out and back in again.");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');

View file

@ -25,6 +25,7 @@ import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler';
import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
import StyledCheckbox from "../elements/StyledCheckbox";
export default class DeactivateAccountDialog extends React.Component {
constructor(props) {
@ -209,13 +210,10 @@ export default class DeactivateAccountDialog extends React.Component {
<div className="mx_DeactivateAccountDialog_input_section">
<p>
<label htmlFor="mx_DeactivateAccountDialog_erase_account_input">
<input
id="mx_DeactivateAccountDialog_erase_account_input"
type="checkbox"
<StyledCheckbox
checked={this.state.shouldErase}
onChange={this._onEraseFieldChange}
/>
>
{_t(
"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 " +
@ -223,7 +221,7 @@ export default class DeactivateAccountDialog extends React.Component {
{},
{ b: (sub) => <b>{ sub }</b> },
)}
</label>
</StyledCheckbox>
</p>
{error}

View file

@ -1,377 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import {ensureDMExists} from "../../../createRoom";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from '../../../settings/SettingsStore';
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
import VerificationQREmojiOptions from "../verification/VerificationQREmojiOptions";
const MODE_LEGACY = 'legacy';
const MODE_SAS = 'sas';
const PHASE_START = 0;
const PHASE_WAIT_FOR_PARTNER_TO_ACCEPT = 1;
const PHASE_PICK_VERIFICATION_OPTION = 2;
const PHASE_SHOW_SAS = 3;
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 4;
const PHASE_VERIFIED = 5;
const PHASE_CANCELLED = 6;
export default class DeviceVerifyDialog extends React.Component {
static propTypes = {
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
this._verifier = null;
this._showSasEvent = null;
this._request = null;
this.state = {
phase: PHASE_START,
mode: MODE_SAS,
sasVerified: false,
};
}
componentWillUnmount() {
if (this._verifier) {
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier.cancel('User cancel');
}
}
_onSwitchToLegacyClick = () => {
if (this._verifier) {
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier.cancel('User cancel');
this._verifier = null;
}
this.setState({mode: MODE_LEGACY});
}
_onSwitchToSasClick = () => {
this.setState({mode: MODE_SAS});
}
_onCancelClick = () => {
this.props.onFinished(false);
}
_onUseSasClick = async () => {
try {
this._verifier = this._request.beginKeyVerification(verificationMethods.SAS);
this._verifier.on('show_sas', this._onVerifierShowSas);
// throws upon cancellation
await this._verifier.verify();
this.setState({phase: PHASE_VERIFIED});
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier = null;
} catch (e) {
console.log("Verification failed", e);
this.setState({
phase: PHASE_CANCELLED,
});
this._verifier = null;
this._request = null;
}
};
_onLegacyFinished = (confirm) => {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.props.device.deviceId, true,
);
}
this.props.onFinished(confirm);
}
_onSasRequestClick = async () => {
this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_ACCEPT,
});
const client = MatrixClientPeg.get();
const verifyingOwnDevice = this.props.userId === client.getUserId();
try {
if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
const roomId = await ensureDMExistsAndOpen(this.props.userId);
// throws upon cancellation before having started
const request = await client.requestVerificationDM(
this.props.userId, roomId,
);
await request.waitFor(r => r.ready || r.started);
if (request.ready) {
this._verifier = request.beginKeyVerification(verificationMethods.SAS);
} else {
this._verifier = request.verifier;
}
} else if (verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
this._request = await client.requestVerification(this.props.userId, [
verificationMethods.SAS,
SHOW_QR_CODE_METHOD,
verificationMethods.RECIPROCATE_QR_CODE,
]);
await this._request.waitFor(r => r.ready || r.started);
this.setState({phase: PHASE_PICK_VERIFICATION_OPTION});
} else {
this._verifier = client.beginKeyVerification(
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
);
}
if (!this._verifier) return;
this._verifier.on('show_sas', this._onVerifierShowSas);
// throws upon cancellation
await this._verifier.verify();
this.setState({phase: PHASE_VERIFIED});
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier = null;
} catch (e) {
console.log("Verification failed", e);
this.setState({
phase: PHASE_CANCELLED,
});
this._verifier = null;
}
}
_onSasMatchesClick = () => {
this._showSasEvent.confirm();
this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM,
});
}
_onVerifiedDoneClick = () => {
this.props.onFinished(true);
}
_onVerifierShowSas = (e) => {
this._showSasEvent = e;
this.setState({
phase: PHASE_SHOW_SAS,
});
}
_renderSasVerification() {
let body;
switch (this.state.phase) {
case PHASE_START:
body = this._renderVerificationPhaseStart();
break;
case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT:
body = this._renderVerificationPhaseWaitAccept();
break;
case PHASE_PICK_VERIFICATION_OPTION:
body = this._renderVerificationPhasePick();
break;
case PHASE_SHOW_SAS:
body = this._renderSasVerificationPhaseShowSas();
break;
case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM:
body = this._renderSasVerificationPhaseWaitForPartnerToConfirm();
break;
case PHASE_VERIFIED:
body = this._renderVerificationPhaseVerified();
break;
case PHASE_CANCELLED:
body = this._renderVerificationPhaseCancelled();
break;
}
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
return (
<BaseDialog
title={_t("Verify session")}
onFinished={this._onCancelClick}
>
{body}
</BaseDialog>
);
}
_renderVerificationPhaseStart() {
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<div>
<AccessibleButton
element="span" className="mx_linkButton" onClick={this._onSwitchToLegacyClick}
>
{_t("Use Legacy Verification (for older clients)")}
</AccessibleButton>
<p>
{ _t("Verify by comparing a short text string.") }
</p>
<p>
{_t("To be secure, do this in person or use a trusted way to communicate.")}
</p>
<DialogButtons
primaryButton={_t('Begin Verifying')}
hasCancel={true}
onPrimaryButtonClick={this._onSasRequestClick}
onCancel={this._onCancelClick}
/>
</div>
);
}
_renderVerificationPhaseWaitAccept() {
const Spinner = sdk.getComponent("views.elements.Spinner");
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
return (
<div>
<Spinner />
<p>{_t("Waiting for partner to accept...")}</p>
<p>{_t(
"Nothing appearing? Not all clients support interactive verification yet. " +
"<button>Use legacy verification</button>.",
{}, {button: sub => <AccessibleButton element='span' className="mx_linkButton"
onClick={this._onSwitchToLegacyClick}
>
{sub}
</AccessibleButton>},
)}</p>
</div>
);
}
_renderVerificationPhasePick() {
return <VerificationQREmojiOptions
request={this._request}
onCancel={this._onCancelClick}
onStartEmoji={this._onUseSasClick}
/>;
}
_renderSasVerificationPhaseShowSas() {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return <VerificationShowSas
sas={this._showSasEvent.sas}
onCancel={this._onCancelClick}
onDone={this._onSasMatchesClick}
isSelf={MatrixClientPeg.get().getUserId() === this.props.userId}
onStartEmoji={this._onUseSasClick}
inDialog={true}
/>;
}
_renderSasVerificationPhaseWaitForPartnerToConfirm() {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <div>
<Spinner />
<p>{_t(
"Waiting for %(userId)s to confirm...", {userId: this.props.userId},
)}</p>
</div>;
}
_renderVerificationPhaseVerified() {
const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete');
return <VerificationComplete onDone={this._onVerifiedDoneClick} />;
}
_renderVerificationPhaseCancelled() {
const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled');
return <VerificationCancelled onDone={this._onCancelClick} />;
}
_renderLegacyVerification() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
let text;
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
text = _t("To verify that this session can be trusted, please check that the key you see " +
"in User Settings on that device matches the key below:");
} else {
text = _t("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:");
}
const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
const body = (
<div>
<AccessibleButton
element="span" className="mx_linkButton" onClick={this._onSwitchToSasClick}
>
{_t("Use two-way text verification")}
</AccessibleButton>
<p>
{ text }
</p>
<div className="mx_DeviceVerifyDialog_cryptoSection">
<ul>
<li><label>{ _t("Session name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
<li><label>{ _t("Session ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
<li><label>{ _t("Session key") }:</label> <span><code><b>{ key }</b></code></span></li>
</ul>
</div>
<p>
{ _t("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.") }
</p>
</div>
);
return (
<QuestionDialog
title={_t("Verify session")}
description={body}
button={_t("I verify that the keys match")}
onFinished={this._onLegacyFinished}
/>
);
}
render() {
if (this.state.mode === MODE_LEGACY) {
return this._renderLegacyVerification();
} else {
return <div>
{this._renderSasVerification()}
</div>;
}
}
}
async function ensureDMExistsAndOpen(userId) {
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
// don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen,
// we causes us to loose the verifier and restart, and we end up having two verification requests
dis.dispatch({
action: 'view_room',
room_id: roomId,
should_peek: false,
});
return roomId;
}

View file

@ -31,9 +31,8 @@ import dis from "../../../dispatcher/dispatcher";
import IdentityAuthClient from "../../../IdentityAuthClient";
import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize";
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
import {inviteMultipleToRoom} from "../../../RoomInvite";
import SettingsStore from '../../../settings/SettingsStore';
import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
@ -576,7 +575,7 @@ export default class InviteDialog extends React.PureComponent {
const createRoomOptions = {inlineErrors: true};
if (SettingsStore.getValue("feature_cross_signing")) {
if (privateShouldBeEncrypted()) {
// Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room.
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);

View file

@ -1,178 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
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 Modal from '../../../Modal';
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
// TODO: We can remove this once cross-signing is the only way.
// https://github.com/vector-im/riot-web/issues/11908
/**
* Dialog which asks the user whether they want to share their keys with
* an unverified device.
*
* onFinished is called with `true` if the key should be shared, `false` if it
* should not, and `undefined` if the dialog is cancelled. (In other words:
* truthy: do the key share. falsy: don't share the keys).
*/
export default createReactClass({
propTypes: {
matrixClient: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
deviceId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {
return {
deviceInfo: null,
wasNewDevice: false,
};
},
componentDidMount: function() {
this._unmounted = false;
const userId = this.props.userId;
const deviceId = this.props.deviceId;
// give the client a chance to refresh the device list
this.props.matrixClient.downloadKeys([userId], false).then((r) => {
if (this._unmounted) { return; }
const deviceInfo = r[userId][deviceId];
if (!deviceInfo) {
console.warn(`No details found for session ${userId}:${deviceId}`);
this.props.onFinished(false);
return;
}
const wasNewDevice = !deviceInfo.isKnown();
this.setState({
deviceInfo: deviceInfo,
wasNewDevice: wasNewDevice,
});
// if the device was new before, it's not any more.
if (wasNewDevice) {
this.props.matrixClient.setDeviceKnown(
userId,
deviceId,
true,
);
}
});
},
componentWillUnmount: function() {
this._unmounted = true;
},
_onVerifyClicked: function() {
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
console.log("KeyShareDialog: Starting verify dialog");
Modal.createTrackedDialog('Key Share', 'Starting dialog', DeviceVerifyDialog, {
userId: this.props.userId,
device: this.state.deviceInfo,
onFinished: (verified) => {
if (verified) {
// can automatically share the keys now.
this.props.onFinished(true);
}
},
}, null, /* priority = */ false, /* static = */ true);
},
_onShareClicked: function() {
console.log("KeyShareDialog: User clicked 'share'");
this.props.onFinished(true);
},
_onIgnoreClicked: function() {
console.log("KeyShareDialog: User clicked 'ignore'");
this.props.onFinished(false);
},
_renderContent: function() {
const displayName = this.state.deviceInfo.getDisplayName() ||
this.state.deviceInfo.deviceId;
let text;
if (this.state.wasNewDevice) {
text = _td("You added a new session '%(displayName)s', which is"
+ " requesting encryption keys.");
} else {
text = _td("Your unverified session '%(displayName)s' is requesting"
+ " encryption keys.");
}
text = _t(text, {displayName: displayName});
return (
<div id='mx_Dialog_content'>
<p>{ text }</p>
<div className="mx_Dialog_buttons">
<button onClick={this._onVerifyClicked} autoFocus="true">
{ _t('Start verification') }
</button>
<button onClick={this._onShareClicked}>
{ _t('Share without verifying') }
</button>
<button onClick={this._onIgnoreClicked}>
{ _t('Ignore request') }
</button>
</div>
</div>
);
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('views.elements.Spinner');
let content;
if (this.state.deviceInfo) {
content = this._renderContent();
} else {
content = (
<div id='mx_Dialog_content'>
<p>{ _t('Loading session info...') }</p>
<Spinner />
</div>
);
}
return (
<BaseDialog className='mx_KeyShareRequestDialog'
onFinished={this.props.onFinished}
title={_t('Encryption key request')}
contentId='mx_Dialog_content'
>
{ content }
</BaseDialog>
);
},
});

View file

@ -29,6 +29,7 @@ import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../..
import * as ContextMenu from "../../structures/ContextMenu";
import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext, selectText} from "../../../utils/strings";
import StyledCheckbox from '../elements/StyledCheckbox';
const socials = [
{
@ -168,13 +169,12 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
const events = this.props.target.getLiveTimeline().getEvents();
if (events.length > 0) {
checkbox = <div>
<input type="checkbox"
id="mx_ShareDialog_checkbox"
<StyledCheckbox
checked={this.state.linkSpecificEvent}
onChange={this.onLinkSpecificEventCheckboxClick} />
<label htmlFor="mx_ShareDialog_checkbox">
onChange={this.onLinkSpecificEventCheckboxClick}
>
{ _t('Link to most recent message') }
</label>
</StyledCheckbox>
</div>;
}
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
@ -184,13 +184,12 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
} else if (this.props.target instanceof MatrixEvent) {
title = _t('Share Room Message');
checkbox = <div>
<input type="checkbox"
id="mx_ShareDialog_checkbox"
<StyledCheckbox
checked={this.state.linkSpecificEvent}
onClick={this.onLinkSpecificEventCheckboxClick} />
<label htmlFor="mx_ShareDialog_checkbox">
onClick={this.onLinkSpecificEventCheckboxClick}
>
{ _t('Link to selected message') }
</label>
</StyledCheckbox>
</div>;
}

View file

@ -1,187 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import { markAllDevicesKnown } from '../../../cryptodevices';
function UserUnknownDeviceList(props) {
const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
const {userId, userDevices} = props;
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
<li key={deviceId}><MemberDeviceInfo device={userDevices[deviceId]} userId={userId} showDeviceId={true} /></li>,
);
return (
<ul className="mx_UnknownDeviceDialog_deviceList">
{ deviceListEntries }
</ul>
);
}
UserUnknownDeviceList.propTypes = {
userId: PropTypes.string.isRequired,
// map from deviceid -> deviceinfo
userDevices: PropTypes.object.isRequired,
};
function UnknownDeviceList(props) {
const {devices} = props;
const userListEntries = Object.keys(devices).map((userId) =>
<li key={userId}>
<p>{ userId }:</p>
<UserUnknownDeviceList userId={userId} userDevices={devices[userId]} />
</li>,
);
return <ul>{ userListEntries }</ul>;
}
UnknownDeviceList.propTypes = {
// map from userid -> deviceid -> deviceinfo
devices: PropTypes.object.isRequired,
};
export default createReactClass({
displayName: 'UnknownDeviceDialog',
propTypes: {
room: PropTypes.object.isRequired,
// map from userid -> deviceid -> deviceinfo or null if devices are not yet loaded
devices: PropTypes.object,
onFinished: PropTypes.func.isRequired,
// Label for the button that marks all devices known and tries the send again
sendAnywayLabel: PropTypes.string.isRequired,
// Label for the button that to send the event if you've verified all devices
sendLabel: PropTypes.string.isRequired,
// function to retry the request once all devices are verified / known
onSend: PropTypes.func.isRequired,
},
componentDidMount: function() {
MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged);
},
componentWillUnmount: function() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("deviceVerificationChanged", this._onDeviceVerificationChanged);
}
},
_onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
if (this.props.devices[userId] && this.props.devices[userId][deviceId]) {
// XXX: Mutating props :/
this.props.devices[userId][deviceId] = deviceInfo;
this.forceUpdate();
}
},
_onDismissClicked: function() {
this.props.onFinished();
},
_onSendAnywayClicked: function() {
markAllDevicesKnown(MatrixClientPeg.get(), this.props.devices);
this.props.onFinished();
this.props.onSend();
},
_onSendClicked: function() {
this.props.onFinished();
this.props.onSend();
},
render: function() {
if (this.props.devices === null) {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
}
let warning;
if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
warning = (
<h4>
{ _t("You are currently blacklisting unverified sessions; to send " +
"messages to these sessions you must verify them.") }
</h4>
);
} else {
warning = (
<div>
<p>
{ _t("We recommend you go through the verification process " +
"for each session to confirm they belong to their legitimate owner, " +
"but you can resend the message without verifying if you prefer.") }
</p>
</div>
);
}
let haveUnknownDevices = false;
Object.keys(this.props.devices).forEach((userId) => {
Object.keys(this.props.devices[userId]).map((deviceId) => {
const device = this.props.devices[userId][deviceId];
if (device.isUnverified() && !device.isKnown()) {
haveUnknownDevices = true;
}
});
});
const sendButtonOnClick = haveUnknownDevices ? this._onSendAnywayClicked : this._onSendClicked;
const sendButtonLabel = haveUnknownDevices ? this.props.sendAnywayLabel : this.props.sendAnywayLabel;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className='mx_UnknownDeviceDialog'
onFinished={this.props.onFinished}
title={_t('Room contains unknown sessions')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<h4>
{ _t('"%(RoomName)s" contains sessions that you haven\'t seen before.', {RoomName: this.props.room.name}) }
</h4>
{ warning }
{ _t("Unknown sessions") }:
<UnknownDeviceList devices={this.props.devices} />
</div>
<DialogButtons primaryButton={sendButtonLabel}
onPrimaryButtonClick={sendButtonOnClick}
onCancel={this._onDismissClicked} />
</BaseDialog>
);
// XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?
// It feels like confused users will likely turn it on and then disappear in a cloud of UISIs...
},
});

View file

@ -84,7 +84,7 @@ export default class UploadConfirmDialog extends React.Component {
preview = <div>
<div>
<img className="mx_UploadConfirmDialog_fileIcon"
src={require("../../../../res/img/files.png")}
src={require("../../../../res/img/feather-customised/files.svg")}
/>
{this.props.file.name} ({filesize(this.props.file.size)})
</div>

View file

@ -20,10 +20,8 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk';
import Modal from '../../../../Modal';
import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager';
import SettingsStore from "../../../../settings/SettingsStore";
const RESTORE_TYPE_PASSPHRASE = 0;
const RESTORE_TYPE_RECOVERYKEY = 1;
@ -90,21 +88,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
_onResetRecoveryClick = () => {
this.props.onFinished(false);
if (SettingsStore.getValue("feature_cross_signing")) {
// If cross-signing is enabled, we reset the SSSS recovery passphrase (and cross-signing keys)
this.props.onFinished(false);
accessSecretStorage(() => {}, /* forceReset = */ true);
} else {
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
{
onFinished: () => {
this._loadBackupStatus();
},
}, null, /* priority = */ false, /* static = */ true,
);
}
}
_onRecoveryKeyChange = (e) => {
@ -243,8 +227,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
loadError: null,
});
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored();
const cli = MatrixClientPeg.get();
const backupInfo = await cli.getKeyBackupVersion();
const has4S = await cli.hasSecretStorageKey();
const backupKeyStored = has4S && await cli.isKeyBackupKeyStored();
this.setState({
backupInfo,
backupKeyStored,

View file

@ -1,127 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
// XXX: This component is *not* cross-signing aware. Once everything is
// cross-signing, this component should just go away.
export default createReactClass({
displayName: 'DeviceVerifyButtons',
propTypes: {
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
},
getInitialState: function() {
return {
device: this.props.device,
};
},
componentDidMount: function() {
const cli = MatrixClientPeg.get();
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
},
componentWillUnmount: function() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
}
},
onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
if (userId === this.props.userId && deviceId === this.props.device.deviceId) {
this.setState({ device: deviceInfo });
}
},
onVerifyClick: function() {
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
userId: this.props.userId,
device: this.state.device,
}, null, /* priority = */ false, /* static = */ true);
},
onUnverifyClick: function() {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.state.device.deviceId, false,
);
},
onBlacklistClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.state.device.deviceId, true,
);
},
onUnblacklistClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.state.device.deviceId, false,
);
},
render: function() {
let blacklistButton = null; let verifyButton = null;
if (this.state.device.isBlocked()) {
blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
onClick={this.onUnblacklistClick}>
{ _t("Unblacklist") }
</button>
);
} else {
blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
onClick={this.onBlacklistClick}>
{ _t("Blacklist") }
</button>
);
}
if (this.state.device.isVerified()) {
verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}>
{ _t("Unverify") }
</button>
);
} else {
verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
onClick={this.onVerifyClick}>
{ _t("Verify...") }
</button>
);
}
return (
<div className="mx_DeviceVerifyButtons" >
{ verifyButton }
{ blacklistButton }
</div>
);
},
});

View file

@ -58,18 +58,15 @@ export default class Draggable extends React.Component<IProps, IState> {
document.addEventListener("mousemove", this.state.onMouseMove);
document.addEventListener("mouseup", this.state.onMouseUp);
console.log("Mouse down")
}
private onMouseUp = (event: MouseEvent): void => {
document.removeEventListener("mousemove", this.state.onMouseMove);
document.removeEventListener("mouseup", this.state.onMouseUp);
this.props.onMouseUp(event);
console.log("Mouse up")
}
private onMouseMove(event: MouseEvent): void {
console.log("Mouse Move")
const newLocation = this.props.dragFunc(this.state.location, event);
this.setState({

View file

@ -15,10 +15,10 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import * as sdk from '../../../index';
import { debounce } from 'lodash';
import {IFieldState, IValidationResult} from "../elements/Validation";
// Invoke validation from user input (when typing, etc.) at most once every N ms.
const VALIDATION_THROTTLE_MS = 200;
@ -29,58 +29,93 @@ function getId() {
return `${BASE_ID}_${count++}`;
}
export default class Field extends React.PureComponent {
static propTypes = {
interface IProps extends React.InputHTMLAttributes<HTMLSelectElement | HTMLInputElement> {
// The field's ID, which binds the input and label together. Immutable.
id: PropTypes.string,
id?: string,
// The element to create. Defaults to "input".
// To define options for a select, use <Field><option ... /></Field>
element: PropTypes.oneOf(["input", "select", "textarea"]),
element?: "input" | "select" | "textarea",
// The field's type (when used as an <input>). Defaults to "text".
type: PropTypes.string,
type?: string,
// id of a <datalist> element for suggestions
list: PropTypes.string,
list?: string,
// The field's label string.
label: PropTypes.string,
label?: string,
// The field's placeholder string. Defaults to the label.
placeholder: PropTypes.string,
placeholder?: string,
// The field's value.
// This is a controlled component, so the value is required.
value: PropTypes.string.isRequired,
value: string,
// Optional component to include inside the field before the input.
prefix: PropTypes.node,
prefixComponent?: React.ReactNode,
// Optional component to include inside the field after the input.
postfix: PropTypes.node,
postfixComponent?: React.ReactNode,
// The callback called whenever the contents of the field
// changes. Returns an object with `valid` boolean field
// and a `feedback` react component field to provide feedback
// to the user.
onValidate: PropTypes.func,
onValidate?: (input: IFieldState) => Promise<IValidationResult>,
// If specified, overrides the value returned by onValidate.
flagInvalid: PropTypes.bool,
flagInvalid?: boolean,
// If specified, contents will appear as a tooltip on the element and
// validation feedback tooltips will be suppressed.
tooltipContent: PropTypes.node,
tooltipContent?: React.ReactNode,
// If specified alongside tooltipContent, the class name to apply to the
// tooltip itself.
tooltipClassName: PropTypes.string,
tooltipClassName?: string,
// If specified, an additional class name to apply to the field container
className: PropTypes.string,
className?: string,
// All other props pass through to the <input>.
};
}
interface IState {
valid: boolean,
feedback: React.ReactNode,
feedbackVisible: boolean,
focused: boolean,
}
export default class Field extends React.PureComponent<IProps, IState> {
private id: string;
private input: HTMLInputElement;
private static defaultProps = {
element: "input",
type: "text",
}
/*
* This was changed from throttle to debounce: this is more traditional for
* form validation since it means that the validation doesn't happen at all
* until the user stops typing for a bit (debounce defaults to not running on
* the leading edge). If we're doing an HTTP hit on each validation, we have more
* incentive to prevent validating input that's very unlikely to be valid.
* We may find that we actually want different behaviour for registration
* fields, in which case we can add some options to control it.
*/
private validateOnChange = debounce(() => {
this.validate({
focused: true,
});
}, VALIDATION_THROTTLE_MS);
constructor(props) {
super(props);
this.state = {
valid: undefined,
feedback: undefined,
feedbackVisible: false,
focused: false,
};
this.id = this.props.id || getId();
}
onFocus = (ev) => {
public focus() {
this.input.focus();
}
private onFocus = (ev) => {
this.setState({
focused: true,
});
@ -93,7 +128,7 @@ export default class Field extends React.PureComponent {
}
};
onChange = (ev) => {
private onChange = (ev) => {
this.validateOnChange();
// Parent component may have supplied its own `onChange` as well
if (this.props.onChange) {
@ -101,7 +136,7 @@ export default class Field extends React.PureComponent {
}
};
onBlur = (ev) => {
private onBlur = (ev) => {
this.setState({
focused: false,
});
@ -114,11 +149,7 @@ export default class Field extends React.PureComponent {
}
};
focus() {
this.input.focus();
}
async validate({ focused, allowEmpty = true }) {
private async validate({ focused, allowEmpty = true }: {focused: boolean, allowEmpty?: boolean}) {
if (!this.props.onValidate) {
return;
}
@ -149,56 +180,42 @@ export default class Field extends React.PureComponent {
}
}
/*
* This was changed from throttle to debounce: this is more traditional for
* form validation since it means that the validation doesn't happen at all
* until the user stops typing for a bit (debounce defaults to not running on
* the leading edge). If we're doing an HTTP hit on each validation, we have more
* incentive to prevent validating input that's very unlikely to be valid.
* We may find that we actually want different behaviour for registration
* fields, in which case we can add some options to control it.
*/
validateOnChange = debounce(() => {
this.validate({
focused: true,
});
}, VALIDATION_THROTTLE_MS);
render() {
public render() {
const {
element, prefix, postfix, className, onValidate, children,
element, prefixComponent, postfixComponent, className, onValidate, children,
tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props;
const inputElement = element || "input";
// Set some defaults for the <input> element
inputProps.type = inputProps.type || "text";
inputProps.ref = input => this.input = input;
const ref = input => this.input = input;
inputProps.placeholder = inputProps.placeholder || inputProps.label;
inputProps.id = this.id; // this overwrites the id from props
inputProps.onFocus = this.onFocus;
inputProps.onChange = this.onChange;
inputProps.onBlur = this.onBlur;
inputProps.list = list;
const fieldInput = React.createElement(inputElement, inputProps, children);
// Appease typescript's inference
const inputProps_ = {...inputProps, ref, list};
const fieldInput = React.createElement(this.props.element, inputProps_, children);
let prefixContainer = null;
if (prefix) {
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
if (prefixComponent) {
prefixContainer = <span className="mx_Field_prefix">{prefixComponent}</span>;
}
let postfixContainer = null;
if (postfix) {
postfixContainer = <span className="mx_Field_postfix">{postfix}</span>;
if (postfixComponent) {
postfixContainer = <span className="mx_Field_postfix">{postfixComponent}</span>;
}
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, className, {
const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, {
// If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do
// properly.
mx_Field_labelAlwaysTopLeft: prefix,
mx_Field_labelAlwaysTopLeft: prefixComponent,
mx_Field_valid: onValidate && this.state.valid === true,
mx_Field_invalid: hasValidationFlag
? flagInvalid

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