mirror of
https://github.com/element-hq/element-web
synced 2024-11-28 20:38:55 +03:00
[CONFLICT CHUNKS] Merge branch 'develop' into travis/sourcemaps-develop
This commit is contained in:
commit
fde32f13a5
190 changed files with 6185 additions and 2225 deletions
|
@ -33,7 +33,6 @@ src/components/views/rooms/RoomList.js
|
||||||
src/components/views/rooms/RoomPreviewBar.js
|
src/components/views/rooms/RoomPreviewBar.js
|
||||||
src/components/views/rooms/SearchBar.js
|
src/components/views/rooms/SearchBar.js
|
||||||
src/components/views/rooms/SearchResultTile.js
|
src/components/views/rooms/SearchResultTile.js
|
||||||
src/components/views/rooms/SlateMessageComposer.js
|
|
||||||
src/components/views/settings/ChangeAvatar.js
|
src/components/views/settings/ChangeAvatar.js
|
||||||
src/components/views/settings/ChangePassword.js
|
src/components/views/settings/ChangePassword.js
|
||||||
src/components/views/settings/DevicesPanel.js
|
src/components/views/settings/DevicesPanel.js
|
||||||
|
@ -58,7 +57,6 @@ src/utils/Receipt.js
|
||||||
src/Velociraptor.js
|
src/Velociraptor.js
|
||||||
test/components/structures/MessagePanel-test.js
|
test/components/structures/MessagePanel-test.js
|
||||||
test/components/views/dialogs/InteractiveAuthDialog-test.js
|
test/components/views/dialogs/InteractiveAuthDialog-test.js
|
||||||
test/components/views/rooms/MessageComposerInput-test.js
|
|
||||||
test/mock-clock.js
|
test/mock-clock.js
|
||||||
test/notifications/ContentRules-test.js
|
test/notifications/ContentRules-test.js
|
||||||
test/notifications/PushRuleVectorState-test.js
|
test/notifications/PushRuleVectorState-test.js
|
||||||
|
|
|
@ -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
|
||||||
|
@ -83,7 +83,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.
|
||||||
|
@ -108,7 +108,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
|
||||||
|
|
22
package.json
22
package.json
|
@ -34,10 +34,22 @@
|
||||||
"i18n": "matrix-gen-i18n",
|
"i18n": "matrix-gen-i18n",
|
||||||
"prunei18n": "matrix-prune-i18n",
|
"prunei18n": "matrix-prune-i18n",
|
||||||
"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",
|
||||||
|
<<<<<<< HEAD
|
||||||
"emoji-data-strip": "node scripts/emoji-data-strip.js",
|
"emoji-data-strip": "node scripts/emoji-data-strip.js",
|
||||||
"reskindex": "node scripts/reskindex.js -h header",
|
"reskindex": "node scripts/reskindex.js -h header",
|
||||||
"reskindex:watch": "node scripts/reskindex.js -h header -w",
|
"reskindex:watch": "node scripts/reskindex.js -h header -w",
|
||||||
"rethemendex": "res/css/rethemendex.sh",
|
"rethemendex": "res/css/rethemendex.sh",
|
||||||
|
=======
|
||||||
|
"build": "yarn reskindex && yarn start:init",
|
||||||
|
"build:watch": "babel src -w --skip-initial-build -d lib --source-maps --copy-files",
|
||||||
|
"start": "yarn start:init && yarn start:all",
|
||||||
|
"start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn build:watch\" \"yarn reskindex:watch\"",
|
||||||
|
"start:init": "babel src -d lib --source-maps --copy-files",
|
||||||
|
"lint": "eslint src/",
|
||||||
|
"lintall": "eslint src/ test/",
|
||||||
|
"lintwithexclusions": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
|
||||||
|
"stylelint": "stylelint 'res/css/**/*.scss'",
|
||||||
|
>>>>>>> develop
|
||||||
"clean": "rimraf lib",
|
"clean": "rimraf lib",
|
||||||
"build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
"build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
||||||
"build:compile": "yarn reskindex && babel -d lib --verbose --extensions \".ts,.js\" src",
|
"build:compile": "yarn reskindex && babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||||
|
@ -70,7 +82,6 @@
|
||||||
"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",
|
|
||||||
"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",
|
||||||
|
@ -78,6 +89,8 @@
|
||||||
"glob": "^5.0.14",
|
"glob": "^5.0.14",
|
||||||
"glob-to-regexp": "^0.4.1",
|
"glob-to-regexp": "^0.4.1",
|
||||||
"highlight.js": "^9.15.8",
|
"highlight.js": "^9.15.8",
|
||||||
|
"html-entities": "^1.2.1",
|
||||||
|
"humanize": "^0.0.9",
|
||||||
"is-ip": "^2.0.0",
|
"is-ip": "^2.0.0",
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.6",
|
"linkifyjs": "^2.1.6",
|
||||||
|
@ -95,13 +108,10 @@
|
||||||
"react-addons-css-transition-group": "15.6.2",
|
"react-addons-css-transition-group": "15.6.2",
|
||||||
"react-beautiful-dnd": "^4.0.1",
|
"react-beautiful-dnd": "^4.0.1",
|
||||||
"react-dom": "^16.9.0",
|
"react-dom": "^16.9.0",
|
||||||
|
"react-focus-lock": "^2.2.1",
|
||||||
"react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#9cf17f63b7c0b0ec5f31df27da0f82f7238dc594",
|
"react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#9cf17f63b7c0b0ec5f31df27da0f82f7238dc594",
|
||||||
"resize-observer-polyfill": "^1.5.0",
|
"resize-observer-polyfill": "^1.5.0",
|
||||||
"sanitize-html": "^1.18.4",
|
"sanitize-html": "^1.18.4",
|
||||||
"slate": "^0.41.2",
|
|
||||||
"slate-html-serializer": "^0.6.1",
|
|
||||||
"slate-md-serializer": "github:matrix-org/slate-md-serializer#f7c4ad3",
|
|
||||||
"slate-react": "^0.18.10",
|
|
||||||
"text-encoding-utf-8": "^1.0.1",
|
"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",
|
||||||
|
@ -128,6 +138,8 @@
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"chokidar": "^2.1.2",
|
"chokidar": "^2.1.2",
|
||||||
"concurrently": "^4.0.1",
|
"concurrently": "^4.0.1",
|
||||||
|
"enzyme": "^3.10.0",
|
||||||
|
"enzyme-adapter-react-16": "^1.15.1",
|
||||||
"eslint": "^5.12.0",
|
"eslint": "^5.12.0",
|
||||||
"eslint-config-google": "^0.7.1",
|
"eslint-config-google": "^0.7.1",
|
||||||
"eslint-plugin-babel": "^5.2.1",
|
"eslint-plugin-babel": "^5.2.1",
|
||||||
|
|
|
@ -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";
|
||||||
|
@ -57,6 +56,7 @@
|
||||||
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
||||||
@import "./views/dialogs/_CreateGroupDialog.scss";
|
@import "./views/dialogs/_CreateGroupDialog.scss";
|
||||||
@import "./views/dialogs/_CreateRoomDialog.scss";
|
@import "./views/dialogs/_CreateRoomDialog.scss";
|
||||||
|
@import "./views/dialogs/_DMInviteDialog.scss";
|
||||||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||||
@import "./views/dialogs/_DeviceVerifyDialog.scss";
|
@import "./views/dialogs/_DeviceVerifyDialog.scss";
|
||||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||||
|
@ -174,7 +174,9 @@
|
||||||
@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/_CrossSigningPanel.scss";
|
||||||
@import "./views/settings/_DevicesPanel.scss";
|
@import "./views/settings/_DevicesPanel.scss";
|
||||||
@import "./views/settings/_EmailAddresses.scss";
|
@import "./views/settings/_EmailAddresses.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');
|
||||||
}
|
}
|
||||||
|
|
197
res/css/views/dialogs/_DMInviteDialog.scss
Normal file
197
res/css/views/dialogs/_DMInviteDialog.scss
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_addressBar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_editor {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%; // Needed to make the Field inside grow
|
||||||
|
background-color: $user-tile-hover-bg-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: 25px;
|
||||||
|
padding-left: 8px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_userTile {
|
||||||
|
display: inline-block;
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
top: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using a textarea for this element, to circumvent autofill
|
||||||
|
// Mostly copied from AddressPickerDialog
|
||||||
|
textarea,
|
||||||
|
textarea:focus {
|
||||||
|
height: 34px;
|
||||||
|
line-height: 34px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-left: 12px;
|
||||||
|
margin: 0 !important;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
resize: none;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
word-wrap: nowrap;
|
||||||
|
|
||||||
|
// Roughly fill about 2/5ths of the available space. This is to try and 'fill' the
|
||||||
|
// remaining space after a bunch of pills, but is a bit hacky. Ideally we'd have
|
||||||
|
// support for "fill remaining width", but traditional tricks don't work with what
|
||||||
|
// we're pushing into this "field". Flexbox just makes things worse. The theory is
|
||||||
|
// that users won't need more than about 2/5ths of the input to find the person
|
||||||
|
// they're looking for.
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_goButton {
|
||||||
|
width: 48px;
|
||||||
|
margin-left: 10px;
|
||||||
|
height: 25px;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_section {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $muted-fg-color;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_roomTile {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $user-tile-hover-bg-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_roomTile_avatarStack {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_roomTile_selected {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 36px;
|
||||||
|
background-color: $username-variant1-color;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
mask-image: url('$(res)/img/feather-customised/check.svg');
|
||||||
|
mask-size: 100%;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
position: absolute;
|
||||||
|
top: 6px; // 50%
|
||||||
|
left: 6px; // 50%
|
||||||
|
background-color: #ffffff; // this is fine without a var because it's for both themes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_roomTile_name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_roomTile_userId {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $muted-fg-color;
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_roomTile_time {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $muted-fg-color;
|
||||||
|
float: right;
|
||||||
|
line-height: 36px; // Height of the avatar to keep the time vertically aligned
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_roomTile_highlight {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Many of these styles are stolen from mx_UserPill, but adjusted for the invite dialog.
|
||||||
|
.mx_DMInviteDialog_userTile {
|
||||||
|
margin-right: 8px;
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_userTile_pill {
|
||||||
|
background-color: $username-variant1-color;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
color: #ffffff; // this is fine without a var because it's for both themes
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_userTile_avatar {
|
||||||
|
border-radius: 20px;
|
||||||
|
position: relative;
|
||||||
|
left: -5px;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.mx_DMInviteDialog_userTile_avatar {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_userTile_name {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_userTile_threepidAvatar {
|
||||||
|
background-color: #ffffff; // this is fine without a var because it's for both themes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DMInviteDialog_userTile_remove {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,11 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/feather-customised/users-sm.svg');
|
mask-image: url('$(res)/img/feather-customised/users-sm.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomSettingsDialog_bridgesIcon::before {
|
||||||
|
// This icon is pants, please improve :)
|
||||||
|
mask-image: url('$(res)/img/feather-customised/bridge.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomSettingsDialog_warningIcon::before {
|
.mx_RoomSettingsDialog_warningIcon::before {
|
||||||
mask-image: url('$(res)/img/feather-customised/warning-triangle.svg');
|
mask-image: url('$(res)/img/feather-customised/warning-triangle.svg');
|
||||||
}
|
}
|
||||||
|
@ -42,3 +47,25 @@ limitations under the License.
|
||||||
padding-left: 40px;
|
padding-left: 40px;
|
||||||
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 {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomSettingsDialog_BridgeList li {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border-width: 1px 0px;
|
||||||
|
border-color: #dee1f3;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -353,7 +354,6 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
|
||||||
left: 46px;
|
left: 46px;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
cursor: pointer;
|
|
||||||
display: block;
|
display: block;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,4 +40,5 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_RoomRecoveryReminder_secondary {
|
.mx_RoomRecoveryReminder_secondary {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
23
res/css/views/rooms/_UserOnlineDot.scss
Normal file
23
res/css/views/rooms/_UserOnlineDot.scss
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
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_UserOnlineDot {
|
||||||
|
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;
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
50
res/img/feather-customised/bridge.svg
Normal file
50
res/img/feather-customised/bridge.svg
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
id="svg8"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 5.487504 5.7341776"
|
||||||
|
height="5.7341776mm"
|
||||||
|
width="5.487504mm">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
transform="translate(14.166523,-96.032669)"
|
||||||
|
id="layer1">
|
||||||
|
<rect
|
||||||
|
y="99.461258"
|
||||||
|
x="-10.861272"
|
||||||
|
height="2.0555882"
|
||||||
|
width="1.9322528"
|
||||||
|
id="rect831-6"
|
||||||
|
style="fill:none;stroke:#b8bec9;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||||
|
<path
|
||||||
|
id="path883"
|
||||||
|
d="m -11.98427,98.338257 1.122998,1.122998"
|
||||||
|
style="fill:#b8bec9;fill-opacity:1;stroke:#b8bec9;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||||
|
<rect
|
||||||
|
transform="scale(-1)"
|
||||||
|
y="-98.338257"
|
||||||
|
x="11.98427"
|
||||||
|
height="2.0555882"
|
||||||
|
width="1.9322529"
|
||||||
|
id="rect831-6-7"
|
||||||
|
style="fill:none;stroke:#b8bec9;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
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
res/img/icon-email-pill-avatar.svg
Normal file
1
res/img/icon-email-pill-avatar.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="65.631" height="67.981"><defs><filter x="-.059" y="-.079" width="1.118" height="1.158" filterUnits="objectBoundingBox" id="a"><feOffset dy="2" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="16" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0.473684211 0 0 0 0 1 0 0 0 0.241258741 0" in="shadowBlurOuter1" result="shadowMatrixOuter1"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g filter="url(#a)" transform="matrix(3.40907 0 0 3.40907 -1493.716 -795.144)" fill="none" fill-rule="evenodd" stroke="#368bd6" stroke-linecap="round" stroke-linejoin="round"><g transform="translate(441.5 237.5)"><circle r="2.286" cy="5.714" cx="6.286"/><path d="M8.571 3.429v2.857a1.714 1.714 0 103.429 0v-.572a5.714 5.714 0 10-2.24 4.537"/></g></g></svg>
|
After Width: | Height: | Size: 918 B |
1
res/img/icon-pill-remove.svg
Normal file
1
res/img/icon-pill-remove.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="58" height="60" viewBox="26 25 6 6" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-5.9%" y="-7.9%" width="111.8%" height="115.8%" filterUnits="objectBoundingBox" id="a"><feOffset dy="2" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="16" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0.473684211 0 0 0 0 1 0 0 0 0.241258741 0" in="shadowBlurOuter1" result="shadowMatrixOuter1"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g filter="url(#a)" transform="translate(-406 -215)" stroke="#61708B"><path d="M438 240l-6 6M432 240l6 6"/></g></svg>
|
After Width: | Height: | Size: 693 B |
|
@ -16,6 +16,7 @@ $room-highlight-color: #343a46;
|
||||||
// typical text (dark-on-white in light skin)
|
// typical text (dark-on-white in light skin)
|
||||||
$primary-fg-color: $text-primary-color;
|
$primary-fg-color: $text-primary-color;
|
||||||
$primary-bg-color: $bg-color;
|
$primary-bg-color: $bg-color;
|
||||||
|
$muted-fg-color: $header-panel-text-primary-color;
|
||||||
|
|
||||||
// used for dialog box text
|
// used for dialog box text
|
||||||
$light-fg-color: $header-panel-text-secondary-color;
|
$light-fg-color: $header-panel-text-secondary-color;
|
||||||
|
@ -172,6 +173,8 @@ $interactive-tooltip-fg-color: #ffffff;
|
||||||
|
|
||||||
$breadcrumb-placeholder-bg-color: #272c35;
|
$breadcrumb-placeholder-bg-color: #272c35;
|
||||||
|
|
||||||
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
@ -243,3 +246,13 @@ $breadcrumb-placeholder-bg-color: #272c35;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// diff highlight colors
|
||||||
|
// intentionally swapped to avoid inversion
|
||||||
|
.hljs-addition {
|
||||||
|
background: #fdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion {
|
||||||
|
background: #dfd;
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ $header-panel-bg-color: #f3f8fd;
|
||||||
// typical text (dark-on-white in light skin)
|
// typical text (dark-on-white in light skin)
|
||||||
$primary-fg-color: #2e2f32;
|
$primary-fg-color: #2e2f32;
|
||||||
$primary-bg-color: #ffffff;
|
$primary-bg-color: #ffffff;
|
||||||
|
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
|
||||||
|
|
||||||
// used for dialog box text
|
// used for dialog box text
|
||||||
$light-fg-color: #747474;
|
$light-fg-color: #747474;
|
||||||
|
@ -293,6 +294,8 @@ $interactive-tooltip-fg-color: #ffffff;
|
||||||
|
|
||||||
$breadcrumb-placeholder-bg-color: #e8eef5;
|
$breadcrumb-placeholder-bg-color: #e8eef5;
|
||||||
|
|
||||||
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
@ -338,3 +341,12 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
|
||||||
color: $accent-color;
|
color: $accent-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// diff highlight colors
|
||||||
|
.hljs-addition {
|
||||||
|
background: #dfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion {
|
||||||
|
background: #fdd;
|
||||||
|
}
|
||||||
|
|
|
@ -36,7 +36,8 @@ echo "--- Install synapse & other dependencies"
|
||||||
./install.sh
|
./install.sh
|
||||||
# install static webserver to server symlinked local copy of riot
|
# install static webserver to server symlinked local copy of riot
|
||||||
./riot/install-webserver.sh
|
./riot/install-webserver.sh
|
||||||
mkdir logs || rm -r logs/*
|
rm -r logs || true
|
||||||
|
mkdir logs
|
||||||
echo "+++ Running end-to-end tests"
|
echo "+++ Running end-to-end tests"
|
||||||
TESTS_STARTED=1
|
TESTS_STARTED=1
|
||||||
./run.sh --no-sandbox --log-directory logs/
|
./run.sh --no-sandbox --log-directory logs/
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// This generates src/stripped-emoji.json as used by the EmojiProvider autocomplete
|
|
||||||
// provider.
|
|
||||||
|
|
||||||
const EMOJIBASE = require('emojibase-data/en/compact.json');
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const output = EMOJIBASE.map(
|
|
||||||
(datum) => {
|
|
||||||
const newDatum = {
|
|
||||||
name: datum.annotation,
|
|
||||||
shortname: `:${datum.shortcodes[0]}:`,
|
|
||||||
category: datum.group,
|
|
||||||
emoji_order: datum.order,
|
|
||||||
};
|
|
||||||
if (datum.shortcodes.length > 1) {
|
|
||||||
newDatum.aliases = datum.shortcodes.slice(1).map(s => `:${s}:`);
|
|
||||||
}
|
|
||||||
if (datum.emoticon) {
|
|
||||||
newDatum.aliases_ascii = [ datum.emoticon ];
|
|
||||||
}
|
|
||||||
return newDatum;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Write to a file in src. Changes should be checked into git. This file is copied by
|
|
||||||
// babel using --copy-files
|
|
||||||
fs.writeFileSync('./src/stripped-emoji.json', JSON.stringify(output));
|
|
|
@ -422,6 +422,9 @@ export default class ContentMessages {
|
||||||
|
|
||||||
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
|
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;
|
||||||
|
|
|
@ -97,7 +97,7 @@ export const crossSigningCallbacks = {
|
||||||
*
|
*
|
||||||
* Additionally, the secret storage keys are cached during the scope of this function
|
* 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
|
* to ensure the user is prompted only once for their secret storage
|
||||||
* passphrase. The cache is then
|
* passphrase. The cache is then cleared once the provided function completes.
|
||||||
*
|
*
|
||||||
* @param {Function} [func] An operation to perform once secret storage has been
|
* @param {Function} [func] An operation to perform once secret storage has been
|
||||||
* bootstrapped. Optional.
|
* bootstrapped. Optional.
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,6 +377,7 @@ class TextHighlighter extends BaseHighlighter {
|
||||||
* opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
|
* opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
|
||||||
* opts.returnString: return an HTML string rather than JSX elements
|
* opts.returnString: return an HTML string rather than JSX elements
|
||||||
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
|
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
|
||||||
|
* opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString)
|
||||||
*/
|
*/
|
||||||
export function bodyToHtml(content, highlights, opts={}) {
|
export function bodyToHtml(content, highlights, opts={}) {
|
||||||
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
|
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
|
||||||
|
@ -476,8 +460,8 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return isDisplayedWithHtml ?
|
return isDisplayedWithHtml ?
|
||||||
<span key="body" className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
|
<span key="body" ref={opts.ref} className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
|
||||||
<span key="body" className={className} dir="auto">{ strippedBody }</span>;
|
<span key="body" ref={opts.ref} className={className} dir="auto">{ strippedBody }</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -111,6 +111,12 @@ export default class KeyRequestHandler {
|
||||||
this._currentUser = null;
|
this._currentUser = null;
|
||||||
this._currentDevice = null;
|
this._currentDevice = null;
|
||||||
|
|
||||||
|
if (!this._pendingKeyRequests[userId] || !this._pendingKeyRequests[userId][deviceId]) {
|
||||||
|
// request was removed in the time the dialog was displayed
|
||||||
|
this._processNextRequest();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (r) {
|
if (r) {
|
||||||
for (const req of this._pendingKeyRequests[userId][deviceId]) {
|
for (const req of this._pendingKeyRequests[userId][deviceId]) {
|
||||||
req.share();
|
req.share();
|
||||||
|
|
|
@ -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",
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,6 +25,7 @@ import * as sdk from './';
|
||||||
import dis from './dispatcher';
|
import dis from './dispatcher';
|
||||||
import DMRoomMap from './utils/DMRoomMap';
|
import DMRoomMap from './utils/DMRoomMap';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invites multiple addresses to a room
|
* Invites multiple addresses to a room
|
||||||
|
@ -41,6 +42,18 @@ function inviteMultipleToRoom(roomId, addrs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showStartChatInviteDialog() {
|
export function showStartChatInviteDialog() {
|
||||||
|
if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) {
|
||||||
|
const DMInviteDialog = sdk.getComponent("dialogs.DMInviteDialog");
|
||||||
|
Modal.createTrackedDialog('Start DM', '', DMInviteDialog, {
|
||||||
|
onFinished: (inviteIds) => {
|
||||||
|
// TODO: Replace _onStartDmFinished with less hacks
|
||||||
|
if (inviteIds.length > 0) _onStartDmFinished(true, inviteIds.map(i => ({address: i})));
|
||||||
|
// else ignore and just do nothing
|
||||||
|
},
|
||||||
|
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||||
|
|
||||||
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
|
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
|
||||||
|
@ -99,7 +112,7 @@ export function isValid3pidInvite(event) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Immutable DMs replaces this
|
// TODO: Canonical DMs replaces this
|
||||||
function _onStartDmFinished(shouldInvite, addrs) {
|
function _onStartDmFinished(shouldInvite, addrs) {
|
||||||
if (!shouldInvite) return;
|
if (!shouldInvite) return;
|
||||||
|
|
||||||
|
|
|
@ -780,15 +780,14 @@ export const CommandMap = {
|
||||||
const deviceId = matches[2];
|
const deviceId = matches[2];
|
||||||
const fingerprint = matches[3];
|
const fingerprint = matches[3];
|
||||||
|
|
||||||
return success(
|
return success((async () => {
|
||||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
const device = await cli.getStoredDevice(userId, deviceId);
|
||||||
// in future
|
|
||||||
Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => {
|
|
||||||
if (!device) {
|
if (!device) {
|
||||||
throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`);
|
throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`);
|
||||||
}
|
}
|
||||||
|
const deviceTrust = await cli.checkDeviceTrust(userId, deviceId);
|
||||||
|
|
||||||
if (device.isVerified()) {
|
if (deviceTrust.isVerified()) {
|
||||||
if (device.getFingerprint() === fingerprint) {
|
if (device.getFingerprint() === fingerprint) {
|
||||||
throw new Error(_t('Device already verified!'));
|
throw new Error(_t('Device already verified!'));
|
||||||
} else {
|
} else {
|
||||||
|
@ -810,8 +809,8 @@ export const CommandMap = {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return cli.setDeviceVerified(userId, deviceId, true);
|
await cli.setDeviceVerified(userId, deviceId, true);
|
||||||
}).then(() => {
|
|
||||||
// Tell the user we verified everything
|
// Tell the user we verified everything
|
||||||
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
|
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
|
||||||
Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, {
|
Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, {
|
||||||
|
@ -826,8 +825,7 @@ export const CommandMap = {
|
||||||
</p>
|
</p>
|
||||||
</div>,
|
</div>,
|
||||||
});
|
});
|
||||||
}),
|
})());
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
//@flow
|
|
||||||
/*
|
|
||||||
Copyright 2017 Aviral Dasgupta
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Value} from 'slate';
|
|
||||||
|
|
||||||
import _clamp from 'lodash/clamp';
|
|
||||||
|
|
||||||
type MessageFormat = 'rich' | 'markdown';
|
|
||||||
|
|
||||||
class HistoryItem {
|
|
||||||
// We store history items in their native format to ensure history is accurate
|
|
||||||
// and then convert them if our RTE has subsequently changed format.
|
|
||||||
value: Value;
|
|
||||||
format: MessageFormat = 'rich';
|
|
||||||
|
|
||||||
constructor(value: ?Value, format: ?MessageFormat) {
|
|
||||||
this.value = value;
|
|
||||||
this.format = format;
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromJSON(obj: Object): HistoryItem {
|
|
||||||
return new HistoryItem(
|
|
||||||
Value.fromJSON(obj.value),
|
|
||||||
obj.format,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): Object {
|
|
||||||
return {
|
|
||||||
value: this.value.toJSON(),
|
|
||||||
format: this.format,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class SlateComposerHistoryManager {
|
|
||||||
history: Array<HistoryItem> = [];
|
|
||||||
prefix: string;
|
|
||||||
lastIndex: number = 0; // used for indexing the storage
|
|
||||||
currentIndex: number = 0; // used for indexing the loaded validated history Array
|
|
||||||
|
|
||||||
constructor(roomId: string, prefix: string = 'mx_composer_history_') {
|
|
||||||
this.prefix = prefix + roomId;
|
|
||||||
|
|
||||||
// TODO: Performance issues?
|
|
||||||
let item;
|
|
||||||
for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
|
|
||||||
try {
|
|
||||||
this.history.push(
|
|
||||||
HistoryItem.fromJSON(JSON.parse(item)),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Throwing away unserialisable history", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.lastIndex = this.currentIndex;
|
|
||||||
// reset currentIndex to account for any unserialisable history
|
|
||||||
this.currentIndex = this.history.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
save(value: Value, format: MessageFormat) {
|
|
||||||
const item = new HistoryItem(value, format);
|
|
||||||
this.history.push(item);
|
|
||||||
this.currentIndex = this.history.length;
|
|
||||||
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON()));
|
|
||||||
}
|
|
||||||
|
|
||||||
getItem(offset: number): ?HistoryItem {
|
|
||||||
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1);
|
|
||||||
return this.history[this.currentIndex];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -473,7 +473,7 @@ function textForPowerEvent(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForPinnedEvent(event) {
|
function textForPinnedEvent(event) {
|
||||||
const senderName = event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
return _t("%(senderName)s changed the pinned messages for the room.", {senderName});
|
return _t("%(senderName)s changed the pinned messages for the room.", {senderName});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
=======
|
||||||
|
import {Key} from "../../../Keyboard";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
>>>>>>> develop
|
||||||
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';
|
||||||
|
@ -22,7 +28,14 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
const sdk = require('../../../index');
|
const sdk = require('../../../index');
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
|
=======
|
||||||
|
// XXX: This component is not cross-signing aware.
|
||||||
|
// https://github.com/vector-im/riot-web/issues/11752 tracks either updating this
|
||||||
|
// component or taking it out to pasture.
|
||||||
|
module.exports = createReactClass({
|
||||||
|
>>>>>>> develop
|
||||||
displayName: 'EncryptedEventDialog',
|
displayName: 'EncryptedEventDialog',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -84,7 +97,7 @@ export default 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);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
Copyright 2018, 2019 New Vector Ltd
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,10 +17,19 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
|
<<<<<<< HEAD
|
||||||
import * as sdk from '../../../../index';
|
import * as sdk from '../../../../index';
|
||||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||||
|
=======
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import sdk from '../../../../index';
|
||||||
|
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
||||||
|
>>>>>>> develop
|
||||||
import { scorePassword } from '../../../../utils/PasswordScorer';
|
import { scorePassword } from '../../../../utils/PasswordScorer';
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
|
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||||
|
import SettingsStore from '../../../../settings/SettingsStore';
|
||||||
|
|
||||||
const PHASE_PASSPHRASE = 0;
|
const PHASE_PASSPHRASE = 0;
|
||||||
const PHASE_PASSPHRASE_CONFIRM = 1;
|
const PHASE_PASSPHRASE_CONFIRM = 1;
|
||||||
|
@ -48,10 +57,20 @@ function selectText(target) {
|
||||||
* on the server.
|
* on the server.
|
||||||
*/
|
*/
|
||||||
export default class CreateKeyBackupDialog extends React.PureComponent {
|
export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
secureSecretStorage: PropTypes.bool,
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this._recoveryKeyNode = null;
|
||||||
|
this._keyBackupInfo = null;
|
||||||
|
this._setZxcvbnResultTimeout = null;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
secureSecretStorage: props.secureSecretStorage,
|
||||||
phase: PHASE_PASSPHRASE,
|
phase: PHASE_PASSPHRASE,
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
passPhraseConfirm: '',
|
passPhraseConfirm: '',
|
||||||
|
@ -60,12 +79,25 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
zxcvbnResult: null,
|
zxcvbnResult: null,
|
||||||
setPassPhrase: false,
|
setPassPhrase: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.state.secureSecretStorage === undefined) {
|
||||||
|
this.state.secureSecretStorage =
|
||||||
|
SettingsStore.isFeatureEnabled("feature_cross_signing");
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
// If we're using secret storage, skip ahead to the backing up step, as
|
||||||
this._recoveryKeyNode = null;
|
// `accessSecretStorage` will handle passphrases as needed.
|
||||||
this._keyBackupInfo = null;
|
if (this.state.secureSecretStorage) {
|
||||||
this._setZxcvbnResultTimeout = null;
|
this.state.phase = PHASE_BACKINGUP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// If we're using secret storage, skip ahead to the backing up step, as
|
||||||
|
// `accessSecretStorage` will handle passphrases as needed.
|
||||||
|
if (this.state.secureSecretStorage) {
|
||||||
|
this._createBackup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -102,15 +134,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
_createBackup = async () => {
|
_createBackup = async () => {
|
||||||
|
const { secureSecretStorage } = this.state;
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_BACKINGUP,
|
phase: PHASE_BACKINGUP,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
let info;
|
let info;
|
||||||
try {
|
try {
|
||||||
|
if (secureSecretStorage) {
|
||||||
|
await accessSecretStorage(async () => {
|
||||||
|
info = await MatrixClientPeg.get().prepareKeyBackupVersion(
|
||||||
|
null /* random key */,
|
||||||
|
{ secureSecretStorage: true },
|
||||||
|
);
|
||||||
|
info = await MatrixClientPeg.get().createKeyBackupVersion(info);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
info = await MatrixClientPeg.get().createKeyBackupVersion(
|
info = await MatrixClientPeg.get().createKeyBackupVersion(
|
||||||
this._keyBackupInfo,
|
this._keyBackupInfo,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
|
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_DONE,
|
phase: PHASE_DONE,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018-2019 New Vector Ltd
|
Copyright 2018, 2019 New Vector Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
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.
|
||||||
|
@ -40,9 +41,11 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
|
||||||
|
|
||||||
onSetupClick = async () => {
|
onSetupClick = async () => {
|
||||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
|
Modal.createTrackedDialog(
|
||||||
|
'Restore Backup', '', RestoreKeyBackupDialog, {
|
||||||
onFinished: this.props.onFinished,
|
onFinished: this.props.onFinished,
|
||||||
});
|
}, null, /* priority = */ false, /* static = */ true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 New Vector Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -35,6 +36,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||||
import("./CreateKeyBackupDialog"),
|
import("./CreateKeyBackupDialog"),
|
||||||
|
null, null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,13 +23,15 @@ import FileSaver from 'file-saver';
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
|
|
||||||
const PHASE_PASSPHRASE = 0;
|
const PHASE_LOADING = 0;
|
||||||
const PHASE_PASSPHRASE_CONFIRM = 1;
|
const PHASE_MIGRATE = 1;
|
||||||
const PHASE_SHOWKEY = 2;
|
const PHASE_PASSPHRASE = 2;
|
||||||
const PHASE_KEEPITSAFE = 3;
|
const PHASE_PASSPHRASE_CONFIRM = 3;
|
||||||
const PHASE_STORING = 4;
|
const PHASE_SHOWKEY = 4;
|
||||||
const PHASE_DONE = 5;
|
const PHASE_KEEPITSAFE = 5;
|
||||||
const PHASE_OPTOUT_CONFIRM = 6;
|
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 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.
|
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
|
||||||
|
@ -58,7 +60,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
this._setZxcvbnResultTimeout = null;
|
this._setZxcvbnResultTimeout = null;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
phase: PHASE_PASSPHRASE,
|
phase: PHASE_LOADING,
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
passPhraseConfirm: '',
|
passPhraseConfirm: '',
|
||||||
copied: false,
|
copied: false,
|
||||||
|
@ -66,6 +68,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
zxcvbnResult: null,
|
zxcvbnResult: null,
|
||||||
setPassPhrase: false,
|
setPassPhrase: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._fetchBackupInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -74,10 +78,23 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _fetchBackupInfo() {
|
||||||
|
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
phase: backupInfo ? PHASE_MIGRATE: PHASE_PASSPHRASE,
|
||||||
|
backupInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_collectRecoveryKeyNode = (n) => {
|
_collectRecoveryKeyNode = (n) => {
|
||||||
this._recoveryKeyNode = n;
|
this._recoveryKeyNode = n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onMigrateNextClick = () => {
|
||||||
|
this._bootstrapSecretStorage();
|
||||||
|
}
|
||||||
|
|
||||||
_onCopyClick = () => {
|
_onCopyClick = () => {
|
||||||
selectText(this._recoveryKeyNode);
|
selectText(this._recoveryKeyNode);
|
||||||
const successful = document.execCommand('copy');
|
const successful = document.execCommand('copy');
|
||||||
|
@ -125,6 +142,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createSecretStorageKey: async () => this._keyInfo,
|
createSecretStorageKey: async () => this._keyInfo,
|
||||||
|
keyBackupInfo: this.state.backupInfo,
|
||||||
});
|
});
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_DONE,
|
phase: PHASE_DONE,
|
||||||
|
@ -250,6 +268,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
|
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() {
|
_renderPhasePassPhrase() {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
||||||
|
@ -449,7 +488,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderBusyPhase(text) {
|
_renderBusyPhase() {
|
||||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||||
return <div>
|
return <div>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -488,6 +527,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
_titleForPhase(phase) {
|
_titleForPhase(phase) {
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
|
case PHASE_MIGRATE:
|
||||||
|
return _t('Migrate from Key Backup');
|
||||||
case PHASE_PASSPHRASE:
|
case PHASE_PASSPHRASE:
|
||||||
return _t('Secure your encrypted messages with a passphrase');
|
return _t('Secure your encrypted messages with a passphrase');
|
||||||
case PHASE_PASSPHRASE_CONFIRM:
|
case PHASE_PASSPHRASE_CONFIRM:
|
||||||
|
@ -525,6 +566,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
switch (this.state.phase) {
|
switch (this.state.phase) {
|
||||||
|
case PHASE_LOADING:
|
||||||
|
content = this._renderBusyPhase();
|
||||||
|
break;
|
||||||
|
case PHASE_MIGRATE:
|
||||||
|
content = this._renderPhaseMigrate();
|
||||||
|
break;
|
||||||
case PHASE_PASSPHRASE:
|
case PHASE_PASSPHRASE:
|
||||||
content = this._renderPhasePassPhrase();
|
content = this._renderPhasePassPhrase();
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Based originally on slate-plain-serializer
|
|
||||||
|
|
||||||
import { Block } from 'slate';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plain text serializer, which converts a Slate `value` to a plain text string,
|
|
||||||
* serializing pills into various different formats as required.
|
|
||||||
*
|
|
||||||
* @type {PlainWithPillsSerializer}
|
|
||||||
*/
|
|
||||||
|
|
||||||
class PlainWithPillsSerializer {
|
|
||||||
/*
|
|
||||||
* @param {String} options.pillFormat - either 'md', 'plain', 'id'
|
|
||||||
*/
|
|
||||||
constructor(options = {}) {
|
|
||||||
const {
|
|
||||||
pillFormat = 'plain',
|
|
||||||
} = options;
|
|
||||||
this.pillFormat = pillFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize a Slate `value` to a plain text string,
|
|
||||||
* serializing pills as either MD links, plain text representations or
|
|
||||||
* ID representations as required.
|
|
||||||
*
|
|
||||||
* @param {Value} value
|
|
||||||
* @return {String}
|
|
||||||
*/
|
|
||||||
serialize = value => {
|
|
||||||
return this._serializeNode(value.document);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize a `node` to plain text.
|
|
||||||
*
|
|
||||||
* @param {Node} node
|
|
||||||
* @return {String}
|
|
||||||
*/
|
|
||||||
_serializeNode = node => {
|
|
||||||
if (
|
|
||||||
node.object == 'document' ||
|
|
||||||
(node.object == 'block' && Block.isBlockList(node.nodes))
|
|
||||||
) {
|
|
||||||
return node.nodes.map(this._serializeNode).join('\n');
|
|
||||||
} else if (node.type == 'emoji') {
|
|
||||||
return node.data.get('emojiUnicode');
|
|
||||||
} else if (node.type == 'pill') {
|
|
||||||
const completion = node.data.get('completion');
|
|
||||||
// over the wire the @room pill is just plaintext
|
|
||||||
if (completion === '@room') return completion;
|
|
||||||
|
|
||||||
switch (this.pillFormat) {
|
|
||||||
case 'plain':
|
|
||||||
return completion;
|
|
||||||
case 'md':
|
|
||||||
return `[${ completion }](${ node.data.get('href') })`;
|
|
||||||
case 'id':
|
|
||||||
return node.data.get('completionId') || completion;
|
|
||||||
}
|
|
||||||
} else if (node.nodes) {
|
|
||||||
return node.nodes.map(this._serializeNode).join('');
|
|
||||||
} else {
|
|
||||||
return node.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export.
|
|
||||||
*
|
|
||||||
* @type {PlainWithPillsSerializer}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default PlainWithPillsSerializer;
|
|
|
@ -71,6 +71,7 @@ export default class QueryMatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const keyValue of keyValues) {
|
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, []);
|
||||||
|
|
|
@ -71,12 +71,12 @@ export class ContextMenu extends React.Component {
|
||||||
// on resize callback
|
// on resize callback
|
||||||
windowResize: PropTypes.func,
|
windowResize: PropTypes.func,
|
||||||
|
|
||||||
catchTab: PropTypes.bool, // whether to close the ContextMenu on TAB (default=true)
|
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,
|
||||||
catchTab: true,
|
managed: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -186,15 +186,19 @@ 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) {
|
||||||
case Key.TAB:
|
case Key.TAB:
|
||||||
if (!this.props.catchTab) {
|
|
||||||
handled = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// fallthrough
|
|
||||||
case Key.ESCAPE:
|
case Key.ESCAPE:
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
break;
|
break;
|
||||||
|
@ -321,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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,9 +25,14 @@ import { _t } from '../../languageHandler';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
import * as sdk from '../../index';
|
import * as sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
|
<<<<<<< HEAD
|
||||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
=======
|
||||||
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
|
>>>>>>> develop
|
||||||
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 +44,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 +107,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({
|
||||||
|
|
|
@ -19,12 +19,15 @@ 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 * as sdk from '../../index';
|
import * as sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
|
<<<<<<< HEAD
|
||||||
import * as VectorConferenceHandler from '../../VectorConferenceHandler';
|
import * as VectorConferenceHandler from '../../VectorConferenceHandler';
|
||||||
import TagPanelButtons from './TagPanelButtons';
|
import TagPanelButtons from './TagPanelButtons';
|
||||||
|
=======
|
||||||
|
import VectorConferenceHandler from '../../VectorConferenceHandler';
|
||||||
|
>>>>>>> develop
|
||||||
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 +42,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 +242,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>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -77,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
|
||||||
|
@ -407,13 +393,6 @@ const LoggedInView = createReactClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: Remove after CIDER replaces Slate completely: https://github.com/vector-im/riot-web/issues/11036
|
|
||||||
// If using Slate, consume the Backspace without first focusing as it causes an implosion
|
|
||||||
if (ev.key === Key.BACKSPACE && !SettingsStore.getValue("useCiderComposer")) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
|
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
|
||||||
// synchronous dispatch so we focus before key generates input
|
// synchronous dispatch so we focus before key generates input
|
||||||
dis.dispatch({action: 'focus_composer'}, true);
|
dis.dispatch({action: 'focus_composer'}, true);
|
||||||
|
@ -631,7 +610,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}>
|
||||||
|
@ -646,6 +633,7 @@ const LoggedInView = createReactClass({
|
||||||
</div>
|
</div>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
</MatrixClientContext.Provider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -74,6 +74,21 @@ export default class MainSplit extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const wasPanelSet = this.props.panel && !prevProps.panel;
|
||||||
|
const wasPanelCleared = !this.props.panel && prevProps.panel;
|
||||||
|
|
||||||
|
if (this.resizeContainer && wasPanelSet) {
|
||||||
|
// The resizer can only be created when **both** expanded and the panel is
|
||||||
|
// set. Once both are true, the container ref will mount, which is required
|
||||||
|
// for the resizer to work.
|
||||||
|
this._createResizer();
|
||||||
|
} else if (this.resizer && wasPanelCleared) {
|
||||||
|
this.resizer.detach();
|
||||||
|
this.resizer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const bodyView = React.Children.only(this.props.children);
|
const bodyView = React.Children.only(this.props.children);
|
||||||
const panelView = this.props.panel;
|
const panelView = this.props.panel;
|
||||||
|
|
|
@ -150,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.
|
||||||
|
@ -1466,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()) {
|
||||||
|
@ -1492,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
|
||||||
|
@ -1579,9 +1569,17 @@ export default createReactClass({
|
||||||
action: 'start_post_registration',
|
action: 'start_post_registration',
|
||||||
});
|
});
|
||||||
} else if (screen.indexOf('room/') == 0) {
|
} else if (screen.indexOf('room/') == 0) {
|
||||||
const segments = screen.substring(5).split('/');
|
// Rooms can have the following formats:
|
||||||
const roomString = segments[0];
|
// #room_alias:domain or !opaque_id:domain
|
||||||
let eventId = segments.splice(1).join("/"); // empty string if no event id given
|
const room = screen.substring(5);
|
||||||
|
const domainOffset = room.indexOf(':') + 1; // 0 in case room does not contain a :
|
||||||
|
let eventOffset = room.length;
|
||||||
|
// room aliases can contain slashes only look for slash after domain
|
||||||
|
if (room.substring(domainOffset).indexOf('/') > -1) {
|
||||||
|
eventOffset = domainOffset + room.substring(domainOffset).indexOf('/');
|
||||||
|
}
|
||||||
|
const roomString = room.substring(0, eventOffset);
|
||||||
|
let eventId = room.substring(eventOffset + 1); // empty string if no event id given
|
||||||
|
|
||||||
// Previously we pulled the eventID from the segments in such a way
|
// Previously we pulled the eventID from the segments in such a way
|
||||||
// where if there was no eventId then we'd get undefined. However, we
|
// where if there was no eventId then we'd get undefined. However, we
|
||||||
|
|
|
@ -17,12 +17,17 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
|
<<<<<<< HEAD
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import * as sdk from '../../index';
|
import * as sdk from '../../index';
|
||||||
|
=======
|
||||||
|
import sdk from '../../index';
|
||||||
|
>>>>>>> develop
|
||||||
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 +39,8 @@ export default createReactClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
contextTypes: {
|
statics: {
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
contextType: MatrixClientContext,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
@ -47,7 +52,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') {
|
||||||
|
|
|
@ -23,13 +23,13 @@ import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as sdk from '../../index';
|
import * as 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 {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
export default class RightPanel extends React.Component {
|
export default class RightPanel extends React.Component {
|
||||||
static get propTypes() {
|
static get propTypes() {
|
||||||
|
@ -40,14 +40,10 @@ export default class RightPanel extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static get contextTypes() {
|
static contextType = MatrixClientContext;
|
||||||
return {
|
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props) {
|
||||||
super(props, context);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
phase: this._getPhaseFromProps(),
|
phase: this._getPhaseFromProps(),
|
||||||
isUserPrivilegedInGroup: null,
|
isUserPrivilegedInGroup: null,
|
||||||
|
@ -93,15 +89,15 @@ export default class RightPanel extends React.Component {
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -190,7 +186,7 @@ export default class RightPanel extends React.Component {
|
||||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.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 === RIGHT_PANEL_PHASES.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",
|
||||||
|
@ -209,7 +205,7 @@ export default class RightPanel extends React.Component {
|
||||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.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 === RIGHT_PANEL_PHASES.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",
|
||||||
|
|
|
@ -27,7 +27,11 @@ 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';
|
||||||
|
<<<<<<< HEAD
|
||||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||||
|
=======
|
||||||
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
>>>>>>> develop
|
||||||
|
|
||||||
const MAX_NAME_LENGTH = 80;
|
const MAX_NAME_LENGTH = 80;
|
||||||
const MAX_TOPIC_LENGTH = 160;
|
const MAX_TOPIC_LENGTH = 160;
|
||||||
|
@ -63,16 +67,6 @@ export default 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;
|
||||||
|
@ -268,6 +262,7 @@ export default 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
|
||||||
|
|
|
@ -26,7 +26,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;
|
||||||
|
@ -273,7 +273,7 @@ export default 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 });
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ import * as 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";
|
||||||
|
@ -186,7 +186,7 @@ export default class RoomSubList extends React.PureComponent {
|
||||||
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)),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -25,10 +25,8 @@ import shouldHideEvent from '../../shouldHideEvent';
|
||||||
|
|
||||||
import React, {createRef} 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 * as 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';
|
||||||
|
@ -55,7 +53,11 @@ 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 RightPanelStore from "../../stores/RightPanelStore";
|
||||||
|
<<<<<<< HEAD
|
||||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||||
|
=======
|
||||||
|
import RoomContext from "../../contexts/RoomContext";
|
||||||
|
>>>>>>> develop
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function() {};
|
let debuglog = function() {};
|
||||||
|
@ -67,6 +69,7 @@ if (DEBUG) {
|
||||||
debuglog = console.log.bind(console);
|
debuglog = console.log.bind(console);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
export const RoomContext = PropTypes.shape({
|
export const RoomContext = PropTypes.shape({
|
||||||
canReact: PropTypes.bool.isRequired,
|
canReact: PropTypes.bool.isRequired,
|
||||||
canReply: PropTypes.bool.isRequired,
|
canReply: PropTypes.bool.isRequired,
|
||||||
|
@ -74,6 +77,9 @@ export const RoomContext = PropTypes.shape({
|
||||||
});
|
});
|
||||||
|
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
|
=======
|
||||||
|
module.exports = createReactClass({
|
||||||
|
>>>>>>> develop
|
||||||
displayName: 'RoomView',
|
displayName: 'RoomView',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
ConferenceHandler: PropTypes.any,
|
ConferenceHandler: PropTypes.any,
|
||||||
|
@ -165,23 +171,6 @@ export default createReactClass({
|
||||||
|
|
||||||
canReact: false,
|
canReact: false,
|
||||||
canReply: false,
|
canReply: false,
|
||||||
|
|
||||||
useCider: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
childContextTypes: {
|
|
||||||
room: RoomContext,
|
|
||||||
},
|
|
||||||
|
|
||||||
getChildContext: function() {
|
|
||||||
const {canReact, canReply, room} = this.state;
|
|
||||||
return {
|
|
||||||
room: {
|
|
||||||
canReact,
|
|
||||||
canReply,
|
|
||||||
room,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -203,18 +192,10 @@ export default createReactClass({
|
||||||
|
|
||||||
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
|
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
|
||||||
|
|
||||||
this._onCiderUpdated();
|
|
||||||
this._ciderWatcherRef = SettingsStore.watchSetting(
|
|
||||||
"useCiderComposer", null, this._onCiderUpdated);
|
|
||||||
|
|
||||||
this._roomView = createRef();
|
this._roomView = createRef();
|
||||||
this._searchResultsPanel = createRef();
|
this._searchResultsPanel = createRef();
|
||||||
},
|
},
|
||||||
|
|
||||||
_onCiderUpdated: function() {
|
|
||||||
this.setState({useCider: SettingsStore.getValue("useCiderComposer")});
|
|
||||||
},
|
|
||||||
|
|
||||||
_onRoomViewStoreUpdate: function(initial) {
|
_onRoomViewStoreUpdate: function(initial) {
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
|
@ -462,7 +443,7 @@ export default createReactClass({
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function() {
|
||||||
if (this._roomView.current) {
|
if (this._roomView.current) {
|
||||||
const roomView = ReactDOM.findDOMNode(this._roomView.current);
|
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);
|
||||||
|
@ -506,7 +487,7 @@ export default createReactClass({
|
||||||
// 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._roomView.current);
|
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);
|
||||||
|
@ -562,15 +543,15 @@ export default 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;
|
||||||
|
@ -793,11 +774,12 @@ export default 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.
|
||||||
|
@ -806,11 +788,39 @@ export default 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() {
|
||||||
|
@ -1800,7 +1810,6 @@ export default createReactClass({
|
||||||
myMembership === 'join' && !this.state.searchResults
|
myMembership === 'join' && !this.state.searchResults
|
||||||
);
|
);
|
||||||
if (canSpeak) {
|
if (canSpeak) {
|
||||||
if (this.state.useCider) {
|
|
||||||
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||||
messageComposer =
|
messageComposer =
|
||||||
<MessageComposer
|
<MessageComposer
|
||||||
|
@ -1811,18 +1820,6 @@ export default createReactClass({
|
||||||
e2eStatus={this.state.e2eStatus}
|
e2eStatus={this.state.e2eStatus}
|
||||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||||
/>;
|
/>;
|
||||||
} else {
|
|
||||||
const SlateMessageComposer = sdk.getComponent('rooms.SlateMessageComposer');
|
|
||||||
messageComposer =
|
|
||||||
<SlateMessageComposer
|
|
||||||
room={this.state.room}
|
|
||||||
callState={this.state.callState}
|
|
||||||
disabled={this.props.disabled}
|
|
||||||
showApps={this.state.showApps}
|
|
||||||
e2eStatus={this.state.e2eStatus}
|
|
||||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Why aren't we storing the term/scope/count in this format
|
// TODO: Why aren't we storing the term/scope/count in this format
|
||||||
|
@ -1925,7 +1922,8 @@ export default 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}
|
||||||
|
@ -1933,7 +1931,8 @@ export default 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}
|
||||||
|
@ -1961,6 +1960,7 @@ export default createReactClass({
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RoomContext.Provider value={this.state}>
|
||||||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref={this._roomView}>
|
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref={this._roomView}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<RoomHeader
|
<RoomHeader
|
||||||
|
@ -1990,7 +1990,7 @@ export default 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>
|
||||||
|
@ -2000,6 +2000,7 @@ export default createReactClass({
|
||||||
</MainSplit>
|
</MainSplit>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</main>
|
</main>
|
||||||
|
</RoomContext.Provider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React, {createRef} 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";
|
||||||
|
|
||||||
|
@ -532,26 +532,26 @@ export default 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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
import React, {createRef} 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';
|
||||||
|
@ -93,8 +93,8 @@ export default createReactClass({
|
||||||
}, 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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
) }
|
) }
|
||||||
|
|
|
@ -25,6 +25,7 @@ import PropTypes from 'prop-types';
|
||||||
import {EventTimeline} from "matrix-js-sdk";
|
import {EventTimeline} from "matrix-js-sdk";
|
||||||
import * as Matrix from "matrix-js-sdk";
|
import * as Matrix from "matrix-js-sdk";
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
<<<<<<< HEAD
|
||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||||
import * as ObjectUtils from "../../ObjectUtils";
|
import * as ObjectUtils from "../../ObjectUtils";
|
||||||
import UserActivity from "../../UserActivity";
|
import UserActivity from "../../UserActivity";
|
||||||
|
@ -32,6 +33,14 @@ import Modal from "../../Modal";
|
||||||
import dis from "../../dispatcher";
|
import dis from "../../dispatcher";
|
||||||
import * as sdk from "../../index";
|
import * as sdk from "../../index";
|
||||||
import { KeyCode } from '../../Keyboard';
|
import { KeyCode } from '../../Keyboard';
|
||||||
|
=======
|
||||||
|
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
|
const dis = require("../../dispatcher");
|
||||||
|
const ObjectUtils = require('../../ObjectUtils');
|
||||||
|
const Modal = require("../../Modal");
|
||||||
|
const UserActivity = require("../../UserActivity");
|
||||||
|
import {Key} from '../../Keyboard';
|
||||||
|
>>>>>>> develop
|
||||||
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';
|
||||||
|
@ -940,8 +949,7 @@ const TimelinePanel = createReactClass({
|
||||||
|
|
||||||
// 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._messagePanel.current.handleScrollKey(ev);
|
this._messagePanel.current.handleScrollKey(ev);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import * as 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>;
|
||||||
|
|
|
@ -20,10 +20,15 @@ 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';
|
||||||
|
<<<<<<< HEAD
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import * as AvatarLogic from '../../../Avatar';
|
import * as AvatarLogic from '../../../Avatar';
|
||||||
|
=======
|
||||||
|
import AvatarLogic from '../../../Avatar';
|
||||||
|
>>>>>>> develop
|
||||||
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";
|
||||||
|
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
displayName: 'BaseAvatar',
|
displayName: 'BaseAvatar',
|
||||||
|
@ -39,10 +44,16 @@ export default 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() {
|
||||||
|
@ -60,12 +71,12 @@ export default 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) {
|
||||||
|
@ -149,7 +160,7 @@ export default 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;
|
||||||
|
|
||||||
|
@ -172,7 +183,7 @@ export default 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 }
|
||||||
|
@ -180,7 +191,7 @@ export default createReactClass({
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="mx_BaseAvatar" {...otherProps}>
|
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
||||||
{ textNode }
|
{ textNode }
|
||||||
{ imgNode }
|
{ imgNode }
|
||||||
</span>
|
</span>
|
||||||
|
@ -189,21 +200,26 @@ export default 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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -422,7 +422,7 @@ export default createReactClass({
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) {
|
if (this.props.eventTileOps) { // this event is rendered using TextualBody
|
||||||
quoteButton = (
|
quoteButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
|
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
|
||||||
{ _t('Quote') }
|
{ _t('Quote') }
|
||||||
|
|
|
@ -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 * as sdk from '../../../index';
|
import * as 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();
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -142,39 +143,41 @@ export default 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._textinput.current.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._textinput.current.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._textinput.current.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._textinput.current.value]);
|
this._addAddressesToList([textInput]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
773
src/components/views/dialogs/DMInviteDialog.js
Normal file
773
src/components/views/dialogs/DMInviteDialog.js
Normal file
|
@ -0,0 +1,773 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {createRef} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {_t} from "../../../languageHandler";
|
||||||
|
import sdk from "../../../index";
|
||||||
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
|
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||||
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
|
import {RoomMember} from "matrix-js-sdk/lib/matrix";
|
||||||
|
import * as humanize from "humanize";
|
||||||
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
import {getHttpUriForMxc} from "matrix-js-sdk/lib/content-repo";
|
||||||
|
import * as Email from "../../../email";
|
||||||
|
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
|
||||||
|
import {abbreviateUrl} from "../../../utils/UrlUtils";
|
||||||
|
import dis from "../../../dispatcher";
|
||||||
|
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
|
||||||
|
// TODO: [TravisR] Make this generic for all kinds of invites
|
||||||
|
|
||||||
|
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||||
|
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||||
|
|
||||||
|
// This is the interface that is expected by various components in this file. It is a bit
|
||||||
|
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||||
|
// for 3PIDs/email addresses.
|
||||||
|
//
|
||||||
|
// XXX: We should use TypeScript interfaces instead of this weird "abstract" class.
|
||||||
|
class Member {
|
||||||
|
/**
|
||||||
|
* The display name of this Member. For users this should be their profile's display
|
||||||
|
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
||||||
|
*/
|
||||||
|
get name(): string { throw new Error("Member class not implemented"); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of this Member. For users this should be their user ID. For 3PIDs this should
|
||||||
|
* be the 3PID address (email).
|
||||||
|
*/
|
||||||
|
get userId(): string { throw new Error("Member class not implemented"); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the MXC URL of this Member's avatar. For users this should be their profile's
|
||||||
|
* avatar MXC URL or null if none set. For 3PIDs this should always be null.
|
||||||
|
*/
|
||||||
|
getMxcAvatarUrl(): string { throw new Error("Member class not implemented"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectoryMember extends Member {
|
||||||
|
_userId: string;
|
||||||
|
_displayName: string;
|
||||||
|
_avatarUrl: string;
|
||||||
|
|
||||||
|
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
||||||
|
super();
|
||||||
|
this._userId = userDirResult.user_id;
|
||||||
|
this._displayName = userDirResult.display_name;
|
||||||
|
this._avatarUrl = userDirResult.avatar_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These next class members are for the Member interface
|
||||||
|
get name(): string {
|
||||||
|
return this._displayName || this._userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get userId(): string {
|
||||||
|
return this._userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMxcAvatarUrl(): string {
|
||||||
|
return this._avatarUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThreepidMember extends Member {
|
||||||
|
_id: string;
|
||||||
|
|
||||||
|
constructor(id: string) {
|
||||||
|
super();
|
||||||
|
this._id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a getter that would be falsey on all other implementations. Until we have
|
||||||
|
// better type support in the react-sdk we can use this trick to determine the kind
|
||||||
|
// of 3PID we're dealing with, if any.
|
||||||
|
get isEmail(): boolean {
|
||||||
|
return this._id.includes('@');
|
||||||
|
}
|
||||||
|
|
||||||
|
// These next class members are for the Member interface
|
||||||
|
get name(): string {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get userId(): string {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMxcAvatarUrl(): string {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DMUserTile extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
||||||
|
onRemove: PropTypes.func.isRequired, // takes 1 argument, the member being removed
|
||||||
|
};
|
||||||
|
|
||||||
|
_onRemove = (e) => {
|
||||||
|
// Stop the browser from highlighting text
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
this.props.onRemove(this.props.member);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||||
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||||
|
|
||||||
|
const avatarSize = 20;
|
||||||
|
const avatar = this.props.member.isEmail
|
||||||
|
? <img
|
||||||
|
className='mx_DMInviteDialog_userTile_avatar mx_DMInviteDialog_userTile_threepidAvatar'
|
||||||
|
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||||
|
width={avatarSize} height={avatarSize} />
|
||||||
|
: <BaseAvatar
|
||||||
|
className='mx_DMInviteDialog_userTile_avatar'
|
||||||
|
url={getHttpUriForMxc(
|
||||||
|
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
||||||
|
avatarSize, avatarSize, "crop")}
|
||||||
|
name={this.props.member.name}
|
||||||
|
idName={this.props.member.userId}
|
||||||
|
width={avatarSize}
|
||||||
|
height={avatarSize} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className='mx_DMInviteDialog_userTile'>
|
||||||
|
<span className='mx_DMInviteDialog_userTile_pill'>
|
||||||
|
{avatar}
|
||||||
|
<span className='mx_DMInviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||||
|
</span>
|
||||||
|
<AccessibleButton
|
||||||
|
className='mx_DMInviteDialog_userTile_remove'
|
||||||
|
onClick={this._onRemove}
|
||||||
|
>
|
||||||
|
<img src={require("../../../../res/img/icon-pill-remove.svg")} alt={_t('Remove')} width={8} height={8} />
|
||||||
|
</AccessibleButton>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DMRoomTile extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
||||||
|
lastActiveTs: PropTypes.number,
|
||||||
|
onToggle: PropTypes.func.isRequired, // takes 1 argument, the member being toggled
|
||||||
|
highlightWord: PropTypes.string,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
_onClick = (e) => {
|
||||||
|
// Stop the browser from highlighting text
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
this.props.onToggle(this.props.member);
|
||||||
|
};
|
||||||
|
|
||||||
|
_highlightName(str: string) {
|
||||||
|
if (!this.props.highlightWord) return str;
|
||||||
|
|
||||||
|
// We convert things to lowercase for index searching, but pull substrings from
|
||||||
|
// the submitted text to preserve case. Note: we don't need to htmlEntities the
|
||||||
|
// string because React will safely encode the text for us.
|
||||||
|
const lowerStr = str.toLowerCase();
|
||||||
|
const filterStr = this.props.highlightWord.toLowerCase();
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
let ii;
|
||||||
|
while ((ii = lowerStr.indexOf(filterStr, i)) >= 0) {
|
||||||
|
// Push any text we missed (first bit/middle of text)
|
||||||
|
if (ii > i) {
|
||||||
|
// Push any text we aren't highlighting (middle of text match, or beginning of text)
|
||||||
|
result.push(<span key={i + 'begin'}>{str.substring(i, ii)}</span>);
|
||||||
|
}
|
||||||
|
|
||||||
|
i = ii; // copy over ii only if we have a match (to preserve i for end-of-text matching)
|
||||||
|
|
||||||
|
// Highlight the word the user entered
|
||||||
|
const substr = str.substring(i, filterStr.length + i);
|
||||||
|
result.push(<span className='mx_DMInviteDialog_roomTile_highlight' key={i + 'bold'}>{substr}</span>);
|
||||||
|
i += substr.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push any text we missed (end of text)
|
||||||
|
if (i < (str.length - 1)) {
|
||||||
|
result.push(<span key={i + 'end'}>{str.substring(i)}</span>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||||
|
|
||||||
|
let timestamp = null;
|
||||||
|
if (this.props.lastActiveTs) {
|
||||||
|
// TODO: [TravisR] Figure out how to i18n this
|
||||||
|
// `humanize` wants seconds for a timestamp, so divide by 1000
|
||||||
|
const humanTs = humanize.relativeTime(this.props.lastActiveTs / 1000);
|
||||||
|
timestamp = <span className='mx_DMInviteDialog_roomTile_time'>{humanTs}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarSize = 36;
|
||||||
|
const avatar = this.props.member.isEmail
|
||||||
|
? <img
|
||||||
|
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||||
|
width={avatarSize} height={avatarSize} />
|
||||||
|
: <BaseAvatar
|
||||||
|
url={getHttpUriForMxc(
|
||||||
|
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
||||||
|
avatarSize, avatarSize, "crop")}
|
||||||
|
name={this.props.member.name}
|
||||||
|
idName={this.props.member.userId}
|
||||||
|
width={avatarSize}
|
||||||
|
height={avatarSize} />;
|
||||||
|
|
||||||
|
let checkmark = null;
|
||||||
|
if (this.props.isSelected) {
|
||||||
|
// To reduce flickering we put the 'selected' room tile above the real avatar
|
||||||
|
checkmark = <div className='mx_DMInviteDialog_roomTile_selected' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// To reduce flickering we put the checkmark on top of the actual avatar (prevents
|
||||||
|
// the browser from reloading the image source when the avatar remounts).
|
||||||
|
const stackedAvatar = (
|
||||||
|
<span className='mx_DMInviteDialog_roomTile_avatarStack'>
|
||||||
|
{avatar}
|
||||||
|
{checkmark}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mx_DMInviteDialog_roomTile' onClick={this._onClick}>
|
||||||
|
{stackedAvatar}
|
||||||
|
<span className='mx_DMInviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</span>
|
||||||
|
<span className='mx_DMInviteDialog_roomTile_userId'>{this._highlightName(this.props.member.userId)}</span>
|
||||||
|
{timestamp}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DMInviteDialog extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
// Takes an array of user IDs/emails to invite.
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
_debounceTimer: number = null;
|
||||||
|
_editorRef: any = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
targets: [], // array of Member objects (see interface above)
|
||||||
|
filterText: "",
|
||||||
|
recents: this._buildRecents(),
|
||||||
|
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||||
|
suggestions: this._buildSuggestions(),
|
||||||
|
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
||||||
|
serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions
|
||||||
|
threepidResultsMixin: [], // { user: ThreepidMember, userId: string}[], like recents and suggestions
|
||||||
|
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
|
||||||
|
tryingIdentityServer: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._editorRef = createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildRecents(): {userId: string, user: RoomMember, lastActive: number} {
|
||||||
|
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals();
|
||||||
|
const recents = [];
|
||||||
|
for (const userId in rooms) {
|
||||||
|
const room = rooms[userId];
|
||||||
|
const member = room.getMember(userId);
|
||||||
|
if (!member) continue; // just skip people who don't have memberships for some reason
|
||||||
|
|
||||||
|
const lastEventTs = room.timeline && room.timeline.length
|
||||||
|
? room.timeline[room.timeline.length - 1].getTs()
|
||||||
|
: 0;
|
||||||
|
if (!lastEventTs) continue; // something weird is going on with this room
|
||||||
|
|
||||||
|
recents.push({userId, user: member, lastActive: lastEventTs});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the recents by last active to save us time later
|
||||||
|
recents.sort((a, b) => b.lastActive - a.lastActive);
|
||||||
|
|
||||||
|
return recents;
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildSuggestions(): {userId: string, user: RoomMember} {
|
||||||
|
const maxConsideredMembers = 200;
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const excludedUserIds = [client.getUserId(), SdkConfig.get()['welcomeUserId']];
|
||||||
|
const joinedRooms = client.getRooms()
|
||||||
|
.filter(r => r.getMyMembership() === 'join')
|
||||||
|
.filter(r => r.getJoinedMemberCount() <= maxConsideredMembers);
|
||||||
|
|
||||||
|
// Generates { userId: {member, rooms[]} }
|
||||||
|
const memberRooms = joinedRooms.reduce((members, room) => {
|
||||||
|
const joinedMembers = room.getJoinedMembers().filter(u => !excludedUserIds.includes(u.userId));
|
||||||
|
for (const member of joinedMembers) {
|
||||||
|
if (!members[member.userId]) {
|
||||||
|
members[member.userId] = {
|
||||||
|
member: member,
|
||||||
|
// Track the room size of the 'picked' member so we can use the profile of
|
||||||
|
// the smallest room (likely a DM).
|
||||||
|
pickedMemberRoomSize: room.getJoinedMemberCount(),
|
||||||
|
rooms: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
members[member.userId].rooms.push(room);
|
||||||
|
|
||||||
|
if (room.getJoinedMemberCount() < members[member.userId].pickedMemberRoomSize) {
|
||||||
|
members[member.userId].member = member;
|
||||||
|
members[member.userId].pickedMemberRoomSize = room.getJoinedMemberCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return members;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// Generates { userId: {member, numRooms, score} }
|
||||||
|
const memberScores = Object.values(memberRooms).reduce((scores, entry) => {
|
||||||
|
const numMembersTotal = entry.rooms.reduce((c, r) => c + r.getJoinedMemberCount(), 0);
|
||||||
|
const maxRange = maxConsideredMembers * entry.rooms.length;
|
||||||
|
scores[entry.member.userId] = {
|
||||||
|
member: entry.member,
|
||||||
|
numRooms: entry.rooms.length,
|
||||||
|
score: Math.max(0, Math.pow(1 - (numMembersTotal / maxRange), 5)),
|
||||||
|
};
|
||||||
|
return scores;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const members = Object.values(memberScores);
|
||||||
|
members.sort((a, b) => {
|
||||||
|
if (a.score === b.score) {
|
||||||
|
if (a.numRooms === b.numRooms) {
|
||||||
|
return a.member.userId.localeCompare(b.member.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.numRooms - a.numRooms;
|
||||||
|
}
|
||||||
|
return b.score - a.score;
|
||||||
|
});
|
||||||
|
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
||||||
|
}
|
||||||
|
|
||||||
|
_startDm = () => {
|
||||||
|
this.props.onFinished(this.state.targets.map(t => t.userId));
|
||||||
|
};
|
||||||
|
|
||||||
|
_cancel = () => {
|
||||||
|
this.props.onFinished([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
_updateFilter = (e) => {
|
||||||
|
const term = e.target.value;
|
||||||
|
this.setState({filterText: term});
|
||||||
|
|
||||||
|
// Debounce server lookups to reduce spam. We don't clear the existing server
|
||||||
|
// results because they might still be vaguely accurate, likewise for races which
|
||||||
|
// could happen here.
|
||||||
|
if (this._debounceTimer) {
|
||||||
|
clearTimeout(this._debounceTimer);
|
||||||
|
}
|
||||||
|
this._debounceTimer = setTimeout(async () => {
|
||||||
|
MatrixClientPeg.get().searchUserDirectory({term}).then(r => {
|
||||||
|
if (term !== this.state.filterText) {
|
||||||
|
// Discard the results - we were probably too slow on the server-side to make
|
||||||
|
// these results useful. This is a race we want to avoid because we could overwrite
|
||||||
|
// more accurate results.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
serverResultsMixin: r.results.map(u => ({
|
||||||
|
userId: u.user_id,
|
||||||
|
user: new DirectoryMember(u),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}).catch(e => {
|
||||||
|
console.error("Error searching user directory:");
|
||||||
|
console.error(e);
|
||||||
|
this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal
|
||||||
|
});
|
||||||
|
|
||||||
|
// Whenever we search the directory, also try to search the identity server. It's
|
||||||
|
// all debounced the same anyways.
|
||||||
|
if (!this.state.canUseIdentityServer) {
|
||||||
|
// The user doesn't have an identity server set - warn them of that.
|
||||||
|
this.setState({tryingIdentityServer: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (term.indexOf('@') > 0 && Email.looksValid(term)) {
|
||||||
|
// Start off by suggesting the plain email while we try and resolve it
|
||||||
|
// to a real account.
|
||||||
|
this.setState({
|
||||||
|
// per above: the userId is a lie here - it's just a regular identifier
|
||||||
|
threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}],
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const authClient = new IdentityAuthClient();
|
||||||
|
const token = await authClient.getAccessToken();
|
||||||
|
if (term !== this.state.filterText) return; // abandon hope
|
||||||
|
|
||||||
|
const lookup = await MatrixClientPeg.get().lookupThreePid(
|
||||||
|
'email',
|
||||||
|
term,
|
||||||
|
undefined, // callback
|
||||||
|
token,
|
||||||
|
);
|
||||||
|
if (term !== this.state.filterText) return; // abandon hope
|
||||||
|
|
||||||
|
if (!lookup || !lookup.mxid) {
|
||||||
|
// We weren't able to find anyone - we're already suggesting the plain email
|
||||||
|
// as an alternative, so do nothing.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We append the user suggestion to give the user an option to click
|
||||||
|
// the email anyways, and so we don't cause things to jump around. In
|
||||||
|
// theory, the user would see the user pop up and think "ah yes, that
|
||||||
|
// person!"
|
||||||
|
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
|
||||||
|
if (term !== this.state.filterText || !profile) return; // abandon hope
|
||||||
|
this.setState({
|
||||||
|
threepidResultsMixin: [...this.state.threepidResultsMixin, {
|
||||||
|
user: new DirectoryMember({
|
||||||
|
user_id: lookup.mxid,
|
||||||
|
display_name: profile.displayname,
|
||||||
|
avatar_url: profile.avatar_url,
|
||||||
|
}),
|
||||||
|
userId: lookup.mxid,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error searching identity server:");
|
||||||
|
console.error(e);
|
||||||
|
this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 150); // 150ms debounce (human reaction time + some)
|
||||||
|
};
|
||||||
|
|
||||||
|
_showMoreRecents = () => {
|
||||||
|
this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN});
|
||||||
|
};
|
||||||
|
|
||||||
|
_showMoreSuggestions = () => {
|
||||||
|
this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN});
|
||||||
|
};
|
||||||
|
|
||||||
|
_toggleMember = (member: Member) => {
|
||||||
|
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||||
|
const idx = targets.indexOf(member);
|
||||||
|
if (idx >= 0) targets.splice(idx, 1);
|
||||||
|
else targets.push(member);
|
||||||
|
this.setState({targets});
|
||||||
|
};
|
||||||
|
|
||||||
|
_removeMember = (member: Member) => {
|
||||||
|
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||||
|
const idx = targets.indexOf(member);
|
||||||
|
if (idx >= 0) {
|
||||||
|
targets.splice(idx, 1);
|
||||||
|
this.setState({targets});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_onPaste = async (e) => {
|
||||||
|
// Prevent the text being pasted into the textarea
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Process it as a list of addresses to add instead
|
||||||
|
const text = e.clipboardData.getData("text");
|
||||||
|
const possibleMembers = [
|
||||||
|
// If we can avoid hitting the profile endpoint, we should.
|
||||||
|
...this.state.recents,
|
||||||
|
...this.state.suggestions,
|
||||||
|
...this.state.serverResultsMixin,
|
||||||
|
...this.state.threepidResultsMixin,
|
||||||
|
];
|
||||||
|
const toAdd = [];
|
||||||
|
const failed = [];
|
||||||
|
const potentialAddresses = text.split(/[\s,]+/);
|
||||||
|
for (const address of potentialAddresses) {
|
||||||
|
const member = possibleMembers.find(m => m.userId === address);
|
||||||
|
if (member) {
|
||||||
|
toAdd.push(member.user);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address.indexOf('@') > 0 && Email.looksValid(address)) {
|
||||||
|
toAdd.push(new ThreepidMember(address));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address[0] !== '@') {
|
||||||
|
failed.push(address); // not a user ID
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const profile = await MatrixClientPeg.get().getProfileInfo(address);
|
||||||
|
const displayName = profile ? profile.displayname : null;
|
||||||
|
const avatarUrl = profile ? profile.avatar_url : null;
|
||||||
|
toAdd.push(new DirectoryMember({
|
||||||
|
user_id: address,
|
||||||
|
display_name: displayName,
|
||||||
|
avatar_url: avatarUrl,
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error looking up profile for " + address);
|
||||||
|
console.error(e);
|
||||||
|
failed.push(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed.length > 0) {
|
||||||
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
|
Modal.createTrackedDialog('Invite Paste Fail', '', QuestionDialog, {
|
||||||
|
title: _t('Failed to find the following users'),
|
||||||
|
description: _t(
|
||||||
|
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s",
|
||||||
|
{csvNames: failed.join(", ")},
|
||||||
|
),
|
||||||
|
button: _t('OK'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({targets: [...this.state.targets, ...toAdd]});
|
||||||
|
};
|
||||||
|
|
||||||
|
_onClickInputArea = (e) => {
|
||||||
|
// Stop the browser from highlighting text
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (this._editorRef && this._editorRef.current) {
|
||||||
|
this._editorRef.current.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_onUseDefaultIdentityServerClick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Update the IS in account data. Actually using it may trigger terms.
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
useDefaultIdentityServer();
|
||||||
|
this.setState({canUseIdentityServer: true, tryingIdentityServer: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
_onManageSettingsClick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dis.dispatch({ action: 'view_user_settings' });
|
||||||
|
this._cancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
_renderSection(kind: "recents"|"suggestions") {
|
||||||
|
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
||||||
|
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
||||||
|
const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this);
|
||||||
|
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
||||||
|
const sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
||||||
|
|
||||||
|
// Mix in the server results if we have any, but only if we're searching. We track the additional
|
||||||
|
// members separately because we want to filter sourceMembers but trust the mixin arrays to have
|
||||||
|
// the right members in them.
|
||||||
|
let additionalMembers = [];
|
||||||
|
const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin;
|
||||||
|
if (this.state.filterText && hasMixins && kind === 'suggestions') {
|
||||||
|
// We don't want to duplicate members though, so just exclude anyone we've already seen.
|
||||||
|
const notAlreadyExists = (u: Member): boolean => {
|
||||||
|
return !sourceMembers.some(m => m.userId === u.userId)
|
||||||
|
&& !additionalMembers.some(m => m.userId === u.userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const uniqueServerResults = this.state.serverResultsMixin.filter(notAlreadyExists);
|
||||||
|
additionalMembers = additionalMembers.concat(...uniqueServerResults);
|
||||||
|
|
||||||
|
const uniqueThreepidResults = this.state.threepidResultsMixin.filter(notAlreadyExists);
|
||||||
|
additionalMembers = additionalMembers.concat(...uniqueThreepidResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the section if there's nothing to filter by
|
||||||
|
if (sourceMembers.length === 0 && additionalMembers.length === 0) return null;
|
||||||
|
|
||||||
|
// Do some simple filtering on the input before going much further. If we get no results, say so.
|
||||||
|
if (this.state.filterText) {
|
||||||
|
const filterBy = this.state.filterText.toLowerCase();
|
||||||
|
sourceMembers = sourceMembers
|
||||||
|
.filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy));
|
||||||
|
|
||||||
|
if (sourceMembers.length === 0 && additionalMembers.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className='mx_DMInviteDialog_section'>
|
||||||
|
<h3>{sectionName}</h3>
|
||||||
|
<p>{_t("No results")}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we mix in the additional members. Again, we presume these have already been filtered. We
|
||||||
|
// also assume they are more relevant than our suggestions and prepend them to the list.
|
||||||
|
sourceMembers = [...additionalMembers, ...sourceMembers];
|
||||||
|
|
||||||
|
// If we're going to hide one member behind 'show more', just use up the space of the button
|
||||||
|
// with the member's tile instead.
|
||||||
|
if (showNum === sourceMembers.length - 1) showNum++;
|
||||||
|
|
||||||
|
// .slice() will return an incomplete array but won't error on us if we go too far
|
||||||
|
const toRender = sourceMembers.slice(0, showNum);
|
||||||
|
const hasMore = toRender.length < sourceMembers.length;
|
||||||
|
|
||||||
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||||
|
let showMore = null;
|
||||||
|
if (hasMore) {
|
||||||
|
showMore = (
|
||||||
|
<AccessibleButton onClick={showMoreFn} kind="link">
|
||||||
|
{_t("Show more")}
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tiles = toRender.map(r => (
|
||||||
|
<DMRoomTile
|
||||||
|
member={r.user}
|
||||||
|
lastActiveTs={lastActive(r)}
|
||||||
|
key={r.userId}
|
||||||
|
onToggle={this._toggleMember}
|
||||||
|
highlightWord={this.state.filterText}
|
||||||
|
isSelected={this.state.targets.some(t => t.userId === r.userId)}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<div className='mx_DMInviteDialog_section'>
|
||||||
|
<h3>{sectionName}</h3>
|
||||||
|
{tiles}
|
||||||
|
{showMore}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderEditor() {
|
||||||
|
const targets = this.state.targets.map(t => (
|
||||||
|
<DMUserTile member={t} onRemove={this._removeMember} key={t.userId} />
|
||||||
|
));
|
||||||
|
const input = (
|
||||||
|
<textarea
|
||||||
|
key={"input"}
|
||||||
|
rows={1}
|
||||||
|
onChange={this._updateFilter}
|
||||||
|
defaultValue={this.state.filterText}
|
||||||
|
ref={this._editorRef}
|
||||||
|
onPaste={this._onPaste}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className='mx_DMInviteDialog_editor' onClick={this._onClickInputArea}>
|
||||||
|
{targets}
|
||||||
|
{input}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderIdentityServerWarning() {
|
||||||
|
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
||||||
|
if (defaultIdentityServerUrl) {
|
||||||
|
return (
|
||||||
|
<div className="mx_AddressPickerDialog_identityServer">{_t(
|
||||||
|
"Use an identity server to invite by email. " +
|
||||||
|
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
|
||||||
|
"or manage in <settings>Settings</settings>.",
|
||||||
|
{
|
||||||
|
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: sub => <a href="#" onClick={this._onUseDefaultIdentityServerClick}>{sub}</a>,
|
||||||
|
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
||||||
|
},
|
||||||
|
)}</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="mx_AddressPickerDialog_identityServer">{_t(
|
||||||
|
"Use an identity server to invite by email. " +
|
||||||
|
"Manage in <settings>Settings</settings>.",
|
||||||
|
{}, {
|
||||||
|
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
||||||
|
},
|
||||||
|
)}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||||
|
|
||||||
|
const userId = MatrixClientPeg.get().getUserId();
|
||||||
|
return (
|
||||||
|
<BaseDialog
|
||||||
|
className='mx_DMInviteDialog'
|
||||||
|
hasCancel={true}
|
||||||
|
onFinished={this._cancel}
|
||||||
|
title={_t("Direct Messages")}
|
||||||
|
>
|
||||||
|
<div className='mx_DMInviteDialog_content'>
|
||||||
|
<p>
|
||||||
|
{_t(
|
||||||
|
"If you can't find someone, ask them for their username, or share your " +
|
||||||
|
"username (%(userId)s) or <a>profile link</a>.",
|
||||||
|
{userId},
|
||||||
|
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div className='mx_DMInviteDialog_addressBar'>
|
||||||
|
{this._renderEditor()}
|
||||||
|
{this._renderIdentityServerWarning()}
|
||||||
|
<AccessibleButton
|
||||||
|
kind="primary"
|
||||||
|
onClick={this._startDm}
|
||||||
|
className='mx_DMInviteDialog_goButton'
|
||||||
|
>
|
||||||
|
{_t("Go")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
{this._renderSection('recents')}
|
||||||
|
{this._renderSection('suggestions')}
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,26 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
<<<<<<< HEAD
|
||||||
import * as sdk from '../../../index';
|
import * as 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 {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
|
=======
|
||||||
|
import { Room } from "matrix-js-sdk";
|
||||||
|
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
>>>>>>> develop
|
||||||
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 +70,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 +97,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 +160,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 +186,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 +243,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 +256,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 +314,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 +383,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 +452,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 +480,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 +517,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 +569,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 +621,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 +641,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 +664,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 +691,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() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
Copyright 2018, 2019 New Vector Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
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.
|
||||||
|
@ -94,10 +95,14 @@ export default class LogoutDialog extends React.Component {
|
||||||
// verified, so restore the backup which will give us the keys from it and
|
// verified, so restore the backup which will give us the keys from it and
|
||||||
// allow us to trust it (ie. upload keys to it)
|
// allow us to trust it (ie. upload keys to it)
|
||||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {});
|
Modal.createTrackedDialog(
|
||||||
|
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||||
|
/* priority = */ false, /* static = */ true,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
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"),
|
||||||
|
null, null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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: "",
|
||||||
|
|
|
@ -24,9 +24,16 @@ import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab";
|
||||||
import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab";
|
import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab";
|
||||||
import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab";
|
import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab";
|
||||||
import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab";
|
import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab";
|
||||||
|
<<<<<<< HEAD
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
|
=======
|
||||||
|
import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab";
|
||||||
|
import sdk from "../../../index";
|
||||||
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
|
>>>>>>> develop
|
||||||
import dis from "../../../dispatcher";
|
import dis from "../../../dispatcher";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
export default class RoomSettingsDialog extends React.Component {
|
export default class RoomSettingsDialog extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -52,6 +59,9 @@ export default class RoomSettingsDialog extends React.Component {
|
||||||
|
|
||||||
_getTabs() {
|
_getTabs() {
|
||||||
const tabs = [];
|
const tabs = [];
|
||||||
|
const featureFlag = SettingsStore.isFeatureEnabled("feature_bridge_state");
|
||||||
|
const shouldShowBridgeIcon = featureFlag &&
|
||||||
|
BridgeSettingsTab.getBridgeStateEvents(this.props.roomId).length > 0;
|
||||||
|
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
_td("General"),
|
_td("General"),
|
||||||
|
@ -73,6 +83,15 @@ export default class RoomSettingsDialog extends React.Component {
|
||||||
"mx_RoomSettingsDialog_rolesIcon",
|
"mx_RoomSettingsDialog_rolesIcon",
|
||||||
<NotificationSettingsTab roomId={this.props.roomId} />,
|
<NotificationSettingsTab roomId={this.props.roomId} />,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
if (shouldShowBridgeIcon) {
|
||||||
|
tabs.push(new Tab(
|
||||||
|
_td("Bridge Info"),
|
||||||
|
"mx_RoomSettingsDialog_bridgesIcon",
|
||||||
|
<BridgeSettingsTab roomId={this.props.roomId} />,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
_td("Advanced"),
|
_td("Advanced"),
|
||||||
"mx_RoomSettingsDialog_warningIcon",
|
"mx_RoomSettingsDialog_warningIcon",
|
||||||
|
|
|
@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
|
||||||
import * as sdk from '../../../index';
|
import * as 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';
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyUp: function(ev) {
|
onKeyUp: function(ev) {
|
||||||
if (ev.keyCode === KeyCode.ENTER) {
|
if (ev.key === Key.ENTER) {
|
||||||
this.onSubmit();
|
this.onSubmit();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
Copyright 2018, 2019 New Vector Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
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,17 +16,23 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
<<<<<<< HEAD
|
||||||
import * as sdk from '../../../../index';
|
import * as sdk from '../../../../index';
|
||||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||||
import Modal from '../../../../Modal';
|
=======
|
||||||
|
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
|
||||||
|
import sdk from '../../../../index';
|
||||||
|
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
||||||
|
>>>>>>> develop
|
||||||
|
import Modal from '../../../../Modal';
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import {Key} from "../../../../Keyboard";
|
import {Key} from "../../../../Keyboard";
|
||||||
|
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||||
|
|
||||||
const RESTORE_TYPE_PASSPHRASE = 0;
|
const RESTORE_TYPE_PASSPHRASE = 0;
|
||||||
const RESTORE_TYPE_RECOVERYKEY = 1;
|
const RESTORE_TYPE_RECOVERYKEY = 1;
|
||||||
|
const RESTORE_TYPE_SECRET_STORAGE = 2;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
|
@ -35,6 +42,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
backupInfo: null,
|
backupInfo: null,
|
||||||
|
backupKeyStored: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
loadError: null,
|
loadError: null,
|
||||||
restoreError: null,
|
restoreError: null,
|
||||||
|
@ -73,7 +81,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
onFinished: () => {
|
onFinished: () => {
|
||||||
this._loadBackupStatus();
|
this._loadBackupStatus();
|
||||||
},
|
},
|
||||||
},
|
}, null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,6 +156,32 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _restoreWithSecretStorage() {
|
||||||
|
this.setState({
|
||||||
|
loading: true,
|
||||||
|
restoreError: null,
|
||||||
|
restoreType: RESTORE_TYPE_SECRET_STORAGE,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
// `accessSecretStorage` may prompt for storage access as needed.
|
||||||
|
const recoverInfo = await accessSecretStorage(async () => {
|
||||||
|
return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
|
||||||
|
this.state.backupInfo,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
recoverInfo,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Error restoring backup", e);
|
||||||
|
this.setState({
|
||||||
|
restoreError: e,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async _loadBackupStatus() {
|
async _loadBackupStatus() {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: true,
|
loading: true,
|
||||||
|
@ -155,10 +189,20 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||||
|
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored();
|
||||||
|
this.setState({
|
||||||
|
backupInfo,
|
||||||
|
backupKeyStored,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the backup key is stored, we can proceed directly to restore.
|
||||||
|
if (backupKeyStored) {
|
||||||
|
return this._restoreWithSecretStorage();
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loadError: null,
|
loadError: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
backupInfo,
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Error loading backup status", e);
|
console.log("Error loading backup status", e);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -22,6 +22,8 @@ import * as sdk from '../../../index';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
// XXX: This component is *not* cross-signing aware. Once everything is
|
||||||
|
// cross-signing, this component should just go away.
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
displayName: 'DeviceVerifyButtons',
|
displayName: 'DeviceVerifyButtons',
|
||||||
|
|
||||||
|
@ -59,7 +61,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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,13 +25,13 @@ import * as 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,
|
|
||||||
};
|
|
||||||
|
|
|
@ -22,10 +22,14 @@ import PropTypes from 'prop-types';
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import {formatDate} from '../../../DateUtils';
|
import {formatDate} from '../../../DateUtils';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
<<<<<<< HEAD
|
||||||
import filesize from "filesize";
|
import filesize from "filesize";
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton from "./AccessibleButton";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
|
=======
|
||||||
|
import {Key} from "../../../Keyboard";
|
||||||
|
>>>>>>> develop
|
||||||
|
|
||||||
export default class ImageView extends React.Component {
|
export default class ImageView extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -60,7 +64,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 * as sdk from '../../../index';
|
import * as 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>;
|
||||||
|
|
|
@ -56,14 +56,20 @@ class ItemRange {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class LazyRenderList extends React.Component {
|
export default class LazyRenderList extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
static getDerivedStateFromProps(props, state) {
|
||||||
const range = LazyRenderList.getVisibleRangeFromProps(props);
|
const range = LazyRenderList.getVisibleRangeFromProps(props);
|
||||||
const intersectRange = range.expand(props.overflowMargin);
|
const intersectRange = range.expand(props.overflowMargin);
|
||||||
const renderRange = range.expand(props.overflowItems);
|
const renderRange = range.expand(props.overflowItems);
|
||||||
const listHasChangedSize = !!state && renderRange.totalSize() !== state.renderRange.totalSize();
|
const listHasChangedSize = !!state.renderRange && renderRange.totalSize() !== state.renderRange.totalSize();
|
||||||
// only update render Range if the list has shrunk/grown and we need to adjust padding OR
|
// only update render Range if the list has shrunk/grown and we need to adjust padding OR
|
||||||
// if the new range + overflowMargin isn't contained by the old anymore
|
// if the new range + overflowMargin isn't contained by the old anymore
|
||||||
if (listHasChangedSize || !state || !state.renderRange.contains(intersectRange)) {
|
if (listHasChangedSize || !state.renderRange || !state.renderRange.contains(intersectRange)) {
|
||||||
return {renderRange};
|
return {renderRange};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -20,12 +20,13 @@ import createReactClass from 'create-react-class';
|
||||||
import * as sdk from '../../../index';
|
import * as 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);
|
||||||
|
@ -271,12 +261,13 @@ const Pill = createReactClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
const classes = classNames("mx_Pill", pillClass, {
|
const classes = classNames("mx_Pill", pillClass, {
|
||||||
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
|
"mx_UserPill_me": userId === MatrixClientPeg.get().getUserId(),
|
||||||
"mx_UserPill_selected": this.props.isSelected,
|
"mx_UserPill_selected": this.props.isSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
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 * as 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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,17 +20,21 @@ 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 classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
<<<<<<< HEAD
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
|
=======
|
||||||
|
import sdk from '../../../index';
|
||||||
|
>>>>>>> develop
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import {_t} from '../../../languageHandler';
|
|
||||||
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
|
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
|
||||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||||
|
|
||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
import GroupStore from '../../../stores/GroupStore';
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||||
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
|
import {ContextMenu, toRightOf} from "../../structures/ContextMenu";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
|
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
|
||||||
// a thing to click on for the user to filter the visible rooms in the RoomList to:
|
// a thing to click on for the user to filter the visible rooms in the RoomList to:
|
||||||
|
@ -46,8 +50,8 @@ export default createReactClass({
|
||||||
tag: PropTypes.string,
|
tag: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
contextTypes: {
|
statics: {
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
contextType: MatrixClientContext,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
|
@ -56,6 +60,8 @@ export default createReactClass({
|
||||||
hover: false,
|
hover: false,
|
||||||
// The profile data of the group if this.props.tag is a group ID
|
// The profile data of the group if this.props.tag is a group ID
|
||||||
profile: null,
|
profile: null,
|
||||||
|
// Whether or not the context menu is open
|
||||||
|
menuDisplayed: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -81,7 +87,7 @@ export default createReactClass({
|
||||||
_onFlairStoreUpdated() {
|
_onFlairStoreUpdated() {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
FlairStore.getGroupProfileCached(
|
FlairStore.getGroupProfileCached(
|
||||||
this.context.matrixClient,
|
this.context,
|
||||||
this.props.tag,
|
this.props.tag,
|
||||||
).then((profile) => {
|
).then((profile) => {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
@ -112,12 +118,10 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseOver: function() {
|
onMouseOver: function() {
|
||||||
console.log("DEBUG onMouseOver");
|
|
||||||
this.setState({hover: true});
|
this.setState({hover: true});
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseOut: function() {
|
onMouseOut: function() {
|
||||||
console.log("DEBUG onMouseOut");
|
|
||||||
this.setState({hover: false});
|
this.setState({hover: false});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -140,12 +144,11 @@ export default createReactClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
|
||||||
const profile = this.state.profile || {};
|
const profile = this.state.profile || {};
|
||||||
const name = profile.name || this.props.tag;
|
const name = profile.name || this.props.tag;
|
||||||
const avatarHeight = 40;
|
const avatarHeight = 40;
|
||||||
|
|
||||||
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
|
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
|
||||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
@ -164,9 +167,6 @@ export default createReactClass({
|
||||||
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 />;
|
|
||||||
// FIXME: this ought to use AccessibleButton for a11y but that causes onMouseOut/onMouseOver to fire too much
|
// FIXME: this ought to use AccessibleButton for a11y but that causes onMouseOut/onMouseOver to fire too much
|
||||||
const contextButton = this.state.hover || this.state.menuDisplayed ?
|
const contextButton = this.state.hover || this.state.menuDisplayed ?
|
||||||
<div className="mx_TagTile_context_button" onClick={this.openMenu} ref={this._contextMenuButton}>
|
<div className="mx_TagTile_context_button" onClick={this.openMenu} ref={this._contextMenuButton}>
|
||||||
|
@ -184,14 +184,9 @@ export default createReactClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton");
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<ContextMenuButton
|
<AccessibleTooltipButton className={className} onClick={this.onClick} onContextMenu={this.openMenu} title={name}>
|
||||||
className={className}
|
|
||||||
onClick={this.onClick}
|
|
||||||
onContextMenu={this.openMenu}
|
|
||||||
label={_t("Options")}
|
|
||||||
isExpanded={this.state.menuDisplayed}
|
|
||||||
>
|
|
||||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
name={name}
|
name={name}
|
||||||
|
@ -200,11 +195,10 @@ export default createReactClass({
|
||||||
width={avatarHeight}
|
width={avatarHeight}
|
||||||
height={avatarHeight}
|
height={avatarHeight}
|
||||||
/>
|
/>
|
||||||
{ tip }
|
|
||||||
{ contextButton }
|
{ contextButton }
|
||||||
{ badgeElement }
|
{ badgeElement }
|
||||||
</div>
|
</div>
|
||||||
</ContextMenuButton>
|
</AccessibleTooltipButton>
|
||||||
|
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue