Merge branch 'develop' into hs/bridge-info
This commit is contained in:
commit
0a8cc416bf
203 changed files with 5331 additions and 2496 deletions
|
@ -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 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
|
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:
|
a 'skin'. A skin provides:
|
||||||
* Customised implementations of presentation components.
|
* Customised implementations of presentation components.
|
||||||
* Custom CSS
|
* 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)
|
'Stealing' styling information from other components (including parents)
|
||||||
is not cool, as it breaks the independence of the components.
|
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
|
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
|
uses the naming prefix "mx_". A company called Yoyodyne Inc might use a
|
||||||
prefix like "yy_" for its app-specific classes.
|
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
|
.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
|
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
|
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
|
* Components should render only within the bounding box of their outermost DOM
|
||||||
element. Page-absolute positioning and negative CSS margins and similar are
|
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
|
<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`
|
- 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):
|
- 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
|
- Think about whether your component really needs state: are you duplicating
|
||||||
information in component state that could be derived from the model?
|
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)
|
||||||
|
|
|
@ -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",
|
"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": "yarn reskindex && yarn start:init",
|
||||||
"build:watch": "babel src -w --skip-initial-build -d lib --source-maps --copy-files",
|
"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": "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: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",
|
"start:init": "babel src -d lib --source-maps --copy-files",
|
||||||
|
@ -75,7 +74,7 @@
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
"filesize": "3.5.6",
|
"filesize": "3.5.6",
|
||||||
"flux": "2.1.1",
|
"flux": "2.1.1",
|
||||||
"focus-trap-react": "^3.0.5",
|
"react-focus-lock": "^2.2.1",
|
||||||
"focus-visible": "^5.0.2",
|
"focus-visible": "^5.0.2",
|
||||||
"fuse.js": "^2.2.0",
|
"fuse.js": "^2.2.0",
|
||||||
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566",
|
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566",
|
||||||
|
@ -88,7 +87,7 @@
|
||||||
"linkifyjs": "^2.1.6",
|
"linkifyjs": "^2.1.6",
|
||||||
"lodash": "^4.17.14",
|
"lodash": "^4.17.14",
|
||||||
"lolex": "4.2",
|
"lolex": "4.2",
|
||||||
"matrix-js-sdk": "2.4.6",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"pako": "^1.0.5",
|
"pako": "^1.0.5",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
|
@ -110,6 +109,7 @@
|
||||||
"text-encoding-utf-8": "^1.0.1",
|
"text-encoding-utf-8": "^1.0.1",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
"velocity-animate": "^1.5.2",
|
"velocity-animate": "^1.5.2",
|
||||||
|
"what-input": "^5.2.6",
|
||||||
"whatwg-fetch": "^1.1.1",
|
"whatwg-fetch": "^1.1.1",
|
||||||
"zxcvbn": "^4.4.2"
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
@import "./structures/_SearchBox.scss";
|
@import "./structures/_SearchBox.scss";
|
||||||
@import "./structures/_TabbedView.scss";
|
@import "./structures/_TabbedView.scss";
|
||||||
@import "./structures/_TagPanel.scss";
|
@import "./structures/_TagPanel.scss";
|
||||||
@import "./structures/_TagPanelButtons.scss";
|
|
||||||
@import "./structures/_ToastContainer.scss";
|
@import "./structures/_ToastContainer.scss";
|
||||||
@import "./structures/_TopLeftMenuButton.scss";
|
@import "./structures/_TopLeftMenuButton.scss";
|
||||||
@import "./structures/_UploadBar.scss";
|
@import "./structures/_UploadBar.scss";
|
||||||
|
@ -64,7 +63,6 @@
|
||||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||||
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
||||||
@import "./views/dialogs/_RestoreKeyBackupDialog.scss";
|
|
||||||
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
||||||
@import "./views/dialogs/_RoomUpgradeDialog.scss";
|
@import "./views/dialogs/_RoomUpgradeDialog.scss";
|
||||||
@import "./views/dialogs/_RoomUpgradeWarningDialog.scss";
|
@import "./views/dialogs/_RoomUpgradeWarningDialog.scss";
|
||||||
|
@ -83,6 +81,8 @@
|
||||||
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
|
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
|
||||||
@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss";
|
@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss";
|
||||||
@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.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/directory/_NetworkDropdown.scss";
|
||||||
@import "./views/elements/_AccessibleButton.scss";
|
@import "./views/elements/_AccessibleButton.scss";
|
||||||
@import "./views/elements/_AddressSelector.scss";
|
@import "./views/elements/_AddressSelector.scss";
|
||||||
|
@ -173,7 +173,10 @@
|
||||||
@import "./views/rooms/_SendMessageComposer.scss";
|
@import "./views/rooms/_SendMessageComposer.scss";
|
||||||
@import "./views/rooms/_Stickers.scss";
|
@import "./views/rooms/_Stickers.scss";
|
||||||
@import "./views/rooms/_TopUnreadMessagesBar.scss";
|
@import "./views/rooms/_TopUnreadMessagesBar.scss";
|
||||||
|
@import "./views/rooms/_UserOnlineDot.scss";
|
||||||
@import "./views/rooms/_WhoIsTypingTile.scss";
|
@import "./views/rooms/_WhoIsTypingTile.scss";
|
||||||
|
@import "./views/settings/_AvatarSetting.scss";
|
||||||
|
@import "./views/settings/_CrossSigningPanel.scss";
|
||||||
@import "./views/settings/_DevicesPanel.scss";
|
@import "./views/settings/_DevicesPanel.scss";
|
||||||
@import "./views/settings/_EmailAddresses.scss";
|
@import "./views/settings/_EmailAddresses.scss";
|
||||||
@import "./views/settings/_IntegrationManager.scss";
|
@import "./views/settings/_IntegrationManager.scss";
|
||||||
|
|
|
@ -26,11 +26,16 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_CustomRoomTagPanel_scroller {
|
.mx_CustomRoomTagPanel_scroller {
|
||||||
max-height: inherit;
|
max-height: inherit;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CustomRoomTagPanel .mx_AccessibleButton {
|
.mx_CustomRoomTagPanel .mx_AccessibleButton {
|
||||||
margin: 9px auto;
|
margin: 0 auto;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
|
padding: 10px 0 9px 0;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CustomRoomTagPanel .mx_BaseAvatar_image {
|
.mx_CustomRoomTagPanel .mx_BaseAvatar_image {
|
||||||
|
@ -39,7 +44,13 @@ limitations under the License.
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CustomRoomTagPanel .mx_AccessibleButton.CustomRoomTagPanel_tileSelected .mx_BaseAvatar_image {
|
.mx_CustomRoomTagPanel .mx_AccessibleButton.CustomRoomTagPanel_tileSelected::before {
|
||||||
border: 3px solid $warning-color;
|
content: '';
|
||||||
border-radius: 40px;
|
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 {
|
.mx_TagPanel .mx_TagPanel_tagTileContainer > div {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
padding: 5px 0 4px 0;
|
padding: 10px 0 9px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel .mx_TagTile {
|
.mx_TagPanel .mx_TagTile {
|
||||||
|
@ -82,21 +82,39 @@ limitations under the License.
|
||||||
// opacity: 1;
|
// opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel .mx_TagTile.mx_TagTile_selected .mx_TagTile_avatar .mx_BaseAvatar {
|
.mx_TagPanel .mx_TagTile_plus {
|
||||||
background-color: $accent-color;
|
margin-bottom: 12px;
|
||||||
border-radius: 40px;
|
|
||||||
|
|
||||||
/* In case this is a "initial" avatar */
|
|
||||||
display: block;
|
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: 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 {
|
.mx_TagPanel .mx_TagTile.mx_TagTile_selected::before {
|
||||||
border: 3px solid $accent-color;
|
content: '';
|
||||||
height: 40px;
|
height: 56px;
|
||||||
width: 40px;
|
background-color: $accent-color;
|
||||||
box-sizing: border-box;
|
width: 5px;
|
||||||
|
position: absolute;
|
||||||
|
left: -15px;
|
||||||
|
border-radius: 0 3px 3px 0;
|
||||||
|
top: -8px; // (56 - 40)/2
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus {
|
.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 {
|
.mx_BaseAvatar_image {
|
||||||
|
object-fit: cover;
|
||||||
border-radius: 40px;
|
border-radius: 40px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
background-color: $avatar-bg-color;
|
background-color: $avatar-bg-color;
|
||||||
|
|
|
@ -53,6 +53,10 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/feather-customised/home.svg');
|
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 {
|
.mx_TopLeftMenu_icon_settings::after {
|
||||||
mask-image: url('$(res)/img/feather-customised/settings.svg');
|
mask-image: url('$(res)/img/feather-customised/settings.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,12 @@ limitations under the License.
|
||||||
padding-right: 80px;
|
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 {
|
.mx_RoomSettingsDialog_BridgeList {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -59,3 +65,4 @@ limitations under the License.
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-bottom: 1px solid $panel-divider-color;
|
border-bottom: 1px solid $panel-divider-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,6 +15,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.mx_RestoreKeyBackupDialog_keyStatus {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RestoreKeyBackupDialog_primaryContainer {
|
.mx_RestoreKeyBackupDialog_primaryContainer {
|
||||||
/* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
|
/* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
|
||||||
padding: 20px;
|
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 {
|
.mx_UserInfo_avatar {
|
||||||
margin: 24px 32px 0 32px;
|
margin: 24px 32px 0 32px;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserInfo_avatar > div {
|
.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),
|
that's why we had to put the margin to center on a parent div),
|
||||||
and not a % of the parent height. */
|
and not a % of the parent height. */
|
||||||
padding-top: 100%;
|
padding-top: 100%;
|
||||||
height: 0;
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_UserInfo_avatar > div > div * {
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
box-sizing: content-box;
|
position: absolute;
|
||||||
background-repeat: no-repeat;
|
top: 0;
|
||||||
background-size: cover;
|
left: 0;
|
||||||
background-position: center;
|
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 {
|
.mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image {
|
||||||
|
|
|
@ -22,7 +22,9 @@ limitations under the License.
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_E2EIcon_verified::after, .mx_E2EIcon_warning::after {
|
.mx_E2EIcon_warning::after,
|
||||||
|
.mx_E2EIcon_normal::after,
|
||||||
|
.mx_E2EIcon_verified::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -34,10 +36,14 @@ limitations under the License.
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_E2EIcon_verified::after {
|
|
||||||
background-image: url('$(res)/img/e2e/verified.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_E2EIcon_warning::after {
|
.mx_E2EIcon_warning::after {
|
||||||
background-image: url('$(res)/img/e2e/warning.svg');
|
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');
|
||||||
|
}
|
||||||
|
|
|
@ -169,6 +169,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_MessageActionBar,
|
.mx_EventTile:hover .mx_MessageActionBar,
|
||||||
.mx_EventTile.mx_EventTile_actionBarFocused .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 {
|
.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,12 +52,18 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LinkPreviewWidget_cancel {
|
.mx_LinkPreviewWidget_cancel {
|
||||||
visibility: hidden;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
|
||||||
|
img {
|
||||||
flex: 0 0 40px;
|
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;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -37,6 +37,10 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SearchBar_buttons {
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SearchBar_button {
|
.mx_SearchBar_button {
|
||||||
border: 0;
|
border: 0;
|
||||||
margin: 0 0 0 22px;
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_RestoreKeyBackupDialog_keyStatus {
|
.mx_UserOnlineDot {
|
||||||
height: 30px;
|
border-radius: 50%;
|
||||||
|
background-color: $accent-color;
|
||||||
|
height: 5px;
|
||||||
|
width: 5px;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
87
res/css/views/settings/_AvatarSetting.scss
Normal file
87
res/css/views/settings/_AvatarSetting.scss
Normal file
|
@ -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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
.mx_CrossSigningPanel_statusList {
|
||||||
import PropTypes from "prop-types";
|
border-spacing: 0;
|
||||||
import {MatrixClient} from "matrix-js-sdk";
|
|
||||||
|
|
||||||
// Higher Order Component to allow use of legacy MatrixClient React Context
|
td {
|
||||||
// in Functional Components which do not otherwise support legacy React Contexts
|
padding: 0;
|
||||||
export default (Component) => class extends React.PureComponent {
|
|
||||||
static contextTypes = {
|
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
&:first-of-type {
|
||||||
return <Component {...this.props} matrixClient={this.context.matrixClient} />;
|
padding-inline-end: 1em;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CrossSigningPanel_buttonRow {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -30,3 +31,7 @@ limitations under the License.
|
||||||
.mx_KeyBackupPanel_deviceName {
|
.mx_KeyBackupPanel_deviceName {
|
||||||
font-style: italic;
|
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 {
|
.mx_ProfileSettings_avatarUpload {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
5
res/img/feather-customised/image.svg
Normal file
5
res/img/feather-customised/image.svg
Normal file
|
@ -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 |
4
res/img/feather-customised/plus.svg
Normal file
4
res/img/feather-customised/plus.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="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 |
|
@ -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");
|
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
|
||||||
let uploadAll = false;
|
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) {
|
for (let i = 0; i < okFiles.length; ++i) {
|
||||||
const file = okFiles[i];
|
const file = okFiles[i];
|
||||||
if (!uploadAll) {
|
if (!uploadAll) {
|
||||||
|
@ -440,11 +443,11 @@ export default class ContentMessages {
|
||||||
});
|
});
|
||||||
if (!shouldContinue) break;
|
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 = {
|
const content = {
|
||||||
body: file.name || 'Attachment',
|
body: file.name || 'Attachment',
|
||||||
info: {
|
info: {
|
||||||
|
@ -517,7 +520,10 @@ export default class ContentMessages {
|
||||||
content.file = result.file;
|
content.file = result.file;
|
||||||
content.url = result.url;
|
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);
|
return matrixClient.sendMessage(roomId, content);
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
error = err;
|
error = err;
|
||||||
|
|
149
src/CrossSigningManager.js
Normal file
149
src/CrossSigningManager.js
Normal file
|
@ -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
|
||||||
|
*
|
||||||
|
* @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 MatrixClientPeg from './MatrixClientPeg';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
|
||||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
|
||||||
import EMOJIBASE_REGEX from 'emojibase-regex';
|
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||||
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
||||||
|
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
@ -58,8 +58,6 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||||
|
|
||||||
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
||||||
|
|
||||||
const VARIATION_SELECTOR = String.fromCharCode(0xFE0F);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Return true if the given string contains emoji
|
* Return true if the given string contains emoji
|
||||||
* Uses a much, much simpler regex than emojibase's so will give false
|
* 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);
|
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.
|
* Returns the shortcode for an emoji character.
|
||||||
*
|
*
|
||||||
|
@ -93,7 +76,7 @@ export function findEmojiData(char) {
|
||||||
* @return {String} The shortcode (such as :thumbup:)
|
* @return {String} The shortcode (such as :thumbup:)
|
||||||
*/
|
*/
|
||||||
export function unicodeToShortcode(char) {
|
export function unicodeToShortcode(char) {
|
||||||
const data = findEmojiData(char);
|
const data = getEmojiFromUnicode(char);
|
||||||
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
|
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +88,7 @@ export function unicodeToShortcode(char) {
|
||||||
*/
|
*/
|
||||||
export function shortcodeToUnicode(shortcode) {
|
export function shortcodeToUnicode(shortcode) {
|
||||||
shortcode = shortcode.slice(1, shortcode.length - 1);
|
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;
|
return data ? data.unicode : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
Copyright 2017 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,52 +16,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 = {
|
export const Key = {
|
||||||
HOME: "Home",
|
HOME: "Home",
|
||||||
END: "End",
|
END: "End",
|
||||||
|
@ -80,13 +35,35 @@ export const Key = {
|
||||||
SHIFT: "Shift",
|
SHIFT: "Shift",
|
||||||
CONTEXT_MENU: "ContextMenu",
|
CONTEXT_MENU: "ContextMenu",
|
||||||
|
|
||||||
|
COMMA: ",",
|
||||||
LESS_THAN: "<",
|
LESS_THAN: "<",
|
||||||
GREATER_THAN: ">",
|
GREATER_THAN: ">",
|
||||||
BACKTICK: "`",
|
BACKTICK: "`",
|
||||||
SPACE: " ",
|
SPACE: " ",
|
||||||
|
A: "a",
|
||||||
B: "b",
|
B: "b",
|
||||||
|
C: "c",
|
||||||
|
D: "d",
|
||||||
|
E: "e",
|
||||||
|
F: "f",
|
||||||
|
G: "g",
|
||||||
|
H: "h",
|
||||||
I: "i",
|
I: "i",
|
||||||
|
J: "j",
|
||||||
K: "k",
|
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",
|
Y: "y",
|
||||||
Z: "z",
|
Z: "z",
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -30,6 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
|
||||||
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
|
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
|
||||||
import * as StorageManager from './utils/StorageManager';
|
import * as StorageManager from './utils/StorageManager';
|
||||||
import IdentityAuthClient from './IdentityAuthClient';
|
import IdentityAuthClient from './IdentityAuthClient';
|
||||||
|
import { crossSigningCallbacks } from './CrossSigningManager';
|
||||||
|
|
||||||
interface MatrixClientCreds {
|
interface MatrixClientCreds {
|
||||||
homeserverUrl: string,
|
homeserverUrl: string,
|
||||||
|
@ -220,14 +222,9 @@ class MatrixClientPeg {
|
||||||
identityServer: new IdentityAuthClient(),
|
identityServer: new IdentityAuthClient(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
opts.cryptoCallbacks = {};
|
||||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
// TODO: Cross-signing keys are temporarily in memory only. A
|
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks);
|
||||||
// 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),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.matrixClient = createMatrixClient(opts);
|
this.matrixClient = createMatrixClient(opts);
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { _t } from './languageHandler';
|
||||||
import * as Roles from './Roles';
|
import * as Roles from './Roles';
|
||||||
import {isValid3pidInvite} from "./RoomInvite";
|
import {isValid3pidInvite} from "./RoomInvite";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
|
||||||
|
|
||||||
function textForMemberEvent(ev) {
|
function textForMemberEvent(ev) {
|
||||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||||
|
@ -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 = {
|
const handlers = {
|
||||||
'm.room.message': textForMessageEvent,
|
'm.room.message': textForMessageEvent,
|
||||||
'm.call.invite': textForCallInviteEvent,
|
'm.call.invite': textForCallInviteEvent,
|
||||||
|
@ -533,6 +615,11 @@ const stateHandlers = {
|
||||||
'im.vector.modular.widgets': textForWidgetEvent,
|
'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 = {
|
module.exports = {
|
||||||
textForEvent: function(ev) {
|
textForEvent: function(ev) {
|
||||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {Key} from "../../../Keyboard";
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
@ -83,7 +85,7 @@ module.exports = createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyDown: function(e) {
|
onKeyDown: function(e) {
|
||||||
if (e.keyCode === 27) { // escape
|
if (e.key === Key.ESCAPE) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -44,6 +44,9 @@ export default createReactClass({
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
|
|
||||||
|
this._passphrase1 = createRef();
|
||||||
|
this._passphrase2 = createRef();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -53,8 +56,8 @@ export default createReactClass({
|
||||||
_onPassphraseFormSubmit: function(ev) {
|
_onPassphraseFormSubmit: function(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
const passphrase = this.refs.passphrase1.value;
|
const passphrase = this._passphrase1.current.value;
|
||||||
if (passphrase !== this.refs.passphrase2.value) {
|
if (passphrase !== this._passphrase2.current.value) {
|
||||||
this.setState({errStr: _t('Passphrases must match')});
|
this.setState({errStr: _t('Passphrases must match')});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -148,7 +151,7 @@ export default createReactClass({
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
<input ref='passphrase1' id='passphrase1'
|
<input ref={this._passphrase1} id='passphrase1'
|
||||||
autoFocus={true} size='64' type='password'
|
autoFocus={true} size='64' type='password'
|
||||||
disabled={disableForm}
|
disabled={disableForm}
|
||||||
/>
|
/>
|
||||||
|
@ -161,7 +164,7 @@ export default createReactClass({
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
<input ref='passphrase2' id='passphrase2'
|
<input ref={this._passphrase2} id='passphrase2'
|
||||||
size='64' type='password'
|
size='64' type='password'
|
||||||
disabled={disableForm}
|
disabled={disableForm}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
|
|
||||||
|
@ -56,6 +56,9 @@ export default createReactClass({
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
|
|
||||||
|
this._file = createRef();
|
||||||
|
this._passphrase = createRef();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -63,15 +66,15 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_onFormChange: function(ev) {
|
_onFormChange: function(ev) {
|
||||||
const files = this.refs.file.files || [];
|
const files = this._file.current.files || [];
|
||||||
this.setState({
|
this.setState({
|
||||||
enableSubmit: (this.refs.passphrase.value !== "" && files.length > 0),
|
enableSubmit: (this._passphrase.current.value !== "" && files.length > 0),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onFormSubmit: function(ev) {
|
_onFormSubmit: function(ev) {
|
||||||
ev.preventDefault();
|
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;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -146,7 +149,10 @@ export default createReactClass({
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
<input ref='file' id='importFile' type='file'
|
<input
|
||||||
|
ref={this._file}
|
||||||
|
id='importFile'
|
||||||
|
type='file'
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
onChange={this._onFormChange}
|
onChange={this._onFormChange}
|
||||||
disabled={disableForm} />
|
disabled={disableForm} />
|
||||||
|
@ -159,8 +165,11 @@ export default createReactClass({
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_E2eKeysDialog_inputCell'>
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
<input ref='passphrase' id='passphrase'
|
<input
|
||||||
size='64' type='password'
|
ref={this._passphrase}
|
||||||
|
id='passphrase'
|
||||||
|
size='64'
|
||||||
|
type='password'
|
||||||
onChange={this._onFormChange}
|
onChange={this._onFormChange}
|
||||||
disabled={disableForm} />
|
disabled={disableForm} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018, 2019 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,13 +16,11 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import FileSaver from 'file-saver';
|
||||||
|
|
||||||
import sdk from '../../../../index';
|
import sdk from '../../../../index';
|
||||||
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
||||||
import { scorePassword } from '../../../../utils/PasswordScorer';
|
import { scorePassword } from '../../../../utils/PasswordScorer';
|
||||||
|
|
||||||
import FileSaver from 'file-saver';
|
|
||||||
|
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
|
|
||||||
const PHASE_PASSPHRASE = 0;
|
const PHASE_PASSPHRASE = 0;
|
||||||
|
@ -45,13 +44,15 @@ function selectText(target) {
|
||||||
selection.addRange(range);
|
selection.addRange(range);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Walks the user through the process of creating an e2e key backup
|
* Walks the user through the process of creating an e2e key backup
|
||||||
* on the server.
|
* on the server.
|
||||||
*/
|
*/
|
||||||
export default createReactClass({
|
export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
getInitialState: function() {
|
constructor(props) {
|
||||||
return {
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
phase: PHASE_PASSPHRASE,
|
phase: PHASE_PASSPHRASE,
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
passPhraseConfirm: '',
|
passPhraseConfirm: '',
|
||||||
|
@ -60,25 +61,25 @@ export default createReactClass({
|
||||||
zxcvbnResult: null,
|
zxcvbnResult: null,
|
||||||
setPassPhrase: false,
|
setPassPhrase: false,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount() {
|
||||||
this._recoveryKeyNode = null;
|
this._recoveryKeyNode = null;
|
||||||
this._keyBackupInfo = null;
|
this._keyBackupInfo = null;
|
||||||
this._setZxcvbnResultTimeout = null;
|
this._setZxcvbnResultTimeout = null;
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount() {
|
||||||
if (this._setZxcvbnResultTimeout !== null) {
|
if (this._setZxcvbnResultTimeout !== null) {
|
||||||
clearTimeout(this._setZxcvbnResultTimeout);
|
clearTimeout(this._setZxcvbnResultTimeout);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_collectRecoveryKeyNode: function(n) {
|
_collectRecoveryKeyNode = (n) => {
|
||||||
this._recoveryKeyNode = n;
|
this._recoveryKeyNode = n;
|
||||||
},
|
}
|
||||||
|
|
||||||
_onCopyClick: function() {
|
_onCopyClick = () => {
|
||||||
selectText(this._recoveryKeyNode);
|
selectText(this._recoveryKeyNode);
|
||||||
const successful = document.execCommand('copy');
|
const successful = document.execCommand('copy');
|
||||||
if (successful) {
|
if (successful) {
|
||||||
|
@ -87,9 +88,9 @@ export default createReactClass({
|
||||||
phase: PHASE_KEEPITSAFE,
|
phase: PHASE_KEEPITSAFE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_onDownloadClick: function() {
|
_onDownloadClick = () => {
|
||||||
const blob = new Blob([this._keyBackupInfo.recovery_key], {
|
const blob = new Blob([this._keyBackupInfo.recovery_key], {
|
||||||
type: 'text/plain;charset=us-ascii',
|
type: 'text/plain;charset=us-ascii',
|
||||||
});
|
});
|
||||||
|
@ -99,9 +100,9 @@ export default createReactClass({
|
||||||
downloaded: true,
|
downloaded: true,
|
||||||
phase: PHASE_KEEPITSAFE,
|
phase: PHASE_KEEPITSAFE,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_createBackup: async function() {
|
_createBackup = async () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_BACKINGUP,
|
phase: PHASE_BACKINGUP,
|
||||||
error: null,
|
error: null,
|
||||||
|
@ -116,7 +117,7 @@ export default createReactClass({
|
||||||
phase: PHASE_DONE,
|
phase: PHASE_DONE,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} 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
|
// TODO: If creating a version succeeds, but backup fails, should we
|
||||||
// delete the version, disable backup, or do nothing? If we just
|
// delete the version, disable backup, or do nothing? If we just
|
||||||
// disable without deleting, we'll enable on next app reload since
|
// disable without deleting, we'll enable on next app reload since
|
||||||
|
@ -128,38 +129,38 @@ export default createReactClass({
|
||||||
error: e,
|
error: e,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_onCancel: function() {
|
_onCancel = () => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
},
|
}
|
||||||
|
|
||||||
_onDone: function() {
|
_onDone = () => {
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
},
|
}
|
||||||
|
|
||||||
_onOptOutClick: function() {
|
_onOptOutClick = () => {
|
||||||
this.setState({phase: PHASE_OPTOUT_CONFIRM});
|
this.setState({phase: PHASE_OPTOUT_CONFIRM});
|
||||||
},
|
}
|
||||||
|
|
||||||
_onSetUpClick: function() {
|
_onSetUpClick = () => {
|
||||||
this.setState({phase: PHASE_PASSPHRASE});
|
this.setState({phase: PHASE_PASSPHRASE});
|
||||||
},
|
}
|
||||||
|
|
||||||
_onSkipPassPhraseClick: async function() {
|
_onSkipPassPhraseClick = async () => {
|
||||||
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
|
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion();
|
||||||
this.setState({
|
this.setState({
|
||||||
copied: false,
|
copied: false,
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
phase: PHASE_SHOWKEY,
|
phase: PHASE_SHOWKEY,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_onPassPhraseNextClick: function() {
|
_onPassPhraseNextClick = () => {
|
||||||
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
||||||
},
|
}
|
||||||
|
|
||||||
_onPassPhraseKeyPress: async function(e) {
|
_onPassPhraseKeyPress = async (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
// If we're waiting for the timeout before updating the result at this point,
|
// 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
|
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
|
||||||
|
@ -177,9 +178,9 @@ export default createReactClass({
|
||||||
this._onPassPhraseNextClick();
|
this._onPassPhraseNextClick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_onPassPhraseConfirmNextClick: async function() {
|
_onPassPhraseConfirmNextClick = async () => {
|
||||||
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
|
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
|
||||||
this.setState({
|
this.setState({
|
||||||
setPassPhrase: true,
|
setPassPhrase: true,
|
||||||
|
@ -187,30 +188,30 @@ export default createReactClass({
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
phase: PHASE_SHOWKEY,
|
phase: PHASE_SHOWKEY,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_onPassPhraseConfirmKeyPress: function(e) {
|
_onPassPhraseConfirmKeyPress = (e) => {
|
||||||
if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) {
|
if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) {
|
||||||
this._onPassPhraseConfirmNextClick();
|
this._onPassPhraseConfirmNextClick();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_onSetAgainClick: function() {
|
_onSetAgainClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
passPhraseConfirm: '',
|
passPhraseConfirm: '',
|
||||||
phase: PHASE_PASSPHRASE,
|
phase: PHASE_PASSPHRASE,
|
||||||
zxcvbnResult: null,
|
zxcvbnResult: null,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_onKeepItSafeBackClick: function() {
|
_onKeepItSafeBackClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_SHOWKEY,
|
phase: PHASE_SHOWKEY,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_onPassPhraseChange: function(e) {
|
_onPassPhraseChange = (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
passPhrase: e.target.value,
|
passPhrase: e.target.value,
|
||||||
});
|
});
|
||||||
|
@ -227,19 +228,19 @@ export default createReactClass({
|
||||||
zxcvbnResult: scorePassword(this.state.passPhrase),
|
zxcvbnResult: scorePassword(this.state.passPhrase),
|
||||||
});
|
});
|
||||||
}, PASSPHRASE_FEEDBACK_DELAY);
|
}, PASSPHRASE_FEEDBACK_DELAY);
|
||||||
},
|
}
|
||||||
|
|
||||||
_onPassPhraseConfirmChange: function(e) {
|
_onPassPhraseConfirmChange = (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
passPhraseConfirm: e.target.value,
|
passPhraseConfirm: e.target.value,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_passPhraseIsValid: function() {
|
_passPhraseIsValid() {
|
||||||
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
|
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
|
||||||
},
|
}
|
||||||
|
|
||||||
_renderPhasePassPhrase: function() {
|
_renderPhasePassPhrase() {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
||||||
let strengthMeter;
|
let strengthMeter;
|
||||||
|
@ -266,7 +267,7 @@ export default createReactClass({
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t(
|
<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> },
|
{ b: sub => <b>{sub}</b> },
|
||||||
)}</p>
|
)}</p>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
|
@ -305,9 +306,9 @@ export default createReactClass({
|
||||||
</button></p>
|
</button></p>
|
||||||
</details>
|
</details>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
}
|
||||||
|
|
||||||
_renderPhasePassPhraseConfirm: function() {
|
_renderPhasePassPhraseConfirm() {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
|
||||||
let matchText;
|
let matchText;
|
||||||
|
@ -361,9 +362,9 @@ export default createReactClass({
|
||||||
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
}
|
||||||
|
|
||||||
_renderPhaseShowKey: function() {
|
_renderPhaseShowKey() {
|
||||||
let bodyText;
|
let bodyText;
|
||||||
if (this.state.setPassPhrase) {
|
if (this.state.setPassPhrase) {
|
||||||
bodyText = _t(
|
bodyText = _t(
|
||||||
|
@ -380,7 +381,7 @@ export default createReactClass({
|
||||||
"access to your encrypted messages if you forget your passphrase.",
|
"access to your encrypted messages if you forget your passphrase.",
|
||||||
)}</p>
|
)}</p>
|
||||||
<p>{_t(
|
<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>
|
||||||
<p>{bodyText}</p>
|
<p>{bodyText}</p>
|
||||||
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||||
|
@ -402,18 +403,18 @@ export default createReactClass({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
}
|
||||||
|
|
||||||
_renderPhaseKeepItSafe: function() {
|
_renderPhaseKeepItSafe() {
|
||||||
let introText;
|
let introText;
|
||||||
if (this.state.copied) {
|
if (this.state.copied) {
|
||||||
introText = _t(
|
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>},
|
{}, {b: s => <b>{s}</b>},
|
||||||
);
|
);
|
||||||
} else if (this.state.downloaded) {
|
} else if (this.state.downloaded) {
|
||||||
introText = _t(
|
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>},
|
{}, {b: s => <b>{s}</b>},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -431,16 +432,16 @@ export default createReactClass({
|
||||||
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
|
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
|
||||||
</DialogButtons>
|
</DialogButtons>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
}
|
||||||
|
|
||||||
_renderBusyPhase: function(text) {
|
_renderBusyPhase(text) {
|
||||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||||
return <div>
|
return <div>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>;
|
</div>;
|
||||||
},
|
}
|
||||||
|
|
||||||
_renderPhaseDone: function() {
|
_renderPhaseDone() {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
|
@ -451,9 +452,9 @@ export default createReactClass({
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
}
|
||||||
|
|
||||||
_renderPhaseOptOutConfirm: function() {
|
_renderPhaseOptOutConfirm() {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <div>
|
return <div>
|
||||||
{_t(
|
{_t(
|
||||||
|
@ -467,9 +468,9 @@ export default createReactClass({
|
||||||
<button onClick={this._onCancel}>I understand, continue without</button>
|
<button onClick={this._onCancel}>I understand, continue without</button>
|
||||||
</DialogButtons>
|
</DialogButtons>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
}
|
||||||
|
|
||||||
_titleForPhase: function(phase) {
|
_titleForPhase(phase) {
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
case PHASE_PASSPHRASE:
|
case PHASE_PASSPHRASE:
|
||||||
return _t('Secure your backup with a passphrase');
|
return _t('Secure your backup with a passphrase');
|
||||||
|
@ -488,9 +489,9 @@ export default createReactClass({
|
||||||
default:
|
default:
|
||||||
return _t("Create Key Backup");
|
return _t("Create Key Backup");
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
render: function() {
|
render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
|
@ -543,5 +544,5 @@ export default createReactClass({
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
|
|
@ -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 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2017, 2018 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -28,7 +29,7 @@ import SettingsStore from "../settings/SettingsStore";
|
||||||
import { shortcodeToUnicode } from '../HtmlUtils';
|
import { shortcodeToUnicode } from '../HtmlUtils';
|
||||||
|
|
||||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||||
import EmojiData from '../stripped-emoji.json';
|
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||||
|
|
||||||
const LIMIT = 20;
|
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.
|
// 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
|
// all it means is that we end up bloating the bundle with precomputed stuff
|
||||||
// which would be trivial to calculate and cache on demand.
|
// which would be trivial to calculate and cache on demand.
|
||||||
const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort(
|
const EMOJI_SHORTNAMES = EMOJIBASE.sort((a, b) => {
|
||||||
(a, b) => {
|
if (a.group === b.group) {
|
||||||
if (a.category === b.category) {
|
return a.order - b.order;
|
||||||
return a.emoji_order - b.emoji_order;
|
|
||||||
}
|
}
|
||||||
return a.category - b.category;
|
return a.group - b.group;
|
||||||
},
|
}).map((emoji, index) => {
|
||||||
).map((a, index) => {
|
|
||||||
return {
|
return {
|
||||||
name: a.name,
|
emoji,
|
||||||
shortname: a.shortname,
|
shortname: `:${emoji.shortcodes[0]}:`,
|
||||||
aliases: a.aliases ? a.aliases.join(' ') : '',
|
|
||||||
aliases_ascii: a.aliases_ascii ? a.aliases_ascii.join(' ') : '',
|
|
||||||
// Include the index so that we can preserve the original order
|
// Include the index so that we can preserve the original order
|
||||||
_orderBy: index,
|
_orderBy: index,
|
||||||
};
|
};
|
||||||
|
@ -69,12 +66,15 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(EMOJI_REGEX);
|
super(EMOJI_REGEX);
|
||||||
this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, {
|
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
|
// For matching against ascii equivalents
|
||||||
shouldMatchWordsOnly: false,
|
shouldMatchWordsOnly: false,
|
||||||
});
|
});
|
||||||
this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, {
|
this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, {
|
||||||
keys: ['name'],
|
keys: ['emoji.annotation'],
|
||||||
// For removing punctuation
|
// For removing punctuation
|
||||||
shouldMatchWordsOnly: true,
|
shouldMatchWordsOnly: true,
|
||||||
});
|
});
|
||||||
|
@ -96,7 +96,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
const sorters = [];
|
const sorters = [];
|
||||||
// make sure that emoticons come first
|
// 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)
|
// then sort by score (Infinity if matchedString not in shortname)
|
||||||
sorters.push((c) => score(matchedString, c.shortname));
|
sorters.push((c) => score(matchedString, c.shortname));
|
||||||
|
@ -110,8 +110,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
sorters.push((c) => c._orderBy);
|
sorters.push((c) => c._orderBy);
|
||||||
completions = _sortBy(_uniq(completions), sorters);
|
completions = _sortBy(_uniq(completions), sorters);
|
||||||
|
|
||||||
completions = completions.map((result) => {
|
completions = completions.map(({shortname}) => {
|
||||||
const { shortname } = result;
|
|
||||||
const unicode = shortcodeToUnicode(shortname);
|
const unicode = shortcodeToUnicode(shortname);
|
||||||
return {
|
return {
|
||||||
completion: unicode,
|
completion: unicode,
|
||||||
|
|
|
@ -71,6 +71,7 @@ export default class QueryMatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const keyValue of keyValues) {
|
for (const keyValue of keyValues) {
|
||||||
|
if (!keyValue) continue; // skip falsy keyValues
|
||||||
const key = stripDiacritics(keyValue).toLowerCase();
|
const key = stripDiacritics(keyValue).toLowerCase();
|
||||||
if (!this._items.has(key)) {
|
if (!this._items.has(key)) {
|
||||||
this._items.set(key, []);
|
this._items.set(key, []);
|
||||||
|
|
|
@ -70,10 +70,13 @@ export class ContextMenu extends React.Component {
|
||||||
|
|
||||||
// on resize callback
|
// on resize callback
|
||||||
windowResize: PropTypes.func,
|
windowResize: PropTypes.func,
|
||||||
|
|
||||||
|
managed: PropTypes.bool, // whether this context menu should be focus managed. If false it must handle itself
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
hasBackground: true,
|
hasBackground: true,
|
||||||
|
managed: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -183,6 +186,15 @@ export class ContextMenu extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
_onKeyDown = (ev) => {
|
_onKeyDown = (ev) => {
|
||||||
|
if (!this.props.managed) {
|
||||||
|
if (ev.key === Key.ESCAPE) {
|
||||||
|
this.props.onFinished();
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let handled = true;
|
let handled = true;
|
||||||
|
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
|
@ -313,7 +325,7 @@ export class ContextMenu extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
|
<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 }
|
{ chevron }
|
||||||
{ props.children }
|
{ props.children }
|
||||||
</div>
|
</div>
|
||||||
|
@ -411,7 +423,7 @@ export const toRightOf = (elementRect, chevronOffset=12) => {
|
||||||
const left = elementRect.right + window.pageXOffset + 3;
|
const left = elementRect.right + window.pageXOffset + 3;
|
||||||
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
||||||
top -= chevronOffset + 8; // where 8 is half the height of the chevron
|
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
|
// 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 {
|
class CustomRoomTagTile extends React.Component {
|
||||||
constructor(props) {
|
onClick = () => {
|
||||||
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() {
|
|
||||||
dis.dispatch({action: 'select_custom_room_tag', tag: this.props.tag.name});
|
dis.dispatch({action: 'select_custom_room_tag', tag: this.props.tag.name});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
|
||||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
|
||||||
|
|
||||||
const tag = this.props.tag;
|
const tag = this.props.tag;
|
||||||
const avatarHeight = 40;
|
const avatarHeight = 40;
|
||||||
|
@ -102,12 +85,9 @@ class CustomRoomTagTile extends React.Component {
|
||||||
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
|
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tip = (this.state.hover ?
|
|
||||||
<Tooltip className="mx_TagTile_tooltip" label={name} /> :
|
|
||||||
<div />);
|
|
||||||
return (
|
return (
|
||||||
<AccessibleButton className={className} onClick={this.onClick}>
|
<AccessibleTooltipButton className={className} onClick={this.onClick} title={name}>
|
||||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
<div className="mx_TagTile_avatar">
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
name={tag.avatarLetter}
|
name={tag.avatarLetter}
|
||||||
idName={name}
|
idName={name}
|
||||||
|
@ -115,9 +95,8 @@ class CustomRoomTagTile extends React.Component {
|
||||||
height={avatarHeight}
|
height={avatarHeight}
|
||||||
/>
|
/>
|
||||||
{ badgeElement }
|
{ badgeElement }
|
||||||
{ tip }
|
|
||||||
</div>
|
</div>
|
||||||
</AccessibleButton>
|
</AccessibleTooltipButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,8 +26,8 @@ import sanitizeHtml from 'sanitize-html';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
export default class EmbeddedPage extends React.PureComponent {
|
export default class EmbeddedPage extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -39,9 +39,7 @@ export default class EmbeddedPage extends React.PureComponent {
|
||||||
scrollbar: PropTypes.bool,
|
scrollbar: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextTypes = {
|
static contextType = MatrixClientContext;
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -104,7 +102,7 @@ export default class EmbeddedPage extends React.PureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// HACK: Workaround for the context's MatrixClient not updating.
|
// 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 isGuest = client ? client.isGuest() : true;
|
||||||
const className = this.props.className;
|
const className = this.props.className;
|
||||||
const classes = classnames({
|
const classes = classnames({
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||||
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
|
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
|
||||||
import {Group} from "matrix-js-sdk";
|
import {Group} from "matrix-js-sdk";
|
||||||
import {allSettled, sleep} from "../../utils/promise";
|
import {allSettled, sleep} from "../../utils/promise";
|
||||||
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
|
|
||||||
const LONG_DESC_PLACEHOLDER = _td(
|
const LONG_DESC_PLACEHOLDER = _td(
|
||||||
`<h1>HTML for your community's page</h1>
|
`<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() {
|
_onEditClick: function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
editing: true,
|
editing: true,
|
||||||
|
@ -583,6 +580,10 @@ export default createReactClass({
|
||||||
profileForm: null,
|
profileForm: null,
|
||||||
});
|
});
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1214,7 +1215,7 @@ export default createReactClass({
|
||||||
|
|
||||||
const EditableText = sdk.getComponent("elements.EditableText");
|
const EditableText = sdk.getComponent("elements.EditableText");
|
||||||
|
|
||||||
nameNode = <EditableText ref="nameEditor"
|
nameNode = <EditableText
|
||||||
className="mx_GroupView_editable"
|
className="mx_GroupView_editable"
|
||||||
placeholderClassName="mx_GroupView_placeholder"
|
placeholderClassName="mx_GroupView_placeholder"
|
||||||
placeholder={_t('Community Name')}
|
placeholder={_t('Community Name')}
|
||||||
|
@ -1224,7 +1225,7 @@ export default createReactClass({
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
dir="auto" />;
|
dir="auto" />;
|
||||||
|
|
||||||
shortDescNode = <EditableText ref="descriptionEditor"
|
shortDescNode = <EditableText
|
||||||
className="mx_GroupView_editable"
|
className="mx_GroupView_editable"
|
||||||
placeholderClassName="mx_GroupView_placeholder"
|
placeholderClassName="mx_GroupView_placeholder"
|
||||||
placeholder={_t("Description")}
|
placeholder={_t("Description")}
|
||||||
|
@ -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 = {
|
const headerClasses = {
|
||||||
"mx_GroupView_header": true,
|
"mx_GroupView_header": true,
|
||||||
|
@ -1326,9 +1329,9 @@ export default createReactClass({
|
||||||
<div className="mx_GroupView_header_rightCol">
|
<div className="mx_GroupView_header_rightCol">
|
||||||
{ rightButtons }
|
{ rightButtons }
|
||||||
</div>
|
</div>
|
||||||
<GroupHeaderButtons collapsedRhs={this.props.collapsedRhs} />
|
<GroupHeaderButtons />
|
||||||
</div>
|
</div>
|
||||||
<MainSplit collapsedRhs={this.props.collapsedRhs} panel={rightPanel}>
|
<MainSplit panel={rightPanel}>
|
||||||
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
||||||
{ this._getMembershipSection() }
|
{ this._getMembershipSection() }
|
||||||
{ this._getGroupSection() }
|
{ this._getGroupSection() }
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
const InteractiveAuth = Matrix.InteractiveAuth;
|
const InteractiveAuth = Matrix.InteractiveAuth;
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
@ -129,6 +129,8 @@ export default createReactClass({
|
||||||
this._authLogic.poll();
|
this._authLogic.poll();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._stageComponent = createRef();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -153,8 +155,8 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
tryContinue: function() {
|
tryContinue: function() {
|
||||||
if (this.refs.stageComponent && this.refs.stageComponent.tryContinue) {
|
if (this._stageComponent.current && this._stageComponent.current.tryContinue) {
|
||||||
this.refs.stageComponent.tryContinue();
|
this._stageComponent.current.tryContinue();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -192,8 +194,8 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_setFocus: function() {
|
_setFocus: function() {
|
||||||
if (this.refs.stageComponent && this.refs.stageComponent.focus) {
|
if (this._stageComponent.current && this._stageComponent.current.focus) {
|
||||||
this.refs.stageComponent.focus();
|
this._stageComponent.current.focus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -214,7 +216,8 @@ export default createReactClass({
|
||||||
|
|
||||||
const StageComponent = getEntryComponentForLoginType(stage);
|
const StageComponent = getEntryComponentForLoginType(stage);
|
||||||
return (
|
return (
|
||||||
<StageComponent ref="stageComponent"
|
<StageComponent
|
||||||
|
ref={this._stageComponent}
|
||||||
loginType={stage}
|
loginType={stage}
|
||||||
matrixClient={this.props.matrixClient}
|
matrixClient={this.props.matrixClient}
|
||||||
authSessionId={this._authLogic.getSessionId()}
|
authSessionId={this._authLogic.getSessionId()}
|
||||||
|
|
|
@ -19,12 +19,10 @@ import React from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
|
||||||
import { Key } from '../../Keyboard';
|
import { Key } from '../../Keyboard';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
import VectorConferenceHandler from '../../VectorConferenceHandler';
|
import VectorConferenceHandler from '../../VectorConferenceHandler';
|
||||||
import TagPanelButtons from './TagPanelButtons';
|
|
||||||
import SettingsStore from '../../settings/SettingsStore';
|
import SettingsStore from '../../settings/SettingsStore';
|
||||||
import {_t} from "../../languageHandler";
|
import {_t} from "../../languageHandler";
|
||||||
import Analytics from "../../Analytics";
|
import Analytics from "../../Analytics";
|
||||||
|
@ -39,10 +37,6 @@ const LeftPanel = createReactClass({
|
||||||
collapsed: PropTypes.bool.isRequired,
|
collapsed: PropTypes.bool.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
contextTypes: {
|
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
searchFilter: '',
|
searchFilter: '',
|
||||||
|
@ -243,7 +237,6 @@ const LeftPanel = createReactClass({
|
||||||
tagPanelContainer = (<div className="mx_LeftPanel_tagPanelContainer">
|
tagPanelContainer = (<div className="mx_LeftPanel_tagPanelContainer">
|
||||||
<TagPanel />
|
<TagPanel />
|
||||||
{ isCustomTagsEnabled ? <CustomRoomTagPanel /> : undefined }
|
{ isCustomTagsEnabled ? <CustomRoomTagPanel /> : undefined }
|
||||||
<TagPanelButtons />
|
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { DragDropContext } from 'react-beautiful-dnd';
|
import { DragDropContext } from 'react-beautiful-dnd';
|
||||||
|
@ -38,6 +38,7 @@ import TagOrderActions from '../../actions/TagOrderActions';
|
||||||
import RoomListActions from '../../actions/RoomListActions';
|
import RoomListActions from '../../actions/RoomListActions';
|
||||||
import ResizeHandle from '../views/elements/ResizeHandle';
|
import ResizeHandle from '../views/elements/ResizeHandle';
|
||||||
import {Resizer, CollapseDistributor} from '../../resizer';
|
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)
|
// We need to fetch each pinned message individually (if we don't already have it)
|
||||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||||
// NB. this is just for server notices rather than pinned messages in general.
|
// NB. this is just for server notices rather than pinned messages in general.
|
||||||
|
@ -70,7 +71,6 @@ const LoggedInView = createReactClass({
|
||||||
// Called with the credentials of a registered user (if they were a ROU that
|
// Called with the credentials of a registered user (if they were a ROU that
|
||||||
// transitioned to PWLU)
|
// transitioned to PWLU)
|
||||||
onRegistered: PropTypes.func,
|
onRegistered: PropTypes.func,
|
||||||
collapsedRhs: PropTypes.bool,
|
|
||||||
|
|
||||||
// Used by the RoomView to handle joining rooms
|
// Used by the RoomView to handle joining rooms
|
||||||
viaServers: PropTypes.arrayOf(PropTypes.string),
|
viaServers: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
@ -78,21 +78,6 @@ const LoggedInView = createReactClass({
|
||||||
// and lots and lots of other stuff.
|
// 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() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
// use compact timeline view
|
// use compact timeline view
|
||||||
|
@ -129,6 +114,8 @@ const LoggedInView = createReactClass({
|
||||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||||
|
|
||||||
fixupColorFonts();
|
fixupColorFonts();
|
||||||
|
|
||||||
|
this._roomView = createRef();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
|
@ -165,10 +152,10 @@ const LoggedInView = createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
canResetTimelineInRoom: function(roomId) {
|
canResetTimelineInRoom: function(roomId) {
|
||||||
if (!this.refs.roomView) {
|
if (!this._roomView.current) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return this.refs.roomView.canResetTimeline();
|
return this._roomView.current.canResetTimeline();
|
||||||
},
|
},
|
||||||
|
|
||||||
_setStateFromSessionStore() {
|
_setStateFromSessionStore() {
|
||||||
|
@ -428,8 +415,8 @@ const LoggedInView = createReactClass({
|
||||||
* @param {Object} ev The key event
|
* @param {Object} ev The key event
|
||||||
*/
|
*/
|
||||||
_onScrollKeyPressed: function(ev) {
|
_onScrollKeyPressed: function(ev) {
|
||||||
if (this.refs.roomView) {
|
if (this._roomView.current) {
|
||||||
this.refs.roomView.handleScrollKey(ev);
|
this._roomView.current.handleScrollKey(ev);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -543,7 +530,7 @@ const LoggedInView = createReactClass({
|
||||||
switch (this.props.page_type) {
|
switch (this.props.page_type) {
|
||||||
case PageTypes.RoomView:
|
case PageTypes.RoomView:
|
||||||
pageElement = <RoomView
|
pageElement = <RoomView
|
||||||
ref='roomView'
|
ref={this._roomView}
|
||||||
autoJoin={this.props.autoJoin}
|
autoJoin={this.props.autoJoin}
|
||||||
onRegistered={this.props.onRegistered}
|
onRegistered={this.props.onRegistered}
|
||||||
thirdPartyInvite={this.props.thirdPartyInvite}
|
thirdPartyInvite={this.props.thirdPartyInvite}
|
||||||
|
@ -552,7 +539,6 @@ const LoggedInView = createReactClass({
|
||||||
eventPixelOffset={this.props.initialEventPixelOffset}
|
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||||
key={this.props.currentRoomId || 'roomview'}
|
key={this.props.currentRoomId || 'roomview'}
|
||||||
disabled={this.props.middleDisabled}
|
disabled={this.props.middleDisabled}
|
||||||
collapsedRhs={this.props.collapsedRhs}
|
|
||||||
ConferenceHandler={this.props.ConferenceHandler}
|
ConferenceHandler={this.props.ConferenceHandler}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
/>;
|
/>;
|
||||||
|
@ -583,7 +569,6 @@ const LoggedInView = createReactClass({
|
||||||
pageElement = <GroupView
|
pageElement = <GroupView
|
||||||
groupId={this.props.currentGroupId}
|
groupId={this.props.currentGroupId}
|
||||||
isNew={this.props.currentGroupIsNew}
|
isNew={this.props.currentGroupIsNew}
|
||||||
collapsedRhs={this.props.collapsedRhs}
|
|
||||||
/>;
|
/>;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -632,7 +617,15 @@ const LoggedInView = createReactClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onPaste={this._onPaste} onKeyDown={this._onReactKeyDown} className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
|
<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 }
|
{ topBar }
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||||
|
@ -647,6 +640,7 @@ const LoggedInView = createReactClass({
|
||||||
</div>
|
</div>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
</MatrixClientContext.Provider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -62,7 +62,7 @@ export default class MainSplit extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.panel && !this.props.collapsedRhs) {
|
if (this.props.panel) {
|
||||||
this._createResizer();
|
this._createResizer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,17 +75,15 @@ export default class MainSplit extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const wasExpanded = !this.props.collapsedRhs && prevProps.collapsedRhs;
|
|
||||||
const wasCollapsed = this.props.collapsedRhs && !prevProps.collapsedRhs;
|
|
||||||
const wasPanelSet = this.props.panel && !prevProps.panel;
|
const wasPanelSet = this.props.panel && !prevProps.panel;
|
||||||
const wasPanelCleared = !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
|
// 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
|
// set. Once both are true, the container ref will mount, which is required
|
||||||
// for the resizer to work.
|
// for the resizer to work.
|
||||||
this._createResizer();
|
this._createResizer();
|
||||||
} else if (this.resizer && (wasCollapsed || wasPanelCleared)) {
|
} else if (this.resizer && wasPanelCleared) {
|
||||||
this.resizer.detach();
|
this.resizer.detach();
|
||||||
this.resizer = null;
|
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
|
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
||||||
import 'focus-visible';
|
import 'focus-visible';
|
||||||
|
// what-input helps improve keyboard accessibility
|
||||||
|
import 'what-input';
|
||||||
|
|
||||||
import Analytics from "../../Analytics";
|
import Analytics from "../../Analytics";
|
||||||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||||
|
@ -148,16 +150,6 @@ export default createReactClass({
|
||||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
childContextTypes: {
|
|
||||||
appConfig: PropTypes.object,
|
|
||||||
},
|
|
||||||
|
|
||||||
getChildContext: function() {
|
|
||||||
return {
|
|
||||||
appConfig: this.props.config,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
const s = {
|
const s = {
|
||||||
// the master view we are showing.
|
// the master view we are showing.
|
||||||
|
@ -175,10 +167,9 @@ export default createReactClass({
|
||||||
viewUserId: null,
|
viewUserId: null,
|
||||||
// this is persisted as mx_lhs_size, loaded in LoggedInView
|
// this is persisted as mx_lhs_size, loaded in LoggedInView
|
||||||
collapseLhs: false,
|
collapseLhs: false,
|
||||||
collapsedRhs: window.localStorage.getItem("mx_rhs_collapsed") === "true",
|
|
||||||
leftDisabled: false,
|
leftDisabled: false,
|
||||||
middleDisabled: false,
|
middleDisabled: false,
|
||||||
rightDisabled: false,
|
// the right panel's disabled state is tracked in its store.
|
||||||
|
|
||||||
version: null,
|
version: null,
|
||||||
newVersion: null,
|
newVersion: null,
|
||||||
|
@ -657,23 +648,11 @@ export default createReactClass({
|
||||||
collapseLhs: false,
|
collapseLhs: false,
|
||||||
});
|
});
|
||||||
break;
|
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': {
|
case 'panel_disable': {
|
||||||
this.setState({
|
this.setState({
|
||||||
leftDisabled: payload.leftDisabled || payload.sideDisabled || false,
|
leftDisabled: payload.leftDisabled || payload.sideDisabled || false,
|
||||||
middleDisabled: payload.middleDisabled || 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;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1245,7 +1224,6 @@ export default createReactClass({
|
||||||
view: VIEWS.LOGIN,
|
view: VIEWS.LOGIN,
|
||||||
ready: false,
|
ready: false,
|
||||||
collapseLhs: false,
|
collapseLhs: false,
|
||||||
collapsedRhs: false,
|
|
||||||
currentRoomId: null,
|
currentRoomId: null,
|
||||||
});
|
});
|
||||||
this.subTitleStatus = '';
|
this.subTitleStatus = '';
|
||||||
|
@ -1261,7 +1239,6 @@ export default createReactClass({
|
||||||
view: VIEWS.SOFT_LOGOUT,
|
view: VIEWS.SOFT_LOGOUT,
|
||||||
ready: false,
|
ready: false,
|
||||||
collapseLhs: false,
|
collapseLhs: false,
|
||||||
collapsedRhs: false,
|
|
||||||
currentRoomId: null,
|
currentRoomId: null,
|
||||||
});
|
});
|
||||||
this.subTitleStatus = '';
|
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 => {
|
cli.on("crypto.verification.request", request => {
|
||||||
let requestObserver;
|
let requestObserver;
|
||||||
if (request.event.getRoomId()) {
|
if (request.event.getRoomId()) {
|
||||||
|
@ -1505,7 +1482,7 @@ export default createReactClass({
|
||||||
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
|
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
|
||||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
||||||
verifier,
|
verifier,
|
||||||
});
|
}, null, /* priority = */ false, /* static = */ true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Fire the tinter right on startup to ensure the default theme is applied
|
// Fire the tinter right on startup to ensure the default theme is applied
|
||||||
|
@ -1705,8 +1682,6 @@ export default createReactClass({
|
||||||
handleResize: function(e) {
|
handleResize: function(e) {
|
||||||
const hideLhsThreshold = 1000;
|
const hideLhsThreshold = 1000;
|
||||||
const showLhsThreshold = 1000;
|
const showLhsThreshold = 1000;
|
||||||
const hideRhsThreshold = 820;
|
|
||||||
const showRhsThreshold = 820;
|
|
||||||
|
|
||||||
if (this._windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
if (this._windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
||||||
dis.dispatch({ action: 'hide_left_panel' });
|
dis.dispatch({ action: 'hide_left_panel' });
|
||||||
|
@ -1714,12 +1689,6 @@ export default createReactClass({
|
||||||
if (this._windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
|
if (this._windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
|
||||||
dis.dispatch({ action: 'show_left_panel' });
|
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.state.resizeNotifier.notifyWindowResized();
|
||||||
this._windowWidth = window.innerWidth;
|
this._windowWidth = window.innerWidth;
|
||||||
|
|
|
@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -159,6 +159,10 @@ export default class MessagePanel extends React.Component {
|
||||||
SettingsStore.getValue("showHiddenEventsInTimeline");
|
SettingsStore.getValue("showHiddenEventsInTimeline");
|
||||||
|
|
||||||
this._isMounted = false;
|
this._isMounted = false;
|
||||||
|
|
||||||
|
this._readMarkerNode = createRef();
|
||||||
|
this._whoIsTyping = createRef();
|
||||||
|
this._scrollPanel = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
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.
|
/* return true if the content is fully scrolled down right now; else false.
|
||||||
*/
|
*/
|
||||||
isAtBottom() {
|
isAtBottom() {
|
||||||
return this.refs.scrollPanel
|
return this._scrollPanel.current && this._scrollPanel.current.isAtBottom();
|
||||||
&& this.refs.scrollPanel.isAtBottom();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* get the current scroll state. See ScrollPanel.getScrollState for
|
/* 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.
|
* returns null if we are not mounted.
|
||||||
*/
|
*/
|
||||||
getScrollState() {
|
getScrollState() {
|
||||||
if (!this.refs.scrollPanel) { return null; }
|
return this._scrollPanel.current ? this._scrollPanel.current.getScrollState() : null;
|
||||||
return this.refs.scrollPanel.getScrollState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns one of:
|
// returns one of:
|
||||||
|
@ -212,8 +214,8 @@ export default class MessagePanel extends React.Component {
|
||||||
// 0: read marker is within the window
|
// 0: read marker is within the window
|
||||||
// +1: read marker is below the window
|
// +1: read marker is below the window
|
||||||
getReadMarkerPosition() {
|
getReadMarkerPosition() {
|
||||||
const readMarker = this.refs.readMarkerNode;
|
const readMarker = this._readMarkerNode.current;
|
||||||
const messageWrapper = this.refs.scrollPanel;
|
const messageWrapper = this._scrollPanel.current;
|
||||||
|
|
||||||
if (!readMarker || !messageWrapper) {
|
if (!readMarker || !messageWrapper) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -236,16 +238,16 @@ export default class MessagePanel extends React.Component {
|
||||||
/* jump to the top of the content.
|
/* jump to the top of the content.
|
||||||
*/
|
*/
|
||||||
scrollToTop() {
|
scrollToTop() {
|
||||||
if (this.refs.scrollPanel) {
|
if (this._scrollPanel.current) {
|
||||||
this.refs.scrollPanel.scrollToTop();
|
this._scrollPanel.current.scrollToTop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* jump to the bottom of the content.
|
/* jump to the bottom of the content.
|
||||||
*/
|
*/
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
if (this.refs.scrollPanel) {
|
if (this._scrollPanel.current) {
|
||||||
this.refs.scrollPanel.scrollToBottom();
|
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
|
* @param {number} mult: -1 to page up, +1 to page down
|
||||||
*/
|
*/
|
||||||
scrollRelative(mult) {
|
scrollRelative(mult) {
|
||||||
if (this.refs.scrollPanel) {
|
if (this._scrollPanel.current) {
|
||||||
this.refs.scrollPanel.scrollRelative(mult);
|
this._scrollPanel.current.scrollRelative(mult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,8 +268,8 @@ export default class MessagePanel extends React.Component {
|
||||||
* @param {KeyboardEvent} ev: the keyboard event to handle
|
* @param {KeyboardEvent} ev: the keyboard event to handle
|
||||||
*/
|
*/
|
||||||
handleScrollKey(ev) {
|
handleScrollKey(ev) {
|
||||||
if (this.refs.scrollPanel) {
|
if (this._scrollPanel.current) {
|
||||||
this.refs.scrollPanel.handleScrollKey(ev);
|
this._scrollPanel.current.handleScrollKey(ev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,8 +284,8 @@ export default class MessagePanel extends React.Component {
|
||||||
* defaults to 0.
|
* defaults to 0.
|
||||||
*/
|
*/
|
||||||
scrollToEvent(eventId, pixelOffset, offsetBase) {
|
scrollToEvent(eventId, pixelOffset, offsetBase) {
|
||||||
if (this.refs.scrollPanel) {
|
if (this._scrollPanel.current) {
|
||||||
this.refs.scrollPanel.scrollToToken(eventId, pixelOffset, offsetBase);
|
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.
|
/* check the scroll state and send out pagination requests if necessary.
|
||||||
*/
|
*/
|
||||||
checkFillState() {
|
checkFillState() {
|
||||||
if (this.refs.scrollPanel) {
|
if (this._scrollPanel.current) {
|
||||||
this.refs.scrollPanel.checkFillState();
|
this._scrollPanel.current.checkFillState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,7 +347,7 @@ export default class MessagePanel extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={"readMarker_"+eventId} ref="readMarkerNode"
|
<li key={"readMarker_"+eventId} ref={this._readMarkerNode}
|
||||||
className="mx_RoomView_myReadMarker_container">
|
className="mx_RoomView_myReadMarker_container">
|
||||||
{ hr }
|
{ hr }
|
||||||
</li>
|
</li>
|
||||||
|
@ -829,14 +831,14 @@ export default class MessagePanel extends React.Component {
|
||||||
// once dynamic content in the events load, make the scrollPanel check the
|
// once dynamic content in the events load, make the scrollPanel check the
|
||||||
// scroll offsets.
|
// scroll offsets.
|
||||||
_onHeightChanged = () => {
|
_onHeightChanged = () => {
|
||||||
const scrollPanel = this.refs.scrollPanel;
|
const scrollPanel = this._scrollPanel.current;
|
||||||
if (scrollPanel) {
|
if (scrollPanel) {
|
||||||
scrollPanel.checkScroll();
|
scrollPanel.checkScroll();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onTypingShown = () => {
|
_onTypingShown = () => {
|
||||||
const scrollPanel = this.refs.scrollPanel;
|
const scrollPanel = this._scrollPanel.current;
|
||||||
// this will make the timeline grow, so checkScroll
|
// this will make the timeline grow, so checkScroll
|
||||||
scrollPanel.checkScroll();
|
scrollPanel.checkScroll();
|
||||||
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
|
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
|
||||||
|
@ -845,7 +847,7 @@ export default class MessagePanel extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
_onTypingHidden = () => {
|
_onTypingHidden = () => {
|
||||||
const scrollPanel = this.refs.scrollPanel;
|
const scrollPanel = this._scrollPanel.current;
|
||||||
if (scrollPanel) {
|
if (scrollPanel) {
|
||||||
// as hiding the typing notifications doesn't
|
// as hiding the typing notifications doesn't
|
||||||
// update the scrollPanel, we tell it to apply
|
// update the scrollPanel, we tell it to apply
|
||||||
|
@ -858,11 +860,11 @@ export default class MessagePanel extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
updateTimelineMinHeight() {
|
updateTimelineMinHeight() {
|
||||||
const scrollPanel = this.refs.scrollPanel;
|
const scrollPanel = this._scrollPanel.current;
|
||||||
|
|
||||||
if (scrollPanel) {
|
if (scrollPanel) {
|
||||||
const isAtBottom = scrollPanel.isAtBottom();
|
const isAtBottom = scrollPanel.isAtBottom();
|
||||||
const whoIsTyping = this.refs.whoIsTyping;
|
const whoIsTyping = this._whoIsTyping.current;
|
||||||
const isTypingVisible = whoIsTyping && whoIsTyping.isVisible();
|
const isTypingVisible = whoIsTyping && whoIsTyping.isVisible();
|
||||||
// when messages get added to the timeline,
|
// when messages get added to the timeline,
|
||||||
// but somebody else is still typing,
|
// but somebody else is still typing,
|
||||||
|
@ -875,7 +877,7 @@ export default class MessagePanel extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onTimelineReset() {
|
onTimelineReset() {
|
||||||
const scrollPanel = this.refs.scrollPanel;
|
const scrollPanel = this._scrollPanel.current;
|
||||||
if (scrollPanel) {
|
if (scrollPanel) {
|
||||||
scrollPanel.clearPreventShrinking();
|
scrollPanel.clearPreventShrinking();
|
||||||
}
|
}
|
||||||
|
@ -909,19 +911,22 @@ export default class MessagePanel extends React.Component {
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
onShown={this._onTypingShown}
|
onShown={this._onTypingShown}
|
||||||
onHidden={this._onTypingHidden}
|
onHidden={this._onTypingHidden}
|
||||||
ref="whoIsTyping" />
|
ref={this._whoIsTyping} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollPanel ref="scrollPanel" className={className}
|
<ScrollPanel
|
||||||
|
ref={this._scrollPanel}
|
||||||
|
className={className}
|
||||||
onScroll={this.props.onScroll}
|
onScroll={this.props.onScroll}
|
||||||
onResize={this.onResize}
|
onResize={this.onResize}
|
||||||
onFillRequest={this.props.onFillRequest}
|
onFillRequest={this.props.onFillRequest}
|
||||||
onUnfillRequest={this.props.onUnfillRequest}
|
onUnfillRequest={this.props.onUnfillRequest}
|
||||||
style={style}
|
style={style}
|
||||||
stickyBottom={this.props.stickyBottom}
|
stickyBottom={this.props.stickyBottom}
|
||||||
resizeNotifier={this.props.resizeNotifier}>
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
>
|
||||||
{ topSpinner }
|
{ topSpinner }
|
||||||
{ this._getEventTiles() }
|
{ this._getEventTiles() }
|
||||||
{ whoIsTyping }
|
{ whoIsTyping }
|
||||||
|
|
|
@ -17,12 +17,11 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
displayName: 'MyGroups',
|
displayName: 'MyGroups',
|
||||||
|
@ -34,8 +33,8 @@ export default createReactClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
contextTypes: {
|
statics: {
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
contextType: MatrixClientContext,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
@ -47,7 +46,7 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_fetch: function() {
|
_fetch: function() {
|
||||||
this.context.matrixClient.getJoinedGroups().then((result) => {
|
this.context.getJoinedGroups().then((result) => {
|
||||||
this.setState({groups: result.groups, error: null});
|
this.setState({groups: result.groups, error: null});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -23,44 +23,31 @@ import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
|
||||||
import RateLimitedFunc from '../../ratelimitedfunc';
|
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||||
import GroupStore from '../../stores/GroupStore';
|
import GroupStore from '../../stores/GroupStore';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import 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 {
|
export default class RightPanel extends React.Component {
|
||||||
static get propTypes() {
|
static get propTypes() {
|
||||||
return {
|
return {
|
||||||
roomId: PropTypes.string, // if showing panels for a given room, this is set
|
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
|
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() {
|
static contextType = MatrixClientContext;
|
||||||
return {
|
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static Phase = Object.freeze({
|
constructor(props) {
|
||||||
RoomMemberList: 'RoomMemberList',
|
super(props);
|
||||||
GroupMemberList: 'GroupMemberList',
|
|
||||||
GroupRoomList: 'GroupRoomList',
|
|
||||||
GroupRoomInfo: 'GroupRoomInfo',
|
|
||||||
FilePanel: 'FilePanel',
|
|
||||||
NotificationPanel: 'NotificationPanel',
|
|
||||||
RoomMemberInfo: 'RoomMemberInfo',
|
|
||||||
Room3pidMemberInfo: 'Room3pidMemberInfo',
|
|
||||||
GroupMemberInfo: 'GroupMemberInfo',
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
this.state = {
|
this.state = {
|
||||||
phase: this._getPhaseFromProps(),
|
phase: this._getPhaseFromProps(),
|
||||||
isUserPrivilegedInGroup: null,
|
isUserPrivilegedInGroup: null,
|
||||||
|
member: this._getUserForPanel(),
|
||||||
};
|
};
|
||||||
this.onAction = this.onAction.bind(this);
|
this.onAction = this.onAction.bind(this);
|
||||||
this.onRoomStateMember = this.onRoomStateMember.bind(this);
|
this.onRoomStateMember = this.onRoomStateMember.bind(this);
|
||||||
|
@ -73,30 +60,44 @@ export default class RightPanel extends React.Component {
|
||||||
}, 500);
|
}, 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() {
|
_getPhaseFromProps() {
|
||||||
|
const rps = RightPanelStore.getSharedInstance();
|
||||||
if (this.props.groupId) {
|
if (this.props.groupId) {
|
||||||
return RightPanel.Phase.GroupMemberList;
|
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
|
||||||
} else if (this.props.user) {
|
dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.GroupMemberList});
|
||||||
return RightPanel.Phase.RoomMemberInfo;
|
return RIGHT_PANEL_PHASES.GroupMemberList;
|
||||||
|
}
|
||||||
|
return rps.groupPanelPhase;
|
||||||
|
} else if (this._getUserForPanel()) {
|
||||||
|
return RIGHT_PANEL_PHASES.RoomMemberInfo;
|
||||||
} else {
|
} 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() {
|
componentWillMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
const cli = this.context.matrixClient;
|
const cli = this.context;
|
||||||
cli.on("RoomState.members", this.onRoomStateMember);
|
cli.on("RoomState.members", this.onRoomStateMember);
|
||||||
this._initGroupStore(this.props.groupId);
|
this._initGroupStore(this.props.groupId);
|
||||||
if (this.props.user) {
|
|
||||||
this.setState({member: this.props.user});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
if (this.context.matrixClient) {
|
if (this.context) {
|
||||||
this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
|
this.context.removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
}
|
}
|
||||||
this._unregisterGroupStore(this.props.groupId);
|
this._unregisterGroupStore(this.props.groupId);
|
||||||
}
|
}
|
||||||
|
@ -126,7 +127,7 @@ export default class RightPanel extends React.Component {
|
||||||
onInviteToGroupButtonClick() {
|
onInviteToGroupButtonClick() {
|
||||||
showGroupInviteDialog(this.props.groupId).then(() => {
|
showGroupInviteDialog(this.props.groupId).then(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: RightPanel.Phase.GroupMemberList,
|
phase: RIGHT_PANEL_PHASES.GroupMemberList,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -142,9 +143,9 @@ export default class RightPanel extends React.Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// redraw the badge on the membership list
|
// 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();
|
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) {
|
member.userId === this.state.member.userId) {
|
||||||
// refresh the member info (e.g. new power level)
|
// refresh the member info (e.g. new power level)
|
||||||
this._delayedUpdate();
|
this._delayedUpdate();
|
||||||
|
@ -152,7 +153,7 @@ export default class RightPanel extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction(payload) {
|
onAction(payload) {
|
||||||
if (payload.action === "view_right_panel_phase") {
|
if (payload.action === "after_right_panel_phase_change") {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: payload.phase,
|
phase: payload.phase,
|
||||||
groupRoomId: payload.groupRoomId,
|
groupRoomId: payload.groupRoomId,
|
||||||
|
@ -178,14 +179,14 @@ export default class RightPanel extends React.Component {
|
||||||
|
|
||||||
let panel = <div />;
|
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} />;
|
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} />;
|
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} />;
|
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||||
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) {
|
} else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) {
|
||||||
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
|
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "view_user",
|
action: "view_user",
|
||||||
|
@ -201,10 +202,10 @@ export default class RightPanel extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
|
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} />;
|
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
|
||||||
} else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) {
|
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) {
|
||||||
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
|
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "view_user",
|
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
|
panel = <GroupRoomInfo
|
||||||
groupRoomId={this.state.groupRoomId}
|
groupRoomId={this.state.groupRoomId}
|
||||||
groupId={this.props.groupId}
|
groupId={this.props.groupId}
|
||||||
key={this.state.groupRoomId} />;
|
key={this.state.groupRoomId} />;
|
||||||
} else if (this.state.phase === RightPanel.Phase.NotificationPanel) {
|
} else if (this.state.phase === RIGHT_PANEL_PHASES.NotificationPanel) {
|
||||||
panel = <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} />;
|
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||||
import Analytics from '../../Analytics';
|
import Analytics from '../../Analytics';
|
||||||
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
const MAX_NAME_LENGTH = 80;
|
const MAX_NAME_LENGTH = 80;
|
||||||
const MAX_TOPIC_LENGTH = 160;
|
const MAX_TOPIC_LENGTH = 160;
|
||||||
|
@ -65,16 +66,6 @@ module.exports = createReactClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
childContextTypes: {
|
|
||||||
matrixClient: PropTypes.object,
|
|
||||||
},
|
|
||||||
|
|
||||||
getChildContext: function() {
|
|
||||||
return {
|
|
||||||
matrixClient: MatrixClientPeg.get(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
this.nextBatch = null;
|
this.nextBatch = null;
|
||||||
|
@ -108,20 +99,9 @@ module.exports = createReactClass({
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// dis.dispatch({
|
|
||||||
// action: 'panel_disable',
|
|
||||||
// sideDisabled: true,
|
|
||||||
// middleDisabled: true,
|
|
||||||
// });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
// dis.dispatch({
|
|
||||||
// action: 'panel_disable',
|
|
||||||
// sideDisabled: false,
|
|
||||||
// middleDisabled: false,
|
|
||||||
// });
|
|
||||||
if (this.filterTimeout) {
|
if (this.filterTimeout) {
|
||||||
clearTimeout(this.filterTimeout);
|
clearTimeout(this.filterTimeout);
|
||||||
}
|
}
|
||||||
|
@ -281,6 +261,7 @@ module.exports = createReactClass({
|
||||||
roomServer: server,
|
roomServer: server,
|
||||||
instanceId: instanceId,
|
instanceId: instanceId,
|
||||||
includeAll: includeAll,
|
includeAll: includeAll,
|
||||||
|
error: null,
|
||||||
}, this.refreshRoomList);
|
}, this.refreshRoomList);
|
||||||
// We also refresh the room list each time even though this
|
// We also refresh the room list each time even though this
|
||||||
// filtering is client-side. It hopefully won't be client side
|
// 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) {
|
if (rows.length === 0 && !this.state.loading) {
|
||||||
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
|
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
|
||||||
} else {
|
} else {
|
||||||
scrollpanel_content = <table ref="directory_table" className="mx_RoomDirectory_table">
|
scrollpanel_content = <table className="mx_RoomDirectory_table">
|
||||||
<tbody>
|
<tbody>
|
||||||
{ rows }
|
{ rows }
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -25,7 +25,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import Resend from '../../Resend';
|
import Resend from '../../Resend';
|
||||||
import * as cryptodevices from '../../cryptodevices';
|
import * as cryptodevices from '../../cryptodevices';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
|
||||||
|
|
||||||
const STATUS_BAR_HIDDEN = 0;
|
const STATUS_BAR_HIDDEN = 0;
|
||||||
const STATUS_BAR_EXPANDED = 1;
|
const STATUS_BAR_EXPANDED = 1;
|
||||||
|
@ -272,7 +272,7 @@ module.exports = createReactClass({
|
||||||
unsentMessages[0].error.data &&
|
unsentMessages[0].error.data &&
|
||||||
unsentMessages[0].error.data.error
|
unsentMessages[0].error.data.error
|
||||||
) {
|
) {
|
||||||
title = unsentMessages[0].error.data.error;
|
title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error;
|
||||||
} else {
|
} else {
|
||||||
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
|
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 React, {createRef} from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
|
@ -26,7 +25,7 @@ import Unread from '../../Unread';
|
||||||
import * as RoomNotifs from '../../RoomNotifs';
|
import * as RoomNotifs from '../../RoomNotifs';
|
||||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||||
import IndicatorScrollbar from './IndicatorScrollbar';
|
import IndicatorScrollbar from './IndicatorScrollbar';
|
||||||
import {Key, KeyCode} from '../../Keyboard';
|
import {Key} from '../../Keyboard';
|
||||||
import { Group } from 'matrix-js-sdk';
|
import { Group } from 'matrix-js-sdk';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import RoomTile from "../views/rooms/RoomTile";
|
import RoomTile from "../views/rooms/RoomTile";
|
||||||
|
@ -36,12 +35,11 @@ import {_t} from "../../languageHandler";
|
||||||
// turn this on for drop & drag console debugging galore
|
// turn this on for drop & drag console debugging galore
|
||||||
const debug = false;
|
const debug = false;
|
||||||
|
|
||||||
const RoomSubList = createReactClass({
|
export default class RoomSubList extends React.PureComponent {
|
||||||
displayName: 'RoomSubList',
|
static displayName = 'RoomSubList';
|
||||||
|
static debug = debug;
|
||||||
|
|
||||||
debug: debug,
|
static propTypes = {
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
tagName: PropTypes.string,
|
tagName: PropTypes.string,
|
||||||
|
@ -59,10 +57,26 @@ const RoomSubList = createReactClass({
|
||||||
incomingCall: PropTypes.object,
|
incomingCall: PropTypes.object,
|
||||||
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
|
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
|
||||||
forceExpand: PropTypes.bool,
|
forceExpand: PropTypes.bool,
|
||||||
},
|
};
|
||||||
|
|
||||||
getInitialState: function() {
|
static defaultProps = {
|
||||||
|
onHeaderClick: function() {
|
||||||
|
}, // NOP
|
||||||
|
extraTiles: [],
|
||||||
|
isInvite: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(props, state) {
|
||||||
return {
|
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,
|
hidden: this.props.startAsHidden || false,
|
||||||
// some values to get LazyRenderList starting
|
// some values to get LazyRenderList starting
|
||||||
scrollerHeight: 800,
|
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...
|
// we have to store the length of the list here so we can see if it's changed or not...
|
||||||
listLength: null,
|
listLength: null,
|
||||||
};
|
};
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
this._header = createRef();
|
||||||
return {
|
this._subList = createRef();
|
||||||
onHeaderClick: function() {
|
this._scroller = createRef();
|
||||||
}, // NOP
|
|
||||||
extraTiles: [],
|
|
||||||
isInvite: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._headerButton = createRef();
|
this._headerButton = createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
},
|
}
|
||||||
|
|
||||||
statics: {
|
componentWillUnmount() {
|
||||||
getDerivedStateFromProps: function(props, state) {
|
|
||||||
return {
|
|
||||||
listLength: props.list.length,
|
|
||||||
scrollTop: props.list.length === state.listLength ? state.scrollTop : 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
},
|
}
|
||||||
|
|
||||||
// The header is collapsible if it is hidden or not stuck
|
// The header is collapsible if it is hidden or not stuck
|
||||||
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
||||||
isCollapsibleOnClick: function() {
|
isCollapsibleOnClick() {
|
||||||
const stuck = this.refs.header.dataset.stuck;
|
const stuck = this._header.current.dataset.stuck;
|
||||||
if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) {
|
if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction = (payload) => {
|
||||||
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
|
// 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
|
// 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).
|
// optimisation of checking that we care about the room being read).
|
||||||
|
@ -124,9 +124,9 @@ const RoomSubList = createReactClass({
|
||||||
) {
|
) {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
onClick: function(ev) {
|
onClick = (ev) => {
|
||||||
if (this.isCollapsibleOnClick()) {
|
if (this.isCollapsibleOnClick()) {
|
||||||
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
|
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
|
||||||
const isHidden = !this.state.hidden;
|
const isHidden = !this.state.hidden;
|
||||||
|
@ -135,11 +135,11 @@ const RoomSubList = createReactClass({
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// The header is stuck, so the click is to be interpreted as a scroll to the header
|
// 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) {
|
switch (ev.key) {
|
||||||
case Key.TAB:
|
case Key.TAB:
|
||||||
// Prevent LeftPanel handling Tab if focus is on the sublist header itself
|
// Prevent LeftPanel handling Tab if focus is on the sublist header itself
|
||||||
|
@ -159,7 +159,7 @@ const RoomSubList = createReactClass({
|
||||||
this.onClick();
|
this.onClick();
|
||||||
} else if (!this.props.forceExpand) {
|
} else if (!this.props.forceExpand) {
|
||||||
// sublist is expanded, go to first room
|
// 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) {
|
if (element) {
|
||||||
element.focus();
|
element.focus();
|
||||||
}
|
}
|
||||||
|
@ -167,9 +167,9 @@ const RoomSubList = createReactClass({
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
onKeyDown: function(ev) {
|
onKeyDown = (ev) => {
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
// On ARROW_LEFT go to the sublist header
|
// On ARROW_LEFT go to the sublist header
|
||||||
case Key.ARROW_LEFT:
|
case Key.ARROW_LEFT:
|
||||||
|
@ -180,24 +180,24 @@ const RoomSubList = createReactClass({
|
||||||
case Key.ARROW_RIGHT:
|
case Key.ARROW_RIGHT:
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
onRoomTileClick(roomId, ev) {
|
onRoomTileClick = (roomId, ev) => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: roomId,
|
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
|
// Force an update by setting the state to the current state
|
||||||
// Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate()
|
// Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate()
|
||||||
// method is honoured
|
// method is honoured
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
},
|
};
|
||||||
|
|
||||||
makeRoomTile: function(room) {
|
makeRoomTile = (room) => {
|
||||||
return <RoomTile
|
return <RoomTile
|
||||||
room={room}
|
room={room}
|
||||||
roomSubList={this}
|
roomSubList={this}
|
||||||
|
@ -212,9 +212,9 @@ const RoomSubList = createReactClass({
|
||||||
incomingCall={null}
|
incomingCall={null}
|
||||||
onClick={this.onRoomTileClick}
|
onClick={this.onRoomTileClick}
|
||||||
/>;
|
/>;
|
||||||
},
|
};
|
||||||
|
|
||||||
_onNotifBadgeClick: function(e) {
|
_onNotifBadgeClick = (e) => {
|
||||||
// prevent the roomsublist collapsing
|
// prevent the roomsublist collapsing
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -225,9 +225,9 @@ const RoomSubList = createReactClass({
|
||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
_onInviteBadgeClick: function(e) {
|
_onInviteBadgeClick = (e) => {
|
||||||
// prevent the roomsublist collapsing
|
// prevent the roomsublist collapsing
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -247,14 +247,14 @@ const RoomSubList = createReactClass({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
onAddRoom: function(e) {
|
onAddRoom = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (this.props.onAddRoom) this.props.onAddRoom();
|
if (this.props.onAddRoom) this.props.onAddRoom();
|
||||||
},
|
};
|
||||||
|
|
||||||
_getHeaderJsx: function(isCollapsed) {
|
_getHeaderJsx(isCollapsed) {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
|
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
|
||||||
const subListNotifications = !this.props.isInvite ?
|
const subListNotifications = !this.props.isInvite ?
|
||||||
|
@ -328,7 +328,7 @@ const RoomSubList = createReactClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<AccessibleButton
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
className="mx_RoomSubList_label"
|
className="mx_RoomSubList_label"
|
||||||
|
@ -346,36 +346,36 @@ const RoomSubList = createReactClass({
|
||||||
{ addRoomButton }
|
{ addRoomButton }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
|
||||||
|
|
||||||
checkOverflow: function() {
|
|
||||||
if (this.refs.scroller) {
|
|
||||||
this.refs.scroller.checkOverflow();
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
setHeight: function(height) {
|
checkOverflow = () => {
|
||||||
if (this.refs.subList) {
|
if (this._scroller.current) {
|
||||||
this.refs.subList.style.height = `${height}px`;
|
this._scroller.current.checkOverflow();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setHeight = (height) => {
|
||||||
|
if (this._subList.current) {
|
||||||
|
this._subList.current.style.height = `${height}px`;
|
||||||
}
|
}
|
||||||
this._updateLazyRenderHeight(height);
|
this._updateLazyRenderHeight(height);
|
||||||
},
|
};
|
||||||
|
|
||||||
_updateLazyRenderHeight: function(height) {
|
_updateLazyRenderHeight(height) {
|
||||||
this.setState({scrollerHeight: height});
|
this.setState({scrollerHeight: height});
|
||||||
},
|
}
|
||||||
|
|
||||||
_onScroll: function() {
|
_onScroll = () => {
|
||||||
this.setState({scrollTop: this.refs.scroller.getScrollTop()});
|
this.setState({scrollTop: this._scroller.current.getScrollTop()});
|
||||||
},
|
};
|
||||||
|
|
||||||
_canUseLazyListRendering() {
|
_canUseLazyListRendering() {
|
||||||
// for now disable lazy rendering as they are already rendered tiles
|
// for now disable lazy rendering as they are already rendered tiles
|
||||||
// not rooms like props.list we pass to LazyRenderList
|
// not rooms like props.list we pass to LazyRenderList
|
||||||
return !this.props.extraTiles || !this.props.extraTiles.length;
|
return !this.props.extraTiles || !this.props.extraTiles.length;
|
||||||
},
|
}
|
||||||
|
|
||||||
render: function() {
|
render() {
|
||||||
const len = this.props.list.length + this.props.extraTiles.length;
|
const len = this.props.list.length + this.props.extraTiles.length;
|
||||||
const isCollapsed = this.state.hidden && !this.props.forceExpand;
|
const isCollapsed = this.state.hidden && !this.props.forceExpand;
|
||||||
|
|
||||||
|
@ -391,7 +391,7 @@ const RoomSubList = createReactClass({
|
||||||
// no body
|
// no body
|
||||||
} else if (this._canUseLazyListRendering()) {
|
} else if (this._canUseLazyListRendering()) {
|
||||||
content = (
|
content = (
|
||||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
||||||
<LazyRenderList
|
<LazyRenderList
|
||||||
scrollTop={this.state.scrollTop }
|
scrollTop={this.state.scrollTop }
|
||||||
height={ this.state.scrollerHeight }
|
height={ this.state.scrollerHeight }
|
||||||
|
@ -404,7 +404,7 @@ const RoomSubList = createReactClass({
|
||||||
const roomTiles = this.props.list.map(r => this.makeRoomTile(r));
|
const roomTiles = this.props.list.map(r => this.makeRoomTile(r));
|
||||||
const tiles = roomTiles.concat(this.props.extraTiles);
|
const tiles = roomTiles.concat(this.props.extraTiles);
|
||||||
content = (
|
content = (
|
||||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
||||||
{ tiles }
|
{ tiles }
|
||||||
</IndicatorScrollbar>
|
</IndicatorScrollbar>
|
||||||
);
|
);
|
||||||
|
@ -418,7 +418,7 @@ const RoomSubList = createReactClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref="subList"
|
ref={this._subList}
|
||||||
className={subListClasses}
|
className={subListClasses}
|
||||||
role="group"
|
role="group"
|
||||||
aria-label={this.props.label}
|
aria-label={this.props.label}
|
||||||
|
@ -428,7 +428,5 @@ const RoomSubList = createReactClass({
|
||||||
{ content }
|
{ content }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
module.exports = RoomSubList;
|
|
||||||
|
|
|
@ -23,12 +23,10 @@ limitations under the License.
|
||||||
|
|
||||||
import shouldHideEvent from '../../shouldHideEvent';
|
import shouldHideEvent from '../../shouldHideEvent';
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {Room} from "matrix-js-sdk";
|
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
|
import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
|
||||||
|
|
||||||
|
@ -44,7 +42,7 @@ import ObjectUtils from '../../ObjectUtils';
|
||||||
import * as Rooms from '../../Rooms';
|
import * as Rooms from '../../Rooms';
|
||||||
import eventSearch from '../../Searching';
|
import eventSearch from '../../Searching';
|
||||||
|
|
||||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard';
|
||||||
|
|
||||||
import MainSplit from './MainSplit';
|
import MainSplit from './MainSplit';
|
||||||
import RightPanel from './RightPanel';
|
import RightPanel from './RightPanel';
|
||||||
|
@ -54,6 +52,8 @@ import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||||
import WidgetUtils from '../../utils/WidgetUtils';
|
import WidgetUtils from '../../utils/WidgetUtils';
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
|
import RoomContext from "../../contexts/RoomContext";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function() {};
|
let debuglog = function() {};
|
||||||
|
@ -65,12 +65,6 @@ if (DEBUG) {
|
||||||
debuglog = console.log.bind(console);
|
debuglog = console.log.bind(console);
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomContext = PropTypes.shape({
|
|
||||||
canReact: PropTypes.bool.isRequired,
|
|
||||||
canReply: PropTypes.bool.isRequired,
|
|
||||||
room: PropTypes.instanceOf(Room),
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = createReactClass({
|
module.exports = createReactClass({
|
||||||
displayName: 'RoomView',
|
displayName: 'RoomView',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -98,9 +92,6 @@ module.exports = createReactClass({
|
||||||
// * invited us to the room
|
// * invited us to the room
|
||||||
oobData: PropTypes.object,
|
oobData: PropTypes.object,
|
||||||
|
|
||||||
// is the RightPanel collapsed?
|
|
||||||
collapsedRhs: PropTypes.bool,
|
|
||||||
|
|
||||||
// Servers the RoomView can use to try and assist joins
|
// Servers the RoomView can use to try and assist joins
|
||||||
viaServers: PropTypes.arrayOf(PropTypes.string),
|
viaServers: PropTypes.arrayOf(PropTypes.string),
|
||||||
},
|
},
|
||||||
|
@ -171,21 +162,6 @@ module.exports = createReactClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
childContextTypes: {
|
|
||||||
room: RoomContext,
|
|
||||||
},
|
|
||||||
|
|
||||||
getChildContext: function() {
|
|
||||||
const {canReact, canReply, room} = this.state;
|
|
||||||
return {
|
|
||||||
room: {
|
|
||||||
canReact,
|
|
||||||
canReply,
|
|
||||||
room,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on("Room", this.onRoom);
|
MatrixClientPeg.get().on("Room", this.onRoom);
|
||||||
|
@ -207,6 +183,9 @@ module.exports = createReactClass({
|
||||||
this._onCiderUpdated();
|
this._onCiderUpdated();
|
||||||
this._ciderWatcherRef = SettingsStore.watchSetting(
|
this._ciderWatcherRef = SettingsStore.watchSetting(
|
||||||
"useCiderComposer", null, this._onCiderUpdated);
|
"useCiderComposer", null, this._onCiderUpdated);
|
||||||
|
|
||||||
|
this._roomView = createRef();
|
||||||
|
this._searchResultsPanel = createRef();
|
||||||
},
|
},
|
||||||
|
|
||||||
_onCiderUpdated: function() {
|
_onCiderUpdated: function() {
|
||||||
|
@ -459,8 +438,8 @@ module.exports = createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function() {
|
||||||
if (this.refs.roomView) {
|
if (this._roomView.current) {
|
||||||
const roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
const roomView = this._roomView.current;
|
||||||
if (!roomView.ondrop) {
|
if (!roomView.ondrop) {
|
||||||
roomView.addEventListener('drop', this.onDrop);
|
roomView.addEventListener('drop', this.onDrop);
|
||||||
roomView.addEventListener('dragover', this.onDragOver);
|
roomView.addEventListener('dragover', this.onDragOver);
|
||||||
|
@ -474,10 +453,10 @@ module.exports = createReactClass({
|
||||||
// in render() prevents the ref from being set on first mount, so we try and
|
// 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,
|
// catch the messagePanel when it does mount. Because we only want the ref once,
|
||||||
// we use a boolean flag to avoid duplicate work.
|
// we use a boolean flag to avoid duplicate work.
|
||||||
if (this.refs.messagePanel && !this.state.atEndOfLiveTimelineInit) {
|
if (this._messagePanel && !this.state.atEndOfLiveTimelineInit) {
|
||||||
this.setState({
|
this.setState({
|
||||||
atEndOfLiveTimelineInit: true,
|
atEndOfLiveTimelineInit: true,
|
||||||
atEndOfLiveTimeline: this.refs.messagePanel.isAtEndOfLiveTimeline(),
|
atEndOfLiveTimeline: this._messagePanel.isAtEndOfLiveTimeline(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -499,12 +478,12 @@ module.exports = createReactClass({
|
||||||
// stop tracking room changes to format permalinks
|
// stop tracking room changes to format permalinks
|
||||||
this._stopAllPermalinkCreators();
|
this._stopAllPermalinkCreators();
|
||||||
|
|
||||||
if (this.refs.roomView) {
|
if (this._roomView.current) {
|
||||||
// disconnect the D&D event listeners from the room view. This
|
// disconnect the D&D event listeners from the room view. This
|
||||||
// is really just for hygiene - we're going to be
|
// is really just for hygiene - we're going to be
|
||||||
// deleted anyway, so it doesn't matter if the event listeners
|
// deleted anyway, so it doesn't matter if the event listeners
|
||||||
// don't get cleaned up.
|
// don't get cleaned up.
|
||||||
const roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
const roomView = this._roomView.current;
|
||||||
roomView.removeEventListener('drop', this.onDrop);
|
roomView.removeEventListener('drop', this.onDrop);
|
||||||
roomView.removeEventListener('dragover', this.onDragOver);
|
roomView.removeEventListener('dragover', this.onDragOver);
|
||||||
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
|
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||||
|
@ -560,15 +539,15 @@ module.exports = createReactClass({
|
||||||
let handled = false;
|
let handled = false;
|
||||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||||
|
|
||||||
switch (ev.keyCode) {
|
switch (ev.key) {
|
||||||
case KeyCode.KEY_D:
|
case Key.D:
|
||||||
if (ctrlCmdOnly) {
|
if (ctrlCmdOnly) {
|
||||||
this.onMuteAudioClick();
|
this.onMuteAudioClick();
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCode.KEY_E:
|
case Key.E:
|
||||||
if (ctrlCmdOnly) {
|
if (ctrlCmdOnly) {
|
||||||
this.onMuteVideoClick();
|
this.onMuteVideoClick();
|
||||||
handled = true;
|
handled = true;
|
||||||
|
@ -584,6 +563,10 @@ module.exports = createReactClass({
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
switch (payload.action) {
|
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_send_failed':
|
||||||
case 'message_sent':
|
case 'message_sent':
|
||||||
this._checkIfAlone(this.state.room);
|
this._checkIfAlone(this.state.room);
|
||||||
|
@ -701,10 +684,10 @@ module.exports = createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
canResetTimeline: function() {
|
canResetTimeline: function() {
|
||||||
if (!this.refs.messagePanel) {
|
if (!this._messagePanel) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return this.refs.messagePanel.canResetTimeline();
|
return this._messagePanel.canResetTimeline();
|
||||||
},
|
},
|
||||||
|
|
||||||
// called when state.room is first initialised (either at initial load,
|
// called when state.room is first initialised (either at initial load,
|
||||||
|
@ -787,11 +770,12 @@ module.exports = createReactClass({
|
||||||
this._updateE2EStatus(room);
|
this._updateE2EStatus(room);
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateE2EStatus: function(room) {
|
_updateE2EStatus: async function(room) {
|
||||||
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) {
|
const cli = MatrixClientPeg.get();
|
||||||
|
if (!cli.isRoomEncrypted(room.roomId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!MatrixClientPeg.get().isCryptoEnabled()) {
|
if (!cli.isCryptoEnabled()) {
|
||||||
// If crypto is not currently enabled, we aren't tracking devices at all,
|
// 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
|
// so we don't know what the answer is. Let's error on the safe side and show
|
||||||
// a warning for this case.
|
// a warning for this case.
|
||||||
|
@ -800,11 +784,39 @@ module.exports = createReactClass({
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
|
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
|
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",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTint: function() {
|
updateTint: function() {
|
||||||
|
@ -1046,7 +1058,7 @@ module.exports = createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessageListScroll: function(ev) {
|
onMessageListScroll: function(ev) {
|
||||||
if (this.refs.messagePanel.isAtEndOfLiveTimeline()) {
|
if (this._messagePanel.isAtEndOfLiveTimeline()) {
|
||||||
this.setState({
|
this.setState({
|
||||||
numUnreadMessages: 0,
|
numUnreadMessages: 0,
|
||||||
atEndOfLiveTimeline: true,
|
atEndOfLiveTimeline: true,
|
||||||
|
@ -1119,8 +1131,8 @@ module.exports = createReactClass({
|
||||||
|
|
||||||
// if we already have a search panel, we need to tell it to forget
|
// if we already have a search panel, we need to tell it to forget
|
||||||
// about its scroll state.
|
// about its scroll state.
|
||||||
if (this.refs.searchResultsPanel) {
|
if (this._searchResultsPanel.current) {
|
||||||
this.refs.searchResultsPanel.resetScrollState();
|
this._searchResultsPanel.current.resetScrollState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure that we don't end up showing results from
|
// make sure that we don't end up showing results from
|
||||||
|
@ -1225,7 +1237,7 @@ module.exports = createReactClass({
|
||||||
// once dynamic content in the search results load, make the scrollPanel check
|
// once dynamic content in the search results load, make the scrollPanel check
|
||||||
// the scroll offsets.
|
// the scroll offsets.
|
||||||
const onHeightChanged = () => {
|
const onHeightChanged = () => {
|
||||||
const scrollPanel = this.refs.searchResultsPanel;
|
const scrollPanel = this._searchResultsPanel.current;
|
||||||
if (scrollPanel) {
|
if (scrollPanel) {
|
||||||
scrollPanel.checkScroll();
|
scrollPanel.checkScroll();
|
||||||
}
|
}
|
||||||
|
@ -1370,28 +1382,28 @@ module.exports = createReactClass({
|
||||||
|
|
||||||
// jump down to the bottom of this room, where new events are arriving
|
// jump down to the bottom of this room, where new events are arriving
|
||||||
jumpToLiveTimeline: function() {
|
jumpToLiveTimeline: function() {
|
||||||
this.refs.messagePanel.jumpToLiveTimeline();
|
this._messagePanel.jumpToLiveTimeline();
|
||||||
dis.dispatch({action: 'focus_composer'});
|
dis.dispatch({action: 'focus_composer'});
|
||||||
},
|
},
|
||||||
|
|
||||||
// jump up to wherever our read marker is
|
// jump up to wherever our read marker is
|
||||||
jumpToReadMarker: function() {
|
jumpToReadMarker: function() {
|
||||||
this.refs.messagePanel.jumpToReadMarker();
|
this._messagePanel.jumpToReadMarker();
|
||||||
},
|
},
|
||||||
|
|
||||||
// update the read marker to match the read-receipt
|
// update the read marker to match the read-receipt
|
||||||
forgetReadMarker: function(ev) {
|
forgetReadMarker: function(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.refs.messagePanel.forgetReadMarker();
|
this._messagePanel.forgetReadMarker();
|
||||||
},
|
},
|
||||||
|
|
||||||
// decide whether or not the top 'unread messages' bar should be shown
|
// decide whether or not the top 'unread messages' bar should be shown
|
||||||
_updateTopUnreadMessagesBar: function() {
|
_updateTopUnreadMessagesBar: function() {
|
||||||
if (!this.refs.messagePanel) {
|
if (!this._messagePanel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showBar = this.refs.messagePanel.canJumpToReadMarker();
|
const showBar = this._messagePanel.canJumpToReadMarker();
|
||||||
if (this.state.showTopUnreadMessagesBar != showBar) {
|
if (this.state.showTopUnreadMessagesBar != showBar) {
|
||||||
this.setState({showTopUnreadMessagesBar: showBar});
|
this.setState({showTopUnreadMessagesBar: showBar});
|
||||||
}
|
}
|
||||||
|
@ -1401,7 +1413,7 @@ module.exports = createReactClass({
|
||||||
// restored when we switch back to it.
|
// restored when we switch back to it.
|
||||||
//
|
//
|
||||||
_getScrollState: function() {
|
_getScrollState: function() {
|
||||||
const messagePanel = this.refs.messagePanel;
|
const messagePanel = this._messagePanel;
|
||||||
if (!messagePanel) return null;
|
if (!messagePanel) return null;
|
||||||
|
|
||||||
// if we're following the live timeline, we want to return null; that
|
// if we're following the live timeline, we want to return null; that
|
||||||
|
@ -1506,10 +1518,10 @@ module.exports = createReactClass({
|
||||||
*/
|
*/
|
||||||
handleScrollKey: function(ev) {
|
handleScrollKey: function(ev) {
|
||||||
let panel;
|
let panel;
|
||||||
if (this.refs.searchResultsPanel) {
|
if (this._searchResultsPanel.current) {
|
||||||
panel = this.refs.searchResultsPanel;
|
panel = this._searchResultsPanel.current;
|
||||||
} else if (this.refs.messagePanel) {
|
} else if (this._messagePanel) {
|
||||||
panel = this.refs.messagePanel;
|
panel = this._messagePanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (panel) {
|
if (panel) {
|
||||||
|
@ -1530,7 +1542,7 @@ module.exports = createReactClass({
|
||||||
// this has to be a proper method rather than an unnamed function,
|
// this has to be a proper method rather than an unnamed function,
|
||||||
// otherwise react calls it with null on each update.
|
// otherwise react calls it with null on each update.
|
||||||
_gatherTimelinePanelRef: function(r) {
|
_gatherTimelinePanelRef: function(r) {
|
||||||
this.refs.messagePanel = r;
|
this._messagePanel = r;
|
||||||
if (r) {
|
if (r) {
|
||||||
console.log("updateTint from RoomView._gatherTimelinePanelRef");
|
console.log("updateTint from RoomView._gatherTimelinePanelRef");
|
||||||
this.updateTint();
|
this.updateTint();
|
||||||
|
@ -1714,12 +1726,12 @@ module.exports = createReactClass({
|
||||||
let aux = null;
|
let aux = null;
|
||||||
let previewBar;
|
let previewBar;
|
||||||
let hideCancel = false;
|
let hideCancel = false;
|
||||||
let hideRightPanel = false;
|
let forceHideRightPanel = false;
|
||||||
if (this.state.forwardingEvent !== null) {
|
if (this.state.forwardingEvent !== null) {
|
||||||
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
|
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
|
||||||
} else if (this.state.searching) {
|
} else if (this.state.searching) {
|
||||||
hideCancel = true; // has own cancel
|
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) {
|
} else if (showRoomUpgradeBar) {
|
||||||
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
||||||
hideCancel = true;
|
hideCancel = true;
|
||||||
|
@ -1760,7 +1772,7 @@ module.exports = createReactClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
hideRightPanel = true;
|
forceHideRightPanel = true;
|
||||||
}
|
}
|
||||||
} else if (hiddenHighlightCount > 0) {
|
} else if (hiddenHighlightCount > 0) {
|
||||||
aux = (
|
aux = (
|
||||||
|
@ -1775,7 +1787,7 @@ module.exports = createReactClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
const auxPanel = (
|
const auxPanel = (
|
||||||
<AuxPanel ref="auxPanel" room={this.state.room}
|
<AuxPanel room={this.state.room}
|
||||||
fullHeight={false}
|
fullHeight={false}
|
||||||
userId={MatrixClientPeg.get().credentials.userId}
|
userId={MatrixClientPeg.get().credentials.userId}
|
||||||
conferenceHandler={this.props.ConferenceHandler}
|
conferenceHandler={this.props.ConferenceHandler}
|
||||||
|
@ -1875,7 +1887,7 @@ module.exports = createReactClass({
|
||||||
searchResultsPanel = (<div className="mx_RoomView_messagePanel mx_RoomView_messagePanelSearchSpinner" />);
|
searchResultsPanel = (<div className="mx_RoomView_messagePanel mx_RoomView_messagePanelSearchSpinner" />);
|
||||||
} else {
|
} else {
|
||||||
searchResultsPanel = (
|
searchResultsPanel = (
|
||||||
<ScrollPanel ref="searchResultsPanel"
|
<ScrollPanel ref={this._searchResultsPanel}
|
||||||
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
|
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
|
||||||
onFillRequest={this.onSearchResultsFillRequest}
|
onFillRequest={this.onSearchResultsFillRequest}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
@ -1898,7 +1910,8 @@ module.exports = createReactClass({
|
||||||
|
|
||||||
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||||
const messagePanel = (
|
const messagePanel = (
|
||||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
<TimelinePanel
|
||||||
|
ref={this._gatherTimelinePanelRef}
|
||||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||||
showReadReceipts={SettingsStore.getValue('showReadReceipts')}
|
showReadReceipts={SettingsStore.getValue('showReadReceipts')}
|
||||||
manageReadReceipts={!this.state.isPeeking}
|
manageReadReceipts={!this.state.isPeeking}
|
||||||
|
@ -1918,7 +1931,8 @@ module.exports = createReactClass({
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
let topUnreadMessagesBar = null;
|
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');
|
const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
|
||||||
topUnreadMessagesBar = (<TopUnreadMessagesBar
|
topUnreadMessagesBar = (<TopUnreadMessagesBar
|
||||||
onScrollUpClick={this.jumpToReadMarker}
|
onScrollUpClick={this.jumpToReadMarker}
|
||||||
|
@ -1926,7 +1940,8 @@ module.exports = createReactClass({
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
let jumpToBottom;
|
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');
|
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
|
||||||
jumpToBottom = (<JumpToBottomButton
|
jumpToBottom = (<JumpToBottomButton
|
||||||
numUnreadMessages={this.state.numUnreadMessages}
|
numUnreadMessages={this.state.numUnreadMessages}
|
||||||
|
@ -1947,17 +1962,21 @@ module.exports = createReactClass({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const rightPanel = !hideRightPanel && this.state.room &&
|
const showRightPanel = !forceHideRightPanel && this.state.room
|
||||||
<RightPanel roomId={this.state.room.roomId} resizeNotifier={this.props.resizeNotifier} />;
|
&& RightPanelStore.getSharedInstance().isOpenForRoom;
|
||||||
const collapsedRhs = hideRightPanel || this.props.collapsedRhs;
|
const rightPanel = showRightPanel
|
||||||
|
? <RightPanel roomId={this.state.room.roomId} resizeNotifier={this.props.resizeNotifier} />
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
|
<RoomContext.Provider value={this.state}>
|
||||||
|
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref={this._roomView}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
<RoomHeader
|
||||||
|
room={this.state.room}
|
||||||
|
searchInfo={searchInfo}
|
||||||
oobData={this.props.oobData}
|
oobData={this.props.oobData}
|
||||||
inRoom={myMembership === 'join'}
|
inRoom={myMembership === 'join'}
|
||||||
collapsedRhs={collapsedRhs}
|
|
||||||
onSearchClick={this.onSearchClick}
|
onSearchClick={this.onSearchClick}
|
||||||
onSettingsClick={this.onSettingsClick}
|
onSettingsClick={this.onSettingsClick}
|
||||||
onPinnedClick={this.onPinnedClick}
|
onPinnedClick={this.onPinnedClick}
|
||||||
|
@ -1968,7 +1987,6 @@ module.exports = createReactClass({
|
||||||
/>
|
/>
|
||||||
<MainSplit
|
<MainSplit
|
||||||
panel={rightPanel}
|
panel={rightPanel}
|
||||||
collapsedRhs={collapsedRhs}
|
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
>
|
>
|
||||||
<div className={fadableSectionClasses}>
|
<div className={fadableSectionClasses}>
|
||||||
|
@ -1981,7 +1999,7 @@ module.exports = createReactClass({
|
||||||
</div>
|
</div>
|
||||||
<div className={statusBarAreaClass}>
|
<div className={statusBarAreaClass}>
|
||||||
<div className="mx_RoomView_statusAreaBox">
|
<div className="mx_RoomView_statusAreaBox">
|
||||||
<div className="mx_RoomView_statusAreaBox_line"></div>
|
<div className="mx_RoomView_statusAreaBox_line" />
|
||||||
{statusBar}
|
{statusBar}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1991,6 +2009,7 @@ module.exports = createReactClass({
|
||||||
</MainSplit>
|
</MainSplit>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</main>
|
</main>
|
||||||
|
</RoomContext.Provider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, {createRef} from "react";
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { KeyCode } from '../../Keyboard';
|
import { Key } from '../../Keyboard';
|
||||||
import Timer from '../../utils/Timer';
|
import Timer from '../../utils/Timer';
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
|
|
||||||
|
@ -166,6 +166,8 @@ module.exports = createReactClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
this.resetScrollState();
|
this.resetScrollState();
|
||||||
|
|
||||||
|
this._itemlist = createRef();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -328,7 +330,7 @@ module.exports = createReactClass({
|
||||||
this._isFilling = true;
|
this._isFilling = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemlist = this.refs.itemlist;
|
const itemlist = this._itemlist.current;
|
||||||
const firstTile = itemlist && itemlist.firstElementChild;
|
const firstTile = itemlist && itemlist.firstElementChild;
|
||||||
const contentTop = firstTile && firstTile.offsetTop;
|
const contentTop = firstTile && firstTile.offsetTop;
|
||||||
const fillPromises = [];
|
const fillPromises = [];
|
||||||
|
@ -373,7 +375,7 @@ module.exports = createReactClass({
|
||||||
|
|
||||||
const origExcessHeight = excessHeight;
|
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
|
// The scroll token of the first/last tile to be unpaginated
|
||||||
let markerScrollToken = null;
|
let markerScrollToken = null;
|
||||||
|
@ -530,26 +532,26 @@ module.exports = createReactClass({
|
||||||
* @param {object} ev the keyboard event
|
* @param {object} ev the keyboard event
|
||||||
*/
|
*/
|
||||||
handleScrollKey: function(ev) {
|
handleScrollKey: function(ev) {
|
||||||
switch (ev.keyCode) {
|
switch (ev.key) {
|
||||||
case KeyCode.PAGE_UP:
|
case Key.PAGE_UP:
|
||||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this.scrollRelative(-1);
|
this.scrollRelative(-1);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCode.PAGE_DOWN:
|
case Key.PAGE_DOWN:
|
||||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this.scrollRelative(1);
|
this.scrollRelative(1);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCode.HOME:
|
case Key.HOME:
|
||||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this.scrollToTop();
|
this.scrollToTop();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyCode.END:
|
case Key.END:
|
||||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
@ -602,7 +604,7 @@ module.exports = createReactClass({
|
||||||
const scrollNode = this._getScrollNode();
|
const scrollNode = this._getScrollNode();
|
||||||
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
|
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
|
||||||
|
|
||||||
const itemlist = this.refs.itemlist;
|
const itemlist = this._itemlist.current;
|
||||||
const messages = itemlist.children;
|
const messages = itemlist.children;
|
||||||
let node = null;
|
let node = null;
|
||||||
|
|
||||||
|
@ -644,7 +646,7 @@ module.exports = createReactClass({
|
||||||
const sn = this._getScrollNode();
|
const sn = this._getScrollNode();
|
||||||
sn.scrollTop = sn.scrollHeight;
|
sn.scrollTop = sn.scrollHeight;
|
||||||
} else if (scrollState.trackedScrollToken) {
|
} else if (scrollState.trackedScrollToken) {
|
||||||
const itemlist = this.refs.itemlist;
|
const itemlist = this._itemlist.current;
|
||||||
const trackedNode = this._getTrackedNode();
|
const trackedNode = this._getTrackedNode();
|
||||||
if (trackedNode) {
|
if (trackedNode) {
|
||||||
const newBottomOffset = this._topFromBottom(trackedNode);
|
const newBottomOffset = this._topFromBottom(trackedNode);
|
||||||
|
@ -682,7 +684,7 @@ module.exports = createReactClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
const sn = this._getScrollNode();
|
const sn = this._getScrollNode();
|
||||||
const itemlist = this.refs.itemlist;
|
const itemlist = this._itemlist.current;
|
||||||
const contentHeight = this._getMessagesHeight();
|
const contentHeight = this._getMessagesHeight();
|
||||||
const minHeight = sn.clientHeight;
|
const minHeight = sn.clientHeight;
|
||||||
const height = Math.max(minHeight, contentHeight);
|
const height = Math.max(minHeight, contentHeight);
|
||||||
|
@ -724,7 +726,7 @@ module.exports = createReactClass({
|
||||||
|
|
||||||
if (!trackedNode || !trackedNode.parentElement) {
|
if (!trackedNode || !trackedNode.parentElement) {
|
||||||
let node;
|
let node;
|
||||||
const messages = this.refs.itemlist.children;
|
const messages = this._itemlist.current.children;
|
||||||
const scrollToken = scrollState.trackedScrollToken;
|
const scrollToken = scrollState.trackedScrollToken;
|
||||||
|
|
||||||
for (let i = messages.length-1; i >= 0; --i) {
|
for (let i = messages.length-1; i >= 0; --i) {
|
||||||
|
@ -756,7 +758,7 @@ module.exports = createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_getMessagesHeight() {
|
_getMessagesHeight() {
|
||||||
const itemlist = this.refs.itemlist;
|
const itemlist = this._itemlist.current;
|
||||||
const lastNode = itemlist.lastElementChild;
|
const lastNode = itemlist.lastElementChild;
|
||||||
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
|
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
|
||||||
const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
|
const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
|
||||||
|
@ -765,7 +767,7 @@ module.exports = createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_topFromBottom(node) {
|
_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
|
/* 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.
|
the same minimum bottom offset, effectively preventing the timeline to shrink.
|
||||||
*/
|
*/
|
||||||
preventShrinking: function() {
|
preventShrinking: function() {
|
||||||
const messageList = this.refs.itemlist;
|
const messageList = this._itemlist.current;
|
||||||
const tiles = messageList && messageList.children;
|
const tiles = messageList && messageList.children;
|
||||||
if (!messageList) {
|
if (!messageList) {
|
||||||
return;
|
return;
|
||||||
|
@ -824,7 +826,7 @@ module.exports = createReactClass({
|
||||||
|
|
||||||
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
|
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
|
||||||
clearPreventShrinking: function() {
|
clearPreventShrinking: function() {
|
||||||
const messageList = this.refs.itemlist;
|
const messageList = this._itemlist.current;
|
||||||
const balanceElement = messageList && messageList.parentElement;
|
const balanceElement = messageList && messageList.parentElement;
|
||||||
if (balanceElement) balanceElement.style.paddingBottom = null;
|
if (balanceElement) balanceElement.style.paddingBottom = null;
|
||||||
this.preventShrinkingState = null;
|
this.preventShrinkingState = null;
|
||||||
|
@ -843,7 +845,7 @@ module.exports = createReactClass({
|
||||||
if (this.preventShrinkingState) {
|
if (this.preventShrinkingState) {
|
||||||
const sn = this._getScrollNode();
|
const sn = this._getScrollNode();
|
||||||
const scrollState = this.scrollState;
|
const scrollState = this.scrollState;
|
||||||
const messageList = this.refs.itemlist;
|
const messageList = this._itemlist.current;
|
||||||
const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
|
const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
|
||||||
// element used to set paddingBottom to balance the typing notifs disappearing
|
// element used to set paddingBottom to balance the typing notifs disappearing
|
||||||
const balanceElement = messageList.parentElement;
|
const balanceElement = messageList.parentElement;
|
||||||
|
@ -879,7 +881,7 @@ module.exports = createReactClass({
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
||||||
<div className="mx_RoomView_messageListWrapper">
|
<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 }
|
{ this.props.children }
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { KeyCode } from '../../Keyboard';
|
import { Key } from '../../Keyboard';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||||
|
@ -53,6 +53,10 @@ module.exports = createReactClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
UNSAFE_componentWillMount: function() {
|
||||||
|
this._search = createRef();
|
||||||
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
},
|
},
|
||||||
|
@ -66,31 +70,31 @@ module.exports = createReactClass({
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'view_room':
|
case 'view_room':
|
||||||
if (this.refs.search && payload.clear_search) {
|
if (this._search.current && payload.clear_search) {
|
||||||
this._clearSearch();
|
this._clearSearch();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'focus_room_filter':
|
case 'focus_room_filter':
|
||||||
if (this.refs.search) {
|
if (this._search.current) {
|
||||||
this.refs.search.focus();
|
this._search.current.focus();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onChange: function() {
|
onChange: function() {
|
||||||
if (!this.refs.search) return;
|
if (!this._search.current) return;
|
||||||
this.setState({ searchTerm: this.refs.search.value });
|
this.setState({ searchTerm: this._search.current.value });
|
||||||
this.onSearch();
|
this.onSearch();
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearch: throttle(function() {
|
onSearch: throttle(function() {
|
||||||
this.props.onSearch(this.refs.search.value);
|
this.props.onSearch(this._search.current.value);
|
||||||
}, 200, {trailing: true, leading: true}),
|
}, 200, {trailing: true, leading: true}),
|
||||||
|
|
||||||
_onKeyDown: function(ev) {
|
_onKeyDown: function(ev) {
|
||||||
switch (ev.keyCode) {
|
switch (ev.key) {
|
||||||
case KeyCode.ESCAPE:
|
case Key.ESCAPE:
|
||||||
this._clearSearch("keyboard");
|
this._clearSearch("keyboard");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -113,7 +117,7 @@ module.exports = createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_clearSearch: function(source) {
|
_clearSearch: function(source) {
|
||||||
this.refs.search.value = "";
|
this._search.current.value = "";
|
||||||
this.onChange();
|
this.onChange();
|
||||||
if (this.props.onCleared) {
|
if (this.props.onCleared) {
|
||||||
this.props.onCleared(source);
|
this.props.onCleared(source);
|
||||||
|
@ -146,7 +150,7 @@ module.exports = createReactClass({
|
||||||
<input
|
<input
|
||||||
key="searchfield"
|
key="searchfield"
|
||||||
type="text"
|
type="text"
|
||||||
ref="search"
|
ref={this._search}
|
||||||
className={"mx_textinput_icon mx_textinput_search " + className}
|
className={"mx_textinput_icon mx_textinput_search " + className}
|
||||||
value={ this.state.searchTerm }
|
value={ this.state.searchTerm }
|
||||||
onFocus={ this._onFocus }
|
onFocus={ this._onFocus }
|
||||||
|
|
|
@ -16,8 +16,6 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
|
||||||
import TagOrderStore from '../../stores/TagOrderStore';
|
import TagOrderStore from '../../stores/TagOrderStore';
|
||||||
|
|
||||||
import GroupActions from '../../actions/GroupActions';
|
import GroupActions from '../../actions/GroupActions';
|
||||||
|
@ -28,12 +26,13 @@ import { _t } from '../../languageHandler';
|
||||||
|
|
||||||
import { Droppable } from 'react-beautiful-dnd';
|
import { Droppable } from 'react-beautiful-dnd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
const TagPanel = createReactClass({
|
const TagPanel = createReactClass({
|
||||||
displayName: 'TagPanel',
|
displayName: 'TagPanel',
|
||||||
|
|
||||||
contextTypes: {
|
statics: {
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
contextType: MatrixClientContext,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
|
@ -45,8 +44,8 @@ const TagPanel = createReactClass({
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this.unmounted = false;
|
this.unmounted = false;
|
||||||
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
this.context.on("Group.myMembership", this._onGroupMyMembership);
|
||||||
this.context.matrixClient.on("sync", this._onClientSync);
|
this.context.on("sync", this._onClientSync);
|
||||||
|
|
||||||
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
|
@ -58,13 +57,13 @@ const TagPanel = createReactClass({
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// This could be done by anything with a matrix client
|
// This could be done by anything with a matrix client
|
||||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
this.context.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
this.context.matrixClient.removeListener("sync", this._onClientSync);
|
this.context.removeListener("sync", this._onClientSync);
|
||||||
if (this._filterStoreToken) {
|
if (this._filterStoreToken) {
|
||||||
this._filterStoreToken.remove();
|
this._filterStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
@ -72,7 +71,7 @@ const TagPanel = createReactClass({
|
||||||
|
|
||||||
_onGroupMyMembership() {
|
_onGroupMyMembership() {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
||||||
},
|
},
|
||||||
|
|
||||||
_onClientSync(syncState, prevState) {
|
_onClientSync(syncState, prevState) {
|
||||||
|
@ -81,7 +80,7 @@ const TagPanel = createReactClass({
|
||||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||||
if (reconnected) {
|
if (reconnected) {
|
||||||
// Load joined groups
|
// Load joined groups
|
||||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -104,6 +103,7 @@ const TagPanel = createReactClass({
|
||||||
render() {
|
render() {
|
||||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||||
|
|
||||||
|
@ -154,6 +154,13 @@ const TagPanel = createReactClass({
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
{ tags }
|
{ tags }
|
||||||
|
<div>
|
||||||
|
<ActionButton
|
||||||
|
tooltip
|
||||||
|
label={_t("Communities")}
|
||||||
|
action="toggle_my_groups"
|
||||||
|
className="mx_TagTile mx_TagTile_plus" />
|
||||||
|
</div>
|
||||||
{ provided.placeholder }
|
{ provided.placeholder }
|
||||||
</div>
|
</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 SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
@ -34,7 +34,7 @@ const dis = require("../../dispatcher");
|
||||||
const ObjectUtils = require('../../ObjectUtils');
|
const ObjectUtils = require('../../ObjectUtils');
|
||||||
const Modal = require("../../Modal");
|
const Modal = require("../../Modal");
|
||||||
const UserActivity = require("../../UserActivity");
|
const UserActivity = require("../../UserActivity");
|
||||||
import { KeyCode } from '../../Keyboard';
|
import {Key} from '../../Keyboard';
|
||||||
import Timer from '../../utils/Timer';
|
import Timer from '../../utils/Timer';
|
||||||
import shouldHideEvent from '../../shouldHideEvent';
|
import shouldHideEvent from '../../shouldHideEvent';
|
||||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||||
|
@ -203,6 +203,8 @@ const TimelinePanel = createReactClass({
|
||||||
this.lastRRSentEventId = undefined;
|
this.lastRRSentEventId = undefined;
|
||||||
this.lastRMSentEventId = undefined;
|
this.lastRMSentEventId = undefined;
|
||||||
|
|
||||||
|
this._messagePanel = createRef();
|
||||||
|
|
||||||
if (this.props.manageReadReceipts) {
|
if (this.props.manageReadReceipts) {
|
||||||
this.updateReadReceiptOnUserActivity();
|
this.updateReadReceiptOnUserActivity();
|
||||||
}
|
}
|
||||||
|
@ -425,8 +427,8 @@ const TimelinePanel = createReactClass({
|
||||||
if (payload.action === "edit_event") {
|
if (payload.action === "edit_event") {
|
||||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||||
this.setState({editState}, () => {
|
this.setState({editState}, () => {
|
||||||
if (payload.event && this.refs.messagePanel) {
|
if (payload.event && this._messagePanel.current) {
|
||||||
this.refs.messagePanel.scrollToEventIfNeeded(
|
this._messagePanel.current.scrollToEventIfNeeded(
|
||||||
payload.event.getId(),
|
payload.event.getId(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -442,9 +444,9 @@ const TimelinePanel = createReactClass({
|
||||||
// updates from pagination will happen when the paginate completes.
|
// updates from pagination will happen when the paginate completes.
|
||||||
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
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
|
// 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
|
// events off the other end of the timeline. But we need to note
|
||||||
// that we can now paginate.
|
// that we can now paginate.
|
||||||
|
@ -499,7 +501,7 @@ const TimelinePanel = createReactClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(updatedState, () => {
|
this.setState(updatedState, () => {
|
||||||
this.refs.messagePanel.updateTimelineMinHeight();
|
this._messagePanel.current.updateTimelineMinHeight();
|
||||||
if (callRMUpdated) {
|
if (callRMUpdated) {
|
||||||
this.props.onReadMarkerUpdated();
|
this.props.onReadMarkerUpdated();
|
||||||
}
|
}
|
||||||
|
@ -510,13 +512,13 @@ const TimelinePanel = createReactClass({
|
||||||
onRoomTimelineReset: function(room, timelineSet) {
|
onRoomTimelineReset: function(room, timelineSet) {
|
||||||
if (timelineSet !== this.props.timelineSet) return;
|
if (timelineSet !== this.props.timelineSet) return;
|
||||||
|
|
||||||
if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) {
|
if (this._messagePanel.current && this._messagePanel.current.isAtBottom()) {
|
||||||
this._loadTimeline();
|
this._loadTimeline();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
canResetTimeline: function() {
|
canResetTimeline: function() {
|
||||||
return this.refs.messagePanel && this.refs.messagePanel.isAtBottom();
|
return this._messagePanel.current && this._messagePanel.current.isAtBottom();
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomRedaction: function(ev, room) {
|
onRoomRedaction: function(ev, room) {
|
||||||
|
@ -629,7 +631,7 @@ const TimelinePanel = createReactClass({
|
||||||
sendReadReceipt: function() {
|
sendReadReceipt: function() {
|
||||||
if (SettingsStore.getValue("lowBandwidth")) return;
|
if (SettingsStore.getValue("lowBandwidth")) return;
|
||||||
|
|
||||||
if (!this.refs.messagePanel) return;
|
if (!this._messagePanel.current) return;
|
||||||
if (!this.props.manageReadReceipts) return;
|
if (!this.props.manageReadReceipts) return;
|
||||||
// This happens on user_activity_end which is delayed, and it's
|
// This happens on user_activity_end which is delayed, and it's
|
||||||
// very possible have logged out within that timeframe, so check
|
// very possible have logged out within that timeframe, so check
|
||||||
|
@ -815,8 +817,8 @@ const TimelinePanel = createReactClass({
|
||||||
if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||||
this._loadTimeline();
|
this._loadTimeline();
|
||||||
} else {
|
} else {
|
||||||
if (this.refs.messagePanel) {
|
if (this._messagePanel.current) {
|
||||||
this.refs.messagePanel.scrollToBottom();
|
this._messagePanel.current.scrollToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -826,7 +828,7 @@ const TimelinePanel = createReactClass({
|
||||||
*/
|
*/
|
||||||
jumpToReadMarker: function() {
|
jumpToReadMarker: function() {
|
||||||
if (!this.props.manageReadMarkers) return;
|
if (!this.props.manageReadMarkers) return;
|
||||||
if (!this.refs.messagePanel) return;
|
if (!this._messagePanel.current) return;
|
||||||
if (!this.state.readMarkerEventId) return;
|
if (!this.state.readMarkerEventId) return;
|
||||||
|
|
||||||
// we may not have loaded the event corresponding to the read-marker
|
// 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
|
// 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.
|
// 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) {
|
if (ret !== null) {
|
||||||
// The messagepanel knows where the RM is, so we must have loaded
|
// The messagepanel knows where the RM is, so we must have loaded
|
||||||
// the relevant event.
|
// the relevant event.
|
||||||
this.refs.messagePanel.scrollToEvent(this.state.readMarkerEventId,
|
this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
|
||||||
0, 1/3);
|
0, 1/3);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -874,8 +876,8 @@ const TimelinePanel = createReactClass({
|
||||||
* at the end of the live timeline.
|
* at the end of the live timeline.
|
||||||
*/
|
*/
|
||||||
isAtEndOfLiveTimeline: function() {
|
isAtEndOfLiveTimeline: function() {
|
||||||
return this.refs.messagePanel
|
return this._messagePanel.current
|
||||||
&& this.refs.messagePanel.isAtBottom()
|
&& this._messagePanel.current.isAtBottom()
|
||||||
&& this._timelineWindow
|
&& this._timelineWindow
|
||||||
&& !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
&& !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||||
},
|
},
|
||||||
|
@ -887,8 +889,8 @@ const TimelinePanel = createReactClass({
|
||||||
* returns null if we are not mounted.
|
* returns null if we are not mounted.
|
||||||
*/
|
*/
|
||||||
getScrollState: function() {
|
getScrollState: function() {
|
||||||
if (!this.refs.messagePanel) { return null; }
|
if (!this._messagePanel.current) { return null; }
|
||||||
return this.refs.messagePanel.getScrollState();
|
return this._messagePanel.current.getScrollState();
|
||||||
},
|
},
|
||||||
|
|
||||||
// returns one of:
|
// returns one of:
|
||||||
|
@ -899,9 +901,9 @@ const TimelinePanel = createReactClass({
|
||||||
// +1: read marker is below the window
|
// +1: read marker is below the window
|
||||||
getReadMarkerPosition: function() {
|
getReadMarkerPosition: function() {
|
||||||
if (!this.props.manageReadMarkers) return null;
|
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) {
|
if (ret !== null) {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -936,15 +938,14 @@ const TimelinePanel = createReactClass({
|
||||||
* We pass it down to the scroll panel.
|
* We pass it down to the scroll panel.
|
||||||
*/
|
*/
|
||||||
handleScrollKey: function(ev) {
|
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
|
// jump to the live timeline on ctrl-end, rather than the end of the
|
||||||
// timeline window.
|
// timeline window.
|
||||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
|
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && ev.key === Key.END) {
|
||||||
ev.keyCode == KeyCode.END) {
|
|
||||||
this.jumpToLiveTimeline();
|
this.jumpToLiveTimeline();
|
||||||
} else {
|
} else {
|
||||||
this.refs.messagePanel.handleScrollKey(ev);
|
this._messagePanel.current.handleScrollKey(ev);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -986,8 +987,8 @@ const TimelinePanel = createReactClass({
|
||||||
const onLoaded = () => {
|
const onLoaded = () => {
|
||||||
// clear the timeline min-height when
|
// clear the timeline min-height when
|
||||||
// (re)loading the timeline
|
// (re)loading the timeline
|
||||||
if (this.refs.messagePanel) {
|
if (this._messagePanel.current) {
|
||||||
this.refs.messagePanel.onTimelineReset();
|
this._messagePanel.current.onTimelineReset();
|
||||||
}
|
}
|
||||||
this._reloadEvents();
|
this._reloadEvents();
|
||||||
|
|
||||||
|
@ -1002,7 +1003,7 @@ const TimelinePanel = createReactClass({
|
||||||
timelineLoading: false,
|
timelineLoading: false,
|
||||||
}, () => {
|
}, () => {
|
||||||
// initialise the scroll state of the message panel
|
// 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
|
// this shouldn't happen - we know we're mounted because
|
||||||
// we're in a setState callback, and we know
|
// we're in a setState callback, and we know
|
||||||
// timelineLoading is now false, so render() should have
|
// timelineLoading is now false, so render() should have
|
||||||
|
@ -1012,10 +1013,10 @@ const TimelinePanel = createReactClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (eventId) {
|
if (eventId) {
|
||||||
this.refs.messagePanel.scrollToEvent(eventId, pixelOffset,
|
this._messagePanel.current.scrollToEvent(eventId, pixelOffset,
|
||||||
offsetBase);
|
offsetBase);
|
||||||
} else {
|
} else {
|
||||||
this.refs.messagePanel.scrollToBottom();
|
this._messagePanel.current.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendReadReceipt();
|
this.sendReadReceipt();
|
||||||
|
@ -1134,7 +1135,7 @@ const TimelinePanel = createReactClass({
|
||||||
const ignoreOwn = opts.ignoreOwn || false;
|
const ignoreOwn = opts.ignoreOwn || false;
|
||||||
const allowPartial = opts.allowPartial || false;
|
const allowPartial = opts.allowPartial || false;
|
||||||
|
|
||||||
const messagePanel = this.refs.messagePanel;
|
const messagePanel = this._messagePanel.current;
|
||||||
if (messagePanel === undefined) return null;
|
if (messagePanel === undefined) return null;
|
||||||
|
|
||||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
|
@ -1313,7 +1314,8 @@ const TimelinePanel = createReactClass({
|
||||||
['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState)
|
['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState)
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<MessagePanel ref="messagePanel"
|
<MessagePanel
|
||||||
|
ref={this._messagePanel}
|
||||||
room={this.props.timelineSet.room}
|
room={this.props.timelineSet.room}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
hidden={this.props.hidden}
|
hidden={this.props.hidden}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -48,6 +48,8 @@ module.exports = createReactClass({
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._captchaWidgetId = null;
|
this._captchaWidgetId = null;
|
||||||
|
|
||||||
|
this._recaptchaContainer = createRef();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -67,7 +69,7 @@ module.exports = createReactClass({
|
||||||
scriptTag.setAttribute(
|
scriptTag.setAttribute(
|
||||||
'src', `${protocol}//www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
|
'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 (
|
return (
|
||||||
<div ref="recaptchaContainer">
|
<div ref={this._recaptchaContainer}>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"This homeserver would like to make sure you are not a robot.",
|
"This homeserver would like to make sure you are not a robot.",
|
||||||
)}</p>
|
)}</p>
|
||||||
<div id={DIV_ID}></div>
|
<div id={DIV_ID} />
|
||||||
{ error }
|
{ error }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import sdk from '../../../index';
|
||||||
|
|
||||||
import { COUNTRIES } from '../../../phonenumber';
|
import { COUNTRIES } from '../../../phonenumber';
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
|
||||||
const COUNTRIES_BY_ISO2 = {};
|
const COUNTRIES_BY_ISO2 = {};
|
||||||
for (const c of COUNTRIES) {
|
for (const c of COUNTRIES) {
|
||||||
|
@ -130,10 +131,17 @@ export default class CountryDropdown extends React.Component {
|
||||||
// values between mounting and the initial value propgating
|
// values between mounting and the initial value propgating
|
||||||
const value = this.props.value || this.state.defaultCountry.iso2;
|
const value = this.props.value || this.state.defaultCountry.iso2;
|
||||||
|
|
||||||
return <Dropdown className={this.props.className + " mx_CountryDropdown"}
|
return <Dropdown
|
||||||
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
|
id="mx_CountryDropdown"
|
||||||
menuWidth={298} getShortOption={this._getShortOption}
|
className={this.props.className + " mx_CountryDropdown"}
|
||||||
value={value} searchEnabled={true} disabled={this.props.disabled}
|
onOptionChange={this._onOptionChange}
|
||||||
|
onSearchChange={this._onSearchChange}
|
||||||
|
menuWidth={298}
|
||||||
|
getShortOption={this._getShortOption}
|
||||||
|
value={value}
|
||||||
|
searchEnabled={true}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
label={_t("Country Dropdown")}
|
||||||
>
|
>
|
||||||
{ options }
|
{ options }
|
||||||
</Dropdown>;
|
</Dropdown>;
|
||||||
|
|
|
@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
@ -581,6 +581,8 @@ export const FallbackAuthEntry = createReactClass({
|
||||||
// the popup if we open it immediately.
|
// the popup if we open it immediately.
|
||||||
this._popupWindow = null;
|
this._popupWindow = null;
|
||||||
window.addEventListener("message", this._onReceiveMessage);
|
window.addEventListener("message", this._onReceiveMessage);
|
||||||
|
|
||||||
|
this._fallbackButton = createRef();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -591,8 +593,8 @@ export const FallbackAuthEntry = createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
focus: function() {
|
focus: function() {
|
||||||
if (this.refs.fallbackButton) {
|
if (this._fallbackButton.current) {
|
||||||
this.refs.fallbackButton.focus();
|
this._fallbackButton.current.focus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -624,7 +626,7 @@ export const FallbackAuthEntry = createReactClass({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<a ref="fallbackButton" onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
<a ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||||
{errorSection}
|
{errorSection}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,10 +19,10 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
|
||||||
import AvatarLogic from '../../../Avatar';
|
import AvatarLogic from '../../../Avatar';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
module.exports = createReactClass({
|
module.exports = createReactClass({
|
||||||
displayName: 'BaseAvatar',
|
displayName: 'BaseAvatar',
|
||||||
|
@ -38,10 +38,16 @@ module.exports = createReactClass({
|
||||||
// XXX resizeMethod not actually used.
|
// XXX resizeMethod not actually used.
|
||||||
resizeMethod: PropTypes.string,
|
resizeMethod: PropTypes.string,
|
||||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
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: {
|
statics: {
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
contextType: MatrixClientContext,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -59,12 +65,12 @@ module.exports = createReactClass({
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unmounted = false;
|
this.unmounted = false;
|
||||||
this.context.matrixClient.on('sync', this.onClientSync);
|
this.context.on('sync', this.onClientSync);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
this.context.removeListener('sync', this.onClientSync);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
@ -148,7 +154,7 @@ module.exports = createReactClass({
|
||||||
|
|
||||||
const {
|
const {
|
||||||
name, idName, title, url, urls, width, height, resizeMethod,
|
name, idName, title, url, urls, width, height, resizeMethod,
|
||||||
defaultToInitialLetter, onClick,
|
defaultToInitialLetter, onClick, inputRef,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -171,7 +177,7 @@ module.exports = createReactClass({
|
||||||
if (onClick != null) {
|
if (onClick != null) {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton element='span' className="mx_BaseAvatar"
|
<AccessibleButton element='span' className="mx_BaseAvatar"
|
||||||
onClick={onClick} {...otherProps}
|
onClick={onClick} inputRef={inputRef} {...otherProps}
|
||||||
>
|
>
|
||||||
{ textNode }
|
{ textNode }
|
||||||
{ imgNode }
|
{ imgNode }
|
||||||
|
@ -179,7 +185,7 @@ module.exports = createReactClass({
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="mx_BaseAvatar" {...otherProps}>
|
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
||||||
{ textNode }
|
{ textNode }
|
||||||
{ imgNode }
|
{ imgNode }
|
||||||
</span>
|
</span>
|
||||||
|
@ -188,21 +194,26 @@ module.exports = createReactClass({
|
||||||
}
|
}
|
||||||
if (onClick != null) {
|
if (onClick != null) {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton className="mx_BaseAvatar mx_BaseAvatar_image"
|
<AccessibleButton
|
||||||
|
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||||
element='img'
|
element='img'
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onError={this.onError}
|
onError={this.onError}
|
||||||
width={width} height={height}
|
width={width} height={height}
|
||||||
title={title} alt=""
|
title={title} alt=""
|
||||||
|
inputRef={inputRef}
|
||||||
{...otherProps} />
|
{...otherProps} />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
|
<img
|
||||||
|
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||||
|
src={imageUrl}
|
||||||
onError={this.onError}
|
onError={this.onError}
|
||||||
width={width} height={height}
|
width={width} height={height}
|
||||||
title={title} alt=""
|
title={title} alt=""
|
||||||
|
ref={inputRef}
|
||||||
{...otherProps} />
|
{...otherProps} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,8 +38,8 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hasStatus: this.hasStatus,
|
hasStatus: this.hasStatus,
|
||||||
|
|
|
@ -31,8 +31,8 @@ export default class GroupInviteTileContextMenu extends React.Component {
|
||||||
onFinished: PropTypes.func,
|
onFinished: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context);
|
super(props);
|
||||||
|
|
||||||
this._onClickReject = this._onClickReject.bind(this);
|
this._onClickReject = this._onClickReject.bind(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,8 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
message: this.comittedStatusMessage,
|
message: this.comittedStatusMessage,
|
||||||
|
|
|
@ -17,12 +17,12 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import TagOrderActions from '../../../actions/TagOrderActions';
|
import TagOrderActions from '../../../actions/TagOrderActions';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import {MenuItem} from "../../structures/ContextMenu";
|
import {MenuItem} from "../../structures/ContextMenu";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
export default class TagTileContextMenu extends React.Component {
|
export default class TagTileContextMenu extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -31,9 +31,7 @@ export default class TagTileContextMenu extends React.Component {
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextTypes = {
|
static contextType = MatrixClientContext;
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
@ -51,7 +49,7 @@ export default class TagTileContextMenu extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRemoveClick() {
|
_onRemoveClick() {
|
||||||
dis.dispatch(TagOrderActions.removeTag(this.context.matrixClient, this.props.tag));
|
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import SdkConfig from '../../../SdkConfig';
|
||||||
import { getHostingLink } from '../../../utils/HostingLink';
|
import { getHostingLink } from '../../../utils/HostingLink';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import {MenuItem} from "../../structures/ContextMenu";
|
import {MenuItem} from "../../structures/ContextMenu";
|
||||||
|
import sdk from "../../../index";
|
||||||
|
|
||||||
export class TopLeftMenu extends React.Component {
|
export class TopLeftMenu extends React.Component {
|
||||||
static propTypes = {
|
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 = (
|
const settingsItem = (
|
||||||
<MenuItem className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
|
<MenuItem className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
|
||||||
{_t("Settings")}
|
{_t("Settings")}
|
||||||
|
@ -115,11 +122,18 @@ export class TopLeftMenu extends React.Component {
|
||||||
<ul className="mx_TopLeftMenu_section_withIcon" role="none">
|
<ul className="mx_TopLeftMenu_section_withIcon" role="none">
|
||||||
{homePageItem}
|
{homePageItem}
|
||||||
{settingsItem}
|
{settingsItem}
|
||||||
|
{helpItem}
|
||||||
{signInOutItem}
|
{signInOutItem}
|
||||||
</ul>
|
</ul>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openHelp = () => {
|
||||||
|
this.closeMenu();
|
||||||
|
const RedesignFeedbackDialog = sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
|
||||||
|
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
|
||||||
|
};
|
||||||
|
|
||||||
viewHomePage() {
|
viewHomePage() {
|
||||||
dis.dispatch({action: 'view_home_page'});
|
dis.dispatch({action: 'view_home_page'});
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
|
|
|
@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
|
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
|
||||||
import { abbreviateUrl } from '../../../utils/UrlUtils';
|
import { abbreviateUrl } from '../../../utils/UrlUtils';
|
||||||
import {sleep} from "../../../utils/promise";
|
import {sleep} from "../../../utils/promise";
|
||||||
|
import {Key} from "../../../Keyboard";
|
||||||
|
|
||||||
const TRUNCATE_QUERY_LIST = 40;
|
const TRUNCATE_QUERY_LIST = 40;
|
||||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||||
|
@ -106,10 +107,14 @@ module.exports = createReactClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
UNSAFE_componentWillMount: function() {
|
||||||
|
this._textinput = createRef();
|
||||||
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
if (this.props.focus) {
|
if (this.props.focus) {
|
||||||
// Set the cursor at the end of the text input
|
// 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();
|
let selectedList = this.state.selectedList.slice();
|
||||||
// Check the text input field to see if user has an unconverted address
|
// 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 there is and it's valid add it to the local selectedList
|
||||||
if (this.refs.textinput.value !== '') {
|
if (this._textinput.current.value !== '') {
|
||||||
selectedList = this._addAddressesToList([this.refs.textinput.value]);
|
selectedList = this._addAddressesToList([this._textinput.current.value]);
|
||||||
if (selectedList === null) return;
|
if (selectedList === null) return;
|
||||||
}
|
}
|
||||||
this.props.onFinished(true, selectedList);
|
this.props.onFinished(true, selectedList);
|
||||||
|
@ -138,39 +143,41 @@ module.exports = createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyDown: function(e) {
|
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.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
} else if (e.keyCode === 38) { // up arrow
|
} else if (e.key === Key.ARROW_UP) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.addressSelector) this.addressSelector.moveSelectionUp();
|
if (this.addressSelector) this.addressSelector.moveSelectionUp();
|
||||||
} else if (e.keyCode === 40) { // down arrow
|
} else if (e.key === Key.ARROW_DOWN) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.addressSelector) this.addressSelector.moveSelectionDown();
|
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.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.addressSelector) this.addressSelector.chooseSelection();
|
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.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.onDismissed(this.state.selectedList.length - 1)();
|
this.onDismissed(this.state.selectedList.length - 1)();
|
||||||
} else if (e.keyCode === 13) { // enter
|
} else if (e.key === Key.ENTER) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.refs.textinput.value === '') {
|
if (textInput === '') {
|
||||||
// if there's nothing in the input box, submit the form
|
// if there's nothing in the input box, submit the form
|
||||||
this.onButtonClick();
|
this.onButtonClick();
|
||||||
} else {
|
} 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.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this._addAddressesToList([this.refs.textinput.value]);
|
this._addAddressesToList([textInput]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -647,7 +654,7 @@ module.exports = createReactClass({
|
||||||
onPaste={this._onPaste}
|
onPaste={this._onPaste}
|
||||||
rows="1"
|
rows="1"
|
||||||
id="textinput"
|
id="textinput"
|
||||||
ref="textinput"
|
ref={this._textinput}
|
||||||
className="mx_AddressPickerDialog_input"
|
className="mx_AddressPickerDialog_input"
|
||||||
onChange={this.onQueryChanged}
|
onChange={this.onQueryChanged}
|
||||||
placeholder={this.getPlaceholder()}
|
placeholder={this.getPlaceholder()}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2018, 2019 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,16 +18,15 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusLock from 'react-focus-lock';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { Key } from '../../../Keyboard';
|
||||||
|
|
||||||
import { KeyCode } from '../../../Keyboard';
|
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic container for modal dialogs.
|
* Basic container for modal dialogs.
|
||||||
|
@ -83,16 +83,6 @@ export default createReactClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
childContextTypes: {
|
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
|
||||||
},
|
|
||||||
|
|
||||||
getChildContext: function() {
|
|
||||||
return {
|
|
||||||
matrixClient: this._matrixClient,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this._matrixClient = MatrixClientPeg.get();
|
this._matrixClient = MatrixClientPeg.get();
|
||||||
},
|
},
|
||||||
|
@ -101,7 +91,7 @@ export default createReactClass({
|
||||||
if (this.props.onKeyDown) {
|
if (this.props.onKeyDown) {
|
||||||
this.props.onKeyDown(e);
|
this.props.onKeyDown(e);
|
||||||
}
|
}
|
||||||
if (this.props.hasCancel && e.keyCode === KeyCode.ESCAPE) {
|
if (this.props.hasCancel && e.key === Key.ESCAPE) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
|
@ -121,20 +111,25 @@ export default createReactClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusTrap onKeyDown={this._onKeyDown}
|
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||||
className={classNames({
|
<FocusLock
|
||||||
[this.props.className]: true,
|
returnFocus={true}
|
||||||
'mx_Dialog_fixedWidth': this.props.fixedWidth,
|
lockProps={{
|
||||||
})}
|
onKeyDown: this._onKeyDown,
|
||||||
role="dialog"
|
role: "dialog",
|
||||||
aria-labelledby='mx_BaseDialog_title'
|
["aria-labelledby"]: "mx_BaseDialog_title",
|
||||||
// This should point to a node describing the dialog.
|
// This should point to a node describing the dialog.
|
||||||
// If we were about to completely follow this recommendation we'd need to
|
// 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.
|
// make all the components relying on BaseDialog to be aware of it.
|
||||||
// So instead we will use the whole content as the description.
|
// So instead we will use the whole content as the description.
|
||||||
// Description comes first and if the content contains more text,
|
// Description comes first and if the content contains more text,
|
||||||
// AT users can skip its presentation.
|
// AT users can skip its presentation.
|
||||||
aria-describedby={this.props.contentId}
|
["aria-describedby"]: this.props.contentId,
|
||||||
|
}}
|
||||||
|
className={classNames({
|
||||||
|
[this.props.className]: true,
|
||||||
|
'mx_Dialog_fixedWidth': this.props.fixedWidth,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<div className={classNames('mx_Dialog_header', {
|
<div className={classNames('mx_Dialog_header', {
|
||||||
'mx_Dialog_headerWithButton': !!this.props.headerButton,
|
'mx_Dialog_headerWithButton': !!this.props.headerButton,
|
||||||
|
@ -146,7 +141,8 @@ export default createReactClass({
|
||||||
{ cancelButton }
|
{ cancelButton }
|
||||||
</div>
|
</div>
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</FocusTrap>
|
</FocusLock>
|
||||||
|
</MatrixClientContext.Provider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,8 +25,8 @@ import Modal from '../../../Modal';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
export default class BugReportDialog extends React.Component {
|
export default class BugReportDialog extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
sendLogs: true,
|
sendLogs: true,
|
||||||
busy: false,
|
busy: false,
|
||||||
|
|
|
@ -173,7 +173,7 @@ export default createReactClass({
|
||||||
const domain = MatrixClientPeg.get().getDomain();
|
const domain = MatrixClientPeg.get().getDomain();
|
||||||
aliasField = (
|
aliasField = (
|
||||||
<div className="mx_CreateRoomDialog_aliasContainer">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -25,8 +25,8 @@ import * as Lifecycle from '../../../Lifecycle';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
export default class DeactivateAccountDialog extends React.Component {
|
export default class DeactivateAccountDialog extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context);
|
super(props);
|
||||||
|
|
||||||
this._onOk = this._onOk.bind(this);
|
this._onOk = this._onOk.bind(this);
|
||||||
this._onCancel = this._onCancel.bind(this);
|
this._onCancel = this._onCancel.bind(this);
|
||||||
|
|
|
@ -97,7 +97,7 @@ export default class DeviceVerifyDialog extends React.Component {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const verifyingOwnDevice = this.props.userId === client.getUserId();
|
const verifyingOwnDevice = this.props.userId === client.getUserId();
|
||||||
try {
|
try {
|
||||||
if (!verifyingOwnDevice && SettingsStore.getValue("feature_dm_verification")) {
|
if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
|
||||||
const roomId = await ensureDMExistsAndOpen(this.props.userId);
|
const roomId = await ensureDMExistsAndOpen(this.props.userId);
|
||||||
// throws upon cancellation before having started
|
// throws upon cancellation before having started
|
||||||
this._verifier = await client.requestVerificationDM(
|
this._verifier = await client.requestVerificationDM(
|
||||||
|
|
|
@ -16,23 +16,19 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { Room } from "matrix-js-sdk";
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
class DevtoolsComponent extends React.Component {
|
class GenericEditor extends React.PureComponent {
|
||||||
static contextTypes = {
|
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class GenericEditor extends DevtoolsComponent {
|
|
||||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
// static propTypes = {onBack: PropTypes.func.isRequired};
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context);
|
super(props);
|
||||||
this._onChange = this._onChange.bind(this);
|
this._onChange = this._onChange.bind(this);
|
||||||
this.onBack = this.onBack.bind(this);
|
this.onBack = this.onBack.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -67,12 +63,15 @@ class SendCustomEvent extends GenericEditor {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onBack: PropTypes.func.isRequired,
|
onBack: PropTypes.func.isRequired,
|
||||||
|
room: PropTypes.instanceOf(Room).isRequired,
|
||||||
forceStateEvent: PropTypes.bool,
|
forceStateEvent: PropTypes.bool,
|
||||||
inputs: PropTypes.object,
|
inputs: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props, context) {
|
static contextType = MatrixClientContext;
|
||||||
super(props, context);
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
this._send = this._send.bind(this);
|
this._send = this._send.bind(this);
|
||||||
|
|
||||||
const {eventType, stateKey, evContent} = Object.assign({
|
const {eventType, stateKey, evContent} = Object.assign({
|
||||||
|
@ -91,11 +90,11 @@ class SendCustomEvent extends GenericEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
send(content) {
|
send(content) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = this.context;
|
||||||
if (this.state.isStateEvent) {
|
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 {
|
} 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 getLabel() { return _t('Send Account Data'); }
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
room: PropTypes.instanceOf(Room).isRequired,
|
||||||
isRoomAccountData: PropTypes.bool,
|
isRoomAccountData: PropTypes.bool,
|
||||||
forceMode: PropTypes.bool,
|
forceMode: PropTypes.bool,
|
||||||
inputs: PropTypes.object,
|
inputs: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props, context) {
|
static contextType = MatrixClientContext;
|
||||||
super(props, context);
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
this._send = this._send.bind(this);
|
this._send = this._send.bind(this);
|
||||||
|
|
||||||
const {eventType, evContent} = Object.assign({
|
const {eventType, evContent} = Object.assign({
|
||||||
|
@ -177,9 +179,9 @@ class SendAccountData extends GenericEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
send(content) {
|
send(content) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = this.context;
|
||||||
if (this.state.isRoomAccountData) {
|
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);
|
return cli.setAccountData(this.state.eventType, content);
|
||||||
}
|
}
|
||||||
|
@ -234,7 +236,7 @@ class SendAccountData extends GenericEditor {
|
||||||
const INITIAL_LOAD_TILES = 20;
|
const INITIAL_LOAD_TILES = 20;
|
||||||
const LOAD_TILES_STEP_SIZE = 50;
|
const LOAD_TILES_STEP_SIZE = 50;
|
||||||
|
|
||||||
class FilteredList extends React.Component {
|
class FilteredList extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
query: PropTypes.string,
|
query: PropTypes.string,
|
||||||
|
@ -247,8 +249,8 @@ class FilteredList extends React.Component {
|
||||||
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query),
|
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 getLabel() { return _t('Explore Room State'); }
|
||||||
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onBack: PropTypes.func.isRequired,
|
onBack: PropTypes.func.isRequired,
|
||||||
|
room: PropTypes.instanceOf(Room).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props, context) {
|
static contextType = MatrixClientContext;
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
|
constructor(props) {
|
||||||
this.roomStateEvents = room.currentState.events;
|
super(props);
|
||||||
|
|
||||||
|
this.roomStateEvents = this.props.room.currentState.events;
|
||||||
|
|
||||||
this.onBack = this.onBack.bind(this);
|
this.onBack = this.onBack.bind(this);
|
||||||
this.editEv = this.editEv.bind(this);
|
this.editEv = this.editEv.bind(this);
|
||||||
|
@ -373,7 +376,7 @@ class RoomStateExplorer extends DevtoolsComponent {
|
||||||
render() {
|
render() {
|
||||||
if (this.state.event) {
|
if (this.state.event) {
|
||||||
if (this.state.editing) {
|
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(),
|
eventType: this.state.event.getType(),
|
||||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||||
stateKey: this.state.event.getStateKey(),
|
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 getLabel() { return _t('Explore Account Data'); }
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onBack: PropTypes.func.isRequired,
|
onBack: PropTypes.func.isRequired,
|
||||||
|
room: PropTypes.instanceOf(Room).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props, context) {
|
static contextType = MatrixClientContext;
|
||||||
super(props, context);
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
this.onBack = this.onBack.bind(this);
|
this.onBack = this.onBack.bind(this);
|
||||||
this.editEv = this.editEv.bind(this);
|
this.editEv = this.editEv.bind(this);
|
||||||
|
@ -467,11 +473,10 @@ class AccountDataExplorer extends DevtoolsComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
getData() {
|
getData() {
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
if (this.state.isRoomAccountData) {
|
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) {
|
onViewSourceClick(event) {
|
||||||
|
@ -505,7 +510,11 @@ class AccountDataExplorer extends DevtoolsComponent {
|
||||||
render() {
|
render() {
|
||||||
if (this.state.event) {
|
if (this.state.event) {
|
||||||
if (this.state.editing) {
|
if (this.state.editing) {
|
||||||
return <SendAccountData isRoomAccountData={this.state.isRoomAccountData} onBack={this.onBack} inputs={{
|
return <SendAccountData
|
||||||
|
room={this.props.room}
|
||||||
|
isRoomAccountData={this.state.isRoomAccountData}
|
||||||
|
onBack={this.onBack}
|
||||||
|
inputs={{
|
||||||
eventType: this.state.event.getType(),
|
eventType: this.state.event.getType(),
|
||||||
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
|
||||||
}} forceMode={true} />;
|
}} forceMode={true} />;
|
||||||
|
@ -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 getLabel() { return _t('View Servers in Room'); }
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onBack: PropTypes.func.isRequired,
|
onBack: PropTypes.func.isRequired,
|
||||||
|
room: PropTypes.instanceOf(Room).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props, context) {
|
static contextType = MatrixClientContext;
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const room = this.props.room;
|
||||||
const servers = new Set();
|
const servers = new Set();
|
||||||
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
|
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
|
||||||
this.servers = Array.from(servers).map(s =>
|
this.servers = Array.from(servers).map(s =>
|
||||||
|
@ -602,19 +614,14 @@ const Entries = [
|
||||||
ServersInRoomList,
|
ServersInRoomList,
|
||||||
];
|
];
|
||||||
|
|
||||||
export default class DevtoolsDialog extends React.Component {
|
export default class DevtoolsDialog extends React.PureComponent {
|
||||||
static childContextTypes = {
|
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
// client: PropTypes.instanceOf(MatixClient),
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
roomId: PropTypes.string.isRequired,
|
roomId: PropTypes.string.isRequired,
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context);
|
super(props);
|
||||||
this.onBack = this.onBack.bind(this);
|
this.onBack = this.onBack.bind(this);
|
||||||
this.onCancel = this.onCancel.bind(this);
|
this.onCancel = this.onCancel.bind(this);
|
||||||
|
|
||||||
|
@ -627,10 +634,6 @@ export default class DevtoolsDialog extends React.Component {
|
||||||
this._unmounted = true;
|
this._unmounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getChildContext() {
|
|
||||||
return { roomId: this.props.roomId };
|
|
||||||
}
|
|
||||||
|
|
||||||
_setMode(mode) {
|
_setMode(mode) {
|
||||||
return () => {
|
return () => {
|
||||||
this.setState({ mode });
|
this.setState({ mode });
|
||||||
|
@ -654,15 +657,17 @@ export default class DevtoolsDialog extends React.Component {
|
||||||
let body;
|
let body;
|
||||||
|
|
||||||
if (this.state.mode) {
|
if (this.state.mode) {
|
||||||
body = <div>
|
body = <MatrixClientContext.Consumer>
|
||||||
|
{(cli) => <React.Fragment>
|
||||||
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</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_right">Room ID: { this.props.roomId }</div>
|
||||||
<div className="mx_DevTools_label_bottom" />
|
<div className="mx_DevTools_label_bottom" />
|
||||||
<this.state.mode onBack={this.onBack} />
|
<this.state.mode onBack={this.onBack} room={cli.getRoom(this.props.roomId)} />
|
||||||
</div>;
|
</React.Fragment>}
|
||||||
|
</MatrixClientContext.Consumer>;
|
||||||
} else {
|
} else {
|
||||||
const classes = "mx_DevTools_RoomStateExplorer_button";
|
const classes = "mx_DevTools_RoomStateExplorer_button";
|
||||||
body = <div>
|
body = <React.Fragment>
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
|
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
|
||||||
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</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">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.onCancel}>{ _t('Cancel') }</button>
|
<button onClick={this.onCancel}>{ _t('Cancel') }</button>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
|
@ -99,7 +99,7 @@ export default createReactClass({
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
}, null, /* priority = */ false, /* static = */ true);
|
||||||
},
|
},
|
||||||
|
|
||||||
_onShareClicked: function() {
|
_onShareClicked: function() {
|
||||||
|
|
|
@ -30,8 +30,8 @@ export default class ReportEventDialog extends PureComponent {
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
reason: "",
|
reason: "",
|
||||||
|
|
|
@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { KeyCode } from '../../../Keyboard';
|
import { Key } from '../../../Keyboard';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
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() {
|
componentDidMount: function() {
|
||||||
this.refs.input_value.select();
|
this._input_value.current.select();
|
||||||
|
|
||||||
this._matrixClient = MatrixClientPeg.get();
|
this._matrixClient = MatrixClientPeg.get();
|
||||||
},
|
},
|
||||||
|
@ -96,14 +101,14 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyUp: function(ev) {
|
onKeyUp: function(ev) {
|
||||||
if (ev.keyCode === KeyCode.ENTER) {
|
if (ev.key === Key.ENTER) {
|
||||||
this.onSubmit();
|
this.onSubmit();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onSubmit: function(ev) {
|
onSubmit: function(ev) {
|
||||||
if (this.refs.uiAuth) {
|
if (this._uiAuth.current) {
|
||||||
this.refs.uiAuth.tryContinue();
|
this._uiAuth.current.tryContinue();
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
doingUIAuth: true,
|
doingUIAuth: true,
|
||||||
|
@ -215,7 +220,7 @@ export default createReactClass({
|
||||||
onAuthFinished={this._onUIAuthFinished}
|
onAuthFinished={this._onUIAuthFinished}
|
||||||
inputs={{}}
|
inputs={{}}
|
||||||
poll={true}
|
poll={true}
|
||||||
ref="uiAuth"
|
ref={this._uiAuth}
|
||||||
continueIsManaged={true}
|
continueIsManaged={true}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
@ -257,7 +262,7 @@ export default createReactClass({
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||||
<div className="mx_SetMxIdDialog_input_group">
|
<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}
|
autoFocus={true}
|
||||||
onChange={this.onValueChange}
|
onChange={this.onValueChange}
|
||||||
onKeyUp={this.onKeyUp}
|
onKeyUp={this.onKeyUp}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
@ -74,6 +74,8 @@ export default class ShareDialog extends React.Component {
|
||||||
// MatrixEvent defaults to share linkSpecificEvent
|
// MatrixEvent defaults to share linkSpecificEvent
|
||||||
linkSpecificEvent: this.props.target instanceof MatrixEvent,
|
linkSpecificEvent: this.props.target instanceof MatrixEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._link = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
static _selectText(target) {
|
static _selectText(target) {
|
||||||
|
@ -94,7 +96,7 @@ export default class ShareDialog extends React.Component {
|
||||||
onCopyClick(e) {
|
onCopyClick(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
ShareDialog._selectText(this.refs.link);
|
ShareDialog._selectText(this._link.current);
|
||||||
|
|
||||||
let successful;
|
let successful;
|
||||||
try {
|
try {
|
||||||
|
@ -106,7 +108,7 @@ export default class ShareDialog extends React.Component {
|
||||||
const buttonRect = e.target.getBoundingClientRect();
|
const buttonRect = e.target.getBoundingClientRect();
|
||||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||||
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||||
...toRightOf(buttonRect, 11),
|
...toRightOf(buttonRect, 2),
|
||||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||||
});
|
});
|
||||||
// Drop a reference to this close handler for componentWillUnmount
|
// Drop a reference to this close handler for componentWillUnmount
|
||||||
|
@ -195,7 +197,7 @@ export default class ShareDialog extends React.Component {
|
||||||
>
|
>
|
||||||
<div className="mx_ShareDialog_content">
|
<div className="mx_ShareDialog_content">
|
||||||
<div className="mx_ShareDialog_matrixto">
|
<div className="mx_ShareDialog_matrixto">
|
||||||
<a ref="link"
|
<a ref={this._link}
|
||||||
href={matrixToUrl}
|
href={matrixToUrl}
|
||||||
onClick={ShareDialog.onLinkClick}
|
onClick={ShareDialog.onLinkClick}
|
||||||
className="mx_ShareDialog_matrixto_link"
|
className="mx_ShareDialog_matrixto_link"
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
@ -42,15 +42,19 @@ export default createReactClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
UNSAFE_componentWillMount: function() {
|
||||||
|
this._textinput = createRef();
|
||||||
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
if (this.props.focus) {
|
if (this.props.focus) {
|
||||||
// Set the cursor at the end of the text input
|
// 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() {
|
onOk: function() {
|
||||||
this.props.onFinished(true, this.refs.textinput.value);
|
this.props.onFinished(true, this._textinput.current.value);
|
||||||
},
|
},
|
||||||
|
|
||||||
onCancel: function() {
|
onCancel: function() {
|
||||||
|
@ -70,7 +74,13 @@ export default createReactClass({
|
||||||
<label htmlFor="textinput"> { this.props.description } </label>
|
<label htmlFor="textinput"> { this.props.description } </label>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
|
||||||
import sdk from '../../../../index';
|
import sdk from '../../../../index';
|
||||||
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
|
@ -28,12 +27,13 @@ import {Key} from "../../../../Keyboard";
|
||||||
const RESTORE_TYPE_PASSPHRASE = 0;
|
const RESTORE_TYPE_PASSPHRASE = 0;
|
||||||
const RESTORE_TYPE_RECOVERYKEY = 1;
|
const RESTORE_TYPE_RECOVERYKEY = 1;
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Dialog for restoring e2e keys from a backup and the user's recovery key
|
* Dialog for restoring e2e keys from a backup and the user's recovery key
|
||||||
*/
|
*/
|
||||||
export default createReactClass({
|
export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
getInitialState: function() {
|
constructor(props) {
|
||||||
return {
|
super(props);
|
||||||
|
this.state = {
|
||||||
backupInfo: null,
|
backupInfo: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
loadError: null,
|
loadError: null,
|
||||||
|
@ -45,27 +45,27 @@ export default createReactClass({
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
restoreType: null,
|
restoreType: null,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentDidMount() {
|
||||||
this._loadBackupStatus();
|
this._loadBackupStatus();
|
||||||
},
|
}
|
||||||
|
|
||||||
_onCancel: function() {
|
_onCancel = () => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
},
|
}
|
||||||
|
|
||||||
_onDone: function() {
|
_onDone = () => {
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
},
|
}
|
||||||
|
|
||||||
_onUseRecoveryKeyClick: function() {
|
_onUseRecoveryKeyClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
forceRecoveryKey: true,
|
forceRecoveryKey: true,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_onResetRecoveryClick: function() {
|
_onResetRecoveryClick = () => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||||
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||||
|
@ -75,16 +75,16 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
_onRecoveryKeyChange: function(e) {
|
_onRecoveryKeyChange = (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
recoveryKey: e.target.value,
|
recoveryKey: e.target.value,
|
||||||
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
|
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_onPassPhraseNext: async function() {
|
_onPassPhraseNext = async () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: true,
|
loading: true,
|
||||||
restoreError: null,
|
restoreError: null,
|
||||||
|
@ -105,9 +105,9 @@ export default createReactClass({
|
||||||
restoreError: e,
|
restoreError: e,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_onRecoveryKeyNext: async function() {
|
_onRecoveryKeyNext = async () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: true,
|
loading: true,
|
||||||
restoreError: null,
|
restoreError: null,
|
||||||
|
@ -128,27 +128,27 @@ export default createReactClass({
|
||||||
restoreError: e,
|
restoreError: e,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_onPassPhraseChange: function(e) {
|
_onPassPhraseChange = (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
passPhrase: e.target.value,
|
passPhrase: e.target.value,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_onPassPhraseKeyPress: function(e) {
|
_onPassPhraseKeyPress = (e) => {
|
||||||
if (e.key === Key.ENTER) {
|
if (e.key === Key.ENTER) {
|
||||||
this._onPassPhraseNext();
|
this._onPassPhraseNext();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_onRecoveryKeyKeyPress: function(e) {
|
_onRecoveryKeyKeyPress = (e) => {
|
||||||
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
|
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
|
||||||
this._onRecoveryKeyNext();
|
this._onRecoveryKeyNext();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_loadBackupStatus: async function() {
|
async _loadBackupStatus() {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: true,
|
loading: true,
|
||||||
loadError: null,
|
loadError: null,
|
||||||
|
@ -167,9 +167,9 @@ export default createReactClass({
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
render: function() {
|
render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
|
@ -296,7 +296,7 @@ export default createReactClass({
|
||||||
|
|
||||||
content = <div>
|
content = <div>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"<b>Warning</b>: you should only set up key backup " +
|
"<b>Warning</b>: You should only set up key backup " +
|
||||||
"from a trusted computer.", {},
|
"from a trusted computer.", {},
|
||||||
{ b: sub => <b>{sub}</b> },
|
{ b: sub => <b>{sub}</b> },
|
||||||
)}</p>
|
)}</p>
|
||||||
|
@ -322,7 +322,7 @@ export default createReactClass({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{_t(
|
{_t(
|
||||||
"If you've forgotten your recovery passphrase you can "+
|
"If you've forgotten your recovery key you can "+
|
||||||
"<button>set up new recovery options</button>"
|
"<button>set up new recovery options</button>"
|
||||||
, {}, {
|
, {}, {
|
||||||
button: s => <AccessibleButton className="mx_linkButton"
|
button: s => <AccessibleButton className="mx_linkButton"
|
||||||
|
@ -345,5 +345,5 @@ export default createReactClass({
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
|
|
@ -0,0 +1,265 @@
|
||||||
|
/*
|
||||||
|
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 PropTypes from "prop-types";
|
||||||
|
import sdk from '../../../../index';
|
||||||
|
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
||||||
|
|
||||||
|
import { _t } from '../../../../languageHandler';
|
||||||
|
import { Key } from "../../../../Keyboard";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Access Secure Secret Storage by requesting the user's passphrase.
|
||||||
|
*/
|
||||||
|
export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
// { passphrase, pubkey }
|
||||||
|
keyInfo: PropTypes.object.isRequired,
|
||||||
|
// Function from one of { passphrase, recoveryKey } -> boolean
|
||||||
|
checkPrivateKey: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
recoveryKey: "",
|
||||||
|
recoveryKeyValid: false,
|
||||||
|
forceRecoveryKey: false,
|
||||||
|
passPhrase: '',
|
||||||
|
keyMatches: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_onCancel = () => {
|
||||||
|
this.props.onFinished(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onUseRecoveryKeyClick = () => {
|
||||||
|
this.setState({
|
||||||
|
forceRecoveryKey: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onResetRecoveryClick = () => {
|
||||||
|
this.props.onFinished(false);
|
||||||
|
throw new Error("Resetting secret storage unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRecoveryKeyChange = (e) => {
|
||||||
|
this.setState({
|
||||||
|
recoveryKey: e.target.value,
|
||||||
|
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
|
||||||
|
keyMatches: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onPassPhraseNext = async () => {
|
||||||
|
this.setState({ keyMatches: null });
|
||||||
|
const input = { passphrase: this.state.passPhrase };
|
||||||
|
const keyMatches = await this.props.checkPrivateKey(input);
|
||||||
|
if (keyMatches) {
|
||||||
|
this.props.onFinished(input);
|
||||||
|
} else {
|
||||||
|
this.setState({ keyMatches });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRecoveryKeyNext = async () => {
|
||||||
|
this.setState({ keyMatches: null });
|
||||||
|
const input = { recoveryKey: this.state.recoveryKey };
|
||||||
|
const keyMatches = await this.props.checkPrivateKey(input);
|
||||||
|
if (keyMatches) {
|
||||||
|
this.props.onFinished(input);
|
||||||
|
} else {
|
||||||
|
this.setState({ keyMatches });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onPassPhraseChange = (e) => {
|
||||||
|
this.setState({
|
||||||
|
passPhrase: e.target.value,
|
||||||
|
keyMatches: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onPassPhraseKeyPress = (e) => {
|
||||||
|
if (e.key === Key.ENTER && this.state.passPhrase.length > 0) {
|
||||||
|
this._onPassPhraseNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRecoveryKeyKeyPress = (e) => {
|
||||||
|
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
|
||||||
|
this._onRecoveryKeyNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
||||||
|
const hasPassphrase = (
|
||||||
|
this.props.keyInfo &&
|
||||||
|
this.props.keyInfo.passphrase &&
|
||||||
|
this.props.keyInfo.passphrase.salt &&
|
||||||
|
this.props.keyInfo.passphrase.iterations
|
||||||
|
);
|
||||||
|
|
||||||
|
let content;
|
||||||
|
let title;
|
||||||
|
if (hasPassphrase && !this.state.forceRecoveryKey) {
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
title = _t("Enter secret storage passphrase");
|
||||||
|
|
||||||
|
let keyStatus;
|
||||||
|
if (this.state.keyMatches === false) {
|
||||||
|
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
||||||
|
{"\uD83D\uDC4E "}{_t(
|
||||||
|
"Unable to access secret storage. Please verify that you " +
|
||||||
|
"entered the correct passphrase.",
|
||||||
|
)}
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = <div>
|
||||||
|
<p>{_t(
|
||||||
|
"<b>Warning</b>: You should only access secret storage " +
|
||||||
|
"from a trusted computer.", {},
|
||||||
|
{ b: sub => <b>{sub}</b> },
|
||||||
|
)}</p>
|
||||||
|
<p>{_t(
|
||||||
|
"Access your secure message history and your cross-signing " +
|
||||||
|
"identity for verifying other devices by entering your passphrase.",
|
||||||
|
)}</p>
|
||||||
|
|
||||||
|
<div className="mx_AccessSecretStorageDialog_primaryContainer">
|
||||||
|
<input type="password"
|
||||||
|
className="mx_AccessSecretStorageDialog_passPhraseInput"
|
||||||
|
onChange={this._onPassPhraseChange}
|
||||||
|
onKeyPress={this._onPassPhraseKeyPress}
|
||||||
|
value={this.state.passPhrase}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
{keyStatus}
|
||||||
|
<DialogButtons primaryButton={_t('Next')}
|
||||||
|
onPrimaryButtonClick={this._onPassPhraseNext}
|
||||||
|
hasCancel={true}
|
||||||
|
onCancel={this._onCancel}
|
||||||
|
focus={false}
|
||||||
|
primaryDisabled={this.state.passPhrase.length === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{_t(
|
||||||
|
"If you've forgotten your passphrase you can "+
|
||||||
|
"<button1>use your recovery key</button1> or " +
|
||||||
|
"<button2>set up new recovery options</button2>."
|
||||||
|
, {}, {
|
||||||
|
button1: s => <AccessibleButton className="mx_linkButton"
|
||||||
|
element="span"
|
||||||
|
onClick={this._onUseRecoveryKeyClick}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</AccessibleButton>,
|
||||||
|
button2: s => <AccessibleButton className="mx_linkButton"
|
||||||
|
element="span"
|
||||||
|
onClick={this._onResetRecoveryClick}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</AccessibleButton>,
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
title = _t("Enter secret storage recovery key");
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
|
||||||
|
let keyStatus;
|
||||||
|
if (this.state.recoveryKey.length === 0) {
|
||||||
|
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"></div>;
|
||||||
|
} else if (this.state.recoveryKeyValid) {
|
||||||
|
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
||||||
|
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
|
||||||
|
</div>;
|
||||||
|
} else if (this.state.keyMatches === false) {
|
||||||
|
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
||||||
|
{"\uD83D\uDC4E "}{_t(
|
||||||
|
"Unable to access secret storage. Please verify that you " +
|
||||||
|
"entered the correct recovery key.",
|
||||||
|
)}
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
||||||
|
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = <div>
|
||||||
|
<p>{_t(
|
||||||
|
"<b>Warning</b>: You should only access secret storage " +
|
||||||
|
"from a trusted computer.", {},
|
||||||
|
{ b: sub => <b>{sub}</b> },
|
||||||
|
)}</p>
|
||||||
|
<p>{_t(
|
||||||
|
"Access your secure message history and your cross-signing " +
|
||||||
|
"identity for verifying other devices by entering your recovery key.",
|
||||||
|
)}</p>
|
||||||
|
|
||||||
|
<div className="mx_AccessSecretStorageDialog_primaryContainer">
|
||||||
|
<input className="mx_AccessSecretStorageDialog_recoveryKeyInput"
|
||||||
|
onChange={this._onRecoveryKeyChange}
|
||||||
|
onKeyPress={this._onRecoveryKeyKeyPress}
|
||||||
|
value={this.state.recoveryKey}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
{keyStatus}
|
||||||
|
<DialogButtons primaryButton={_t('Next')}
|
||||||
|
onPrimaryButtonClick={this._onRecoveryKeyNext}
|
||||||
|
hasCancel={true}
|
||||||
|
onCancel={this._onCancel}
|
||||||
|
focus={false}
|
||||||
|
primaryDisabled={!this.state.recoveryKeyValid}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{_t(
|
||||||
|
"If you've forgotten your recovery key you can "+
|
||||||
|
"<button>set up new recovery options</button>."
|
||||||
|
, {}, {
|
||||||
|
button: s => <AccessibleButton className="mx_linkButton"
|
||||||
|
element="span"
|
||||||
|
onClick={this._onResetRecoveryClick}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</AccessibleButton>,
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog className='mx_AccessSecretStorageDialog'
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { KeyCode } from '../../../Keyboard';
|
import {Key} from '../../../Keyboard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AccessibleButton is a generic wrapper for any element that should be treated
|
* AccessibleButton is a generic wrapper for any element that should be treated
|
||||||
|
@ -40,23 +40,23 @@ export default function AccessibleButton(props) {
|
||||||
// Browsers handle space and enter keypresses differently and we are only adjusting to the
|
// Browsers handle space and enter keypresses differently and we are only adjusting to the
|
||||||
// inconsistencies here
|
// inconsistencies here
|
||||||
restProps.onKeyDown = function(e) {
|
restProps.onKeyDown = function(e) {
|
||||||
if (e.keyCode === KeyCode.ENTER) {
|
if (e.key === Key.ENTER) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return onClick(e);
|
return onClick(e);
|
||||||
}
|
}
|
||||||
if (e.keyCode === KeyCode.SPACE) {
|
if (e.key === Key.SPACE) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
restProps.onKeyUp = function(e) {
|
restProps.onKeyUp = function(e) {
|
||||||
if (e.keyCode === KeyCode.SPACE) {
|
if (e.key === Key.SPACE) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return onClick(e);
|
return onClick(e);
|
||||||
}
|
}
|
||||||
if (e.keyCode === KeyCode.ENTER) {
|
if (e.key === Key.ENTER) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ export default class AccessibleTooltipButton extends React.PureComponent {
|
||||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||||
|
|
||||||
const {title, ...props} = this.props;
|
const {title, children, ...props} = this.props;
|
||||||
|
|
||||||
const tip = this.state.hover ? <Tooltip
|
const tip = this.state.hover ? <Tooltip
|
||||||
className="mx_AccessibleTooltipButton_container"
|
className="mx_AccessibleTooltipButton_container"
|
||||||
|
@ -57,6 +57,7 @@ export default class AccessibleTooltipButton extends React.PureComponent {
|
||||||
/> : <div />;
|
/> : <div />;
|
||||||
return (
|
return (
|
||||||
<AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} aria-label={title}>
|
<AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} aria-label={title}>
|
||||||
|
{ children }
|
||||||
{ tip }
|
{ tip }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
|
|
@ -64,6 +64,8 @@ export default class AppTile extends React.Component {
|
||||||
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
|
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
|
||||||
|
|
||||||
this._contextMenuButton = createRef();
|
this._contextMenuButton = createRef();
|
||||||
|
this._appFrame = createRef();
|
||||||
|
this._menu_bar = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -337,14 +339,14 @@ export default class AppTile extends React.Component {
|
||||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||||
// its hold on the webcam. Without this, the widget holds a media
|
// its hold on the webcam. Without this, the widget holds a media
|
||||||
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
|
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
|
||||||
if (this.refs.appFrame) {
|
if (this._appFrame.current) {
|
||||||
// In practice we could just do `+= ''` to trick the browser
|
// In practice we could just do `+= ''` to trick the browser
|
||||||
// into thinking the URL changed, however I can foresee this
|
// into thinking the URL changed, however I can foresee this
|
||||||
// being optimized out by a browser. Instead, we'll just point
|
// being optimized out by a browser. Instead, we'll just point
|
||||||
// the iframe at a page that is reasonably safe to use in the
|
// the iframe at a page that is reasonably safe to use in the
|
||||||
// event the iframe doesn't wink away.
|
// event the iframe doesn't wink away.
|
||||||
// This is relative to where the Riot instance is located.
|
// This is relative to where the Riot instance is located.
|
||||||
this.refs.appFrame.src = 'about:blank';
|
this._appFrame.current.src = 'about:blank';
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetUtils.setRoomWidget(
|
WidgetUtils.setRoomWidget(
|
||||||
|
@ -389,7 +391,7 @@ export default class AppTile extends React.Component {
|
||||||
// FIXME: There's probably no reason to do this here: it should probably be done entirely
|
// FIXME: There's probably no reason to do this here: it should probably be done entirely
|
||||||
// in ActiveWidgetStore.
|
// in ActiveWidgetStore.
|
||||||
const widgetMessaging = new WidgetMessaging(
|
const widgetMessaging = new WidgetMessaging(
|
||||||
this.props.id, this.props.url, this.props.userWidget, this.refs.appFrame.contentWindow);
|
this.props.id, this.props.url, this.props.userWidget, this._appFrame.current.contentWindow);
|
||||||
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
|
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
|
||||||
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||||
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
|
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
|
||||||
|
@ -496,7 +498,7 @@ export default class AppTile extends React.Component {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
// Ignore clicks on menu bar children
|
// Ignore clicks on menu bar children
|
||||||
if (ev.target !== this.refs.menu_bar) {
|
if (ev.target !== this._menu_bar.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -555,7 +557,7 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
_onReloadWidgetClick() {
|
_onReloadWidgetClick() {
|
||||||
// Reload iframe in this way to avoid cross-origin restrictions
|
// Reload iframe in this way to avoid cross-origin restrictions
|
||||||
this.refs.appFrame.src = this.refs.appFrame.src;
|
this._appFrame.current.src = this._appFrame.current.src;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onContextMenuClick = () => {
|
_onContextMenuClick = () => {
|
||||||
|
@ -626,7 +628,7 @@ export default class AppTile extends React.Component {
|
||||||
{ this.state.loading && loadingElement }
|
{ this.state.loading && loadingElement }
|
||||||
<iframe
|
<iframe
|
||||||
allow={iframeFeatures}
|
allow={iframeFeatures}
|
||||||
ref="appFrame"
|
ref={this._appFrame}
|
||||||
src={this._getSafeUrl()}
|
src={this._getSafeUrl()}
|
||||||
allowFullScreen={true}
|
allowFullScreen={true}
|
||||||
sandbox={sandboxFlags}
|
sandbox={sandboxFlags}
|
||||||
|
@ -694,7 +696,7 @@ export default class AppTile extends React.Component {
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<div className={appTileClass} id={this.props.id}>
|
<div className={appTileClass} id={this.props.id}>
|
||||||
{ this.props.showMenubar &&
|
{ this.props.showMenubar &&
|
||||||
<div ref="menu_bar" className={menuBarClasses} onClick={this.onClickMenuBar}>
|
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
|
||||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||||
{ /* Minimise widget */ }
|
{ /* Minimise widget */ }
|
||||||
{ showMinimiseButton && <AccessibleButton
|
{ showMinimiseButton && <AccessibleButton
|
||||||
|
|
|
@ -59,7 +59,7 @@ export default createReactClass({
|
||||||
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
||||||
userId: this.props.userId,
|
userId: this.props.userId,
|
||||||
device: this.state.device,
|
device: this.state.device,
|
||||||
});
|
}, null, /* priority = */ false, /* static = */ true);
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnverifyClick: function() {
|
onUnverifyClick: function() {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,11 +16,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import AccessibleButton from './AccessibleButton';
|
import AccessibleButton from './AccessibleButton';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import {Key} from "../../../Keyboard";
|
||||||
|
|
||||||
class MenuOption extends React.Component {
|
class MenuOption extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -48,9 +50,14 @@ class MenuOption extends React.Component {
|
||||||
mx_Dropdown_option_highlight: this.props.highlighted,
|
mx_Dropdown_option_highlight: this.props.highlighted,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className={optClasses}
|
return <div
|
||||||
|
id={this.props.id}
|
||||||
|
className={optClasses}
|
||||||
onClick={this._onClick}
|
onClick={this._onClick}
|
||||||
onMouseEnter={this._onMouseEnter}
|
onMouseEnter={this._onMouseEnter}
|
||||||
|
role="option"
|
||||||
|
aria-selected={this.props.highlighted}
|
||||||
|
ref={this.props.inputRef}
|
||||||
>
|
>
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -66,6 +73,7 @@ MenuOption.propTypes = {
|
||||||
dropdownKey: PropTypes.string,
|
dropdownKey: PropTypes.string,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
onMouseEnter: PropTypes.func.isRequired,
|
onMouseEnter: PropTypes.func.isRequired,
|
||||||
|
inputRef: PropTypes.any,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -86,8 +94,6 @@ export default class Dropdown extends React.Component {
|
||||||
this._onRootClick = this._onRootClick.bind(this);
|
this._onRootClick = this._onRootClick.bind(this);
|
||||||
this._onDocumentClick = this._onDocumentClick.bind(this);
|
this._onDocumentClick = this._onDocumentClick.bind(this);
|
||||||
this._onMenuOptionClick = this._onMenuOptionClick.bind(this);
|
this._onMenuOptionClick = this._onMenuOptionClick.bind(this);
|
||||||
this._onInputKeyPress = this._onInputKeyPress.bind(this);
|
|
||||||
this._onInputKeyUp = this._onInputKeyUp.bind(this);
|
|
||||||
this._onInputChange = this._onInputChange.bind(this);
|
this._onInputChange = this._onInputChange.bind(this);
|
||||||
this._collectRoot = this._collectRoot.bind(this);
|
this._collectRoot = this._collectRoot.bind(this);
|
||||||
this._collectInputTextBox = this._collectInputTextBox.bind(this);
|
this._collectInputTextBox = this._collectInputTextBox.bind(this);
|
||||||
|
@ -111,6 +117,7 @@ export default class Dropdown extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
|
this._button = createRef();
|
||||||
// Listen for all clicks on the document so we can close the
|
// Listen for all clicks on the document so we can close the
|
||||||
// menu when the user clicks somewhere else
|
// menu when the user clicks somewhere else
|
||||||
document.addEventListener('click', this._onDocumentClick, false);
|
document.addEventListener('click', this._onDocumentClick, false);
|
||||||
|
@ -169,40 +176,49 @@ export default class Dropdown extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMenuOptionClick(dropdownKey) {
|
_close() {
|
||||||
this.setState({
|
this.setState({
|
||||||
expanded: false,
|
expanded: false,
|
||||||
});
|
});
|
||||||
|
// their focus was on the input, its getting unmounted, move it to the button
|
||||||
|
if (this._button.current) {
|
||||||
|
this._button.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMenuOptionClick(dropdownKey) {
|
||||||
|
this._close();
|
||||||
this.props.onOptionChange(dropdownKey);
|
this.props.onOptionChange(dropdownKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onInputKeyPress(e) {
|
_onInputKeyDown = (e) => {
|
||||||
// This needs to be on the keypress event because otherwise
|
let handled = true;
|
||||||
// it can't cancel the form submission
|
|
||||||
if (e.key == 'Enter') {
|
|
||||||
this.setState({
|
|
||||||
expanded: false,
|
|
||||||
});
|
|
||||||
this.props.onOptionChange(this.state.highlightedOption);
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onInputKeyUp(e) {
|
// These keys don't generate keypress events and so needs to be on keyup
|
||||||
// These keys don't generate keypress events and so needs to
|
switch (e.key) {
|
||||||
// be on keyup
|
case Key.ENTER:
|
||||||
if (e.key == 'Escape') {
|
this.props.onOptionChange(this.state.highlightedOption);
|
||||||
this.setState({
|
// fallthrough
|
||||||
expanded: false,
|
case Key.ESCAPE:
|
||||||
});
|
this._close();
|
||||||
} else if (e.key == 'ArrowDown') {
|
break;
|
||||||
|
case Key.ARROW_DOWN:
|
||||||
this.setState({
|
this.setState({
|
||||||
highlightedOption: this._nextOption(this.state.highlightedOption),
|
highlightedOption: this._nextOption(this.state.highlightedOption),
|
||||||
});
|
});
|
||||||
} else if (e.key == 'ArrowUp') {
|
break;
|
||||||
|
case Key.ARROW_UP:
|
||||||
this.setState({
|
this.setState({
|
||||||
highlightedOption: this._prevOption(this.state.highlightedOption),
|
highlightedOption: this._prevOption(this.state.highlightedOption),
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
handled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,20 +266,34 @@ export default class Dropdown extends React.Component {
|
||||||
return keys[(index - 1) % keys.length];
|
return keys[(index - 1) % keys.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_scrollIntoView(node) {
|
||||||
|
if (node) {
|
||||||
|
node.scrollIntoView({
|
||||||
|
block: "nearest",
|
||||||
|
behavior: "auto",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_getMenuOptions() {
|
_getMenuOptions() {
|
||||||
const options = React.Children.map(this.props.children, (child) => {
|
const options = React.Children.map(this.props.children, (child) => {
|
||||||
|
const highlighted = this.state.highlightedOption === child.key;
|
||||||
return (
|
return (
|
||||||
<MenuOption key={child.key} dropdownKey={child.key}
|
<MenuOption
|
||||||
highlighted={this.state.highlightedOption == child.key}
|
id={`${this.props.id}__${child.key}`}
|
||||||
|
key={child.key}
|
||||||
|
dropdownKey={child.key}
|
||||||
|
highlighted={highlighted}
|
||||||
onMouseEnter={this._setHighlightedOption}
|
onMouseEnter={this._setHighlightedOption}
|
||||||
onClick={this._onMenuOptionClick}
|
onClick={this._onMenuOptionClick}
|
||||||
|
inputRef={highlighted ? this._scrollIntoView : undefined}
|
||||||
>
|
>
|
||||||
{ child }
|
{ child }
|
||||||
</MenuOption>
|
</MenuOption>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (options.length === 0) {
|
if (options.length === 0) {
|
||||||
return [<div key="0" className="mx_Dropdown_option">
|
return [<div key="0" className="mx_Dropdown_option" role="option">
|
||||||
{ _t("No results") }
|
{ _t("No results") }
|
||||||
</div>];
|
</div>];
|
||||||
}
|
}
|
||||||
|
@ -279,23 +309,35 @@ export default class Dropdown extends React.Component {
|
||||||
let menu;
|
let menu;
|
||||||
if (this.state.expanded) {
|
if (this.state.expanded) {
|
||||||
if (this.props.searchEnabled) {
|
if (this.props.searchEnabled) {
|
||||||
currentValue = <input type="text" className="mx_Dropdown_option"
|
currentValue = (
|
||||||
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
|
<input
|
||||||
onKeyUp={this._onInputKeyUp}
|
type="text"
|
||||||
|
className="mx_Dropdown_option"
|
||||||
|
ref={this._collectInputTextBox}
|
||||||
|
onKeyDown={this._onInputKeyDown}
|
||||||
onChange={this._onInputChange}
|
onChange={this._onInputChange}
|
||||||
value={this.state.searchQuery}
|
value={this.state.searchQuery}
|
||||||
/>;
|
role="combobox"
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-activedescendant={`${this.props.id}__${this.state.highlightedOption}`}
|
||||||
|
aria-owns={`${this.props.id}_listbox`}
|
||||||
|
aria-disabled={this.props.disabled}
|
||||||
|
aria-label={this.props.label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
menu = <div className="mx_Dropdown_menu" style={menuStyle}>
|
menu = (
|
||||||
|
<div className="mx_Dropdown_menu" style={menuStyle} role="listbox" id={`${this.props.id}_listbox`}>
|
||||||
{ this._getMenuOptions() }
|
{ this._getMenuOptions() }
|
||||||
</div>;
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentValue) {
|
if (!currentValue) {
|
||||||
const selectedChild = this.props.getShortOption ?
|
const selectedChild = this.props.getShortOption ?
|
||||||
this.props.getShortOption(this.props.value) :
|
this.props.getShortOption(this.props.value) :
|
||||||
this.childrenByKey[this.props.value];
|
this.childrenByKey[this.props.value];
|
||||||
currentValue = <div className="mx_Dropdown_option">
|
currentValue = <div className="mx_Dropdown_option" id={`${this.props.id}_value`}>
|
||||||
{ selectedChild }
|
{ selectedChild }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -311,9 +353,18 @@ export default class Dropdown extends React.Component {
|
||||||
// Note the menu sits inside the AccessibleButton div so it's anchored
|
// Note the menu sits inside the AccessibleButton div so it's anchored
|
||||||
// to the input, but overflows below it. The root contains both.
|
// to the input, but overflows below it. The root contains both.
|
||||||
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
|
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
|
||||||
<AccessibleButton className="mx_Dropdown_input mx_no_textinput" onClick={this._onInputClick}>
|
<AccessibleButton
|
||||||
|
className="mx_Dropdown_input mx_no_textinput"
|
||||||
|
onClick={this._onInputClick}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={this.state.expanded}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
inputRef={this._button}
|
||||||
|
aria-label={this.props.label}
|
||||||
|
aria-describedby={`${this.props.id}_value`}
|
||||||
|
>
|
||||||
{ currentValue }
|
{ currentValue }
|
||||||
<span className="mx_Dropdown_arrow"></span>
|
<span className="mx_Dropdown_arrow" />
|
||||||
{ menu }
|
{ menu }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -321,6 +372,7 @@ export default class Dropdown extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
Dropdown.propTypes = {
|
Dropdown.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
// The width that the dropdown should be. If specified,
|
// The width that the dropdown should be. If specified,
|
||||||
// the dropped-down part of the menu will be set to this
|
// the dropped-down part of the menu will be set to this
|
||||||
// width.
|
// width.
|
||||||
|
@ -340,4 +392,6 @@ Dropdown.propTypes = {
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
// negative for consistency with HTML
|
// negative for consistency with HTML
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
// ARIA label
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
import {Key} from "../../../Keyboard";
|
import {Key} from "../../../Keyboard";
|
||||||
|
@ -65,7 +65,7 @@ module.exports = createReactClass({
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
if (nextProps.initialValue !== this.props.initialValue) {
|
if (nextProps.initialValue !== this.props.initialValue) {
|
||||||
this.value = nextProps.initialValue;
|
this.value = nextProps.initialValue;
|
||||||
if (this.refs.editable_div) {
|
if (this._editable_div.current) {
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,24 +76,27 @@ module.exports = createReactClass({
|
||||||
// as React doesn't play nice with contentEditable.
|
// as React doesn't play nice with contentEditable.
|
||||||
this.value = '';
|
this.value = '';
|
||||||
this.placeholder = false;
|
this.placeholder = false;
|
||||||
|
|
||||||
|
this._editable_div = createRef();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.value = this.props.initialValue;
|
this.value = this.props.initialValue;
|
||||||
if (this.refs.editable_div) {
|
if (this._editable_div.current) {
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
showPlaceholder: function(show) {
|
showPlaceholder: function(show) {
|
||||||
if (show) {
|
if (show) {
|
||||||
this.refs.editable_div.textContent = this.props.placeholder;
|
this._editable_div.current.textContent = this.props.placeholder;
|
||||||
this.refs.editable_div.setAttribute("class", this.props.className + " " + this.props.placeholderClassName);
|
this._editable_div.current.setAttribute("class", this.props.className
|
||||||
|
+ " " + this.props.placeholderClassName);
|
||||||
this.placeholder = true;
|
this.placeholder = true;
|
||||||
this.value = '';
|
this.value = '';
|
||||||
} else {
|
} else {
|
||||||
this.refs.editable_div.textContent = this.value;
|
this._editable_div.current.textContent = this.value;
|
||||||
this.refs.editable_div.setAttribute("class", this.props.className);
|
this._editable_div.current.setAttribute("class", this.props.className);
|
||||||
this.placeholder = false;
|
this.placeholder = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -120,7 +123,7 @@ module.exports = createReactClass({
|
||||||
this.value = this.props.initialValue;
|
this.value = this.props.initialValue;
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
this.onValueChanged(false);
|
this.onValueChanged(false);
|
||||||
this.refs.editable_div.blur();
|
this._editable_div.current.blur();
|
||||||
},
|
},
|
||||||
|
|
||||||
onValueChanged: function(shouldSubmit) {
|
onValueChanged: function(shouldSubmit) {
|
||||||
|
@ -219,7 +222,7 @@ module.exports = createReactClass({
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||||
editableEl = <div ref="editable_div"
|
editableEl = <div ref={this._editable_div}
|
||||||
contentEditable={true}
|
contentEditable={true}
|
||||||
className={className}
|
className={className}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
|
|
|
@ -25,13 +25,13 @@ import sdk from '../../../index';
|
||||||
* Parent components should supply an 'onSubmit' callback which returns a
|
* Parent components should supply an 'onSubmit' callback which returns a
|
||||||
* promise; a spinner is shown until the promise resolves.
|
* promise; a spinner is shown until the promise resolves.
|
||||||
*
|
*
|
||||||
* The parent can also supply a 'getIntialValue' callback, which works in a
|
* The parent can also supply a 'getInitialValue' callback, which works in a
|
||||||
* similarly asynchronous way. If this is not provided, the initial value is
|
* similarly asynchronous way. If this is not provided, the initial value is
|
||||||
* taken from the 'initialValue' property.
|
* taken from the 'initialValue' property.
|
||||||
*/
|
*/
|
||||||
export default class EditableTextContainer extends React.Component {
|
export default class EditableTextContainer extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context);
|
super(props);
|
||||||
|
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
this.state = {
|
this.state = {
|
||||||
|
|
|
@ -66,10 +66,14 @@ export default class Field extends React.PureComponent {
|
||||||
this.state = {
|
this.state = {
|
||||||
valid: undefined,
|
valid: undefined,
|
||||||
feedback: undefined,
|
feedback: undefined,
|
||||||
|
focused: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onFocus = (ev) => {
|
onFocus = (ev) => {
|
||||||
|
this.setState({
|
||||||
|
focused: true,
|
||||||
|
});
|
||||||
this.validate({
|
this.validate({
|
||||||
focused: true,
|
focused: true,
|
||||||
});
|
});
|
||||||
|
@ -88,6 +92,9 @@ export default class Field extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
onBlur = (ev) => {
|
onBlur = (ev) => {
|
||||||
|
this.setState({
|
||||||
|
focused: false,
|
||||||
|
});
|
||||||
this.validate({
|
this.validate({
|
||||||
focused: false,
|
focused: false,
|
||||||
});
|
});
|
||||||
|
@ -112,7 +119,9 @@ export default class Field extends React.PureComponent {
|
||||||
allowEmpty,
|
allowEmpty,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (feedback) {
|
// this method is async and so we may have been blurred since the method was called
|
||||||
|
// if we have then hide the feedback as withValidation does
|
||||||
|
if (this.state.focused && feedback) {
|
||||||
this.setState({
|
this.setState({
|
||||||
valid,
|
valid,
|
||||||
feedback,
|
feedback,
|
||||||
|
|
|
@ -18,9 +18,9 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {MatrixClient} from 'matrix-js-sdk';
|
|
||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
|
|
||||||
class FlairAvatar extends React.Component {
|
class FlairAvatar extends React.Component {
|
||||||
|
@ -40,7 +40,7 @@ class FlairAvatar extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const httpUrl = this.context.matrixClient.mxcUrlToHttp(
|
const httpUrl = this.context.mxcUrlToHttp(
|
||||||
this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
|
this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
|
||||||
const tooltip = this.props.groupProfile.name ?
|
const tooltip = this.props.groupProfile.name ?
|
||||||
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
|
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
|
||||||
|
@ -62,9 +62,7 @@ FlairAvatar.propTypes = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
FlairAvatar.contextTypes = {
|
FlairAvatar.contextType = MatrixClientContext;
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class Flair extends React.Component {
|
export default class Flair extends React.Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -92,7 +90,7 @@ export default class Flair extends React.Component {
|
||||||
for (const groupId of groups) {
|
for (const groupId of groups) {
|
||||||
let groupProfile = null;
|
let groupProfile = null;
|
||||||
try {
|
try {
|
||||||
groupProfile = await FlairStore.getGroupProfileCached(this.context.matrixClient, groupId);
|
groupProfile = await FlairStore.getGroupProfileCached(this.context, groupId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Could not get profile for group', groupId, err);
|
console.error('Could not get profile for group', groupId, err);
|
||||||
}
|
}
|
||||||
|
@ -134,6 +132,4 @@ Flair.propTypes = {
|
||||||
groups: PropTypes.arrayOf(PropTypes.string),
|
groups: PropTypes.arrayOf(PropTypes.string),
|
||||||
};
|
};
|
||||||
|
|
||||||
Flair.contextTypes = {
|
Flair.contextType = MatrixClientContext;
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import sdk from '../../../index';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
|
|
||||||
const GroupsButton = function(props) {
|
|
||||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
|
||||||
return (
|
|
||||||
<ActionButton className="mx_GroupsButton" action="toggle_my_groups"
|
|
||||||
label={_t("Communities")}
|
|
||||||
size={props.size}
|
|
||||||
tooltip={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
GroupsButton.propTypes = {
|
|
||||||
size: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GroupsButton;
|
|
|
@ -28,6 +28,7 @@ const AccessibleButton = require('../../../components/views/elements/AccessibleB
|
||||||
const Modal = require('../../../Modal');
|
const Modal = require('../../../Modal');
|
||||||
const sdk = require('../../../index');
|
const sdk = require('../../../index');
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import {Key} from "../../../Keyboard";
|
||||||
|
|
||||||
export default class ImageView extends React.Component {
|
export default class ImageView extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -62,7 +63,7 @@ export default class ImageView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown = (ev) => {
|
onKeyDown = (ev) => {
|
||||||
if (ev.keyCode === 27) { // escape
|
if (ev.key === Key.ESCAPE) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
|
|
|
@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import * as languageHandler from '../../../languageHandler';
|
import * as languageHandler from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
|
||||||
function languageMatchesSearchQuery(query, language) {
|
function languageMatchesSearchQuery(query, language) {
|
||||||
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
||||||
|
@ -105,9 +106,14 @@ export default class LanguageDropdown extends React.Component {
|
||||||
value = this.props.value || language;
|
value = this.props.value || language;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Dropdown className={this.props.className}
|
return <Dropdown
|
||||||
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
|
id="mx_LanguageDropdown"
|
||||||
searchEnabled={true} value={value}
|
className={this.props.className}
|
||||||
|
onOptionChange={this.props.onOptionChange}
|
||||||
|
onSearchChange={this._onSearchChange}
|
||||||
|
searchEnabled={true}
|
||||||
|
value={value}
|
||||||
|
label={_t("Language Dropdown")}
|
||||||
>
|
>
|
||||||
{ options }
|
{ options }
|
||||||
</Dropdown>;
|
</Dropdown>;
|
||||||
|
|
|
@ -20,12 +20,13 @@ import createReactClass from 'create-react-class';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk';
|
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { getDisplayAliasForRoom } from '../../../Rooms';
|
import { getDisplayAliasForRoom } from '../../../Rooms';
|
||||||
import FlairStore from "../../../stores/FlairStore";
|
import FlairStore from "../../../stores/FlairStore";
|
||||||
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
|
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||||
|
@ -66,17 +67,6 @@ const Pill = createReactClass({
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
childContextTypes: {
|
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
|
||||||
},
|
|
||||||
|
|
||||||
getChildContext() {
|
|
||||||
return {
|
|
||||||
matrixClient: this._matrixClient,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
return {
|
return {
|
||||||
// ID/alias of the room/user
|
// ID/alias of the room/user
|
||||||
|
@ -127,7 +117,7 @@ const Pill = createReactClass({
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Pill.TYPE_USER_MENTION: {
|
case Pill.TYPE_USER_MENTION: {
|
||||||
const localMember = nextProps.room.getMember(resourceId);
|
const localMember = nextProps.room ? nextProps.room.getMember(resourceId) : undefined;
|
||||||
member = localMember;
|
member = localMember;
|
||||||
if (!localMember) {
|
if (!localMember) {
|
||||||
member = new RoomMember(null, resourceId);
|
member = new RoomMember(null, resourceId);
|
||||||
|
@ -276,7 +266,8 @@ const Pill = createReactClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.state.pillType) {
|
if (this.state.pillType) {
|
||||||
return this.props.inMessage ?
|
return <MatrixClientContext.Provider value={this._matrixClient}>
|
||||||
|
{ this.props.inMessage ?
|
||||||
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
|
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
|
||||||
{ avatar }
|
{ avatar }
|
||||||
{ linkText }
|
{ linkText }
|
||||||
|
@ -284,7 +275,8 @@ const Pill = createReactClass({
|
||||||
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
|
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
|
||||||
{ avatar }
|
{ avatar }
|
||||||
{ linkText }
|
{ linkText }
|
||||||
</span>;
|
</span> }
|
||||||
|
</MatrixClientContext.Provider>;
|
||||||
} else {
|
} else {
|
||||||
// Deliberately render nothing if the URL isn't recognised
|
// Deliberately render nothing if the URL isn't recognised
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -21,10 +21,11 @@ import {_t} from '../../../languageHandler';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import {wantsDateSeparator} from '../../../DateUtils';
|
import {wantsDateSeparator} from '../../../DateUtils';
|
||||||
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
import {MatrixEvent} from 'matrix-js-sdk';
|
||||||
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import escapeHtml from "escape-html";
|
import escapeHtml from "escape-html";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
||||||
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
|
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
|
||||||
|
@ -38,12 +39,10 @@ export default class ReplyThread extends React.Component {
|
||||||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextTypes = {
|
static contextType = MatrixClientContext;
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
// The loaded events to be rendered as linear-replies
|
// The loaded events to be rendered as linear-replies
|
||||||
|
@ -187,7 +186,7 @@ export default class ReplyThread extends React.Component {
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.unmounted = false;
|
this.unmounted = false;
|
||||||
this.room = this.context.matrixClient.getRoom(this.props.parentEv.getRoomId());
|
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
|
||||||
this.room.on("Room.redaction", this.onRoomRedaction);
|
this.room.on("Room.redaction", this.onRoomRedaction);
|
||||||
// same event handler as Room.redaction as for both we just do forceUpdate
|
// same event handler as Room.redaction as for both we just do forceUpdate
|
||||||
this.room.on("Room.redactionCancelled", this.onRoomRedaction);
|
this.room.on("Room.redactionCancelled", this.onRoomRedaction);
|
||||||
|
@ -259,7 +258,7 @@ export default class ReplyThread extends React.Component {
|
||||||
try {
|
try {
|
||||||
// ask the client to fetch the event we want using the context API, only interface to do so is to ask
|
// ask the client to fetch the event we want using the context API, only interface to do so is to ask
|
||||||
// for a timeline with that event, but once it is loaded we can use findEventById to look up the ev map
|
// for a timeline with that event, but once it is loaded we can use findEventById to look up the ev map
|
||||||
await this.context.matrixClient.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId);
|
await this.context.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// if it fails catch the error and return early, there's no point trying to find the event in this case.
|
// if it fails catch the error and return early, there's no point trying to find the event in this case.
|
||||||
// Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved).
|
// Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved).
|
||||||
|
@ -300,7 +299,7 @@ export default class ReplyThread extends React.Component {
|
||||||
} else if (this.state.loadedEv) {
|
} else if (this.state.loadedEv) {
|
||||||
const ev = this.state.loadedEv;
|
const ev = this.state.loadedEv;
|
||||||
const Pill = sdk.getComponent('elements.Pill');
|
const Pill = sdk.getComponent('elements.Pill');
|
||||||
const room = this.context.matrixClient.getRoom(ev.getRoomId());
|
const room = this.context.getRoom(ev.getRoomId());
|
||||||
header = <blockquote className="mx_ReplyThread">
|
header = <blockquote className="mx_ReplyThread">
|
||||||
{
|
{
|
||||||
_t('<a>In reply to</a> <pill>', {}, {
|
_t('<a>In reply to</a> <pill>', {}, {
|
||||||
|
|
|
@ -20,11 +20,13 @@ import sdk from '../../../index';
|
||||||
import withValidation from './Validation';
|
import withValidation from './Validation';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
|
||||||
|
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain
|
||||||
export default class RoomAliasField extends React.PureComponent {
|
export default class RoomAliasField extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -53,6 +55,7 @@ export default class RoomAliasField extends React.PureComponent {
|
||||||
onValidate={this._onValidate}
|
onValidate={this._onValidate}
|
||||||
placeholder={_t("e.g. my-room")}
|
placeholder={_t("e.g. my-room")}
|
||||||
onChange={this._onChange}
|
onChange={this._onChange}
|
||||||
|
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
|
||||||
maxLength={maxlength} />
|
maxLength={maxlength} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -61,7 +64,7 @@ export default class RoomAliasField extends React.PureComponent {
|
||||||
if (this.props.onChange) {
|
if (this.props.onChange) {
|
||||||
this.props.onChange(this._asFullAlias(ev.target.value));
|
this.props.onChange(this._asFullAlias(ev.target.value));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_onValidate = async (fieldState) => {
|
_onValidate = async (fieldState) => {
|
||||||
const result = await this._validationRules(fieldState);
|
const result = await this._validationRules(fieldState);
|
||||||
|
|
|
@ -24,8 +24,8 @@ export default class SyntaxHighlight extends React.Component {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context);
|
super(props);
|
||||||
|
|
||||||
this._ref = this._ref.bind(this);
|
this._ref = this._ref.bind(this);
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue