mirror of https://github.com/vector-im/riot-web
Merge branches 'develop' and 't3chguy/clean_up_TextualBody' of github.com:matrix-org/matrix-react-sdk into t3chguy/clean_up_TextualBody
Conflicts: src/components/views/messages/TextualBody.jspull/21833/head
commit
9c500e3544
|
@ -33,7 +33,6 @@ src/components/views/rooms/RoomList.js
|
|||
src/components/views/rooms/RoomPreviewBar.js
|
||||
src/components/views/rooms/SearchBar.js
|
||||
src/components/views/rooms/SearchResultTile.js
|
||||
src/components/views/rooms/SlateMessageComposer.js
|
||||
src/components/views/settings/ChangeAvatar.js
|
||||
src/components/views/settings/ChangePassword.js
|
||||
src/components/views/settings/DevicesPanel.js
|
||||
|
@ -58,7 +57,6 @@ src/utils/Receipt.js
|
|||
src/Velociraptor.js
|
||||
test/components/structures/MessagePanel-test.js
|
||||
test/components/views/dialogs/InteractiveAuthDialog-test.js
|
||||
test/components/views/rooms/MessageComposerInput-test.js
|
||||
test/mock-clock.js
|
||||
test/notifications/ContentRules-test.js
|
||||
test/notifications/PushRuleVectorState-test.js
|
||||
|
|
114
CHANGELOG.md
114
CHANGELOG.md
|
@ -1,3 +1,117 @@
|
|||
Changes in [1.7.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.5) (2019-12-09)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.5-rc.1...v1.7.5)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [1.7.5-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.5-rc.1) (2019-12-04)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.4...v1.7.5-rc.1)
|
||||
|
||||
* Remove logs before running end-to-end tests
|
||||
[\#3700](https://github.com/matrix-org/matrix-react-sdk/pull/3700)
|
||||
* Update from Weblate
|
||||
[\#3699](https://github.com/matrix-org/matrix-react-sdk/pull/3699)
|
||||
* Match e2e icons on events to expectations
|
||||
[\#3697](https://github.com/matrix-org/matrix-react-sdk/pull/3697)
|
||||
* Match room upgrade warning to the new design
|
||||
[\#3695](https://github.com/matrix-org/matrix-react-sdk/pull/3695)
|
||||
* Remove unused translations
|
||||
[\#3683](https://github.com/matrix-org/matrix-react-sdk/pull/3683)
|
||||
* Remove broken velocity-ui animation
|
||||
[\#3678](https://github.com/matrix-org/matrix-react-sdk/pull/3678)
|
||||
* Update from Weblate
|
||||
[\#3696](https://github.com/matrix-org/matrix-react-sdk/pull/3696)
|
||||
* Hide Remove button in message editing history if you don't have permission
|
||||
to redact
|
||||
[\#3685](https://github.com/matrix-org/matrix-react-sdk/pull/3685)
|
||||
* Add an option to invite users to upgraded private rooms
|
||||
[\#3684](https://github.com/matrix-org/matrix-react-sdk/pull/3684)
|
||||
* Do not trap Key ContextMenu into composer for keyboard a11y
|
||||
[\#3689](https://github.com/matrix-org/matrix-react-sdk/pull/3689)
|
||||
* Make EmojiPicker filtering case-insensitive
|
||||
[\#3690](https://github.com/matrix-org/matrix-react-sdk/pull/3690)
|
||||
* Ensure the settings page accurately represents theme choices
|
||||
[\#3686](https://github.com/matrix-org/matrix-react-sdk/pull/3686)
|
||||
* Ensure read receipts end up with a valid reference to checkUnmounting
|
||||
[\#3688](https://github.com/matrix-org/matrix-react-sdk/pull/3688)
|
||||
* Convert Velociraptor component to a class
|
||||
[\#3687](https://github.com/matrix-org/matrix-react-sdk/pull/3687)
|
||||
* Add a link to the labs feature documentation
|
||||
[\#3675](https://github.com/matrix-org/matrix-react-sdk/pull/3675)
|
||||
* Improve translatable strings for calls
|
||||
[\#3682](https://github.com/matrix-org/matrix-react-sdk/pull/3682)
|
||||
* Don't assume that diffs will have an appropriate child node
|
||||
[\#3680](https://github.com/matrix-org/matrix-react-sdk/pull/3680)
|
||||
* Fix persisted widgets getting stuck at loading screens
|
||||
[\#3681](https://github.com/matrix-org/matrix-react-sdk/pull/3681)
|
||||
* Add button to clear all notification counts, sometimes stuck in historical
|
||||
[\#2959](https://github.com/matrix-org/matrix-react-sdk/pull/2959)
|
||||
* Fix multi-invite error dialog messaging
|
||||
[\#3679](https://github.com/matrix-org/matrix-react-sdk/pull/3679)
|
||||
* Make the communities button behave more like a toggle
|
||||
[\#3670](https://github.com/matrix-org/matrix-react-sdk/pull/3670)
|
||||
* Change read markers to use CSS transitions
|
||||
[\#3674](https://github.com/matrix-org/matrix-react-sdk/pull/3674)
|
||||
* fix font smoothing to match figma
|
||||
[\#3677](https://github.com/matrix-org/matrix-react-sdk/pull/3677)
|
||||
* Update breadcrumbs when we do eventually see upgraded rooms
|
||||
[\#3669](https://github.com/matrix-org/matrix-react-sdk/pull/3669)
|
||||
* Fix override behaviour of system vs defined themes
|
||||
[\#3673](https://github.com/matrix-org/matrix-react-sdk/pull/3673)
|
||||
* console.log doesn't take %s substitutions
|
||||
[\#3671](https://github.com/matrix-org/matrix-react-sdk/pull/3671)
|
||||
* EventIndex: Move the checkpoint loading logic into the init method.
|
||||
[\#3648](https://github.com/matrix-org/matrix-react-sdk/pull/3648)
|
||||
* Clarify that cross-signing is in development
|
||||
[\#3668](https://github.com/matrix-org/matrix-react-sdk/pull/3668)
|
||||
* Hide tooltips with CSS when they aren't visible
|
||||
[\#3665](https://github.com/matrix-org/matrix-react-sdk/pull/3665)
|
||||
* a11y: adjustments for toasts
|
||||
[\#3667](https://github.com/matrix-org/matrix-react-sdk/pull/3667)
|
||||
* Update from Weblate
|
||||
[\#3666](https://github.com/matrix-org/matrix-react-sdk/pull/3666)
|
||||
* Null check on thumbnail_file
|
||||
[\#3664](https://github.com/matrix-org/matrix-react-sdk/pull/3664)
|
||||
* Fix double date separator for room upgrade tiles
|
||||
[\#3662](https://github.com/matrix-org/matrix-react-sdk/pull/3662)
|
||||
* Show incoming verification requests in in-app notifications
|
||||
[\#3661](https://github.com/matrix-org/matrix-react-sdk/pull/3661)
|
||||
* Show m.room.create event before the ELS on room upgrade
|
||||
[\#3655](https://github.com/matrix-org/matrix-react-sdk/pull/3655)
|
||||
* Convert MessagePanel to React class
|
||||
[\#3656](https://github.com/matrix-org/matrix-react-sdk/pull/3656)
|
||||
* Make addEventListener conditional
|
||||
[\#3657](https://github.com/matrix-org/matrix-react-sdk/pull/3657)
|
||||
* Fix e2e icons
|
||||
[\#3653](https://github.com/matrix-org/matrix-react-sdk/pull/3653)
|
||||
* Workaround for soft-crash with calls on startup
|
||||
[\#3654](https://github.com/matrix-org/matrix-react-sdk/pull/3654)
|
||||
* Catch exceptions when we can't play audio
|
||||
[\#3652](https://github.com/matrix-org/matrix-react-sdk/pull/3652)
|
||||
* Rename section heading for integrations in settings
|
||||
[\#3650](https://github.com/matrix-org/matrix-react-sdk/pull/3650)
|
||||
* Update copy for widgets not using message encryption
|
||||
[\#3651](https://github.com/matrix-org/matrix-react-sdk/pull/3651)
|
||||
* Ignore media actions
|
||||
[\#3649](https://github.com/matrix-org/matrix-react-sdk/pull/3649)
|
||||
* Add an option to disable the use of integration managers for provisioning
|
||||
[\#3646](https://github.com/matrix-org/matrix-react-sdk/pull/3646)
|
||||
* Move many widget options to a context menu
|
||||
[\#3645](https://github.com/matrix-org/matrix-react-sdk/pull/3645)
|
||||
* Re-add encryption warning to widget permission prompt
|
||||
[\#3644](https://github.com/matrix-org/matrix-react-sdk/pull/3644)
|
||||
* Update CIDER docs now that it is used for main composer as well
|
||||
[\#3647](https://github.com/matrix-org/matrix-react-sdk/pull/3647)
|
||||
* get rid of bluebird
|
||||
[\#3593](https://github.com/matrix-org/matrix-react-sdk/pull/3593)
|
||||
* Remove getBaseTheme
|
||||
[\#3638](https://github.com/matrix-org/matrix-react-sdk/pull/3638)
|
||||
* ReactionsRowButtonTooltip: fix null dereference if emoji owner left room
|
||||
[\#3643](https://github.com/matrix-org/matrix-react-sdk/pull/3643)
|
||||
* Add eslint-plugin-jest because we inherit js-sdk's eslintrc and it wants
|
||||
[\#3642](https://github.com/matrix-org/matrix-react-sdk/pull/3642)
|
||||
|
||||
Changes in [1.7.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.4) (2019-11-27)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.3...v1.7.4)
|
||||
|
|
|
@ -4,7 +4,7 @@ matrix-react-sdk
|
|||
This is a react-based SDK for inserting a Matrix chat/voip client into a web page.
|
||||
|
||||
This package provides the React components needed to build a Matrix web client
|
||||
using React. It is not useable in isolation, and instead must must be used from
|
||||
using React. It is not useable in isolation, and instead must be used from
|
||||
a 'skin'. A skin provides:
|
||||
* Customised implementations of presentation components.
|
||||
* Custom CSS
|
||||
|
@ -82,7 +82,7 @@ practices that anyone working with the SDK needs to be be aware of and uphold:
|
|||
'Stealing' styling information from other components (including parents)
|
||||
is not cool, as it breaks the independence of the components.
|
||||
|
||||
* CSS classes are named with an app-specific namespacing prefix to try to avoid
|
||||
* CSS classes are named with an app-specific name-spacing prefix to try to avoid
|
||||
CSS collisions. The base skin shipped by Matrix.org with the matrix-react-sdk
|
||||
uses the naming prefix "mx_". A company called Yoyodyne Inc might use a
|
||||
prefix like "yy_" for its app-specific classes.
|
||||
|
@ -107,7 +107,7 @@ practices that anyone working with the SDK needs to be be aware of and uphold:
|
|||
.mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its
|
||||
own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override
|
||||
only to the context of RoomList views. N.B. overrides should be relatively
|
||||
rare as in general CSS inheritence should be enough.
|
||||
rare as in general CSS inheritance should be enough.
|
||||
|
||||
* Components should render only within the bounding box of their outermost DOM
|
||||
element. Page-absolute positioning and negative CSS margins and similar are
|
||||
|
|
|
@ -174,12 +174,6 @@ React
|
|||
<Foo onClick={this.onFooClick}> // Best, if onFooClick would do anything other than directly calling doStuff
|
||||
```
|
||||
|
||||
Not doing so is acceptable in a single case: in function-refs:
|
||||
|
||||
```jsx
|
||||
<Foo ref={(self) => this.component = self}>
|
||||
```
|
||||
|
||||
- Prefer classes that extend `React.Component` (or `React.PureComponent`) instead of `React.createClass`
|
||||
- You can avoid the need to bind handler functions by using [property initializers](https://reactjs.org/docs/react-component.html#constructor):
|
||||
|
||||
|
@ -208,3 +202,5 @@ React
|
|||
```
|
||||
- Think about whether your component really needs state: are you duplicating
|
||||
information in component state that could be derived from the model?
|
||||
|
||||
- Avoid things marked as Legacy or Deprecated in React 16 (e.g string refs and legacy contexts)
|
||||
|
|
11
package.json
11
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "1.7.4",
|
||||
"version": "1.7.5",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -43,7 +43,6 @@
|
|||
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
|
||||
"build": "yarn reskindex && yarn start:init",
|
||||
"build:watch": "babel src -w --skip-initial-build -d lib --source-maps --copy-files",
|
||||
"emoji-data-strip": "node scripts/emoji-data-strip.js",
|
||||
"start": "yarn start:init && yarn start:all",
|
||||
"start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn build:watch\" \"yarn reskindex:watch\"",
|
||||
"start:init": "babel src -d lib --source-maps --copy-files",
|
||||
|
@ -75,7 +74,6 @@
|
|||
"file-saver": "^1.3.3",
|
||||
"filesize": "3.5.6",
|
||||
"flux": "2.1.1",
|
||||
"focus-trap-react": "^3.0.5",
|
||||
"focus-visible": "^5.0.2",
|
||||
"fuse.js": "^2.2.0",
|
||||
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566",
|
||||
|
@ -83,6 +81,7 @@
|
|||
"glob": "^5.0.14",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"highlight.js": "^9.15.8",
|
||||
"humanize": "^0.0.9",
|
||||
"is-ip": "^2.0.0",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"linkifyjs": "^2.1.6",
|
||||
|
@ -100,16 +99,14 @@
|
|||
"react-addons-css-transition-group": "15.6.2",
|
||||
"react-beautiful-dnd": "^4.0.1",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-focus-lock": "^2.2.1",
|
||||
"react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#9cf17f63b7c0b0ec5f31df27da0f82f7238dc594",
|
||||
"resize-observer-polyfill": "^1.5.0",
|
||||
"sanitize-html": "^1.18.4",
|
||||
"slate": "^0.41.2",
|
||||
"slate-html-serializer": "^0.6.1",
|
||||
"slate-md-serializer": "github:matrix-org/slate-md-serializer#f7c4ad3",
|
||||
"slate-react": "^0.18.10",
|
||||
"text-encoding-utf-8": "^1.0.1",
|
||||
"url": "^0.11.0",
|
||||
"velocity-animate": "^1.5.2",
|
||||
"what-input": "^5.2.6",
|
||||
"whatwg-fetch": "^1.1.1",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
@import "./structures/_SearchBox.scss";
|
||||
@import "./structures/_TabbedView.scss";
|
||||
@import "./structures/_TagPanel.scss";
|
||||
@import "./structures/_TagPanelButtons.scss";
|
||||
@import "./structures/_ToastContainer.scss";
|
||||
@import "./structures/_TopLeftMenuButton.scss";
|
||||
@import "./structures/_UploadBar.scss";
|
||||
|
@ -57,6 +56,7 @@
|
|||
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
||||
@import "./views/dialogs/_CreateGroupDialog.scss";
|
||||
@import "./views/dialogs/_CreateRoomDialog.scss";
|
||||
@import "./views/dialogs/_DMInviteDialog.scss";
|
||||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||
@import "./views/dialogs/_DeviceVerifyDialog.scss";
|
||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||
|
@ -64,7 +64,6 @@
|
|||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
||||
@import "./views/dialogs/_RestoreKeyBackupDialog.scss";
|
||||
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
||||
@import "./views/dialogs/_RoomUpgradeDialog.scss";
|
||||
@import "./views/dialogs/_RoomUpgradeWarningDialog.scss";
|
||||
|
@ -83,6 +82,8 @@
|
|||
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
|
||||
@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss";
|
||||
@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
|
||||
@import "./views/dialogs/secretstorage/_AccessSecretStorageDialog.scss";
|
||||
@import "./views/dialogs/secretstorage/_CreateSecretStorageDialog.scss";
|
||||
@import "./views/directory/_NetworkDropdown.scss";
|
||||
@import "./views/elements/_AccessibleButton.scss";
|
||||
@import "./views/elements/_AddressSelector.scss";
|
||||
|
@ -173,7 +174,10 @@
|
|||
@import "./views/rooms/_SendMessageComposer.scss";
|
||||
@import "./views/rooms/_Stickers.scss";
|
||||
@import "./views/rooms/_TopUnreadMessagesBar.scss";
|
||||
@import "./views/rooms/_UserOnlineDot.scss";
|
||||
@import "./views/rooms/_WhoIsTypingTile.scss";
|
||||
@import "./views/settings/_AvatarSetting.scss";
|
||||
@import "./views/settings/_CrossSigningPanel.scss";
|
||||
@import "./views/settings/_DevicesPanel.scss";
|
||||
@import "./views/settings/_EmailAddresses.scss";
|
||||
@import "./views/settings/_IntegrationManager.scss";
|
||||
|
|
|
@ -26,11 +26,16 @@ limitations under the License.
|
|||
|
||||
.mx_CustomRoomTagPanel_scroller {
|
||||
max-height: inherit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_CustomRoomTagPanel .mx_AccessibleButton {
|
||||
margin: 9px auto;
|
||||
margin: 0 auto;
|
||||
width: 40px;
|
||||
padding: 10px 0 9px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mx_CustomRoomTagPanel .mx_BaseAvatar_image {
|
||||
|
@ -39,7 +44,13 @@ limitations under the License.
|
|||
height: 40px;
|
||||
}
|
||||
|
||||
.mx_CustomRoomTagPanel .mx_AccessibleButton.CustomRoomTagPanel_tileSelected .mx_BaseAvatar_image {
|
||||
border: 3px solid $warning-color;
|
||||
border-radius: 40px;
|
||||
.mx_CustomRoomTagPanel .mx_AccessibleButton.CustomRoomTagPanel_tileSelected::before {
|
||||
content: '';
|
||||
height: 56px;
|
||||
background-color: $accent-color-alt;
|
||||
width: 5px;
|
||||
position: absolute;
|
||||
left: -15px;
|
||||
border-radius: 0 3px 3px 0;
|
||||
top: 2px; // 10 [padding-top] - (56 - 40)/2
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ limitations under the License.
|
|||
}
|
||||
.mx_TagPanel .mx_TagPanel_tagTileContainer > div {
|
||||
height: 40px;
|
||||
padding: 5px 0 4px 0;
|
||||
padding: 10px 0 9px 0;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile {
|
||||
|
@ -82,21 +82,39 @@ limitations under the License.
|
|||
// opacity: 1;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile.mx_TagTile_selected .mx_TagTile_avatar .mx_BaseAvatar {
|
||||
background-color: $accent-color;
|
||||
border-radius: 40px;
|
||||
|
||||
/* In case this is a "initial" avatar */
|
||||
display: block;
|
||||
.mx_TagPanel .mx_TagTile_plus {
|
||||
margin-bottom: 12px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 20px;
|
||||
background-color: $roomheader-addroom-bg-color;
|
||||
position: relative;
|
||||
/* overwrite mx_RoleButton inline-block */
|
||||
display: block !important;
|
||||
|
||||
&::before {
|
||||
background-color: $roomheader-addroom-fg-color;
|
||||
mask-image: url('$(res)/img/feather-customised/plus.svg');
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile_selected .mx_BaseAvatar_image {
|
||||
border: 3px solid $accent-color;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
box-sizing: border-box;
|
||||
.mx_TagPanel .mx_TagTile.mx_TagTile_selected::before {
|
||||
content: '';
|
||||
height: 56px;
|
||||
background-color: $accent-color;
|
||||
width: 5px;
|
||||
position: absolute;
|
||||
left: -15px;
|
||||
border-radius: 0 3px 3px 0;
|
||||
top: -8px; // (56 - 40)/2
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus {
|
||||
|
|
|
@ -1,56 +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.
|
||||
*/
|
||||
|
||||
.mx_TagPanelButtons {
|
||||
background-color: $tagpanel-bg-color;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 17px 0 3px 0;
|
||||
}
|
||||
|
||||
.mx_TagPanelButtons > .mx_GroupsButton::before {
|
||||
mask: url('$(res)/img/feather-customised/users.svg');
|
||||
mask-position: center 11px;
|
||||
}
|
||||
|
||||
.mx_TagPanelButtons > .mx_TagPanelButtons_report::before {
|
||||
mask: url('$(res)/img/feather-customised/life-buoy.svg');
|
||||
mask-position: center 9px;
|
||||
}
|
||||
|
||||
.mx_TagPanelButtons > .mx_AccessibleButton {
|
||||
margin-bottom: 12px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 20px;
|
||||
background-color: $tagpanel-button-color;
|
||||
position: relative;
|
||||
/* overwrite mx_RoleButton inline-block */
|
||||
display: block !important;
|
||||
|
||||
&::before {
|
||||
background-color: $tagpanel-bg-color;
|
||||
mask-repeat: no-repeat;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
|
@ -40,6 +40,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_BaseAvatar_image {
|
||||
object-fit: cover;
|
||||
border-radius: 40px;
|
||||
vertical-align: top;
|
||||
background-color: $avatar-bg-color;
|
||||
|
|
|
@ -53,6 +53,10 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/feather-customised/home.svg');
|
||||
}
|
||||
|
||||
.mx_TopLeftMenu_icon_help::after {
|
||||
mask-image: url('$(res)/img/feather-customised/life-buoy.svg');
|
||||
}
|
||||
|
||||
.mx_TopLeftMenu_icon_settings::after {
|
||||
mask-image: url('$(res)/img/feather-customised/settings.svg');
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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_DMInviteDialog_addressBar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.mx_DMInviteDialog_editor {
|
||||
flex: 1;
|
||||
width: 100%; // Needed to make the Field inside grow
|
||||
}
|
||||
|
||||
.mx_Field {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_goButton {
|
||||
width: 48px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_section {
|
||||
padding-bottom: 10px;
|
||||
|
||||
h3 {
|
||||
font-size: 12px;
|
||||
color: $muted-fg-color;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_roomTile {
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: $user-tile-hover-bg-color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
* {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_roomTile_name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: $primary-fg-color;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_roomTile_userId {
|
||||
font-size: 12px;
|
||||
color: $muted-fg-color;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.mx_DMInviteDialog_roomTile_time {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: $muted-fg-color;
|
||||
float: right;
|
||||
line-height: 36px; // Height of the avatar to keep the time vertically aligned
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,11 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/feather-customised/users-sm.svg');
|
||||
}
|
||||
|
||||
.mx_RoomSettingsDialog_bridgesIcon::before {
|
||||
// This icon is pants, please improve :)
|
||||
mask-image: url('$(res)/img/feather-customised/bridge.svg');
|
||||
}
|
||||
|
||||
.mx_RoomSettingsDialog_warningIcon::before {
|
||||
mask-image: url('$(res)/img/feather-customised/warning-triangle.svg');
|
||||
}
|
||||
|
@ -42,3 +47,25 @@ limitations under the License.
|
|||
padding-left: 40px;
|
||||
padding-right: 80px;
|
||||
}
|
||||
|
||||
// show a different AvatarSetting placeholder for RoomProfileSettings which is basically a clone of ProfileSettings
|
||||
.mx_RoomSettingsDialog .mx_AvatarSetting_avatar .mx_AvatarSetting_avatarPlaceholder::before {
|
||||
mask: url("$(res)/img/feather-customised/image.svg");
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 36px;
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RoomSettingsDialog_BridgeList {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mx_RoomSettingsDialog_BridgeList li {
|
||||
list-style-type: none;
|
||||
padding: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-width: 1px 0px;
|
||||
border-color: #dee1f3;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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,6 +15,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_RestoreKeyBackupDialog_keyStatus {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.mx_RestoreKeyBackupDialog_primaryContainer {
|
||||
/* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
|
||||
padding: 20px;
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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_AccessSecretStorageDialog_keyStatus {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.mx_AccessSecretStorageDialog_primaryContainer {
|
||||
/* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.mx_AccessSecretStorageDialog_passPhraseInput,
|
||||
.mx_AccessSecretStorageDialog_recoveryKeyInput {
|
||||
width: 300px;
|
||||
border: 1px solid $accent-color;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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_CreateSecretStorageDialog .mx_Dialog_title {
|
||||
/* TODO: Consider setting this for all dialog titles. */
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_primaryContainer {
|
||||
/* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_primaryContainer::after {
|
||||
content: "";
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_passPhraseContainer {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_passPhraseHelp {
|
||||
flex: 1;
|
||||
height: 85px;
|
||||
margin-left: 20px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_passPhraseHelp progress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_passPhraseInput {
|
||||
flex: none;
|
||||
width: 250px;
|
||||
border: 1px solid $accent-color;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_passPhraseMatch {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_recoveryKeyHeader {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_recoveryKeyContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_recoveryKey {
|
||||
width: 262px;
|
||||
padding: 20px;
|
||||
color: $info-plinth-fg-color;
|
||||
background-color: $info-plinth-bg-color;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_recoveryKeyButtons {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_recoveryKeyButtons button {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
|
@ -63,7 +63,6 @@ limitations under the License.
|
|||
|
||||
.mx_UserInfo_avatar {
|
||||
margin: 24px 32px 0 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_UserInfo_avatar > div {
|
||||
|
@ -77,12 +76,27 @@ limitations under the License.
|
|||
that's why we had to put the margin to center on a parent div),
|
||||
and not a % of the parent height. */
|
||||
padding-top: 100%;
|
||||
height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mx_UserInfo_avatar > div > div * {
|
||||
border-radius: 100%;
|
||||
box-sizing: content-box;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mx_UserInfo_avatar .mx_BaseAvatar_initial {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
// override the calculated sizes so that the letter isn't HUGE
|
||||
font-size: 26px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image {
|
||||
|
|
|
@ -22,7 +22,9 @@ limitations under the License.
|
|||
display: block;
|
||||
}
|
||||
|
||||
.mx_E2EIcon_verified::after, .mx_E2EIcon_warning::after {
|
||||
.mx_E2EIcon_warning::after,
|
||||
.mx_E2EIcon_normal::after,
|
||||
.mx_E2EIcon_verified::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
@ -34,10 +36,14 @@ limitations under the License.
|
|||
background-size: contain;
|
||||
}
|
||||
|
||||
.mx_E2EIcon_verified::after {
|
||||
background-image: url('$(res)/img/e2e/verified.svg');
|
||||
}
|
||||
|
||||
.mx_E2EIcon_warning::after {
|
||||
background-image: url('$(res)/img/e2e/warning.svg');
|
||||
}
|
||||
|
||||
.mx_E2EIcon_normal::after {
|
||||
background-image: url('$(res)/img/e2e/normal.svg');
|
||||
}
|
||||
|
||||
.mx_E2EIcon_verified::after {
|
||||
background-image: url('$(res)/img/e2e/verified.svg');
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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.
|
||||
|
@ -169,6 +170,7 @@ limitations under the License.
|
|||
|
||||
.mx_EventTile:hover .mx_MessageActionBar,
|
||||
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
|
||||
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
|
||||
.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
|
||||
visibility: visible;
|
||||
}
|
||||
|
@ -352,7 +354,6 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
|
|||
left: 46px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
|
|
@ -52,12 +52,18 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_LinkPreviewWidget_cancel {
|
||||
visibility: hidden;
|
||||
cursor: pointer;
|
||||
flex: 0 0 40px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
img {
|
||||
flex: 0 0 40px;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_LinkPreviewWidget:hover .mx_LinkPreviewWidget_cancel {
|
||||
.mx_LinkPreviewWidget:hover .mx_LinkPreviewWidget_cancel img,
|
||||
.mx_LinkPreviewWidget_cancel.focus-visible:focus img {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
|
|
@ -40,4 +40,5 @@ limitations under the License.
|
|||
|
||||
.mx_RoomRecoveryReminder_secondary {
|
||||
font-size: 90%;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 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.
|
||||
|
|
|
@ -37,6 +37,10 @@ limitations under the License.
|
|||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_SearchBar_buttons {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.mx_SearchBar_button {
|
||||
border: 0;
|
||||
margin: 0 0 0 22px;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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,6 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_RestoreKeyBackupDialog_keyStatus {
|
||||
height: 30px;
|
||||
.mx_UserOnlineDot {
|
||||
border-radius: 50%;
|
||||
background-color: $accent-color;
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
display: inline-block;
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
Copyright 2019 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_AvatarSetting_avatar {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
margin-left: 13px;
|
||||
position: relative;
|
||||
|
||||
& > * {
|
||||
width: 88px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton.mx_AccessibleButton_kind_primary {
|
||||
margin-top: 8px;
|
||||
|
||||
div {
|
||||
position: relative;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
display: inline;
|
||||
padding-right: 6px; // 0.5 * 12px
|
||||
left: -6px; // 0.5 * 12px
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
div::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
|
||||
background-color: $button-primary-fg-color;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
mask-image: url('$(res)/img/feather-customised/upload.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton.mx_AccessibleButton_kind_link_sm {
|
||||
color: $button-danger-bg-color;
|
||||
}
|
||||
|
||||
& > img {
|
||||
cursor: pointer;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
& > img,
|
||||
.mx_AvatarSetting_avatarPlaceholder {
|
||||
display: block;
|
||||
height: 88px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mx_AvatarSetting_avatarPlaceholder::before {
|
||||
background-color: $settings-profile-overlay-placeholder-fg-color;
|
||||
mask: url("$(res)/img/feather-customised/user.svg");
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 36px;
|
||||
mask-position: center;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AvatarSetting_avatar .mx_AvatarSetting_avatarPlaceholder {
|
||||
background-color: $settings-profile-placeholder-bg-color;
|
||||
}
|
|
@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
.mx_CrossSigningPanel_statusList {
|
||||
border-spacing: 0;
|
||||
|
||||
// Higher Order Component to allow use of legacy MatrixClient React Context
|
||||
// in Functional Components which do not otherwise support legacy React Contexts
|
||||
export default (Component) => class extends React.PureComponent {
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
td {
|
||||
padding: 0;
|
||||
|
||||
render() {
|
||||
return <Component {...this.props} matrixClient={this.context.matrixClient} />;
|
||||
&:first-of-type {
|
||||
padding-inline-end: 1em;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
.mx_CrossSigningPanel_buttonRow {
|
||||
margin: 1em 0;
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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.
|
||||
|
@ -30,3 +31,7 @@ limitations under the License.
|
|||
.mx_KeyBackupPanel_deviceName {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.mx_KeyBackupPanel_buttonRow {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
|
|
@ -38,91 +38,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_avatar {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
margin-left: 13px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_avatar > * {
|
||||
display: block;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_avatar .mx_ProfileSettings_avatarOverlay_disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_avatar .mx_ProfileSettings_avatarPlaceholder {
|
||||
background-color: $settings-profile-placeholder-bg-color;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_avatarOverlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_avatar:hover .mx_ProfileSettings_avatarOverlay:not(.mx_ProfileSettings_avatarOverlay_disabled) {
|
||||
display: inline-block;
|
||||
opacity: 0.5 !important;
|
||||
color: $settings-profile-overlay-fg-color !important;
|
||||
background-color: $settings-profile-overlay-bg-color !important;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_avatarOverlay_show {
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
color: $settings-profile-overlay-placeholder-fg-color;
|
||||
background-color: $settings-profile-overlay-placeholder-bg-color;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_avatarOverlayText {
|
||||
display: block;
|
||||
margin-top: 17px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_noAvatarText {
|
||||
display: block;
|
||||
margin: 34px auto auto;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_avatarOverlayImgContainer {
|
||||
position: relative;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_avatarOverlayImg::before {
|
||||
background-color: $settings-profile-overlay-placeholder-fg-color;
|
||||
mask: url("$(res)/img/feather-customised/upload.svg");
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 14px;
|
||||
mask-position: center;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_avatar:hover .mx_ProfileSettings_avatarOverlayImg::before {
|
||||
background-color: $settings-profile-overlay-fg-color !important;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_avatarUpload {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg8"
|
||||
version="1.1"
|
||||
viewBox="0 0 5.487504 5.7341776"
|
||||
height="5.7341776mm"
|
||||
width="5.487504mm">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
transform="translate(14.166523,-96.032669)"
|
||||
id="layer1">
|
||||
<rect
|
||||
y="99.461258"
|
||||
x="-10.861272"
|
||||
height="2.0555882"
|
||||
width="1.9322528"
|
||||
id="rect831-6"
|
||||
style="fill:none;stroke:#b8bec9;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
<path
|
||||
id="path883"
|
||||
d="m -11.98427,98.338257 1.122998,1.122998"
|
||||
style="fill:#b8bec9;fill-opacity:1;stroke:#b8bec9;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<rect
|
||||
transform="scale(-1)"
|
||||
y="-98.338257"
|
||||
x="11.98427"
|
||||
height="2.0555882"
|
||||
width="1.9322529"
|
||||
id="rect831-6-7"
|
||||
style="fill:none;stroke:#b8bec9;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,5 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 8C6 6.89543 6.89543 6 8 6H40C41.1046 6 42 6.89543 42 8V40C42 41.1046 41.1046 42 40 42H8C6.89543 42 6 41.1046 6 40V8Z" stroke="#61708B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 20C18.6569 20 20 18.6569 20 17C20 15.3431 18.6569 14 17 14C15.3431 14 14 15.3431 14 17C14 18.6569 15.3431 20 17 20Z" stroke="#61708B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M42 30L32 20L10 42" stroke="#61708B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 721 B |
|
@ -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="M12 5V19" stroke="#2E2F32" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 12H19" stroke="#2E2F32" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 311 B |
|
@ -16,6 +16,7 @@ $room-highlight-color: #343a46;
|
|||
// typical text (dark-on-white in light skin)
|
||||
$primary-fg-color: $text-primary-color;
|
||||
$primary-bg-color: $bg-color;
|
||||
$muted-fg-color: $header-panel-text-primary-color;
|
||||
|
||||
// used for dialog box text
|
||||
$light-fg-color: $header-panel-text-secondary-color;
|
||||
|
@ -172,6 +173,8 @@ $interactive-tooltip-fg-color: #ffffff;
|
|||
|
||||
$breadcrumb-placeholder-bg-color: #272c35;
|
||||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
// ***** Mixins! *****
|
||||
|
||||
@define-mixin mx_DialogButton {
|
||||
|
@ -243,3 +246,13 @@ $breadcrumb-placeholder-bg-color: #272c35;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// diff highlight colors
|
||||
// intentionally swapped to avoid inversion
|
||||
.hljs-addition {
|
||||
background: #fdd;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
background: #dfd;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ $header-panel-bg-color: #f3f8fd;
|
|||
// typical text (dark-on-white in light skin)
|
||||
$primary-fg-color: #2e2f32;
|
||||
$primary-bg-color: #ffffff;
|
||||
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
|
||||
|
||||
// used for dialog box text
|
||||
$light-fg-color: #747474;
|
||||
|
@ -293,6 +294,8 @@ $interactive-tooltip-fg-color: #ffffff;
|
|||
|
||||
$breadcrumb-placeholder-bg-color: #e8eef5;
|
||||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
// ***** Mixins! *****
|
||||
|
||||
@define-mixin mx_DialogButton {
|
||||
|
@ -338,3 +341,12 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
|
|||
color: $accent-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// diff highlight colors
|
||||
.hljs-addition {
|
||||
background: #dfd;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
background: #fdd;
|
||||
}
|
||||
|
|
|
@ -36,7 +36,8 @@ echo "--- Install synapse & other dependencies"
|
|||
./install.sh
|
||||
# install static webserver to server symlinked local copy of riot
|
||||
./riot/install-webserver.sh
|
||||
mkdir logs || rm -r logs/*
|
||||
rm -r logs || true
|
||||
mkdir logs
|
||||
echo "+++ Running end-to-end tests"
|
||||
TESTS_STARTED=1
|
||||
./run.sh --no-sandbox --log-directory logs/
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// This generates src/stripped-emoji.json as used by the EmojiProvider autocomplete
|
||||
// provider.
|
||||
|
||||
const EMOJIBASE = require('emojibase-data/en/compact.json');
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const output = EMOJIBASE.map(
|
||||
(datum) => {
|
||||
const newDatum = {
|
||||
name: datum.annotation,
|
||||
shortname: `:${datum.shortcodes[0]}:`,
|
||||
category: datum.group,
|
||||
emoji_order: datum.order,
|
||||
};
|
||||
if (datum.shortcodes.length > 1) {
|
||||
newDatum.aliases = datum.shortcodes.slice(1).map(s => `:${s}:`);
|
||||
}
|
||||
if (datum.emoticon) {
|
||||
newDatum.aliases_ascii = [ datum.emoticon ];
|
||||
}
|
||||
return newDatum;
|
||||
}
|
||||
);
|
||||
|
||||
// Write to a file in src. Changes should be checked into git. This file is copied by
|
||||
// babel using --copy-files
|
||||
fs.writeFileSync('./src/stripped-emoji.json', JSON.stringify(output));
|
|
@ -422,6 +422,9 @@ export default class ContentMessages {
|
|||
|
||||
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
|
||||
let uploadAll = false;
|
||||
// Promise to complete before sending next file into room, used for synchronisation of file-sending
|
||||
// to match the order the files were specified in
|
||||
let promBefore = Promise.resolve();
|
||||
for (let i = 0; i < okFiles.length; ++i) {
|
||||
const file = okFiles[i];
|
||||
if (!uploadAll) {
|
||||
|
@ -440,11 +443,11 @@ export default class ContentMessages {
|
|||
});
|
||||
if (!shouldContinue) break;
|
||||
}
|
||||
this._sendContentToRoom(file, roomId, matrixClient);
|
||||
promBefore = this._sendContentToRoom(file, roomId, matrixClient, promBefore);
|
||||
}
|
||||
}
|
||||
|
||||
_sendContentToRoom(file, roomId, matrixClient) {
|
||||
_sendContentToRoom(file, roomId, matrixClient, promBefore) {
|
||||
const content = {
|
||||
body: file.name || 'Attachment',
|
||||
info: {
|
||||
|
@ -517,7 +520,10 @@ export default class ContentMessages {
|
|||
content.file = result.file;
|
||||
content.url = result.url;
|
||||
});
|
||||
}).then(function(url) {
|
||||
}).then((url) => {
|
||||
// Await previous message being sent into the room
|
||||
return promBefore;
|
||||
}).then(function() {
|
||||
return matrixClient.sendMessage(roomId, content);
|
||||
}, function(err) {
|
||||
error = err;
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
Copyright 2019 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 Modal from './Modal';
|
||||
import sdk from './index';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase';
|
||||
import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto/recoverykey';
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
// This stores the secret storage private keys in memory for the JS SDK. This is
|
||||
// only meant to act as a cache to avoid prompting the user multiple times
|
||||
// during the same single operation. Use `accessSecretStorage` below to scope a
|
||||
// single secret storage operation, as it will clear the cached keys once the
|
||||
// operation ends.
|
||||
let secretStorageKeys = {};
|
||||
let cachingAllowed = false;
|
||||
|
||||
async function getSecretStorageKey({ keys: keyInfos }) {
|
||||
const keyInfoEntries = Object.entries(keyInfos);
|
||||
if (keyInfoEntries.length > 1) {
|
||||
throw new Error("Multiple storage key requests not implemented");
|
||||
}
|
||||
const [name, info] = keyInfoEntries[0];
|
||||
|
||||
// Check the in-memory cache
|
||||
if (cachingAllowed && secretStorageKeys[name]) {
|
||||
return [name, secretStorageKeys[name]];
|
||||
}
|
||||
|
||||
const inputToKey = async ({ passphrase, recoveryKey }) => {
|
||||
if (passphrase) {
|
||||
return deriveKey(
|
||||
passphrase,
|
||||
info.passphrase.salt,
|
||||
info.passphrase.iterations,
|
||||
);
|
||||
} else {
|
||||
return decodeRecoveryKey(recoveryKey);
|
||||
}
|
||||
};
|
||||
const AccessSecretStorageDialog =
|
||||
sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog");
|
||||
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
|
||||
AccessSecretStorageDialog,
|
||||
{
|
||||
keyInfo: info,
|
||||
checkPrivateKey: async (input) => {
|
||||
const key = await inputToKey(input);
|
||||
return MatrixClientPeg.get().checkSecretStoragePrivateKey(key, info.pubkey);
|
||||
},
|
||||
},
|
||||
);
|
||||
const [input] = await finished;
|
||||
if (!input) {
|
||||
throw new Error("Secret storage access canceled");
|
||||
}
|
||||
const key = await inputToKey(input);
|
||||
|
||||
// Save to cache to avoid future prompts in the current session
|
||||
if (cachingAllowed) {
|
||||
secretStorageKeys[name] = key;
|
||||
}
|
||||
|
||||
return [name, key];
|
||||
}
|
||||
|
||||
export const crossSigningCallbacks = {
|
||||
getSecretStorageKey,
|
||||
};
|
||||
|
||||
/**
|
||||
* This helper should be used whenever you need to access secret storage. It
|
||||
* ensures that secret storage (and also cross-signing since they each depend on
|
||||
* each other in a cycle of sorts) have been bootstrapped before running the
|
||||
* provided function.
|
||||
*
|
||||
* Bootstrapping secret storage may take one of these paths:
|
||||
* 1. Create secret storage from a passphrase and store cross-signing keys
|
||||
* in secret storage.
|
||||
* 2. Access existing secret storage by requesting passphrase and accessing
|
||||
* cross-signing keys as needed.
|
||||
* 3. All keys are loaded and there's nothing to do.
|
||||
*
|
||||
* Additionally, the secret storage keys are cached during the scope of this function
|
||||
* to ensure the user is prompted only once for their secret storage
|
||||
* passphrase. The cache is then cleared once the provided function completes.
|
||||
*
|
||||
* @param {Function} [func] An operation to perform once secret storage has been
|
||||
* bootstrapped. Optional.
|
||||
*/
|
||||
export async function accessSecretStorage(func = async () => { }) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cachingAllowed = true;
|
||||
|
||||
try {
|
||||
if (!cli.hasSecretStorageKey()) {
|
||||
// This dialog calls bootstrap itself after guiding the user through
|
||||
// passphrase creation.
|
||||
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
|
||||
import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"),
|
||||
null, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Secret storage creation canceled");
|
||||
}
|
||||
} else {
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
await cli.bootstrapSecretStorage({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Cross-signing keys dialog', '', InteractiveAuthDialog,
|
||||
{
|
||||
title: _t("Send cross-signing keys to homeserver"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
makeRequest,
|
||||
},
|
||||
);
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// `return await` needed here to ensure `finally` block runs after the
|
||||
// inner operation completes.
|
||||
return await func();
|
||||
} finally {
|
||||
// Clear secret storage key cache now that work is complete
|
||||
cachingAllowed = false;
|
||||
secretStorageKeys = {};
|
||||
}
|
||||
}
|
|
@ -32,9 +32,9 @@ import classNames from 'classnames';
|
|||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import url from 'url';
|
||||
|
||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
||||
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -58,8 +58,6 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
|||
|
||||
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
||||
|
||||
const VARIATION_SELECTOR = String.fromCharCode(0xFE0F);
|
||||
|
||||
/*
|
||||
* Return true if the given string contains emoji
|
||||
* Uses a much, much simpler regex than emojibase's so will give false
|
||||
|
@ -71,21 +69,6 @@ function mightContainEmoji(str) {
|
|||
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find emoji data in emojibase by character.
|
||||
*
|
||||
* @param {String} char The emoji character
|
||||
* @return {Object} The emoji data
|
||||
*/
|
||||
export function findEmojiData(char) {
|
||||
// Check against both the char and the char with an empty variation selector
|
||||
// appended because that's how emojibase stores its base emojis which have
|
||||
// variations.
|
||||
// See also https://github.com/vector-im/riot-web/issues/9785.
|
||||
const emptyVariation = char + VARIATION_SELECTOR;
|
||||
return EMOJIBASE.find(e => e.unicode === char || e.unicode === emptyVariation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shortcode for an emoji character.
|
||||
*
|
||||
|
@ -93,7 +76,7 @@ export function findEmojiData(char) {
|
|||
* @return {String} The shortcode (such as :thumbup:)
|
||||
*/
|
||||
export function unicodeToShortcode(char) {
|
||||
const data = findEmojiData(char);
|
||||
const data = getEmojiFromUnicode(char);
|
||||
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
|
||||
}
|
||||
|
||||
|
@ -105,7 +88,7 @@ export function unicodeToShortcode(char) {
|
|||
*/
|
||||
export function shortcodeToUnicode(shortcode) {
|
||||
shortcode = shortcode.slice(1, shortcode.length - 1);
|
||||
const data = EMOJIBASE.find(e => e.shortcodes && e.shortcodes.includes(shortcode));
|
||||
const data = SHORTCODE_TO_EMOJI.get(shortcode);
|
||||
return data ? data.unicode : null;
|
||||
}
|
||||
|
||||
|
|
|
@ -111,6 +111,12 @@ export default class KeyRequestHandler {
|
|||
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();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 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,52 +16,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* a selection of key codes, as used in KeyboardEvent.keyCode */
|
||||
export const KeyCode = {
|
||||
BACKSPACE: 8,
|
||||
TAB: 9,
|
||||
ENTER: 13,
|
||||
SHIFT: 16,
|
||||
ESCAPE: 27,
|
||||
SPACE: 32,
|
||||
PAGE_UP: 33,
|
||||
PAGE_DOWN: 34,
|
||||
END: 35,
|
||||
HOME: 36,
|
||||
LEFT: 37,
|
||||
UP: 38,
|
||||
RIGHT: 39,
|
||||
DOWN: 40,
|
||||
DELETE: 46,
|
||||
KEY_A: 65,
|
||||
KEY_B: 66,
|
||||
KEY_C: 67,
|
||||
KEY_D: 68,
|
||||
KEY_E: 69,
|
||||
KEY_F: 70,
|
||||
KEY_G: 71,
|
||||
KEY_H: 72,
|
||||
KEY_I: 73,
|
||||
KEY_J: 74,
|
||||
KEY_K: 75,
|
||||
KEY_L: 76,
|
||||
KEY_M: 77,
|
||||
KEY_N: 78,
|
||||
KEY_O: 79,
|
||||
KEY_P: 80,
|
||||
KEY_Q: 81,
|
||||
KEY_R: 82,
|
||||
KEY_S: 83,
|
||||
KEY_T: 84,
|
||||
KEY_U: 85,
|
||||
KEY_V: 86,
|
||||
KEY_W: 87,
|
||||
KEY_X: 88,
|
||||
KEY_Y: 89,
|
||||
KEY_Z: 90,
|
||||
KEY_BACKTICK: 223, // DO NOT USE THIS: browsers disagree on backtick 192 vs 223
|
||||
};
|
||||
|
||||
export const Key = {
|
||||
HOME: "Home",
|
||||
END: "End",
|
||||
|
@ -80,13 +35,35 @@ export const Key = {
|
|||
SHIFT: "Shift",
|
||||
CONTEXT_MENU: "ContextMenu",
|
||||
|
||||
COMMA: ",",
|
||||
LESS_THAN: "<",
|
||||
GREATER_THAN: ">",
|
||||
BACKTICK: "`",
|
||||
SPACE: " ",
|
||||
A: "a",
|
||||
B: "b",
|
||||
C: "c",
|
||||
D: "d",
|
||||
E: "e",
|
||||
F: "f",
|
||||
G: "g",
|
||||
H: "h",
|
||||
I: "i",
|
||||
J: "j",
|
||||
K: "k",
|
||||
L: "l",
|
||||
M: "m",
|
||||
N: "n",
|
||||
O: "o",
|
||||
P: "p",
|
||||
Q: "q",
|
||||
R: "r",
|
||||
S: "s",
|
||||
T: "t",
|
||||
U: "u",
|
||||
V: "v",
|
||||
W: "w",
|
||||
X: "x",
|
||||
Y: "y",
|
||||
Z: "z",
|
||||
};
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 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.
|
||||
|
@ -30,6 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
|
|||
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
|
||||
import * as StorageManager from './utils/StorageManager';
|
||||
import IdentityAuthClient from './IdentityAuthClient';
|
||||
import { crossSigningCallbacks } from './CrossSigningManager';
|
||||
|
||||
interface MatrixClientCreds {
|
||||
homeserverUrl: string,
|
||||
|
@ -220,14 +222,9 @@ class MatrixClientPeg {
|
|||
identityServer: new IdentityAuthClient(),
|
||||
};
|
||||
|
||||
opts.cryptoCallbacks = {};
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
// TODO: Cross-signing keys are temporarily in memory only. A
|
||||
// separate task in the cross-signing project will build from here.
|
||||
const keys = [];
|
||||
opts.cryptoCallbacks = {
|
||||
getCrossSigningKey: k => keys[k],
|
||||
saveCrossSigningKeys: newKeys => Object.assign(keys, newKeys),
|
||||
};
|
||||
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks);
|
||||
}
|
||||
|
||||
this.matrixClient = createMatrixClient(opts);
|
||||
|
|
|
@ -25,6 +25,7 @@ import sdk from './';
|
|||
import dis from './dispatcher';
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import { _t } from './languageHandler';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
|
||||
/**
|
||||
* Invites multiple addresses to a room
|
||||
|
@ -41,6 +42,18 @@ function inviteMultipleToRoom(roomId, addrs) {
|
|||
}
|
||||
|
||||
export function showStartChatInviteDialog() {
|
||||
if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) {
|
||||
const DMInviteDialog = sdk.getComponent("dialogs.DMInviteDialog");
|
||||
Modal.createTrackedDialog('Start DM', '', DMInviteDialog, {
|
||||
onFinished: (inviteIds) => {
|
||||
// TODO: Replace _onStartDmFinished with less hacks
|
||||
if (inviteIds.length > 0) _onStartDmFinished(true, inviteIds.map(i => ({address: i})));
|
||||
// else ignore and just do nothing
|
||||
},
|
||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||
return;
|
||||
}
|
||||
|
||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||
|
||||
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
|
||||
|
@ -99,7 +112,7 @@ export function isValid3pidInvite(event) {
|
|||
return true;
|
||||
}
|
||||
|
||||
// TODO: Immutable DMs replaces this
|
||||
// TODO: Canonical DMs replaces this
|
||||
function _onStartDmFinished(shouldInvite, addrs) {
|
||||
if (!shouldInvite) return;
|
||||
|
||||
|
|
|
@ -780,54 +780,52 @@ export const CommandMap = {
|
|||
const deviceId = matches[2];
|
||||
const fingerprint = matches[3];
|
||||
|
||||
return success(
|
||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
||||
// in future
|
||||
Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => {
|
||||
if (!device) {
|
||||
throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`);
|
||||
}
|
||||
return success((async () => {
|
||||
const device = await cli.getStoredDevice(userId, deviceId);
|
||||
if (!device) {
|
||||
throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`);
|
||||
}
|
||||
const deviceTrust = await cli.checkDeviceTrust(userId, deviceId);
|
||||
|
||||
if (device.isVerified()) {
|
||||
if (device.getFingerprint() === fingerprint) {
|
||||
throw new Error(_t('Device already verified!'));
|
||||
} else {
|
||||
throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!'));
|
||||
}
|
||||
if (deviceTrust.isVerified()) {
|
||||
if (device.getFingerprint() === fingerprint) {
|
||||
throw new Error(_t('Device already verified!'));
|
||||
} else {
|
||||
throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!'));
|
||||
}
|
||||
}
|
||||
|
||||
if (device.getFingerprint() !== fingerprint) {
|
||||
const fprint = device.getFingerprint();
|
||||
throw new Error(
|
||||
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
||||
' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
|
||||
'"%(fingerprint)s". This could mean your communications are being intercepted!',
|
||||
{
|
||||
fprint,
|
||||
userId,
|
||||
deviceId,
|
||||
fingerprint,
|
||||
}));
|
||||
}
|
||||
if (device.getFingerprint() !== fingerprint) {
|
||||
const fprint = device.getFingerprint();
|
||||
throw new Error(
|
||||
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
||||
' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
|
||||
'"%(fingerprint)s". This could mean your communications are being intercepted!',
|
||||
{
|
||||
fprint,
|
||||
userId,
|
||||
deviceId,
|
||||
fingerprint,
|
||||
}));
|
||||
}
|
||||
|
||||
return cli.setDeviceVerified(userId, deviceId, true);
|
||||
}).then(() => {
|
||||
// Tell the user we verified everything
|
||||
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
|
||||
Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, {
|
||||
title: _t('Verified key'),
|
||||
description: <div>
|
||||
<p>
|
||||
{
|
||||
_t('The signing key you provided matches the signing key you received ' +
|
||||
'from %(userId)s\'s device %(deviceId)s. Device marked as verified.',
|
||||
{userId, deviceId})
|
||||
}
|
||||
</p>
|
||||
</div>,
|
||||
});
|
||||
}),
|
||||
);
|
||||
await cli.setDeviceVerified(userId, deviceId, true);
|
||||
|
||||
// Tell the user we verified everything
|
||||
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
|
||||
Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, {
|
||||
title: _t('Verified key'),
|
||||
description: <div>
|
||||
<p>
|
||||
{
|
||||
_t('The signing key you provided matches the signing key you received ' +
|
||||
'from %(userId)s\'s device %(deviceId)s. Device marked as verified.',
|
||||
{userId, deviceId})
|
||||
}
|
||||
</p>
|
||||
</div>,
|
||||
});
|
||||
})());
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
//@flow
|
||||
/*
|
||||
Copyright 2017 Aviral Dasgupta
|
||||
|
||||
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 {Value} from 'slate';
|
||||
|
||||
import _clamp from 'lodash/clamp';
|
||||
|
||||
type MessageFormat = 'rich' | 'markdown';
|
||||
|
||||
class HistoryItem {
|
||||
// We store history items in their native format to ensure history is accurate
|
||||
// and then convert them if our RTE has subsequently changed format.
|
||||
value: Value;
|
||||
format: MessageFormat = 'rich';
|
||||
|
||||
constructor(value: ?Value, format: ?MessageFormat) {
|
||||
this.value = value;
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Object): HistoryItem {
|
||||
return new HistoryItem(
|
||||
Value.fromJSON(obj.value),
|
||||
obj.format,
|
||||
);
|
||||
}
|
||||
|
||||
toJSON(): Object {
|
||||
return {
|
||||
value: this.value.toJSON(),
|
||||
format: this.format,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default class SlateComposerHistoryManager {
|
||||
history: Array<HistoryItem> = [];
|
||||
prefix: string;
|
||||
lastIndex: number = 0; // used for indexing the storage
|
||||
currentIndex: number = 0; // used for indexing the loaded validated history Array
|
||||
|
||||
constructor(roomId: string, prefix: string = 'mx_composer_history_') {
|
||||
this.prefix = prefix + roomId;
|
||||
|
||||
// TODO: Performance issues?
|
||||
let item;
|
||||
for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
|
||||
try {
|
||||
this.history.push(
|
||||
HistoryItem.fromJSON(JSON.parse(item)),
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("Throwing away unserialisable history", e);
|
||||
}
|
||||
}
|
||||
this.lastIndex = this.currentIndex;
|
||||
// reset currentIndex to account for any unserialisable history
|
||||
this.currentIndex = this.history.length;
|
||||
}
|
||||
|
||||
save(value: Value, format: MessageFormat) {
|
||||
const item = new HistoryItem(value, format);
|
||||
this.history.push(item);
|
||||
this.currentIndex = this.history.length;
|
||||
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON()));
|
||||
}
|
||||
|
||||
getItem(offset: number): ?HistoryItem {
|
||||
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1);
|
||||
return this.history[this.currentIndex];
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ import { _t } from './languageHandler';
|
|||
import * as Roles from './Roles';
|
||||
import {isValid3pidInvite} from "./RoomInvite";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
|
||||
|
||||
function textForMemberEvent(ev) {
|
||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||
|
@ -472,7 +473,7 @@ function textForPowerEvent(event) {
|
|||
}
|
||||
|
||||
function textForPinnedEvent(event) {
|
||||
const senderName = event.getSender();
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
return _t("%(senderName)s changed the pinned messages for the room.", {senderName});
|
||||
}
|
||||
|
||||
|
@ -506,6 +507,87 @@ function textForWidgetEvent(event) {
|
|||
}
|
||||
}
|
||||
|
||||
function textForMjolnirEvent(event) {
|
||||
const senderName = event.getSender();
|
||||
const {entity: prevEntity} = event.getPrevContent();
|
||||
const {entity, recommendation, reason} = event.getContent();
|
||||
|
||||
// Rule removed
|
||||
if (!entity) {
|
||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s removed the rule banning users matching %(glob)s",
|
||||
{senderName, glob: prevEntity});
|
||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
|
||||
{senderName, glob: prevEntity});
|
||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s removed the rule banning servers matching %(glob)s",
|
||||
{senderName, glob: prevEntity});
|
||||
}
|
||||
|
||||
// Unknown type. We'll say something, but we shouldn't end up here.
|
||||
return _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
|
||||
}
|
||||
|
||||
// Invalid rule
|
||||
if (!recommendation || !reason) return _t(`%(senderName)s updated an invalid ban rule`, {senderName});
|
||||
|
||||
// Rule updated
|
||||
if (entity === prevEntity) {
|
||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
}
|
||||
|
||||
// Unknown type. We'll say something but we shouldn't end up here.
|
||||
return _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
}
|
||||
|
||||
// New rule
|
||||
if (!prevEntity) {
|
||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
}
|
||||
|
||||
// Unknown type. We'll say something but we shouldn't end up here.
|
||||
return _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
|
||||
{senderName, glob: entity, reason});
|
||||
}
|
||||
|
||||
// else the entity !== prevEntity - count as a removal & add
|
||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
|
||||
"%(newGlob)s for %(reason)s",
|
||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
|
||||
"%(newGlob)s for %(reason)s",
|
||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||
return _t("%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
|
||||
"%(newGlob)s for %(reason)s",
|
||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
||||
}
|
||||
|
||||
// Unknown type. We'll say something but we shouldn't end up here.
|
||||
return _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
|
||||
"for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
'm.room.message': textForMessageEvent,
|
||||
'm.call.invite': textForCallInviteEvent,
|
||||
|
@ -533,6 +615,11 @@ const stateHandlers = {
|
|||
'im.vector.modular.widgets': textForWidgetEvent,
|
||||
};
|
||||
|
||||
// Add all the Mjolnir stuff to the renderer
|
||||
for (const evType of ALL_RULE_TYPES) {
|
||||
stateHandlers[evType] = textForMjolnirEvent;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
textForEvent: function(ev) {
|
||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||
|
|
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
const React = require("react");
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -21,6 +23,9 @@ import { _t } from '../../../languageHandler';
|
|||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
|
||||
// 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.
|
||||
module.exports = createReactClass({
|
||||
displayName: 'EncryptedEventDialog',
|
||||
|
||||
|
@ -83,7 +88,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
onKeyDown: function(e) {
|
||||
if (e.keyCode === 27) { // escape
|
||||
if (e.key === Key.ESCAPE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import FileSaver from 'file-saver';
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -44,6 +44,9 @@ export default createReactClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
|
||||
this._passphrase1 = createRef();
|
||||
this._passphrase2 = createRef();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -53,8 +56,8 @@ export default createReactClass({
|
|||
_onPassphraseFormSubmit: function(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
const passphrase = this.refs.passphrase1.value;
|
||||
if (passphrase !== this.refs.passphrase2.value) {
|
||||
const passphrase = this._passphrase1.current.value;
|
||||
if (passphrase !== this._passphrase2.current.value) {
|
||||
this.setState({errStr: _t('Passphrases must match')});
|
||||
return false;
|
||||
}
|
||||
|
@ -148,7 +151,7 @@ export default createReactClass({
|
|||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input ref='passphrase1' id='passphrase1'
|
||||
<input ref={this._passphrase1} id='passphrase1'
|
||||
autoFocus={true} size='64' type='password'
|
||||
disabled={disableForm}
|
||||
/>
|
||||
|
@ -161,7 +164,7 @@ export default createReactClass({
|
|||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input ref='passphrase2' id='passphrase2'
|
||||
<input ref={this._passphrase2} id='passphrase2'
|
||||
size='64' type='password'
|
||||
disabled={disableForm}
|
||||
/>
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
|
@ -56,6 +56,9 @@ export default createReactClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
|
||||
this._file = createRef();
|
||||
this._passphrase = createRef();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -63,15 +66,15 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
_onFormChange: function(ev) {
|
||||
const files = this.refs.file.files || [];
|
||||
const files = this._file.current.files || [];
|
||||
this.setState({
|
||||
enableSubmit: (this.refs.passphrase.value !== "" && files.length > 0),
|
||||
enableSubmit: (this._passphrase.current.value !== "" && files.length > 0),
|
||||
});
|
||||
},
|
||||
|
||||
_onFormSubmit: function(ev) {
|
||||
ev.preventDefault();
|
||||
this._startImport(this.refs.file.files[0], this.refs.passphrase.value);
|
||||
this._startImport(this._file.current.files[0], this._passphrase.current.value);
|
||||
return false;
|
||||
},
|
||||
|
||||
|
@ -146,7 +149,10 @@ export default createReactClass({
|
|||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input ref='file' id='importFile' type='file'
|
||||
<input
|
||||
ref={this._file}
|
||||
id='importFile'
|
||||
type='file'
|
||||
autoFocus={true}
|
||||
onChange={this._onFormChange}
|
||||
disabled={disableForm} />
|
||||
|
@ -159,8 +165,11 @@ export default createReactClass({
|
|||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input ref='passphrase' id='passphrase'
|
||||
size='64' type='password'
|
||||
<input
|
||||
ref={this._passphrase}
|
||||
id='passphrase'
|
||||
size='64'
|
||||
type='password'
|
||||
onChange={this._onFormChange}
|
||||
disabled={disableForm} />
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,14 +16,15 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import FileSaver from 'file-saver';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../../index';
|
||||
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
||||
import { scorePassword } from '../../../../utils/PasswordScorer';
|
||||
|
||||
import FileSaver from 'file-saver';
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||
import SettingsStore from '../../../../../lib/settings/SettingsStore';
|
||||
|
||||
const PHASE_PASSPHRASE = 0;
|
||||
const PHASE_PASSPHRASE_CONFIRM = 1;
|
||||
|
@ -45,13 +47,25 @@ function selectText(target) {
|
|||
selection.addRange(range);
|
||||
}
|
||||
|
||||
/**
|
||||
/*
|
||||
* Walks the user through the process of creating an e2e key backup
|
||||
* on the server.
|
||||
*/
|
||||
export default createReactClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
secureSecretStorage: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._recoveryKeyNode = null;
|
||||
this._keyBackupInfo = null;
|
||||
this._setZxcvbnResultTimeout = null;
|
||||
|
||||
this.state = {
|
||||
secureSecretStorage: props.secureSecretStorage,
|
||||
phase: PHASE_PASSPHRASE,
|
||||
passPhrase: '',
|
||||
passPhraseConfirm: '',
|
||||
|
@ -60,25 +74,38 @@ export default createReactClass({
|
|||
zxcvbnResult: null,
|
||||
setPassPhrase: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._recoveryKeyNode = null;
|
||||
this._keyBackupInfo = null;
|
||||
this._setZxcvbnResultTimeout = null;
|
||||
},
|
||||
if (this.state.secureSecretStorage === undefined) {
|
||||
this.state.secureSecretStorage =
|
||||
SettingsStore.isFeatureEnabled("feature_cross_signing");
|
||||
}
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// If we're using secret storage, skip ahead to the backing up step, as
|
||||
// `accessSecretStorage` will handle passphrases as needed.
|
||||
if (this.state.secureSecretStorage) {
|
||||
this.state.phase = PHASE_BACKINGUP;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// If we're using secret storage, skip ahead to the backing up step, as
|
||||
// `accessSecretStorage` will handle passphrases as needed.
|
||||
if (this.state.secureSecretStorage) {
|
||||
this._createBackup();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._setZxcvbnResultTimeout !== null) {
|
||||
clearTimeout(this._setZxcvbnResultTimeout);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_collectRecoveryKeyNode: function(n) {
|
||||
_collectRecoveryKeyNode = (n) => {
|
||||
this._recoveryKeyNode = n;
|
||||
},
|
||||
}
|
||||
|
||||
_onCopyClick: function() {
|
||||
_onCopyClick = () => {
|
||||
selectText(this._recoveryKeyNode);
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
|
@ -87,9 +114,9 @@ export default createReactClass({
|
|||
phase: PHASE_KEEPITSAFE,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_onDownloadClick: function() {
|
||||
_onDownloadClick = () => {
|
||||
const blob = new Blob([this._keyBackupInfo.recovery_key], {
|
||||
type: 'text/plain;charset=us-ascii',
|
||||
});
|
||||
|
@ -99,24 +126,35 @@ export default createReactClass({
|
|||
downloaded: true,
|
||||
phase: PHASE_KEEPITSAFE,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_createBackup: async function() {
|
||||
_createBackup = async () => {
|
||||
const { secureSecretStorage } = this.state;
|
||||
this.setState({
|
||||
phase: PHASE_BACKINGUP,
|
||||
error: null,
|
||||
});
|
||||
let info;
|
||||
try {
|
||||
info = await MatrixClientPeg.get().createKeyBackupVersion(
|
||||
this._keyBackupInfo,
|
||||
);
|
||||
if (secureSecretStorage) {
|
||||
await accessSecretStorage(async () => {
|
||||
info = await MatrixClientPeg.get().prepareKeyBackupVersion(
|
||||
null /* random key */,
|
||||
{ secureSecretStorage: true },
|
||||
);
|
||||
info = await MatrixClientPeg.get().createKeyBackupVersion(info);
|
||||
});
|
||||
} else {
|
||||
info = await MatrixClientPeg.get().createKeyBackupVersion(
|
||||
this._keyBackupInfo,
|
||||
);
|
||||
}
|
||||
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
|
||||
this.setState({
|
||||
phase: PHASE_DONE,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error creating key backup", e);
|
||||
console.error("Error creating key backup", e);
|
||||
// TODO: If creating a version succeeds, but backup fails, should we
|
||||
// delete the version, disable backup, or do nothing? If we just
|
||||
// disable without deleting, we'll enable on next app reload since
|
||||
|
@ -128,38 +166,38 @@ export default createReactClass({
|
|||
error: e,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_onCancel: function() {
|
||||
_onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
}
|
||||
|
||||
_onDone: function() {
|
||||
_onDone = () => {
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
}
|
||||
|
||||
_onOptOutClick: function() {
|
||||
_onOptOutClick = () => {
|
||||
this.setState({phase: PHASE_OPTOUT_CONFIRM});
|
||||
},
|
||||
}
|
||||
|
||||
_onSetUpClick: function() {
|
||||
_onSetUpClick = () => {
|
||||
this.setState({phase: PHASE_PASSPHRASE});
|
||||
},
|
||||
}
|
||||
|
||||
_onSkipPassPhraseClick: async function() {
|
||||
_onSkipPassPhraseClick = async () => {
|
||||
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
phase: PHASE_SHOWKEY,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_onPassPhraseNextClick: function() {
|
||||
_onPassPhraseNextClick = () => {
|
||||
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
||||
},
|
||||
}
|
||||
|
||||
_onPassPhraseKeyPress: async function(e) {
|
||||
_onPassPhraseKeyPress = async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
// If we're waiting for the timeout before updating the result at this point,
|
||||
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
|
||||
|
@ -177,9 +215,9 @@ export default createReactClass({
|
|||
this._onPassPhraseNextClick();
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_onPassPhraseConfirmNextClick: async function() {
|
||||
_onPassPhraseConfirmNextClick = async () => {
|
||||
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
|
||||
this.setState({
|
||||
setPassPhrase: true,
|
||||
|
@ -187,30 +225,30 @@ export default createReactClass({
|
|||
downloaded: false,
|
||||
phase: PHASE_SHOWKEY,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_onPassPhraseConfirmKeyPress: function(e) {
|
||||
_onPassPhraseConfirmKeyPress = (e) => {
|
||||
if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) {
|
||||
this._onPassPhraseConfirmNextClick();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_onSetAgainClick: function() {
|
||||
_onSetAgainClick = () => {
|
||||
this.setState({
|
||||
passPhrase: '',
|
||||
passPhraseConfirm: '',
|
||||
phase: PHASE_PASSPHRASE,
|
||||
zxcvbnResult: null,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_onKeepItSafeBackClick: function() {
|
||||
_onKeepItSafeBackClick = () => {
|
||||
this.setState({
|
||||
phase: PHASE_SHOWKEY,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_onPassPhraseChange: function(e) {
|
||||
_onPassPhraseChange = (e) => {
|
||||
this.setState({
|
||||
passPhrase: e.target.value,
|
||||
});
|
||||
|
@ -227,19 +265,19 @@ export default createReactClass({
|
|||
zxcvbnResult: scorePassword(this.state.passPhrase),
|
||||
});
|
||||
}, PASSPHRASE_FEEDBACK_DELAY);
|
||||
},
|
||||
}
|
||||
|
||||
_onPassPhraseConfirmChange: function(e) {
|
||||
_onPassPhraseConfirmChange = (e) => {
|
||||
this.setState({
|
||||
passPhraseConfirm: e.target.value,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_passPhraseIsValid: function() {
|
||||
_passPhraseIsValid() {
|
||||
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
|
||||
},
|
||||
}
|
||||
|
||||
_renderPhasePassPhrase: function() {
|
||||
_renderPhasePassPhrase() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
let strengthMeter;
|
||||
|
@ -266,7 +304,7 @@ export default createReactClass({
|
|||
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"<b>Warning</b>: you should only set up key backup from a trusted computer.", {},
|
||||
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
|
||||
{ b: sub => <b>{sub}</b> },
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
|
@ -305,9 +343,9 @@ export default createReactClass({
|
|||
</button></p>
|
||||
</details>
|
||||
</div>;
|
||||
},
|
||||
}
|
||||
|
||||
_renderPhasePassPhraseConfirm: function() {
|
||||
_renderPhasePassPhraseConfirm() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let matchText;
|
||||
|
@ -361,9 +399,9 @@ export default createReactClass({
|
|||
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
}
|
||||
|
||||
_renderPhaseShowKey: function() {
|
||||
_renderPhaseShowKey() {
|
||||
let bodyText;
|
||||
if (this.state.setPassPhrase) {
|
||||
bodyText = _t(
|
||||
|
@ -380,7 +418,7 @@ export default createReactClass({
|
|||
"access to your encrypted messages if you forget your passphrase.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"Keep your recovery key somewhere very secure, like a password manager (or a safe)",
|
||||
"Keep your recovery key somewhere very secure, like a password manager (or a safe).",
|
||||
)}</p>
|
||||
<p>{bodyText}</p>
|
||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||
|
@ -402,18 +440,18 @@ export default createReactClass({
|
|||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
}
|
||||
|
||||
_renderPhaseKeepItSafe: function() {
|
||||
_renderPhaseKeepItSafe() {
|
||||
let introText;
|
||||
if (this.state.copied) {
|
||||
introText = _t(
|
||||
"Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:",
|
||||
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
|
||||
{}, {b: s => <b>{s}</b>},
|
||||
);
|
||||
} else if (this.state.downloaded) {
|
||||
introText = _t(
|
||||
"Your Recovery Key is in your <b>Downloads</b> folder.",
|
||||
"Your recovery key is in your <b>Downloads</b> folder.",
|
||||
{}, {b: s => <b>{s}</b>},
|
||||
);
|
||||
}
|
||||
|
@ -431,16 +469,16 @@ export default createReactClass({
|
|||
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
|
||||
</DialogButtons>
|
||||
</div>;
|
||||
},
|
||||
}
|
||||
|
||||
_renderBusyPhase: function(text) {
|
||||
_renderBusyPhase(text) {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
return <div>
|
||||
<Spinner />
|
||||
</div>;
|
||||
},
|
||||
}
|
||||
|
||||
_renderPhaseDone: function() {
|
||||
_renderPhaseDone() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <div>
|
||||
<p>{_t(
|
||||
|
@ -451,9 +489,9 @@ export default createReactClass({
|
|||
hasCancel={false}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
}
|
||||
|
||||
_renderPhaseOptOutConfirm: function() {
|
||||
_renderPhaseOptOutConfirm() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <div>
|
||||
{_t(
|
||||
|
@ -467,9 +505,9 @@ export default createReactClass({
|
|||
<button onClick={this._onCancel}>I understand, continue without</button>
|
||||
</DialogButtons>
|
||||
</div>;
|
||||
},
|
||||
}
|
||||
|
||||
_titleForPhase: function(phase) {
|
||||
_titleForPhase(phase) {
|
||||
switch (phase) {
|
||||
case PHASE_PASSPHRASE:
|
||||
return _t('Secure your backup with a passphrase');
|
||||
|
@ -488,9 +526,9 @@ export default createReactClass({
|
|||
default:
|
||||
return _t("Create Key Backup");
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
let content;
|
||||
|
@ -543,5 +581,5 @@ export default createReactClass({
|
|||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
Copyright 2018, 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.
|
||||
|
@ -40,9 +41,11 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
|
|||
|
||||
onSetupClick = async () => {
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
|
||||
onFinished: this.props.onFinished,
|
||||
});
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, {
|
||||
onFinished: this.props.onFinished,
|
||||
}, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -35,6 +36,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
|
|||
this.props.onFinished();
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("./CreateKeyBackupDialog"),
|
||||
null, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,611 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../../index';
|
||||
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
||||
import { scorePassword } from '../../../../utils/PasswordScorer';
|
||||
import FileSaver from 'file-saver';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import Modal from '../../../../Modal';
|
||||
|
||||
const PHASE_LOADING = 0;
|
||||
const PHASE_MIGRATE = 1;
|
||||
const PHASE_PASSPHRASE = 2;
|
||||
const PHASE_PASSPHRASE_CONFIRM = 3;
|
||||
const PHASE_SHOWKEY = 4;
|
||||
const PHASE_KEEPITSAFE = 5;
|
||||
const PHASE_STORING = 6;
|
||||
const PHASE_DONE = 7;
|
||||
const PHASE_OPTOUT_CONFIRM = 8;
|
||||
|
||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
||||
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
|
||||
|
||||
// XXX: copied from ShareDialog: factor out into utils
|
||||
function selectText(target) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
/*
|
||||
* Walks the user through the process of creating a passphrase to guard Secure
|
||||
* Secret Storage in account data.
|
||||
*/
|
||||
export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._keyInfo = null;
|
||||
this._encodedRecoveryKey = null;
|
||||
this._recoveryKeyNode = null;
|
||||
this._setZxcvbnResultTimeout = null;
|
||||
|
||||
this.state = {
|
||||
phase: PHASE_LOADING,
|
||||
passPhrase: '',
|
||||
passPhraseConfirm: '',
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
zxcvbnResult: null,
|
||||
setPassPhrase: false,
|
||||
};
|
||||
|
||||
this._fetchBackupInfo();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._setZxcvbnResultTimeout !== null) {
|
||||
clearTimeout(this._setZxcvbnResultTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
async _fetchBackupInfo() {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
|
||||
this.setState({
|
||||
phase: backupInfo ? PHASE_MIGRATE: PHASE_PASSPHRASE,
|
||||
backupInfo,
|
||||
});
|
||||
}
|
||||
|
||||
_collectRecoveryKeyNode = (n) => {
|
||||
this._recoveryKeyNode = n;
|
||||
}
|
||||
|
||||
_onMigrateNextClick = () => {
|
||||
this._bootstrapSecretStorage();
|
||||
}
|
||||
|
||||
_onCopyClick = () => {
|
||||
selectText(this._recoveryKeyNode);
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
this.setState({
|
||||
copied: true,
|
||||
phase: PHASE_KEEPITSAFE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onDownloadClick = () => {
|
||||
const blob = new Blob([this._encodedRecoveryKey], {
|
||||
type: 'text/plain;charset=us-ascii',
|
||||
});
|
||||
FileSaver.saveAs(blob, 'recovery-key.txt');
|
||||
|
||||
this.setState({
|
||||
downloaded: true,
|
||||
phase: PHASE_KEEPITSAFE,
|
||||
});
|
||||
}
|
||||
|
||||
_bootstrapSecretStorage = async () => {
|
||||
this.setState({
|
||||
phase: PHASE_STORING,
|
||||
error: null,
|
||||
});
|
||||
const cli = MatrixClientPeg.get();
|
||||
try {
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
await cli.bootstrapSecretStorage({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Cross-signing keys dialog', '', InteractiveAuthDialog,
|
||||
{
|
||||
title: _t("Send cross-signing keys to homeserver"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
makeRequest,
|
||||
},
|
||||
);
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
},
|
||||
createSecretStorageKey: async () => this._keyInfo,
|
||||
keyBackupInfo: this.state.backupInfo,
|
||||
});
|
||||
this.setState({
|
||||
phase: PHASE_DONE,
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState({ error: e });
|
||||
console.error("Error bootstrapping secret storage", e);
|
||||
}
|
||||
}
|
||||
|
||||
_onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onDone = () => {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
_onOptOutClick = () => {
|
||||
this.setState({phase: PHASE_OPTOUT_CONFIRM});
|
||||
}
|
||||
|
||||
_onSetUpClick = () => {
|
||||
this.setState({phase: PHASE_PASSPHRASE});
|
||||
}
|
||||
|
||||
_onSkipPassPhraseClick = async () => {
|
||||
const [keyInfo, encodedRecoveryKey] =
|
||||
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase();
|
||||
this._keyInfo = keyInfo;
|
||||
this._encodedRecoveryKey = encodedRecoveryKey;
|
||||
this.setState({
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
phase: PHASE_SHOWKEY,
|
||||
});
|
||||
}
|
||||
|
||||
_onPassPhraseNextClick = () => {
|
||||
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
||||
}
|
||||
|
||||
_onPassPhraseKeyPress = async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
// If we're waiting for the timeout before updating the result at this point,
|
||||
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
|
||||
// even if the user entered a valid passphrase
|
||||
if (this._setZxcvbnResultTimeout !== null) {
|
||||
clearTimeout(this._setZxcvbnResultTimeout);
|
||||
this._setZxcvbnResultTimeout = null;
|
||||
await new Promise((resolve) => {
|
||||
this.setState({
|
||||
zxcvbnResult: scorePassword(this.state.passPhrase),
|
||||
}, resolve);
|
||||
});
|
||||
}
|
||||
if (this._passPhraseIsValid()) {
|
||||
this._onPassPhraseNextClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onPassPhraseConfirmNextClick = async () => {
|
||||
const [keyInfo, encodedRecoveryKey] =
|
||||
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
|
||||
this._keyInfo = keyInfo;
|
||||
this._encodedRecoveryKey = encodedRecoveryKey;
|
||||
this.setState({
|
||||
setPassPhrase: true,
|
||||
copied: false,
|
||||
downloaded: false,
|
||||
phase: PHASE_SHOWKEY,
|
||||
});
|
||||
}
|
||||
|
||||
_onPassPhraseConfirmKeyPress = (e) => {
|
||||
if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) {
|
||||
this._onPassPhraseConfirmNextClick();
|
||||
}
|
||||
}
|
||||
|
||||
_onSetAgainClick = () => {
|
||||
this.setState({
|
||||
passPhrase: '',
|
||||
passPhraseConfirm: '',
|
||||
phase: PHASE_PASSPHRASE,
|
||||
zxcvbnResult: null,
|
||||
});
|
||||
}
|
||||
|
||||
_onKeepItSafeBackClick = () => {
|
||||
this.setState({
|
||||
phase: PHASE_SHOWKEY,
|
||||
});
|
||||
}
|
||||
|
||||
_onPassPhraseChange = (e) => {
|
||||
this.setState({
|
||||
passPhrase: e.target.value,
|
||||
});
|
||||
|
||||
if (this._setZxcvbnResultTimeout !== null) {
|
||||
clearTimeout(this._setZxcvbnResultTimeout);
|
||||
}
|
||||
this._setZxcvbnResultTimeout = setTimeout(() => {
|
||||
this._setZxcvbnResultTimeout = null;
|
||||
this.setState({
|
||||
// precompute this and keep it in state: zxcvbn is fast but
|
||||
// we use it in a couple of different places so no point recomputing
|
||||
// it unnecessarily.
|
||||
zxcvbnResult: scorePassword(this.state.passPhrase),
|
||||
});
|
||||
}, PASSPHRASE_FEEDBACK_DELAY);
|
||||
}
|
||||
|
||||
_onPassPhraseConfirmChange = (e) => {
|
||||
this.setState({
|
||||
passPhraseConfirm: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
_passPhraseIsValid() {
|
||||
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
|
||||
}
|
||||
|
||||
_renderPhaseMigrate() {
|
||||
// TODO: This is a temporary screen so people who have the labs flag turned on and
|
||||
// click the button are aware they're making a change to their account.
|
||||
// Once we're confident enough in this (and it's supported enough) we can do
|
||||
// it automatically.
|
||||
// https://github.com/vector-im/riot-web/issues/11696
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"Secret Storage will be set up using your existing key backup details." +
|
||||
"Your secret storage passphrase and recovery key will be the same as " +
|
||||
" they were for your key backup",
|
||||
)}</p>
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
onPrimaryButtonClick={this._onMigrateNextClick}
|
||||
hasCancel={true}
|
||||
onCancel={this._onCancel}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhasePassPhrase() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
let strengthMeter;
|
||||
let helpText;
|
||||
if (this.state.zxcvbnResult) {
|
||||
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
|
||||
helpText = _t("Great! This passphrase looks strong enough.");
|
||||
} else {
|
||||
const suggestions = [];
|
||||
for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) {
|
||||
suggestions.push(<div key={i}>{this.state.zxcvbnResult.feedback.suggestions[i]}</div>);
|
||||
}
|
||||
const suggestionBlock = <div>{suggestions.length > 0 ? suggestions : _t("Keep going...")}</div>;
|
||||
|
||||
helpText = <div>
|
||||
{this.state.zxcvbnResult.feedback.warning}
|
||||
{suggestionBlock}
|
||||
</div>;
|
||||
}
|
||||
strengthMeter = <div>
|
||||
<progress max={PASSWORD_MIN_SCORE} value={this.state.zxcvbnResult.score} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"<b>Warning</b>: You should only set up secret storage from a trusted computer.", {},
|
||||
{ b: sub => <b>{sub}</b> },
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"We'll use secret storage to optionally store an encrypted copy of " +
|
||||
"your cross-signing identity for verifying other devices and message " +
|
||||
"keys on our server. Protect your access to encrypted messages with a " +
|
||||
"passphrase to keep it secure.",
|
||||
)}</p>
|
||||
<p>{_t("For maximum security, this should be different from your account password.")}</p>
|
||||
|
||||
<div className="mx_CreateSecretStorageDialog_primaryContainer">
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<input type="password"
|
||||
onChange={this._onPassPhraseChange}
|
||||
onKeyPress={this._onPassPhraseKeyPress}
|
||||
value={this.state.passPhrase}
|
||||
className="mx_CreateSecretStorageDialog_passPhraseInput"
|
||||
placeholder={_t("Enter a passphrase...")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseHelp">
|
||||
{strengthMeter}
|
||||
{helpText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
||||
hasCancel={false}
|
||||
disabled={!this._passPhraseIsValid()}
|
||||
/>
|
||||
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<p><button onClick={this._onSkipPassPhraseClick} >
|
||||
{_t("Set up with a recovery key")}
|
||||
</button></p>
|
||||
</details>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhasePassPhraseConfirm() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let matchText;
|
||||
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
||||
matchText = _t("That matches!");
|
||||
} 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
|
||||
// on this screen, this would make it easy for a malicious person to guess
|
||||
// your passphrase one letter at a time, but they could get this faster by
|
||||
// just opening the browser's developer tools and reading it.
|
||||
// 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.");
|
||||
}
|
||||
|
||||
let passPhraseMatch = null;
|
||||
if (matchText) {
|
||||
passPhraseMatch = <div className="mx_CreateSecretStorageDialog_passPhraseMatch">
|
||||
<div>{matchText}</div>
|
||||
<div>
|
||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||
{_t("Go back to set it again.")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"Please enter your passphrase a second time to confirm.",
|
||||
)}</p>
|
||||
<div className="mx_CreateSecretStorageDialog_primaryContainer">
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<div>
|
||||
<input type="password"
|
||||
onChange={this._onPassPhraseConfirmChange}
|
||||
onKeyPress={this._onPassPhraseConfirmKeyPress}
|
||||
value={this.state.passPhraseConfirm}
|
||||
className="mx_CreateSecretStorageDialog_passPhraseInput"
|
||||
placeholder={_t("Repeat your passphrase...")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
{passPhraseMatch}
|
||||
</div>
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
|
||||
hasCancel={false}
|
||||
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseShowKey() {
|
||||
let bodyText;
|
||||
if (this.state.setPassPhrase) {
|
||||
bodyText = _t(
|
||||
"As a safety net, you can use it to restore your access to encrypted " +
|
||||
"messages if you forget your passphrase.",
|
||||
);
|
||||
} else {
|
||||
bodyText = _t(
|
||||
"As a safety net, you can use it to restore your access to encrypted " +
|
||||
"messages.",
|
||||
);
|
||||
}
|
||||
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"Your recovery key is a safety net - you can use it to restore " +
|
||||
"access to your encrypted messages if you forget your passphrase.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"Keep your recovery key somewhere very secure, like a password manager (or a safe).",
|
||||
)}</p>
|
||||
<p>{bodyText}</p>
|
||||
<div className="mx_CreateSecretStorageDialog_primaryContainer">
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyHeader">
|
||||
{_t("Your Recovery Key")}
|
||||
</div>
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKey">
|
||||
<code ref={this._collectRecoveryKeyNode}>{this._encodedRecoveryKey}</code>
|
||||
</div>
|
||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
|
||||
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
|
||||
{_t("Copy to clipboard")}
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
|
||||
{_t("Download")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseKeepItSafe() {
|
||||
let introText;
|
||||
if (this.state.copied) {
|
||||
introText = _t(
|
||||
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
|
||||
{}, {b: s => <b>{s}</b>},
|
||||
);
|
||||
} else if (this.state.downloaded) {
|
||||
introText = _t(
|
||||
"Your recovery key is in your <b>Downloads</b> folder.",
|
||||
{}, {b: s => <b>{s}</b>},
|
||||
);
|
||||
}
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <div>
|
||||
{introText}
|
||||
<ul>
|
||||
<li>{_t("<b>Print it</b> and store it somewhere safe", {}, {b: s => <b>{s}</b>})}</li>
|
||||
<li>{_t("<b>Save it</b> on a USB key or backup drive", {}, {b: s => <b>{s}</b>})}</li>
|
||||
<li>{_t("<b>Copy it</b> to your personal cloud storage", {}, {b: s => <b>{s}</b>})}</li>
|
||||
</ul>
|
||||
<DialogButtons primaryButton={_t("OK")}
|
||||
onPrimaryButtonClick={this._bootstrapSecretStorage}
|
||||
hasCancel={false}>
|
||||
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
|
||||
</DialogButtons>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderBusyPhase() {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
return <div>
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseDone() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"Your access to encrypted messages is now protected.",
|
||||
)}</p>
|
||||
<DialogButtons primaryButton={_t('OK')}
|
||||
onPrimaryButtonClick={this._onDone}
|
||||
hasCancel={false}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhaseOptOutConfirm() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <div>
|
||||
{_t(
|
||||
"Without setting up secret storage, you won't be able to restore your " +
|
||||
"access to encrypted messages or your cross-signing identity for " +
|
||||
"verifying other devices if you log out or use another device.",
|
||||
)}
|
||||
<DialogButtons primaryButton={_t('Set up secret storage')}
|
||||
onPrimaryButtonClick={this._onSetUpClick}
|
||||
hasCancel={false}
|
||||
>
|
||||
<button onClick={this._onCancel}>I understand, continue without</button>
|
||||
</DialogButtons>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_titleForPhase(phase) {
|
||||
switch (phase) {
|
||||
case PHASE_MIGRATE:
|
||||
return _t('Migrate from Key Backup');
|
||||
case PHASE_PASSPHRASE:
|
||||
return _t('Secure your encrypted messages with a passphrase');
|
||||
case PHASE_PASSPHRASE_CONFIRM:
|
||||
return _t('Confirm your passphrase');
|
||||
case PHASE_OPTOUT_CONFIRM:
|
||||
return _t('Warning!');
|
||||
case PHASE_SHOWKEY:
|
||||
return _t('Recovery key');
|
||||
case PHASE_KEEPITSAFE:
|
||||
return _t('Keep it safe');
|
||||
case PHASE_STORING:
|
||||
return _t('Storing secrets...');
|
||||
case PHASE_DONE:
|
||||
return _t('Success!');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
let content;
|
||||
if (this.state.error) {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
content = <div>
|
||||
<p>{_t("Unable to set up secret storage")}</p>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<DialogButtons primaryButton={_t('Retry')}
|
||||
onPrimaryButtonClick={this._bootstrapSecretStorage}
|
||||
hasCancel={true}
|
||||
onCancel={this._onCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
switch (this.state.phase) {
|
||||
case PHASE_LOADING:
|
||||
content = this._renderBusyPhase();
|
||||
break;
|
||||
case PHASE_MIGRATE:
|
||||
content = this._renderPhaseMigrate();
|
||||
break;
|
||||
case PHASE_PASSPHRASE:
|
||||
content = this._renderPhasePassPhrase();
|
||||
break;
|
||||
case PHASE_PASSPHRASE_CONFIRM:
|
||||
content = this._renderPhasePassPhraseConfirm();
|
||||
break;
|
||||
case PHASE_SHOWKEY:
|
||||
content = this._renderPhaseShowKey();
|
||||
break;
|
||||
case PHASE_KEEPITSAFE:
|
||||
content = this._renderPhaseKeepItSafe();
|
||||
break;
|
||||
case PHASE_STORING:
|
||||
content = this._renderBusyPhase();
|
||||
break;
|
||||
case PHASE_DONE:
|
||||
content = this._renderPhaseDone();
|
||||
break;
|
||||
case PHASE_OPTOUT_CONFIRM:
|
||||
content = this._renderPhaseOptOutConfirm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_CreateSecretStorageDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={this._titleForPhase(this.state.phase)}
|
||||
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
|
||||
>
|
||||
<div>
|
||||
{content}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2019 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.
|
||||
|
@ -28,7 +29,7 @@ import SettingsStore from "../settings/SettingsStore";
|
|||
import { shortcodeToUnicode } from '../HtmlUtils';
|
||||
|
||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||
import EmojiData from '../stripped-emoji.json';
|
||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
|
@ -38,19 +39,15 @@ const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|:[+-\\w]*:?)$', '
|
|||
// XXX: it's very unclear why we bother with this generated emojidata file.
|
||||
// all it means is that we end up bloating the bundle with precomputed stuff
|
||||
// which would be trivial to calculate and cache on demand.
|
||||
const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort(
|
||||
(a, b) => {
|
||||
if (a.category === b.category) {
|
||||
return a.emoji_order - b.emoji_order;
|
||||
}
|
||||
return a.category - b.category;
|
||||
},
|
||||
).map((a, index) => {
|
||||
const EMOJI_SHORTNAMES = EMOJIBASE.sort((a, b) => {
|
||||
if (a.group === b.group) {
|
||||
return a.order - b.order;
|
||||
}
|
||||
return a.group - b.group;
|
||||
}).map((emoji, index) => {
|
||||
return {
|
||||
name: a.name,
|
||||
shortname: a.shortname,
|
||||
aliases: a.aliases ? a.aliases.join(' ') : '',
|
||||
aliases_ascii: a.aliases_ascii ? a.aliases_ascii.join(' ') : '',
|
||||
emoji,
|
||||
shortname: `:${emoji.shortcodes[0]}:`,
|
||||
// Include the index so that we can preserve the original order
|
||||
_orderBy: index,
|
||||
};
|
||||
|
@ -69,12 +66,15 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
constructor() {
|
||||
super(EMOJI_REGEX);
|
||||
this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, {
|
||||
keys: ['aliases_ascii', 'shortname', 'aliases'],
|
||||
keys: ['emoji.emoticon', 'shortname'],
|
||||
funcs: [
|
||||
(o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases
|
||||
],
|
||||
// For matching against ascii equivalents
|
||||
shouldMatchWordsOnly: false,
|
||||
});
|
||||
this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, {
|
||||
keys: ['name'],
|
||||
keys: ['emoji.annotation'],
|
||||
// For removing punctuation
|
||||
shouldMatchWordsOnly: true,
|
||||
});
|
||||
|
@ -96,7 +96,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
|
||||
const sorters = [];
|
||||
// make sure that emoticons come first
|
||||
sorters.push((c) => score(matchedString, c.aliases_ascii));
|
||||
sorters.push((c) => score(matchedString, c.emoji.emoticon || ""));
|
||||
|
||||
// then sort by score (Infinity if matchedString not in shortname)
|
||||
sorters.push((c) => score(matchedString, c.shortname));
|
||||
|
@ -110,8 +110,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
sorters.push((c) => c._orderBy);
|
||||
completions = _sortBy(_uniq(completions), sorters);
|
||||
|
||||
completions = completions.map((result) => {
|
||||
const { shortname } = result;
|
||||
completions = completions.map(({shortname}) => {
|
||||
const unicode = shortcodeToUnicode(shortname);
|
||||
return {
|
||||
completion: unicode,
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 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.
|
||||
*/
|
||||
|
||||
// Based originally on slate-plain-serializer
|
||||
|
||||
import { Block } from 'slate';
|
||||
|
||||
/**
|
||||
* Plain text serializer, which converts a Slate `value` to a plain text string,
|
||||
* serializing pills into various different formats as required.
|
||||
*
|
||||
* @type {PlainWithPillsSerializer}
|
||||
*/
|
||||
|
||||
class PlainWithPillsSerializer {
|
||||
/*
|
||||
* @param {String} options.pillFormat - either 'md', 'plain', 'id'
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
const {
|
||||
pillFormat = 'plain',
|
||||
} = options;
|
||||
this.pillFormat = pillFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a Slate `value` to a plain text string,
|
||||
* serializing pills as either MD links, plain text representations or
|
||||
* ID representations as required.
|
||||
*
|
||||
* @param {Value} value
|
||||
* @return {String}
|
||||
*/
|
||||
serialize = value => {
|
||||
return this._serializeNode(value.document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a `node` to plain text.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {String}
|
||||
*/
|
||||
_serializeNode = node => {
|
||||
if (
|
||||
node.object == 'document' ||
|
||||
(node.object == 'block' && Block.isBlockList(node.nodes))
|
||||
) {
|
||||
return node.nodes.map(this._serializeNode).join('\n');
|
||||
} else if (node.type == 'emoji') {
|
||||
return node.data.get('emojiUnicode');
|
||||
} else if (node.type == 'pill') {
|
||||
const completion = node.data.get('completion');
|
||||
// over the wire the @room pill is just plaintext
|
||||
if (completion === '@room') return completion;
|
||||
|
||||
switch (this.pillFormat) {
|
||||
case 'plain':
|
||||
return completion;
|
||||
case 'md':
|
||||
return `[${ completion }](${ node.data.get('href') })`;
|
||||
case 'id':
|
||||
return node.data.get('completionId') || completion;
|
||||
}
|
||||
} else if (node.nodes) {
|
||||
return node.nodes.map(this._serializeNode).join('');
|
||||
} else {
|
||||
return node.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {PlainWithPillsSerializer}
|
||||
*/
|
||||
|
||||
export default PlainWithPillsSerializer;
|
|
@ -71,6 +71,7 @@ export default class QueryMatcher {
|
|||
}
|
||||
|
||||
for (const keyValue of keyValues) {
|
||||
if (!keyValue) continue; // skip falsy keyValues
|
||||
const key = stripDiacritics(keyValue).toLowerCase();
|
||||
if (!this._items.has(key)) {
|
||||
this._items.set(key, []);
|
||||
|
|
|
@ -70,10 +70,13 @@ export class ContextMenu extends React.Component {
|
|||
|
||||
// on resize callback
|
||||
windowResize: PropTypes.func,
|
||||
|
||||
managed: PropTypes.bool, // whether this context menu should be focus managed. If false it must handle itself
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
hasBackground: true,
|
||||
managed: true,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -183,6 +186,15 @@ export class ContextMenu extends React.Component {
|
|||
};
|
||||
|
||||
_onKeyDown = (ev) => {
|
||||
if (!this.props.managed) {
|
||||
if (ev.key === Key.ESCAPE) {
|
||||
this.props.onFinished();
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let handled = true;
|
||||
|
||||
switch (ev.key) {
|
||||
|
@ -313,7 +325,7 @@ export class ContextMenu extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
|
||||
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role="menu">
|
||||
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role={this.props.managed ? "menu" : undefined}>
|
||||
{ chevron }
|
||||
{ props.children }
|
||||
</div>
|
||||
|
@ -411,7 +423,7 @@ export const toRightOf = (elementRect, chevronOffset=12) => {
|
|||
const left = elementRect.right + window.pageXOffset + 3;
|
||||
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
||||
top -= chevronOffset + 8; // where 8 is half the height of the chevron
|
||||
return {left, top};
|
||||
return {left, top, chevronOffset};
|
||||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
||||
|
|
|
@ -61,30 +61,13 @@ class CustomRoomTagPanel extends React.Component {
|
|||
}
|
||||
|
||||
class CustomRoomTagTile extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {hover: false};
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.onMouseOut = this.onMouseOut.bind(this);
|
||||
this.onMouseOver = this.onMouseOver.bind(this);
|
||||
}
|
||||
|
||||
onMouseOver() {
|
||||
this.setState({hover: true});
|
||||
}
|
||||
|
||||
onMouseOut() {
|
||||
this.setState({hover: false});
|
||||
}
|
||||
|
||||
onClick() {
|
||||
onClick = () => {
|
||||
dis.dispatch({action: 'select_custom_room_tag', tag: this.props.tag.name});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
||||
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
|
||||
|
||||
const tag = this.props.tag;
|
||||
const avatarHeight = 40;
|
||||
|
@ -102,12 +85,9 @@ class CustomRoomTagTile extends React.Component {
|
|||
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
|
||||
}
|
||||
|
||||
const tip = (this.state.hover ?
|
||||
<Tooltip className="mx_TagTile_tooltip" label={name} /> :
|
||||
<div />);
|
||||
return (
|
||||
<AccessibleButton className={className} onClick={this.onClick}>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<AccessibleTooltipButton className={className} onClick={this.onClick} title={name}>
|
||||
<div className="mx_TagTile_avatar">
|
||||
<BaseAvatar
|
||||
name={tag.avatarLetter}
|
||||
idName={name}
|
||||
|
@ -115,9 +95,8 @@ class CustomRoomTagTile extends React.Component {
|
|||
height={avatarHeight}
|
||||
/>
|
||||
{ badgeElement }
|
||||
{ tip }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,8 +26,8 @@ import sanitizeHtml from 'sanitize-html';
|
|||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import classnames from 'classnames';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
|
||||
export default class EmbeddedPage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
@ -39,9 +39,7 @@ export default class EmbeddedPage extends React.PureComponent {
|
|||
scrollbar: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -104,7 +102,7 @@ export default class EmbeddedPage extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
// HACK: Workaround for the context's MatrixClient not updating.
|
||||
const client = this.context.matrixClient || MatrixClientPeg.get();
|
||||
const client = this.context || MatrixClientPeg.get();
|
||||
const isGuest = client ? client.isGuest() : true;
|
||||
const className = this.props.className;
|
||||
const classes = classnames({
|
||||
|
|
|
@ -38,6 +38,7 @@ import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
|||
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
|
||||
import {Group} from "matrix-js-sdk";
|
||||
import {allSettled, sleep} from "../../utils/promise";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
|
||||
const LONG_DESC_PLACEHOLDER = _td(
|
||||
`<h1>HTML for your community's page</h1>
|
||||
|
@ -542,10 +543,6 @@ export default createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onShowRhsClick: function(ev) {
|
||||
dis.dispatch({ action: 'show_right_panel' });
|
||||
},
|
||||
|
||||
_onEditClick: function() {
|
||||
this.setState({
|
||||
editing: true,
|
||||
|
@ -583,6 +580,10 @@ export default createReactClass({
|
|||
profileForm: null,
|
||||
});
|
||||
break;
|
||||
case 'after_right_panel_phase_change':
|
||||
// We don't keep state on the right panel, so just re-render to update
|
||||
this.forceUpdate();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -1214,25 +1215,25 @@ export default createReactClass({
|
|||
|
||||
const EditableText = sdk.getComponent("elements.EditableText");
|
||||
|
||||
nameNode = <EditableText ref="nameEditor"
|
||||
className="mx_GroupView_editable"
|
||||
placeholderClassName="mx_GroupView_placeholder"
|
||||
placeholder={_t('Community Name')}
|
||||
blurToCancel={false}
|
||||
initialValue={this.state.profileForm.name}
|
||||
onValueChanged={this._onNameChange}
|
||||
tabIndex="0"
|
||||
dir="auto" />;
|
||||
nameNode = <EditableText
|
||||
className="mx_GroupView_editable"
|
||||
placeholderClassName="mx_GroupView_placeholder"
|
||||
placeholder={_t('Community Name')}
|
||||
blurToCancel={false}
|
||||
initialValue={this.state.profileForm.name}
|
||||
onValueChanged={this._onNameChange}
|
||||
tabIndex="0"
|
||||
dir="auto" />;
|
||||
|
||||
shortDescNode = <EditableText ref="descriptionEditor"
|
||||
className="mx_GroupView_editable"
|
||||
placeholderClassName="mx_GroupView_placeholder"
|
||||
placeholder={_t("Description")}
|
||||
blurToCancel={false}
|
||||
initialValue={this.state.profileForm.short_description}
|
||||
onValueChanged={this._onShortDescChange}
|
||||
tabIndex="0"
|
||||
dir="auto" />;
|
||||
shortDescNode = <EditableText
|
||||
className="mx_GroupView_editable"
|
||||
placeholderClassName="mx_GroupView_placeholder"
|
||||
placeholder={_t("Description")}
|
||||
blurToCancel={false}
|
||||
initialValue={this.state.profileForm.short_description}
|
||||
onValueChanged={this._onShortDescChange}
|
||||
tabIndex="0"
|
||||
dir="auto" />;
|
||||
} else {
|
||||
const onGroupHeaderItemClick = this.state.isUserMember ? this._onEditClick : null;
|
||||
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
|
||||
|
@ -1298,7 +1299,9 @@ export default createReactClass({
|
|||
);
|
||||
}
|
||||
|
||||
const rightPanel = !this.props.collapsedRhs ? <RightPanel groupId={this.props.groupId} /> : undefined;
|
||||
const rightPanel = !RightPanelStore.getSharedInstance().isOpenForGroup
|
||||
? <RightPanel groupId={this.props.groupId} />
|
||||
: undefined;
|
||||
|
||||
const headerClasses = {
|
||||
"mx_GroupView_header": true,
|
||||
|
@ -1326,9 +1329,9 @@ export default createReactClass({
|
|||
<div className="mx_GroupView_header_rightCol">
|
||||
{ rightButtons }
|
||||
</div>
|
||||
<GroupHeaderButtons collapsedRhs={this.props.collapsedRhs} />
|
||||
<GroupHeaderButtons />
|
||||
</div>
|
||||
<MainSplit collapsedRhs={this.props.collapsedRhs} panel={rightPanel}>
|
||||
<MainSplit panel={rightPanel}>
|
||||
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
||||
{ this._getMembershipSection() }
|
||||
{ this._getGroupSection() }
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
import Matrix from 'matrix-js-sdk';
|
||||
const InteractiveAuth = Matrix.InteractiveAuth;
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
@ -129,6 +129,8 @@ export default createReactClass({
|
|||
this._authLogic.poll();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
this._stageComponent = createRef();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -153,8 +155,8 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
tryContinue: function() {
|
||||
if (this.refs.stageComponent && this.refs.stageComponent.tryContinue) {
|
||||
this.refs.stageComponent.tryContinue();
|
||||
if (this._stageComponent.current && this._stageComponent.current.tryContinue) {
|
||||
this._stageComponent.current.tryContinue();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -192,8 +194,8 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
_setFocus: function() {
|
||||
if (this.refs.stageComponent && this.refs.stageComponent.focus) {
|
||||
this.refs.stageComponent.focus();
|
||||
if (this._stageComponent.current && this._stageComponent.current.focus) {
|
||||
this._stageComponent.current.focus();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -214,7 +216,8 @@ export default createReactClass({
|
|||
|
||||
const StageComponent = getEntryComponentForLoginType(stage);
|
||||
return (
|
||||
<StageComponent ref="stageComponent"
|
||||
<StageComponent
|
||||
ref={this._stageComponent}
|
||||
loginType={stage}
|
||||
matrixClient={this.props.matrixClient}
|
||||
authSessionId={this._authLogic.getSessionId()}
|
||||
|
|
|
@ -19,12 +19,10 @@ import React from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { Key } from '../../Keyboard';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import VectorConferenceHandler from '../../VectorConferenceHandler';
|
||||
import TagPanelButtons from './TagPanelButtons';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import {_t} from "../../languageHandler";
|
||||
import Analytics from "../../Analytics";
|
||||
|
@ -39,10 +37,6 @@ const LeftPanel = createReactClass({
|
|||
collapsed: PropTypes.bool.isRequired,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
searchFilter: '',
|
||||
|
@ -243,7 +237,6 @@ const LeftPanel = createReactClass({
|
|||
tagPanelContainer = (<div className="mx_LeftPanel_tagPanelContainer">
|
||||
<TagPanel />
|
||||
{ isCustomTagsEnabled ? <CustomRoomTagPanel /> : undefined }
|
||||
<TagPanelButtons />
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
@ -38,6 +38,7 @@ import TagOrderActions from '../../actions/TagOrderActions';
|
|||
import RoomListActions from '../../actions/RoomListActions';
|
||||
import ResizeHandle from '../views/elements/ResizeHandle';
|
||||
import {Resizer, CollapseDistributor} from '../../resizer';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
// 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.
|
||||
|
@ -70,7 +71,6 @@ const LoggedInView = createReactClass({
|
|||
// Called with the credentials of a registered user (if they were a ROU that
|
||||
// transitioned to PWLU)
|
||||
onRegistered: PropTypes.func,
|
||||
collapsedRhs: PropTypes.bool,
|
||||
|
||||
// Used by the RoomView to handle joining rooms
|
||||
viaServers: PropTypes.arrayOf(PropTypes.string),
|
||||
|
@ -78,21 +78,6 @@ const LoggedInView = createReactClass({
|
|||
// and lots and lots of other stuff.
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
authCache: PropTypes.object,
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
matrixClient: this._matrixClient,
|
||||
authCache: {
|
||||
auth: {},
|
||||
lastUpdate: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
// use compact timeline view
|
||||
|
@ -129,6 +114,8 @@ const LoggedInView = createReactClass({
|
|||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
|
||||
fixupColorFonts();
|
||||
|
||||
this._roomView = createRef();
|
||||
},
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
@ -165,10 +152,10 @@ const LoggedInView = createReactClass({
|
|||
},
|
||||
|
||||
canResetTimelineInRoom: function(roomId) {
|
||||
if (!this.refs.roomView) {
|
||||
if (!this._roomView.current) {
|
||||
return true;
|
||||
}
|
||||
return this.refs.roomView.canResetTimeline();
|
||||
return this._roomView.current.canResetTimeline();
|
||||
},
|
||||
|
||||
_setStateFromSessionStore() {
|
||||
|
@ -406,13 +393,6 @@ const LoggedInView = createReactClass({
|
|||
return;
|
||||
}
|
||||
|
||||
// XXX: Remove after CIDER replaces Slate completely: https://github.com/vector-im/riot-web/issues/11036
|
||||
// If using Slate, consume the Backspace without first focusing as it causes an implosion
|
||||
if (ev.key === Key.BACKSPACE && !SettingsStore.getValue("useCiderComposer")) {
|
||||
ev.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
|
||||
// synchronous dispatch so we focus before key generates input
|
||||
dis.dispatch({action: 'focus_composer'}, true);
|
||||
|
@ -428,8 +408,8 @@ const LoggedInView = createReactClass({
|
|||
* @param {Object} ev The key event
|
||||
*/
|
||||
_onScrollKeyPressed: function(ev) {
|
||||
if (this.refs.roomView) {
|
||||
this.refs.roomView.handleScrollKey(ev);
|
||||
if (this._roomView.current) {
|
||||
this._roomView.current.handleScrollKey(ev);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -543,7 +523,7 @@ const LoggedInView = createReactClass({
|
|||
switch (this.props.page_type) {
|
||||
case PageTypes.RoomView:
|
||||
pageElement = <RoomView
|
||||
ref='roomView'
|
||||
ref={this._roomView}
|
||||
autoJoin={this.props.autoJoin}
|
||||
onRegistered={this.props.onRegistered}
|
||||
thirdPartyInvite={this.props.thirdPartyInvite}
|
||||
|
@ -552,7 +532,6 @@ const LoggedInView = createReactClass({
|
|||
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||
key={this.props.currentRoomId || 'roomview'}
|
||||
disabled={this.props.middleDisabled}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
ConferenceHandler={this.props.ConferenceHandler}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>;
|
||||
|
@ -583,7 +562,6 @@ const LoggedInView = createReactClass({
|
|||
pageElement = <GroupView
|
||||
groupId={this.props.currentGroupId}
|
||||
isNew={this.props.currentGroupIsNew}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
/>;
|
||||
break;
|
||||
}
|
||||
|
@ -632,21 +610,30 @@ const LoggedInView = createReactClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div onPaste={this._onPaste} onKeyDown={this._onReactKeyDown} className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
|
||||
{ topBar }
|
||||
<ToastContainer />
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div ref={this._setResizeContainerRef} className={bodyClasses}>
|
||||
<LeftPanel
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
<ResizeHandle />
|
||||
{ pageElement }
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||
<div
|
||||
onPaste={this._onPaste}
|
||||
onKeyDown={this._onReactKeyDown}
|
||||
className='mx_MatrixChat_wrapper'
|
||||
aria-hidden={this.props.hideToSRUsers}
|
||||
onMouseDown={this._onMouseDown}
|
||||
onMouseUp={this._onMouseUp}
|
||||
>
|
||||
{ topBar }
|
||||
<ToastContainer />
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div ref={this._setResizeContainerRef} className={bodyClasses}>
|
||||
<LeftPanel
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
<ResizeHandle />
|
||||
{ pageElement }
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -62,7 +62,7 @@ export default class MainSplit extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.panel && !this.props.collapsedRhs) {
|
||||
if (this.props.panel) {
|
||||
this._createResizer();
|
||||
}
|
||||
}
|
||||
|
@ -75,17 +75,15 @@ export default class MainSplit extends React.Component {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const wasExpanded = !this.props.collapsedRhs && prevProps.collapsedRhs;
|
||||
const wasCollapsed = this.props.collapsedRhs && !prevProps.collapsedRhs;
|
||||
const wasPanelSet = this.props.panel && !prevProps.panel;
|
||||
const wasPanelCleared = !this.props.panel && prevProps.panel;
|
||||
|
||||
if (this.resizeContainer && (wasExpanded || wasPanelSet)) {
|
||||
if (this.resizeContainer && wasPanelSet) {
|
||||
// The resizer can only be created when **both** expanded and the panel is
|
||||
// set. Once both are true, the container ref will mount, which is required
|
||||
// for the resizer to work.
|
||||
this._createResizer();
|
||||
} else if (this.resizer && (wasCollapsed || wasPanelCleared)) {
|
||||
} else if (this.resizer && wasPanelCleared) {
|
||||
this.resizer.detach();
|
||||
this.resizer = null;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@ import Matrix from "matrix-js-sdk";
|
|||
|
||||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
||||
import 'focus-visible';
|
||||
// what-input helps improve keyboard accessibility
|
||||
import 'what-input';
|
||||
|
||||
import Analytics from "../../Analytics";
|
||||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||
|
@ -148,16 +150,6 @@ export default createReactClass({
|
|||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
appConfig: PropTypes.object,
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
appConfig: this.props.config,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
const s = {
|
||||
// the master view we are showing.
|
||||
|
@ -175,10 +167,9 @@ export default createReactClass({
|
|||
viewUserId: null,
|
||||
// this is persisted as mx_lhs_size, loaded in LoggedInView
|
||||
collapseLhs: false,
|
||||
collapsedRhs: window.localStorage.getItem("mx_rhs_collapsed") === "true",
|
||||
leftDisabled: false,
|
||||
middleDisabled: false,
|
||||
rightDisabled: false,
|
||||
// the right panel's disabled state is tracked in its store.
|
||||
|
||||
version: null,
|
||||
newVersion: null,
|
||||
|
@ -657,23 +648,11 @@ export default createReactClass({
|
|||
collapseLhs: false,
|
||||
});
|
||||
break;
|
||||
case 'hide_right_panel':
|
||||
window.localStorage.setItem("mx_rhs_collapsed", true);
|
||||
this.setState({
|
||||
collapsedRhs: true,
|
||||
});
|
||||
break;
|
||||
case 'show_right_panel':
|
||||
window.localStorage.setItem("mx_rhs_collapsed", false);
|
||||
this.setState({
|
||||
collapsedRhs: false,
|
||||
});
|
||||
break;
|
||||
case 'panel_disable': {
|
||||
this.setState({
|
||||
leftDisabled: payload.leftDisabled || payload.sideDisabled || false,
|
||||
middleDisabled: payload.middleDisabled || false,
|
||||
rightDisabled: payload.rightDisabled || payload.sideDisabled || false,
|
||||
// We don't track the right panel being disabled here - it's tracked in the store.
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -1245,7 +1224,6 @@ export default createReactClass({
|
|||
view: VIEWS.LOGIN,
|
||||
ready: false,
|
||||
collapseLhs: false,
|
||||
collapsedRhs: false,
|
||||
currentRoomId: null,
|
||||
});
|
||||
this.subTitleStatus = '';
|
||||
|
@ -1261,7 +1239,6 @@ export default createReactClass({
|
|||
view: VIEWS.SOFT_LOGOUT,
|
||||
ready: false,
|
||||
collapseLhs: false,
|
||||
collapsedRhs: false,
|
||||
currentRoomId: null,
|
||||
});
|
||||
this.subTitleStatus = '';
|
||||
|
@ -1479,7 +1456,7 @@ export default createReactClass({
|
|||
}
|
||||
});
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
cli.on("crypto.verification.request", request => {
|
||||
let requestObserver;
|
||||
if (request.event.getRoomId()) {
|
||||
|
@ -1505,7 +1482,7 @@ export default createReactClass({
|
|||
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
|
||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
||||
verifier,
|
||||
});
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
});
|
||||
}
|
||||
// Fire the tinter right on startup to ensure the default theme is applied
|
||||
|
@ -1592,9 +1569,17 @@ export default createReactClass({
|
|||
action: 'start_post_registration',
|
||||
});
|
||||
} else if (screen.indexOf('room/') == 0) {
|
||||
const segments = screen.substring(5).split('/');
|
||||
const roomString = segments[0];
|
||||
let eventId = segments.splice(1).join("/"); // empty string if no event id given
|
||||
// Rooms can have the following formats:
|
||||
// #room_alias:domain or !opaque_id:domain
|
||||
const room = screen.substring(5);
|
||||
const domainOffset = room.indexOf(':') + 1; // 0 in case room does not contain a :
|
||||
let eventOffset = room.length;
|
||||
// room aliases can contain slashes only look for slash after domain
|
||||
if (room.substring(domainOffset).indexOf('/') > -1) {
|
||||
eventOffset = domainOffset + room.substring(domainOffset).indexOf('/');
|
||||
}
|
||||
const roomString = room.substring(0, eventOffset);
|
||||
let eventId = room.substring(eventOffset + 1); // empty string if no event id given
|
||||
|
||||
// Previously we pulled the eventID from the segments in such a way
|
||||
// where if there was no eventId then we'd get undefined. However, we
|
||||
|
@ -1705,8 +1690,6 @@ export default createReactClass({
|
|||
handleResize: function(e) {
|
||||
const hideLhsThreshold = 1000;
|
||||
const showLhsThreshold = 1000;
|
||||
const hideRhsThreshold = 820;
|
||||
const showRhsThreshold = 820;
|
||||
|
||||
if (this._windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
||||
dis.dispatch({ action: 'hide_left_panel' });
|
||||
|
@ -1714,12 +1697,6 @@ export default createReactClass({
|
|||
if (this._windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
|
||||
dis.dispatch({ action: 'show_left_panel' });
|
||||
}
|
||||
if (this._windowWidth > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) {
|
||||
dis.dispatch({ action: 'hide_right_panel' });
|
||||
}
|
||||
if (this._windowWidth <= showRhsThreshold && window.innerWidth > showRhsThreshold) {
|
||||
dis.dispatch({ action: 'show_right_panel' });
|
||||
}
|
||||
|
||||
this.state.resizeNotifier.notifyWindowResized();
|
||||
this._windowWidth = window.innerWidth;
|
||||
|
|
|
@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
@ -159,6 +159,10 @@ export default class MessagePanel extends React.Component {
|
|||
SettingsStore.getValue("showHiddenEventsInTimeline");
|
||||
|
||||
this._isMounted = false;
|
||||
|
||||
this._readMarkerNode = createRef();
|
||||
this._whoIsTyping = createRef();
|
||||
this._scrollPanel = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -191,8 +195,7 @@ export default class MessagePanel extends React.Component {
|
|||
/* return true if the content is fully scrolled down right now; else false.
|
||||
*/
|
||||
isAtBottom() {
|
||||
return this.refs.scrollPanel
|
||||
&& this.refs.scrollPanel.isAtBottom();
|
||||
return this._scrollPanel.current && this._scrollPanel.current.isAtBottom();
|
||||
}
|
||||
|
||||
/* get the current scroll state. See ScrollPanel.getScrollState for
|
||||
|
@ -201,8 +204,7 @@ export default class MessagePanel extends React.Component {
|
|||
* returns null if we are not mounted.
|
||||
*/
|
||||
getScrollState() {
|
||||
if (!this.refs.scrollPanel) { return null; }
|
||||
return this.refs.scrollPanel.getScrollState();
|
||||
return this._scrollPanel.current ? this._scrollPanel.current.getScrollState() : null;
|
||||
}
|
||||
|
||||
// returns one of:
|
||||
|
@ -212,8 +214,8 @@ export default class MessagePanel extends React.Component {
|
|||
// 0: read marker is within the window
|
||||
// +1: read marker is below the window
|
||||
getReadMarkerPosition() {
|
||||
const readMarker = this.refs.readMarkerNode;
|
||||
const messageWrapper = this.refs.scrollPanel;
|
||||
const readMarker = this._readMarkerNode.current;
|
||||
const messageWrapper = this._scrollPanel.current;
|
||||
|
||||
if (!readMarker || !messageWrapper) {
|
||||
return null;
|
||||
|
@ -236,16 +238,16 @@ export default class MessagePanel extends React.Component {
|
|||
/* jump to the top of the content.
|
||||
*/
|
||||
scrollToTop() {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollToTop();
|
||||
if (this._scrollPanel.current) {
|
||||
this._scrollPanel.current.scrollToTop();
|
||||
}
|
||||
}
|
||||
|
||||
/* jump to the bottom of the content.
|
||||
*/
|
||||
scrollToBottom() {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollToBottom();
|
||||
if (this._scrollPanel.current) {
|
||||
this._scrollPanel.current.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,8 +257,8 @@ export default class MessagePanel extends React.Component {
|
|||
* @param {number} mult: -1 to page up, +1 to page down
|
||||
*/
|
||||
scrollRelative(mult) {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollRelative(mult);
|
||||
if (this._scrollPanel.current) {
|
||||
this._scrollPanel.current.scrollRelative(mult);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -266,8 +268,8 @@ export default class MessagePanel extends React.Component {
|
|||
* @param {KeyboardEvent} ev: the keyboard event to handle
|
||||
*/
|
||||
handleScrollKey(ev) {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.handleScrollKey(ev);
|
||||
if (this._scrollPanel.current) {
|
||||
this._scrollPanel.current.handleScrollKey(ev);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,8 +284,8 @@ export default class MessagePanel extends React.Component {
|
|||
* defaults to 0.
|
||||
*/
|
||||
scrollToEvent(eventId, pixelOffset, offsetBase) {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollToToken(eventId, pixelOffset, offsetBase);
|
||||
if (this._scrollPanel.current) {
|
||||
this._scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -297,8 +299,8 @@ export default class MessagePanel extends React.Component {
|
|||
/* check the scroll state and send out pagination requests if necessary.
|
||||
*/
|
||||
checkFillState() {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.checkFillState();
|
||||
if (this._scrollPanel.current) {
|
||||
this._scrollPanel.current.checkFillState();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -345,7 +347,7 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<li key={"readMarker_"+eventId} ref="readMarkerNode"
|
||||
<li key={"readMarker_"+eventId} ref={this._readMarkerNode}
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{ hr }
|
||||
</li>
|
||||
|
@ -829,14 +831,14 @@ export default class MessagePanel extends React.Component {
|
|||
// once dynamic content in the events load, make the scrollPanel check the
|
||||
// scroll offsets.
|
||||
_onHeightChanged = () => {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
const scrollPanel = this._scrollPanel.current;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.checkScroll();
|
||||
}
|
||||
};
|
||||
|
||||
_onTypingShown = () => {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
const scrollPanel = this._scrollPanel.current;
|
||||
// this will make the timeline grow, so checkScroll
|
||||
scrollPanel.checkScroll();
|
||||
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
|
||||
|
@ -845,7 +847,7 @@ export default class MessagePanel extends React.Component {
|
|||
};
|
||||
|
||||
_onTypingHidden = () => {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
const scrollPanel = this._scrollPanel.current;
|
||||
if (scrollPanel) {
|
||||
// as hiding the typing notifications doesn't
|
||||
// update the scrollPanel, we tell it to apply
|
||||
|
@ -858,11 +860,11 @@ export default class MessagePanel extends React.Component {
|
|||
};
|
||||
|
||||
updateTimelineMinHeight() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
const scrollPanel = this._scrollPanel.current;
|
||||
|
||||
if (scrollPanel) {
|
||||
const isAtBottom = scrollPanel.isAtBottom();
|
||||
const whoIsTyping = this.refs.whoIsTyping;
|
||||
const whoIsTyping = this._whoIsTyping.current;
|
||||
const isTypingVisible = whoIsTyping && whoIsTyping.isVisible();
|
||||
// when messages get added to the timeline,
|
||||
// but somebody else is still typing,
|
||||
|
@ -875,7 +877,7 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
onTimelineReset() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
const scrollPanel = this._scrollPanel.current;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.clearPreventShrinking();
|
||||
}
|
||||
|
@ -909,19 +911,22 @@ export default class MessagePanel extends React.Component {
|
|||
room={this.props.room}
|
||||
onShown={this._onTypingShown}
|
||||
onHidden={this._onTypingHidden}
|
||||
ref="whoIsTyping" />
|
||||
ref={this._whoIsTyping} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollPanel ref="scrollPanel" className={className}
|
||||
onScroll={this.props.onScroll}
|
||||
onResize={this.onResize}
|
||||
onFillRequest={this.props.onFillRequest}
|
||||
onUnfillRequest={this.props.onUnfillRequest}
|
||||
style={style}
|
||||
stickyBottom={this.props.stickyBottom}
|
||||
resizeNotifier={this.props.resizeNotifier}>
|
||||
<ScrollPanel
|
||||
ref={this._scrollPanel}
|
||||
className={className}
|
||||
onScroll={this.props.onScroll}
|
||||
onResize={this.onResize}
|
||||
onFillRequest={this.props.onFillRequest}
|
||||
onUnfillRequest={this.props.onUnfillRequest}
|
||||
style={style}
|
||||
stickyBottom={this.props.stickyBottom}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
{ topSpinner }
|
||||
{ this._getEventTiles() }
|
||||
{ whoIsTyping }
|
||||
|
|
|
@ -17,12 +17,11 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../index';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from '../../dispatcher';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MyGroups',
|
||||
|
@ -34,8 +33,8 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -47,7 +46,7 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
_fetch: function() {
|
||||
this.context.matrixClient.getJoinedGroups().then((result) => {
|
||||
this.context.getJoinedGroups().then((result) => {
|
||||
this.setState({groups: result.groups, error: null});
|
||||
}, (err) => {
|
||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -23,44 +23,31 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
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";
|
||||
|
||||
export default class RightPanel extends React.Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
roomId: PropTypes.string, // if showing panels for a given room, this is set
|
||||
groupId: PropTypes.string, // if showing panels for a given group, this is set
|
||||
user: PropTypes.object,
|
||||
user: PropTypes.object, // used if we know the user ahead of opening the panel
|
||||
};
|
||||
}
|
||||
|
||||
static get contextTypes() {
|
||||
return {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
}
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
static Phase = Object.freeze({
|
||||
RoomMemberList: 'RoomMemberList',
|
||||
GroupMemberList: 'GroupMemberList',
|
||||
GroupRoomList: 'GroupRoomList',
|
||||
GroupRoomInfo: 'GroupRoomInfo',
|
||||
FilePanel: 'FilePanel',
|
||||
NotificationPanel: 'NotificationPanel',
|
||||
RoomMemberInfo: 'RoomMemberInfo',
|
||||
Room3pidMemberInfo: 'Room3pidMemberInfo',
|
||||
GroupMemberInfo: 'GroupMemberInfo',
|
||||
});
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
phase: this._getPhaseFromProps(),
|
||||
isUserPrivilegedInGroup: null,
|
||||
member: this._getUserForPanel(),
|
||||
};
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this.onRoomStateMember = this.onRoomStateMember.bind(this);
|
||||
|
@ -73,30 +60,44 @@ export default class RightPanel extends React.Component {
|
|||
}, 500);
|
||||
}
|
||||
|
||||
// Helper function to split out the logic for _getPhaseFromProps() and the constructor
|
||||
// as both are called at the same time in the constructor.
|
||||
_getUserForPanel() {
|
||||
if (this.state && this.state.member) return this.state.member;
|
||||
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
|
||||
return this.props.user || lastParams['member'];
|
||||
}
|
||||
|
||||
_getPhaseFromProps() {
|
||||
const rps = RightPanelStore.getSharedInstance();
|
||||
if (this.props.groupId) {
|
||||
return RightPanel.Phase.GroupMemberList;
|
||||
} else if (this.props.user) {
|
||||
return RightPanel.Phase.RoomMemberInfo;
|
||||
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
|
||||
dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.GroupMemberList});
|
||||
return RIGHT_PANEL_PHASES.GroupMemberList;
|
||||
}
|
||||
return rps.groupPanelPhase;
|
||||
} else if (this._getUserForPanel()) {
|
||||
return RIGHT_PANEL_PHASES.RoomMemberInfo;
|
||||
} else {
|
||||
return RightPanel.Phase.RoomMemberList;
|
||||
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) {
|
||||
dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.RoomMemberList});
|
||||
return RIGHT_PANEL_PHASES.RoomMemberList;
|
||||
}
|
||||
return rps.roomPanelPhase;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
const cli = this.context.matrixClient;
|
||||
const cli = this.context;
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
this._initGroupStore(this.props.groupId);
|
||||
if (this.props.user) {
|
||||
this.setState({member: this.props.user});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (this.context.matrixClient) {
|
||||
this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
if (this.context) {
|
||||
this.context.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
}
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
}
|
||||
|
@ -126,7 +127,7 @@ export default class RightPanel extends React.Component {
|
|||
onInviteToGroupButtonClick() {
|
||||
showGroupInviteDialog(this.props.groupId).then(() => {
|
||||
this.setState({
|
||||
phase: RightPanel.Phase.GroupMemberList,
|
||||
phase: RIGHT_PANEL_PHASES.GroupMemberList,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -142,9 +143,9 @@ export default class RightPanel extends React.Component {
|
|||
return;
|
||||
}
|
||||
// redraw the badge on the membership list
|
||||
if (this.state.phase === RightPanel.Phase.RoomMemberList && member.roomId === this.props.roomId) {
|
||||
if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList && member.roomId === this.props.roomId) {
|
||||
this._delayedUpdate();
|
||||
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo && member.roomId === this.props.roomId &&
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo && member.roomId === this.props.roomId &&
|
||||
member.userId === this.state.member.userId) {
|
||||
// refresh the member info (e.g. new power level)
|
||||
this._delayedUpdate();
|
||||
|
@ -152,7 +153,7 @@ export default class RightPanel extends React.Component {
|
|||
}
|
||||
|
||||
onAction(payload) {
|
||||
if (payload.action === "view_right_panel_phase") {
|
||||
if (payload.action === "after_right_panel_phase_change") {
|
||||
this.setState({
|
||||
phase: payload.phase,
|
||||
groupRoomId: payload.groupRoomId,
|
||||
|
@ -178,14 +179,14 @@ export default class RightPanel extends React.Component {
|
|||
|
||||
let panel = <div />;
|
||||
|
||||
if (this.props.roomId && this.state.phase === RightPanel.Phase.RoomMemberList) {
|
||||
if (this.props.roomId && this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList) {
|
||||
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
|
||||
} else if (this.props.groupId && this.state.phase === RightPanel.Phase.GroupMemberList) {
|
||||
} else if (this.props.groupId && this.state.phase === RIGHT_PANEL_PHASES.GroupMemberList) {
|
||||
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.GroupRoomList) {
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomList) {
|
||||
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) {
|
||||
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) {
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
const onClose = () => {
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
|
@ -201,10 +202,10 @@ export default class RightPanel extends React.Component {
|
|||
} else {
|
||||
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
|
||||
}
|
||||
} else if (this.state.phase === RightPanel.Phase.Room3pidMemberInfo) {
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.Room3pidMemberInfo) {
|
||||
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) {
|
||||
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) {
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
const onClose = () => {
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
|
@ -225,14 +226,14 @@ export default class RightPanel extends React.Component {
|
|||
/>
|
||||
);
|
||||
}
|
||||
} else if (this.state.phase === RightPanel.Phase.GroupRoomInfo) {
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomInfo) {
|
||||
panel = <GroupRoomInfo
|
||||
groupRoomId={this.state.groupRoomId}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.groupRoomId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.NotificationPanel) {
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.NotificationPanel) {
|
||||
panel = <NotificationPanel />;
|
||||
} else if (this.state.phase === RightPanel.Phase.FilePanel) {
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) {
|
||||
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import PropTypes from 'prop-types';
|
|||
import { _t } from '../../languageHandler';
|
||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||
import Analytics from '../../Analytics';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
|
||||
const MAX_NAME_LENGTH = 80;
|
||||
const MAX_TOPIC_LENGTH = 160;
|
||||
|
@ -65,16 +66,6 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: PropTypes.object,
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this.nextBatch = null;
|
||||
|
@ -108,20 +99,9 @@ module.exports = createReactClass({
|
|||
),
|
||||
});
|
||||
});
|
||||
|
||||
// dis.dispatch({
|
||||
// action: 'panel_disable',
|
||||
// sideDisabled: true,
|
||||
// middleDisabled: true,
|
||||
// });
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// dis.dispatch({
|
||||
// action: 'panel_disable',
|
||||
// sideDisabled: false,
|
||||
// middleDisabled: false,
|
||||
// });
|
||||
if (this.filterTimeout) {
|
||||
clearTimeout(this.filterTimeout);
|
||||
}
|
||||
|
@ -281,6 +261,7 @@ module.exports = createReactClass({
|
|||
roomServer: server,
|
||||
instanceId: instanceId,
|
||||
includeAll: includeAll,
|
||||
error: null,
|
||||
}, this.refreshRoomList);
|
||||
// We also refresh the room list each time even though this
|
||||
// filtering is client-side. It hopefully won't be client side
|
||||
|
@ -572,7 +553,7 @@ module.exports = createReactClass({
|
|||
if (rows.length === 0 && !this.state.loading) {
|
||||
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
|
||||
} else {
|
||||
scrollpanel_content = <table ref="directory_table" className="mx_RoomDirectory_table">
|
||||
scrollpanel_content = <table className="mx_RoomDirectory_table">
|
||||
<tbody>
|
||||
{ rows }
|
||||
</tbody>
|
||||
|
|
|
@ -25,7 +25,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
|
|||
import Resend from '../../Resend';
|
||||
import * as cryptodevices from '../../cryptodevices';
|
||||
import dis from '../../dispatcher';
|
||||
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
||||
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
|
@ -272,7 +272,7 @@ module.exports = createReactClass({
|
|||
unsentMessages[0].error.data &&
|
||||
unsentMessages[0].error.data.error
|
||||
) {
|
||||
title = 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 });
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
|
@ -26,7 +25,7 @@ import Unread from '../../Unread';
|
|||
import * as RoomNotifs from '../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
import IndicatorScrollbar from './IndicatorScrollbar';
|
||||
import {Key, KeyCode} from '../../Keyboard';
|
||||
import {Key} from '../../Keyboard';
|
||||
import { Group } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
import RoomTile from "../views/rooms/RoomTile";
|
||||
|
@ -36,12 +35,11 @@ import {_t} from "../../languageHandler";
|
|||
// turn this on for drop & drag console debugging galore
|
||||
const debug = false;
|
||||
|
||||
const RoomSubList = createReactClass({
|
||||
displayName: 'RoomSubList',
|
||||
export default class RoomSubList extends React.PureComponent {
|
||||
static displayName = 'RoomSubList';
|
||||
static debug = debug;
|
||||
|
||||
debug: debug,
|
||||
|
||||
propTypes: {
|
||||
static propTypes = {
|
||||
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
tagName: PropTypes.string,
|
||||
|
@ -59,10 +57,26 @@ const RoomSubList = createReactClass({
|
|||
incomingCall: PropTypes.object,
|
||||
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
|
||||
forceExpand: PropTypes.bool,
|
||||
},
|
||||
};
|
||||
|
||||
getInitialState: function() {
|
||||
static defaultProps = {
|
||||
onHeaderClick: function() {
|
||||
}, // NOP
|
||||
extraTiles: [],
|
||||
isInvite: false,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
return {
|
||||
listLength: props.list.length,
|
||||
scrollTop: props.list.length === state.listLength ? state.scrollTop : 0,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hidden: this.props.startAsHidden || false,
|
||||
// some values to get LazyRenderList starting
|
||||
scrollerHeight: 800,
|
||||
|
@ -71,47 +85,33 @@ const RoomSubList = createReactClass({
|
|||
// we have to store the length of the list here so we can see if it's changed or not...
|
||||
listLength: null,
|
||||
};
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onHeaderClick: function() {
|
||||
}, // NOP
|
||||
extraTiles: [],
|
||||
isInvite: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._header = createRef();
|
||||
this._subList = createRef();
|
||||
this._scroller = createRef();
|
||||
this._headerButton = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
}
|
||||
|
||||
statics: {
|
||||
getDerivedStateFromProps: function(props, state) {
|
||||
return {
|
||||
listLength: props.list.length,
|
||||
scrollTop: props.list.length === state.listLength ? state.scrollTop : 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
}
|
||||
|
||||
// The header is collapsible if it is hidden or not stuck
|
||||
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
||||
isCollapsibleOnClick: function() {
|
||||
const stuck = this.refs.header.dataset.stuck;
|
||||
isCollapsibleOnClick() {
|
||||
const stuck = this._header.current.dataset.stuck;
|
||||
if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onAction: function(payload) {
|
||||
onAction = (payload) => {
|
||||
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
|
||||
// but this is no longer true, so we must do it here (and can apply the small
|
||||
// optimisation of checking that we care about the room being read).
|
||||
|
@ -124,9 +124,9 @@ const RoomSubList = createReactClass({
|
|||
) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onClick: function(ev) {
|
||||
onClick = (ev) => {
|
||||
if (this.isCollapsibleOnClick()) {
|
||||
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
|
||||
const isHidden = !this.state.hidden;
|
||||
|
@ -135,11 +135,11 @@ const RoomSubList = createReactClass({
|
|||
});
|
||||
} else {
|
||||
// The header is stuck, so the click is to be interpreted as a scroll to the header
|
||||
this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition);
|
||||
this.props.onHeaderClick(this.state.hidden, this._header.current.dataset.originalPosition);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onHeaderKeyDown: function(ev) {
|
||||
onHeaderKeyDown = (ev) => {
|
||||
switch (ev.key) {
|
||||
case Key.TAB:
|
||||
// Prevent LeftPanel handling Tab if focus is on the sublist header itself
|
||||
|
@ -159,7 +159,7 @@ const RoomSubList = createReactClass({
|
|||
this.onClick();
|
||||
} else if (!this.props.forceExpand) {
|
||||
// sublist is expanded, go to first room
|
||||
const element = this.refs.subList && this.refs.subList.querySelector(".mx_RoomTile");
|
||||
const element = this._subList.current && this._subList.current.querySelector(".mx_RoomTile");
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
|
@ -167,9 +167,9 @@ const RoomSubList = createReactClass({
|
|||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
onKeyDown = (ev) => {
|
||||
switch (ev.key) {
|
||||
// On ARROW_LEFT go to the sublist header
|
||||
case Key.ARROW_LEFT:
|
||||
|
@ -180,24 +180,24 @@ const RoomSubList = createReactClass({
|
|||
case Key.ARROW_RIGHT:
|
||||
ev.stopPropagation();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onRoomTileClick(roomId, ev) {
|
||||
onRoomTileClick = (roomId, ev) => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
clear_search: (ev && (ev.keyCode === KeyCode.ENTER || ev.keyCode === KeyCode.SPACE)),
|
||||
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
_updateSubListCount: function() {
|
||||
_updateSubListCount = () => {
|
||||
// Force an update by setting the state to the current state
|
||||
// Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate()
|
||||
// method is honoured
|
||||
this.setState(this.state);
|
||||
},
|
||||
};
|
||||
|
||||
makeRoomTile: function(room) {
|
||||
makeRoomTile = (room) => {
|
||||
return <RoomTile
|
||||
room={room}
|
||||
roomSubList={this}
|
||||
|
@ -212,9 +212,9 @@ const RoomSubList = createReactClass({
|
|||
incomingCall={null}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>;
|
||||
},
|
||||
};
|
||||
|
||||
_onNotifBadgeClick: function(e) {
|
||||
_onNotifBadgeClick = (e) => {
|
||||
// prevent the roomsublist collapsing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -225,9 +225,9 @@ const RoomSubList = createReactClass({
|
|||
room_id: room.roomId,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_onInviteBadgeClick: function(e) {
|
||||
_onInviteBadgeClick = (e) => {
|
||||
// prevent the roomsublist collapsing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -247,14 +247,14 @@ const RoomSubList = createReactClass({
|
|||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onAddRoom: function(e) {
|
||||
onAddRoom = (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.props.onAddRoom) this.props.onAddRoom();
|
||||
},
|
||||
};
|
||||
|
||||
_getHeaderJsx: function(isCollapsed) {
|
||||
_getHeaderJsx(isCollapsed) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
|
||||
const subListNotifications = !this.props.isInvite ?
|
||||
|
@ -328,7 +328,7 @@ const RoomSubList = createReactClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSubList_labelContainer" title={title} ref="header" onKeyDown={this.onHeaderKeyDown}>
|
||||
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
|
||||
<AccessibleButton
|
||||
onClick={this.onClick}
|
||||
className="mx_RoomSubList_label"
|
||||
|
@ -346,36 +346,36 @@ const RoomSubList = createReactClass({
|
|||
{ addRoomButton }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
checkOverflow: function() {
|
||||
if (this.refs.scroller) {
|
||||
this.refs.scroller.checkOverflow();
|
||||
checkOverflow = () => {
|
||||
if (this._scroller.current) {
|
||||
this._scroller.current.checkOverflow();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
setHeight: function(height) {
|
||||
if (this.refs.subList) {
|
||||
this.refs.subList.style.height = `${height}px`;
|
||||
setHeight = (height) => {
|
||||
if (this._subList.current) {
|
||||
this._subList.current.style.height = `${height}px`;
|
||||
}
|
||||
this._updateLazyRenderHeight(height);
|
||||
},
|
||||
};
|
||||
|
||||
_updateLazyRenderHeight: function(height) {
|
||||
_updateLazyRenderHeight(height) {
|
||||
this.setState({scrollerHeight: height});
|
||||
},
|
||||
}
|
||||
|
||||
_onScroll: function() {
|
||||
this.setState({scrollTop: this.refs.scroller.getScrollTop()});
|
||||
},
|
||||
_onScroll = () => {
|
||||
this.setState({scrollTop: this._scroller.current.getScrollTop()});
|
||||
};
|
||||
|
||||
_canUseLazyListRendering() {
|
||||
// for now disable lazy rendering as they are already rendered tiles
|
||||
// not rooms like props.list we pass to LazyRenderList
|
||||
return !this.props.extraTiles || !this.props.extraTiles.length;
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
const len = this.props.list.length + this.props.extraTiles.length;
|
||||
const isCollapsed = this.state.hidden && !this.props.forceExpand;
|
||||
|
||||
|
@ -391,7 +391,7 @@ const RoomSubList = createReactClass({
|
|||
// no body
|
||||
} else if (this._canUseLazyListRendering()) {
|
||||
content = (
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
||||
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
||||
<LazyRenderList
|
||||
scrollTop={this.state.scrollTop }
|
||||
height={ this.state.scrollerHeight }
|
||||
|
@ -404,7 +404,7 @@ const RoomSubList = createReactClass({
|
|||
const roomTiles = this.props.list.map(r => this.makeRoomTile(r));
|
||||
const tiles = roomTiles.concat(this.props.extraTiles);
|
||||
content = (
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
||||
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
||||
{ tiles }
|
||||
</IndicatorScrollbar>
|
||||
);
|
||||
|
@ -418,7 +418,7 @@ const RoomSubList = createReactClass({
|
|||
|
||||
return (
|
||||
<div
|
||||
ref="subList"
|
||||
ref={this._subList}
|
||||
className={subListClasses}
|
||||
role="group"
|
||||
aria-label={this.props.label}
|
||||
|
@ -428,7 +428,5 @@ const RoomSubList = createReactClass({
|
|||
{ content }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = RoomSubList;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,12 +23,10 @@ limitations under the License.
|
|||
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {Room} from "matrix-js-sdk";
|
||||
import { _t } from '../../languageHandler';
|
||||
import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
|
||||
|
||||
|
@ -44,7 +42,7 @@ import ObjectUtils from '../../ObjectUtils';
|
|||
import * as Rooms from '../../Rooms';
|
||||
import eventSearch from '../../Searching';
|
||||
|
||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard';
|
||||
|
||||
import MainSplit from './MainSplit';
|
||||
import RightPanel from './RightPanel';
|
||||
|
@ -54,6 +52,8 @@ import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
|||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
import WidgetUtils from '../../utils/WidgetUtils';
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function() {};
|
||||
|
@ -65,12 +65,6 @@ if (DEBUG) {
|
|||
debuglog = console.log.bind(console);
|
||||
}
|
||||
|
||||
const RoomContext = PropTypes.shape({
|
||||
canReact: PropTypes.bool.isRequired,
|
||||
canReply: PropTypes.bool.isRequired,
|
||||
room: PropTypes.instanceOf(Room),
|
||||
});
|
||||
|
||||
module.exports = createReactClass({
|
||||
displayName: 'RoomView',
|
||||
propTypes: {
|
||||
|
@ -98,9 +92,6 @@ module.exports = createReactClass({
|
|||
// * invited us to the room
|
||||
oobData: PropTypes.object,
|
||||
|
||||
// is the RightPanel collapsed?
|
||||
collapsedRhs: PropTypes.bool,
|
||||
|
||||
// Servers the RoomView can use to try and assist joins
|
||||
viaServers: PropTypes.arrayOf(PropTypes.string),
|
||||
},
|
||||
|
@ -166,23 +157,6 @@ module.exports = createReactClass({
|
|||
|
||||
canReact: false,
|
||||
canReply: false,
|
||||
|
||||
useCider: false,
|
||||
};
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
room: RoomContext,
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
const {canReact, canReply, room} = this.state;
|
||||
return {
|
||||
room: {
|
||||
canReact,
|
||||
canReply,
|
||||
room,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -204,13 +178,8 @@ module.exports = createReactClass({
|
|||
|
||||
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
|
||||
|
||||
this._onCiderUpdated();
|
||||
this._ciderWatcherRef = SettingsStore.watchSetting(
|
||||
"useCiderComposer", null, this._onCiderUpdated);
|
||||
},
|
||||
|
||||
_onCiderUpdated: function() {
|
||||
this.setState({useCider: SettingsStore.getValue("useCiderComposer")});
|
||||
this._roomView = createRef();
|
||||
this._searchResultsPanel = createRef();
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdate: function(initial) {
|
||||
|
@ -459,8 +428,8 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.refs.roomView) {
|
||||
const roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||
if (this._roomView.current) {
|
||||
const roomView = this._roomView.current;
|
||||
if (!roomView.ondrop) {
|
||||
roomView.addEventListener('drop', this.onDrop);
|
||||
roomView.addEventListener('dragover', this.onDragOver);
|
||||
|
@ -474,10 +443,10 @@ module.exports = createReactClass({
|
|||
// in render() prevents the ref from being set on first mount, so we try and
|
||||
// catch the messagePanel when it does mount. Because we only want the ref once,
|
||||
// we use a boolean flag to avoid duplicate work.
|
||||
if (this.refs.messagePanel && !this.state.atEndOfLiveTimelineInit) {
|
||||
if (this._messagePanel && !this.state.atEndOfLiveTimelineInit) {
|
||||
this.setState({
|
||||
atEndOfLiveTimelineInit: true,
|
||||
atEndOfLiveTimeline: this.refs.messagePanel.isAtEndOfLiveTimeline(),
|
||||
atEndOfLiveTimeline: this._messagePanel.isAtEndOfLiveTimeline(),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -499,12 +468,12 @@ module.exports = createReactClass({
|
|||
// stop tracking room changes to format permalinks
|
||||
this._stopAllPermalinkCreators();
|
||||
|
||||
if (this.refs.roomView) {
|
||||
if (this._roomView.current) {
|
||||
// disconnect the D&D event listeners from the room view. This
|
||||
// is really just for hygiene - we're going to be
|
||||
// deleted anyway, so it doesn't matter if the event listeners
|
||||
// don't get cleaned up.
|
||||
const roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||
const roomView = this._roomView.current;
|
||||
roomView.removeEventListener('drop', this.onDrop);
|
||||
roomView.removeEventListener('dragover', this.onDragOver);
|
||||
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
|
@ -560,15 +529,15 @@ module.exports = createReactClass({
|
|||
let handled = false;
|
||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.KEY_D:
|
||||
switch (ev.key) {
|
||||
case Key.D:
|
||||
if (ctrlCmdOnly) {
|
||||
this.onMuteAudioClick();
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.KEY_E:
|
||||
case Key.E:
|
||||
if (ctrlCmdOnly) {
|
||||
this.onMuteVideoClick();
|
||||
handled = true;
|
||||
|
@ -584,6 +553,10 @@ module.exports = createReactClass({
|
|||
|
||||
onAction: function(payload) {
|
||||
switch (payload.action) {
|
||||
case 'after_right_panel_phase_change':
|
||||
// We don't keep state on the right panel, so just re-render to update
|
||||
this.forceUpdate();
|
||||
break;
|
||||
case 'message_send_failed':
|
||||
case 'message_sent':
|
||||
this._checkIfAlone(this.state.room);
|
||||
|
@ -701,10 +674,10 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
canResetTimeline: function() {
|
||||
if (!this.refs.messagePanel) {
|
||||
if (!this._messagePanel) {
|
||||
return true;
|
||||
}
|
||||
return this.refs.messagePanel.canResetTimeline();
|
||||
return this._messagePanel.canResetTimeline();
|
||||
},
|
||||
|
||||
// called when state.room is first initialised (either at initial load,
|
||||
|
@ -787,11 +760,12 @@ module.exports = createReactClass({
|
|||
this._updateE2EStatus(room);
|
||||
},
|
||||
|
||||
_updateE2EStatus: function(room) {
|
||||
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) {
|
||||
_updateE2EStatus: async function(room) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli.isRoomEncrypted(room.roomId)) {
|
||||
return;
|
||||
}
|
||||
if (!MatrixClientPeg.get().isCryptoEnabled()) {
|
||||
if (!cli.isCryptoEnabled()) {
|
||||
// If crypto is not currently enabled, we aren't tracking devices at all,
|
||||
// so we don't know what the answer is. Let's error on the safe side and show
|
||||
// a warning for this case.
|
||||
|
@ -800,10 +774,38 @@ module.exports = createReactClass({
|
|||
});
|
||||
return;
|
||||
}
|
||||
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
|
||||
this.setState({
|
||||
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
|
||||
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
|
||||
this.setState({
|
||||
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
const e2eMembers = await room.getEncryptionTargetMembers();
|
||||
for (const member of e2eMembers) {
|
||||
const { userId } = member;
|
||||
const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified();
|
||||
if (!userVerified) {
|
||||
this.setState({
|
||||
e2eStatus: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const devices = await cli.getStoredDevicesForUser(userId);
|
||||
const allDevicesVerified = devices.every(device => {
|
||||
const { deviceId } = device;
|
||||
return cli.checkDeviceTrust(userId, deviceId).isCrossSigningVerified();
|
||||
});
|
||||
if (!allDevicesVerified) {
|
||||
this.setState({
|
||||
e2eStatus: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
e2eStatus: "verified",
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -1046,7 +1048,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
onMessageListScroll: function(ev) {
|
||||
if (this.refs.messagePanel.isAtEndOfLiveTimeline()) {
|
||||
if (this._messagePanel.isAtEndOfLiveTimeline()) {
|
||||
this.setState({
|
||||
numUnreadMessages: 0,
|
||||
atEndOfLiveTimeline: true,
|
||||
|
@ -1119,8 +1121,8 @@ module.exports = createReactClass({
|
|||
|
||||
// if we already have a search panel, we need to tell it to forget
|
||||
// about its scroll state.
|
||||
if (this.refs.searchResultsPanel) {
|
||||
this.refs.searchResultsPanel.resetScrollState();
|
||||
if (this._searchResultsPanel.current) {
|
||||
this._searchResultsPanel.current.resetScrollState();
|
||||
}
|
||||
|
||||
// make sure that we don't end up showing results from
|
||||
|
@ -1225,7 +1227,7 @@ module.exports = createReactClass({
|
|||
// once dynamic content in the search results load, make the scrollPanel check
|
||||
// the scroll offsets.
|
||||
const onHeightChanged = () => {
|
||||
const scrollPanel = this.refs.searchResultsPanel;
|
||||
const scrollPanel = this._searchResultsPanel.current;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.checkScroll();
|
||||
}
|
||||
|
@ -1370,28 +1372,28 @@ module.exports = createReactClass({
|
|||
|
||||
// jump down to the bottom of this room, where new events are arriving
|
||||
jumpToLiveTimeline: function() {
|
||||
this.refs.messagePanel.jumpToLiveTimeline();
|
||||
this._messagePanel.jumpToLiveTimeline();
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
// jump up to wherever our read marker is
|
||||
jumpToReadMarker: function() {
|
||||
this.refs.messagePanel.jumpToReadMarker();
|
||||
this._messagePanel.jumpToReadMarker();
|
||||
},
|
||||
|
||||
// update the read marker to match the read-receipt
|
||||
forgetReadMarker: function(ev) {
|
||||
ev.stopPropagation();
|
||||
this.refs.messagePanel.forgetReadMarker();
|
||||
this._messagePanel.forgetReadMarker();
|
||||
},
|
||||
|
||||
// decide whether or not the top 'unread messages' bar should be shown
|
||||
_updateTopUnreadMessagesBar: function() {
|
||||
if (!this.refs.messagePanel) {
|
||||
if (!this._messagePanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const showBar = this.refs.messagePanel.canJumpToReadMarker();
|
||||
const showBar = this._messagePanel.canJumpToReadMarker();
|
||||
if (this.state.showTopUnreadMessagesBar != showBar) {
|
||||
this.setState({showTopUnreadMessagesBar: showBar});
|
||||
}
|
||||
|
@ -1401,7 +1403,7 @@ module.exports = createReactClass({
|
|||
// restored when we switch back to it.
|
||||
//
|
||||
_getScrollState: function() {
|
||||
const messagePanel = this.refs.messagePanel;
|
||||
const messagePanel = this._messagePanel;
|
||||
if (!messagePanel) return null;
|
||||
|
||||
// if we're following the live timeline, we want to return null; that
|
||||
|
@ -1506,10 +1508,10 @@ module.exports = createReactClass({
|
|||
*/
|
||||
handleScrollKey: function(ev) {
|
||||
let panel;
|
||||
if (this.refs.searchResultsPanel) {
|
||||
panel = this.refs.searchResultsPanel;
|
||||
} else if (this.refs.messagePanel) {
|
||||
panel = this.refs.messagePanel;
|
||||
if (this._searchResultsPanel.current) {
|
||||
panel = this._searchResultsPanel.current;
|
||||
} else if (this._messagePanel) {
|
||||
panel = this._messagePanel;
|
||||
}
|
||||
|
||||
if (panel) {
|
||||
|
@ -1530,7 +1532,7 @@ module.exports = createReactClass({
|
|||
// this has to be a proper method rather than an unnamed function,
|
||||
// otherwise react calls it with null on each update.
|
||||
_gatherTimelinePanelRef: function(r) {
|
||||
this.refs.messagePanel = r;
|
||||
this._messagePanel = r;
|
||||
if (r) {
|
||||
console.log("updateTint from RoomView._gatherTimelinePanelRef");
|
||||
this.updateTint();
|
||||
|
@ -1714,12 +1716,12 @@ module.exports = createReactClass({
|
|||
let aux = null;
|
||||
let previewBar;
|
||||
let hideCancel = false;
|
||||
let hideRightPanel = false;
|
||||
let forceHideRightPanel = false;
|
||||
if (this.state.forwardingEvent !== null) {
|
||||
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
|
||||
} else if (this.state.searching) {
|
||||
hideCancel = true; // has own cancel
|
||||
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress} onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch} />;
|
||||
aux = <SearchBar searchInProgress={this.state.searchInProgress} onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch} />;
|
||||
} else if (showRoomUpgradeBar) {
|
||||
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
||||
hideCancel = true;
|
||||
|
@ -1760,7 +1762,7 @@ module.exports = createReactClass({
|
|||
</div>
|
||||
);
|
||||
} else {
|
||||
hideRightPanel = true;
|
||||
forceHideRightPanel = true;
|
||||
}
|
||||
} else if (hiddenHighlightCount > 0) {
|
||||
aux = (
|
||||
|
@ -1775,7 +1777,7 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
const auxPanel = (
|
||||
<AuxPanel ref="auxPanel" room={this.state.room}
|
||||
<AuxPanel room={this.state.room}
|
||||
fullHeight={false}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
conferenceHandler={this.props.ConferenceHandler}
|
||||
|
@ -1794,29 +1796,16 @@ module.exports = createReactClass({
|
|||
myMembership === 'join' && !this.state.searchResults
|
||||
);
|
||||
if (canSpeak) {
|
||||
if (this.state.useCider) {
|
||||
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||
messageComposer =
|
||||
<MessageComposer
|
||||
room={this.state.room}
|
||||
callState={this.state.callState}
|
||||
disabled={this.props.disabled}
|
||||
showApps={this.state.showApps}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
/>;
|
||||
} else {
|
||||
const SlateMessageComposer = sdk.getComponent('rooms.SlateMessageComposer');
|
||||
messageComposer =
|
||||
<SlateMessageComposer
|
||||
room={this.state.room}
|
||||
callState={this.state.callState}
|
||||
disabled={this.props.disabled}
|
||||
showApps={this.state.showApps}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
/>;
|
||||
}
|
||||
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||
messageComposer =
|
||||
<MessageComposer
|
||||
room={this.state.room}
|
||||
callState={this.state.callState}
|
||||
disabled={this.props.disabled}
|
||||
showApps={this.state.showApps}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
/>;
|
||||
}
|
||||
|
||||
// TODO: Why aren't we storing the term/scope/count in this format
|
||||
|
@ -1875,7 +1864,7 @@ module.exports = createReactClass({
|
|||
searchResultsPanel = (<div className="mx_RoomView_messagePanel mx_RoomView_messagePanelSearchSpinner" />);
|
||||
} else {
|
||||
searchResultsPanel = (
|
||||
<ScrollPanel ref="searchResultsPanel"
|
||||
<ScrollPanel ref={this._searchResultsPanel}
|
||||
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
|
||||
onFillRequest={this.onSearchResultsFillRequest}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
|
@ -1898,7 +1887,8 @@ module.exports = createReactClass({
|
|||
|
||||
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||
const messagePanel = (
|
||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||
<TimelinePanel
|
||||
ref={this._gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
showReadReceipts={SettingsStore.getValue('showReadReceipts')}
|
||||
manageReadReceipts={!this.state.isPeeking}
|
||||
|
@ -1918,7 +1908,8 @@ module.exports = createReactClass({
|
|||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
if (this.state.showTopUnreadMessagesBar) {
|
||||
// Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense
|
||||
if (this.state.showTopUnreadMessagesBar && !this.state.searchResults) {
|
||||
const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
|
||||
topUnreadMessagesBar = (<TopUnreadMessagesBar
|
||||
onScrollUpClick={this.jumpToReadMarker}
|
||||
|
@ -1926,7 +1917,8 @@ module.exports = createReactClass({
|
|||
/>);
|
||||
}
|
||||
let jumpToBottom;
|
||||
if (!this.state.atEndOfLiveTimeline) {
|
||||
// Do not show JumpToBottomButton if we have search results showing, it makes no sense
|
||||
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
|
||||
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
|
||||
jumpToBottom = (<JumpToBottomButton
|
||||
numUnreadMessages={this.state.numUnreadMessages}
|
||||
|
@ -1947,50 +1939,54 @@ module.exports = createReactClass({
|
|||
},
|
||||
);
|
||||
|
||||
const rightPanel = !hideRightPanel && this.state.room &&
|
||||
<RightPanel roomId={this.state.room.roomId} resizeNotifier={this.props.resizeNotifier} />;
|
||||
const collapsedRhs = hideRightPanel || this.props.collapsedRhs;
|
||||
const showRightPanel = !forceHideRightPanel && this.state.room
|
||||
&& RightPanelStore.getSharedInstance().isOpenForRoom;
|
||||
const rightPanel = showRightPanel
|
||||
? <RightPanel roomId={this.state.room.roomId} resizeNotifier={this.props.resizeNotifier} />
|
||||
: null;
|
||||
|
||||
return (
|
||||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
|
||||
<ErrorBoundary>
|
||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||
oobData={this.props.oobData}
|
||||
inRoom={myMembership === 'join'}
|
||||
collapsedRhs={collapsedRhs}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onPinnedClick={this.onPinnedClick}
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
/>
|
||||
<MainSplit
|
||||
panel={rightPanel}
|
||||
collapsedRhs={collapsedRhs}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
<div className={fadableSectionClasses}>
|
||||
{auxPanel}
|
||||
<div className="mx_RoomView_timeline">
|
||||
{topUnreadMessagesBar}
|
||||
{jumpToBottom}
|
||||
{messagePanel}
|
||||
{searchResultsPanel}
|
||||
</div>
|
||||
<div className={statusBarAreaClass}>
|
||||
<div className="mx_RoomView_statusAreaBox">
|
||||
<div className="mx_RoomView_statusAreaBox_line"></div>
|
||||
{statusBar}
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref={this._roomView}>
|
||||
<ErrorBoundary>
|
||||
<RoomHeader
|
||||
room={this.state.room}
|
||||
searchInfo={searchInfo}
|
||||
oobData={this.props.oobData}
|
||||
inRoom={myMembership === 'join'}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onPinnedClick={this.onPinnedClick}
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
/>
|
||||
<MainSplit
|
||||
panel={rightPanel}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
<div className={fadableSectionClasses}>
|
||||
{auxPanel}
|
||||
<div className="mx_RoomView_timeline">
|
||||
{topUnreadMessagesBar}
|
||||
{jumpToBottom}
|
||||
{messagePanel}
|
||||
{searchResultsPanel}
|
||||
</div>
|
||||
<div className={statusBarAreaClass}>
|
||||
<div className="mx_RoomView_statusAreaBox">
|
||||
<div className="mx_RoomView_statusAreaBox_line" />
|
||||
{statusBar}
|
||||
</div>
|
||||
</div>
|
||||
{previewBar}
|
||||
{messageComposer}
|
||||
</div>
|
||||
{previewBar}
|
||||
{messageComposer}
|
||||
</div>
|
||||
</MainSplit>
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
</MainSplit>
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, {createRef} from "react";
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import { Key } from '../../Keyboard';
|
||||
import Timer from '../../utils/Timer';
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
|
@ -166,6 +166,8 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
this.resetScrollState();
|
||||
|
||||
this._itemlist = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -328,7 +330,7 @@ module.exports = createReactClass({
|
|||
this._isFilling = true;
|
||||
}
|
||||
|
||||
const itemlist = this.refs.itemlist;
|
||||
const itemlist = this._itemlist.current;
|
||||
const firstTile = itemlist && itemlist.firstElementChild;
|
||||
const contentTop = firstTile && firstTile.offsetTop;
|
||||
const fillPromises = [];
|
||||
|
@ -373,7 +375,7 @@ module.exports = createReactClass({
|
|||
|
||||
const origExcessHeight = excessHeight;
|
||||
|
||||
const tiles = this.refs.itemlist.children;
|
||||
const tiles = this._itemlist.current.children;
|
||||
|
||||
// The scroll token of the first/last tile to be unpaginated
|
||||
let markerScrollToken = null;
|
||||
|
@ -530,26 +532,26 @@ module.exports = createReactClass({
|
|||
* @param {object} ev the keyboard event
|
||||
*/
|
||||
handleScrollKey: function(ev) {
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.PAGE_UP:
|
||||
switch (ev.key) {
|
||||
case Key.PAGE_UP:
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollRelative(-1);
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.PAGE_DOWN:
|
||||
case Key.PAGE_DOWN:
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollRelative(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.HOME:
|
||||
case Key.HOME:
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollToTop();
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.END:
|
||||
case Key.END:
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
@ -602,7 +604,7 @@ module.exports = createReactClass({
|
|||
const scrollNode = this._getScrollNode();
|
||||
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
|
||||
|
||||
const itemlist = this.refs.itemlist;
|
||||
const itemlist = this._itemlist.current;
|
||||
const messages = itemlist.children;
|
||||
let node = null;
|
||||
|
||||
|
@ -644,7 +646,7 @@ module.exports = createReactClass({
|
|||
const sn = this._getScrollNode();
|
||||
sn.scrollTop = sn.scrollHeight;
|
||||
} else if (scrollState.trackedScrollToken) {
|
||||
const itemlist = this.refs.itemlist;
|
||||
const itemlist = this._itemlist.current;
|
||||
const trackedNode = this._getTrackedNode();
|
||||
if (trackedNode) {
|
||||
const newBottomOffset = this._topFromBottom(trackedNode);
|
||||
|
@ -682,7 +684,7 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
const sn = this._getScrollNode();
|
||||
const itemlist = this.refs.itemlist;
|
||||
const itemlist = this._itemlist.current;
|
||||
const contentHeight = this._getMessagesHeight();
|
||||
const minHeight = sn.clientHeight;
|
||||
const height = Math.max(minHeight, contentHeight);
|
||||
|
@ -724,7 +726,7 @@ module.exports = createReactClass({
|
|||
|
||||
if (!trackedNode || !trackedNode.parentElement) {
|
||||
let node;
|
||||
const messages = this.refs.itemlist.children;
|
||||
const messages = this._itemlist.current.children;
|
||||
const scrollToken = scrollState.trackedScrollToken;
|
||||
|
||||
for (let i = messages.length-1; i >= 0; --i) {
|
||||
|
@ -756,7 +758,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_getMessagesHeight() {
|
||||
const itemlist = this.refs.itemlist;
|
||||
const itemlist = this._itemlist.current;
|
||||
const lastNode = itemlist.lastElementChild;
|
||||
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
|
||||
const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
|
||||
|
@ -765,7 +767,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_topFromBottom(node) {
|
||||
return this.refs.itemlist.clientHeight - node.offsetTop;
|
||||
return this._itemlist.current.clientHeight - node.offsetTop;
|
||||
},
|
||||
|
||||
/* get the DOM node which has the scrollTop property we care about for our
|
||||
|
@ -797,7 +799,7 @@ module.exports = createReactClass({
|
|||
the same minimum bottom offset, effectively preventing the timeline to shrink.
|
||||
*/
|
||||
preventShrinking: function() {
|
||||
const messageList = this.refs.itemlist;
|
||||
const messageList = this._itemlist.current;
|
||||
const tiles = messageList && messageList.children;
|
||||
if (!messageList) {
|
||||
return;
|
||||
|
@ -824,7 +826,7 @@ module.exports = createReactClass({
|
|||
|
||||
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
|
||||
clearPreventShrinking: function() {
|
||||
const messageList = this.refs.itemlist;
|
||||
const messageList = this._itemlist.current;
|
||||
const balanceElement = messageList && messageList.parentElement;
|
||||
if (balanceElement) balanceElement.style.paddingBottom = null;
|
||||
this.preventShrinkingState = null;
|
||||
|
@ -843,7 +845,7 @@ module.exports = createReactClass({
|
|||
if (this.preventShrinkingState) {
|
||||
const sn = this._getScrollNode();
|
||||
const scrollState = this.scrollState;
|
||||
const messageList = this.refs.itemlist;
|
||||
const messageList = this._itemlist.current;
|
||||
const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
|
||||
// element used to set paddingBottom to balance the typing notifs disappearing
|
||||
const balanceElement = messageList.parentElement;
|
||||
|
@ -879,7 +881,7 @@ module.exports = createReactClass({
|
|||
onScroll={this.onScroll}
|
||||
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
|
||||
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite">
|
||||
{ this.props.children }
|
||||
</ol>
|
||||
</div>
|
||||
|
|
|
@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import { Key } from '../../Keyboard';
|
||||
import dis from '../../dispatcher';
|
||||
import { throttle } from 'lodash';
|
||||
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||
|
@ -53,6 +53,10 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._search = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
@ -66,31 +70,31 @@ module.exports = createReactClass({
|
|||
|
||||
switch (payload.action) {
|
||||
case 'view_room':
|
||||
if (this.refs.search && payload.clear_search) {
|
||||
if (this._search.current && payload.clear_search) {
|
||||
this._clearSearch();
|
||||
}
|
||||
break;
|
||||
case 'focus_room_filter':
|
||||
if (this.refs.search) {
|
||||
this.refs.search.focus();
|
||||
if (this._search.current) {
|
||||
this._search.current.focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onChange: function() {
|
||||
if (!this.refs.search) return;
|
||||
this.setState({ searchTerm: this.refs.search.value });
|
||||
if (!this._search.current) return;
|
||||
this.setState({ searchTerm: this._search.current.value });
|
||||
this.onSearch();
|
||||
},
|
||||
|
||||
onSearch: throttle(function() {
|
||||
this.props.onSearch(this.refs.search.value);
|
||||
this.props.onSearch(this._search.current.value);
|
||||
}, 200, {trailing: true, leading: true}),
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.ESCAPE:
|
||||
switch (ev.key) {
|
||||
case Key.ESCAPE:
|
||||
this._clearSearch("keyboard");
|
||||
break;
|
||||
}
|
||||
|
@ -113,7 +117,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_clearSearch: function(source) {
|
||||
this.refs.search.value = "";
|
||||
this._search.current.value = "";
|
||||
this.onChange();
|
||||
if (this.props.onCleared) {
|
||||
this.props.onCleared(source);
|
||||
|
@ -146,7 +150,7 @@ module.exports = createReactClass({
|
|||
<input
|
||||
key="searchfield"
|
||||
type="text"
|
||||
ref="search"
|
||||
ref={this._search}
|
||||
className={"mx_textinput_icon mx_textinput_search " + className}
|
||||
value={ this.state.searchTerm }
|
||||
onFocus={ this._onFocus }
|
||||
|
|
|
@ -16,8 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import TagOrderStore from '../../stores/TagOrderStore';
|
||||
|
||||
import GroupActions from '../../actions/GroupActions';
|
||||
|
@ -28,12 +26,13 @@ import { _t } from '../../languageHandler';
|
|||
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
|
||||
const TagPanel = createReactClass({
|
||||
displayName: 'TagPanel',
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -45,8 +44,8 @@ const TagPanel = createReactClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.matrixClient.on("sync", this._onClientSync);
|
||||
this.context.on("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.on("sync", this._onClientSync);
|
||||
|
||||
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
||||
if (this.unmounted) {
|
||||
|
@ -58,13 +57,13 @@ const TagPanel = createReactClass({
|
|||
});
|
||||
});
|
||||
// This could be done by anything with a matrix client
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.matrixClient.removeListener("sync", this._onClientSync);
|
||||
this.context.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.removeListener("sync", this._onClientSync);
|
||||
if (this._filterStoreToken) {
|
||||
this._filterStoreToken.remove();
|
||||
}
|
||||
|
@ -72,7 +71,7 @@ const TagPanel = createReactClass({
|
|||
|
||||
_onGroupMyMembership() {
|
||||
if (this.unmounted) return;
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
||||
},
|
||||
|
||||
_onClientSync(syncState, prevState) {
|
||||
|
@ -81,7 +80,7 @@ const TagPanel = createReactClass({
|
|||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected) {
|
||||
// Load joined groups
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -104,6 +103,7 @@ const TagPanel = createReactClass({
|
|||
render() {
|
||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
|
@ -154,6 +154,13 @@ const TagPanel = createReactClass({
|
|||
ref={provided.innerRef}
|
||||
>
|
||||
{ tags }
|
||||
<div>
|
||||
<ActionButton
|
||||
tooltip
|
||||
label={_t("Communities")}
|
||||
action="toggle_my_groups"
|
||||
className="mx_TagTile mx_TagTile_plus" />
|
||||
</div>
|
||||
{ provided.placeholder }
|
||||
</div>
|
||||
) }
|
||||
|
|
|
@ -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 sdk from '../../index';
|
||||
import dis from '../../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;
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
|||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import ReactDOM from "react-dom";
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -34,7 +34,7 @@ const dis = require("../../dispatcher");
|
|||
const ObjectUtils = require('../../ObjectUtils');
|
||||
const Modal = require("../../Modal");
|
||||
const UserActivity = require("../../UserActivity");
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import {Key} from '../../Keyboard';
|
||||
import Timer from '../../utils/Timer';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
|
@ -203,6 +203,8 @@ const TimelinePanel = createReactClass({
|
|||
this.lastRRSentEventId = undefined;
|
||||
this.lastRMSentEventId = undefined;
|
||||
|
||||
this._messagePanel = createRef();
|
||||
|
||||
if (this.props.manageReadReceipts) {
|
||||
this.updateReadReceiptOnUserActivity();
|
||||
}
|
||||
|
@ -425,8 +427,8 @@ const TimelinePanel = createReactClass({
|
|||
if (payload.action === "edit_event") {
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({editState}, () => {
|
||||
if (payload.event && this.refs.messagePanel) {
|
||||
this.refs.messagePanel.scrollToEventIfNeeded(
|
||||
if (payload.event && this._messagePanel.current) {
|
||||
this._messagePanel.current.scrollToEventIfNeeded(
|
||||
payload.event.getId(),
|
||||
);
|
||||
}
|
||||
|
@ -442,9 +444,9 @@ const TimelinePanel = createReactClass({
|
|||
// updates from pagination will happen when the paginate completes.
|
||||
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
||||
|
||||
if (!this.refs.messagePanel) return;
|
||||
if (!this._messagePanel.current) return;
|
||||
|
||||
if (!this.refs.messagePanel.getScrollState().stuckAtBottom) {
|
||||
if (!this._messagePanel.current.getScrollState().stuckAtBottom) {
|
||||
// we won't load this event now, because we don't want to push any
|
||||
// events off the other end of the timeline. But we need to note
|
||||
// that we can now paginate.
|
||||
|
@ -499,7 +501,7 @@ const TimelinePanel = createReactClass({
|
|||
}
|
||||
|
||||
this.setState(updatedState, () => {
|
||||
this.refs.messagePanel.updateTimelineMinHeight();
|
||||
this._messagePanel.current.updateTimelineMinHeight();
|
||||
if (callRMUpdated) {
|
||||
this.props.onReadMarkerUpdated();
|
||||
}
|
||||
|
@ -510,13 +512,13 @@ const TimelinePanel = createReactClass({
|
|||
onRoomTimelineReset: function(room, timelineSet) {
|
||||
if (timelineSet !== this.props.timelineSet) return;
|
||||
|
||||
if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) {
|
||||
if (this._messagePanel.current && this._messagePanel.current.isAtBottom()) {
|
||||
this._loadTimeline();
|
||||
}
|
||||
},
|
||||
|
||||
canResetTimeline: function() {
|
||||
return this.refs.messagePanel && this.refs.messagePanel.isAtBottom();
|
||||
return this._messagePanel.current && this._messagePanel.current.isAtBottom();
|
||||
},
|
||||
|
||||
onRoomRedaction: function(ev, room) {
|
||||
|
@ -629,7 +631,7 @@ const TimelinePanel = createReactClass({
|
|||
sendReadReceipt: function() {
|
||||
if (SettingsStore.getValue("lowBandwidth")) return;
|
||||
|
||||
if (!this.refs.messagePanel) return;
|
||||
if (!this._messagePanel.current) return;
|
||||
if (!this.props.manageReadReceipts) return;
|
||||
// This happens on user_activity_end which is delayed, and it's
|
||||
// very possible have logged out within that timeframe, so check
|
||||
|
@ -815,8 +817,8 @@ const TimelinePanel = createReactClass({
|
|||
if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||
this._loadTimeline();
|
||||
} else {
|
||||
if (this.refs.messagePanel) {
|
||||
this.refs.messagePanel.scrollToBottom();
|
||||
if (this._messagePanel.current) {
|
||||
this._messagePanel.current.scrollToBottom();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -826,7 +828,7 @@ const TimelinePanel = createReactClass({
|
|||
*/
|
||||
jumpToReadMarker: function() {
|
||||
if (!this.props.manageReadMarkers) return;
|
||||
if (!this.refs.messagePanel) return;
|
||||
if (!this._messagePanel.current) return;
|
||||
if (!this.state.readMarkerEventId) return;
|
||||
|
||||
// we may not have loaded the event corresponding to the read-marker
|
||||
|
@ -835,11 +837,11 @@ const TimelinePanel = createReactClass({
|
|||
//
|
||||
// a quick way to figure out if we've loaded the relevant event is
|
||||
// simply to check if the messagepanel knows where the read-marker is.
|
||||
const ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||
const ret = this._messagePanel.current.getReadMarkerPosition();
|
||||
if (ret !== null) {
|
||||
// The messagepanel knows where the RM is, so we must have loaded
|
||||
// the relevant event.
|
||||
this.refs.messagePanel.scrollToEvent(this.state.readMarkerEventId,
|
||||
this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
|
||||
0, 1/3);
|
||||
return;
|
||||
}
|
||||
|
@ -874,8 +876,8 @@ const TimelinePanel = createReactClass({
|
|||
* at the end of the live timeline.
|
||||
*/
|
||||
isAtEndOfLiveTimeline: function() {
|
||||
return this.refs.messagePanel
|
||||
&& this.refs.messagePanel.isAtBottom()
|
||||
return this._messagePanel.current
|
||||
&& this._messagePanel.current.isAtBottom()
|
||||
&& this._timelineWindow
|
||||
&& !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||
},
|
||||
|
@ -887,8 +889,8 @@ const TimelinePanel = createReactClass({
|
|||
* returns null if we are not mounted.
|
||||
*/
|
||||
getScrollState: function() {
|
||||
if (!this.refs.messagePanel) { return null; }
|
||||
return this.refs.messagePanel.getScrollState();
|
||||
if (!this._messagePanel.current) { return null; }
|
||||
return this._messagePanel.current.getScrollState();
|
||||
},
|
||||
|
||||
// returns one of:
|
||||
|
@ -899,9 +901,9 @@ const TimelinePanel = createReactClass({
|
|||
// +1: read marker is below the window
|
||||
getReadMarkerPosition: function() {
|
||||
if (!this.props.manageReadMarkers) return null;
|
||||
if (!this.refs.messagePanel) return null;
|
||||
if (!this._messagePanel.current) return null;
|
||||
|
||||
const ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||
const ret = this._messagePanel.current.getReadMarkerPosition();
|
||||
if (ret !== null) {
|
||||
return ret;
|
||||
}
|
||||
|
@ -936,15 +938,14 @@ const TimelinePanel = createReactClass({
|
|||
* We pass it down to the scroll panel.
|
||||
*/
|
||||
handleScrollKey: function(ev) {
|
||||
if (!this.refs.messagePanel) { return; }
|
||||
if (!this._messagePanel.current) { return; }
|
||||
|
||||
// jump to the live timeline on ctrl-end, rather than the end of the
|
||||
// timeline window.
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
|
||||
ev.keyCode == KeyCode.END) {
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && ev.key === Key.END) {
|
||||
this.jumpToLiveTimeline();
|
||||
} else {
|
||||
this.refs.messagePanel.handleScrollKey(ev);
|
||||
this._messagePanel.current.handleScrollKey(ev);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -986,8 +987,8 @@ const TimelinePanel = createReactClass({
|
|||
const onLoaded = () => {
|
||||
// clear the timeline min-height when
|
||||
// (re)loading the timeline
|
||||
if (this.refs.messagePanel) {
|
||||
this.refs.messagePanel.onTimelineReset();
|
||||
if (this._messagePanel.current) {
|
||||
this._messagePanel.current.onTimelineReset();
|
||||
}
|
||||
this._reloadEvents();
|
||||
|
||||
|
@ -1002,7 +1003,7 @@ const TimelinePanel = createReactClass({
|
|||
timelineLoading: false,
|
||||
}, () => {
|
||||
// initialise the scroll state of the message panel
|
||||
if (!this.refs.messagePanel) {
|
||||
if (!this._messagePanel.current) {
|
||||
// this shouldn't happen - we know we're mounted because
|
||||
// we're in a setState callback, and we know
|
||||
// timelineLoading is now false, so render() should have
|
||||
|
@ -1012,10 +1013,10 @@ const TimelinePanel = createReactClass({
|
|||
return;
|
||||
}
|
||||
if (eventId) {
|
||||
this.refs.messagePanel.scrollToEvent(eventId, pixelOffset,
|
||||
this._messagePanel.current.scrollToEvent(eventId, pixelOffset,
|
||||
offsetBase);
|
||||
} else {
|
||||
this.refs.messagePanel.scrollToBottom();
|
||||
this._messagePanel.current.scrollToBottom();
|
||||
}
|
||||
|
||||
this.sendReadReceipt();
|
||||
|
@ -1134,7 +1135,7 @@ const TimelinePanel = createReactClass({
|
|||
const ignoreOwn = opts.ignoreOwn || false;
|
||||
const allowPartial = opts.allowPartial || false;
|
||||
|
||||
const messagePanel = this.refs.messagePanel;
|
||||
const messagePanel = this._messagePanel.current;
|
||||
if (messagePanel === undefined) return null;
|
||||
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
|
@ -1313,7 +1314,8 @@ const TimelinePanel = createReactClass({
|
|||
['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState)
|
||||
);
|
||||
return (
|
||||
<MessagePanel ref="messagePanel"
|
||||
<MessagePanel
|
||||
ref={this._messagePanel}
|
||||
room={this.props.timelineSet.room}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
hidden={this.props.hidden}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -48,6 +48,8 @@ module.exports = createReactClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
this._captchaWidgetId = null;
|
||||
|
||||
this._recaptchaContainer = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -67,7 +69,7 @@ module.exports = createReactClass({
|
|||
scriptTag.setAttribute(
|
||||
'src', `${protocol}//www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
|
||||
);
|
||||
this.refs.recaptchaContainer.appendChild(scriptTag);
|
||||
this._recaptchaContainer.current.appendChild(scriptTag);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -124,11 +126,11 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div ref="recaptchaContainer">
|
||||
<div ref={this._recaptchaContainer}>
|
||||
<p>{_t(
|
||||
"This homeserver would like to make sure you are not a robot.",
|
||||
)}</p>
|
||||
<div id={DIV_ID}></div>
|
||||
<div id={DIV_ID} />
|
||||
{ error }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -21,6 +21,7 @@ import sdk from '../../../index';
|
|||
|
||||
import { COUNTRIES } from '../../../phonenumber';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
const COUNTRIES_BY_ISO2 = {};
|
||||
for (const c of COUNTRIES) {
|
||||
|
@ -130,10 +131,17 @@ export default class CountryDropdown extends React.Component {
|
|||
// values between mounting and the initial value propgating
|
||||
const value = this.props.value || this.state.defaultCountry.iso2;
|
||||
|
||||
return <Dropdown className={this.props.className + " mx_CountryDropdown"}
|
||||
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
|
||||
menuWidth={298} getShortOption={this._getShortOption}
|
||||
value={value} searchEnabled={true} disabled={this.props.disabled}
|
||||
return <Dropdown
|
||||
id="mx_CountryDropdown"
|
||||
className={this.props.className + " mx_CountryDropdown"}
|
||||
onOptionChange={this._onOptionChange}
|
||||
onSearchChange={this._onSearchChange}
|
||||
menuWidth={298}
|
||||
getShortOption={this._getShortOption}
|
||||
value={value}
|
||||
searchEnabled={true}
|
||||
disabled={this.props.disabled}
|
||||
label={_t("Country Dropdown")}
|
||||
>
|
||||
{ options }
|
||||
</Dropdown>;
|
||||
|
|
|
@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import url from 'url';
|
||||
|
@ -581,6 +581,8 @@ export const FallbackAuthEntry = createReactClass({
|
|||
// the popup if we open it immediately.
|
||||
this._popupWindow = null;
|
||||
window.addEventListener("message", this._onReceiveMessage);
|
||||
|
||||
this._fallbackButton = createRef();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -591,8 +593,8 @@ export const FallbackAuthEntry = createReactClass({
|
|||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.refs.fallbackButton) {
|
||||
this.refs.fallbackButton.focus();
|
||||
if (this._fallbackButton.current) {
|
||||
this._fallbackButton.current.focus();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -624,7 +626,7 @@ export const FallbackAuthEntry = createReactClass({
|
|||
}
|
||||
return (
|
||||
<div>
|
||||
<a ref="fallbackButton" onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||
<a ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||
{errorSection}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -19,10 +19,10 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import AvatarLogic from '../../../Avatar';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
module.exports = createReactClass({
|
||||
displayName: 'BaseAvatar',
|
||||
|
@ -38,10 +38,16 @@ module.exports = createReactClass({
|
|||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: PropTypes.string,
|
||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||
inputRef: PropTypes.oneOfType([
|
||||
// Either a function
|
||||
PropTypes.func,
|
||||
// Or the instance of a DOM native element
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -59,12 +65,12 @@ module.exports = createReactClass({
|
|||
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on('sync', this.onClientSync);
|
||||
this.context.on('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||
this.context.removeListener('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
|
@ -148,7 +154,7 @@ module.exports = createReactClass({
|
|||
|
||||
const {
|
||||
name, idName, title, url, urls, width, height, resizeMethod,
|
||||
defaultToInitialLetter, onClick,
|
||||
defaultToInitialLetter, onClick, inputRef,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
|
@ -171,7 +177,7 @@ module.exports = createReactClass({
|
|||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton element='span' className="mx_BaseAvatar"
|
||||
onClick={onClick} {...otherProps}
|
||||
onClick={onClick} inputRef={inputRef} {...otherProps}
|
||||
>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
|
@ -179,7 +185,7 @@ module.exports = createReactClass({
|
|||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_BaseAvatar" {...otherProps}>
|
||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
|
@ -188,21 +194,26 @@ module.exports = createReactClass({
|
|||
}
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
<AccessibleButton
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
element='img'
|
||||
src={imageUrl}
|
||||
onClick={onClick}
|
||||
onError={this.onError}
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
inputRef={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
|
||||
<img
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
src={imageUrl}
|
||||
onError={this.onError}
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
ref={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -38,8 +38,8 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
|||
resizeMethod: 'crop',
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hasStatus: this.hasStatus,
|
||||
|
|
|
@ -31,8 +31,8 @@ export default class GroupInviteTileContextMenu extends React.Component {
|
|||
onFinished: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onClickReject = this._onClickReject.bind(this);
|
||||
}
|
||||
|
|
|
@ -27,8 +27,8 @@ export default class StatusMessageContextMenu extends React.Component {
|
|||
user: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
message: this.comittedStatusMessage,
|
||||
|
|
|
@ -17,12 +17,12 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher';
|
||||
import TagOrderActions from '../../../actions/TagOrderActions';
|
||||
import sdk from '../../../index';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
export default class TagTileContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -31,9 +31,7 @@ export default class TagTileContextMenu extends React.Component {
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -51,7 +49,7 @@ export default class TagTileContextMenu extends React.Component {
|
|||
}
|
||||
|
||||
_onRemoveClick() {
|
||||
dis.dispatch(TagOrderActions.removeTag(this.context.matrixClient, this.props.tag));
|
||||
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import { getHostingLink } from '../../../utils/HostingLink';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import sdk from "../../../index";
|
||||
|
||||
export class TopLeftMenu extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -100,6 +101,12 @@ export class TopLeftMenu extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const helpItem = (
|
||||
<MenuItem className="mx_TopLeftMenu_icon_help" onClick={this.openHelp}>
|
||||
{_t("Help")}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const settingsItem = (
|
||||
<MenuItem className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
|
||||
{_t("Settings")}
|
||||
|
@ -115,11 +122,18 @@ export class TopLeftMenu extends React.Component {
|
|||
<ul className="mx_TopLeftMenu_section_withIcon" role="none">
|
||||
{homePageItem}
|
||||
{settingsItem}
|
||||
{helpItem}
|
||||
{signInOutItem}
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
|
||||
openHelp = () => {
|
||||
this.closeMenu();
|
||||
const RedesignFeedbackDialog = sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
|
||||
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
|
||||
};
|
||||
|
||||
viewHomePage() {
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
this.closeMenu();
|
||||
|
|
|
@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
|
@ -32,6 +32,7 @@ import IdentityAuthClient from '../../../IdentityAuthClient';
|
|||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
|
||||
import { abbreviateUrl } from '../../../utils/UrlUtils';
|
||||
import {sleep} from "../../../utils/promise";
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
@ -106,10 +107,14 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._textinput = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.focus) {
|
||||
// Set the cursor at the end of the text input
|
||||
this.refs.textinput.value = this.props.value;
|
||||
this._textinput.current.value = this.props.value;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -126,8 +131,8 @@ module.exports = createReactClass({
|
|||
let selectedList = this.state.selectedList.slice();
|
||||
// Check the text input field to see if user has an unconverted address
|
||||
// If there is and it's valid add it to the local selectedList
|
||||
if (this.refs.textinput.value !== '') {
|
||||
selectedList = this._addAddressesToList([this.refs.textinput.value]);
|
||||
if (this._textinput.current.value !== '') {
|
||||
selectedList = this._addAddressesToList([this._textinput.current.value]);
|
||||
if (selectedList === null) return;
|
||||
}
|
||||
this.props.onFinished(true, selectedList);
|
||||
|
@ -138,39 +143,41 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
onKeyDown: function(e) {
|
||||
if (e.keyCode === 27) { // escape
|
||||
const textInput = this._textinput.current ? this._textinput.current.value : undefined;
|
||||
|
||||
if (e.key === Key.ESCAPE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
} else if (e.keyCode === 38) { // up arrow
|
||||
} else if (e.key === Key.ARROW_UP) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionUp();
|
||||
} else if (e.keyCode === 40) { // down arrow
|
||||
} else if (e.key === Key.ARROW_DOWN) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionDown();
|
||||
} else if (this.state.suggestedList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
} else if (this.state.suggestedList.length > 0 && [Key.COMMA, Key.ENTER, Key.TAB].includes(e.key)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.chooseSelection();
|
||||
} else if (this.refs.textinput.value.length === 0 && this.state.selectedList.length && e.keyCode === 8) { // backspace
|
||||
} else if (textInput.length === 0 && this.state.selectedList.length && e.key === Key.BACKSPACE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.onDismissed(this.state.selectedList.length - 1)();
|
||||
} else if (e.keyCode === 13) { // enter
|
||||
} else if (e.key === Key.ENTER) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.refs.textinput.value === '') {
|
||||
if (textInput === '') {
|
||||
// if there's nothing in the input box, submit the form
|
||||
this.onButtonClick();
|
||||
} else {
|
||||
this._addAddressesToList([this.refs.textinput.value]);
|
||||
this._addAddressesToList([textInput]);
|
||||
}
|
||||
} else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab
|
||||
} else if (textInput && (e.key === Key.COMMA || e.key === Key.TAB)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this._addAddressesToList([this.refs.textinput.value]);
|
||||
this._addAddressesToList([textInput]);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -647,7 +654,7 @@ module.exports = createReactClass({
|
|||
onPaste={this._onPaste}
|
||||
rows="1"
|
||||
id="textinput"
|
||||
ref="textinput"
|
||||
ref={this._textinput}
|
||||
className="mx_AddressPickerDialog_input"
|
||||
onChange={this.onQueryChanged}
|
||||
placeholder={this.getPlaceholder()}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,16 +18,15 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import FocusLock from 'react-focus-lock';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import { Key } from '../../../Keyboard';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
/**
|
||||
* Basic container for modal dialogs.
|
||||
|
@ -83,16 +83,6 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
matrixClient: this._matrixClient,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
},
|
||||
|
@ -101,7 +91,7 @@ export default createReactClass({
|
|||
if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
if (this.props.hasCancel && e.keyCode === KeyCode.ESCAPE) {
|
||||
if (this.props.hasCancel && e.key === Key.ESCAPE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
|
@ -121,32 +111,38 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<FocusTrap onKeyDown={this._onKeyDown}
|
||||
className={classNames({
|
||||
[this.props.className]: true,
|
||||
'mx_Dialog_fixedWidth': this.props.fixedWidth,
|
||||
})}
|
||||
role="dialog"
|
||||
aria-labelledby='mx_BaseDialog_title'
|
||||
// This should point to a node describing the dialog.
|
||||
// If we were about to completely follow this recommendation we'd need to
|
||||
// make all the components relying on BaseDialog to be aware of it.
|
||||
// So instead we will use the whole content as the description.
|
||||
// Description comes first and if the content contains more text,
|
||||
// AT users can skip its presentation.
|
||||
aria-describedby={this.props.contentId}
|
||||
>
|
||||
<div className={classNames('mx_Dialog_header', {
|
||||
'mx_Dialog_headerWithButton': !!this.props.headerButton,
|
||||
})}>
|
||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||
{ this.props.title }
|
||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||
<FocusLock
|
||||
returnFocus={true}
|
||||
lockProps={{
|
||||
onKeyDown: this._onKeyDown,
|
||||
role: "dialog",
|
||||
["aria-labelledby"]: "mx_BaseDialog_title",
|
||||
// This should point to a node describing the dialog.
|
||||
// If we were about to completely follow this recommendation we'd need to
|
||||
// make all the components relying on BaseDialog to be aware of it.
|
||||
// So instead we will use the whole content as the description.
|
||||
// Description comes first and if the content contains more text,
|
||||
// AT users can skip its presentation.
|
||||
["aria-describedby"]: this.props.contentId,
|
||||
}}
|
||||
className={classNames({
|
||||
[this.props.className]: true,
|
||||
'mx_Dialog_fixedWidth': this.props.fixedWidth,
|
||||
})}
|
||||
>
|
||||
<div className={classNames('mx_Dialog_header', {
|
||||
'mx_Dialog_headerWithButton': !!this.props.headerButton,
|
||||
})}>
|
||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||
{ this.props.title }
|
||||
</div>
|
||||
{ this.props.headerButton }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
{ this.props.headerButton }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
{ this.props.children }
|
||||
</FocusTrap>
|
||||
{ this.props.children }
|
||||
</FocusLock>
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -25,8 +25,8 @@ import Modal from '../../../Modal';
|
|||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class BugReportDialog extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
sendLogs: true,
|
||||
busy: false,
|
||||
|
|
|
@ -173,7 +173,7 @@ export default createReactClass({
|
|||
const domain = MatrixClientPeg.get().getDomain();
|
||||
aliasField = (
|
||||
<div className="mx_CreateRoomDialog_aliasContainer">
|
||||
<RoomAliasField id="alias" ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} />
|
||||
<RoomAliasField id="alias" ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 {_t} from "../../../languageHandler";
|
||||
import sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {RoomMember} from "matrix-js-sdk/lib/matrix";
|
||||
import * as humanize from "humanize";
|
||||
|
||||
// TODO: [TravisR] Make this generic for all kinds of invites
|
||||
|
||||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||
|
||||
class DMRoomTile extends React.PureComponent {
|
||||
static propTypes = {
|
||||
member: PropTypes.object.isRequired,
|
||||
lastActiveTs: PropTypes.number,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
_onClick = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.props.onToggle(this.props.member.userId);
|
||||
};
|
||||
|
||||
render() {
|
||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
|
||||
let timestamp = null;
|
||||
if (this.props.lastActiveTs) {
|
||||
// TODO: [TravisR] Figure out how to i18n this
|
||||
// `humanize` wants seconds for a timestamp, so divide by 1000
|
||||
const humanTs = humanize.relativeTime(this.props.lastActiveTs / 1000);
|
||||
timestamp = <span className='mx_DMInviteDialog_roomTile_time'>{humanTs}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx_DMInviteDialog_roomTile' onClick={this._onClick}>
|
||||
<MemberAvatar member={this.props.member} width={36} height={36} />
|
||||
<span className='mx_DMInviteDialog_roomTile_name'>{this.props.member.name}</span>
|
||||
<span className='mx_DMInviteDialog_roomTile_userId'>{this.props.member.userId}</span>
|
||||
{timestamp}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class DMInviteDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// Takes an array of user IDs/emails to invite.
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
targets: [], // string[] of mxids/email addresses
|
||||
filterText: "",
|
||||
recents: this._buildRecents(),
|
||||
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||
};
|
||||
}
|
||||
|
||||
_buildRecents(): {userId: string, user: RoomMember, lastActive: number} {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals();
|
||||
const recents = [];
|
||||
for (const userId in rooms) {
|
||||
const room = rooms[userId];
|
||||
const member = room.getMember(userId);
|
||||
if (!member) continue; // just skip people who don't have memberships for some reason
|
||||
|
||||
const lastEventTs = room.timeline && room.timeline.length
|
||||
? room.timeline[room.timeline.length - 1].getTs()
|
||||
: 0;
|
||||
if (!lastEventTs) continue; // something weird is going on with this room
|
||||
|
||||
recents.push({userId, user: member, lastActive: lastEventTs});
|
||||
}
|
||||
|
||||
// Sort the recents by last active to save us time later
|
||||
recents.sort((a, b) => b.lastActive - a.lastActive);
|
||||
|
||||
return recents;
|
||||
}
|
||||
|
||||
_startDm = () => {
|
||||
this.props.onFinished(this.state.targets);
|
||||
};
|
||||
|
||||
_cancel = () => {
|
||||
this.props.onFinished([]);
|
||||
};
|
||||
|
||||
_updateFilter = (e) => {
|
||||
this.setState({filterText: e.target.value});
|
||||
};
|
||||
|
||||
_showMoreRecents = () => {
|
||||
this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN});
|
||||
};
|
||||
|
||||
_toggleMember = (userId) => {
|
||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
const idx = targets.indexOf(userId);
|
||||
if (idx >= 0) targets.splice(idx, 1);
|
||||
else targets.push(userId);
|
||||
this.setState({targets});
|
||||
};
|
||||
|
||||
_renderRecents() {
|
||||
if (!this.state.recents || this.state.recents.length === 0) return null;
|
||||
|
||||
// .slice() will return an incomplete array but won't error on us if we go too far
|
||||
const toRender = this.state.recents.slice(0, this.state.numRecentsShown);
|
||||
const hasMore = toRender.length < this.state.recents.length;
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
let showMore = null;
|
||||
if (hasMore) {
|
||||
showMore = (
|
||||
<AccessibleButton onClick={this._showMoreRecents} kind="link">
|
||||
{_t("Show more")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
const tiles = toRender.map(r => (
|
||||
<DMRoomTile member={r.user} lastActiveTs={r.lastActive} key={r.userId} onToggle={this._toggleMember} />
|
||||
));
|
||||
return (
|
||||
<div className='mx_DMInviteDialog_section'>
|
||||
<h3>{_t("Recent Conversations")}</h3>
|
||||
{tiles}
|
||||
{showMore}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Field = sdk.getComponent("elements.Field");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
// Dev note: The use of Field is temporary/incomplete pending https://github.com/vector-im/riot-web/issues/11197
|
||||
// For now, we just list who the targets are.
|
||||
const editor = (
|
||||
<div className='mx_DMInviteDialog_editor'>
|
||||
<Field
|
||||
id="inviteTargets"
|
||||
value={this.state.filterText}
|
||||
onChange={this._updateFilter}
|
||||
placeholder="TODO: Implement filtering/searching (vector-im/riot-web#11199)"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const targets = this.state.targets.map(t => <div key={t}>{t}</div>);
|
||||
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_DMInviteDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this._cancel}
|
||||
title={_t("Direct Messages")}
|
||||
>
|
||||
<div className='mx_DMInviteDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
"If you can't find someone, ask them for their username, or share your " +
|
||||
"username (%(userId)s) or <a>profile link</a>.",
|
||||
{userId},
|
||||
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
|
||||
)}
|
||||
</p>
|
||||
{targets}
|
||||
<div className='mx_DMInviteDialog_addressBar'>
|
||||
{editor}
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this._startDm}
|
||||
className='mx_DMInviteDialog_goButton'
|
||||
>
|
||||
{_t("Go")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{this._renderRecents()}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -25,8 +25,8 @@ import * as Lifecycle from '../../../Lifecycle';
|
|||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class DeactivateAccountDialog extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onOk = this._onOk.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
|
|
|
@ -97,7 +97,7 @@ export default class DeviceVerifyDialog extends React.Component {
|
|||
const client = MatrixClientPeg.get();
|
||||
const verifyingOwnDevice = this.props.userId === client.getUserId();
|
||||
try {
|
||||
if (!verifyingOwnDevice && SettingsStore.getValue("feature_dm_verification")) {
|
||||
if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
|
||||
const roomId = await ensureDMExistsAndOpen(this.props.userId);
|
||||
// throws upon cancellation before having started
|
||||
this._verifier = await client.requestVerificationDM(
|
||||
|
|
|
@ -16,23 +16,19 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Room } from "matrix-js-sdk";
|
||||
|
||||
import sdk from '../../../index';
|
||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Field from "../elements/Field";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
class DevtoolsComponent extends React.Component {
|
||||
static contextTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
class GenericEditor extends DevtoolsComponent {
|
||||
class GenericEditor extends React.PureComponent {
|
||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onChange = this._onChange.bind(this);
|
||||
this.onBack = this.onBack.bind(this);
|
||||
}
|
||||
|
@ -67,12 +63,15 @@ class SendCustomEvent extends GenericEditor {
|
|||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
forceStateEvent: PropTypes.bool,
|
||||
inputs: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._send = this._send.bind(this);
|
||||
|
||||
const {eventType, stateKey, evContent} = Object.assign({
|
||||
|
@ -91,11 +90,11 @@ class SendCustomEvent extends GenericEditor {
|
|||
}
|
||||
|
||||
send(content) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cli = this.context;
|
||||
if (this.state.isStateEvent) {
|
||||
return cli.sendStateEvent(this.context.roomId, this.state.eventType, content, this.state.stateKey);
|
||||
return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey);
|
||||
} else {
|
||||
return cli.sendEvent(this.context.roomId, this.state.eventType, content);
|
||||
return cli.sendEvent(this.props.room.roomId, this.state.eventType, content);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,13 +153,16 @@ class SendAccountData extends GenericEditor {
|
|||
static getLabel() { return _t('Send Account Data'); }
|
||||
|
||||
static propTypes = {
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
isRoomAccountData: PropTypes.bool,
|
||||
forceMode: PropTypes.bool,
|
||||
inputs: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._send = this._send.bind(this);
|
||||
|
||||
const {eventType, evContent} = Object.assign({
|
||||
|
@ -177,9 +179,9 @@ class SendAccountData extends GenericEditor {
|
|||
}
|
||||
|
||||
send(content) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cli = this.context;
|
||||
if (this.state.isRoomAccountData) {
|
||||
return cli.setRoomAccountData(this.context.roomId, this.state.eventType, content);
|
||||
return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content);
|
||||
}
|
||||
return cli.setAccountData(this.state.eventType, content);
|
||||
}
|
||||
|
@ -234,7 +236,7 @@ class SendAccountData extends GenericEditor {
|
|||
const INITIAL_LOAD_TILES = 20;
|
||||
const LOAD_TILES_STEP_SIZE = 50;
|
||||
|
||||
class FilteredList extends React.Component {
|
||||
class FilteredList extends React.PureComponent {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
query: PropTypes.string,
|
||||
|
@ -247,8 +249,8 @@ class FilteredList extends React.Component {
|
|||
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query),
|
||||
|
@ -305,19 +307,20 @@ class FilteredList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
class RoomStateExplorer extends DevtoolsComponent {
|
||||
class RoomStateExplorer extends React.PureComponent {
|
||||
static getLabel() { return _t('Explore Room State'); }
|
||||
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
|
||||
this.roomStateEvents = room.currentState.events;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.roomStateEvents = this.props.room.currentState.events;
|
||||
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.editEv = this.editEv.bind(this);
|
||||
|
@ -373,7 +376,7 @@ class RoomStateExplorer extends DevtoolsComponent {
|
|||
render() {
|
||||
if (this.state.event) {
|
||||
if (this.state.editing) {
|
||||
return <SendCustomEvent forceStateEvent={true} onBack={this.onBack} inputs={{
|
||||
return <SendCustomEvent room={this.props.room} forceStateEvent={true} onBack={this.onBack} inputs={{
|
||||
eventType: this.state.event.getType(),
|
||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||
stateKey: this.state.event.getStateKey(),
|
||||
|
@ -442,15 +445,18 @@ class RoomStateExplorer extends DevtoolsComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class AccountDataExplorer extends DevtoolsComponent {
|
||||
class AccountDataExplorer extends React.PureComponent {
|
||||
static getLabel() { return _t('Explore Account Data'); }
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.editEv = this.editEv.bind(this);
|
||||
|
@ -467,11 +473,10 @@ class AccountDataExplorer extends DevtoolsComponent {
|
|||
}
|
||||
|
||||
getData() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (this.state.isRoomAccountData) {
|
||||
return cli.getRoom(this.context.roomId).accountData;
|
||||
return this.props.room.accountData;
|
||||
}
|
||||
return cli.store.accountData;
|
||||
return this.context.store.accountData;
|
||||
}
|
||||
|
||||
onViewSourceClick(event) {
|
||||
|
@ -505,10 +510,14 @@ class AccountDataExplorer extends DevtoolsComponent {
|
|||
render() {
|
||||
if (this.state.event) {
|
||||
if (this.state.editing) {
|
||||
return <SendAccountData isRoomAccountData={this.state.isRoomAccountData} onBack={this.onBack} inputs={{
|
||||
eventType: this.state.event.getType(),
|
||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||
}} forceMode={true} />;
|
||||
return <SendAccountData
|
||||
room={this.props.room}
|
||||
isRoomAccountData={this.state.isRoomAccountData}
|
||||
onBack={this.onBack}
|
||||
inputs={{
|
||||
eventType: this.state.event.getType(),
|
||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||
}} forceMode={true} />;
|
||||
}
|
||||
|
||||
return <div className="mx_ViewSource">
|
||||
|
@ -553,17 +562,20 @@ class AccountDataExplorer extends DevtoolsComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class ServersInRoomList extends DevtoolsComponent {
|
||||
class ServersInRoomList extends React.PureComponent {
|
||||
static getLabel() { return _t('View Servers in Room'); }
|
||||
|
||||
static propTypes = {
|
||||
onBack: PropTypes.func.isRequired,
|
||||
room: PropTypes.instanceOf(Room).isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const room = this.props.room;
|
||||
const servers = new Set();
|
||||
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
|
||||
this.servers = Array.from(servers).map(s =>
|
||||
|
@ -602,19 +614,14 @@ const Entries = [
|
|||
ServersInRoomList,
|
||||
];
|
||||
|
||||
export default class DevtoolsDialog extends React.Component {
|
||||
static childContextTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
// client: PropTypes.instanceOf(MatixClient),
|
||||
};
|
||||
|
||||
export default class DevtoolsDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onBack = this.onBack.bind(this);
|
||||
this.onCancel = this.onCancel.bind(this);
|
||||
|
||||
|
@ -627,10 +634,6 @@ export default class DevtoolsDialog extends React.Component {
|
|||
this._unmounted = true;
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return { roomId: this.props.roomId };
|
||||
}
|
||||
|
||||
_setMode(mode) {
|
||||
return () => {
|
||||
this.setState({ mode });
|
||||
|
@ -654,15 +657,17 @@ export default class DevtoolsDialog extends React.Component {
|
|||
let body;
|
||||
|
||||
if (this.state.mode) {
|
||||
body = <div>
|
||||
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
|
||||
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
|
||||
<div className="mx_DevTools_label_bottom" />
|
||||
<this.state.mode onBack={this.onBack} />
|
||||
</div>;
|
||||
body = <MatrixClientContext.Consumer>
|
||||
{(cli) => <React.Fragment>
|
||||
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
|
||||
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
|
||||
<div className="mx_DevTools_label_bottom" />
|
||||
<this.state.mode onBack={this.onBack} room={cli.getRoom(this.props.roomId)} />
|
||||
</React.Fragment>}
|
||||
</MatrixClientContext.Consumer>;
|
||||
} else {
|
||||
const classes = "mx_DevTools_RoomStateExplorer_button";
|
||||
body = <div>
|
||||
body = <React.Fragment>
|
||||
<div>
|
||||
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
|
||||
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
|
||||
|
@ -679,7 +684,7 @@ export default class DevtoolsDialog extends React.Component {
|
|||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onCancel}>{ _t('Cancel') }</button>
|
||||
</div>
|
||||
</div>;
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
|
|
@ -99,7 +99,7 @@ export default createReactClass({
|
|||
this.props.onFinished(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
},
|
||||
|
||||
_onShareClicked: function() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018, 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.
|
||||
|
@ -94,10 +95,14 @@ export default class LogoutDialog extends React.Component {
|
|||
// verified, so restore the backup which will give us the keys from it and
|
||||
// allow us to trust it (ie. upload keys to it)
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {});
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||
/* priority = */ false, /* static = */ true,
|
||||
);
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||
null, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -30,8 +30,8 @@ export default class ReportEventDialog extends PureComponent {
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
reason: "",
|
||||
|
|
|
@ -24,9 +24,11 @@ import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab";
|
|||
import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab";
|
||||
import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab";
|
||||
import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab";
|
||||
import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab";
|
||||
import sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default class RoomSettingsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -52,6 +54,9 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
|
||||
_getTabs() {
|
||||
const tabs = [];
|
||||
const featureFlag = SettingsStore.isFeatureEnabled("feature_bridge_state");
|
||||
const shouldShowBridgeIcon = featureFlag &&
|
||||
BridgeSettingsTab.getBridgeStateEvents(this.props.roomId).length > 0;
|
||||
|
||||
tabs.push(new Tab(
|
||||
_td("General"),
|
||||
|
@ -73,6 +78,15 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
"mx_RoomSettingsDialog_rolesIcon",
|
||||
<NotificationSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
|
||||
if (shouldShowBridgeIcon) {
|
||||
tabs.push(new Tab(
|
||||
_td("Bridge Info"),
|
||||
"mx_RoomSettingsDialog_bridgesIcon",
|
||||
<BridgeSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
}
|
||||
|
||||
tabs.push(new Tab(
|
||||
_td("Advanced"),
|
||||
"mx_RoomSettingsDialog_warningIcon",
|
||||
|
|
|
@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import { Key } from '../../../Keyboard';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||
|
||||
|
@ -62,8 +62,13 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._input_value = createRef();
|
||||
this._uiAuth = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.refs.input_value.select();
|
||||
this._input_value.current.select();
|
||||
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
},
|
||||
|
@ -96,14 +101,14 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
onKeyUp: function(ev) {
|
||||
if (ev.keyCode === KeyCode.ENTER) {
|
||||
if (ev.key === Key.ENTER) {
|
||||
this.onSubmit();
|
||||
}
|
||||
},
|
||||
|
||||
onSubmit: function(ev) {
|
||||
if (this.refs.uiAuth) {
|
||||
this.refs.uiAuth.tryContinue();
|
||||
if (this._uiAuth.current) {
|
||||
this._uiAuth.current.tryContinue();
|
||||
}
|
||||
this.setState({
|
||||
doingUIAuth: true,
|
||||
|
@ -215,7 +220,7 @@ export default createReactClass({
|
|||
onAuthFinished={this._onUIAuthFinished}
|
||||
inputs={{}}
|
||||
poll={true}
|
||||
ref="uiAuth"
|
||||
ref={this._uiAuth}
|
||||
continueIsManaged={true}
|
||||
/>;
|
||||
}
|
||||
|
@ -257,7 +262,7 @@ export default createReactClass({
|
|||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<div className="mx_SetMxIdDialog_input_group">
|
||||
<input type="text" ref="input_value" value={this.state.username}
|
||||
<input type="text" ref={this._input_value} value={this.state.username}
|
||||
autoFocus={true}
|
||||
onChange={this.onValueChange}
|
||||
onKeyUp={this.onKeyUp}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
|
@ -74,6 +74,8 @@ export default class ShareDialog extends React.Component {
|
|||
// MatrixEvent defaults to share linkSpecificEvent
|
||||
linkSpecificEvent: this.props.target instanceof MatrixEvent,
|
||||
};
|
||||
|
||||
this._link = createRef();
|
||||
}
|
||||
|
||||
static _selectText(target) {
|
||||
|
@ -94,7 +96,7 @@ export default class ShareDialog extends React.Component {
|
|||
onCopyClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
ShareDialog._selectText(this.refs.link);
|
||||
ShareDialog._selectText(this._link.current);
|
||||
|
||||
let successful;
|
||||
try {
|
||||
|
@ -106,7 +108,7 @@ export default class ShareDialog extends React.Component {
|
|||
const buttonRect = e.target.getBoundingClientRect();
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||
...toRightOf(buttonRect, 11),
|
||||
...toRightOf(buttonRect, 2),
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
// Drop a reference to this close handler for componentWillUnmount
|
||||
|
@ -195,7 +197,7 @@ export default class ShareDialog extends React.Component {
|
|||
>
|
||||
<div className="mx_ShareDialog_content">
|
||||
<div className="mx_ShareDialog_matrixto">
|
||||
<a ref="link"
|
||||
<a ref={this._link}
|
||||
href={matrixToUrl}
|
||||
onClick={ShareDialog.onLinkClick}
|
||||
className="mx_ShareDialog_matrixto_link"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
|
@ -42,15 +42,19 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._textinput = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.focus) {
|
||||
// Set the cursor at the end of the text input
|
||||
this.refs.textinput.value = this.props.value;
|
||||
this._textinput.current.value = this.props.value;
|
||||
}
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true, this.refs.textinput.value);
|
||||
this.props.onFinished(true, this._textinput.current.value);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
|
@ -70,7 +74,13 @@ export default createReactClass({
|
|||
<label htmlFor="textinput"> { this.props.description } </label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
|
||||
<input
|
||||
id="textinput"
|
||||
ref={this._textinput}
|
||||
className="mx_TextInputDialog_input"
|
||||
defaultValue={this.props.value}
|
||||
autoFocus={this.props.focus}
|
||||
size="64" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue