[CONFLICT CHUNKS] Merge branch 'develop' into travis/sourcemaps-develop

This commit is contained in:
Travis Ralston 2020-01-09 14:15:09 -07:00
commit fde32f13a5
190 changed files with 6185 additions and 2225 deletions

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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";

View file

@ -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
} }

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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');
} }

View 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;
}
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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');
}

View file

@ -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;

View file

@ -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;
} }

View file

@ -40,4 +40,5 @@ limitations under the License.
.mx_RoomRecoveryReminder_secondary { .mx_RoomRecoveryReminder_secondary {
font-size: 90%; font-size: 90%;
margin-top: 1em;
} }

View file

@ -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.

View 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;
}

View 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;
}

View file

@ -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;
} }

View 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

View 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

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="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

View 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

View 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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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/

View file

@ -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));

View file

@ -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;

View file

@ -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.

View file

@ -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>;
} }
/** /**

View file

@ -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();

View file

@ -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",
}; };

View file

@ -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;

View file

@ -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());

View file

@ -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];
}
}

View file

@ -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});
} }

View file

@ -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);

View file

@ -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,

View file

@ -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() {

View file

@ -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,
); );
} }

View file

@ -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;

View file

@ -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,

View file

@ -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;

View file

@ -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, []);

View file

@ -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>

View file

@ -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>
); );
} }
} }

View file

@ -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({

View file

@ -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>);
} }

View file

@ -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>
); );
}, },
}); });

View file

@ -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;

View file

@ -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

View file

@ -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') {

View file

@ -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",

View file

@ -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

View file

@ -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 });
} }

View file

@ -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)),
}); });
}; };

View file

@ -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>
); );
}, },
}); });

View file

@ -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();
} }

View file

@ -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;
} }

View file

@ -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>
) } ) }

View file

@ -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);

View file

@ -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>;

View file

@ -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} />
); );
} }

View file

@ -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,

View file

@ -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);
} }

View file

@ -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') }

View file

@ -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,

View file

@ -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();
} }

View file

@ -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();

View file

@ -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]);
} }
}, },

View file

@ -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>
); );
}, },
}); });

View file

@ -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,

View file

@ -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 {

View 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>
);
}
}

View file

@ -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);

View file

@ -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(

View file

@ -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');

View file

@ -99,7 +99,7 @@ export default createReactClass({
this.props.onFinished(true); this.props.onFinished(true);
} }
}, },
}); }, null, /* priority = */ false, /* static = */ true);
}, },
_onShareClicked: function() { _onShareClicked: function() {

View file

@ -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,
); );
} }

View file

@ -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: "",

View file

@ -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",

View file

@ -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();
} }
}, },

View file

@ -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);

View file

@ -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();
} }

View file

@ -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>
); );

View file

@ -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() {

View file

@ -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,
}; };

View file

@ -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 = {

View file

@ -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,

View file

@ -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,
};

View file

@ -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();

View file

@ -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>;

View file

@ -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;

View file

@ -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;

View file

@ -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>', {}, {

View file

@ -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);

View file

@ -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);
} }

View file

@ -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