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/avatars/MemberAvatar.js
src/components/views/create_room/RoomAlias.js src/components/views/create_room/RoomAlias.js
src/components/views/dialogs/SetPasswordDialog.js src/components/views/dialogs/SetPasswordDialog.js
src/components/views/dialogs/UnknownDeviceDialog.js
src/components/views/elements/AddressSelector.js src/components/views/elements/AddressSelector.js
src/components/views/elements/DirectorySearchBox.js src/components/views/elements/DirectorySearchBox.js
src/components/views/elements/MemberEventListSummary.js src/components/views/elements/MemberEventListSummary.js
src/components/views/elements/UserSelector.js src/components/views/elements/UserSelector.js
src/components/views/globals/MatrixToolbar.js
src/components/views/globals/NewVersionBar.js src/components/views/globals/NewVersionBar.js
src/components/views/globals/UpdateCheckBar.js
src/components/views/messages/MFileBody.js src/components/views/messages/MFileBody.js
src/components/views/messages/TextualBody.js src/components/views/messages/TextualBody.js
src/components/views/room_settings/ColorSettings.js src/components/views/room_settings/ColorSettings.js
src/components/views/rooms/Autocomplete.js src/components/views/rooms/Autocomplete.js
src/components/views/rooms/AuxPanel.js src/components/views/rooms/AuxPanel.js
src/components/views/rooms/LinkPreviewWidget.js src/components/views/rooms/LinkPreviewWidget.js
src/components/views/rooms/MemberDeviceInfo.js
src/components/views/rooms/MemberInfo.js src/components/views/rooms/MemberInfo.js
src/components/views/rooms/MemberList.js src/components/views/rooms/MemberList.js
src/components/views/rooms/RoomList.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) 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) [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, 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 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 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 This document reflects how we would like Matrix JavaScript code to look, with
acknowledgement that a significant amount of code is written to older 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 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 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 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 General Style
------------- -------------

View file

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

View file

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

View file

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

View file

@ -19,9 +19,18 @@ limitations under the License.
display: flex; display: flex;
/* LeftPanel 260px */ /* LeftPanel 260px */
min-width: 260px; min-width: 260px;
max-width: 50%;
flex: 0 0 auto; 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 { .mx_LeftPanel_container.collapsed {
min-width: unset; min-width: unset;
/* Collapsed LeftPanel 50px */ /* Collapsed LeftPanel 50px */

View file

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

View file

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

View file

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

View file

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

View file

@ -18,8 +18,3 @@ limitations under the License.
margin-top: 10px; margin-top: 10px;
display: flex; 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; margin-left: 5px;
width: 20px; width: 20px;
height: 20px; height: 20px;
background-repeat: none;
} }
.mx_ShareDialog_split { .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-radius: 3px;
border: solid 1px $accent-color; border: solid 1px $accent-color;
cursor: pointer; cursor: pointer;
z-index: 1;
} }
.mx_AddressSelector.mx_AddressSelector_empty { .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 { .mx_EmojiPicker_footer {
border-top: 1px solid $message-action-bar-border-color; border-top: 1px solid $message-action-bar-border-color;
height: 72px; min-height: 72px;
display: flex; display: flex;
align-items: center; 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; height: $AppsDrawerBodyHeight;
} }
.mx_AppTile_persistedWrapper > div {
height: 100%;
}
.mx_AppTile_mini .mx_AppTile_persistedWrapper { .mx_AppTile_mini .mx_AppTile_persistedWrapper {
height: 114px; height: 114px;
} }

View file

@ -18,7 +18,6 @@ limitations under the License.
$left-gutter: 65px; $left-gutter: 65px;
.mx_GroupLayout { .mx_GroupLayout {
.mx_EventTile { .mx_EventTile {
> .mx_SenderProfile { > .mx_SenderProfile {
line-height: $font-17px; line-height: $font-17px;
@ -55,77 +54,77 @@ $left-gutter: 65px;
.mx_MatrixChat_useCompactLayout { .mx_MatrixChat_useCompactLayout {
.mx_EventTile { .mx_EventTile {
padding-top: 4px; padding-top: 4px;
}
.mx_EventTile.mx_EventTile_info {
// same as the padding for non-compact .mx_EventTile.mx_EventTile_info
padding-top: 0px;
font-size: $font-13px;
.mx_EventTile_line, .mx_EventTile_reply { .mx_EventTile_line, .mx_EventTile_reply {
line-height: $font-20px; padding-top: 0;
padding-bottom: 0;
} }
.mx_EventTile_avatar {
top: 4px; &.mx_EventTile_info {
// same as the padding for non-compact .mx_EventTile.mx_EventTile_info
padding-top: 0px;
font-size: $font-13px;
.mx_EventTile_line, .mx_EventTile_reply {
line-height: $font-20px;
}
.mx_EventTile_avatar {
top: 4px;
}
} }
}
.mx_EventTile .mx_SenderProfile { .mx_SenderProfile {
font-size: $font-13px; font-size: $font-13px;
} }
&.mx_EventTile_emote {
// add a bit more space for emotes so that avatars don't collide
padding-top: 8px;
.mx_EventTile_avatar {
top: 2px;
}
.mx_EventTile_line, .mx_EventTile_reply {
padding-top: 0px;
padding-bottom: 1px;
}
}
&.mx_EventTile_emote.mx_EventTile_continuation {
padding-top: 0;
.mx_EventTile_line, .mx_EventTile_reply {
padding-top: 0px;
padding-bottom: 0px;
}
}
.mx_EventTile.mx_EventTile_emote {
// add a bit more space for emotes so that avatars don't collide
padding-top: 8px;
.mx_EventTile_avatar { .mx_EventTile_avatar {
top: 2px; top: 2px;
} }
.mx_EventTile_line, .mx_EventTile_reply {
padding-top: 0px; .mx_EventTile_e2eIcon {
padding-bottom: 1px; top: 3px;
} }
}
.mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation { .mx_EventTile_readAvatars {
padding-top: 0; top: 27px;
.mx_EventTile_line, .mx_EventTile_reply {
padding-top: 0px;
padding-bottom: 0px;
} }
}
.mx_EventTile_line, .mx_EventTile_reply { .mx_EventTile_continuation .mx_EventTile_readAvatars,
padding-top: 0px; .mx_EventTile_emote .mx_EventTile_readAvatars {
padding-bottom: 0px; top: 5px;
} }
.mx_EventTile_avatar { .mx_EventTile_info .mx_EventTile_readAvatars {
top: 2px; top: 4px;
} }
.mx_EventTile_e2eIcon { .mx_EventTile_content .markdown-body {
top: 3px; p, ul, ol, dl, blockquote, pre, table {
} margin-bottom: 4px; // 1/4 of the non-compact margin-bottom
}
.mx_EventTile_readAvatars { }
top: 27px;
}
.mx_EventTile_continuation .mx_EventTile_readAvatars,
.mx_EventTile_emote .mx_EventTile_readAvatars {
top: 5px;
}
.mx_EventTile_info .mx_EventTile_readAvatars {
top: 4px;
} }
.mx_RoomView_MessageList h2 { .mx_RoomView_MessageList h2 {
margin-top: 6px; 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
}
}
} }

View file

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

View file

@ -15,6 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_RoomList.mx_RoomList2 {
overflow-y: auto;
}
.mx_RoomList { .mx_RoomList {
/* take up remaining space below TopLeftMenu */ /* take up remaining space below TopLeftMenu */
flex: 1; 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,16 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_DeviceVerifyDialog_cryptoSection ul { @import "../../../../node_modules/react-resizable/css/styles.css";
display: table;
}
.mx_DeviceVerifyDialog_cryptoSection li { .mx_RoomList2 .mx_RoomSubList_labelContainer {
display: table-row; z-index: 12;
}
.mx_DeviceVerifyDialog_cryptoSection label,
.mx_DeviceVerifyDialog_cryptoSection span {
display: table-cell;
padding-right: 1em;
} }

View file

@ -20,7 +20,7 @@ limitations under the License.
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
height: 32px; height: 34px;
margin: 0; margin: 0;
padding: 0 8px 0 10px; padding: 0 8px 0 10px;
position: relative; 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; 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 * 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 { declare global {
interface Window { interface Window {
Modernizr: ModernizrStatic; Modernizr: ModernizrStatic;
mxMatrixClientPeg: IMatrixClientPeg;
Olm: { Olm: {
init: () => Promise<void>; init: () => Promise<void>;
}; };
mx_ContentMessages: ContentMessages;
mx_ToastStore: ToastStore;
mx_DeviceListener: DeviceListener;
mx_RoomListStore2: RoomListStore2;
} }
// workaround for https://github.com/microsoft/TypeScript/issues/30933 // 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 DMRoomMap from './utils/DMRoomMap';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; 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) { export function avatarUrlForMember(member, width, height, resizeMethod) {
let url; let url;
if (member && member.getAvatarUrl) { if (member && member.getAvatarUrl) {

View file

@ -21,6 +21,22 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import BaseEventIndexManager from './indexing/BaseEventIndexManager'; import BaseEventIndexManager from './indexing/BaseEventIndexManager';
import {ActionPayload} from "./dispatcher/payloads"; 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 * Base class for classes that provide platform-specific functionality
@ -34,6 +50,7 @@ export default abstract class BasePlatform {
constructor() { constructor() {
dis.register(this.onAction); dis.register(this.onAction);
this.startUpdateCheck = this.startUpdateCheck.bind(this);
} }
protected onAction = (payload: ActionPayload) => { protected onAction = (payload: ActionPayload) => {
@ -56,6 +73,53 @@ export default abstract class BasePlatform {
this.errorDidOccur = errorDidOccur; 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 * Returns true if the platform supports displaying
* notifications, otherwise false. * notifications, otherwise false.
@ -157,11 +221,9 @@ export default abstract class BasePlatform {
setLanguage(preferredLangs: string[]) {} setLanguage(preferredLangs: string[]) {}
getSSOCallbackUrl(hsUrl: string, isUrl: string, fragmentAfterLogin: string): URL { getSSOCallbackUrl(fragmentAfterLogin: string): URL {
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.hash = fragmentAfterLogin || ""; url.hash = fragmentAfterLogin || "";
url.searchParams.set("homeserver", hsUrl);
url.searchParams.set("identityServer", isUrl);
return url; return url;
} }
@ -172,12 +234,47 @@ export default abstract class BasePlatform {
* @param {string} fragmentAfterLogin the hash to pass to the app during sso callback. * @param {string} fragmentAfterLogin the hash to pass to the app during sso callback.
*/ */
startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) { startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) {
const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl(), // persist hs url and is url for when the user is returned to the app with the login token
fragmentAfterLogin); 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 window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO
} }
onKeyDown(ev: KeyboardEvent): boolean { onKeyDown(ev: KeyboardEvent): boolean {
return false; // no shortcuts implemented 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 { _t } from './languageHandler';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
import WidgetUtils from './utils/WidgetUtils'; import WidgetUtils from './utils/WidgetUtils';
import WidgetEchoStore from './stores/WidgetEchoStore'; import WidgetEchoStore from './stores/WidgetEchoStore';
import SettingsStore, { SettingLevel } from './settings/SettingsStore'; import SettingsStore, { SettingLevel } from './settings/SettingsStore';
@ -119,62 +118,22 @@ 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) { function _setCallListeners(call) {
call.on("error", function(err) { call.on("error", function(err) {
console.error("Call error:", err); console.error("Call error:", err);
if (err.code === 'unknown_devices') { if (
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); MatrixClientPeg.get().getTurnServers().length === 0 &&
SettingsStore.getValue("fallbackICEServerAllowed") === null
Modal.createTrackedDialog('Call Failed', '', QuestionDialog, { ) {
title: _t('Call Failed'), _showICEFallbackPrompt();
description: _t( return;
"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
) {
_showICEFallbackPrompt();
return;
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
title: _t('Call Failed'),
description: err.message,
});
} }
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
title: _t('Call Failed'),
description: err.message,
});
}); });
call.on("hangup", function() { call.on("hangup", function() {
_setCallState(undefined, call.roomId, "ended"); _setCallState(undefined, call.roomId, "ended");

View file

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

View file

@ -14,43 +14,42 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixClientPeg } from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import SettingsStore from './settings/SettingsStore'; import {
import * as sdk from './index'; hideToast as hideBulkUnverifiedSessionsToast,
import { _t } from './languageHandler'; showToast as showBulkUnverifiedSessionsToast
import ToastStore from './stores/ToastStore'; } 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 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 { export default class DeviceListener {
// device IDs for which the user has dismissed the verify toast ('Later')
private dismissed = new Set<string>();
// has the user dismissed any of the various nag toasts to setup encryption on this device?
private dismissedThisDeviceToast = false;
// cache of the key backup info
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.
private ourDeviceIdsAtStart: Set<string> = null;
// The set of device IDs we're currently displaying toasts for
private displayingToastsForDeviceIds = new Set<string>();
static sharedInstance() { static sharedInstance() {
if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener(); if (!window.mx_DeviceListener) window.mx_DeviceListener = new DeviceListener();
return global.mx_DeviceListener; return window.mx_DeviceListener;
}
constructor() {
// device IDs for which the user has dismissed the verify toast ('Later')
this._dismissed = new Set();
// has the user dismissed any of the various nag toasts to setup encryption on this device?
this._dismissedThisDeviceToast = false;
// cache of the key backup info
this._keyBackupInfo = null;
this._keyBackupFetchedAt = 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;
// The set of device IDs we're currently displaying toasts for
this._displayingToastsForDeviceIds = new Set();
} }
start() { start() {
@ -74,12 +73,12 @@ export default class DeviceListener {
MatrixClientPeg.get().removeListener('accountData', this._onAccountData); MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
MatrixClientPeg.get().removeListener('sync', this._onSync); MatrixClientPeg.get().removeListener('sync', this._onSync);
} }
this._dismissed.clear(); this.dismissed.clear();
this._dismissedThisDeviceToast = false; this.dismissedThisDeviceToast = false;
this._keyBackupInfo = null; this.keyBackupInfo = null;
this._keyBackupFetchedAt = null; this.keyBackupFetchedAt = null;
this._ourDeviceIdsAtStart = null; this.ourDeviceIdsAtStart = null;
this._displayingToastsForDeviceIds = new Set(); this.displayingToastsForDeviceIds = new Set();
} }
/** /**
@ -87,29 +86,29 @@ export default class DeviceListener {
* *
* @param {String[]} deviceIds List of device IDs to dismiss notifications for * @param {String[]} deviceIds List of device IDs to dismiss notifications for
*/ */
async dismissUnverifiedSessions(deviceIds) { async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
for (const d of deviceIds) { for (const d of deviceIds) {
this._dismissed.add(d); this.dismissed.add(d);
} }
this._recheck(); this._recheck();
} }
dismissEncryptionSetup() { dismissEncryptionSetup() {
this._dismissedThisDeviceToast = true; this.dismissedThisDeviceToast = true;
this._recheck(); this._recheck();
} }
_ensureDeviceIdsAtStartPopulated() { _ensureDeviceIdsAtStartPopulated() {
if (this._ourDeviceIdsAtStart === null) { if (this.ourDeviceIdsAtStart === null) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
this._ourDeviceIdsAtStart = new Set( this.ourDeviceIdsAtStart = new Set(
cli.getStoredDevicesForUser(cli.getUserId()).map(d => d.deviceId), 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), // 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 // then they are all pre-existing devices, so ignore this and set the
// devicesAtStart list to the devices that we see after the fetch. // 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. // before we download any new ones.
} }
_onDevicesUpdated = (users) => { _onDevicesUpdated = (users: string[]) => {
if (!users.includes(MatrixClientPeg.get().getUserId())) return; if (!users.includes(MatrixClientPeg.get().getUserId())) return;
this._recheck(); this._recheck();
} }
_onDeviceVerificationChanged = (userId) => { _onDeviceVerificationChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return; if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck(); this._recheck();
} }
_onUserTrustStatusChanged = (userId, trustLevel) => { _onUserTrustStatusChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return; if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck(); this._recheck();
} }
@ -163,20 +162,25 @@ export default class DeviceListener {
// & cache the result // & cache the result
async _getKeyBackupInfo() { async _getKeyBackupInfo() {
const now = (new Date()).getTime(); const now = (new Date()).getTime();
if (!this._keyBackupInfo || this._keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) { if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
this._keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this._keyBackupFetchedAt = now; 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() { async _recheck() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if ( if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return;
!SettingsStore.getValue("feature_cross_signing") ||
!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
) return;
if (!cli.isCryptoEnabled()) return; if (!cli.isCryptoEnabled()) return;
// don't recheck until the initial sync is complete: lots of account data events will fire // don't recheck until the initial sync is complete: lots of account data events will fire
@ -186,48 +190,25 @@ export default class DeviceListener {
const crossSigningReady = await cli.isCrossSigningReady(); const crossSigningReady = await cli.isCrossSigningReady();
if (this._dismissedThisDeviceToast) { if (this.dismissedThisDeviceToast || crossSigningReady) {
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); hideSetupEncryptionToast();
} else { } else if (this.shouldShowSetupEncryptionToast()) {
if (!crossSigningReady) { // make sure our keys are finished downloading
// make sure our keys are finished downlaoding await cli.downloadKeys([cli.getUserId()]);
await cli.downloadKeys([cli.getUserId()]); // cross signing isn't enabled - nag to enable it
// cross signing isn't enabled - nag to enable it // There are 3 different toasts for:
// There are 3 different toasts for: if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
if (cli.getStoredCrossSigningForUser(cli.getUserId())) { // Cross-signing on account but this device doesn't trust the master key (verify this session)
// Cross-signing on account but this device doesn't trust the master key (verify this session) showSetupEncryptionToast(SetupKind.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"),
});
} 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"),
});
} 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"),
});
}
}
} else { } else {
// cross-signing is ready, and we don't need to upgrade encryption const backupInfo = await this._getKeyBackupInfo();
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); if (backupInfo) {
// No cross-signing on account but key backup available (upgrade encryption)
showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION);
} else {
// No cross-signing or key backup on account (set up encryption)
showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
}
} }
} }
@ -239,20 +220,20 @@ export default class DeviceListener {
// (technically could just be a boolean: we don't actually // (technically could just be a boolean: we don't actually
// need to remember the device IDs, but for the sake of // need to remember the device IDs, but for the sake of
// symmetry...). // symmetry...).
const oldUnverifiedDeviceIds = new Set(); const oldUnverifiedDeviceIds = new Set<string>();
// Unverified devices that have appeared since then // Unverified devices that have appeared since then
const newUnverifiedDeviceIds = new Set(); const newUnverifiedDeviceIds = new Set<string>();
// as long as cross-signing isn't ready, // as long as cross-signing isn't ready,
// you can't see or dismiss any device toasts // you can't see or dismiss any device toasts
if (crossSigningReady) { if (crossSigningReady) {
const devices = cli.getStoredDevicesForUser(cli.getUserId()); const devices = cli.getStoredDevicesForUser(cli.getUserId());
for (const device of devices) { 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); const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
if (!deviceTrust.isCrossSigningVerified() && !this._dismissed.has(device.deviceId)) { if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) {
if (this._ourDeviceIdsAtStart.has(device.deviceId)) { if (this.ourDeviceIdsAtStart.has(device.deviceId)) {
oldUnverifiedDeviceIds.add(device.deviceId); oldUnverifiedDeviceIds.add(device.deviceId);
} else { } else {
newUnverifiedDeviceIds.add(device.deviceId); newUnverifiedDeviceIds.add(device.deviceId);
@ -263,38 +244,23 @@ export default class DeviceListener {
// Display or hide the batch toast for old unverified sessions // Display or hide the batch toast for old unverified sessions
if (oldUnverifiedDeviceIds.size > 0) { if (oldUnverifiedDeviceIds.size > 0) {
ToastStore.sharedInstance().addOrReplaceToast({ showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
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"),
});
} else { } else {
ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY); hideBulkUnverifiedSessionsToast();
} }
// Show toasts for new unverified devices if they aren't already there // Show toasts for new unverified devices if they aren't already there
for (const deviceId of newUnverifiedDeviceIds) { for (const deviceId of newUnverifiedDeviceIds) {
ToastStore.sharedInstance().addOrReplaceToast({ showUnverifiedSessionsToast(deviceId);
key: toastKey(deviceId),
title: _t("New login. Was this you?"),
icon: "verification_warning",
props: { deviceId },
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
});
} }
// ...and hide any we don't need any more // ...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)) { 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 {MatrixClientPeg} from './MatrixClientPeg';
import GroupStore from './stores/GroupStore'; import GroupStore from './stores/GroupStore';
import {allSettled} from "./utils/promise"; import {allSettled} from "./utils/promise";
import StyledCheckbox from './components/views/elements/StyledCheckbox';
export function showGroupInviteDialog(groupId) { export function showGroupInviteDialog(groupId) {
return new Promise((resolve, reject) => { 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>{ _t("Which rooms would you like to add to this community?") }</div>
</div>; </div>;
const checkboxContainer = <label className="mx_GroupAddressPicker_checkboxContainer"> const checkboxContainer = <StyledCheckbox
<input type="checkbox" onChange={onCheckboxClicked} /> className="mx_GroupAddressPicker_checkboxContainer"
<div> onChange={onCheckboxClicked}
{ _t("Show these rooms to non-members on the community page and room list?") } >
</div> { _t("Show these rooms to non-members on the community page and room list?") }
</label>; </StyledCheckbox>;
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, { Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
title: _t("Add rooms to the community"), title: _t("Add rooms to the community"),
description: description, description: description,
extraNode: checkboxContainer, extraNode: checkboxContainer,
placeholder: _t("Room name or alias"), placeholder: _t("Room name or address"),
button: _t("Add to community"), button: _t("Add to community"),
pickerType: 'room', pickerType: 'room',
validAddressTypes: ['mx-room-id'], 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 {Mjolnir} from "./mjolnir/Mjolnir";
import DeviceListener from "./DeviceListener"; import DeviceListener from "./DeviceListener";
import {Jitsi} from "./widgets/Jitsi"; 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 * 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); 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"); console.warn("Cannot log in with token: can't determine HS URL to use");
return Promise.resolve(false); return Promise.resolve(false);
} }
return sendLoginRequest( return sendLoginRequest(
queryParams.homeserver, homeserver,
queryParams.identityServer, identityServer,
"m.login.token", { "m.login.token", {
token: queryParams.loginToken, token: queryParams.loginToken,
initial_device_display_name: defaultDeviceDisplayName, initial_device_display_name: defaultDeviceDisplayName,
@ -256,8 +259,8 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
* @returns {Object} Information about the session - see implementation for variables. * @returns {Object} Information about the session - see implementation for variables.
*/ */
export function getLocalStorageSessionVars() { export function getLocalStorageSessionVars() {
const hsUrl = localStorage.getItem("mx_hs_url"); const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
const isUrl = localStorage.getItem("mx_is_url"); const isUrl = localStorage.getItem(ID_SERVER_URL_KEY);
const accessToken = localStorage.getItem("mx_access_token"); const accessToken = localStorage.getItem("mx_access_token");
const userId = localStorage.getItem("mx_user_id"); const userId = localStorage.getItem("mx_user_id");
const deviceId = localStorage.getItem("mx_device_id"); const deviceId = localStorage.getItem("mx_device_id");
@ -298,6 +301,8 @@ async function _restoreFromLocalStorage(opts) {
return false; return false;
} }
const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId);
console.log(`Restoring session for ${userId}`); console.log(`Restoring session for ${userId}`);
await _doSetLoggedIn({ await _doSetLoggedIn({
userId: userId, userId: userId,
@ -306,6 +311,7 @@ async function _restoreFromLocalStorage(opts) {
homeserverUrl: hsUrl, homeserverUrl: hsUrl,
identityServerUrl: isUrl, identityServerUrl: isUrl,
guest: isGuest, guest: isGuest,
pickleKey: pickleKey,
}, false); }, false);
return true; return true;
} else { } else {
@ -348,9 +354,13 @@ async function _handleLoadSessionFailure(e) {
* *
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started * @returns {Promise} promise which resolves to the new MatrixClient once it has been started
*/ */
export function setLoggedIn(credentials) { export async function setLoggedIn(credentials) {
stopMatrixClient(); 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 { } class AbortLoginAndRebuildStorage extends Error { }
function _persistCredentialsToLocalStorage(credentials) { function _persistCredentialsToLocalStorage(credentials) {
localStorage.setItem("mx_hs_url", credentials.homeserverUrl); localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
if (credentials.identityServerUrl) { 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_user_id", credentials.userId);
localStorage.setItem("mx_access_token", credentials.accessToken); localStorage.setItem("mx_access_token", credentials.accessToken);
@ -516,7 +526,9 @@ export function logout() {
} }
_isLoggingOut = true; _isLoggingOut = true;
MatrixClientPeg.get().logout().then(onLoggedOut, const client = MatrixClientPeg.get();
PlatformPeg.get().destroyPickleKey(client.getUserId(), client.getDeviceId());
client.logout().then(onLoggedOut,
(err) => { (err) => {
// Just throwing an error here is going to be very unhelpful // Just throwing an error here is going to be very unhelpful
// if you're trying to log out because your server's down and // if you're trying to log out because your server's down and
@ -575,10 +587,12 @@ async function startMatrixClient(startSyncing=true) {
// to work). // to work).
dis.dispatch({action: 'will_start_client'}, true); dis.dispatch({action: 'will_start_client'}, true);
// reset things first just in case
TypingStore.sharedInstance().reset();
ToastStore.sharedInstance().reset();
Notifier.start(); Notifier.start();
UserActivity.sharedInstance().start(); UserActivity.sharedInstance().start();
TypingStore.sharedInstance().reset(); // just in case
ToastStore.sharedInstance().reset();
DMRoomMap.makeShared().start(); DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching(); IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start(); ActiveWidgetStore.start();
@ -608,7 +622,7 @@ async function startMatrixClient(startSyncing=true) {
} }
// Now that we have a MatrixClientPeg, update the Jitsi info // 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 // dispatch that we finished starting up to wire up any other bits
// of the matrix client that cannot be set prior to starting up. // of the matrix client that cannot be set prior to starting up.

View file

@ -17,8 +17,8 @@ See the License for the specific language governing permissions and
limitations under the License. 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 * as utils from 'matrix-js-sdk/src/utils';
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
import {EventTimelineSet} from 'matrix-js-sdk/src/models/event-timeline-set'; import {EventTimelineSet} from 'matrix-js-sdk/src/models/event-timeline-set';
@ -34,37 +34,26 @@ import IdentityAuthClient from './IdentityAuthClient';
import { crossSigningCallbacks } from './CrossSigningManager'; import { crossSigningCallbacks } from './CrossSigningManager';
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
interface MatrixClientCreds { export interface IMatrixClientCreds {
homeserverUrl: string, homeserverUrl: string,
identityServerUrl: string, identityServerUrl: string,
userId: string, userId: string,
deviceId: string, deviceId: string,
accessToken: string, accessToken: string,
guest: boolean, guest: boolean,
pickleKey?: string,
} }
/** // TODO: Move this to the js-sdk
* Wrapper object for handling the js-sdk Matrix Client object in the react-sdk export interface IOpts {
* Handles the creation/initialisation of client objects. initialSyncLimit?: number;
* This module provides a singleton instance of this class so the 'current' pendingEventOrdering?: "detached" | "chronological";
* Matrix Client object is available easily. lazyLoadMembers?: boolean;
*/ clientWellKnownPollPeriod?: number;
class _MatrixClientPeg { }
constructor() {
this.matrixClient = null;
this._justRegisteredUserId = null;
// These are the default options used when when the export interface IMatrixClientPeg {
// client is started in 'start'. These can be altered opts: IOpts;
// 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;
}
/** /**
* Sets the script href passed to the IndexedDB web worker * Sets the script href passed to the IndexedDB web worker
@ -73,19 +62,23 @@ class _MatrixClientPeg {
* *
* @param {string} script href to the script to be passed to the web worker * @param {string} script href to the script to be passed to the web worker
*/ */
setIndexedDbWorkerScript(script) { setIndexedDbWorkerScript(script: string): void;
createMatrixClient.indexedDbWorkerScript = script;
}
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() { get(): MatrixClient;
this.matrixClient = null; 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 * 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 * @param {string} uid The user ID of the user we've just registered
*/ */
setJustRegisteredUserId(uid) { setJustRegisteredUserId(uid: string): void;
this._justRegisteredUserId = uid;
}
/** /**
* Returns true if the current user has just been registered by this * 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 * @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 ( return (
this.matrixClient && this.matrixClient &&
this.matrixClient.credentials.userId === this._justRegisteredUserId this.matrixClient.credentials.userId === this.justRegisteredUserId
); );
} }
/* public replaceUsingCreds(creds: IMatrixClientCreds): void {
* Replace this MatrixClientPeg's client with a client instance that has this.currentClientCreds = creds;
* homeserver / identity server URLs and active credentials this.createClient(creds);
*/
replaceUsingCreds(creds: MatrixClientCreds) {
this._currentClientCreds = creds;
this._createClient(creds);
} }
async assign() { public async assign(): Promise<any> {
for (const dbType of ['indexeddb', 'memory']) { for (const dbType of ['indexeddb', 'memory']) {
try { try {
const promise = this.matrixClient.store.startup(); const promise = this.matrixClient.store.startup();
@ -132,7 +173,7 @@ class _MatrixClientPeg {
if (dbType === 'indexeddb') { if (dbType === 'indexeddb') {
console.error('Error starting matrixclient store - falling back to memory store', err); console.error('Error starting matrixclient store - falling back to memory store', err);
this.matrixClient.store = new MemoryStore({ this.matrixClient.store = new MemoryStore({
localStorage: global.localStorage, localStorage: localStorage,
}); });
} else { } else {
console.error('Failed to start memory store!', err); 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 // The js-sdk found a crypto DB too new for it to use
const CryptoStoreTooNewDialog = const CryptoStoreTooNewDialog =
sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog"); sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog");
Modal.createDialog(CryptoStoreTooNewDialog, { Modal.createDialog(CryptoStoreTooNewDialog);
host: window.location.host,
});
} }
// this can happen for a number of reasons, the most likely being // this can happen for a number of reasons, the most likely being
// that the olm library was missing. It's not fatal. // 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 // the react sdk doesn't work without this, so don't allow
opts.pendingEventOrdering = "detached"; opts.pendingEventOrdering = "detached";
opts.lazyLoadMembers = true; opts.lazyLoadMembers = true;
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
// Connect the matrix client to the dispatcher and setting handlers // Connect the matrix client to the dispatcher and setting handlers
MatrixActionCreators.start(this.matrixClient); MatrixActionCreators.start(this.matrixClient);
@ -179,7 +219,7 @@ class _MatrixClientPeg {
return opts; return opts;
} }
async start() { public async start(): Promise<any> {
const opts = await this.assign(); const opts = await this.assign();
console.log(`MatrixClientPeg: really starting MatrixClient`); console.log(`MatrixClientPeg: really starting MatrixClient`);
@ -187,7 +227,7 @@ class _MatrixClientPeg {
console.log(`MatrixClientPeg: MatrixClient started`); console.log(`MatrixClientPeg: MatrixClient started`);
} }
getCredentials(): MatrixClientCreds { public getCredentials(): IMatrixClientCreds {
return { return {
homeserverUrl: this.matrixClient.baseUrl, homeserverUrl: this.matrixClient.baseUrl,
identityServerUrl: this.matrixClient.idBaseUrl, identityServerUrl: this.matrixClient.idBaseUrl,
@ -198,12 +238,7 @@ class _MatrixClientPeg {
}; };
} }
/* public getHomeserverName(): string {
* 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() {
const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId); const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId);
if (matches === null || matches.length < 1) { if (matches === null || matches.length < 1) {
throw new Error("Failed to derive homeserver name from user ID!"); throw new Error("Failed to derive homeserver name from user ID!");
@ -211,13 +246,15 @@ class _MatrixClientPeg {
return matches[1]; return matches[1];
} }
_createClient(creds: MatrixClientCreds) { private createClient(creds: IMatrixClientCreds): void {
// TODO: Make these opts typesafe with the js-sdk
const opts = { const opts = {
baseUrl: creds.homeserverUrl, baseUrl: creds.homeserverUrl,
idBaseUrl: creds.identityServerUrl, idBaseUrl: creds.identityServerUrl,
accessToken: creds.accessToken, accessToken: creds.accessToken,
userId: creds.userId, userId: creds.userId,
deviceId: creds.deviceId, deviceId: creds.deviceId,
pickleKey: creds.pickleKey,
timelineSupport: true, timelineSupport: true,
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false), forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'),
@ -228,9 +265,9 @@ class _MatrixClientPeg {
], ],
unstableClientRelationAggregation: true, unstableClientRelationAggregation: true,
identityServer: new IdentityAuthClient(), identityServer: new IdentityAuthClient(),
cryptoCallbacks: {},
}; };
opts.cryptoCallbacks = {};
// These are always installed regardless of the labs flag so that // These are always installed regardless of the labs flag so that
// cross-signing features can toggle on without reloading and also be // cross-signing features can toggle on without reloading and also be
// accessed immediately after login. // accessed immediately after login.
@ -253,8 +290,8 @@ class _MatrixClientPeg {
} }
} }
if (!global.mxMatrixClientPeg) { if (!window.mxMatrixClientPeg) {
global.mxMatrixClientPeg = new _MatrixClientPeg(); 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 { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
import {
hideToast as hideNotificationsToast,
} from "./toasts/DesktopNotificationsToast";
/* /*
* Dispatches: * Dispatches:
@ -278,12 +281,7 @@ const Notifier = {
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden); Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
// XXX: why are we dispatching this here? hideNotificationsToast();
// this is nothing to do with notifier_enabled
dis.dispatch({
action: "notifier_enabled",
value: this.isEnabled(),
});
// update the info to localStorage for persistent settings // update the info to localStorage for persistent settings
if (persistent && global.localStorage) { if (persistent && global.localStorage) {

View file

@ -84,8 +84,14 @@ export default class PasswordReset {
try { try {
await this.client.setPassword({ 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", 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, threepid_creds: creds,
threepidCreds: creds,
}, this.password); }, this.password);
} catch (err) { } catch (err) {
if (err.httpStatus === 401) { if (err.httpStatus === 401) {

View file

@ -450,8 +450,8 @@ export const Commands = [
new Command({ new Command({
command: 'join', command: 'join',
aliases: ['j', 'goto'], aliases: ['j', 'goto'],
args: '<room-alias>', args: '<room-address>',
description: _td('Joins room with given alias'), description: _td('Joins room with given address'),
runFn: function(_, args) { runFn: function(_, args) {
if (args) { if (args) {
// Note: we support 2 versions of this command. The first is // Note: we support 2 versions of this command. The first is
@ -562,7 +562,7 @@ export const Commands = [
}), }),
new Command({ new Command({
command: 'part', command: 'part',
args: '[<room-alias>]', args: '[<room-address>]',
description: _td('Leave room'), description: _td('Leave room'),
runFn: function(roomId, args) { runFn: function(roomId, args) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -594,7 +594,7 @@ export const Commands = [
} }
if (targetRoomId) break; 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 PropTypes from 'prop-types';
import {_t, _td} from '../../../../languageHandler'; import {_t, _td} from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager'; import { accessSecretStorage } from '../../../../CrossSigningManager';
import SettingsStore from '../../../../settings/SettingsStore';
import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import {copyNode} from "../../../../utils/strings"; import {copyNode} from "../../../../utils/strings";
import PassphraseField from "../../../../components/views/auth/PassphraseField"; import PassphraseField from "../../../../components/views/auth/PassphraseField";
@ -67,10 +66,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
async componentDidMount() { async componentDidMount() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const secureSecretStorage = ( const secureSecretStorage = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
SettingsStore.getValue("feature_cross_signing") &&
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
);
this.setState({ secureSecretStorage }); this.setState({ secureSecretStorage });
// If we're using secret storage, skip ahead to the backing up step, as // 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'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let matchText; let matchText;
let changeText;
if (this.state.passPhraseConfirm === this.state.passPhrase) { if (this.state.passPhraseConfirm === this.state.passPhrase) {
matchText = _t("That matches!"); matchText = _t("That matches!");
changeText = _t("Use a different passphrase?");
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
// only tell them they're wrong if they've actually gone wrong. // only tell them they're wrong if they've actually gone wrong.
// Security concious readers will note that if you left riot-web unattended // 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 // Note that not having typed anything at all will not hit this clause and
// fall through so empty box === no hint. // fall through so empty box === no hint.
matchText = _t("That doesn't match."); matchText = _t("That doesn't match.");
changeText = _t("Go back to set it again.");
} }
let passPhraseMatch = null; let passPhraseMatch = null;
@ -303,7 +302,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<div>{matchText}</div> <div>{matchText}</div>
<div> <div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}> <AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{_t("Go back to set it again.")} {changeText}
</AccessibleButton> </AccessibleButton>
</div> </div>
</div>; </div>;

View file

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

View file

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

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, {forwardRef} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
/* These were earlier stateless functional components but had to be converted /* These were earlier stateless functional components but had to be converted
@ -30,50 +30,37 @@ interface ITextualCompletionProps {
className?: string; className?: string;
} }
export class TextualCompletion extends React.PureComponent<ITextualCompletionProps> { export const TextualCompletion = forwardRef<ITextualCompletionProps, any>((props, ref) => {
render() { const {title, subtitle, description, className, ...restProps} = props;
const { return (
title, <div {...restProps}
subtitle, className={classNames('mx_Autocomplete_Completion_block', className)}
description, role="option"
className, ref={ref}
...restProps >
} = this.props; <span className="mx_Autocomplete_Completion_title">{ title }</span>
return ( <span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
<div className={classNames('mx_Autocomplete_Completion_block', className)} role="option" {...restProps}> <span className="mx_Autocomplete_Completion_description">{ description }</span>
<span className="mx_Autocomplete_Completion_title">{ title }</span> </div>
<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 { export const PillCompletion = forwardRef<IPillCompletionProps, any>((props, ref) => {
title?: string; const {title, subtitle, description, className, children, ...restProps} = props;
subtitle?: string; return (
description?: string; <div {...restProps}
initialComponent?: React.ReactNode, className={classNames('mx_Autocomplete_Completion_pill', className)}
className?: string; role="option"
} ref={ref}
>
export class PillCompletion extends React.PureComponent<IPillCompletionProps> { { children }
render() { <span className="mx_Autocomplete_Completion_title">{ title }</span>
const { <span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
title, <span className="mx_Autocomplete_Completion_description">{ description }</span>
subtitle, </div>
description, );
initialComponent, });
className,
...restProps
} = this.props;
return (
<div className={classNames('mx_Autocomplete_Completion_pill', className)} role="option" {...restProps}>
{ initialComponent }
<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() { constructor() {
super(EMOJI_REGEX); super(EMOJI_REGEX);
this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTNAMES, {
keys: ['emoji.emoticon', 'shortname'], keys: ['emoji.emoticon', 'shortname'],
funcs: [ funcs: [
(o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases (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 { return {
completion: unicode, completion: unicode,
component: ( component: (
<PillCompletion title={shortname} aria-label={unicode} initialComponent={ <PillCompletion title={shortname} aria-label={unicode}>
<span style={{maxWidth: '1em'}}>{ unicode }</span> <span>{ unicode }</span>
} /> </PillCompletion>
), ),
range, range,
}; };

View file

@ -48,7 +48,9 @@ export default class NotifProvider extends AutocompleteProvider {
type: "at-room", type: "at-room",
suffix: ' ', suffix: ' ',
component: ( 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, range,
}]; }];

View file

@ -45,7 +45,7 @@ interface IOptions<T extends {}> {
* @param {function[]} options.funcs List of functions that when called with the * @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 * 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 _options: IOptions<T>;
private _keys: IOptions<T>["keys"]; private _keys: IOptions<T>["keys"];
private _funcs: Required<IOptions<T>["funcs"]>; private _funcs: Required<IOptions<T>["funcs"]>;
@ -75,7 +75,11 @@ export default class QueryMatcher<T> {
this._items = new Map(); this._items = new Map();
for (const object of objects) { 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) { for (const f of this._funcs) {
keyValues.push(f(object)); keyValues.push(f(object));

View file

@ -103,7 +103,9 @@ export default class RoomProvider extends AutocompleteProvider {
suffix: ' ', suffix: ' ',
href: makeRoomPermalink(room.displayedAlias), href: makeRoomPermalink(room.displayedAlias),
component: ( 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, range,
}; };

View file

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

View file

@ -92,7 +92,7 @@ const CategoryRoomList = createReactClass({
Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, { Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
title: _t('Add rooms to the community summary'), title: _t('Add rooms to the community summary'),
description: _t("Which rooms would you like to add to this 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"), button: _t("Add to summary"),
pickerType: 'room', pickerType: 'room',
validAddressTypes: ['mx-room-id'], validAddressTypes: ['mx-room-id'],

View file

@ -26,7 +26,7 @@ import * as VectorConferenceHandler from '../../VectorConferenceHandler';
import SettingsStore from '../../settings/SettingsStore'; import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler"; import {_t} from "../../languageHandler";
import Analytics from "../../Analytics"; import Analytics from "../../Analytics";
import RoomList2 from "../views/rooms/RoomList2"; import {Action} from "../../dispatcher/actions";
const LeftPanel = createReactClass({ const LeftPanel = createReactClass({
@ -198,7 +198,7 @@ const LeftPanel = createReactClass({
onSearchCleared: function(source) { onSearchCleared: function(source) {
if (source === "keyboard") { if (source === "keyboard") {
dis.dispatch({action: 'focus_composer'}); dis.fire(Action.FocusComposer);
} }
this.setState({searchExpanded: false}); this.setState({searchExpanded: false});
}, },
@ -274,28 +274,15 @@ const LeftPanel = createReactClass({
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />); breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
} }
let roomList = null; const roomList = <RoomList
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { onKeyDown={this._onKeyDown}
roomList = <RoomList2 onFocus={this._onFocus}
onKeyDown={this._onKeyDown} onBlur={this._onBlur}
resizeNotifier={this.props.resizeNotifier} ref={this.collectRoomList}
collapsed={this.props.collapsed} resizeNotifier={this.props.resizeNotifier}
searchFilter={this.state.searchFilter} collapsed={this.props.collapsed}
ref={this.collectRoomList} searchFilter={this.state.searchFilter}
onFocus={this._onFocus} ConferenceHandler={VectorConferenceHandler} />;
onBlur={this._onBlur}
/>;
} else {
roomList = <RoomList
onKeyDown={this._onKeyDown}
onFocus={this._onFocus}
onBlur={this._onBlur}
ref={this.collectRoomList}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />;
}
return ( return (
<div className={containerClasses}> <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 * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import sessionStore from '../../stores/SessionStore'; import sessionStore from '../../stores/SessionStore';
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg'; import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import TagOrderActions from '../../actions/TagOrderActions'; import TagOrderActions from '../../actions/TagOrderActions';
@ -43,6 +43,17 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy"; import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
import { DefaultTagID } from "../../stores/room-list/models"; 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) // We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity. // so each pinned message may trigger a request. Limit the number per room for sanity.
// NB. this is just for server notices rather than pinned messages in general. // NB. this is just for server notices rather than pinned messages in general.
@ -57,7 +68,7 @@ function canElementReceiveInput(el) {
interface IProps { interface IProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
onRegistered: (credentials: MatrixClientCreds) => Promise<MatrixClient>; onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
viaServers?: string[]; viaServers?: string[];
hideToSRUsers: boolean; hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
@ -65,10 +76,6 @@ interface IProps {
initialEventPixelOffset: number; initialEventPixelOffset: number;
leftDisabled: boolean; leftDisabled: boolean;
rightDisabled: boolean; rightDisabled: boolean;
showCookieBar: boolean;
hasNewVersion: boolean;
userHasGeneratedPassword: boolean;
showNotifierToolbar: boolean;
page_type: string; page_type: string;
autoJoin: boolean; autoJoin: boolean;
thirdPartyInvite?: object; thirdPartyInvite?: object;
@ -76,7 +83,6 @@ interface IProps {
currentRoomId: string; currentRoomId: string;
ConferenceHandler?: object; ConferenceHandler?: object;
collapseLhs: boolean; collapseLhs: boolean;
checkingForUpdate: boolean;
config: { config: {
piwik: { piwik: {
policyUrl: string; policyUrl: string;
@ -86,19 +92,26 @@ interface IProps {
currentUserId?: string; currentUserId?: string;
currentGroupId?: string; currentGroupId?: string;
currentGroupIsNew?: boolean; currentGroupIsNew?: boolean;
version?: string;
newVersion?: string;
newVersionReleaseNotes?: string;
} }
interface IUsageLimit {
limit_type: "monthly_active_user" | string;
admin_contact?: string;
}
interface IState { interface IState {
mouseDown?: { mouseDown?: {
x: number; x: number;
y: number; y: number;
}; };
syncErrorData: any; syncErrorData?: {
error: {
data: IUsageLimit;
errcode: string;
};
};
usageLimitEventContent?: IUsageLimit;
useCompactLayout: boolean; useCompactLayout: boolean;
serverNoticeEvents: MatrixEvent[];
userHasGeneratedPassword: boolean;
} }
/** /**
@ -141,11 +154,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
this.state = { this.state = {
mouseDown: undefined, mouseDown: undefined,
syncErrorData: undefined, syncErrorData: undefined,
userHasGeneratedPassword: false,
// use compact timeline view // use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'), useCompactLayout: SettingsStore.getValue('useCompactLayout'),
// any currently active server notice events
serverNoticeEvents: [],
}; };
// stash the MatrixClient in case we log out before we are unmounted // 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(); 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() { componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false); document.removeEventListener('keydown', this._onNativeKeyDown, false);
this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("accountData", this.onAccountData);
@ -220,9 +218,11 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
}; };
_setStateFromSessionStore = () => { _setStateFromSessionStore = () => {
this.setState({ if (this._sessionStore.getCachedPassword()) {
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), showSetPasswordToast();
}); } else {
hideSetPasswordToast();
}
}; };
_createResizer() { _createResizer() {
@ -294,6 +294,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents(); 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 () => { _updateServerNoticeEvents = async () => {
const roomLists = RoomListStoreTempProxy.getRoomLists(); const roomLists = RoomListStoreTempProxy.getRoomLists();
if (!roomLists[DefaultTagID.ServerNotice]) return []; if (!roomLists[DefaultTagID.ServerNotice]) return [];
const pinnedEvents = []; const events = [];
for (const room of roomLists[DefaultTagID.ServerNotice]) { for (const room of roomLists[DefaultTagID.ServerNotice]) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); 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) { for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
const event = timeline.getEvents().find(ev => ev.getId() === eventId); 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) => { _onPaste = (ev) => {
@ -338,7 +360,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
// refocusing during a paste event will make the // refocusing during a paste event will make the
// paste end up in the newly focused element, // paste end up in the newly focused element,
// so dispatch synchronously before paste happens // 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)) { if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
// synchronous dispatch so we focus before key generates input // synchronous dispatch so we focus before key generates input
dis.dispatch({action: 'focus_composer'}, true); dis.fire(Action.FocusComposer, true);
ev.stopPropagation(); ev.stopPropagation();
// we should *not* preventDefault() here as // we should *not* preventDefault() here as
// that would prevent typing in the now-focussed composer // 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 GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups'); const MyGroups = sdk.getComponent('structures.MyGroups');
const ToastContainer = sdk.getComponent('structures.ToastContainer'); const ToastContainer = sdk.getComponent('structures.ToastContainer');
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; let pageElement;
@ -648,50 +664,25 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
break; 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'; let bodyClasses = 'mx_MatrixChat';
if (topBar) {
bodyClasses += ' mx_MatrixChat_toolbarShowing';
}
if (this.state.useCompactLayout) { if (this.state.useCompactLayout) {
bodyClasses += ' mx_MatrixChat_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 ( return (
<MatrixClientContext.Provider value={this._matrixClient}> <MatrixClientContext.Provider value={this._matrixClient}>
<div <div
@ -702,15 +693,10 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
onMouseDown={this._onMouseDown} onMouseDown={this._onMouseDown}
onMouseUp={this._onMouseUp} onMouseUp={this._onMouseUp}
> >
{ topBar }
<ToastContainer /> <ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}> <DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._resizeContainer} className={bodyClasses}> <div ref={this._resizeContainer} className={bodyClasses}>
<LeftPanel { leftPanel }
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
<ResizeHandle /> <ResizeHandle />
{ pageElement } { pageElement }
</div> </div>

View file

@ -49,7 +49,6 @@ import PageTypes from '../../PageTypes';
import { getHomePageUrl } from '../../utils/pages'; import { getHomePageUrl } from '../../utils/pages';
import createRoom from "../../createRoom"; import createRoom from "../../createRoom";
import KeyRequestHandler from '../../KeyRequestHandler';
import { _t, getCurrentLanguage } from '../../languageHandler'; import { _t, getCurrentLanguage } from '../../languageHandler';
import SettingsStore, { SettingLevel } from "../../settings/SettingsStore"; import SettingsStore, { SettingLevel } from "../../settings/SettingsStore";
import ThemeController from "../../settings/controllers/ThemeController"; import ThemeController from "../../settings/controllers/ThemeController";
@ -59,8 +58,8 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
import DMRoomMap from '../../utils/DMRoomMap'; import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs'; import { countRoomsWithNotif } from '../../RoomNotifs';
import { ThemeWatcher } from "../../theme"; import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import { FontWatcher } from '../../FontWatcher'; import { FontWatcher } from '../../settings/watchers/FontWatcher';
import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer, IDeferred } from "../../utils/promise"; import { defer, IDeferred } from "../../utils/promise";
import ToastStore from "../../stores/ToastStore"; import ToastStore from "../../stores/ToastStore";
@ -68,6 +67,11 @@ import * as StorageManager from "../../utils/StorageManager";
import type LoggedInViewType from "./LoggedInView"; import type LoggedInViewType from "./LoggedInView";
import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload"; import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload";
import { Action } from "../../dispatcher/actions"; 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 */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -169,12 +173,6 @@ interface IState {
leftDisabled: boolean; leftDisabled: boolean;
middleDisabled: boolean; middleDisabled: boolean;
// the right panel's disabled state is tracked in its store. // 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 // Parameters used in the registration dance with the IS
register_client_secret?: string; register_client_secret?: string;
register_session_id?: string; register_session_id?: string;
@ -184,7 +182,6 @@ interface IState {
hideToSRUsers: boolean; hideToSRUsers: boolean;
syncError?: Error; syncError?: Error;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
showNotifierToolbar: boolean;
serverConfig?: ValidatedServerConfig; serverConfig?: ValidatedServerConfig;
ready: boolean; ready: boolean;
thirdPartyInvite?: object; thirdPartyInvite?: object;
@ -228,17 +225,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
leftDisabled: false, leftDisabled: false,
middleDisabled: false, middleDisabled: false,
hasNewVersion: false,
newVersionReleaseNotes: null,
checkingForUpdate: null,
showCookieBar: false,
hideToSRUsers: false, hideToSRUsers: false,
syncError: null, // If the current syncing status is ERROR, the error object, otherwise null. syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
resizeNotifier: new ResizeNotifier(), resizeNotifier: new ResizeNotifier(),
showNotifierToolbar: false,
ready: 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")) { if (SettingsStore.getValue("analyticsOptIn")) {
Analytics.enable(); Analytics.enable();
} }
@ -363,7 +347,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Analytics.trackPageChange(durationMs); Analytics.trackPageChange(durationMs);
} }
if (this.focusComposer) { if (this.focusComposer) {
dis.dispatch({action: 'focus_composer'}); dis.fire(Action.FocusComposer);
this.focusComposer = false; this.focusComposer = false;
} }
} }
@ -686,9 +670,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
dis.dispatch({action: 'view_my_groups'}); dis.dispatch({action: 'view_my_groups'});
} }
break; break;
case 'notifier_enabled':
this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()});
break;
case 'hide_left_panel': case 'hide_left_panel':
this.setState({ this.setState({
collapseLhs: true, collapseLhs: true,
@ -736,15 +717,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'client_started': case 'client_started':
this.onClientStarted(); this.onClientStarted();
break; 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': case 'send_event':
this.onSendEvent(payload.room_id, payload.event); this.onSendEvent(payload.room_id, payload.event);
break; break;
@ -761,19 +733,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'accept_cookies': case 'accept_cookies':
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true); SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true);
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false); SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
hideAnalyticsToast();
this.setState({
showCookieBar: false,
});
Analytics.enable(); Analytics.enable();
break; break;
case 'reject_cookies': case 'reject_cookies':
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false); SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false);
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false); SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
hideAnalyticsToast();
this.setState({
showCookieBar: false,
});
break; 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; 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({ this.setState({
view: Views.LOGGED_IN,
currentGroupId: groupId, currentGroupId: groupId,
currentGroupIsNew: payload.group_is_new, currentGroupIsNew: payload.group_is_new,
}); });
@ -1251,6 +1228,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
StorageManager.tryPersistStorage(); 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() { private showScreenAfterLogin() {
@ -1378,10 +1359,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.firstSyncComplete = true; this.firstSyncComplete = true;
this.firstSyncPromise.resolve(); this.firstSyncPromise.resolve();
dis.dispatch({action: 'focus_composer'}); if (Notifier.shouldShowToolbar()) {
showNotificationsToast();
}
dis.fire(Action.FocusComposer);
this.setState({ this.setState({
ready: true, ready: true,
showNotifierToolbar: Notifier.shouldShowToolbar(),
}); });
}); });
cli.on('Call.incoming', function(call) { 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("Session.logged_out", () => dft.stop());
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err)); 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) => { cli.on("Room", (room) => {
if (MatrixClientPeg.get().isCryptoEnabled()) { if (MatrixClientPeg.get().isCryptoEnabled()) {
const blacklistEnabled = SettingsStore.getValueAt( const blacklistEnabled = SettingsStore.getValueAt(
@ -1540,13 +1514,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
cli.on("crypto.verification.request", request => { 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) { if (request.verifier) {
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog"); const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
@ -1559,7 +1526,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
icon: "verification", icon: "verification",
props: {request}, props: {request},
component: sdk.getComponent("toasts.VerificationRequestToast"), 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 // be aware of will be signalled through the room shield
// changing colour. More advanced behaviour will come once // changing colour. More advanced behaviour will come once
// we implement more settings. // we implement more settings.
cli.setGlobalErrorOnUnknownDevices( cli.setGlobalErrorOnUnknownDevices(false);
!SettingsStore.getValue("feature_cross_signing"),
);
} }
} }
@ -1833,16 +1798,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.showScreen("settings"); 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) { onSendEvent(roomId: string, event: MatrixEvent) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (!cli) { 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. // whether cross-signing has been set up on the account.
const masterKeyInStorage = !!cli.getAccountData("m.cross_signing.master"); const masterKeyInStorage = !!cli.getAccountData("m.cross_signing.master");
if (masterKeyInStorage) { 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 }); this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
} else if ( } else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
SettingsStore.getValue("feature_cross_signing") &&
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
) {
// This will only work if the feature is set to 'enable' in the config,
// since it's too early in the lifecycle for users to have turned the
// labs flag on.
this.setStateForNewView({ view: Views.E2E_SETUP }); this.setStateForNewView({ view: Views.E2E_SETUP });
} else { } else {
this.onLoggedIn(); this.onLoggedIn();
@ -1978,7 +1924,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// console.log(`Rendering MatrixChat with view ${this.state.view}`); // console.log(`Rendering MatrixChat with view ${this.state.view}`);
let fragmentAfterLogin = ""; 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}`; fragmentAfterLogin = `/${this.props.initialScreenAfterLogin.screen}`;
} }
@ -2037,7 +1986,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onCloseAllSettings={this.onCloseAllSettings} onCloseAllSettings={this.onCloseAllSettings}
onRegistered={this.onRegistered} onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId} currentRoomId={this.state.currentRoomId}
showCookieBar={this.state.showCookieBar}
/> />
); );
} else { } else {

View file

@ -34,6 +34,30 @@ import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResiz
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message']; 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'; 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. /* (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 // whether to show reactions for an event
showReactions: PropTypes.bool, showReactions: PropTypes.bool,
// whether to use the irc layout
useIRCLayout: PropTypes.bool,
}; };
// Force props to be loaded for useIRCLayout // 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 // display 'ghost' read markers that are animating away
ghostReadMarkers: [], ghostReadMarkers: [],
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")),
}; };
// opaque readreceipt info for each userId; used by ReadReceiptMarker // opaque readreceipt info for each userId; used by ReadReceiptMarker
@ -172,8 +198,6 @@ export default class MessagePanel extends React.Component {
this._showTypingNotificationsWatcherRef = this._showTypingNotificationsWatcherRef =
SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange);
this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange);
} }
componentDidMount() { componentDidMount() {
@ -183,7 +207,6 @@ export default class MessagePanel extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this._isMounted = false; this._isMounted = false;
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef); SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
SettingsStore.unwatchSetting(this._layoutWatcherRef);
} }
componentDidUpdate(prevProps, prevState) { 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 */ /* get the DOM node representing the given event */
getNodeForEventId(eventId) { getNodeForEventId(eventId) {
if (!this.eventNodes) { if (!this.eventNodes) {
@ -527,39 +539,6 @@ export default class MessagePanel extends React.Component {
const isEditing = this.props.editState && const isEditing = this.props.editState &&
this.props.editState.getEvent().getId() === mxEv.getId(); 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 // local echoes have a fake date, which could even be yesterday. Treat them
// as 'today' for the date separators. // 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? // 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>; const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
ret.push(dateSeparator); ret.push(dateSeparator);
continuation = false;
} }
// is this a continuation of the previous message?
const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv);
const eventId = mxEv.getId(); const eventId = mxEv.getId();
const highlight = (eventId === this.props.highlightedEventId); const highlight = (eventId === this.props.highlightedEventId);
@ -614,7 +596,7 @@ export default class MessagePanel extends React.Component {
isSelectedEvent={highlight} isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent} getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions} showReactions={this.props.showReactions}
useIRCLayout={this.state.useIRCLayout} useIRCLayout={this.props.useIRCLayout}
/> />
</TileErrorBoundary> </TileErrorBoundary>
</li>, </li>,
@ -797,8 +779,6 @@ export default class MessagePanel extends React.Component {
this.props.className, this.props.className,
{ {
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps, "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; let ircResizer = null;
if (this.state.useIRCLayout) { if (this.props.useIRCLayout) {
ircResizer = <IRCTimelineProfileResizer ircResizer = <IRCTimelineProfileResizer
minWidth={20} minWidth={20}
maxWidth={600} 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 RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore'; import GroupStore from '../../stores/GroupStore';
import SettingsStore from "../../settings/SettingsStore";
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases"; import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore"; import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext"; 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() { render() {
const MemberList = sdk.getComponent('rooms.MemberList'); const MemberList = sdk.getComponent('rooms.MemberList');
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
const UserInfo = sdk.getComponent('right_panel.UserInfo'); const UserInfo = sdk.getComponent('right_panel.UserInfo');
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo'); const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
const NotificationPanel = sdk.getComponent('structures.NotificationPanel'); const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
const FilePanel = sdk.getComponent('structures.FilePanel'); const FilePanel = sdk.getComponent('structures.FilePanel');
const GroupMemberList = sdk.getComponent('groups.GroupMemberList'); const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo');
const GroupRoomList = sdk.getComponent('groups.GroupRoomList'); const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo'); const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
@ -220,71 +240,25 @@ export default class RightPanel extends React.Component {
break; break;
case RIGHT_PANEL_PHASES.RoomMemberInfo: case RIGHT_PANEL_PHASES.RoomMemberInfo:
case RIGHT_PANEL_PHASES.EncryptionPanel: case RIGHT_PANEL_PHASES.EncryptionPanel:
if (SettingsStore.getValue("feature_cross_signing")) { panel = <UserInfo
const onClose = () => { user={this.state.member}
// XXX: There are three different ways of 'closing' this panel depending on what state roomId={this.props.roomId}
// things are in... this knows far more than it should do about the state of the rest key={this.props.roomId || this.state.member.userId}
// of the app and is generally a bit silly. onClose={this.onCloseUserInfo}
if (this.props.user) { phase={this.state.phase}
// If we have a user prop then we're displaying a user from the 'user' page type verificationRequest={this.state.verificationRequest}
// in LoggedInView, so need to change the page type to close the panel (we switch verificationRequestPromise={this.state.verificationRequestPromise}
// 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}
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; break;
case RIGHT_PANEL_PHASES.Room3pidMemberInfo: case RIGHT_PANEL_PHASES.Room3pidMemberInfo:
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />; panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
break; break;
case RIGHT_PANEL_PHASES.GroupMemberInfo: case RIGHT_PANEL_PHASES.GroupMemberInfo:
if (SettingsStore.getValue("feature_cross_signing")) { panel = <UserInfo
const onClose = () => { user={this.state.member}
dis.dispatch({ groupId={this.props.groupId}
action: Action.ViewUser, key={this.state.member.userId}
member: null, onClose={this.onCloseUserInfo} />;
});
};
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}
/>
);
}
break; break;
case RIGHT_PANEL_PHASES.GroupRoomInfo: case RIGHT_PANEL_PHASES.GroupRoomInfo:
panel = <GroupRoomInfo panel = <GroupRoomInfo

View file

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

View file

@ -24,9 +24,9 @@ import { _t, _td } from '../../languageHandler';
import * as sdk from '../../index'; import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import Resend from '../../Resend'; import Resend from '../../Resend';
import * as cryptodevices from '../../cryptodevices';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
import {Action} from "../../dispatcher/actions";
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1; 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() { _onResendAllClick: function() {
Resend.resendUnsentEvents(this.props.room); Resend.resendUnsentEvents(this.props.room);
dis.dispatch({action: 'focus_composer'}); dis.fire(Action.FocusComposer);
}, },
_onCancelAllClick: function() { _onCancelAllClick: function() {
Resend.cancelUnsentEvents(this.props.room); Resend.cancelUnsentEvents(this.props.room);
dis.dispatch({action: 'focus_composer'}); dis.fire(Action.FocusComposer);
},
_onShowDevicesClick: function() {
cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
}, },
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) { _onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {
@ -213,82 +202,65 @@ export default createReactClass({
if (!unsentMessages.length) return null; if (!unsentMessages.length) return null;
let title; let title;
let content;
const hasUDE = unsentMessages.some((m) => { let consentError = null;
return m.error && m.error.name === "UnknownDeviceError"; let resourceLimitError = null;
}); for (const m of unsentMessages) {
if (m.error && m.error.errcode === 'M_CONSENT_NOT_GIVEN') {
if (hasUDE) { consentError = m.error;
title = _t("Message not sent due to unknown sessions being present"); break;
content = _t( } else if (m.error && m.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
"<showSessionsText>Show sessions</showSessionsText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.", resourceLimitError = m.error;
break;
}
}
if (consentError) {
title = _t(
"You can't send any messages until you review and agree to " +
"<consentLink>our terms and conditions</consentLink>.",
{}, {},
{ {
'showSessionsText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>, 'consentLink': (sub) =>
'sendAnywayText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="sendAnyway" onClick={this._onSendWithoutVerifyingClick}>{ sub }</a>, <a href={consentError.data && consentError.data.consent_uri} target="_blank">
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>, { sub }
</a>,
}, },
); );
} else if (resourceLimitError) {
title = messageForResourceLimitError(
resourceLimitError.data.limit_type,
resourceLimitError.data.admin_contact, {
'monthly_active_user': _td(
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
"Please <a>contact your service administrator</a> to continue using the service.",
),
'': _td(
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
"Please <a>contact your service administrator</a> to continue using the service.",
),
});
} else if (
unsentMessages.length === 1 &&
unsentMessages[0].error &&
unsentMessages[0].error.data &&
unsentMessages[0].error.data.error
) {
title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error;
} else { } else {
let consentError = null; title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
let resourceLimitError = null;
for (const m of unsentMessages) {
if (m.error && m.error.errcode === 'M_CONSENT_NOT_GIVEN') {
consentError = m.error;
break;
} else if (m.error && m.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
resourceLimitError = m.error;
break;
}
}
if (consentError) {
title = _t(
"You can't send any messages until you review and agree to " +
"<consentLink>our terms and conditions</consentLink>.",
{},
{
'consentLink': (sub) =>
<a href={consentError.data && consentError.data.consent_uri} target="_blank">
{ sub }
</a>,
},
);
} else if (resourceLimitError) {
title = messageForResourceLimitError(
resourceLimitError.data.limit_type,
resourceLimitError.data.admin_contact, {
'monthly_active_user': _td(
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
"Please <a>contact your service administrator</a> to continue using the service.",
),
'': _td(
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
"Please <a>contact your service administrator</a> to continue using the service.",
),
});
} else if (
unsentMessages.length === 1 &&
unsentMessages[0].error &&
unsentMessages[0].error.data &&
unsentMessages[0].error.data.error
) {
title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error;
} 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.",
{ count: unsentMessages.length },
{
'resendText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
'cancelText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
},
);
} }
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) =>
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
'cancelText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
},
);
return <div className="mx_RoomStatusBar_connectionLostBar"> return <div className="mx_RoomStatusBar_connectionLostBar">
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title={_t("Warning")} alt="" /> <img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title={_t("Warning")} alt="" />
<div> <div>

View file

@ -55,6 +55,7 @@ import {haveTileForEvent} from "../views/rooms/EventTile";
import RoomContext from "../../contexts/RoomContext"; import RoomContext from "../../contexts/RoomContext";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import { shieldStatusForRoom } from '../../utils/ShieldUtils'; import { shieldStatusForRoom } from '../../utils/ShieldUtils';
import {Action} from "../../dispatcher/actions";
const DEBUG = false; const DEBUG = false;
let debuglog = function() {}; let debuglog = function() {};
@ -164,6 +165,10 @@ export default createReactClass({
canReact: false, canReact: false,
canReply: 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._roomView = createRef();
this._searchResultsPanel = createRef(); this._searchResultsPanel = createRef();
this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange);
}, },
_onReadReceiptsChange: function() { _onReadReceiptsChange: function() {
@ -232,7 +239,8 @@ export default createReactClass({
initialEventId: RoomViewStore.getInitialEventId(), initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
forwardingEvent: RoomViewStore.getForwardingEvent(), 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), showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
showReadReceipts: SettingsStore.getValue("showReadReceipts", 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. // no need to do this as Dir & Settings are now overlays. It just burnt CPU.
// console.log("Tinter.tint from RoomView.unmount"); // console.log("Tinter.tint from RoomView.unmount");
// Tinter.tint(); // reset colourscheme // Tinter.tint(); // reset colourscheme
SettingsStore.unwatchSetting(this._layoutWatcherRef);
},
onLayoutChange: function() {
this.setState({
useIRCLayout: SettingsStore.getValue("feature_irc_ui"),
});
}, },
_onRightPanelStoreUpdate: function() { _onRightPanelStoreUpdate: function() {
@ -681,6 +697,16 @@ export default createReactClass({
}); });
} }
break; 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; 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 */ /* At this point, the user has encryption on and cross-signing on */
this.setState({ this.setState({
@ -1146,7 +1163,7 @@ export default createReactClass({
ev.dataTransfer.files, this.state.room.roomId, this.context, ev.dataTransfer.files, this.state.room.roomId, this.context,
); );
this.setState({ draggingFile: false }); this.setState({ draggingFile: false });
dis.dispatch({action: 'focus_composer'}); dis.fire(Action.FocusComposer);
}, },
onDragLeaveOrEnd: function(ev) { onDragLeaveOrEnd: function(ev) {
@ -1352,7 +1369,7 @@ export default createReactClass({
event: null, event: null,
}); });
} }
dis.dispatch({action: 'focus_composer'}); dis.fire(Action.FocusComposer);
}, },
onLeaveClick: function() { onLeaveClick: function() {
@ -1463,7 +1480,7 @@ export default createReactClass({
// jump down to the bottom of this room, where new events are arriving // jump down to the bottom of this room, where new events are arriving
jumpToLiveTimeline: function() { jumpToLiveTimeline: function() {
this._messagePanel.jumpToLiveTimeline(); this._messagePanel.jumpToLiveTimeline();
dis.dispatch({action: 'focus_composer'}); dis.fire(Action.FocusComposer);
}, },
// jump up to wherever our read marker is // jump up to wherever our read marker is
@ -1663,14 +1680,16 @@ export default createReactClass({
const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary"); const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
if (!this.state.room) { 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) { 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 ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<ErrorBoundary> <ErrorBoundary>
<RoomPreviewBar <RoomPreviewBar
canPreview={false} canPreview={false}
previewLoading={this.state.peekLoading} previewLoading={previewLoading && !this.state.roomLoadError}
error={this.state.roomLoadError} error={this.state.roomLoadError}
loading={loading} loading={loading}
joining={this.state.joining} joining={this.state.joining}
@ -1695,7 +1714,8 @@ export default createReactClass({
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<ErrorBoundary> <ErrorBoundary>
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} <RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick} onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked} onRejectClick={this.onRejectThreepidInviteButtonClicked}
canPreview={false} error={this.state.roomLoadError} canPreview={false} error={this.state.roomLoadError}
@ -1980,6 +2000,13 @@ export default createReactClass({
highlightedEventId = this.state.initialEventId; 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); // console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
const messagePanel = ( const messagePanel = (
<TimelinePanel <TimelinePanel
@ -1995,11 +2022,12 @@ export default createReactClass({
onScroll={this.onMessageListScroll} onScroll={this.onMessageListScroll}
onReadMarkerUpdated={this._updateTopUnreadMessagesBar} onReadMarkerUpdated={this._updateTopUnreadMessagesBar}
showUrlPreview = {this.state.showUrlPreview} showUrlPreview = {this.state.showUrlPreview}
className="mx_RoomView_messagePanel" className={messagePanelClassNames}
membersLoaded={this.state.membersLoaded} membersLoaded={this.state.membersLoaded}
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)} permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
showReactions={true} showReactions={true}
useIRCLayout={this.state.useIRCLayout}
/>); />);
let topUnreadMessagesBar = null; 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 // whether to show reactions for an event
showReactions: PropTypes.bool, showReactions: PropTypes.bool,
// whether to use the irc layout
useIRCLayout: PropTypes.bool,
}, },
statics: { statics: {
@ -1447,6 +1450,7 @@ const TimelinePanel = createReactClass({
getRelationsForEvent={this.getRelationsForEvent} getRelationsForEvent={this.getRelationsForEvent}
editState={this.state.editState} editState={this.state.editState}
showReactions={this.props.showReactions} showReactions={this.props.showReactions}
useIRCLayout={this.props.useIRCLayout}
/> />
); );
}, },

View file

@ -15,14 +15,21 @@ limitations under the License.
*/ */
import * as React from "react"; import * as React from "react";
import { _t } from '../../languageHandler'; import ToastStore, {IToast} from "../../stores/ToastStore";
import ToastStore from "../../stores/ToastStore";
import classNames from "classnames"; import classNames from "classnames";
export default class ToastContainer extends React.Component { interface IState {
constructor() { toasts: IToast<any>[];
super(); countSeen: number;
this.state = {toasts: ToastStore.sharedInstance().getToasts()}; }
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 // Start listening here rather than in componentDidMount because
// toasts may dismiss themselves in their didMount if they find // toasts may dismiss themselves in their didMount if they find
@ -36,7 +43,10 @@ export default class ToastContainer extends React.Component {
} }
_onToastStoreUpdate = () => { _onToastStoreUpdate = () => {
this.setState({toasts: ToastStore.sharedInstance().getToasts()}); this.setState({
toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(),
});
}; };
render() { render() {
@ -50,14 +60,21 @@ export default class ToastContainer extends React.Component {
"mx_Toast_hasIcon": icon, "mx_Toast_hasIcon": icon,
[`mx_Toast_icon_${icon}`]: 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, { const toastProps = Object.assign({}, props, {
key, key,
toastKey: key, toastKey: key,
}); });
toast = (<div className={toastClasses}> 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 className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
</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 // do SSO instead. If we've already started the UI Auth process though, we don't
// need to. // need to.
if (!this.state.doingUIAuth) { if (!this.state.doingUIAuth) {
await this._makeRegisterRequest({}); await this._makeRegisterRequest(null);
// This should never succeed since we specified an empty // This should never succeed since we specified no auth object.
// auth object.
console.log("Expecting 401 from register request but got success!"); console.log("Expecting 401 from register request but got success!");
} }
} catch (e) { } catch (e) {

View file

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

View file

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

View file

@ -23,7 +23,8 @@ import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import * as ServerType from '../../views/auth/ServerTypeSelector'; import * as ServerType from '../../views/auth/ServerTypeSelector';
import ServerConfig from "./ServerConfig"; 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? // TODO: TravisR - Can this extend ServerConfig for most things?

View file

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

View file

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

View file

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

View file

@ -22,7 +22,8 @@ import classnames from 'classnames';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import {makeType} from "../../../utils/TypeUtils"; 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 FREE = 'Free';
export const PREMIUM = 'Premium'; export const PREMIUM = 'Premium';

View file

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

View file

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

View file

@ -116,11 +116,6 @@ export default createReactClass({
this.closeMenu(); this.closeMenu();
}, },
e2eInfoClicked: function() {
this.props.e2eInfoCallback();
this.closeMenu();
},
onReportEventClick: function() { onReportEventClick: function() {
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog"); const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
Modal.createTrackedDialog('Report Event', '', 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; let reportEventButton;
if (mxEvent.getSender() !== me) { if (mxEvent.getSender() !== me) {
reportEventButton = ( reportEventButton = (
@ -500,7 +486,6 @@ export default createReactClass({
{ quoteButton } { quoteButton }
{ externalURLButton } { externalURLButton }
{ collapseReplyThread } { collapseReplyThread }
{ e2eInfo }
{ reportEventButton } { reportEventButton }
</div> </div>
); );

View file

@ -98,7 +98,7 @@ export default createReactClass({
render: function() { render: function() {
return ( 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} onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
value={this.props.alias} /> value={this.props.alias} />
); );

View file

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

View file

@ -24,7 +24,7 @@ import withValidation from '../elements/Validation';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
import SettingsStore from "../../../settings/SettingsStore"; import {privateShouldBeEncrypted} from "../../../createRoom";
export default createReactClass({ export default createReactClass({
displayName: 'CreateRoomDialog', displayName: 'CreateRoomDialog',
@ -37,7 +37,7 @@ export default createReactClass({
const config = SdkConfig.get(); const config = SdkConfig.get();
return { return {
isPublic: this.props.defaultPublic || false, isPublic: this.props.defaultPublic || false,
isEncrypted: true, isEncrypted: privateShouldBeEncrypted(),
name: "", name: "",
topic: "", topic: "",
alias: "", alias: "",
@ -66,7 +66,7 @@ export default createReactClass({
createOpts.creation_content = {'m.federate': false}; createOpts.creation_content = {'m.federate': false};
} }
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) { if (!this.state.isPublic) {
opts.encryption = this.state.isEncrypted; opts.encryption = this.state.isEncrypted;
} }
@ -181,7 +181,7 @@ export default createReactClass({
let publicPrivateLabel; let publicPrivateLabel;
let aliasField; let aliasField;
if (this.state.isPublic) { 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(); const domain = MatrixClientPeg.get().getDomain();
aliasField = ( aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer"> <div className="mx_CreateRoomDialog_aliasContainer">
@ -193,7 +193,14 @@ export default createReactClass({
} }
let e2eeSection; 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> e2eeSection = <React.Fragment>
<LabelledToggleSwitch <LabelledToggleSwitch
label={ _t("Enable end-to-end encryption")} label={ _t("Enable end-to-end encryption")}
@ -201,7 +208,7 @@ export default createReactClass({
value={this.state.isEncrypted} value={this.state.isEncrypted}
className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests 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>; </React.Fragment>;
} }

View file

@ -42,11 +42,9 @@ export default (props) => {
}; };
const description = 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 " + "To use this version again with end to end encryption, you will " +
"need to sign out and back in again. ", "need to sign out and back in again.");
{host: props.host},
);
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');

View file

@ -25,6 +25,7 @@ import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth"; import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents"; import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
import StyledCheckbox from "../elements/StyledCheckbox";
export default class DeactivateAccountDialog extends React.Component { export default class DeactivateAccountDialog extends React.Component {
constructor(props) { constructor(props) {
@ -209,21 +210,18 @@ export default class DeactivateAccountDialog extends React.Component {
<div className="mx_DeactivateAccountDialog_input_section"> <div className="mx_DeactivateAccountDialog_input_section">
<p> <p>
<label htmlFor="mx_DeactivateAccountDialog_erase_account_input"> <StyledCheckbox
<input checked={this.state.shouldErase}
id="mx_DeactivateAccountDialog_erase_account_input" onChange={this._onEraseFieldChange}
type="checkbox" >
checked={this.state.shouldErase} {_t(
onChange={this._onEraseFieldChange}
/>
{ _t(
"Please forget all messages I have sent when my account is deactivated " + "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 " + "(<b>Warning:</b> this will cause future users to see an incomplete view " +
"of conversations)", "of conversations)",
{}, {},
{ b: (sub) => <b>{ sub }</b> }, { b: (sub) => <b>{ sub }</b> },
) } )}
</label> </StyledCheckbox>
</p> </p>
{error} {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 IdentityAuthClient from "../../../IdentityAuthClient";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize"; import {humanizeTime} from "../../../utils/humanize";
import createRoom, {canEncryptToAllUsers} from "../../../createRoom"; import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
import {inviteMultipleToRoom} from "../../../RoomInvite"; import {inviteMultipleToRoom} from "../../../RoomInvite";
import SettingsStore from '../../../settings/SettingsStore';
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy"; import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
@ -576,7 +575,7 @@ export default class InviteDialog extends React.PureComponent {
const createRoomOptions = {inlineErrors: true}; const createRoomOptions = {inlineErrors: true};
if (SettingsStore.getValue("feature_cross_signing")) { if (privateShouldBeEncrypted()) {
// Check whether all users have uploaded device keys before. // Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room. // If so, enable encryption in the new room.
const has3PidMembers = targets.some(t => t instanceof ThreepidMember); 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 * as ContextMenu from "../../structures/ContextMenu";
import {toRightOf} from "../../structures/ContextMenu"; import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext, selectText} from "../../../utils/strings"; import {copyPlaintext, selectText} from "../../../utils/strings";
import StyledCheckbox from '../elements/StyledCheckbox';
const socials = [ const socials = [
{ {
@ -168,13 +169,12 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
const events = this.props.target.getLiveTimeline().getEvents(); const events = this.props.target.getLiveTimeline().getEvents();
if (events.length > 0) { if (events.length > 0) {
checkbox = <div> checkbox = <div>
<input type="checkbox" <StyledCheckbox
id="mx_ShareDialog_checkbox" checked={this.state.linkSpecificEvent}
checked={this.state.linkSpecificEvent} onChange={this.onLinkSpecificEventCheckboxClick}
onChange={this.onLinkSpecificEventCheckboxClick} /> >
<label htmlFor="mx_ShareDialog_checkbox">
{ _t('Link to most recent message') } { _t('Link to most recent message') }
</label> </StyledCheckbox>
</div>; </div>;
} }
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) { } 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) { } else if (this.props.target instanceof MatrixEvent) {
title = _t('Share Room Message'); title = _t('Share Room Message');
checkbox = <div> checkbox = <div>
<input type="checkbox" <StyledCheckbox
id="mx_ShareDialog_checkbox"
checked={this.state.linkSpecificEvent} checked={this.state.linkSpecificEvent}
onClick={this.onLinkSpecificEventCheckboxClick} /> onClick={this.onLinkSpecificEventCheckboxClick}
<label htmlFor="mx_ShareDialog_checkbox"> >
{ _t('Link to selected message') } { _t('Link to selected message') }
</label> </StyledCheckbox>
</div>; </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> preview = <div>
<div> <div>
<img className="mx_UploadConfirmDialog_fileIcon" <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)}) {this.props.file.name} ({filesize(this.props.file.size)})
</div> </div>

View file

@ -20,10 +20,8 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../../index'; import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk'; import { MatrixClient } from 'matrix-js-sdk';
import Modal from '../../../../Modal';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager'; import { accessSecretStorage } from '../../../../CrossSigningManager';
import SettingsStore from "../../../../settings/SettingsStore";
const RESTORE_TYPE_PASSPHRASE = 0; const RESTORE_TYPE_PASSPHRASE = 0;
const RESTORE_TYPE_RECOVERYKEY = 1; const RESTORE_TYPE_RECOVERYKEY = 1;
@ -90,21 +88,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
_onResetRecoveryClick = () => { _onResetRecoveryClick = () => {
this.props.onFinished(false); this.props.onFinished(false);
accessSecretStorage(() => {}, /* forceReset = */ true);
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) => { _onRecoveryKeyChange = (e) => {
@ -243,8 +227,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
loadError: null, loadError: null,
}); });
try { try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); const cli = MatrixClientPeg.get();
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored(); const backupInfo = await cli.getKeyBackupVersion();
const has4S = await cli.hasSecretStorageKey();
const backupKeyStored = has4S && await cli.isKeyBackupKeyStored();
this.setState({ this.setState({
backupInfo, backupInfo,
backupKeyStored, 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("mousemove", this.state.onMouseMove);
document.addEventListener("mouseup", this.state.onMouseUp); document.addEventListener("mouseup", this.state.onMouseUp);
console.log("Mouse down")
} }
private onMouseUp = (event: MouseEvent): void => { private onMouseUp = (event: MouseEvent): void => {
document.removeEventListener("mousemove", this.state.onMouseMove); document.removeEventListener("mousemove", this.state.onMouseMove);
document.removeEventListener("mouseup", this.state.onMouseUp); document.removeEventListener("mouseup", this.state.onMouseUp);
this.props.onMouseUp(event); this.props.onMouseUp(event);
console.log("Mouse up")
} }
private onMouseMove(event: MouseEvent): void { private onMouseMove(event: MouseEvent): void {
console.log("Mouse Move")
const newLocation = this.props.dragFunc(this.state.location, event); const newLocation = this.props.dragFunc(this.state.location, event);
this.setState({ this.setState({

View file

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

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