Merge remote-tracking branch 'upstream/develop' into compact-reply-rendering
This commit is contained in:
commit
4521e9feb1
234 changed files with 6906 additions and 6817 deletions
|
@ -9,21 +9,17 @@ src/components/structures/UploadBar.js
|
|||
src/components/views/avatars/MemberAvatar.js
|
||||
src/components/views/create_room/RoomAlias.js
|
||||
src/components/views/dialogs/SetPasswordDialog.js
|
||||
src/components/views/dialogs/UnknownDeviceDialog.js
|
||||
src/components/views/elements/AddressSelector.js
|
||||
src/components/views/elements/DirectorySearchBox.js
|
||||
src/components/views/elements/MemberEventListSummary.js
|
||||
src/components/views/elements/UserSelector.js
|
||||
src/components/views/globals/MatrixToolbar.js
|
||||
src/components/views/globals/NewVersionBar.js
|
||||
src/components/views/globals/UpdateCheckBar.js
|
||||
src/components/views/messages/MFileBody.js
|
||||
src/components/views/messages/TextualBody.js
|
||||
src/components/views/room_settings/ColorSettings.js
|
||||
src/components/views/rooms/Autocomplete.js
|
||||
src/components/views/rooms/AuxPanel.js
|
||||
src/components/views/rooms/LinkPreviewWidget.js
|
||||
src/components/views/rooms/MemberDeviceInfo.js
|
||||
src/components/views/rooms/MemberInfo.js
|
||||
src/components/views/rooms/MemberList.js
|
||||
src/components/views/rooms/RoomList.js
|
||||
|
|
195
CHANGELOG.md
195
CHANGELOG.md
|
@ -1,3 +1,198 @@
|
|||
Changes in [2.7.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.1) (2020-06-05)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0...v2.7.1)
|
||||
|
||||
* Upgrade to JS SDK 6.2.1
|
||||
* Fix exceptions from Tooltip
|
||||
[\#4716](https://github.com/matrix-org/matrix-react-sdk/pull/4716)
|
||||
* Fix not being able to dismiss new login toasts
|
||||
[\#4715](https://github.com/matrix-org/matrix-react-sdk/pull/4715)
|
||||
* Fix compact layout regression
|
||||
[\#4714](https://github.com/matrix-org/matrix-react-sdk/pull/4714)
|
||||
|
||||
Changes in [2.7.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0) (2020-06-04)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0-rc.2...v2.7.0)
|
||||
|
||||
* Upgrade to JS SDK 6.2.0
|
||||
* Prevent (double) 4S bootstrap from RestoreKeyBackupDialog
|
||||
[\#4703](https://github.com/matrix-org/matrix-react-sdk/pull/4703)
|
||||
* Fix checkbox bleed
|
||||
[\#4702](https://github.com/matrix-org/matrix-react-sdk/pull/4702)
|
||||
* Fix login loop where the sso flow returns to `#/login` to release
|
||||
[\#4693](https://github.com/matrix-org/matrix-react-sdk/pull/4693)
|
||||
|
||||
Changes in [2.7.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0-rc.2) (2020-06-02)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0-rc.1...v2.7.0-rc.2)
|
||||
|
||||
* Rewire the Sticker button to be an Emoji Picker
|
||||
[\#3747](https://github.com/matrix-org/matrix-react-sdk/pull/3747)
|
||||
|
||||
Changes in [2.7.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0-rc.1) (2020-06-02)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.1...v2.7.0-rc.1)
|
||||
|
||||
* Upgrade to JS SDK 6.2.0-rc.1
|
||||
* Update from Weblate
|
||||
[\#4683](https://github.com/matrix-org/matrix-react-sdk/pull/4683)
|
||||
* Make auth argument in the register request compliant with r0.6.0
|
||||
[\#4347](https://github.com/matrix-org/matrix-react-sdk/pull/4347)
|
||||
* Revert "Prevent PersistedElements overflowing scrolled areas"
|
||||
[\#4682](https://github.com/matrix-org/matrix-react-sdk/pull/4682)
|
||||
* Remove unused TagPanelButtons
|
||||
[\#4680](https://github.com/matrix-org/matrix-react-sdk/pull/4680)
|
||||
* Pass roomId to IRCTimelineProfileResizer
|
||||
[\#4679](https://github.com/matrix-org/matrix-react-sdk/pull/4679)
|
||||
* Remove logging to console for irc name resize
|
||||
[\#4678](https://github.com/matrix-org/matrix-react-sdk/pull/4678)
|
||||
* Use arrow functions instead of binding `this`
|
||||
[\#4677](https://github.com/matrix-org/matrix-react-sdk/pull/4677)
|
||||
* Increase specificity of compact layout selectors
|
||||
[\#4675](https://github.com/matrix-org/matrix-react-sdk/pull/4675)
|
||||
* Create and use stylised checkboxes
|
||||
[\#4665](https://github.com/matrix-org/matrix-react-sdk/pull/4665)
|
||||
* useIRCLayout moved to props
|
||||
[\#4676](https://github.com/matrix-org/matrix-react-sdk/pull/4676)
|
||||
* Fix paste image to upload
|
||||
[\#4674](https://github.com/matrix-org/matrix-react-sdk/pull/4674)
|
||||
* Fix FilePanel and NotificationsPanel regression
|
||||
[\#4647](https://github.com/matrix-org/matrix-react-sdk/pull/4647)
|
||||
* Allow deferring of Update Toast until the next morning
|
||||
[\#4669](https://github.com/matrix-org/matrix-react-sdk/pull/4669)
|
||||
* Give contextual feedback for manual update check instead of banner
|
||||
[\#4668](https://github.com/matrix-org/matrix-react-sdk/pull/4668)
|
||||
* Dialog wrap title instead of taking same space as the close/cancel button
|
||||
[\#4659](https://github.com/matrix-org/matrix-react-sdk/pull/4659)
|
||||
* Update Modular hosting link
|
||||
[\#4627](https://github.com/matrix-org/matrix-react-sdk/pull/4627)
|
||||
* Fix field placeholder regression
|
||||
[\#4663](https://github.com/matrix-org/matrix-react-sdk/pull/4663)
|
||||
* Fix/document a number of UIA oddities
|
||||
[\#4667](https://github.com/matrix-org/matrix-react-sdk/pull/4667)
|
||||
* Stop copy icon repeating weirdly
|
||||
[\#4662](https://github.com/matrix-org/matrix-react-sdk/pull/4662)
|
||||
* Try and fix the Notifier race
|
||||
[\#4661](https://github.com/matrix-org/matrix-react-sdk/pull/4661)
|
||||
* set the client's pickle key if the platform can store one
|
||||
[\#4657](https://github.com/matrix-org/matrix-react-sdk/pull/4657)
|
||||
* Migrate Banners to Toasts
|
||||
[\#4624](https://github.com/matrix-org/matrix-react-sdk/pull/4624)
|
||||
* Move Appearance tab to ts
|
||||
[\#4658](https://github.com/matrix-org/matrix-react-sdk/pull/4658)
|
||||
* Fix room alias lookup vs peeking race condition
|
||||
[\#4606](https://github.com/matrix-org/matrix-react-sdk/pull/4606)
|
||||
* Fix encryption icon miss-alignment
|
||||
[\#4651](https://github.com/matrix-org/matrix-react-sdk/pull/4651)
|
||||
* Fix sublist sizing regression
|
||||
[\#4649](https://github.com/matrix-org/matrix-react-sdk/pull/4649)
|
||||
* Fix lines overflowing room list width
|
||||
[\#4650](https://github.com/matrix-org/matrix-react-sdk/pull/4650)
|
||||
* Remove the keyshare dialog
|
||||
[\#4648](https://github.com/matrix-org/matrix-react-sdk/pull/4648)
|
||||
* Update badge counts in new room list as needed
|
||||
[\#4654](https://github.com/matrix-org/matrix-react-sdk/pull/4654)
|
||||
* EventIndex: Handle invalid m.room.redaction events correctly.
|
||||
[\#4653](https://github.com/matrix-org/matrix-react-sdk/pull/4653)
|
||||
* EventIndex: Print out the checkpoint if there was an error during a crawl
|
||||
[\#4652](https://github.com/matrix-org/matrix-react-sdk/pull/4652)
|
||||
* Move Field to Typescript
|
||||
[\#4635](https://github.com/matrix-org/matrix-react-sdk/pull/4635)
|
||||
* Use connection error to detect network problem
|
||||
[\#4646](https://github.com/matrix-org/matrix-react-sdk/pull/4646)
|
||||
* Revert default font size to 15px
|
||||
[\#4641](https://github.com/matrix-org/matrix-react-sdk/pull/4641)
|
||||
* Add logging when room join fails
|
||||
[\#4645](https://github.com/matrix-org/matrix-react-sdk/pull/4645)
|
||||
* Remove EncryptedEventDialog
|
||||
[\#4644](https://github.com/matrix-org/matrix-react-sdk/pull/4644)
|
||||
* Migrate Toasts to Typescript and to granular priority system
|
||||
[\#4618](https://github.com/matrix-org/matrix-react-sdk/pull/4618)
|
||||
* Update Crypto Store Too New copy
|
||||
[\#4632](https://github.com/matrix-org/matrix-react-sdk/pull/4632)
|
||||
* MemberAvatar should not have its own letter fallback, it should use
|
||||
BaseAvatar
|
||||
[\#4643](https://github.com/matrix-org/matrix-react-sdk/pull/4643)
|
||||
* Fix media upload issues with abort and status bar
|
||||
[\#4630](https://github.com/matrix-org/matrix-react-sdk/pull/4630)
|
||||
* fix viewGroup to actually show the group if possible
|
||||
[\#4633](https://github.com/matrix-org/matrix-react-sdk/pull/4633)
|
||||
* Update confirm passphrase copy
|
||||
[\#4634](https://github.com/matrix-org/matrix-react-sdk/pull/4634)
|
||||
* Improve accessibility of the emoji picker
|
||||
[\#4636](https://github.com/matrix-org/matrix-react-sdk/pull/4636)
|
||||
* Fix Emoji Picker footer being too small if text overflows
|
||||
[\#4631](https://github.com/matrix-org/matrix-react-sdk/pull/4631)
|
||||
* Improve style of toasts to match Figma
|
||||
[\#4613](https://github.com/matrix-org/matrix-react-sdk/pull/4613)
|
||||
* Iterate toast count indicator more logically
|
||||
[\#4620](https://github.com/matrix-org/matrix-react-sdk/pull/4620)
|
||||
* Fix reacting to redactions
|
||||
[\#4626](https://github.com/matrix-org/matrix-react-sdk/pull/4626)
|
||||
* Fix sentMessageAndIsAlone by dispatching `message_sent` more consistently
|
||||
[\#4628](https://github.com/matrix-org/matrix-react-sdk/pull/4628)
|
||||
* Update from Weblate
|
||||
[\#4640](https://github.com/matrix-org/matrix-react-sdk/pull/4640)
|
||||
* Replace `alias` with `address` in copy for consistency
|
||||
[\#4402](https://github.com/matrix-org/matrix-react-sdk/pull/4402)
|
||||
* Convert MatrixClientPeg to TypeScript
|
||||
[\#4638](https://github.com/matrix-org/matrix-react-sdk/pull/4638)
|
||||
* Fix BaseAvatar wrongly retrying urls
|
||||
[\#4629](https://github.com/matrix-org/matrix-react-sdk/pull/4629)
|
||||
* Fix event highlights not being updated to reflect edits
|
||||
[\#4637](https://github.com/matrix-org/matrix-react-sdk/pull/4637)
|
||||
* Calculate badges in the new room list more reliably
|
||||
[\#4625](https://github.com/matrix-org/matrix-react-sdk/pull/4625)
|
||||
* Transition BaseAvatar to hooks
|
||||
[\#4101](https://github.com/matrix-org/matrix-react-sdk/pull/4101)
|
||||
* Convert BasePlatform and BaseEventIndexManager to Typescript
|
||||
[\#4614](https://github.com/matrix-org/matrix-react-sdk/pull/4614)
|
||||
* Fix: Tag_DM is not defined
|
||||
[\#4619](https://github.com/matrix-org/matrix-react-sdk/pull/4619)
|
||||
* Fix visibility of message timestamps
|
||||
[\#4615](https://github.com/matrix-org/matrix-react-sdk/pull/4615)
|
||||
* Rewrite the room list store
|
||||
[\#4253](https://github.com/matrix-org/matrix-react-sdk/pull/4253)
|
||||
* Update code style to mention switch statements
|
||||
[\#4610](https://github.com/matrix-org/matrix-react-sdk/pull/4610)
|
||||
* Fix key backup restore with SSSS
|
||||
[\#4612](https://github.com/matrix-org/matrix-react-sdk/pull/4612)
|
||||
* Handle null tokens in the crawler loop.
|
||||
[\#4608](https://github.com/matrix-org/matrix-react-sdk/pull/4608)
|
||||
* Font scaling settings and slider
|
||||
[\#4424](https://github.com/matrix-org/matrix-react-sdk/pull/4424)
|
||||
* Prevent PersistedElements overflowing scrolled areas
|
||||
[\#4494](https://github.com/matrix-org/matrix-react-sdk/pull/4494)
|
||||
* IRC ui layout
|
||||
[\#4531](https://github.com/matrix-org/matrix-react-sdk/pull/4531)
|
||||
* Remove SSSS key upgrade check from rageshake
|
||||
[\#4607](https://github.com/matrix-org/matrix-react-sdk/pull/4607)
|
||||
* Label the create room button better than "Add room"
|
||||
[\#4603](https://github.com/matrix-org/matrix-react-sdk/pull/4603)
|
||||
* Convert the dispatcher to TypeScript
|
||||
[\#4593](https://github.com/matrix-org/matrix-react-sdk/pull/4593)
|
||||
* Consolidate password/passphrase fields into a component & add dynamic colour
|
||||
to progress
|
||||
[\#4599](https://github.com/matrix-org/matrix-react-sdk/pull/4599)
|
||||
* UserView, show Welcome page in the mid panel instead of empty space
|
||||
[\#4590](https://github.com/matrix-org/matrix-react-sdk/pull/4590)
|
||||
* Update from Weblate
|
||||
[\#4601](https://github.com/matrix-org/matrix-react-sdk/pull/4601)
|
||||
* Make email auth component fail better if server claims email isn't validated
|
||||
[\#4600](https://github.com/matrix-org/matrix-react-sdk/pull/4600)
|
||||
* Add new keyboard shortcuts for jump to unread and upload file
|
||||
[\#4588](https://github.com/matrix-org/matrix-react-sdk/pull/4588)
|
||||
* accept and linkify local domains like those from mDNS
|
||||
[\#4594](https://github.com/matrix-org/matrix-react-sdk/pull/4594)
|
||||
* Revert "ImageView make clicking off it easier"
|
||||
[\#4586](https://github.com/matrix-org/matrix-react-sdk/pull/4586)
|
||||
* wrap node-qrcode in a React FC and use it for ShareDialog
|
||||
[\#4394](https://github.com/matrix-org/matrix-react-sdk/pull/4394)
|
||||
* Pass screenAfterLogin through SSO in the callback url
|
||||
[\#4585](https://github.com/matrix-org/matrix-react-sdk/pull/4585)
|
||||
* Remove debugging that causes email addresses to load forever
|
||||
[\#4597](https://github.com/matrix-org/matrix-react-sdk/pull/4597)
|
||||
|
||||
Changes in [2.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.1) (2020-05-22)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.0...v2.6.1)
|
||||
|
|
|
@ -4,7 +4,7 @@ Matrix JavaScript/ECMAScript Style Guide
|
|||
The intention of this guide is to make Matrix's JavaScript codebase clean,
|
||||
consistent with other popular JavaScript styles and consistent with the rest of
|
||||
the Matrix codebase. For reference, the Matrix Python style guide can be found
|
||||
at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst
|
||||
at https://github.com/matrix-org/synapse/blob/master/docs/code_style.md
|
||||
|
||||
This document reflects how we would like Matrix JavaScript code to look, with
|
||||
acknowledgement that a significant amount of code is written to older
|
||||
|
@ -17,7 +17,7 @@ writing in modern ECMAScript and using a transpile step to generate the file
|
|||
that applications can then include. There are significant benefits in being
|
||||
able to use modern ECMAScript, although the tooling for doing so can be awkward
|
||||
for library code, especially with regard to translating source maps and line
|
||||
number throgh from the original code to the final application.
|
||||
number through from the original code to the final application.
|
||||
|
||||
General Style
|
||||
-------------
|
||||
|
|
16
package.json
16
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.1",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -65,8 +65,8 @@
|
|||
"create-react-class": "^15.6.0",
|
||||
"diff-dom": "^4.1.3",
|
||||
"diff-match-patch": "^1.0.4",
|
||||
"emojibase-data": "^4.0.2",
|
||||
"emojibase-regex": "^3.0.0",
|
||||
"emojibase-data": "^5.0.1",
|
||||
"emojibase-regex": "^4.0.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"file-saver": "^1.3.3",
|
||||
"filesize": "3.5.6",
|
||||
|
@ -93,6 +93,7 @@
|
|||
"react-beautiful-dnd": "^4.0.1",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-focus-lock": "^2.2.1",
|
||||
"react-resizable": "^1.10.1",
|
||||
"resize-observer-polyfill": "^1.5.0",
|
||||
"sanitize-html": "^1.18.4",
|
||||
"text-encoding-utf-8": "^1.0.1",
|
||||
|
@ -119,9 +120,12 @@
|
|||
"@peculiar/webcrypto": "^1.0.22",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/flux": "^3.1.9",
|
||||
"@types/lodash": "^4.14.152",
|
||||
"@types/modernizr": "^3.5.3",
|
||||
"@types/node": "^12.12.41",
|
||||
"@types/qrcode": "^1.3.4",
|
||||
"@types/react": "16.9",
|
||||
"@types/react": "^16.9",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
|
@ -161,7 +165,9 @@
|
|||
"testMatch": [
|
||||
"<rootDir>/test/**/*-test.js"
|
||||
],
|
||||
"setupFiles": ["jest-canvas-mock"],
|
||||
"setupFiles": [
|
||||
"jest-canvas-mock"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/test/setupTests.js"
|
||||
],
|
||||
|
|
|
@ -335,6 +335,9 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
.mx_Dialog_header.mx_Dialog_headerWithButton > .mx_Dialog_title {
|
||||
text-align: center;
|
||||
}
|
||||
.mx_Dialog_header.mx_Dialog_headerWithCancel > .mx_Dialog_title {
|
||||
margin-right: 20px; // leave space for the 'X' cancel button
|
||||
}
|
||||
|
||||
.mx_Dialog_title.danger {
|
||||
color: $warning-color;
|
||||
|
|
|
@ -61,9 +61,7 @@
|
|||
@import "./views/dialogs/_CreateGroupDialog.scss";
|
||||
@import "./views/dialogs/_CreateRoomDialog.scss";
|
||||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||
@import "./views/dialogs/_DeviceVerifyDialog.scss";
|
||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||
@import "./views/dialogs/_EncryptedEventDialog.scss";
|
||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||
@import "./views/dialogs/_InviteDialog.scss";
|
||||
|
@ -82,7 +80,6 @@
|
|||
@import "./views/dialogs/_SlashCommandHelpDialog.scss";
|
||||
@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
|
||||
@import "./views/dialogs/_TermsDialog.scss";
|
||||
@import "./views/dialogs/_UnknownDeviceDialog.scss";
|
||||
@import "./views/dialogs/_UploadConfirmDialog.scss";
|
||||
@import "./views/dialogs/_UserSettingsDialog.scss";
|
||||
@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
|
||||
|
@ -117,6 +114,7 @@
|
|||
@import "./views/elements/_RoomAliasField.scss";
|
||||
@import "./views/elements/_Slider.scss";
|
||||
@import "./views/elements/_Spinner.scss";
|
||||
@import "./views/elements/_StyledCheckbox.scss";
|
||||
@import "./views/elements/_SyntaxHighlight.scss";
|
||||
@import "./views/elements/_TextWithTooltip.scss";
|
||||
@import "./views/elements/_ToggleSwitch.scss";
|
||||
|
@ -124,7 +122,6 @@
|
|||
@import "./views/elements/_TooltipButton.scss";
|
||||
@import "./views/elements/_Validation.scss";
|
||||
@import "./views/emojipicker/_EmojiPicker.scss";
|
||||
@import "./views/globals/_MatrixToolbar.scss";
|
||||
@import "./views/groups/_GroupPublicityToggle.scss";
|
||||
@import "./views/groups/_GroupRoomList.scss";
|
||||
@import "./views/groups/_GroupUserSettings.scss";
|
||||
|
@ -169,7 +166,6 @@
|
|||
@import "./views/rooms/_InviteOnlyIcon.scss";
|
||||
@import "./views/rooms/_JumpToBottomButton.scss";
|
||||
@import "./views/rooms/_LinkPreviewWidget.scss";
|
||||
@import "./views/rooms/_MemberDeviceInfo.scss";
|
||||
@import "./views/rooms/_MemberInfo.scss";
|
||||
@import "./views/rooms/_MemberList.scss";
|
||||
@import "./views/rooms/_MessageComposer.scss";
|
||||
|
@ -185,6 +181,7 @@
|
|||
@import "./views/rooms/_RoomList.scss";
|
||||
@import "./views/rooms/_RoomPreviewBar.scss";
|
||||
@import "./views/rooms/_RoomRecoveryReminder.scss";
|
||||
@import "./views/rooms/_RoomSublist2.scss";
|
||||
@import "./views/rooms/_RoomTile.scss";
|
||||
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
|
||||
@import "./views/rooms/_SearchBar.scss";
|
||||
|
@ -205,6 +202,7 @@
|
|||
@import "./views/settings/_ProfileSettings.scss";
|
||||
@import "./views/settings/_SetIdServer.scss";
|
||||
@import "./views/settings/_SetIntegrationManager.scss";
|
||||
@import "./views/settings/_UpdateCheckButton.scss";
|
||||
@import "./views/settings/tabs/_SettingsTab.scss";
|
||||
@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";
|
||||
@import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss";
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
$font-1px: 0.067rem;
|
||||
$font-1-5px: 0.100rem;
|
||||
$font-2px: 0.133rem;
|
||||
$font-3px: 0.200rem;
|
||||
$font-4px: 0.267rem;
|
||||
|
|
|
@ -19,9 +19,18 @@ limitations under the License.
|
|||
display: flex;
|
||||
/* LeftPanel 260px */
|
||||
min-width: 260px;
|
||||
max-width: 50%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
// TODO: Remove temporary indicator of new room list implementation.
|
||||
// This border is meant to visually distinguish between the two components when the
|
||||
// user has turned on the new room list implementation, at least until the designs
|
||||
// themselves give it away.
|
||||
.mx_LeftPanel2 .mx_LeftPanel {
|
||||
border-left: 5px #e26dff solid;
|
||||
}
|
||||
|
||||
.mx_LeftPanel_container.collapsed {
|
||||
min-width: unset;
|
||||
/* Collapsed LeftPanel 50px */
|
||||
|
|
|
@ -41,10 +41,6 @@ limitations under the License.
|
|||
height: 40px;
|
||||
}
|
||||
|
||||
.mx_MatrixChat_toolbarShowing {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.mx_MatrixChat {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
@ -63,6 +63,10 @@ limitations under the License.
|
|||
padding-left: 32px;
|
||||
padding-top: 8px;
|
||||
position: relative;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_NotificationPanel .mx_EventTile_roomName a,
|
||||
|
|
|
@ -20,6 +20,7 @@ limitations under the License.
|
|||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
min-width: 264px;
|
||||
max-width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -67,22 +68,27 @@ limitations under the License.
|
|||
|
||||
.mx_RightPanel_membersButton::before {
|
||||
mask-image: url('$(res)/img/feather-customised/user.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RightPanel_filesButton::before {
|
||||
mask-image: url('$(res)/img/feather-customised/files.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RightPanel_notifsButton::before {
|
||||
mask-image: url('$(res)/img/feather-customised/notifications.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RightPanel_groupMembersButton::before {
|
||||
mask-image: url('$(res)/img/icons-people.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RightPanel_roomsButton::before {
|
||||
mask-image: url('$(res)/img/icons-room-nobg.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RightPanel_headerButton_highlight::after {
|
||||
|
|
|
@ -28,8 +28,8 @@ limitations under the License.
|
|||
margin: 0 4px;
|
||||
grid-row: 2 / 4;
|
||||
grid-column: 1;
|
||||
background-color: white;
|
||||
box-shadow: 0px 4px 12px $menu-box-shadow-color;
|
||||
background-color: $dark-panel-bg-color;
|
||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
|
@ -37,16 +37,15 @@ limitations under the License.
|
|||
grid-row: 1 / 3;
|
||||
grid-column: 1;
|
||||
color: $primary-fg-color;
|
||||
background-color: $primary-bg-color;
|
||||
box-shadow: 0px 4px 12px $menu-box-shadow-color;
|
||||
background-color: $dark-panel-bg-color;
|
||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr;
|
||||
column-gap: 10px;
|
||||
grid-template-columns: 22px 1fr;
|
||||
column-gap: 8px;
|
||||
row-gap: 4px;
|
||||
padding: 8px;
|
||||
padding-right: 16px;
|
||||
|
||||
&.mx_Toast_hasIcon {
|
||||
&::after {
|
||||
|
@ -68,17 +67,45 @@ limitations under the License.
|
|||
background-image: url("$(res)/img/e2e/warning.svg");
|
||||
}
|
||||
|
||||
h2, .mx_Toast_body {
|
||||
.mx_Toast_title, .mx_Toast_body {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
&:not(.mx_Toast_hasIcon) {
|
||||
padding-left: 12px;
|
||||
|
||||
h2 {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1;
|
||||
margin: 0;
|
||||
font-size: $font-15px;
|
||||
font-weight: 600;
|
||||
.mx_Toast_title {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Toast_title,
|
||||
.mx_Toast_description {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.mx_Toast_title {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
h2 {
|
||||
grid-column: 1 / 3;
|
||||
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 {
|
||||
|
@ -87,7 +114,13 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_Toast_buttons {
|
||||
float: right;
|
||||
display: flex;
|
||||
|
||||
.mx_FormButton {
|
||||
min-width: 96px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Toast_description {
|
||||
|
@ -96,6 +129,15 @@ limitations under the License.
|
|||
text-overflow: ellipsis;
|
||||
margin: 4px 0 11px 0;
|
||||
font-size: $font-12px;
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
font-size: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Toast_deviceID {
|
||||
|
|
|
@ -18,8 +18,3 @@ limitations under the License.
|
|||
margin-top: 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mx_GroupAddressPicker_checkboxContainer input[type="checkbox"] {
|
||||
/* Stop flex from shrinking the checkbox */
|
||||
width: 20px;
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ limitations under the License.
|
|||
margin-left: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-repeat: none;
|
||||
}
|
||||
|
||||
.mx_ShareDialog_split {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -23,6 +23,7 @@ limitations under the License.
|
|||
border-radius: 3px;
|
||||
border: solid 1px $accent-color;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mx_AddressSelector.mx_AddressSelector_empty {
|
||||
|
|
66
res/css/views/elements/_StyledCheckbox.scss
Normal file
66
res/css/views/elements/_StyledCheckbox.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -190,7 +190,7 @@ limitations under the License.
|
|||
|
||||
.mx_EmojiPicker_footer {
|
||||
border-top: 1px solid $message-action-bar-border-color;
|
||||
height: 72px;
|
||||
min-height: 72px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -96,10 +96,6 @@ $AppsDrawerBodyHeight: 273px;
|
|||
height: $AppsDrawerBodyHeight;
|
||||
}
|
||||
|
||||
.mx_AppTile_persistedWrapper > div {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
|
||||
height: 114px;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
$left-gutter: 65px;
|
||||
|
||||
.mx_GroupLayout {
|
||||
|
||||
.mx_EventTile {
|
||||
> .mx_SenderProfile {
|
||||
line-height: $font-17px;
|
||||
|
@ -55,77 +54,77 @@ $left-gutter: 65px;
|
|||
.mx_MatrixChat_useCompactLayout {
|
||||
.mx_EventTile {
|
||||
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 {
|
||||
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 {
|
||||
font-size: $font-13px;
|
||||
}
|
||||
.mx_SenderProfile {
|
||||
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 {
|
||||
top: 2px;
|
||||
}
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 1px;
|
||||
|
||||
.mx_EventTile_e2eIcon {
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation {
|
||||
padding-top: 0;
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
.mx_EventTile_readAvatars {
|
||||
top: 27px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.mx_EventTile_continuation .mx_EventTile_readAvatars,
|
||||
.mx_EventTile_emote .mx_EventTile_readAvatars {
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.mx_EventTile_avatar {
|
||||
top: 2px;
|
||||
}
|
||||
.mx_EventTile_info .mx_EventTile_readAvatars {
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.mx_EventTile_e2eIcon {
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.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_EventTile_content .markdown-body {
|
||||
p, ul, ol, dl, blockquote, pre, table {
|
||||
margin-bottom: 4px; // 1/4 of the non-compact margin-bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomView_MessageList h2 {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.mx_EventTile_content .markdown-body {
|
||||
p, ul, ol, dl, blockquote, pre, table {
|
||||
margin-bottom: 4px; // 1/4 of the non-compact margin-bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ $irc-line-height: $font-18px;
|
|||
}
|
||||
|
||||
> .mx_EventTile_msgOption {
|
||||
order: 4;
|
||||
order: 5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
@ -63,6 +63,8 @@ $irc-line-height: $font-18px;
|
|||
flex-direction: column;
|
||||
order: 3;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
> .mx_EventTile_avatar {
|
||||
|
@ -90,12 +92,14 @@ $irc-line-height: $font-18px;
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
.mx_EventTile_e2eIcon {
|
||||
> .mx_EventTile_e2eIcon {
|
||||
position: relative;
|
||||
right: unset;
|
||||
left: unset;
|
||||
top: -2px;
|
||||
padding: 0;
|
||||
order: 3;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.mx_EventTile_line {
|
||||
|
@ -113,7 +117,7 @@ $irc-line-height: $font-18px;
|
|||
}
|
||||
|
||||
.mx_EventTile_reply {
|
||||
order: 3;
|
||||
order: 4;
|
||||
}
|
||||
|
||||
.mx_EditMessageComposer_buttons {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -214,8 +214,12 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/feather-customised/video.svg');
|
||||
}
|
||||
|
||||
.mx_MessageComposer_emoji::before {
|
||||
mask-image: url('$(res)/img/feather-customised/emoji3.custom.svg');
|
||||
}
|
||||
|
||||
.mx_MessageComposer_stickers::before {
|
||||
mask-image: url('$(res)/img/feather-customised/face.svg');
|
||||
mask-image: url('$(res)/img/feather-customised/sticker.custom.svg');
|
||||
}
|
||||
|
||||
.mx_MessageComposer_formatting {
|
||||
|
|
|
@ -15,6 +15,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_RoomList.mx_RoomList2 {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mx_RoomList {
|
||||
/* take up remaining space below TopLeftMenu */
|
||||
flex: 1;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd.
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,16 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_DeviceVerifyDialog_cryptoSection ul {
|
||||
display: table;
|
||||
}
|
||||
@import "../../../../node_modules/react-resizable/css/styles.css";
|
||||
|
||||
.mx_DeviceVerifyDialog_cryptoSection li {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.mx_DeviceVerifyDialog_cryptoSection label,
|
||||
.mx_DeviceVerifyDialog_cryptoSection span {
|
||||
display: table-cell;
|
||||
padding-right: 1em;
|
||||
.mx_RoomList2 .mx_RoomSubList_labelContainer {
|
||||
z-index: 12;
|
||||
}
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
height: 32px;
|
||||
height: 34px;
|
||||
margin: 0;
|
||||
padding: 0 8px 0 10px;
|
||||
position: relative;
|
||||
|
|
23
res/css/views/settings/_UpdateCheckButton.scss
Normal file
23
res/css/views/settings/_UpdateCheckButton.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -63,4 +63,25 @@ limitations under the License.
|
|||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SecurityUserSettingsTab_warning {
|
||||
color: $notice-primary-color;
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
margin-top: 30px;
|
||||
|
||||
&::before {
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 0 center;
|
||||
mask-size: $font-24px;
|
||||
position: absolute;
|
||||
width: $font-24px;
|
||||
height: $font-24px;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: $notice-primary-color;
|
||||
mask-image: url('$(res)/img/feather-customised/alert-triangle.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
5
res/img/feather-customised/alert-triangle.svg
Normal file
5
res/img/feather-customised/alert-triangle.svg
Normal 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 |
6
res/img/feather-customised/emoji3.custom.svg
Normal file
6
res/img/feather-customised/emoji3.custom.svg
Normal 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 |
4
res/img/feather-customised/sticker.custom.svg
Normal file
4
res/img/feather-customised/sticker.custom.svg
Normal 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 |
BIN
res/img/file.png
BIN
res/img/file.png
Binary file not shown.
Before Width: | Height: | Size: 482 B |
Binary file not shown.
Before Width: | Height: | Size: 503 B |
|
@ -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
19
src/@types/common.ts
Normal 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;
|
11
src/@types/global.d.ts
vendored
11
src/@types/global.d.ts
vendored
|
@ -15,13 +15,24 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as ModernizrStatic from "modernizr";
|
||||
import ContentMessages from "../ContentMessages";
|
||||
import { IMatrixClientPeg } from "../MatrixClientPeg";
|
||||
import ToastStore from "../stores/ToastStore";
|
||||
import DeviceListener from "../DeviceListener";
|
||||
import { RoomListStore2 } from "../stores/room-list/RoomListStore2";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Modernizr: ModernizrStatic;
|
||||
mxMatrixClientPeg: IMatrixClientPeg;
|
||||
Olm: {
|
||||
init: () => Promise<void>;
|
||||
};
|
||||
|
||||
mx_ContentMessages: ContentMessages;
|
||||
mx_ToastStore: ToastStore;
|
||||
mx_DeviceListener: DeviceListener;
|
||||
mx_RoomListStore2: RoomListStore2;
|
||||
}
|
||||
|
||||
// workaround for https://github.com/microsoft/TypeScript/issues/30933
|
||||
|
|
|
@ -19,6 +19,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
|||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
|
||||
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
||||
export function avatarUrlForMember(member, width, height, resizeMethod) {
|
||||
let url;
|
||||
if (member && member.getAvatarUrl) {
|
||||
|
|
|
@ -21,6 +21,22 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
|
|||
import dis from './dispatcher/dispatcher';
|
||||
import BaseEventIndexManager from './indexing/BaseEventIndexManager';
|
||||
import {ActionPayload} from "./dispatcher/payloads";
|
||||
import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload";
|
||||
import {Action} from "./dispatcher/actions";
|
||||
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
|
||||
|
||||
export const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
export const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
|
||||
export enum UpdateCheckStatus {
|
||||
Checking = "CHECKING",
|
||||
Error = "ERROR",
|
||||
NotAvailable = "NOTAVAILABLE",
|
||||
Downloading = "DOWNLOADING",
|
||||
Ready = "READY",
|
||||
}
|
||||
|
||||
const UPDATE_DEFER_KEY = "mx_defer_update";
|
||||
|
||||
/**
|
||||
* Base class for classes that provide platform-specific functionality
|
||||
|
@ -34,6 +50,7 @@ export default abstract class BasePlatform {
|
|||
|
||||
constructor() {
|
||||
dis.register(this.onAction);
|
||||
this.startUpdateCheck = this.startUpdateCheck.bind(this);
|
||||
}
|
||||
|
||||
protected onAction = (payload: ActionPayload) => {
|
||||
|
@ -56,6 +73,53 @@ export default abstract class BasePlatform {
|
|||
this.errorDidOccur = errorDidOccur;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we can call checkForUpdate on this platform build
|
||||
*/
|
||||
async canSelfUpdate(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
startUpdateCheck() {
|
||||
hideUpdateToast();
|
||||
localStorage.removeItem(UPDATE_DEFER_KEY);
|
||||
dis.dispatch<CheckUpdatesPayload>({
|
||||
action: Action.CheckUpdates,
|
||||
status: UpdateCheckStatus.Checking,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the currently running app to the latest available version
|
||||
* and replace this instance of the app with the new version.
|
||||
*/
|
||||
installUpdate() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the version update has been deferred and that deferment is still in effect
|
||||
* @param newVersion the version string to check
|
||||
*/
|
||||
protected shouldShowUpdate(newVersion: string): boolean {
|
||||
try {
|
||||
const [version, deferUntil] = JSON.parse(localStorage.getItem(UPDATE_DEFER_KEY));
|
||||
return newVersion !== version || Date.now() > deferUntil;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore the pending update and don't prompt about this version
|
||||
* until the next morning (8am).
|
||||
*/
|
||||
deferUpdate(newVersion: string) {
|
||||
const date = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
date.setHours(8, 0, 0, 0); // set to next 8am
|
||||
localStorage.setItem(UPDATE_DEFER_KEY, JSON.stringify([newVersion, date.getTime()]));
|
||||
hideUpdateToast();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the platform supports displaying
|
||||
* notifications, otherwise false.
|
||||
|
@ -157,11 +221,9 @@ export default abstract class BasePlatform {
|
|||
|
||||
setLanguage(preferredLangs: string[]) {}
|
||||
|
||||
getSSOCallbackUrl(hsUrl: string, isUrl: string, fragmentAfterLogin: string): URL {
|
||||
getSSOCallbackUrl(fragmentAfterLogin: string): URL {
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = fragmentAfterLogin || "";
|
||||
url.searchParams.set("homeserver", hsUrl);
|
||||
url.searchParams.set("identityServer", isUrl);
|
||||
return url;
|
||||
}
|
||||
|
||||
|
@ -172,12 +234,47 @@ export default abstract class BasePlatform {
|
|||
* @param {string} fragmentAfterLogin the hash to pass to the app during sso callback.
|
||||
*/
|
||||
startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) {
|
||||
const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl(),
|
||||
fragmentAfterLogin);
|
||||
// persist hs url and is url for when the user is returned to the app with the login token
|
||||
localStorage.setItem(HOMESERVER_URL_KEY, mxClient.getHomeserverUrl());
|
||||
if (mxClient.getIdentityServerUrl()) {
|
||||
localStorage.setItem(ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl());
|
||||
}
|
||||
const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin);
|
||||
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO
|
||||
}
|
||||
|
||||
onKeyDown(ev: KeyboardEvent): boolean {
|
||||
return false; // no shortcuts implemented
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a previously stored pickle key. The pickle key is used for
|
||||
* encrypting libolm objects.
|
||||
* @param {string} userId the user ID for the user that the pickle key is for.
|
||||
* @param {string} userId the device ID that the pickle key is for.
|
||||
* @returns {string|null} the previously stored pickle key, or null if no
|
||||
* pickle key has been stored.
|
||||
*/
|
||||
async getPickleKey(userId: string, deviceId: string): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and store a pickle key for encrypting libolm objects.
|
||||
* @param {string} userId the user ID for the user that the pickle key is for.
|
||||
* @param {string} userId the device ID that the pickle key is for.
|
||||
* @returns {string|null} the pickle key, or null if the platform does not
|
||||
* support storing pickle keys.
|
||||
*/
|
||||
async createPickleKey(userId: string, deviceId: string): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a previously stored pickle key from storage.
|
||||
* @param {string} userId the user ID for the user that the pickle key is for.
|
||||
* @param {string} userId the device ID that the pickle key is for.
|
||||
*/
|
||||
async destroyPickleKey(userId: string, deviceId: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,6 @@ import * as sdk from './index';
|
|||
import { _t } from './languageHandler';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
|
||||
import WidgetUtils from './utils/WidgetUtils';
|
||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
||||
import SettingsStore, { SettingLevel } from './settings/SettingsStore';
|
||||
|
@ -119,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) {
|
||||
call.on("error", function(err) {
|
||||
console.error("Call error:", err);
|
||||
if (err.code === 'unknown_devices') {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
Modal.createTrackedDialog('Call Failed', '', QuestionDialog, {
|
||||
title: _t('Call Failed'),
|
||||
description: _t(
|
||||
"There are unknown sessions in this room: "+
|
||||
"if you proceed without verifying them, it will be "+
|
||||
"possible for someone to eavesdrop on your call.",
|
||||
),
|
||||
button: _t('Review Sessions'),
|
||||
onFinished: function(confirmed) {
|
||||
if (confirmed) {
|
||||
const room = MatrixClientPeg.get().getRoom(call.roomId);
|
||||
showUnknownDeviceDialogForCalls(
|
||||
MatrixClientPeg.get(),
|
||||
room,
|
||||
() => {
|
||||
_reAttemptCall(call);
|
||||
},
|
||||
call.direction === 'outbound' ? _t("Call Anyway") : _t("Answer Anyway"),
|
||||
call.direction === 'outbound' ? _t("Call") : _t("Answer"),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (
|
||||
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
||||
SettingsStore.getValue("fallbackICEServerAllowed") === null
|
||||
) {
|
||||
_showICEFallbackPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
|
||||
title: _t('Call Failed'),
|
||||
description: err.message,
|
||||
});
|
||||
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,
|
||||
});
|
||||
});
|
||||
call.on("hangup", function() {
|
||||
_setCallState(undefined, call.roomId, "ended");
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,20 +16,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import extend from './extend';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
import RoomViewStore from './stores/RoomViewStore';
|
||||
import encrypt from "browser-encrypt-attachment";
|
||||
import extractPngChunks from "png-chunks-extract";
|
||||
import Spinner from "./components/views/elements/Spinner";
|
||||
|
||||
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
|
||||
import "blueimp-canvas-to-blob";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
|
||||
const MAX_WIDTH = 800;
|
||||
const MAX_HEIGHT = 600;
|
||||
|
@ -39,6 +42,50 @@ const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
|
|||
|
||||
export class UploadCanceledError extends Error {}
|
||||
|
||||
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
||||
|
||||
interface IUpload {
|
||||
fileName: string;
|
||||
roomId: string;
|
||||
total: number;
|
||||
loaded: number;
|
||||
promise: Promise<any>;
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
interface IMediaConfig {
|
||||
"m.upload.size"?: number;
|
||||
}
|
||||
|
||||
interface IContent {
|
||||
body: string;
|
||||
msgtype: string;
|
||||
info: {
|
||||
size: number;
|
||||
mimetype?: string;
|
||||
};
|
||||
file?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface IThumbnail {
|
||||
info: {
|
||||
thumbnail_info: {
|
||||
w: number;
|
||||
h: number;
|
||||
mimetype: string;
|
||||
size: number;
|
||||
};
|
||||
w: number;
|
||||
h: number;
|
||||
};
|
||||
thumbnail: Blob;
|
||||
}
|
||||
|
||||
interface IAbortablePromise<T> extends Promise<T> {
|
||||
abort(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a thumbnail for a image DOM element.
|
||||
* The image will be smaller than MAX_WIDTH and MAX_HEIGHT.
|
||||
|
@ -51,13 +98,13 @@ export class UploadCanceledError extends Error {}
|
|||
* about the original image and the thumbnail.
|
||||
*
|
||||
* @param {HTMLElement} element The element to thumbnail.
|
||||
* @param {integer} inputWidth The width of the image in the input element.
|
||||
* @param {integer} inputHeight the width of the image in the input element.
|
||||
* @param {number} inputWidth The width of the image in the input element.
|
||||
* @param {number} inputHeight the width of the image in the input element.
|
||||
* @param {String} mimeType The mimeType to save the blob as.
|
||||
* @return {Promise} A promise that resolves with an object with an info key
|
||||
* and a thumbnail key.
|
||||
*/
|
||||
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
||||
function createThumbnail(element: ThumbnailableElement, inputWidth: number, inputHeight: number, mimeType: string): Promise<IThumbnail> {
|
||||
return new Promise((resolve) => {
|
||||
let targetWidth = inputWidth;
|
||||
let targetHeight = inputHeight;
|
||||
|
@ -98,7 +145,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
|||
* @param {File} imageFile The file to load in an image element.
|
||||
* @return {Promise} A promise that resolves with the html image element.
|
||||
*/
|
||||
async function loadImageElement(imageFile) {
|
||||
async function loadImageElement(imageFile: File) {
|
||||
// Load the file into an html element
|
||||
const img = document.createElement("img");
|
||||
const objectUrl = URL.createObjectURL(imageFile);
|
||||
|
@ -128,8 +175,7 @@ async function loadImageElement(imageFile) {
|
|||
for (const chunk of chunks) {
|
||||
if (chunk.name === 'pHYs') {
|
||||
if (chunk.data.byteLength !== PHYS_HIDPI.length) return;
|
||||
const hidpi = chunk.data.every((val, i) => val === PHYS_HIDPI[i]);
|
||||
return hidpi;
|
||||
return chunk.data.every((val, i) => val === PHYS_HIDPI[i]);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
@ -152,7 +198,7 @@ async function loadImageElement(imageFile) {
|
|||
*/
|
||||
function infoForImageFile(matrixClient, roomId, imageFile) {
|
||||
let thumbnailType = "image/png";
|
||||
if (imageFile.type == "image/jpeg") {
|
||||
if (imageFile.type === "image/jpeg") {
|
||||
thumbnailType = "image/jpeg";
|
||||
}
|
||||
|
||||
|
@ -175,15 +221,15 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
|
|||
* @param {File} videoFile The file to load in an video element.
|
||||
* @return {Promise} A promise that resolves with the video image element.
|
||||
*/
|
||||
function loadVideoElement(videoFile) {
|
||||
function loadVideoElement(videoFile): Promise<HTMLVideoElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Load the file into an html element
|
||||
const video = document.createElement("video");
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
video.src = e.target.result;
|
||||
reader.onload = function(ev) {
|
||||
video.src = ev.target.result as string;
|
||||
|
||||
// Once ready, returns its size
|
||||
// Wait until we have enough data to thumbnail the first frame.
|
||||
|
@ -231,11 +277,11 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
|
|||
* @return {Promise} A promise that resolves with an ArrayBuffer when the file
|
||||
* is read.
|
||||
*/
|
||||
function readFileAsArrayBuffer(file) {
|
||||
function readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
resolve(e.target.result);
|
||||
resolve(e.target.result as ArrayBuffer);
|
||||
};
|
||||
reader.onerror = function(e) {
|
||||
reject(e);
|
||||
|
@ -257,11 +303,11 @@ function readFileAsArrayBuffer(file) {
|
|||
* If the file is unencrypted then the object will have a "url" key.
|
||||
* If the file is encrypted then the object will have a "file" key.
|
||||
*/
|
||||
function uploadFile(matrixClient, roomId, file, progressHandler) {
|
||||
function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blob, progressHandler?: any) {
|
||||
let canceled = false;
|
||||
if (matrixClient.isRoomEncrypted(roomId)) {
|
||||
// If the room is encrypted then encrypt the file before uploading it.
|
||||
// First read the file into memory.
|
||||
let canceled = false;
|
||||
let uploadPromise;
|
||||
let encryptInfo;
|
||||
const prom = readFileAsArrayBuffer(file).then(function(data) {
|
||||
|
@ -278,9 +324,9 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
|
|||
progressHandler: progressHandler,
|
||||
includeFilename: false,
|
||||
});
|
||||
|
||||
return uploadPromise;
|
||||
}).then(function(url) {
|
||||
if (canceled) throw new UploadCanceledError();
|
||||
// If the attachment is encrypted then bundle the URL along
|
||||
// with the information needed to decrypt the attachment and
|
||||
// add it under a file key.
|
||||
|
@ -290,7 +336,7 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
|
|||
}
|
||||
return {"file": encryptInfo};
|
||||
});
|
||||
prom.abort = () => {
|
||||
(prom as IAbortablePromise<any>).abort = () => {
|
||||
canceled = true;
|
||||
if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise);
|
||||
};
|
||||
|
@ -300,55 +346,23 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
|
|||
progressHandler: progressHandler,
|
||||
});
|
||||
const promise1 = basePromise.then(function(url) {
|
||||
if (canceled) throw new UploadCanceledError();
|
||||
// If the attachment isn't encrypted then include the URL directly.
|
||||
return {"url": url};
|
||||
});
|
||||
// XXX: copy over the abort method to the new promise
|
||||
promise1.abort = basePromise.abort;
|
||||
promise1.abort = () => {
|
||||
canceled = true;
|
||||
MatrixClientPeg.get().cancelUpload(basePromise);
|
||||
};
|
||||
return promise1;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ContentMessages {
|
||||
constructor() {
|
||||
this.inprogress = [];
|
||||
this.nextId = 0;
|
||||
this._mediaConfig = null;
|
||||
}
|
||||
private inprogress: IUpload[] = [];
|
||||
private mediaConfig: IMediaConfig = null;
|
||||
|
||||
static sharedInstance() {
|
||||
if (global.mx_ContentMessages === undefined) {
|
||||
global.mx_ContentMessages = new ContentMessages();
|
||||
}
|
||||
return global.mx_ContentMessages;
|
||||
}
|
||||
|
||||
_isFileSizeAcceptable(file) {
|
||||
if (this._mediaConfig !== null &&
|
||||
this._mediaConfig["m.upload.size"] !== undefined &&
|
||||
file.size > this._mediaConfig["m.upload.size"]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_ensureMediaConfigFetched() {
|
||||
if (this._mediaConfig !== null) return;
|
||||
|
||||
console.log("[Media Config] Fetching");
|
||||
return MatrixClientPeg.get().getMediaConfig().then((config) => {
|
||||
console.log("[Media Config] Fetched config:", config);
|
||||
return config;
|
||||
}).catch(() => {
|
||||
// Media repo can't or won't report limits, so provide an empty object (no limits).
|
||||
console.log("[Media Config] Could not fetch config, so not limiting uploads.");
|
||||
return {};
|
||||
}).then((config) => {
|
||||
this._mediaConfig = config;
|
||||
});
|
||||
}
|
||||
|
||||
sendStickerContentToRoom(url, roomId, info, text, matrixClient) {
|
||||
sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) {
|
||||
return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
|
||||
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
||||
throw e;
|
||||
|
@ -356,14 +370,14 @@ export default class ContentMessages {
|
|||
}
|
||||
|
||||
getUploadLimit() {
|
||||
if (this._mediaConfig !== null && this._mediaConfig["m.upload.size"] !== undefined) {
|
||||
return this._mediaConfig["m.upload.size"];
|
||||
if (this.mediaConfig !== null && this.mediaConfig["m.upload.size"] !== undefined) {
|
||||
return this.mediaConfig["m.upload.size"];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async sendContentListToRoom(files, roomId, matrixClient) {
|
||||
async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient) {
|
||||
if (matrixClient.isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
|
@ -372,32 +386,32 @@ export default class ContentMessages {
|
|||
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
|
||||
if (isQuoting) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const shouldUpload = await new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, {
|
||||
title: _t('Replying With Files'),
|
||||
description: (
|
||||
<div>{_t(
|
||||
'At this time it is not possible to reply with a file. ' +
|
||||
'Would you like to upload this file without replying?',
|
||||
)}</div>
|
||||
),
|
||||
hasCancelButton: true,
|
||||
button: _t("Continue"),
|
||||
onFinished: (shouldUpload) => {
|
||||
resolve(shouldUpload);
|
||||
},
|
||||
});
|
||||
const {finished} = Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, {
|
||||
title: _t('Replying With Files'),
|
||||
description: (
|
||||
<div>{_t(
|
||||
'At this time it is not possible to reply with a file. ' +
|
||||
'Would you like to upload this file without replying?',
|
||||
)}</div>
|
||||
),
|
||||
hasCancelButton: true,
|
||||
button: _t("Continue"),
|
||||
});
|
||||
const [shouldUpload]: [boolean] = await finished;
|
||||
if (!shouldUpload) return;
|
||||
}
|
||||
|
||||
await this._ensureMediaConfigFetched();
|
||||
if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to
|
||||
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
||||
await this.ensureMediaConfigFetched();
|
||||
modal.close();
|
||||
}
|
||||
|
||||
const tooBigFiles = [];
|
||||
const okFiles = [];
|
||||
|
||||
for (let i = 0; i < files.length; ++i) {
|
||||
if (this._isFileSizeAcceptable(files[i])) {
|
||||
if (this.isFileSizeAcceptable(files[i])) {
|
||||
okFiles.push(files[i]);
|
||||
} else {
|
||||
tooBigFiles.push(files[i]);
|
||||
|
@ -406,17 +420,12 @@ export default class ContentMessages {
|
|||
|
||||
if (tooBigFiles.length > 0) {
|
||||
const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog");
|
||||
const uploadFailureDialogPromise = new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, {
|
||||
badFiles: tooBigFiles,
|
||||
totalFiles: files.length,
|
||||
contentMessages: this,
|
||||
onFinished: (shouldContinue) => {
|
||||
resolve(shouldContinue);
|
||||
},
|
||||
});
|
||||
const {finished} = Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, {
|
||||
badFiles: tooBigFiles,
|
||||
totalFiles: files.length,
|
||||
contentMessages: this,
|
||||
});
|
||||
const shouldContinue = await uploadFailureDialogPromise;
|
||||
const [shouldContinue]: [boolean] = await finished;
|
||||
if (!shouldContinue) return;
|
||||
}
|
||||
|
||||
|
@ -428,31 +437,47 @@ export default class ContentMessages {
|
|||
for (let i = 0; i < okFiles.length; ++i) {
|
||||
const file = okFiles[i];
|
||||
if (!uploadAll) {
|
||||
const shouldContinue = await new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
|
||||
file,
|
||||
currentIndex: i,
|
||||
totalFiles: okFiles.length,
|
||||
onFinished: (shouldContinue, shouldUploadAll) => {
|
||||
if (shouldUploadAll) {
|
||||
uploadAll = true;
|
||||
}
|
||||
resolve(shouldContinue);
|
||||
},
|
||||
});
|
||||
const {finished} = Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
|
||||
file,
|
||||
currentIndex: i,
|
||||
totalFiles: okFiles.length,
|
||||
});
|
||||
const [shouldContinue, shouldUploadAll]: [boolean, boolean] = await finished;
|
||||
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) {
|
||||
const content = {
|
||||
getCurrentUploads() {
|
||||
return this.inprogress.filter(u => !u.canceled);
|
||||
}
|
||||
|
||||
cancelUpload(promise: Promise<any>) {
|
||||
let upload: IUpload;
|
||||
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||
if (this.inprogress[i].promise === promise) {
|
||||
upload = this.inprogress[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (upload) {
|
||||
upload.canceled = true;
|
||||
MatrixClientPeg.get().cancelUpload(upload.promise);
|
||||
dis.dispatch({action: 'upload_canceled', upload});
|
||||
}
|
||||
}
|
||||
|
||||
private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise<any>) {
|
||||
const content: IContent = {
|
||||
body: file.name || 'Attachment',
|
||||
info: {
|
||||
size: file.size,
|
||||
},
|
||||
msgtype: "", // set later
|
||||
};
|
||||
|
||||
// if we have a mime type for the file, add it to the message metadata
|
||||
|
@ -461,25 +486,25 @@ export default class ContentMessages {
|
|||
}
|
||||
|
||||
const prom = new Promise((resolve) => {
|
||||
if (file.type.indexOf('image/') == 0) {
|
||||
if (file.type.indexOf('image/') === 0) {
|
||||
content.msgtype = 'm.image';
|
||||
infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{
|
||||
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
|
||||
extend(content.info, imageInfo);
|
||||
resolve();
|
||||
}, (error)=>{
|
||||
console.error(error);
|
||||
}, (e) => {
|
||||
console.error(e);
|
||||
content.msgtype = 'm.file';
|
||||
resolve();
|
||||
});
|
||||
} else if (file.type.indexOf('audio/') == 0) {
|
||||
} else if (file.type.indexOf('audio/') === 0) {
|
||||
content.msgtype = 'm.audio';
|
||||
resolve();
|
||||
} else if (file.type.indexOf('video/') == 0) {
|
||||
} else if (file.type.indexOf('video/') === 0) {
|
||||
content.msgtype = 'm.video';
|
||||
infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{
|
||||
infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => {
|
||||
extend(content.info, videoInfo);
|
||||
resolve();
|
||||
}, (error)=>{
|
||||
}, (e) => {
|
||||
content.msgtype = 'm.file';
|
||||
resolve();
|
||||
});
|
||||
|
@ -489,19 +514,23 @@ export default class ContentMessages {
|
|||
}
|
||||
});
|
||||
|
||||
const upload = {
|
||||
// create temporary abort handler for before the actual upload gets passed off to js-sdk
|
||||
(prom as IAbortablePromise<any>).abort = () => {
|
||||
upload.canceled = true;
|
||||
};
|
||||
|
||||
const upload: IUpload = {
|
||||
fileName: file.name || 'Attachment',
|
||||
roomId: roomId,
|
||||
total: 0,
|
||||
total: file.size,
|
||||
loaded: 0,
|
||||
promise: prom,
|
||||
};
|
||||
this.inprogress.push(upload);
|
||||
dis.dispatch({action: 'upload_started'});
|
||||
|
||||
// Focus the composer view
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
|
||||
let error;
|
||||
dis.fire(Action.FocusComposer);
|
||||
|
||||
function onProgress(ev) {
|
||||
upload.total = ev.total;
|
||||
|
@ -509,7 +538,9 @@ export default class ContentMessages {
|
|||
dis.dispatch({action: 'upload_progress', upload: upload});
|
||||
}
|
||||
|
||||
let error;
|
||||
return prom.then(function() {
|
||||
if (upload.canceled) throw new UploadCanceledError();
|
||||
// XXX: upload.promise must be the promise that
|
||||
// is returned by uploadFile as it has an abort()
|
||||
// method hacked onto it.
|
||||
|
@ -520,16 +551,17 @@ export default class ContentMessages {
|
|||
content.file = result.file;
|
||||
content.url = result.url;
|
||||
});
|
||||
}).then((url) => {
|
||||
}).then(() => {
|
||||
// Await previous message being sent into the room
|
||||
return promBefore;
|
||||
}).then(function() {
|
||||
if (upload.canceled) throw new UploadCanceledError();
|
||||
return matrixClient.sendMessage(roomId, content);
|
||||
}, function(err) {
|
||||
error = err;
|
||||
if (!upload.canceled) {
|
||||
let desc = _t("The file '%(fileName)s' failed to upload.", {fileName: upload.fileName});
|
||||
if (err.http_status == 413) {
|
||||
if (err.http_status === 413) {
|
||||
desc = _t(
|
||||
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
|
||||
{fileName: upload.fileName},
|
||||
|
@ -542,11 +574,9 @@ export default class ContentMessages {
|
|||
});
|
||||
}
|
||||
}).finally(() => {
|
||||
const inprogressKeys = Object.keys(this.inprogress);
|
||||
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||
const k = inprogressKeys[i];
|
||||
if (this.inprogress[k].promise === upload.promise) {
|
||||
this.inprogress.splice(k, 1);
|
||||
if (this.inprogress[i].promise === upload.promise) {
|
||||
this.inprogress.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -555,7 +585,7 @@ export default class ContentMessages {
|
|||
// clear the media size limit so we fetch it again next time
|
||||
// we try to upload
|
||||
if (error && error.http_status === 413) {
|
||||
this._mediaConfig = null;
|
||||
this.mediaConfig = null;
|
||||
}
|
||||
dis.dispatch({action: 'upload_failed', upload, error});
|
||||
} else {
|
||||
|
@ -565,24 +595,35 @@ export default class ContentMessages {
|
|||
});
|
||||
}
|
||||
|
||||
getCurrentUploads() {
|
||||
return this.inprogress.filter(u => !u.canceled);
|
||||
private isFileSizeAcceptable(file: File) {
|
||||
if (this.mediaConfig !== null &&
|
||||
this.mediaConfig["m.upload.size"] !== undefined &&
|
||||
file.size > this.mediaConfig["m.upload.size"]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
cancelUpload(promise) {
|
||||
const inprogressKeys = Object.keys(this.inprogress);
|
||||
let upload;
|
||||
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||
const k = inprogressKeys[i];
|
||||
if (this.inprogress[k].promise === promise) {
|
||||
upload = this.inprogress[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (upload) {
|
||||
upload.canceled = true;
|
||||
MatrixClientPeg.get().cancelUpload(upload.promise);
|
||||
dis.dispatch({action: 'upload_canceled', upload});
|
||||
private ensureMediaConfigFetched() {
|
||||
if (this.mediaConfig !== null) return;
|
||||
|
||||
console.log("[Media Config] Fetching");
|
||||
return MatrixClientPeg.get().getMediaConfig().then((config) => {
|
||||
console.log("[Media Config] Fetched config:", config);
|
||||
return config;
|
||||
}).catch(() => {
|
||||
// Media repo can't or won't report limits, so provide an empty object (no limits).
|
||||
console.log("[Media Config] Could not fetch config, so not limiting uploads.");
|
||||
return {};
|
||||
}).then((config) => {
|
||||
this.mediaConfig = config;
|
||||
});
|
||||
}
|
||||
|
||||
static sharedInstance() {
|
||||
if (window.mx_ContentMessages === undefined) {
|
||||
window.mx_ContentMessages = new ContentMessages();
|
||||
}
|
||||
return window.mx_ContentMessages;
|
||||
}
|
||||
}
|
|
@ -14,43 +14,42 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import ToastStore from './stores/ToastStore';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import {
|
||||
hideToast as hideBulkUnverifiedSessionsToast,
|
||||
showToast as showBulkUnverifiedSessionsToast
|
||||
} from "./toasts/BulkUnverifiedSessionsToast";
|
||||
import {
|
||||
hideToast as hideSetupEncryptionToast,
|
||||
Kind as SetupKind,
|
||||
showToast as showSetupEncryptionToast
|
||||
} from "./toasts/SetupEncryptionToast";
|
||||
import {
|
||||
hideToast as hideUnverifiedSessionsToast,
|
||||
showToast as showUnverifiedSessionsToast
|
||||
} from "./toasts/UnverifiedSessionToast";
|
||||
import {privateShouldBeEncrypted} from "./createRoom";
|
||||
|
||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||
const THIS_DEVICE_TOAST_KEY = 'setupencryption';
|
||||
const OTHER_DEVICES_TOAST_KEY = 'reviewsessions';
|
||||
|
||||
function toastKey(deviceId) {
|
||||
return "unverified_session_" + deviceId;
|
||||
}
|
||||
|
||||
export default class DeviceListener {
|
||||
// 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() {
|
||||
if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener();
|
||||
return global.mx_DeviceListener;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// device IDs for which the user has dismissed the verify toast ('Later')
|
||||
this._dismissed = new Set();
|
||||
// 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();
|
||||
if (!window.mx_DeviceListener) window.mx_DeviceListener = new DeviceListener();
|
||||
return window.mx_DeviceListener;
|
||||
}
|
||||
|
||||
start() {
|
||||
|
@ -74,12 +73,12 @@ export default class DeviceListener {
|
|||
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
|
||||
MatrixClientPeg.get().removeListener('sync', this._onSync);
|
||||
}
|
||||
this._dismissed.clear();
|
||||
this._dismissedThisDeviceToast = false;
|
||||
this._keyBackupInfo = null;
|
||||
this._keyBackupFetchedAt = null;
|
||||
this._ourDeviceIdsAtStart = null;
|
||||
this._displayingToastsForDeviceIds = new Set();
|
||||
this.dismissed.clear();
|
||||
this.dismissedThisDeviceToast = false;
|
||||
this.keyBackupInfo = null;
|
||||
this.keyBackupFetchedAt = null;
|
||||
this.ourDeviceIdsAtStart = null;
|
||||
this.displayingToastsForDeviceIds = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -87,29 +86,29 @@ export default class DeviceListener {
|
|||
*
|
||||
* @param {String[]} deviceIds List of device IDs to dismiss notifications for
|
||||
*/
|
||||
async dismissUnverifiedSessions(deviceIds) {
|
||||
async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
|
||||
for (const d of deviceIds) {
|
||||
this._dismissed.add(d);
|
||||
this.dismissed.add(d);
|
||||
}
|
||||
|
||||
this._recheck();
|
||||
}
|
||||
|
||||
dismissEncryptionSetup() {
|
||||
this._dismissedThisDeviceToast = true;
|
||||
this.dismissedThisDeviceToast = true;
|
||||
this._recheck();
|
||||
}
|
||||
|
||||
_ensureDeviceIdsAtStartPopulated() {
|
||||
if (this._ourDeviceIdsAtStart === null) {
|
||||
if (this.ourDeviceIdsAtStart === null) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
this._ourDeviceIdsAtStart = new Set(
|
||||
this.ourDeviceIdsAtStart = new Set(
|
||||
cli.getStoredDevicesForUser(cli.getUserId()).map(d => d.deviceId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_onWillUpdateDevices = async (users, initialFetch) => {
|
||||
_onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
|
||||
// If we didn't know about *any* devices before (ie. it's fresh login),
|
||||
// then they are all pre-existing devices, so ignore this and set the
|
||||
// devicesAtStart list to the devices that we see after the fetch.
|
||||
|
@ -122,17 +121,17 @@ export default class DeviceListener {
|
|||
// before we download any new ones.
|
||||
}
|
||||
|
||||
_onDevicesUpdated = (users) => {
|
||||
_onDevicesUpdated = (users: string[]) => {
|
||||
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
||||
this._recheck();
|
||||
}
|
||||
|
||||
_onDeviceVerificationChanged = (userId) => {
|
||||
_onDeviceVerificationChanged = (userId: string) => {
|
||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this._recheck();
|
||||
}
|
||||
|
||||
_onUserTrustStatusChanged = (userId, trustLevel) => {
|
||||
_onUserTrustStatusChanged = (userId: string) => {
|
||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this._recheck();
|
||||
}
|
||||
|
@ -163,20 +162,25 @@ export default class DeviceListener {
|
|||
// & cache the result
|
||||
async _getKeyBackupInfo() {
|
||||
const now = (new Date()).getTime();
|
||||
if (!this._keyBackupInfo || this._keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
|
||||
this._keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
this._keyBackupFetchedAt = now;
|
||||
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
|
||||
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
this.keyBackupFetchedAt = now;
|
||||
}
|
||||
return this._keyBackupInfo;
|
||||
return this.keyBackupInfo;
|
||||
}
|
||||
|
||||
private shouldShowSetupEncryptionToast() {
|
||||
// In a default configuration, show the toasts. If the well-known config causes e2ee default to be false
|
||||
// then do not show the toasts until user is in at least one encrypted room.
|
||||
if (privateShouldBeEncrypted()) return true;
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
|
||||
}
|
||||
|
||||
async _recheck() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
if (
|
||||
!SettingsStore.getValue("feature_cross_signing") ||
|
||||
!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
|
||||
) return;
|
||||
if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return;
|
||||
|
||||
if (!cli.isCryptoEnabled()) return;
|
||||
// don't recheck until the initial sync is complete: lots of account data events will fire
|
||||
|
@ -186,48 +190,25 @@ export default class DeviceListener {
|
|||
|
||||
const crossSigningReady = await cli.isCrossSigningReady();
|
||||
|
||||
if (this._dismissedThisDeviceToast) {
|
||||
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
|
||||
} else {
|
||||
if (!crossSigningReady) {
|
||||
// make sure our keys are finished downlaoding
|
||||
await cli.downloadKeys([cli.getUserId()]);
|
||||
// cross signing isn't enabled - nag to enable it
|
||||
// There are 3 different toasts for:
|
||||
if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
|
||||
// Cross-signing on account but this device doesn't trust the master key (verify this session)
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: THIS_DEVICE_TOAST_KEY,
|
||||
title: _t("Verify this session"),
|
||||
icon: "verification_warning",
|
||||
props: {kind: 'verify_this_session'},
|
||||
component: sdk.getComponent("toasts.SetupEncryptionToast"),
|
||||
});
|
||||
} 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"),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.dismissedThisDeviceToast || crossSigningReady) {
|
||||
hideSetupEncryptionToast();
|
||||
} else if (this.shouldShowSetupEncryptionToast()) {
|
||||
// make sure our keys are finished downloading
|
||||
await cli.downloadKeys([cli.getUserId()]);
|
||||
// cross signing isn't enabled - nag to enable it
|
||||
// There are 3 different toasts for:
|
||||
if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
|
||||
// Cross-signing on account but this device doesn't trust the master key (verify this session)
|
||||
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
|
||||
} else {
|
||||
// cross-signing is ready, and we don't need to upgrade encryption
|
||||
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
|
||||
const backupInfo = await this._getKeyBackupInfo();
|
||||
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
|
||||
// need to remember the device IDs, but for the sake of
|
||||
// symmetry...).
|
||||
const oldUnverifiedDeviceIds = new Set();
|
||||
const oldUnverifiedDeviceIds = new Set<string>();
|
||||
// Unverified devices that have appeared since then
|
||||
const newUnverifiedDeviceIds = new Set();
|
||||
const newUnverifiedDeviceIds = new Set<string>();
|
||||
|
||||
// as long as cross-signing isn't ready,
|
||||
// you can't see or dismiss any device toasts
|
||||
if (crossSigningReady) {
|
||||
const devices = cli.getStoredDevicesForUser(cli.getUserId());
|
||||
for (const device of devices) {
|
||||
if (device.deviceId == cli.deviceId) continue;
|
||||
if (device.deviceId === cli.deviceId) continue;
|
||||
|
||||
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
|
||||
if (!deviceTrust.isCrossSigningVerified() && !this._dismissed.has(device.deviceId)) {
|
||||
if (this._ourDeviceIdsAtStart.has(device.deviceId)) {
|
||||
if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) {
|
||||
if (this.ourDeviceIdsAtStart.has(device.deviceId)) {
|
||||
oldUnverifiedDeviceIds.add(device.deviceId);
|
||||
} else {
|
||||
newUnverifiedDeviceIds.add(device.deviceId);
|
||||
|
@ -263,38 +244,23 @@ export default class DeviceListener {
|
|||
|
||||
// Display or hide the batch toast for old unverified sessions
|
||||
if (oldUnverifiedDeviceIds.size > 0) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: OTHER_DEVICES_TOAST_KEY,
|
||||
title: _t("Review where you’re logged in"),
|
||||
icon: "verification_warning",
|
||||
priority: ToastStore.PRIORITY_LOW,
|
||||
props: {
|
||||
deviceIds: oldUnverifiedDeviceIds,
|
||||
},
|
||||
component: sdk.getComponent("toasts.BulkUnverifiedSessionsToast"),
|
||||
});
|
||||
showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
|
||||
} else {
|
||||
ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY);
|
||||
hideBulkUnverifiedSessionsToast();
|
||||
}
|
||||
|
||||
// Show toasts for new unverified devices if they aren't already there
|
||||
for (const deviceId of newUnverifiedDeviceIds) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: toastKey(deviceId),
|
||||
title: _t("New login. Was this you?"),
|
||||
icon: "verification_warning",
|
||||
props: { deviceId },
|
||||
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
|
||||
});
|
||||
showUnverifiedSessionsToast(deviceId);
|
||||
}
|
||||
|
||||
// ...and hide any we don't need any more
|
||||
for (const deviceId of this._displayingToastsForDeviceIds) {
|
||||
for (const deviceId of this.displayingToastsForDeviceIds) {
|
||||
if (!newUnverifiedDeviceIds.has(deviceId)) {
|
||||
ToastStore.sharedInstance().dismissToast(toastKey(deviceId));
|
||||
hideUnverifiedSessionsToast(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
this._displayingToastsForDeviceIds = newUnverifiedDeviceIds;
|
||||
this.displayingToastsForDeviceIds = newUnverifiedDeviceIds;
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import { _t } from './languageHandler';
|
|||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import GroupStore from './stores/GroupStore';
|
||||
import {allSettled} from "./utils/promise";
|
||||
import StyledCheckbox from './components/views/elements/StyledCheckbox';
|
||||
|
||||
export function showGroupInviteDialog(groupId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -61,19 +62,19 @@ export function showGroupAddRoomDialog(groupId) {
|
|||
<div>{ _t("Which rooms would you like to add to this community?") }</div>
|
||||
</div>;
|
||||
|
||||
const checkboxContainer = <label className="mx_GroupAddressPicker_checkboxContainer">
|
||||
<input type="checkbox" onChange={onCheckboxClicked} />
|
||||
<div>
|
||||
{ _t("Show these rooms to non-members on the community page and room list?") }
|
||||
</div>
|
||||
</label>;
|
||||
const checkboxContainer = <StyledCheckbox
|
||||
className="mx_GroupAddressPicker_checkboxContainer"
|
||||
onChange={onCheckboxClicked}
|
||||
>
|
||||
{ _t("Show these rooms to non-members on the community page and room list?") }
|
||||
</StyledCheckbox>;
|
||||
|
||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
|
||||
title: _t("Add rooms to the community"),
|
||||
description: description,
|
||||
extraNode: checkboxContainer,
|
||||
placeholder: _t("Room name or alias"),
|
||||
placeholder: _t("Room name or address"),
|
||||
button: _t("Add to community"),
|
||||
pickerType: 'room',
|
||||
validAddressTypes: ['mx-room-id'],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
|||
import {Mjolnir} from "./mjolnir/Mjolnir";
|
||||
import DeviceListener from "./DeviceListener";
|
||||
import {Jitsi} from "./widgets/Jitsi";
|
||||
import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "./BasePlatform";
|
||||
|
||||
/**
|
||||
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
||||
|
@ -163,14 +164,16 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
|||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
if (!queryParams.homeserver) {
|
||||
const homeserver = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||
const identityServer = localStorage.getItem(ID_SERVER_URL_KEY);
|
||||
if (!homeserver) {
|
||||
console.warn("Cannot log in with token: can't determine HS URL to use");
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return sendLoginRequest(
|
||||
queryParams.homeserver,
|
||||
queryParams.identityServer,
|
||||
homeserver,
|
||||
identityServer,
|
||||
"m.login.token", {
|
||||
token: queryParams.loginToken,
|
||||
initial_device_display_name: defaultDeviceDisplayName,
|
||||
|
@ -256,8 +259,8 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
|||
* @returns {Object} Information about the session - see implementation for variables.
|
||||
*/
|
||||
export function getLocalStorageSessionVars() {
|
||||
const hsUrl = localStorage.getItem("mx_hs_url");
|
||||
const isUrl = localStorage.getItem("mx_is_url");
|
||||
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY);
|
||||
const accessToken = localStorage.getItem("mx_access_token");
|
||||
const userId = localStorage.getItem("mx_user_id");
|
||||
const deviceId = localStorage.getItem("mx_device_id");
|
||||
|
@ -298,6 +301,8 @@ async function _restoreFromLocalStorage(opts) {
|
|||
return false;
|
||||
}
|
||||
|
||||
const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId);
|
||||
|
||||
console.log(`Restoring session for ${userId}`);
|
||||
await _doSetLoggedIn({
|
||||
userId: userId,
|
||||
|
@ -306,6 +311,7 @@ async function _restoreFromLocalStorage(opts) {
|
|||
homeserverUrl: hsUrl,
|
||||
identityServerUrl: isUrl,
|
||||
guest: isGuest,
|
||||
pickleKey: pickleKey,
|
||||
}, false);
|
||||
return true;
|
||||
} else {
|
||||
|
@ -348,9 +354,13 @@ async function _handleLoadSessionFailure(e) {
|
|||
*
|
||||
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
|
||||
*/
|
||||
export function setLoggedIn(credentials) {
|
||||
export async function setLoggedIn(credentials) {
|
||||
stopMatrixClient();
|
||||
return _doSetLoggedIn(credentials, true);
|
||||
const pickleKey = credentials.userId && credentials.deviceId
|
||||
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
|
||||
: null;
|
||||
|
||||
return _doSetLoggedIn(Object.assign({}, credentials, {pickleKey}), true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -479,9 +489,9 @@ function _showStorageEvictedDialog() {
|
|||
class AbortLoginAndRebuildStorage extends Error { }
|
||||
|
||||
function _persistCredentialsToLocalStorage(credentials) {
|
||||
localStorage.setItem("mx_hs_url", credentials.homeserverUrl);
|
||||
localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
|
||||
if (credentials.identityServerUrl) {
|
||||
localStorage.setItem("mx_is_url", credentials.identityServerUrl);
|
||||
localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl);
|
||||
}
|
||||
localStorage.setItem("mx_user_id", credentials.userId);
|
||||
localStorage.setItem("mx_access_token", credentials.accessToken);
|
||||
|
@ -516,7 +526,9 @@ export function logout() {
|
|||
}
|
||||
|
||||
_isLoggingOut = true;
|
||||
MatrixClientPeg.get().logout().then(onLoggedOut,
|
||||
const client = MatrixClientPeg.get();
|
||||
PlatformPeg.get().destroyPickleKey(client.getUserId(), client.getDeviceId());
|
||||
client.logout().then(onLoggedOut,
|
||||
(err) => {
|
||||
// Just throwing an error here is going to be very unhelpful
|
||||
// if you're trying to log out because your server's down and
|
||||
|
@ -575,10 +587,12 @@ async function startMatrixClient(startSyncing=true) {
|
|||
// to work).
|
||||
dis.dispatch({action: 'will_start_client'}, true);
|
||||
|
||||
// reset things first just in case
|
||||
TypingStore.sharedInstance().reset();
|
||||
ToastStore.sharedInstance().reset();
|
||||
|
||||
Notifier.start();
|
||||
UserActivity.sharedInstance().start();
|
||||
TypingStore.sharedInstance().reset(); // just in case
|
||||
ToastStore.sharedInstance().reset();
|
||||
DMRoomMap.makeShared().start();
|
||||
IntegrationManagers.sharedInstance().startWatching();
|
||||
ActiveWidgetStore.start();
|
||||
|
@ -608,7 +622,7 @@ async function startMatrixClient(startSyncing=true) {
|
|||
}
|
||||
|
||||
// Now that we have a MatrixClientPeg, update the Jitsi info
|
||||
await Jitsi.getInstance().update();
|
||||
await Jitsi.getInstance().start();
|
||||
|
||||
// dispatch that we finished starting up to wire up any other bits
|
||||
// of the matrix client that cannot be set prior to starting up.
|
||||
|
|
|
@ -17,8 +17,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClient, MemoryStore} from 'matrix-js-sdk';
|
||||
|
||||
import {MatrixClient} from 'matrix-js-sdk/src/client';
|
||||
import {MemoryStore} from 'matrix-js-sdk/src/store/memory';
|
||||
import * as utils from 'matrix-js-sdk/src/utils';
|
||||
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
|
||||
import {EventTimelineSet} from 'matrix-js-sdk/src/models/event-timeline-set';
|
||||
|
@ -34,37 +34,26 @@ import IdentityAuthClient from './IdentityAuthClient';
|
|||
import { crossSigningCallbacks } from './CrossSigningManager';
|
||||
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
|
||||
interface MatrixClientCreds {
|
||||
export interface IMatrixClientCreds {
|
||||
homeserverUrl: string,
|
||||
identityServerUrl: string,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
accessToken: string,
|
||||
guest: boolean,
|
||||
pickleKey?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper object for handling the js-sdk Matrix Client object in the react-sdk
|
||||
* Handles the creation/initialisation of client objects.
|
||||
* This module provides a singleton instance of this class so the 'current'
|
||||
* Matrix Client object is available easily.
|
||||
*/
|
||||
class _MatrixClientPeg {
|
||||
constructor() {
|
||||
this.matrixClient = null;
|
||||
this._justRegisteredUserId = null;
|
||||
// TODO: Move this to the js-sdk
|
||||
export interface IOpts {
|
||||
initialSyncLimit?: number;
|
||||
pendingEventOrdering?: "detached" | "chronological";
|
||||
lazyLoadMembers?: boolean;
|
||||
clientWellKnownPollPeriod?: number;
|
||||
}
|
||||
|
||||
// These are the default options used when when the
|
||||
// client is started in 'start'. These can be altered
|
||||
// at any time up to after the 'will_start_client'
|
||||
// event is finished processing.
|
||||
this.opts = {
|
||||
initialSyncLimit: 20,
|
||||
};
|
||||
// the credentials used to init the current client object.
|
||||
// used if we tear it down & recreate it with a different store
|
||||
this._currentClientCreds = null;
|
||||
}
|
||||
export interface IMatrixClientPeg {
|
||||
opts: IOpts;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
setIndexedDbWorkerScript(script) {
|
||||
createMatrixClient.indexedDbWorkerScript = script;
|
||||
}
|
||||
setIndexedDbWorkerScript(script: string): void;
|
||||
|
||||
get(): MatrixClient {
|
||||
return this.matrixClient;
|
||||
}
|
||||
/**
|
||||
* Return the server name of the user's homeserver
|
||||
* Throws an error if unable to deduce the homeserver name
|
||||
* (eg. if the user is not logged in)
|
||||
*
|
||||
* @returns {string} The homeserver name, if present.
|
||||
*/
|
||||
getHomeserverName(): string;
|
||||
|
||||
unset() {
|
||||
this.matrixClient = null;
|
||||
get(): MatrixClient;
|
||||
unset(): void;
|
||||
assign(): Promise<any>;
|
||||
start(): Promise<any>;
|
||||
|
||||
MatrixActionCreators.stop();
|
||||
}
|
||||
getCredentials(): IMatrixClientCreds;
|
||||
|
||||
/**
|
||||
* If we've registered a user ID we set this to the ID of the
|
||||
|
@ -95,9 +88,7 @@ class _MatrixClientPeg {
|
|||
*
|
||||
* @param {string} uid The user ID of the user we've just registered
|
||||
*/
|
||||
setJustRegisteredUserId(uid) {
|
||||
this._justRegisteredUserId = uid;
|
||||
}
|
||||
setJustRegisteredUserId(uid: string): void;
|
||||
|
||||
/**
|
||||
* Returns true if the current user has just been registered by this
|
||||
|
@ -105,23 +96,73 @@ class _MatrixClientPeg {
|
|||
*
|
||||
* @returns {bool} True if user has just been registered
|
||||
*/
|
||||
currentUserIsJustRegistered() {
|
||||
currentUserIsJustRegistered(): boolean;
|
||||
|
||||
/**
|
||||
* Replace this MatrixClientPeg's client with a client instance that has
|
||||
* homeserver / identity server URLs and active credentials
|
||||
*
|
||||
* @param {IMatrixClientCreds} creds The new credentials to use.
|
||||
*/
|
||||
replaceUsingCreds(creds: IMatrixClientCreds): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper object for handling the js-sdk Matrix Client object in the react-sdk
|
||||
* Handles the creation/initialisation of client objects.
|
||||
* This module provides a singleton instance of this class so the 'current'
|
||||
* Matrix Client object is available easily.
|
||||
*/
|
||||
class _MatrixClientPeg implements IMatrixClientPeg {
|
||||
// These are the default options used when when the
|
||||
// client is started in 'start'. These can be altered
|
||||
// at any time up to after the 'will_start_client'
|
||||
// event is finished processing.
|
||||
public opts: IOpts = {
|
||||
initialSyncLimit: 20,
|
||||
};
|
||||
|
||||
private matrixClient: MatrixClient = null;
|
||||
private justRegisteredUserId: string;
|
||||
|
||||
// the credentials used to init the current client object.
|
||||
// used if we tear it down & recreate it with a different store
|
||||
private currentClientCreds: IMatrixClientCreds;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public setIndexedDbWorkerScript(script: string): void {
|
||||
createMatrixClient.indexedDbWorkerScript = script;
|
||||
}
|
||||
|
||||
public get(): MatrixClient {
|
||||
return this.matrixClient;
|
||||
}
|
||||
|
||||
public unset(): void {
|
||||
this.matrixClient = null;
|
||||
|
||||
MatrixActionCreators.stop();
|
||||
}
|
||||
|
||||
public setJustRegisteredUserId(uid: string): void {
|
||||
this.justRegisteredUserId = uid;
|
||||
}
|
||||
|
||||
public currentUserIsJustRegistered(): boolean {
|
||||
return (
|
||||
this.matrixClient &&
|
||||
this.matrixClient.credentials.userId === this._justRegisteredUserId
|
||||
this.matrixClient.credentials.userId === this.justRegisteredUserId
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Replace this MatrixClientPeg's client with a client instance that has
|
||||
* homeserver / identity server URLs and active credentials
|
||||
*/
|
||||
replaceUsingCreds(creds: MatrixClientCreds) {
|
||||
this._currentClientCreds = creds;
|
||||
this._createClient(creds);
|
||||
public replaceUsingCreds(creds: IMatrixClientCreds): void {
|
||||
this.currentClientCreds = creds;
|
||||
this.createClient(creds);
|
||||
}
|
||||
|
||||
async assign() {
|
||||
public async assign(): Promise<any> {
|
||||
for (const dbType of ['indexeddb', 'memory']) {
|
||||
try {
|
||||
const promise = this.matrixClient.store.startup();
|
||||
|
@ -132,7 +173,7 @@ class _MatrixClientPeg {
|
|||
if (dbType === 'indexeddb') {
|
||||
console.error('Error starting matrixclient store - falling back to memory store', err);
|
||||
this.matrixClient.store = new MemoryStore({
|
||||
localStorage: global.localStorage,
|
||||
localStorage: localStorage,
|
||||
});
|
||||
} else {
|
||||
console.error('Failed to start memory store!', err);
|
||||
|
@ -158,9 +199,7 @@ class _MatrixClientPeg {
|
|||
// The js-sdk found a crypto DB too new for it to use
|
||||
const CryptoStoreTooNewDialog =
|
||||
sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog");
|
||||
Modal.createDialog(CryptoStoreTooNewDialog, {
|
||||
host: window.location.host,
|
||||
});
|
||||
Modal.createDialog(CryptoStoreTooNewDialog);
|
||||
}
|
||||
// this can happen for a number of reasons, the most likely being
|
||||
// that the olm library was missing. It's not fatal.
|
||||
|
@ -171,6 +210,7 @@ class _MatrixClientPeg {
|
|||
// the react sdk doesn't work without this, so don't allow
|
||||
opts.pendingEventOrdering = "detached";
|
||||
opts.lazyLoadMembers = true;
|
||||
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
|
||||
|
||||
// Connect the matrix client to the dispatcher and setting handlers
|
||||
MatrixActionCreators.start(this.matrixClient);
|
||||
|
@ -179,7 +219,7 @@ class _MatrixClientPeg {
|
|||
return opts;
|
||||
}
|
||||
|
||||
async start() {
|
||||
public async start(): Promise<any> {
|
||||
const opts = await this.assign();
|
||||
|
||||
console.log(`MatrixClientPeg: really starting MatrixClient`);
|
||||
|
@ -187,7 +227,7 @@ class _MatrixClientPeg {
|
|||
console.log(`MatrixClientPeg: MatrixClient started`);
|
||||
}
|
||||
|
||||
getCredentials(): MatrixClientCreds {
|
||||
public getCredentials(): IMatrixClientCreds {
|
||||
return {
|
||||
homeserverUrl: this.matrixClient.baseUrl,
|
||||
identityServerUrl: this.matrixClient.idBaseUrl,
|
||||
|
@ -198,12 +238,7 @@ class _MatrixClientPeg {
|
|||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Return the server name of the user's homeserver
|
||||
* Throws an error if unable to deduce the homeserver name
|
||||
* (eg. if the user is not logged in)
|
||||
*/
|
||||
getHomeserverName() {
|
||||
public getHomeserverName(): string {
|
||||
const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId);
|
||||
if (matches === null || matches.length < 1) {
|
||||
throw new Error("Failed to derive homeserver name from user ID!");
|
||||
|
@ -211,13 +246,15 @@ class _MatrixClientPeg {
|
|||
return matches[1];
|
||||
}
|
||||
|
||||
_createClient(creds: MatrixClientCreds) {
|
||||
private createClient(creds: IMatrixClientCreds): void {
|
||||
// TODO: Make these opts typesafe with the js-sdk
|
||||
const opts = {
|
||||
baseUrl: creds.homeserverUrl,
|
||||
idBaseUrl: creds.identityServerUrl,
|
||||
accessToken: creds.accessToken,
|
||||
userId: creds.userId,
|
||||
deviceId: creds.deviceId,
|
||||
pickleKey: creds.pickleKey,
|
||||
timelineSupport: true,
|
||||
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
|
||||
fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'),
|
||||
|
@ -228,9 +265,9 @@ class _MatrixClientPeg {
|
|||
],
|
||||
unstableClientRelationAggregation: true,
|
||||
identityServer: new IdentityAuthClient(),
|
||||
cryptoCallbacks: {},
|
||||
};
|
||||
|
||||
opts.cryptoCallbacks = {};
|
||||
// These are always installed regardless of the labs flag so that
|
||||
// cross-signing features can toggle on without reloading and also be
|
||||
// accessed immediately after login.
|
||||
|
@ -253,8 +290,8 @@ class _MatrixClientPeg {
|
|||
}
|
||||
}
|
||||
|
||||
if (!global.mxMatrixClientPeg) {
|
||||
global.mxMatrixClientPeg = new _MatrixClientPeg();
|
||||
if (!window.mxMatrixClientPeg) {
|
||||
window.mxMatrixClientPeg = new _MatrixClientPeg();
|
||||
}
|
||||
|
||||
export const MatrixClientPeg = global.mxMatrixClientPeg;
|
||||
export const MatrixClientPeg = window.mxMatrixClientPeg;
|
|
@ -26,6 +26,9 @@ import * as sdk from './index';
|
|||
import { _t } from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||
import {
|
||||
hideToast as hideNotificationsToast,
|
||||
} from "./toasts/DesktopNotificationsToast";
|
||||
|
||||
/*
|
||||
* Dispatches:
|
||||
|
@ -278,12 +281,7 @@ const Notifier = {
|
|||
|
||||
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
|
||||
|
||||
// XXX: why are we dispatching this here?
|
||||
// this is nothing to do with notifier_enabled
|
||||
dis.dispatch({
|
||||
action: "notifier_enabled",
|
||||
value: this.isEnabled(),
|
||||
});
|
||||
hideNotificationsToast();
|
||||
|
||||
// update the info to localStorage for persistent settings
|
||||
if (persistent && global.localStorage) {
|
||||
|
|
|
@ -84,8 +84,14 @@ export default class PasswordReset {
|
|||
|
||||
try {
|
||||
await this.client.setPassword({
|
||||
// Note: Though this sounds like a login type for identity servers only, it
|
||||
// has a dual purpose of being used for homeservers too.
|
||||
type: "m.login.email.identity",
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/matrix-org/synapse/issues/5665
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
threepid_creds: creds,
|
||||
threepidCreds: creds,
|
||||
}, this.password);
|
||||
} catch (err) {
|
||||
if (err.httpStatus === 401) {
|
||||
|
|
|
@ -450,8 +450,8 @@ export const Commands = [
|
|||
new Command({
|
||||
command: 'join',
|
||||
aliases: ['j', 'goto'],
|
||||
args: '<room-alias>',
|
||||
description: _td('Joins room with given alias'),
|
||||
args: '<room-address>',
|
||||
description: _td('Joins room with given address'),
|
||||
runFn: function(_, args) {
|
||||
if (args) {
|
||||
// Note: we support 2 versions of this command. The first is
|
||||
|
@ -562,7 +562,7 @@ export const Commands = [
|
|||
}),
|
||||
new Command({
|
||||
command: 'part',
|
||||
args: '[<room-alias>]',
|
||||
args: '[<room-address>]',
|
||||
description: _td('Leave room'),
|
||||
runFn: function(roomId, args) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -594,7 +594,7 @@ export const Commands = [
|
|||
}
|
||||
if (targetRoomId) break;
|
||||
}
|
||||
if (!targetRoomId) return reject(_t('Unrecognised room alias:') + ' ' + roomAlias);
|
||||
if (!targetRoomId) return reject(_t('Unrecognised room address:') + ' ' + roomAlias);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -22,7 +22,6 @@ import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
|||
import PropTypes from 'prop-types';
|
||||
import {_t, _td} from '../../../../languageHandler';
|
||||
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||
import SettingsStore from '../../../../settings/SettingsStore';
|
||||
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
||||
import {copyNode} from "../../../../utils/strings";
|
||||
import PassphraseField from "../../../../components/views/auth/PassphraseField";
|
||||
|
@ -67,10 +66,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
|
||||
async componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const secureSecretStorage = (
|
||||
SettingsStore.getValue("feature_cross_signing") &&
|
||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
|
||||
);
|
||||
const secureSecretStorage = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||
this.setState({ secureSecretStorage });
|
||||
|
||||
// If we're using secret storage, skip ahead to the backing up step, as
|
||||
|
@ -284,8 +280,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let matchText;
|
||||
let changeText;
|
||||
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
||||
matchText = _t("That matches!");
|
||||
changeText = _t("Use a different passphrase?");
|
||||
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
|
||||
// only tell them they're wrong if they've actually gone wrong.
|
||||
// Security concious readers will note that if you left riot-web unattended
|
||||
|
@ -295,6 +293,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
// Note that not having typed anything at all will not hit this clause and
|
||||
// fall through so empty box === no hint.
|
||||
matchText = _t("That doesn't match.");
|
||||
changeText = _t("Go back to set it again.");
|
||||
}
|
||||
|
||||
let passPhraseMatch = null;
|
||||
|
@ -303,7 +302,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
<div>{matchText}</div>
|
||||
<div>
|
||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||
{_t("Go back to set it again.")}
|
||||
{changeText}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -201,7 +201,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
type: 'm.id.user',
|
||||
user: MatrixClientPeg.get().getUserId(),
|
||||
},
|
||||
// https://github.com/matrix-org/synapse/issues/5665
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/matrix-org/synapse/issues/5665
|
||||
user: MatrixClientPeg.get().getUserId(),
|
||||
password: this.state.accountPassword,
|
||||
});
|
||||
|
@ -538,8 +539,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
const Field = sdk.getComponent('views.elements.Field');
|
||||
|
||||
let matchText;
|
||||
let changeText;
|
||||
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
||||
matchText = _t("That matches!");
|
||||
changeText = _t("Use a different passphrase?");
|
||||
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
|
||||
// only tell them they're wrong if they've actually gone wrong.
|
||||
// Security concious readers will note that if you left riot-web unattended
|
||||
|
@ -549,6 +552,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
// Note that not having typed anything at all will not hit this clause and
|
||||
// fall through so empty box === no hint.
|
||||
matchText = _t("That doesn't match.");
|
||||
changeText = _t("Go back to set it again.");
|
||||
}
|
||||
|
||||
let passPhraseMatch = null;
|
||||
|
@ -557,7 +561,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
<div>{matchText}</div>
|
||||
<div>
|
||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||
{_t("Go back to set it again.")}
|
||||
{changeText}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -90,11 +90,12 @@ export default class CommunityProvider extends AutocompleteProvider {
|
|||
type: "community",
|
||||
href: makeGroupPermalink(groupId),
|
||||
component: (
|
||||
<PillCompletion initialComponent={
|
||||
<PillCompletion title={name} description={groupId}>
|
||||
<BaseAvatar name={name || groupId}
|
||||
width={24} height={24}
|
||||
width={24}
|
||||
height={24}
|
||||
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} />
|
||||
} title={name} description={groupId} />
|
||||
</PillCompletion>
|
||||
),
|
||||
range,
|
||||
}))
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {forwardRef} from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/* These were earlier stateless functional components but had to be converted
|
||||
|
@ -30,50 +30,37 @@ interface ITextualCompletionProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
export class TextualCompletion extends React.PureComponent<ITextualCompletionProps> {
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
className,
|
||||
...restProps
|
||||
} = this.props;
|
||||
return (
|
||||
<div className={classNames('mx_Autocomplete_Completion_block', className)} role="option" {...restProps}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
export const TextualCompletion = forwardRef<ITextualCompletionProps, any>((props, ref) => {
|
||||
const {title, subtitle, description, className, ...restProps} = props;
|
||||
return (
|
||||
<div {...restProps}
|
||||
className={classNames('mx_Autocomplete_Completion_block', className)}
|
||||
role="option"
|
||||
ref={ref}
|
||||
>
|
||||
<span className="mx_Autocomplete_Completion_title">{ title }</span>
|
||||
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
|
||||
<span className="mx_Autocomplete_Completion_description">{ description }</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface IPillCompletionProps extends ITextualCompletionProps {
|
||||
children?: React.ReactNode,
|
||||
}
|
||||
|
||||
interface IPillCompletionProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
initialComponent?: React.ReactNode,
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export class PillCompletion extends React.PureComponent<IPillCompletionProps> {
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
initialComponent,
|
||||
className,
|
||||
...restProps
|
||||
} = this.props;
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
export const PillCompletion = forwardRef<IPillCompletionProps, any>((props, ref) => {
|
||||
const {title, subtitle, description, className, children, ...restProps} = props;
|
||||
return (
|
||||
<div {...restProps}
|
||||
className={classNames('mx_Autocomplete_Completion_pill', className)}
|
||||
role="option"
|
||||
ref={ref}
|
||||
>
|
||||
{ children }
|
||||
<span className="mx_Autocomplete_Completion_title">{ title }</span>
|
||||
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
|
||||
<span className="mx_Autocomplete_Completion_description">{ description }</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -69,7 +69,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
|
||||
constructor() {
|
||||
super(EMOJI_REGEX);
|
||||
this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, {
|
||||
this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTNAMES, {
|
||||
keys: ['emoji.emoticon', 'shortname'],
|
||||
funcs: [
|
||||
(o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases
|
||||
|
@ -121,9 +121,9 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
return {
|
||||
completion: unicode,
|
||||
component: (
|
||||
<PillCompletion title={shortname} aria-label={unicode} initialComponent={
|
||||
<span style={{maxWidth: '1em'}}>{ unicode }</span>
|
||||
} />
|
||||
<PillCompletion title={shortname} aria-label={unicode}>
|
||||
<span>{ unicode }</span>
|
||||
</PillCompletion>
|
||||
),
|
||||
range,
|
||||
};
|
||||
|
|
|
@ -48,7 +48,9 @@ export default class NotifProvider extends AutocompleteProvider {
|
|||
type: "at-room",
|
||||
suffix: ' ',
|
||||
component: (
|
||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={this.room} />} title="@room" description={_t("Notify the whole room")} />
|
||||
<PillCompletion title="@room" description={_t("Notify the whole room")}>
|
||||
<RoomAvatar width={24} height={24} room={this.room} />
|
||||
</PillCompletion>
|
||||
),
|
||||
range,
|
||||
}];
|
||||
|
|
|
@ -45,7 +45,7 @@ interface IOptions<T extends {}> {
|
|||
* @param {function[]} options.funcs List of functions that when called with the
|
||||
* object as an arg will return a string to use as an index
|
||||
*/
|
||||
export default class QueryMatcher<T> {
|
||||
export default class QueryMatcher<T extends Object> {
|
||||
private _options: IOptions<T>;
|
||||
private _keys: IOptions<T>["keys"];
|
||||
private _funcs: Required<IOptions<T>["funcs"]>;
|
||||
|
@ -75,7 +75,11 @@ export default class QueryMatcher<T> {
|
|||
this._items = new Map();
|
||||
|
||||
for (const object of objects) {
|
||||
const keyValues = _at(object, this._keys);
|
||||
// Need to use unsafe coerce here because the objects can have any
|
||||
// type for their values. We assume that those values who's keys have
|
||||
// been specified will be string. Also, we cannot infer all the
|
||||
// types of the keys of the objects at compile.
|
||||
const keyValues = _at<string>(<any>object, this._keys);
|
||||
|
||||
for (const f of this._funcs) {
|
||||
keyValues.push(f(object));
|
||||
|
|
|
@ -103,7 +103,9 @@ export default class RoomProvider extends AutocompleteProvider {
|
|||
suffix: ' ',
|
||||
href: makeRoomPermalink(room.displayedAlias),
|
||||
component: (
|
||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.room.name} description={room.displayedAlias} />
|
||||
<PillCompletion title={room.room.name} description={room.displayedAlias}>
|
||||
<RoomAvatar width={24} height={24} room={room.room} />
|
||||
</PillCompletion>
|
||||
),
|
||||
range,
|
||||
};
|
||||
|
|
|
@ -125,10 +125,9 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
suffix: (selection.beginning && range.start === 0) ? ': ' : ' ',
|
||||
href: makeUserPermalink(user.userId),
|
||||
component: (
|
||||
<PillCompletion
|
||||
initialComponent={<MemberAvatar member={user} width={24} height={24} />}
|
||||
title={displayName}
|
||||
description={user.userId} />
|
||||
<PillCompletion title={displayName} description={user.userId}>
|
||||
<MemberAvatar member={user} width={24} height={24} />
|
||||
</PillCompletion>
|
||||
),
|
||||
range,
|
||||
};
|
||||
|
|
|
@ -92,7 +92,7 @@ const CategoryRoomList = createReactClass({
|
|||
Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
|
||||
title: _t('Add rooms to the community summary'),
|
||||
description: _t("Which rooms would you like to add to this summary?"),
|
||||
placeholder: _t("Room name or alias"),
|
||||
placeholder: _t("Room name or address"),
|
||||
button: _t("Add to summary"),
|
||||
pickerType: 'room',
|
||||
validAddressTypes: ['mx-room-id'],
|
||||
|
|
|
@ -26,7 +26,7 @@ import * as VectorConferenceHandler from '../../VectorConferenceHandler';
|
|||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import {_t} from "../../languageHandler";
|
||||
import Analytics from "../../Analytics";
|
||||
import RoomList2 from "../views/rooms/RoomList2";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
|
||||
|
||||
const LeftPanel = createReactClass({
|
||||
|
@ -198,7 +198,7 @@ const LeftPanel = createReactClass({
|
|||
|
||||
onSearchCleared: function(source) {
|
||||
if (source === "keyboard") {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
this.setState({searchExpanded: false});
|
||||
},
|
||||
|
@ -274,28 +274,15 @@ const LeftPanel = createReactClass({
|
|||
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
|
||||
}
|
||||
|
||||
let roomList = null;
|
||||
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
|
||||
roomList = <RoomList2
|
||||
onKeyDown={this._onKeyDown}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ref={this.collectRoomList}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
/>;
|
||||
} else {
|
||||
roomList = <RoomList
|
||||
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} />;
|
||||
}
|
||||
const 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 (
|
||||
<div className={containerClasses}>
|
||||
|
|
154
src/components/structures/LeftPanel2.tsx
Normal file
154
src/components/structures/LeftPanel2.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ import { fixupColorFonts } from '../../utils/FontManager';
|
|||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import sessionStore from '../../stores/SessionStore';
|
||||
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
|
||||
import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
|
@ -43,6 +43,17 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
|
|||
import PlatformPeg from "../../PlatformPeg";
|
||||
import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
|
||||
import { DefaultTagID } from "../../stores/room-list/models";
|
||||
import {
|
||||
showToast as showSetPasswordToast,
|
||||
hideToast as hideSetPasswordToast
|
||||
} from "../../toasts/SetPasswordToast";
|
||||
import {
|
||||
showToast as showServerLimitToast,
|
||||
hideToast as hideServerLimitToast
|
||||
} from "../../toasts/ServerLimitToast";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import LeftPanel2 from "./LeftPanel2";
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
// NB. this is just for server notices rather than pinned messages in general.
|
||||
|
@ -57,7 +68,7 @@ function canElementReceiveInput(el) {
|
|||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
onRegistered: (credentials: MatrixClientCreds) => Promise<MatrixClient>;
|
||||
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
|
||||
viaServers?: string[];
|
||||
hideToSRUsers: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
|
@ -65,10 +76,6 @@ interface IProps {
|
|||
initialEventPixelOffset: number;
|
||||
leftDisabled: boolean;
|
||||
rightDisabled: boolean;
|
||||
showCookieBar: boolean;
|
||||
hasNewVersion: boolean;
|
||||
userHasGeneratedPassword: boolean;
|
||||
showNotifierToolbar: boolean;
|
||||
page_type: string;
|
||||
autoJoin: boolean;
|
||||
thirdPartyInvite?: object;
|
||||
|
@ -76,7 +83,6 @@ interface IProps {
|
|||
currentRoomId: string;
|
||||
ConferenceHandler?: object;
|
||||
collapseLhs: boolean;
|
||||
checkingForUpdate: boolean;
|
||||
config: {
|
||||
piwik: {
|
||||
policyUrl: string;
|
||||
|
@ -86,19 +92,26 @@ interface IProps {
|
|||
currentUserId?: string;
|
||||
currentGroupId?: string;
|
||||
currentGroupIsNew?: boolean;
|
||||
version?: string;
|
||||
newVersion?: string;
|
||||
newVersionReleaseNotes?: string;
|
||||
}
|
||||
|
||||
interface IUsageLimit {
|
||||
limit_type: "monthly_active_user" | string;
|
||||
admin_contact?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
mouseDown?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
syncErrorData: any;
|
||||
syncErrorData?: {
|
||||
error: {
|
||||
data: IUsageLimit;
|
||||
errcode: string;
|
||||
};
|
||||
};
|
||||
usageLimitEventContent?: IUsageLimit;
|
||||
useCompactLayout: boolean;
|
||||
serverNoticeEvents: MatrixEvent[];
|
||||
userHasGeneratedPassword: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -141,11 +154,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
this.state = {
|
||||
mouseDown: undefined,
|
||||
syncErrorData: undefined,
|
||||
userHasGeneratedPassword: false,
|
||||
// use compact timeline view
|
||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||
// any currently active server notice events
|
||||
serverNoticeEvents: [],
|
||||
};
|
||||
|
||||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
|
@ -179,18 +189,6 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
this._loadResizerPreferences();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
// attempt to guess when a banner was opened or closed
|
||||
if (
|
||||
(prevProps.showCookieBar !== this.props.showCookieBar) ||
|
||||
(prevProps.hasNewVersion !== this.props.hasNewVersion) ||
|
||||
(prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) ||
|
||||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
|
||||
) {
|
||||
this.props.resizeNotifier.notifyBannersChanged();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
|
@ -220,9 +218,11 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
_setStateFromSessionStore = () => {
|
||||
this.setState({
|
||||
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
|
||||
});
|
||||
if (this._sessionStore.getCachedPassword()) {
|
||||
showSetPasswordToast();
|
||||
} else {
|
||||
hideSetPasswordToast();
|
||||
}
|
||||
};
|
||||
|
||||
_createResizer() {
|
||||
|
@ -294,6 +294,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
|
||||
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
|
||||
this._updateServerNoticeEvents();
|
||||
} else {
|
||||
this._calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -304,11 +306,24 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
_calculateServerLimitToast(syncErrorData: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
|
||||
const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
|
||||
if (error) {
|
||||
usageLimitEventContent = syncErrorData.error.data;
|
||||
}
|
||||
|
||||
if (usageLimitEventContent) {
|
||||
showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error);
|
||||
} else {
|
||||
hideServerLimitToast();
|
||||
}
|
||||
}
|
||||
|
||||
_updateServerNoticeEvents = async () => {
|
||||
const roomLists = RoomListStoreTempProxy.getRoomLists();
|
||||
if (!roomLists[DefaultTagID.ServerNotice]) return [];
|
||||
|
||||
const pinnedEvents = [];
|
||||
const events = [];
|
||||
for (const room of roomLists[DefaultTagID.ServerNotice]) {
|
||||
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
|
||||
|
@ -318,12 +333,19 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
for (const eventId of pinnedEventIds) {
|
||||
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
|
||||
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
|
||||
if (event) pinnedEvents.push(event);
|
||||
if (event) events.push(event);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
serverNoticeEvents: pinnedEvents,
|
||||
|
||||
const usageLimitEvent = events.find((e) => {
|
||||
return (
|
||||
e && e.getType() === 'm.room.message' &&
|
||||
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
|
||||
);
|
||||
});
|
||||
const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
|
||||
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
|
||||
this.setState({ usageLimitEventContent });
|
||||
};
|
||||
|
||||
_onPaste = (ev) => {
|
||||
|
@ -338,7 +360,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
// refocusing during a paste event will make the
|
||||
// paste end up in the newly focused element,
|
||||
// so dispatch synchronously before paste happens
|
||||
dis.dispatch({action: 'focus_composer'}, true);
|
||||
dis.fire(Action.FocusComposer, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -488,7 +510,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
|
||||
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
|
||||
// synchronous dispatch so we focus before key generates input
|
||||
dis.dispatch({action: 'focus_composer'}, true);
|
||||
dis.fire(Action.FocusComposer, true);
|
||||
ev.stopPropagation();
|
||||
// we should *not* preventDefault() here as
|
||||
// that would prevent typing in the now-focussed composer
|
||||
|
@ -599,12 +621,6 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
const GroupView = sdk.getComponent('structures.GroupView');
|
||||
const MyGroups = sdk.getComponent('structures.MyGroups');
|
||||
const ToastContainer = sdk.getComponent('structures.ToastContainer');
|
||||
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
const CookieBar = sdk.getComponent('globals.CookieBar');
|
||||
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
|
||||
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
|
||||
const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar');
|
||||
|
||||
let pageElement;
|
||||
|
||||
|
@ -648,50 +664,25 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
|
||||
const usageLimitEvent = this.state.serverNoticeEvents.find((e) => {
|
||||
return (
|
||||
e && e.getType() === 'm.room.message' &&
|
||||
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
|
||||
);
|
||||
});
|
||||
|
||||
let topBar;
|
||||
if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
topBar = <ServerLimitBar kind='hard'
|
||||
adminContact={this.state.syncErrorData.error.data.admin_contact}
|
||||
limitType={this.state.syncErrorData.error.data.limit_type}
|
||||
/>;
|
||||
} else if (usageLimitEvent) {
|
||||
topBar = <ServerLimitBar kind='soft'
|
||||
adminContact={usageLimitEvent.getContent().admin_contact}
|
||||
limitType={usageLimitEvent.getContent().limit_type}
|
||||
/>;
|
||||
} else if (this.props.showCookieBar &&
|
||||
this.props.config.piwik &&
|
||||
navigator.doNotTrack !== "1"
|
||||
) {
|
||||
const policyUrl = this.props.config.piwik.policyUrl || null;
|
||||
topBar = <CookieBar policyUrl={policyUrl} />;
|
||||
} else if (this.props.hasNewVersion) {
|
||||
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
|
||||
releaseNotes={this.props.newVersionReleaseNotes}
|
||||
/>;
|
||||
} else if (this.props.checkingForUpdate) {
|
||||
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
|
||||
} else if (this.state.userHasGeneratedPassword) {
|
||||
topBar = <PasswordNagBar />;
|
||||
} else if (this.props.showNotifierToolbar) {
|
||||
topBar = <MatrixToolbar />;
|
||||
}
|
||||
|
||||
let bodyClasses = 'mx_MatrixChat';
|
||||
if (topBar) {
|
||||
bodyClasses += ' mx_MatrixChat_toolbarShowing';
|
||||
}
|
||||
if (this.state.useCompactLayout) {
|
||||
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
||||
}
|
||||
|
||||
let leftPanel = (
|
||||
<LeftPanel
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
);
|
||||
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
|
||||
// TODO: Supply props like collapsed and disabled to LeftPanel2
|
||||
leftPanel = (
|
||||
<LeftPanel2 />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||
<div
|
||||
|
@ -702,15 +693,10 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
onMouseDown={this._onMouseDown}
|
||||
onMouseUp={this._onMouseUp}
|
||||
>
|
||||
{ topBar }
|
||||
<ToastContainer />
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div ref={this._resizeContainer} className={bodyClasses}>
|
||||
<LeftPanel
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
{ leftPanel }
|
||||
<ResizeHandle />
|
||||
{ pageElement }
|
||||
</div>
|
||||
|
|
|
@ -49,7 +49,6 @@ import PageTypes from '../../PageTypes';
|
|||
import { getHomePageUrl } from '../../utils/pages';
|
||||
|
||||
import createRoom from "../../createRoom";
|
||||
import KeyRequestHandler from '../../KeyRequestHandler';
|
||||
import { _t, getCurrentLanguage } from '../../languageHandler';
|
||||
import SettingsStore, { SettingLevel } from "../../settings/SettingsStore";
|
||||
import ThemeController from "../../settings/controllers/ThemeController";
|
||||
|
@ -59,8 +58,8 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
|
|||
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||
import DMRoomMap from '../../utils/DMRoomMap';
|
||||
import { countRoomsWithNotif } from '../../RoomNotifs';
|
||||
import { ThemeWatcher } from "../../theme";
|
||||
import { FontWatcher } from '../../FontWatcher';
|
||||
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
||||
import { FontWatcher } from '../../settings/watchers/FontWatcher';
|
||||
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
||||
import { defer, IDeferred } from "../../utils/promise";
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
|
@ -68,6 +67,11 @@ import * as StorageManager from "../../utils/StorageManager";
|
|||
import type LoggedInViewType from "./LoggedInView";
|
||||
import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import {
|
||||
showToast as showAnalyticsToast,
|
||||
hideToast as hideAnalyticsToast
|
||||
} from "../../toasts/AnalyticsToast";
|
||||
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -169,12 +173,6 @@ interface IState {
|
|||
leftDisabled: boolean;
|
||||
middleDisabled: boolean;
|
||||
// the right panel's disabled state is tracked in its store.
|
||||
version?: string;
|
||||
newVersion?: string;
|
||||
hasNewVersion: boolean;
|
||||
newVersionReleaseNotes?: string;
|
||||
checkingForUpdate?: string; // updateCheckStatusEnum
|
||||
showCookieBar: boolean;
|
||||
// Parameters used in the registration dance with the IS
|
||||
register_client_secret?: string;
|
||||
register_session_id?: string;
|
||||
|
@ -184,7 +182,6 @@ interface IState {
|
|||
hideToSRUsers: boolean;
|
||||
syncError?: Error;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
showNotifierToolbar: boolean;
|
||||
serverConfig?: ValidatedServerConfig;
|
||||
ready: boolean;
|
||||
thirdPartyInvite?: object;
|
||||
|
@ -228,17 +225,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
leftDisabled: false,
|
||||
middleDisabled: false,
|
||||
|
||||
hasNewVersion: false,
|
||||
newVersionReleaseNotes: null,
|
||||
checkingForUpdate: null,
|
||||
|
||||
showCookieBar: false,
|
||||
|
||||
hideToSRUsers: false,
|
||||
|
||||
syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
|
||||
resizeNotifier: new ResizeNotifier(),
|
||||
showNotifierToolbar: false,
|
||||
ready: false,
|
||||
};
|
||||
|
||||
|
@ -339,12 +329,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("showCookieBar")) {
|
||||
this.setState({
|
||||
showCookieBar: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("analyticsOptIn")) {
|
||||
Analytics.enable();
|
||||
}
|
||||
|
@ -363,7 +347,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
Analytics.trackPageChange(durationMs);
|
||||
}
|
||||
if (this.focusComposer) {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
dis.fire(Action.FocusComposer);
|
||||
this.focusComposer = false;
|
||||
}
|
||||
}
|
||||
|
@ -686,9 +670,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
dis.dispatch({action: 'view_my_groups'});
|
||||
}
|
||||
break;
|
||||
case 'notifier_enabled':
|
||||
this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()});
|
||||
break;
|
||||
case 'hide_left_panel':
|
||||
this.setState({
|
||||
collapseLhs: true,
|
||||
|
@ -736,15 +717,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
case 'client_started':
|
||||
this.onClientStarted();
|
||||
break;
|
||||
case 'new_version':
|
||||
this.onVersion(
|
||||
payload.currentVersion, payload.newVersion,
|
||||
payload.releaseNotes,
|
||||
);
|
||||
break;
|
||||
case 'check_updates':
|
||||
this.setState({ checkingForUpdate: payload.value });
|
||||
break;
|
||||
case 'send_event':
|
||||
this.onSendEvent(payload.room_id, payload.event);
|
||||
break;
|
||||
|
@ -761,19 +733,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
case 'accept_cookies':
|
||||
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true);
|
||||
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
|
||||
|
||||
this.setState({
|
||||
showCookieBar: false,
|
||||
});
|
||||
hideAnalyticsToast();
|
||||
Analytics.enable();
|
||||
break;
|
||||
case 'reject_cookies':
|
||||
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false);
|
||||
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
|
||||
|
||||
this.setState({
|
||||
showCookieBar: false,
|
||||
});
|
||||
hideAnalyticsToast();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -932,9 +898,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private viewGroup(payload) {
|
||||
private async viewGroup(payload) {
|
||||
const groupId = payload.group_id;
|
||||
|
||||
// Wait for the first sync to complete
|
||||
if (!this.firstSyncComplete) {
|
||||
if (!this.firstSyncPromise) {
|
||||
console.warn('Cannot view a group before first sync. group_id:', groupId);
|
||||
return;
|
||||
}
|
||||
await this.firstSyncPromise.promise;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
view: Views.LOGGED_IN,
|
||||
currentGroupId: groupId,
|
||||
currentGroupIsNew: payload.group_is_new,
|
||||
});
|
||||
|
@ -1251,6 +1228,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
StorageManager.tryPersistStorage();
|
||||
|
||||
if (SettingsStore.getValue("showCookieBar") && this.props.config.piwik && navigator.doNotTrack !== "1") {
|
||||
showAnalyticsToast(this.props.config.piwik && this.props.config.piwik.policyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private showScreenAfterLogin() {
|
||||
|
@ -1378,10 +1359,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.firstSyncComplete = true;
|
||||
this.firstSyncPromise.resolve();
|
||||
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
if (Notifier.shouldShowToolbar()) {
|
||||
showNotificationsToast();
|
||||
}
|
||||
|
||||
dis.fire(Action.FocusComposer);
|
||||
this.setState({
|
||||
ready: true,
|
||||
showNotifierToolbar: Notifier.shouldShowToolbar(),
|
||||
});
|
||||
});
|
||||
cli.on('Call.incoming', function(call) {
|
||||
|
@ -1460,16 +1444,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
cli.on("Session.logged_out", () => dft.stop());
|
||||
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err));
|
||||
|
||||
// TODO: We can remove this once cross-signing is the only way.
|
||||
// https://github.com/vector-im/riot-web/issues/11908
|
||||
const krh = new KeyRequestHandler(cli);
|
||||
cli.on("crypto.roomKeyRequest", (req) => {
|
||||
krh.handleKeyRequest(req);
|
||||
});
|
||||
cli.on("crypto.roomKeyRequestCancellation", (req) => {
|
||||
krh.handleKeyRequestCancellation(req);
|
||||
});
|
||||
|
||||
cli.on("Room", (room) => {
|
||||
if (MatrixClientPeg.get().isCryptoEnabled()) {
|
||||
const blacklistEnabled = SettingsStore.getValueAt(
|
||||
|
@ -1540,13 +1514,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
|
||||
cli.on("crypto.verification.request", request => {
|
||||
const isFlagOn = SettingsStore.getValue("feature_cross_signing");
|
||||
|
||||
if (!isFlagOn && !request.channel.deviceId) {
|
||||
request.cancel({code: "m.invalid_message", reason: "This client has cross-signing disabled"});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.verifier) {
|
||||
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
|
||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
||||
|
@ -1559,7 +1526,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
icon: "verification",
|
||||
props: {request},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
priority: ToastStore.PRIORITY_REALTIME,
|
||||
priority: 90,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1589,9 +1556,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// be aware of will be signalled through the room shield
|
||||
// changing colour. More advanced behaviour will come once
|
||||
// we implement more settings.
|
||||
cli.setGlobalErrorOnUnknownDevices(
|
||||
!SettingsStore.getValue("feature_cross_signing"),
|
||||
);
|
||||
cli.setGlobalErrorOnUnknownDevices(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1833,16 +1798,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.showScreen("settings");
|
||||
};
|
||||
|
||||
onVersion(current: string, latest: string, releaseNotes?: string) {
|
||||
this.setState({
|
||||
version: current,
|
||||
newVersion: latest,
|
||||
hasNewVersion: current !== latest,
|
||||
newVersionReleaseNotes: releaseNotes,
|
||||
checkingForUpdate: null,
|
||||
});
|
||||
}
|
||||
|
||||
onSendEvent(roomId: string, event: MatrixEvent) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli) {
|
||||
|
@ -1949,17 +1904,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// whether cross-signing has been set up on the account.
|
||||
const masterKeyInStorage = !!cli.getAccountData("m.cross_signing.master");
|
||||
if (masterKeyInStorage) {
|
||||
// Auto-enable cross-signing for the new session when key found in
|
||||
// secret storage.
|
||||
SettingsStore.setValue("feature_cross_signing", null, SettingLevel.DEVICE, true);
|
||||
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
|
||||
} else if (
|
||||
SettingsStore.getValue("feature_cross_signing") &&
|
||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
|
||||
) {
|
||||
// This will only work if the feature is set to 'enable' in the config,
|
||||
// since it's too early in the lifecycle for users to have turned the
|
||||
// labs flag on.
|
||||
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
|
||||
this.setStateForNewView({ view: Views.E2E_SETUP });
|
||||
} else {
|
||||
this.onLoggedIn();
|
||||
|
@ -1978,7 +1924,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
|
||||
|
||||
let fragmentAfterLogin = "";
|
||||
if (this.props.initialScreenAfterLogin) {
|
||||
if (this.props.initialScreenAfterLogin &&
|
||||
// XXX: workaround for https://github.com/vector-im/riot-web/issues/11643 causing a login-loop
|
||||
!["welcome", "login", "register"].includes(this.props.initialScreenAfterLogin.screen)
|
||||
) {
|
||||
fragmentAfterLogin = `/${this.props.initialScreenAfterLogin.screen}`;
|
||||
}
|
||||
|
||||
|
@ -2037,7 +1986,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
onCloseAllSettings={this.onCloseAllSettings}
|
||||
onRegistered={this.onRegistered}
|
||||
currentRoomId={this.state.currentRoomId}
|
||||
showCookieBar={this.state.showCookieBar}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -34,6 +34,30 @@ import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResiz
|
|||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
||||
// check if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
function shouldFormContinuation(prevEvent, mxEvent) {
|
||||
// sanity check inputs
|
||||
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
|
||||
// check if within the max continuation period
|
||||
if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false;
|
||||
|
||||
// Some events should appear as continuations from previous events of different types.
|
||||
if (mxEvent.getType() !== prevEvent.getType() &&
|
||||
(!continuedTypes.includes(mxEvent.getType()) ||
|
||||
!continuedTypes.includes(prevEvent.getType()))) return false;
|
||||
|
||||
// Check if the sender is the same and hasn't changed their displayname/avatar between these events
|
||||
if (mxEvent.sender.userId !== prevEvent.sender.userId ||
|
||||
mxEvent.sender.name !== prevEvent.sender.name ||
|
||||
mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false;
|
||||
|
||||
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
|
||||
if (!haveTileForEvent(prevEvent)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite';
|
||||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
|
@ -108,6 +132,9 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
};
|
||||
|
||||
// Force props to be loaded for useIRCLayout
|
||||
|
@ -119,7 +146,6 @@ export default class MessagePanel extends React.Component {
|
|||
// display 'ghost' read markers that are animating away
|
||||
ghostReadMarkers: [],
|
||||
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
|
||||
useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")),
|
||||
};
|
||||
|
||||
// opaque readreceipt info for each userId; used by ReadReceiptMarker
|
||||
|
@ -172,8 +198,6 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
this._showTypingNotificationsWatcherRef =
|
||||
SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange);
|
||||
|
||||
this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -183,7 +207,6 @@ export default class MessagePanel extends React.Component {
|
|||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
|
||||
SettingsStore.unwatchSetting(this._layoutWatcherRef);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
|
@ -202,17 +225,6 @@ export default class MessagePanel extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onLayoutChange = () => {
|
||||
this.setState({
|
||||
useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")),
|
||||
});
|
||||
}
|
||||
|
||||
useIRCLayout(ircLayoutSelected) {
|
||||
// if room is null we are not in a normal room list
|
||||
return ircLayoutSelected && this.props.room;
|
||||
}
|
||||
|
||||
/* get the DOM node representing the given event */
|
||||
getNodeForEventId(eventId) {
|
||||
if (!this.eventNodes) {
|
||||
|
@ -527,39 +539,6 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
const isEditing = this.props.editState &&
|
||||
this.props.editState.getEvent().getId() === mxEv.getId();
|
||||
// is this a continuation of the previous message?
|
||||
let continuation = false;
|
||||
|
||||
// Some events should appear as continuations from previous events of
|
||||
// different types.
|
||||
|
||||
const eventTypeContinues =
|
||||
prevEvent !== null &&
|
||||
continuedTypes.includes(mxEv.getType()) &&
|
||||
continuedTypes.includes(prevEvent.getType());
|
||||
|
||||
// if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId &&
|
||||
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
|
||||
haveTileForEvent(prevEvent) && (mxEv.getType() === prevEvent.getType() || eventTypeContinues) &&
|
||||
(mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) {
|
||||
continuation = true;
|
||||
}
|
||||
|
||||
/*
|
||||
// Work out if this is still a continuation, as we are now showing commands
|
||||
// and /me messages with their own little avatar. The case of a change of
|
||||
// event type (commands) is handled above, but we need to handle the /me
|
||||
// messages seperately as they have a msgtype of 'm.emote' but are classed
|
||||
// as normal messages
|
||||
if (prevEvent !== null && prevEvent.sender && mxEv.sender
|
||||
&& mxEv.sender.userId === prevEvent.sender.userId
|
||||
&& mxEv.getType() == prevEvent.getType()
|
||||
&& prevEvent.getContent().msgtype === 'm.emote') {
|
||||
continuation = false;
|
||||
}
|
||||
*/
|
||||
|
||||
// local echoes have a fake date, which could even be yesterday. Treat them
|
||||
// as 'today' for the date separators.
|
||||
|
@ -571,12 +550,15 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
// do we need a date separator since the last event?
|
||||
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
||||
const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate);
|
||||
if (wantsDateSeparator) {
|
||||
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
|
||||
ret.push(dateSeparator);
|
||||
continuation = false;
|
||||
}
|
||||
|
||||
// is this a continuation of the previous message?
|
||||
const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv);
|
||||
|
||||
const eventId = mxEv.getId();
|
||||
const highlight = (eventId === this.props.highlightedEventId);
|
||||
|
||||
|
@ -614,7 +596,7 @@ export default class MessagePanel extends React.Component {
|
|||
isSelectedEvent={highlight}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
showReactions={this.props.showReactions}
|
||||
useIRCLayout={this.state.useIRCLayout}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
/>
|
||||
</TileErrorBoundary>
|
||||
</li>,
|
||||
|
@ -797,8 +779,6 @@ export default class MessagePanel extends React.Component {
|
|||
this.props.className,
|
||||
{
|
||||
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
|
||||
"mx_IRCLayout": this.state.useIRCLayout,
|
||||
"mx_GroupLayout": !this.state.useIRCLayout,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -813,11 +793,11 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
let ircResizer = null;
|
||||
if (this.state.useIRCLayout) {
|
||||
if (this.props.useIRCLayout) {
|
||||
ircResizer = <IRCTimelineProfileResizer
|
||||
minWidth={20}
|
||||
maxWidth={600}
|
||||
roomId={this.props.room ? this.props.roomroomId : null}
|
||||
roomId={this.props.room ? this.props.room.roomId : null}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ import dis from '../../dispatcher/dispatcher';
|
|||
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
|
@ -189,16 +188,37 @@ export default class RightPanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onCloseUserInfo = () => {
|
||||
// XXX: There are three different ways of 'closing' this panel depending on what state
|
||||
// things are in... this knows far more than it should do about the state of the rest
|
||||
// of the app and is generally a bit silly.
|
||||
if (this.props.user) {
|
||||
// If we have a user prop then we're displaying a user from the 'user' page type
|
||||
// in LoggedInView, so need to change the page type to close the panel (we switch
|
||||
// to the home page which is not obviously the correct thing to do, but I'm not sure
|
||||
// anything else is - we could hide the close button altogether?)
|
||||
dis.dispatch({
|
||||
action: "view_home_page",
|
||||
});
|
||||
} else {
|
||||
// Otherwise we have got our user from RoomViewStore which means we're being shown
|
||||
// within a room/group, so go back to the member panel if we were in the encryption panel,
|
||||
// or the member list if we were in the member panel... phew.
|
||||
dis.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
||||
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
|
||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
||||
|
||||
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
|
||||
const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo');
|
||||
const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
|
||||
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
|
||||
|
||||
|
@ -220,71 +240,25 @@ export default class RightPanel extends React.Component {
|
|||
break;
|
||||
case RIGHT_PANEL_PHASES.RoomMemberInfo:
|
||||
case RIGHT_PANEL_PHASES.EncryptionPanel:
|
||||
if (SettingsStore.getValue("feature_cross_signing")) {
|
||||
const onClose = () => {
|
||||
// XXX: There are three different ways of 'closing' this panel depending on what state
|
||||
// things are in... this knows far more than it should do about the state of the rest
|
||||
// of the app and is generally a bit silly.
|
||||
if (this.props.user) {
|
||||
// If we have a user prop then we're displaying a user from the 'user' page type
|
||||
// in LoggedInView, so need to change the page type to close the panel (we switch
|
||||
// to the home page which is not obviously the correct thing to do, but I'm not sure
|
||||
// anything else is - we could hide the close button altogether?)
|
||||
dis.dispatch({
|
||||
action: "view_home_page",
|
||||
});
|
||||
} else {
|
||||
// Otherwise we have got our user from RoomViewStore which means we're being shown
|
||||
// within a room, so go back to the member panel if we were in the encryption panel,
|
||||
// or the member list if we were in the member panel... phew.
|
||||
dis.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ?
|
||||
this.state.member : null,
|
||||
});
|
||||
}
|
||||
};
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
roomId={this.props.roomId}
|
||||
key={this.props.roomId || this.state.member.userId}
|
||||
onClose={onClose}
|
||||
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}
|
||||
/>;
|
||||
}
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
roomId={this.props.roomId}
|
||||
key={this.props.roomId || this.state.member.userId}
|
||||
onClose={this.onCloseUserInfo}
|
||||
phase={this.state.phase}
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
verificationRequestPromise={this.state.verificationRequestPromise}
|
||||
/>;
|
||||
break;
|
||||
case RIGHT_PANEL_PHASES.Room3pidMemberInfo:
|
||||
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
|
||||
break;
|
||||
case RIGHT_PANEL_PHASES.GroupMemberInfo:
|
||||
if (SettingsStore.getValue("feature_cross_signing")) {
|
||||
const onClose = () => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: null,
|
||||
});
|
||||
};
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.userId}
|
||||
onClose={onClose} />;
|
||||
} else {
|
||||
panel = (
|
||||
<GroupMemberInfo
|
||||
groupMember={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.user_id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.userId}
|
||||
onClose={this.onCloseUserInfo} />;
|
||||
break;
|
||||
case RIGHT_PANEL_PHASES.GroupRoomInfo:
|
||||
panel = <GroupRoomInfo
|
||||
|
|
|
@ -199,7 +199,7 @@ export default createReactClass({
|
|||
|
||||
let desc;
|
||||
if (alias) {
|
||||
desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
|
||||
desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
|
||||
} else {
|
||||
desc = _t('Remove %(name)s from the directory?', {name: name});
|
||||
}
|
||||
|
@ -216,7 +216,7 @@ export default createReactClass({
|
|||
|
||||
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
|
||||
if (!alias) return;
|
||||
step = _t('delete the alias.');
|
||||
step = _t('delete the address.');
|
||||
return MatrixClientPeg.get().deleteAlias(alias);
|
||||
}).then(() => {
|
||||
modal.close();
|
||||
|
|
|
@ -24,9 +24,9 @@ import { _t, _td } from '../../languageHandler';
|
|||
import * as sdk from '../../index';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import Resend from '../../Resend';
|
||||
import * as cryptodevices from '../../cryptodevices';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
|
@ -126,25 +126,14 @@ export default createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onSendWithoutVerifyingClick: function() {
|
||||
cryptodevices.getUnknownDevicesForRoom(MatrixClientPeg.get(), this.props.room).then((devices) => {
|
||||
cryptodevices.markAllDevicesKnown(MatrixClientPeg.get(), devices);
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
});
|
||||
},
|
||||
|
||||
_onResendAllClick: function() {
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
dis.fire(Action.FocusComposer);
|
||||
},
|
||||
|
||||
_onCancelAllClick: function() {
|
||||
Resend.cancelUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
_onShowDevicesClick: function() {
|
||||
cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
|
||||
dis.fire(Action.FocusComposer);
|
||||
},
|
||||
|
||||
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {
|
||||
|
@ -213,82 +202,65 @@ export default createReactClass({
|
|||
if (!unsentMessages.length) return null;
|
||||
|
||||
let title;
|
||||
let content;
|
||||
|
||||
const hasUDE = unsentMessages.some((m) => {
|
||||
return m.error && m.error.name === "UnknownDeviceError";
|
||||
});
|
||||
|
||||
if (hasUDE) {
|
||||
title = _t("Message not sent due to unknown sessions being present");
|
||||
content = _t(
|
||||
"<showSessionsText>Show sessions</showSessionsText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.",
|
||||
let consentError = null;
|
||||
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>.",
|
||||
{},
|
||||
{
|
||||
'showSessionsText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
|
||||
'sendAnywayText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="sendAnyway" onClick={this._onSendWithoutVerifyingClick}>{ sub }</a>,
|
||||
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||
'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 {
|
||||
let consentError = null;
|
||||
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>,
|
||||
},
|
||||
);
|
||||
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
|
||||
}
|
||||
|
||||
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">
|
||||
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title={_t("Warning")} alt="" />
|
||||
<div>
|
||||
|
|
|
@ -55,6 +55,7 @@ import {haveTileForEvent} from "../views/rooms/EventTile";
|
|||
import RoomContext from "../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function() {};
|
||||
|
@ -164,6 +165,10 @@ export default createReactClass({
|
|||
|
||||
canReact: false,
|
||||
canReply: false,
|
||||
|
||||
useIRCLayout: SettingsStore.getValue("feature_irc_ui"),
|
||||
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -193,6 +198,8 @@ export default createReactClass({
|
|||
|
||||
this._roomView = createRef();
|
||||
this._searchResultsPanel = createRef();
|
||||
|
||||
this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange);
|
||||
},
|
||||
|
||||
_onReadReceiptsChange: function() {
|
||||
|
@ -232,7 +239,8 @@ export default createReactClass({
|
|||
initialEventId: RoomViewStore.getInitialEventId(),
|
||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
shouldPeek: RoomViewStore.shouldPeek(),
|
||||
// we should only peek once we have a ready client
|
||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
};
|
||||
|
@ -532,6 +540,14 @@ export default createReactClass({
|
|||
// no need to do this as Dir & Settings are now overlays. It just burnt CPU.
|
||||
// console.log("Tinter.tint from RoomView.unmount");
|
||||
// Tinter.tint(); // reset colourscheme
|
||||
|
||||
SettingsStore.unwatchSetting(this._layoutWatcherRef);
|
||||
},
|
||||
|
||||
onLayoutChange: function() {
|
||||
this.setState({
|
||||
useIRCLayout: SettingsStore.getValue("feature_irc_ui"),
|
||||
});
|
||||
},
|
||||
|
||||
_onRightPanelStoreUpdate: function() {
|
||||
|
@ -681,6 +697,16 @@ export default createReactClass({
|
|||
});
|
||||
}
|
||||
break;
|
||||
case 'sync_state':
|
||||
if (!this.state.matrixClientIsReady) {
|
||||
this.setState({
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
}, () => {
|
||||
// send another "initial" RVS update to trigger peeking if needed
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -854,15 +880,6 @@ export default createReactClass({
|
|||
});
|
||||
return;
|
||||
}
|
||||
if (!SettingsStore.getValue("feature_cross_signing")) {
|
||||
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
|
||||
this.setState({
|
||||
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
|
||||
});
|
||||
});
|
||||
debuglog("e2e check is warning/verified only as cross-signing is off");
|
||||
return;
|
||||
}
|
||||
|
||||
/* At this point, the user has encryption on and cross-signing on */
|
||||
this.setState({
|
||||
|
@ -1146,7 +1163,7 @@ export default createReactClass({
|
|||
ev.dataTransfer.files, this.state.room.roomId, this.context,
|
||||
);
|
||||
this.setState({ draggingFile: false });
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
dis.fire(Action.FocusComposer);
|
||||
},
|
||||
|
||||
onDragLeaveOrEnd: function(ev) {
|
||||
|
@ -1352,7 +1369,7 @@ export default createReactClass({
|
|||
event: null,
|
||||
});
|
||||
}
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
dis.fire(Action.FocusComposer);
|
||||
},
|
||||
|
||||
onLeaveClick: function() {
|
||||
|
@ -1463,7 +1480,7 @@ export default createReactClass({
|
|||
// jump down to the bottom of this room, where new events are arriving
|
||||
jumpToLiveTimeline: function() {
|
||||
this._messagePanel.jumpToLiveTimeline();
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
dis.fire(Action.FocusComposer);
|
||||
},
|
||||
|
||||
// jump up to wherever our read marker is
|
||||
|
@ -1663,14 +1680,16 @@ export default createReactClass({
|
|||
const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
|
||||
|
||||
if (!this.state.room) {
|
||||
const loading = this.state.roomLoading || this.state.peekLoading;
|
||||
const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading;
|
||||
if (loading) {
|
||||
// Assume preview loading if we don't have a ready client or a room ID (still resolving the alias)
|
||||
const previewLoading = !this.state.matrixClientIsReady || !this.state.roomId || this.state.peekLoading;
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
previewLoading={this.state.peekLoading}
|
||||
previewLoading={previewLoading && !this.state.roomLoadError}
|
||||
error={this.state.roomLoadError}
|
||||
loading={loading}
|
||||
joining={this.state.joining}
|
||||
|
@ -1695,7 +1714,8 @@ export default createReactClass({
|
|||
return (
|
||||
<div className="mx_RoomView">
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
<RoomPreviewBar
|
||||
onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
canPreview={false} error={this.state.roomLoadError}
|
||||
|
@ -1980,6 +2000,13 @@ export default createReactClass({
|
|||
highlightedEventId = this.state.initialEventId;
|
||||
}
|
||||
|
||||
const messagePanelClassNames = classNames(
|
||||
"mx_RoomView_messagePanel",
|
||||
{
|
||||
"mx_IRCLayout": this.state.useIRCLayout,
|
||||
"mx_GroupLayout": !this.state.useIRCLayout,
|
||||
});
|
||||
|
||||
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||
const messagePanel = (
|
||||
<TimelinePanel
|
||||
|
@ -1995,11 +2022,12 @@ export default createReactClass({
|
|||
onScroll={this.onMessageListScroll}
|
||||
onReadMarkerUpdated={this._updateTopUnreadMessagesBar}
|
||||
showUrlPreview = {this.state.showUrlPreview}
|
||||
className="mx_RoomView_messagePanel"
|
||||
className={messagePanelClassNames}
|
||||
membersLoaded={this.state.membersLoaded}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showReactions={true}
|
||||
useIRCLayout={this.state.useIRCLayout}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
|
|
@ -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;
|
|
@ -112,6 +112,9 @@ const TimelinePanel = createReactClass({
|
|||
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -1447,6 +1450,7 @@ const TimelinePanel = createReactClass({
|
|||
getRelationsForEvent={this.getRelationsForEvent}
|
||||
editState={this.state.editState}
|
||||
showReactions={this.props.showReactions}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -15,14 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { _t } from '../../languageHandler';
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
import ToastStore, {IToast} from "../../stores/ToastStore";
|
||||
import classNames from "classnames";
|
||||
|
||||
export default class ToastContainer extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {toasts: ToastStore.sharedInstance().getToasts()};
|
||||
interface IState {
|
||||
toasts: IToast<any>[];
|
||||
countSeen: number;
|
||||
}
|
||||
|
||||
export default class ToastContainer extends React.Component<{}, IState> {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
toasts: ToastStore.sharedInstance().getToasts(),
|
||||
countSeen: ToastStore.sharedInstance().getCountSeen(),
|
||||
};
|
||||
|
||||
// Start listening here rather than in componentDidMount because
|
||||
// toasts may dismiss themselves in their didMount if they find
|
||||
|
@ -36,7 +43,10 @@ export default class ToastContainer extends React.Component {
|
|||
}
|
||||
|
||||
_onToastStoreUpdate = () => {
|
||||
this.setState({toasts: ToastStore.sharedInstance().getToasts()});
|
||||
this.setState({
|
||||
toasts: ToastStore.sharedInstance().getToasts(),
|
||||
countSeen: ToastStore.sharedInstance().getCountSeen(),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -50,14 +60,21 @@ export default class ToastContainer extends React.Component {
|
|||
"mx_Toast_hasIcon": icon,
|
||||
[`mx_Toast_icon_${icon}`]: icon,
|
||||
});
|
||||
const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
|
||||
|
||||
let countIndicator;
|
||||
if (isStacked || this.state.countSeen > 0) {
|
||||
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
|
||||
}
|
||||
|
||||
const toastProps = Object.assign({}, props, {
|
||||
key,
|
||||
toastKey: key,
|
||||
});
|
||||
toast = (<div className={toastClasses}>
|
||||
<h2>{title}{countIndicator}</h2>
|
||||
<div className="mx_Toast_title">
|
||||
<h2>{title}</h2>
|
||||
<span>{countIndicator}</span>
|
||||
</div>
|
||||
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
||||
</div>);
|
||||
}
|
|
@ -247,9 +247,8 @@ export default createReactClass({
|
|||
// do SSO instead. If we've already started the UI Auth process though, we don't
|
||||
// need to.
|
||||
if (!this.state.doingUIAuth) {
|
||||
await this._makeRegisterRequest({});
|
||||
// This should never succeed since we specified an empty
|
||||
// auth object.
|
||||
await this._makeRegisterRequest(null);
|
||||
// This should never succeed since we specified no auth object.
|
||||
console.log("Expecting 401 from register request but got success!");
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -25,6 +25,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|||
import {sendLoginRequest} from "../../../Login";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import SSOButton from "../../views/elements/SSOButton";
|
||||
import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "../../../BasePlatform";
|
||||
|
||||
const LOGIN_VIEW = {
|
||||
LOADING: 1,
|
||||
|
@ -43,7 +44,7 @@ const FLOWS_TO_VIEWS = {
|
|||
export default class SoftLogout extends React.Component {
|
||||
static propTypes = {
|
||||
// Query parameters from MatrixChat
|
||||
realQueryParams: PropTypes.object, // {homeserver, identityServer, loginToken}
|
||||
realQueryParams: PropTypes.object, // {loginToken}
|
||||
|
||||
// Called when the SSO login completes
|
||||
onTokenLoginCompleted: PropTypes.func,
|
||||
|
@ -90,7 +91,7 @@ export default class SoftLogout extends React.Component {
|
|||
|
||||
async _initLogin() {
|
||||
const queryParams = this.props.realQueryParams;
|
||||
const hasAllParams = queryParams && queryParams['homeserver'] && queryParams['loginToken'];
|
||||
const hasAllParams = queryParams && queryParams['loginToken'];
|
||||
if (hasAllParams) {
|
||||
this.setState({loginView: LOGIN_VIEW.LOADING});
|
||||
this.trySsoLogin();
|
||||
|
@ -157,8 +158,8 @@ export default class SoftLogout extends React.Component {
|
|||
async trySsoLogin() {
|
||||
this.setState({busy: true});
|
||||
|
||||
const hsUrl = this.props.realQueryParams['homeserver'];
|
||||
const isUrl = this.props.realQueryParams['identityServer'] || MatrixClientPeg.get().getIdentityServerUrl();
|
||||
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY) || MatrixClientPeg.get().getIdentityServerUrl();
|
||||
const loginType = "m.login.token";
|
||||
const loginParams = {
|
||||
token: this.props.realQueryParams['loginToken'],
|
||||
|
|
|
@ -355,6 +355,7 @@ export const TermsAuthEntry = createReactClass({
|
|||
allChecked = allChecked && checked;
|
||||
|
||||
checkboxes.push(
|
||||
// XXX: replace with StyledCheckbox
|
||||
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
||||
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
|
||||
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
|
||||
|
@ -538,6 +539,7 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/vector-im/riot-web/issues/10312
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
threepid_creds: creds,
|
||||
threepidCreds: creds,
|
||||
});
|
||||
|
|
|
@ -23,7 +23,8 @@ import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
|||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||
import ServerConfig from "./ServerConfig";
|
||||
|
||||
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
const MODULAR_URL = 'https://modular.im/services/matrix-hosting-riot' +
|
||||
'?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
|
||||
// TODO: TravisR - Can this extend ServerConfig for most things?
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ interface IProps {
|
|||
labelStrongPassword?: string;
|
||||
labelAllowedButUnsafe?: string;
|
||||
|
||||
onChange(ev: KeyboardEvent);
|
||||
onChange(ev: React.FormEvent<HTMLElement>);
|
||||
onValidate(result: IValidationResult);
|
||||
}
|
||||
|
||||
|
|
|
@ -238,7 +238,7 @@ export default class PasswordLogin extends React.Component {
|
|||
type="text"
|
||||
label={_t("Phone")}
|
||||
value={this.state.phoneNumber}
|
||||
prefix={phoneCountry}
|
||||
prefixComponent={phoneCountry}
|
||||
onChange={this.onPhoneNumberChanged}
|
||||
onBlur={this.onPhoneNumberBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
|
|
|
@ -473,7 +473,7 @@ export default createReactClass({
|
|||
type="text"
|
||||
label={phoneLabel}
|
||||
value={this.state.phoneNumber}
|
||||
prefix={phoneCountry}
|
||||
prefixComponent={phoneCountry}
|
||||
onChange={this.onPhoneNumberChange}
|
||||
onValidate={this.onPhoneNumberValidate}
|
||||
/>;
|
||||
|
|
|
@ -22,7 +22,8 @@ import classnames from 'classnames';
|
|||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import {makeType} from "../../../utils/TypeUtils";
|
||||
|
||||
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
const MODULAR_URL = 'https://modular.im/services/matrix-hosting-riot' +
|
||||
'?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
|
||||
export const FREE = 'Free';
|
||||
export const PREMIUM = 'Premium';
|
||||
|
|
|
@ -26,58 +26,48 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {toPx} from "../../../utils/units";
|
||||
|
||||
const useImageUrl = ({url, urls, idName, name, defaultToInitialLetter}) => {
|
||||
const useImageUrl = ({url, urls}) => {
|
||||
const [imageUrls, setUrls] = useState([]);
|
||||
const [urlsIndex, setIndex] = useState();
|
||||
|
||||
const onError = () => {
|
||||
const nextIndex = urlsIndex + 1;
|
||||
if (nextIndex < imageUrls.length) {
|
||||
// try the next one
|
||||
setIndex(nextIndex);
|
||||
}
|
||||
};
|
||||
|
||||
const defaultImageUrl = useMemo(() => AvatarLogic.defaultAvatarUrlForString(idName || name), [idName, name]);
|
||||
const onError = useCallback(() => {
|
||||
setIndex(i => i + 1); // try the next one
|
||||
}, []);
|
||||
const memoizedUrls = useMemo(() => urls, [JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, ...props.urls, default image ]
|
||||
// imageUrls: [ props.url, ...props.urls ]
|
||||
|
||||
let _urls = [];
|
||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||
_urls = urls || [];
|
||||
_urls = memoizedUrls || [];
|
||||
|
||||
if (url) {
|
||||
_urls.unshift(url); // put in urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultToInitialLetter) {
|
||||
_urls.push(defaultImageUrl); // lowest priority
|
||||
}
|
||||
|
||||
// deduplicate URLs
|
||||
_urls = Array.from(new Set(_urls));
|
||||
|
||||
setIndex(0);
|
||||
setUrls(_urls);
|
||||
}, [url, ...(urls || [])]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const onClientSync = useCallback((syncState, prevState) => {
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected && urlsIndex > 0 ) { // Did we fall back?
|
||||
// Start from the highest priority URL again
|
||||
if (reconnected) {
|
||||
setIndex(0);
|
||||
}
|
||||
}, [urlsIndex]);
|
||||
}, []);
|
||||
useEventEmitter(cli, "sync", onClientSync);
|
||||
|
||||
const imageUrl = imageUrls[urlsIndex];
|
||||
return [imageUrl, imageUrl === defaultImageUrl, onError];
|
||||
return [imageUrl, onError];
|
||||
};
|
||||
|
||||
const BaseAvatar = (props) => {
|
||||
|
@ -96,9 +86,9 @@ const BaseAvatar = (props) => {
|
|||
...otherProps
|
||||
} = props;
|
||||
|
||||
const [imageUrl, isDefault, onError] = useImageUrl({url, urls, idName, name, defaultToInitialLetter});
|
||||
const [imageUrl, onError] = useImageUrl({url, urls});
|
||||
|
||||
if (isDefault) {
|
||||
if (!imageUrl && defaultToInitialLetter) {
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<span
|
||||
|
@ -116,7 +106,7 @@ const BaseAvatar = (props) => {
|
|||
const imgNode = (
|
||||
<img
|
||||
className="mx_BaseAvatar_image"
|
||||
src={imageUrl}
|
||||
src={AvatarLogic.defaultAvatarUrlForString(idName || name)}
|
||||
alt=""
|
||||
title={title}
|
||||
onError={onError}
|
||||
|
|
|
@ -18,10 +18,10 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import * as sdk from "../../../index";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MemberAvatar',
|
||||
|
@ -62,10 +62,14 @@ export default createReactClass({
|
|||
return {
|
||||
name: props.member.name,
|
||||
title: props.title || props.member.userId,
|
||||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod),
|
||||
imageUrl: props.member.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
};
|
||||
} else if (props.fallbackUserId) {
|
||||
return {
|
||||
|
|
|
@ -116,11 +116,6 @@ export default createReactClass({
|
|||
this.closeMenu();
|
||||
},
|
||||
|
||||
e2eInfoClicked: function() {
|
||||
this.props.e2eInfoCallback();
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onReportEventClick: function() {
|
||||
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
|
||||
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
|
||||
|
@ -465,15 +460,6 @@ export default createReactClass({
|
|||
);
|
||||
}
|
||||
|
||||
let e2eInfo;
|
||||
if (this.props.e2eInfoCallback) {
|
||||
e2eInfo = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
|
||||
{ _t('End-to-end encryption information') }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
let reportEventButton;
|
||||
if (mxEvent.getSender() !== me) {
|
||||
reportEventButton = (
|
||||
|
@ -500,7 +486,6 @@ export default createReactClass({
|
|||
{ quoteButton }
|
||||
{ externalURLButton }
|
||||
{ collapseReplyThread }
|
||||
{ e2eInfo }
|
||||
{ reportEventButton }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -98,7 +98,7 @@ export default createReactClass({
|
|||
|
||||
render: function() {
|
||||
return (
|
||||
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
|
||||
<input type="text" className="mx_RoomAlias" placeholder={_t("Address (optional)")}
|
||||
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
|
||||
value={this.props.alias} />
|
||||
);
|
||||
|
|
|
@ -144,6 +144,7 @@ export default createReactClass({
|
|||
>
|
||||
<div className={classNames('mx_Dialog_header', {
|
||||
'mx_Dialog_headerWithButton': !!this.props.headerButton,
|
||||
'mx_Dialog_headerWithCancel': !!cancelButton,
|
||||
})}>
|
||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||
{headerImage}
|
||||
|
|
|
@ -24,7 +24,7 @@ import withValidation from '../elements/Validation';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {privateShouldBeEncrypted} from "../../../createRoom";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'CreateRoomDialog',
|
||||
|
@ -37,7 +37,7 @@ export default createReactClass({
|
|||
const config = SdkConfig.get();
|
||||
return {
|
||||
isPublic: this.props.defaultPublic || false,
|
||||
isEncrypted: true,
|
||||
isEncrypted: privateShouldBeEncrypted(),
|
||||
name: "",
|
||||
topic: "",
|
||||
alias: "",
|
||||
|
@ -66,7 +66,7 @@ export default createReactClass({
|
|||
createOpts.creation_content = {'m.federate': false};
|
||||
}
|
||||
|
||||
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
|
||||
if (!this.state.isPublic) {
|
||||
opts.encryption = this.state.isEncrypted;
|
||||
}
|
||||
|
||||
|
@ -181,7 +181,7 @@ export default createReactClass({
|
|||
let publicPrivateLabel;
|
||||
let aliasField;
|
||||
if (this.state.isPublic) {
|
||||
publicPrivateLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
|
||||
publicPrivateLabel = (<p>{_t("Set a room address to easily share your room with other people.")}</p>);
|
||||
const domain = MatrixClientPeg.get().getDomain();
|
||||
aliasField = (
|
||||
<div className="mx_CreateRoomDialog_aliasContainer">
|
||||
|
@ -193,7 +193,14 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
let e2eeSection;
|
||||
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
|
||||
if (!this.state.isPublic) {
|
||||
let microcopy;
|
||||
if (privateShouldBeEncrypted()) {
|
||||
microcopy = _t("You can’t disable this later. Bridges & most bots won’t work yet.");
|
||||
} else {
|
||||
microcopy = _t("Your server admin has disabled end-to-end encryption by default " +
|
||||
"in private rooms & Direct Messages.");
|
||||
}
|
||||
e2eeSection = <React.Fragment>
|
||||
<LabelledToggleSwitch
|
||||
label={ _t("Enable end-to-end encryption")}
|
||||
|
@ -201,7 +208,7 @@ export default createReactClass({
|
|||
value={this.state.isEncrypted}
|
||||
className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests
|
||||
/>
|
||||
<p>{ _t("You can’t disable this later. Bridges & most bots won’t work yet.") }</p>
|
||||
<p>{ microcopy }</p>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
|
|
|
@ -42,11 +42,9 @@ export default (props) => {
|
|||
};
|
||||
|
||||
const description =
|
||||
_t("You've previously used a newer version of Riot on %(host)s. " +
|
||||
_t("You've previously used a newer version of Riot with this session. " +
|
||||
"To use this version again with end to end encryption, you will " +
|
||||
"need to sign out and back in again. ",
|
||||
{host: props.host},
|
||||
);
|
||||
"need to sign out and back in again.");
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
|
|
@ -25,6 +25,7 @@ import * as Lifecycle from '../../../Lifecycle';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
|
||||
import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
|
||||
export default class DeactivateAccountDialog extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -209,21 +210,18 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
|
||||
<div className="mx_DeactivateAccountDialog_input_section">
|
||||
<p>
|
||||
<label htmlFor="mx_DeactivateAccountDialog_erase_account_input">
|
||||
<input
|
||||
id="mx_DeactivateAccountDialog_erase_account_input"
|
||||
type="checkbox"
|
||||
checked={this.state.shouldErase}
|
||||
onChange={this._onEraseFieldChange}
|
||||
/>
|
||||
{ _t(
|
||||
<StyledCheckbox
|
||||
checked={this.state.shouldErase}
|
||||
onChange={this._onEraseFieldChange}
|
||||
>
|
||||
{_t(
|
||||
"Please forget all messages I have sent when my account is deactivated " +
|
||||
"(<b>Warning:</b> this will cause future users to see an incomplete view " +
|
||||
"of conversations)",
|
||||
{},
|
||||
{ b: (sub) => <b>{ sub }</b> },
|
||||
) }
|
||||
</label>
|
||||
)}
|
||||
</StyledCheckbox>
|
||||
</p>
|
||||
|
||||
{error}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -31,9 +31,8 @@ import dis from "../../../dispatcher/dispatcher";
|
|||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
|
||||
import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
|
||||
|
@ -576,7 +575,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
|
||||
const createRoomOptions = {inlineErrors: true};
|
||||
|
||||
if (SettingsStore.getValue("feature_cross_signing")) {
|
||||
if (privateShouldBeEncrypted()) {
|
||||
// Check whether all users have uploaded device keys before.
|
||||
// If so, enable encryption in the new room.
|
||||
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -29,6 +29,7 @@ import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../..
|
|||
import * as ContextMenu from "../../structures/ContextMenu";
|
||||
import {toRightOf} from "../../structures/ContextMenu";
|
||||
import {copyPlaintext, selectText} from "../../../utils/strings";
|
||||
import StyledCheckbox from '../elements/StyledCheckbox';
|
||||
|
||||
const socials = [
|
||||
{
|
||||
|
@ -168,13 +169,12 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
|
|||
const events = this.props.target.getLiveTimeline().getEvents();
|
||||
if (events.length > 0) {
|
||||
checkbox = <div>
|
||||
<input type="checkbox"
|
||||
id="mx_ShareDialog_checkbox"
|
||||
checked={this.state.linkSpecificEvent}
|
||||
onChange={this.onLinkSpecificEventCheckboxClick} />
|
||||
<label htmlFor="mx_ShareDialog_checkbox">
|
||||
<StyledCheckbox
|
||||
checked={this.state.linkSpecificEvent}
|
||||
onChange={this.onLinkSpecificEventCheckboxClick}
|
||||
>
|
||||
{ _t('Link to most recent message') }
|
||||
</label>
|
||||
</StyledCheckbox>
|
||||
</div>;
|
||||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
|
@ -184,13 +184,12 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
|
|||
} else if (this.props.target instanceof MatrixEvent) {
|
||||
title = _t('Share Room Message');
|
||||
checkbox = <div>
|
||||
<input type="checkbox"
|
||||
id="mx_ShareDialog_checkbox"
|
||||
<StyledCheckbox
|
||||
checked={this.state.linkSpecificEvent}
|
||||
onClick={this.onLinkSpecificEventCheckboxClick} />
|
||||
<label htmlFor="mx_ShareDialog_checkbox">
|
||||
onClick={this.onLinkSpecificEventCheckboxClick}
|
||||
>
|
||||
{ _t('Link to selected message') }
|
||||
</label>
|
||||
</StyledCheckbox>
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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...
|
||||
},
|
||||
});
|
|
@ -84,7 +84,7 @@ export default class UploadConfirmDialog extends React.Component {
|
|||
preview = <div>
|
||||
<div>
|
||||
<img className="mx_UploadConfirmDialog_fileIcon"
|
||||
src={require("../../../../res/img/files.png")}
|
||||
src={require("../../../../res/img/feather-customised/files.svg")}
|
||||
/>
|
||||
{this.props.file.name} ({filesize(this.props.file.size)})
|
||||
</div>
|
||||
|
|
|
@ -20,10 +20,8 @@ import PropTypes from 'prop-types';
|
|||
import * as sdk from '../../../../index';
|
||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import Modal from '../../../../Modal';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
|
||||
const RESTORE_TYPE_PASSPHRASE = 0;
|
||||
const RESTORE_TYPE_RECOVERYKEY = 1;
|
||||
|
@ -90,21 +88,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
|
||||
_onResetRecoveryClick = () => {
|
||||
this.props.onFinished(false);
|
||||
|
||||
if (SettingsStore.getValue("feature_cross_signing")) {
|
||||
// If cross-signing is enabled, we reset the SSSS recovery passphrase (and cross-signing keys)
|
||||
this.props.onFinished(false);
|
||||
accessSecretStorage(() => {}, /* forceReset = */ true);
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
{
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
},
|
||||
}, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
accessSecretStorage(() => {}, /* forceReset = */ true);
|
||||
}
|
||||
|
||||
_onRecoveryKeyChange = (e) => {
|
||||
|
@ -243,8 +227,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
loadError: null,
|
||||
});
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored();
|
||||
const cli = MatrixClientPeg.get();
|
||||
const backupInfo = await cli.getKeyBackupVersion();
|
||||
const has4S = await cli.hasSecretStorageKey();
|
||||
const backupKeyStored = has4S && await cli.isKeyBackupKeyStored();
|
||||
this.setState({
|
||||
backupInfo,
|
||||
backupKeyStored,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -58,18 +58,15 @@ export default class Draggable extends React.Component<IProps, IState> {
|
|||
|
||||
document.addEventListener("mousemove", this.state.onMouseMove);
|
||||
document.addEventListener("mouseup", this.state.onMouseUp);
|
||||
console.log("Mouse down")
|
||||
}
|
||||
|
||||
private onMouseUp = (event: MouseEvent): void => {
|
||||
document.removeEventListener("mousemove", this.state.onMouseMove);
|
||||
document.removeEventListener("mouseup", this.state.onMouseUp);
|
||||
this.props.onMouseUp(event);
|
||||
console.log("Mouse up")
|
||||
}
|
||||
|
||||
private onMouseMove(event: MouseEvent): void {
|
||||
console.log("Mouse Move")
|
||||
const newLocation = this.props.dragFunc(this.state.location, event);
|
||||
|
||||
this.setState({
|
||||
|
|
|
@ -15,10 +15,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import * as sdk from '../../../index';
|
||||
import { debounce } from 'lodash';
|
||||
import {IFieldState, IValidationResult} from "../elements/Validation";
|
||||
|
||||
// Invoke validation from user input (when typing, etc.) at most once every N ms.
|
||||
const VALIDATION_THROTTLE_MS = 200;
|
||||
|
@ -29,58 +29,93 @@ function getId() {
|
|||
return `${BASE_ID}_${count++}`;
|
||||
}
|
||||
|
||||
export default class Field extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The field's ID, which binds the input and label together. Immutable.
|
||||
id: PropTypes.string,
|
||||
// The element to create. Defaults to "input".
|
||||
// To define options for a select, use <Field><option ... /></Field>
|
||||
element: PropTypes.oneOf(["input", "select", "textarea"]),
|
||||
// The field's type (when used as an <input>). Defaults to "text".
|
||||
type: PropTypes.string,
|
||||
// id of a <datalist> element for suggestions
|
||||
list: PropTypes.string,
|
||||
// The field's label string.
|
||||
label: PropTypes.string,
|
||||
// The field's placeholder string. Defaults to the label.
|
||||
placeholder: PropTypes.string,
|
||||
// The field's value.
|
||||
// This is a controlled component, so the value is required.
|
||||
value: PropTypes.string.isRequired,
|
||||
// Optional component to include inside the field before the input.
|
||||
prefix: PropTypes.node,
|
||||
// Optional component to include inside the field after the input.
|
||||
postfix: PropTypes.node,
|
||||
// The callback called whenever the contents of the field
|
||||
// changes. Returns an object with `valid` boolean field
|
||||
// and a `feedback` react component field to provide feedback
|
||||
// to the user.
|
||||
onValidate: PropTypes.func,
|
||||
// If specified, overrides the value returned by onValidate.
|
||||
flagInvalid: PropTypes.bool,
|
||||
// If specified, contents will appear as a tooltip on the element and
|
||||
// validation feedback tooltips will be suppressed.
|
||||
tooltipContent: PropTypes.node,
|
||||
// If specified alongside tooltipContent, the class name to apply to the
|
||||
// tooltip itself.
|
||||
tooltipClassName: PropTypes.string,
|
||||
// If specified, an additional class name to apply to the field container
|
||||
className: PropTypes.string,
|
||||
// All other props pass through to the <input>.
|
||||
};
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLSelectElement | HTMLInputElement> {
|
||||
// The field's ID, which binds the input and label together. Immutable.
|
||||
id?: string,
|
||||
// The element to create. Defaults to "input".
|
||||
// To define options for a select, use <Field><option ... /></Field>
|
||||
element?: "input" | "select" | "textarea",
|
||||
// The field's type (when used as an <input>). Defaults to "text".
|
||||
type?: string,
|
||||
// id of a <datalist> element for suggestions
|
||||
list?: string,
|
||||
// The field's label string.
|
||||
label?: string,
|
||||
// The field's placeholder string. Defaults to the label.
|
||||
placeholder?: string,
|
||||
// The field's value.
|
||||
// This is a controlled component, so the value is required.
|
||||
value: string,
|
||||
// Optional component to include inside the field before the input.
|
||||
prefixComponent?: React.ReactNode,
|
||||
// Optional component to include inside the field after the input.
|
||||
postfixComponent?: React.ReactNode,
|
||||
// The callback called whenever the contents of the field
|
||||
// changes. Returns an object with `valid` boolean field
|
||||
// and a `feedback` react component field to provide feedback
|
||||
// to the user.
|
||||
onValidate?: (input: IFieldState) => Promise<IValidationResult>,
|
||||
// If specified, overrides the value returned by onValidate.
|
||||
flagInvalid?: boolean,
|
||||
// If specified, contents will appear as a tooltip on the element and
|
||||
// validation feedback tooltips will be suppressed.
|
||||
tooltipContent?: React.ReactNode,
|
||||
// If specified alongside tooltipContent, the class name to apply to the
|
||||
// tooltip itself.
|
||||
tooltipClassName?: string,
|
||||
// If specified, an additional class name to apply to the field container
|
||||
className?: string,
|
||||
// All other props pass through to the <input>.
|
||||
}
|
||||
|
||||
interface IState {
|
||||
valid: boolean,
|
||||
feedback: React.ReactNode,
|
||||
feedbackVisible: boolean,
|
||||
focused: boolean,
|
||||
}
|
||||
|
||||
export default class Field extends React.PureComponent<IProps, IState> {
|
||||
private id: string;
|
||||
private input: HTMLInputElement;
|
||||
|
||||
private static defaultProps = {
|
||||
element: "input",
|
||||
type: "text",
|
||||
}
|
||||
|
||||
/*
|
||||
* This was changed from throttle to debounce: this is more traditional for
|
||||
* form validation since it means that the validation doesn't happen at all
|
||||
* until the user stops typing for a bit (debounce defaults to not running on
|
||||
* the leading edge). If we're doing an HTTP hit on each validation, we have more
|
||||
* incentive to prevent validating input that's very unlikely to be valid.
|
||||
* We may find that we actually want different behaviour for registration
|
||||
* fields, in which case we can add some options to control it.
|
||||
*/
|
||||
private validateOnChange = debounce(() => {
|
||||
this.validate({
|
||||
focused: true,
|
||||
});
|
||||
}, VALIDATION_THROTTLE_MS);
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
valid: undefined,
|
||||
feedback: undefined,
|
||||
feedbackVisible: false,
|
||||
focused: false,
|
||||
};
|
||||
|
||||
this.id = this.props.id || getId();
|
||||
}
|
||||
|
||||
onFocus = (ev) => {
|
||||
public focus() {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
private onFocus = (ev) => {
|
||||
this.setState({
|
||||
focused: true,
|
||||
});
|
||||
|
@ -93,7 +128,7 @@ export default class Field extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
onChange = (ev) => {
|
||||
private onChange = (ev) => {
|
||||
this.validateOnChange();
|
||||
// Parent component may have supplied its own `onChange` as well
|
||||
if (this.props.onChange) {
|
||||
|
@ -101,7 +136,7 @@ export default class Field extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
onBlur = (ev) => {
|
||||
private onBlur = (ev) => {
|
||||
this.setState({
|
||||
focused: false,
|
||||
});
|
||||
|
@ -114,11 +149,7 @@ export default class Field extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
focus() {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
async validate({ focused, allowEmpty = true }) {
|
||||
private async validate({ focused, allowEmpty = true }: {focused: boolean, allowEmpty?: boolean}) {
|
||||
if (!this.props.onValidate) {
|
||||
return;
|
||||
}
|
||||
|
@ -149,56 +180,42 @@ export default class Field extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This was changed from throttle to debounce: this is more traditional for
|
||||
* form validation since it means that the validation doesn't happen at all
|
||||
* until the user stops typing for a bit (debounce defaults to not running on
|
||||
* the leading edge). If we're doing an HTTP hit on each validation, we have more
|
||||
* incentive to prevent validating input that's very unlikely to be valid.
|
||||
* We may find that we actually want different behaviour for registration
|
||||
* fields, in which case we can add some options to control it.
|
||||
*/
|
||||
validateOnChange = debounce(() => {
|
||||
this.validate({
|
||||
focused: true,
|
||||
});
|
||||
}, VALIDATION_THROTTLE_MS);
|
||||
|
||||
render() {
|
||||
|
||||
public render() {
|
||||
const {
|
||||
element, prefix, postfix, className, onValidate, children,
|
||||
element, prefixComponent, postfixComponent, className, onValidate, children,
|
||||
tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props;
|
||||
|
||||
const inputElement = element || "input";
|
||||
|
||||
// Set some defaults for the <input> element
|
||||
inputProps.type = inputProps.type || "text";
|
||||
inputProps.ref = input => this.input = input;
|
||||
const ref = input => this.input = input;
|
||||
inputProps.placeholder = inputProps.placeholder || inputProps.label;
|
||||
inputProps.id = this.id; // this overwrites the id from props
|
||||
|
||||
inputProps.onFocus = this.onFocus;
|
||||
inputProps.onChange = this.onChange;
|
||||
inputProps.onBlur = this.onBlur;
|
||||
inputProps.list = list;
|
||||
|
||||
const fieldInput = React.createElement(inputElement, inputProps, children);
|
||||
// Appease typescript's inference
|
||||
const inputProps_ = {...inputProps, ref, list};
|
||||
|
||||
const fieldInput = React.createElement(this.props.element, inputProps_, children);
|
||||
|
||||
let prefixContainer = null;
|
||||
if (prefix) {
|
||||
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
|
||||
if (prefixComponent) {
|
||||
prefixContainer = <span className="mx_Field_prefix">{prefixComponent}</span>;
|
||||
}
|
||||
let postfixContainer = null;
|
||||
if (postfix) {
|
||||
postfixContainer = <span className="mx_Field_postfix">{postfix}</span>;
|
||||
if (postfixComponent) {
|
||||
postfixContainer = <span className="mx_Field_postfix">{postfixComponent}</span>;
|
||||
}
|
||||
|
||||
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
|
||||
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, className, {
|
||||
const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, {
|
||||
// If we have a prefix element, leave the label always at the top left and
|
||||
// don't animate it, as it looks a bit clunky and would add complexity to do
|
||||
// properly.
|
||||
mx_Field_labelAlwaysTopLeft: prefix,
|
||||
mx_Field_labelAlwaysTopLeft: prefixComponent,
|
||||
mx_Field_valid: onValidate && this.state.valid === true,
|
||||
mx_Field_invalid: hasValidationFlag
|
||||
? flagInvalid
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue