Merge branches 'develop' and 't3chguy/fix_react_complaining' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix_react_complaining

 Conflicts:
	src/components/views/elements/Pill.js
This commit is contained in:
Michael Telatynski 2020-01-06 09:35:59 +00:00
commit bd0a96f2b2
130 changed files with 2039 additions and 1346 deletions

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
@ -82,7 +82,7 @@ practices that anyone working with the SDK needs to be be aware of and uphold:
'Stealing' styling information from other components (including parents) 'Stealing' styling information from other components (including parents)
is not cool, as it breaks the independence of the components. is not cool, as it breaks the independence of the components.
* CSS classes are named with an app-specific namespacing prefix to try to avoid * CSS classes are named with an app-specific name-spacing prefix to try to avoid
CSS collisions. The base skin shipped by Matrix.org with the matrix-react-sdk CSS collisions. The base skin shipped by Matrix.org with the matrix-react-sdk
uses the naming prefix "mx_". A company called Yoyodyne Inc might use a uses the naming prefix "mx_". A company called Yoyodyne Inc might use a
prefix like "yy_" for its app-specific classes. prefix like "yy_" for its app-specific classes.
@ -107,7 +107,7 @@ practices that anyone working with the SDK needs to be be aware of and uphold:
.mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its .mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its
own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override
only to the context of RoomList views. N.B. overrides should be relatively only to the context of RoomList views. N.B. overrides should be relatively
rare as in general CSS inheritence should be enough. rare as in general CSS inheritance should be enough.
* Components should render only within the bounding box of their outermost DOM * Components should render only within the bounding box of their outermost DOM
element. Page-absolute positioning and negative CSS margins and similar are element. Page-absolute positioning and negative CSS margins and similar are

View file

@ -43,7 +43,6 @@
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
"build": "yarn reskindex && yarn start:init", "build": "yarn reskindex && yarn start:init",
"build:watch": "babel src -w --skip-initial-build -d lib --source-maps --copy-files", "build:watch": "babel src -w --skip-initial-build -d lib --source-maps --copy-files",
"emoji-data-strip": "node scripts/emoji-data-strip.js",
"start": "yarn start:init && yarn start:all", "start": "yarn start:init && yarn start:all",
"start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn build:watch\" \"yarn reskindex:watch\"", "start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn build:watch\" \"yarn reskindex:watch\"",
"start:init": "babel src -d lib --source-maps --copy-files", "start:init": "babel src -d lib --source-maps --copy-files",
@ -75,7 +74,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",
"react-focus-lock": "^2.2.1",
"focus-visible": "^5.0.2", "focus-visible": "^5.0.2",
"fuse.js": "^2.2.0", "fuse.js": "^2.2.0",
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566", "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566",
@ -83,6 +81,7 @@
"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",
"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",
@ -100,6 +99,7 @@
"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",

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,6 +174,7 @@
@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/_AvatarSetting.scss";
@import "./views/settings/_CrossSigningPanel.scss"; @import "./views/settings/_CrossSigningPanel.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

@ -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,81 @@
/*
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_DMInviteDialog_addressBar {
display: flex;
flex-direction: row;
.mx_DMInviteDialog_editor {
flex: 1;
width: 100%; // Needed to make the Field inside grow
}
.mx_Field {
margin: 0;
}
.mx_DMInviteDialog_goButton {
width: 48px;
margin-left: 10px;
}
}
.mx_DMInviteDialog_section {
padding-bottom: 10px;
h3 {
font-size: 12px;
color: $muted-fg-color;
font-weight: bold;
text-transform: uppercase;
}
}
.mx_DMInviteDialog_roomTile {
cursor: pointer;
padding: 5px 10px;
&:hover {
background-color: $user-tile-hover-bg-color;
border-radius: 4px;
}
* {
vertical-align: middle;
}
.mx_DMInviteDialog_roomTile_name {
font-weight: 600;
font-size: 14px;
color: $primary-fg-color;
margin-left: 7px;
}
.mx_DMInviteDialog_roomTile_userId {
font-size: 12px;
color: $muted-fg-color;
margin-left: 7px;
}
.mx_DMInviteDialog_roomTile_time {
text-align: right;
font-size: 12px;
color: $muted-fg-color;
float: right;
line-height: 36px; // Height of the avatar to keep the time vertically aligned
}
}

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');
} }
@ -50,3 +55,17 @@ limitations under the License.
mask-size: 36px; mask-size: 36px;
mask-position: center; 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

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

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

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

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

@ -25,6 +25,7 @@ import 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,54 +780,52 @@ 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 if (!device) {
Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => { throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`);
if (!device) { }
throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`); const deviceTrust = await cli.checkDeviceTrust(userId, deviceId);
}
if (device.isVerified()) { if (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 {
throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!')); throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!'));
}
} }
}
if (device.getFingerprint() !== fingerprint) { if (device.getFingerprint() !== fingerprint) {
const fprint = device.getFingerprint(); const fprint = device.getFingerprint();
throw new Error( throw new Error(
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
'"%(fingerprint)s". This could mean your communications are being intercepted!', '"%(fingerprint)s". This could mean your communications are being intercepted!',
{ {
fprint, fprint,
userId, userId,
deviceId, deviceId,
fingerprint, fingerprint,
})); }));
} }
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, {
title: _t('Verified key'), title: _t('Verified key'),
description: <div> description: <div>
<p> <p>
{ {
_t('The signing key you provided matches the signing key you received ' + _t('The signing key you provided matches the signing key you received ' +
'from %(userId)s\'s device %(deviceId)s. Device marked as verified.', 'from %(userId)s\'s device %(deviceId)s. Device marked as verified.',
{userId, deviceId}) {userId, deviceId})
} }
</p> </p>
</div>, </div>,
}); });
}), })());
);
} }
} }
return reject(this.getUsage()); return reject(this.getUsage());

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

@ -23,6 +23,9 @@ import { _t } from '../../../languageHandler';
const sdk = require('../../../index'); const sdk = require('../../../index');
const MatrixClientPeg = require("../../../MatrixClientPeg"); const MatrixClientPeg = require("../../../MatrixClientPeg");
// XXX: This component is not cross-signing aware.
// https://github.com/vector-im/riot-web/issues/11752 tracks either updating this
// component or taking it out to pasture.
module.exports = createReactClass({ module.exports = createReactClass({
displayName: 'EncryptedEventDialog', displayName: 'EncryptedEventDialog',

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,11 +17,14 @@ limitations under the License.
import React from 'react'; import React from 'react';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import PropTypes from 'prop-types';
import sdk from '../../../../index'; import sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg'; import MatrixClientPeg from '../../../../MatrixClientPeg';
import { scorePassword } from '../../../../utils/PasswordScorer'; import { scorePassword } from '../../../../utils/PasswordScorer';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager';
import SettingsStore from '../../../../../lib/settings/SettingsStore';
const PHASE_PASSPHRASE = 0; const PHASE_PASSPHRASE = 0;
const PHASE_PASSPHRASE_CONFIRM = 1; const PHASE_PASSPHRASE_CONFIRM = 1;
@ -49,10 +52,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: '',
@ -61,12 +74,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");
}
// If we're using secret storage, skip ahead to the backing up step, as
// `accessSecretStorage` will handle passphrases as needed.
if (this.state.secureSecretStorage) {
this.state.phase = PHASE_BACKINGUP;
}
} }
componentWillMount() { componentDidMount() {
this._recoveryKeyNode = null; // If we're using secret storage, skip ahead to the backing up step, as
this._keyBackupInfo = null; // `accessSecretStorage` will handle passphrases as needed.
this._setZxcvbnResultTimeout = null; if (this.state.secureSecretStorage) {
this._createBackup();
}
} }
componentWillUnmount() { componentWillUnmount() {
@ -103,15 +129,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 {
info = await MatrixClientPeg.get().createKeyBackupVersion( if (secureSecretStorage) {
this._keyBackupInfo, await accessSecretStorage(async () => {
); info = await MatrixClientPeg.get().prepareKeyBackupVersion(
null /* random key */,
{ secureSecretStorage: true },
);
info = await MatrixClientPeg.get().createKeyBackupVersion(info);
});
} else {
info = await MatrixClientPeg.get().createKeyBackupVersion(
this._keyBackupInfo,
);
}
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup(); 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(
onFinished: this.props.onFinished, 'Restore Backup', '', RestoreKeyBackupDialog, {
}); 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.group - b.group;
return a.category - b.category; }).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

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

@ -26,8 +26,8 @@ import sanitizeHtml from 'sanitize-html';
import sdk from '../../index'; import sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk';
import classnames from 'classnames'; import classnames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext";
export default class EmbeddedPage extends React.PureComponent { export default class EmbeddedPage extends React.PureComponent {
static propTypes = { static propTypes = {
@ -39,9 +39,7 @@ export default class EmbeddedPage extends React.PureComponent {
scrollbar: PropTypes.bool, scrollbar: PropTypes.bool,
}; };
static contextTypes = { static contextType = MatrixClientContext;
matrixClient: PropTypes.instanceOf(MatrixClient),
};
constructor(props) { constructor(props) {
super(props); super(props);
@ -104,7 +102,7 @@ export default class EmbeddedPage extends React.PureComponent {
render() { render() {
// HACK: Workaround for the context's MatrixClient not updating. // HACK: Workaround for the context's MatrixClient not updating.
const client = this.context.matrixClient || MatrixClientPeg.get(); const client = this.context || MatrixClientPeg.get();
const isGuest = client ? client.isGuest() : true; const isGuest = client ? client.isGuest() : true;
const className = this.props.className; const className = this.props.className;
const classes = classnames({ const classes = classnames({

View file

@ -19,12 +19,10 @@ import React from 'react';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import { Key } from '../../Keyboard'; import { Key } from '../../Keyboard';
import sdk from '../../index'; import sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import VectorConferenceHandler from '../../VectorConferenceHandler'; import VectorConferenceHandler from '../../VectorConferenceHandler';
import TagPanelButtons from './TagPanelButtons';
import SettingsStore from '../../settings/SettingsStore'; import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler"; import {_t} from "../../languageHandler";
import Analytics from "../../Analytics"; import Analytics from "../../Analytics";
@ -39,10 +37,6 @@ const LeftPanel = createReactClass({
collapsed: PropTypes.bool.isRequired, collapsed: PropTypes.bool.isRequired,
}, },
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getInitialState: function() { getInitialState: function() {
return { return {
searchFilter: '', searchFilter: '',
@ -243,7 +237,6 @@ const LeftPanel = createReactClass({
tagPanelContainer = (<div className="mx_LeftPanel_tagPanelContainer"> tagPanelContainer = (<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel /> <TagPanel />
{ isCustomTagsEnabled ? <CustomRoomTagPanel /> : undefined } { isCustomTagsEnabled ? <CustomRoomTagPanel /> : undefined }
<TagPanelButtons />
</div>); </div>);
} }

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
@ -631,21 +617,30 @@ 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}>
{ topBar } <div
<ToastContainer /> onPaste={this._onPaste}
<DragDropContext onDragEnd={this._onDragEnd}> onKeyDown={this._onReactKeyDown}
<div ref={this._setResizeContainerRef} className={bodyClasses}> className='mx_MatrixChat_wrapper'
<LeftPanel aria-hidden={this.props.hideToSRUsers}
resizeNotifier={this.props.resizeNotifier} onMouseDown={this._onMouseDown}
collapsed={this.props.collapseLhs || false} onMouseUp={this._onMouseUp}
disabled={this.props.leftDisabled} >
/> { topBar }
<ResizeHandle /> <ToastContainer />
{ pageElement } <DragDropContext onDragEnd={this._onDragEnd}>
</div> <div ref={this._setResizeContainerRef} className={bodyClasses}>
</DragDropContext> <LeftPanel
</div> resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
<ResizeHandle />
{ pageElement }
</div>
</DragDropContext>
</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()) {
@ -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,11 @@ limitations under the License.
import React from 'react'; import React from 'react';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../index'; import sdk from '../../index';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import AccessibleButton from '../views/elements/AccessibleButton'; import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext";
export default createReactClass({ export default createReactClass({
displayName: 'MyGroups', displayName: 'MyGroups',
@ -34,8 +33,8 @@ export default createReactClass({
}; };
}, },
contextTypes: { statics: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, contextType: MatrixClientContext,
}, },
componentWillMount: function() { componentWillMount: function() {
@ -47,7 +46,7 @@ export default createReactClass({
}, },
_fetch: function() { _fetch: function() {
this.context.matrixClient.getJoinedGroups().then((result) => { this.context.getJoinedGroups().then((result) => {
this.setState({groups: result.groups, error: null}); this.setState({groups: result.groups, error: null});
}, (err) => { }, (err) => {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') { if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {

View file

@ -23,13 +23,13 @@ import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import sdk from '../../index'; import sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import { MatrixClient } from 'matrix-js-sdk';
import RateLimitedFunc from '../../ratelimitedfunc'; import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore'; import GroupStore from '../../stores/GroupStore';
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases"; import {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

@ -30,6 +30,7 @@ import PropTypes from 'prop-types';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils'; import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
import Analytics from '../../Analytics'; import Analytics from '../../Analytics';
import MatrixClientContext from "../../contexts/MatrixClientContext";
const MAX_NAME_LENGTH = 80; const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 160; const MAX_TOPIC_LENGTH = 160;
@ -65,16 +66,6 @@ module.exports = createReactClass({
}; };
}, },
childContextTypes: {
matrixClient: PropTypes.object,
},
getChildContext: function() {
return {
matrixClient: MatrixClientPeg.get(),
};
},
componentWillMount: function() { componentWillMount: function() {
this._unmounted = false; this._unmounted = false;
this.nextBatch = null; this.nextBatch = null;

View file

@ -25,7 +25,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
import Resend from '../../Resend'; import Resend from '../../Resend';
import * as cryptodevices from '../../cryptodevices'; import * as cryptodevices from '../../cryptodevices';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import { messageForResourceLimitError } from '../../utils/ErrorUtils'; import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED = 1;
@ -272,7 +272,7 @@ module.exports = createReactClass({
unsentMessages[0].error.data && unsentMessages[0].error.data &&
unsentMessages[0].error.data.error unsentMessages[0].error.data.error
) { ) {
title = unsentMessages[0].error.data.error; title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error;
} else { } else {
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length }); title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
} }

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';
@ -55,6 +53,7 @@ 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";
import RoomContext from "../../contexts/RoomContext";
const DEBUG = false; const DEBUG = false;
let debuglog = function() {}; let debuglog = function() {};
@ -66,12 +65,6 @@ if (DEBUG) {
debuglog = console.log.bind(console); debuglog = console.log.bind(console);
} }
const RoomContext = PropTypes.shape({
canReact: PropTypes.bool.isRequired,
canReply: PropTypes.bool.isRequired,
room: PropTypes.instanceOf(Room),
});
module.exports = createReactClass({ module.exports = createReactClass({
displayName: 'RoomView', displayName: 'RoomView',
propTypes: { propTypes: {
@ -169,21 +162,6 @@ module.exports = createReactClass({
}; };
}, },
childContextTypes: {
room: RoomContext,
},
getChildContext: function() {
const {canReact, canReply, room} = this.state;
return {
room: {
canReact,
canReply,
room,
},
};
},
componentWillMount: function() { componentWillMount: function() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room", this.onRoom); MatrixClientPeg.get().on("Room", this.onRoom);
@ -461,7 +439,7 @@ module.exports = 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);
@ -505,7 +483,7 @@ module.exports = 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);
@ -1953,7 +1931,8 @@ module.exports = createReactClass({
/>); />);
let topUnreadMessagesBar = null; let topUnreadMessagesBar = null;
if (this.state.showTopUnreadMessagesBar) { // Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense
if (this.state.showTopUnreadMessagesBar && !this.state.searchResults) {
const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar'); const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
topUnreadMessagesBar = (<TopUnreadMessagesBar topUnreadMessagesBar = (<TopUnreadMessagesBar
onScrollUpClick={this.jumpToReadMarker} onScrollUpClick={this.jumpToReadMarker}
@ -1961,7 +1940,8 @@ module.exports = createReactClass({
/>); />);
} }
let jumpToBottom; let jumpToBottom;
if (!this.state.atEndOfLiveTimeline) { // Do not show JumpToBottomButton if we have search results showing, it makes no sense
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton'); const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
jumpToBottom = (<JumpToBottomButton jumpToBottom = (<JumpToBottomButton
numUnreadMessages={this.state.numUnreadMessages} numUnreadMessages={this.state.numUnreadMessages}
@ -1989,45 +1969,47 @@ module.exports = createReactClass({
: null; : null;
return ( return (
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref={this._roomView}> <RoomContext.Provider value={this.state}>
<ErrorBoundary> <main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref={this._roomView}>
<RoomHeader <ErrorBoundary>
room={this.state.room} <RoomHeader
searchInfo={searchInfo} room={this.state.room}
oobData={this.props.oobData} searchInfo={searchInfo}
inRoom={myMembership === 'join'} oobData={this.props.oobData}
onSearchClick={this.onSearchClick} inRoom={myMembership === 'join'}
onSettingsClick={this.onSettingsClick} onSearchClick={this.onSearchClick}
onPinnedClick={this.onPinnedClick} onSettingsClick={this.onSettingsClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null} onPinnedClick={this.onPinnedClick}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null} onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null} onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
e2eStatus={this.state.e2eStatus} onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
/> e2eStatus={this.state.e2eStatus}
<MainSplit />
panel={rightPanel} <MainSplit
resizeNotifier={this.props.resizeNotifier} panel={rightPanel}
> resizeNotifier={this.props.resizeNotifier}
<div className={fadableSectionClasses}> >
{auxPanel} <div className={fadableSectionClasses}>
<div className="mx_RoomView_timeline"> {auxPanel}
{topUnreadMessagesBar} <div className="mx_RoomView_timeline">
{jumpToBottom} {topUnreadMessagesBar}
{messagePanel} {jumpToBottom}
{searchResultsPanel} {messagePanel}
</div> {searchResultsPanel}
<div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div>
{statusBar}
</div> </div>
<div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line" />
{statusBar}
</div>
</div>
{previewBar}
{messageComposer}
</div> </div>
{previewBar} </MainSplit>
{messageComposer} </ErrorBoundary>
</div> </main>
</MainSplit> </RoomContext.Provider>
</ErrorBoundary>
</main>
); );
}, },
}); });

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

@ -1,59 +0,0 @@
/*
Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import sdk from '../../index';
import dis from '../../dispatcher';
import Modal from '../../Modal';
import { _t } from '../../languageHandler';
const TagPanelButtons = createReactClass({
displayName: 'TagPanelButtons',
componentDidMount: function() {
this._dispatcherRef = dis.register(this._onAction);
},
componentWillUnmount() {
if (this._dispatcherRef) {
dis.unregister(this._dispatcherRef);
this._dispatcherRef = null;
}
},
_onAction(payload) {
if (payload.action === "show_redesign_feedback_dialog") {
const RedesignFeedbackDialog =
sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
}
},
render() {
const GroupsButton = sdk.getComponent('elements.GroupsButton');
const ActionButton = sdk.getComponent("elements.ActionButton");
return (<div className="mx_TagPanelButtons">
<GroupsButton />
<ActionButton
className="mx_TagPanelButtons_report" action="show_redesign_feedback_dialog"
label={_t("Report bugs & give feedback")} tooltip={true} />
</div>);
},
});
export default TagPanelButtons;

View file

@ -19,10 +19,10 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import AvatarLogic from '../../../Avatar'; import AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
module.exports = createReactClass({ module.exports = createReactClass({
displayName: 'BaseAvatar', displayName: 'BaseAvatar',
@ -38,10 +38,16 @@ module.exports = createReactClass({
// XXX resizeMethod not actually used. // XXX resizeMethod not actually used.
resizeMethod: PropTypes.string, resizeMethod: PropTypes.string,
defaultToInitialLetter: PropTypes.bool, // true to add default url defaultToInitialLetter: PropTypes.bool, // true to add default url
inputRef: PropTypes.oneOfType([
// Either a function
PropTypes.func,
// Or the instance of a DOM native element
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
}, },
contextTypes: { statics: {
matrixClient: PropTypes.instanceOf(MatrixClient), contextType: MatrixClientContext,
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -59,12 +65,12 @@ module.exports = createReactClass({
componentDidMount() { componentDidMount() {
this.unmounted = false; this.unmounted = false;
this.context.matrixClient.on('sync', this.onClientSync); this.context.on('sync', this.onClientSync);
}, },
componentWillUnmount() { componentWillUnmount() {
this.unmounted = true; this.unmounted = true;
this.context.matrixClient.removeListener('sync', this.onClientSync); this.context.removeListener('sync', this.onClientSync);
}, },
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps: function(nextProps) {
@ -148,7 +154,7 @@ module.exports = createReactClass({
const { const {
name, idName, title, url, urls, width, height, resizeMethod, name, idName, title, url, urls, width, height, resizeMethod,
defaultToInitialLetter, onClick, defaultToInitialLetter, onClick, inputRef,
...otherProps ...otherProps
} = this.props; } = this.props;
@ -171,7 +177,7 @@ module.exports = createReactClass({
if (onClick != null) { if (onClick != null) {
return ( return (
<AccessibleButton element='span' className="mx_BaseAvatar" <AccessibleButton element='span' className="mx_BaseAvatar"
onClick={onClick} {...otherProps} onClick={onClick} inputRef={inputRef} {...otherProps}
> >
{ textNode } { textNode }
{ imgNode } { imgNode }
@ -179,7 +185,7 @@ module.exports = createReactClass({
); );
} else { } else {
return ( return (
<span className="mx_BaseAvatar" {...otherProps}> <span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
{ textNode } { textNode }
{ imgNode } { imgNode }
</span> </span>
@ -188,21 +194,26 @@ module.exports = createReactClass({
} }
if (onClick != null) { if (onClick != null) {
return ( return (
<AccessibleButton className="mx_BaseAvatar mx_BaseAvatar_image" <AccessibleButton
className="mx_BaseAvatar mx_BaseAvatar_image"
element='img' element='img'
src={imageUrl} src={imageUrl}
onClick={onClick} onClick={onClick}
onError={this.onError} onError={this.onError}
width={width} height={height} width={width} height={height}
title={title} alt="" title={title} alt=""
inputRef={inputRef}
{...otherProps} /> {...otherProps} />
); );
} else { } else {
return ( return (
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl} <img
className="mx_BaseAvatar mx_BaseAvatar_image"
src={imageUrl}
onError={this.onError} onError={this.onError}
width={width} height={height} width={width} height={height}
title={title} alt="" title={title} alt=""
ref={inputRef}
{...otherProps} /> {...otherProps} />
); );
} }

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

@ -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 sdk from '../../../index'; import sdk from '../../../index';
import {MenuItem} from "../../structures/ContextMenu"; import {MenuItem} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default class TagTileContextMenu extends React.Component { export default class TagTileContextMenu extends React.Component {
static propTypes = { static propTypes = {
@ -31,9 +31,7 @@ export default class TagTileContextMenu extends React.Component {
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}; };
static contextTypes = { static contextType = MatrixClientContext;
matrixClient: PropTypes.instanceOf(MatrixClient),
};
constructor() { constructor() {
super(); super();
@ -51,7 +49,7 @@ export default class TagTileContextMenu extends React.Component {
} }
_onRemoveClick() { _onRemoveClick() {
dis.dispatch(TagOrderActions.removeTag(this.context.matrixClient, this.props.tag)); dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
this.props.onFinished(); this.props.onFinished();
} }

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

@ -22,12 +22,11 @@ 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 { Key } 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.
@ -84,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();
}, },
@ -122,36 +111,38 @@ export default createReactClass({
} }
return ( return (
<FocusLock <MatrixClientContext.Provider value={this._matrixClient}>
returnFocus={true} <FocusLock
lockProps={{ returnFocus={true}
onKeyDown: this._onKeyDown, lockProps={{
role: "dialog", onKeyDown: this._onKeyDown,
["aria-labelledby"]: "mx_BaseDialog_title", role: "dialog",
// This should point to a node describing the dialog. ["aria-labelledby"]: "mx_BaseDialog_title",
// If we were about to completely follow this recommendation we'd need to // This should point to a node describing the dialog.
// make all the components relying on BaseDialog to be aware of it. // If we were about to completely follow this recommendation we'd need to
// So instead we will use the whole content as the description. // make all the components relying on BaseDialog to be aware of it.
// Description comes first and if the content contains more text, // So instead we will use the whole content as the description.
// AT users can skip its presentation. // Description comes first and if the content contains more text,
["aria-describedby"]: this.props.contentId, // AT users can skip its presentation.
}} ["aria-describedby"]: this.props.contentId,
className={classNames({ }}
[this.props.className]: true, className={classNames({
'mx_Dialog_fixedWidth': this.props.fixedWidth, [this.props.className]: true,
})} 'mx_Dialog_fixedWidth': this.props.fixedWidth,
> })}
<div className={classNames('mx_Dialog_header', { >
'mx_Dialog_headerWithButton': !!this.props.headerButton, <div className={classNames('mx_Dialog_header', {
})}> 'mx_Dialog_headerWithButton': !!this.props.headerButton,
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'> })}>
{ this.props.title } <div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
{ this.props.title }
</div>
{ this.props.headerButton }
{ cancelButton }
</div> </div>
{ this.props.headerButton } { this.props.children }
{ cancelButton } </FocusLock>
</div> </MatrixClientContext.Provider>
{ this.props.children }
</FocusLock>
); );
}, },
}); });

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,217 @@
/*
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg";
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import DMRoomMap from "../../../utils/DMRoomMap";
import {RoomMember} from "matrix-js-sdk/lib/matrix";
import * as humanize from "humanize";
// TODO: [TravisR] Make this generic for all kinds of invites
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
class DMRoomTile extends React.PureComponent {
static propTypes = {
member: PropTypes.object.isRequired,
lastActiveTs: PropTypes.number,
onToggle: PropTypes.func.isRequired,
};
constructor() {
super();
}
_onClick = (e) => {
// Stop the browser from highlighting text
e.preventDefault();
e.stopPropagation();
this.props.onToggle(this.props.member.userId);
};
render() {
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
let timestamp = null;
if (this.props.lastActiveTs) {
// TODO: [TravisR] Figure out how to i18n this
// `humanize` wants seconds for a timestamp, so divide by 1000
const humanTs = humanize.relativeTime(this.props.lastActiveTs / 1000);
timestamp = <span className='mx_DMInviteDialog_roomTile_time'>{humanTs}</span>;
}
return (
<div className='mx_DMInviteDialog_roomTile' onClick={this._onClick}>
<MemberAvatar member={this.props.member} width={36} height={36} />
<span className='mx_DMInviteDialog_roomTile_name'>{this.props.member.name}</span>
<span className='mx_DMInviteDialog_roomTile_userId'>{this.props.member.userId}</span>
{timestamp}
</div>
);
}
}
export default class DMInviteDialog extends React.PureComponent {
static propTypes = {
// Takes an array of user IDs/emails to invite.
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {
targets: [], // string[] of mxids/email addresses
filterText: "",
recents: this._buildRecents(),
numRecentsShown: INITIAL_ROOMS_SHOWN,
};
}
_buildRecents(): {userId: string, user: RoomMember, lastActive: number} {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals();
const recents = [];
for (const userId in rooms) {
const room = rooms[userId];
const member = room.getMember(userId);
if (!member) continue; // just skip people who don't have memberships for some reason
const lastEventTs = room.timeline && room.timeline.length
? room.timeline[room.timeline.length - 1].getTs()
: 0;
if (!lastEventTs) continue; // something weird is going on with this room
recents.push({userId, user: member, lastActive: lastEventTs});
}
// Sort the recents by last active to save us time later
recents.sort((a, b) => b.lastActive - a.lastActive);
return recents;
}
_startDm = () => {
this.props.onFinished(this.state.targets);
};
_cancel = () => {
this.props.onFinished([]);
};
_updateFilter = (e) => {
this.setState({filterText: e.target.value});
};
_showMoreRecents = () => {
this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN});
};
_toggleMember = (userId) => {
const targets = this.state.targets.map(t => t); // cheap clone for mutation
const idx = targets.indexOf(userId);
if (idx >= 0) targets.splice(idx, 1);
else targets.push(userId);
this.setState({targets});
};
_renderRecents() {
if (!this.state.recents || this.state.recents.length === 0) return null;
// .slice() will return an incomplete array but won't error on us if we go too far
const toRender = this.state.recents.slice(0, this.state.numRecentsShown);
const hasMore = toRender.length < this.state.recents.length;
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
let showMore = null;
if (hasMore) {
showMore = (
<AccessibleButton onClick={this._showMoreRecents} kind="link">
{_t("Show more")}
</AccessibleButton>
);
}
const tiles = toRender.map(r => (
<DMRoomTile member={r.user} lastActiveTs={r.lastActive} key={r.userId} onToggle={this._toggleMember} />
));
return (
<div className='mx_DMInviteDialog_section'>
<h3>{_t("Recent Conversations")}</h3>
{tiles}
{showMore}
</div>
);
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Field = sdk.getComponent("elements.Field");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
// Dev note: The use of Field is temporary/incomplete pending https://github.com/vector-im/riot-web/issues/11197
// For now, we just list who the targets are.
const editor = (
<div className='mx_DMInviteDialog_editor'>
<Field
id="inviteTargets"
value={this.state.filterText}
onChange={this._updateFilter}
placeholder="TODO: Implement filtering/searching (vector-im/riot-web#11199)"
/>
</div>
);
const targets = this.state.targets.map(t => <div key={t}>{t}</div>);
const userId = MatrixClientPeg.get().getUserId();
return (
<BaseDialog
className='mx_DMInviteDialog'
hasCancel={true}
onFinished={this._cancel}
title={_t("Direct Messages")}
>
<div className='mx_DMInviteDialog_content'>
<p>
{_t(
"If you can't find someone, ask them for their username, or share your " +
"username (%(userId)s) or <a>profile link</a>.",
{userId},
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
)}
</p>
{targets}
<div className='mx_DMInviteDialog_addressBar'>
{editor}
<AccessibleButton
kind="primary"
onClick={this._startDm}
className='mx_DMInviteDialog_goButton'
>
{_t("Go")}
</AccessibleButton>
</div>
{this._renderRecents()}
</div>
</BaseDialog>
);
}
}

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,19 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Room } from "matrix-js-sdk";
import sdk from '../../../index'; import sdk from '../../../index';
import SyntaxHighlight from '../elements/SyntaxHighlight'; import SyntaxHighlight from '../elements/SyntaxHighlight';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Field from "../elements/Field"; import Field from "../elements/Field";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
class DevtoolsComponent extends React.Component { class GenericEditor extends React.PureComponent {
static contextTypes = {
roomId: PropTypes.string.isRequired,
};
}
class GenericEditor extends DevtoolsComponent {
// static propTypes = {onBack: PropTypes.func.isRequired}; // static propTypes = {onBack: PropTypes.func.isRequired};
constructor(props, context) { constructor(props) {
super(props, context); super(props);
this._onChange = this._onChange.bind(this); this._onChange = this._onChange.bind(this);
this.onBack = this.onBack.bind(this); this.onBack = this.onBack.bind(this);
} }
@ -67,12 +63,15 @@ class SendCustomEvent extends GenericEditor {
static propTypes = { static propTypes = {
onBack: PropTypes.func.isRequired, onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
forceStateEvent: PropTypes.bool, forceStateEvent: PropTypes.bool,
inputs: PropTypes.object, inputs: PropTypes.object,
}; };
constructor(props, context) { static contextType = MatrixClientContext;
super(props, context);
constructor(props) {
super(props);
this._send = this._send.bind(this); this._send = this._send.bind(this);
const {eventType, stateKey, evContent} = Object.assign({ const {eventType, stateKey, evContent} = Object.assign({
@ -91,11 +90,11 @@ class SendCustomEvent extends GenericEditor {
} }
send(content) { send(content) {
const cli = MatrixClientPeg.get(); const cli = this.context;
if (this.state.isStateEvent) { if (this.state.isStateEvent) {
return cli.sendStateEvent(this.context.roomId, this.state.eventType, content, this.state.stateKey); return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey);
} else { } else {
return cli.sendEvent(this.context.roomId, this.state.eventType, content); return cli.sendEvent(this.props.room.roomId, this.state.eventType, content);
} }
} }
@ -154,13 +153,16 @@ class SendAccountData extends GenericEditor {
static getLabel() { return _t('Send Account Data'); } static getLabel() { return _t('Send Account Data'); }
static propTypes = { static propTypes = {
room: PropTypes.instanceOf(Room).isRequired,
isRoomAccountData: PropTypes.bool, isRoomAccountData: PropTypes.bool,
forceMode: PropTypes.bool, forceMode: PropTypes.bool,
inputs: PropTypes.object, inputs: PropTypes.object,
}; };
constructor(props, context) { static contextType = MatrixClientContext;
super(props, context);
constructor(props) {
super(props);
this._send = this._send.bind(this); this._send = this._send.bind(this);
const {eventType, evContent} = Object.assign({ const {eventType, evContent} = Object.assign({
@ -177,9 +179,9 @@ class SendAccountData extends GenericEditor {
} }
send(content) { send(content) {
const cli = MatrixClientPeg.get(); const cli = this.context;
if (this.state.isRoomAccountData) { if (this.state.isRoomAccountData) {
return cli.setRoomAccountData(this.context.roomId, this.state.eventType, content); return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content);
} }
return cli.setAccountData(this.state.eventType, content); return cli.setAccountData(this.state.eventType, content);
} }
@ -234,7 +236,7 @@ class SendAccountData extends GenericEditor {
const INITIAL_LOAD_TILES = 20; const INITIAL_LOAD_TILES = 20;
const LOAD_TILES_STEP_SIZE = 50; const LOAD_TILES_STEP_SIZE = 50;
class FilteredList extends React.Component { class FilteredList extends React.PureComponent {
static propTypes = { static propTypes = {
children: PropTypes.any, children: PropTypes.any,
query: PropTypes.string, query: PropTypes.string,
@ -247,8 +249,8 @@ class FilteredList extends React.Component {
return children.filter((child) => child.key.toLowerCase().includes(lcQuery)); return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
} }
constructor(props, context) { constructor(props) {
super(props, context); super(props);
this.state = { this.state = {
filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query), filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query),
@ -305,19 +307,20 @@ class FilteredList extends React.Component {
} }
} }
class RoomStateExplorer extends DevtoolsComponent { class RoomStateExplorer extends React.PureComponent {
static getLabel() { return _t('Explore Room State'); } static getLabel() { return _t('Explore Room State'); }
static propTypes = { static propTypes = {
onBack: PropTypes.func.isRequired, onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
}; };
constructor(props, context) { static contextType = MatrixClientContext;
super(props, context);
const room = MatrixClientPeg.get().getRoom(this.context.roomId); constructor(props) {
this.roomStateEvents = room.currentState.events; super(props);
this.roomStateEvents = this.props.room.currentState.events;
this.onBack = this.onBack.bind(this); this.onBack = this.onBack.bind(this);
this.editEv = this.editEv.bind(this); this.editEv = this.editEv.bind(this);
@ -373,7 +376,7 @@ class RoomStateExplorer extends DevtoolsComponent {
render() { render() {
if (this.state.event) { if (this.state.event) {
if (this.state.editing) { if (this.state.editing) {
return <SendCustomEvent forceStateEvent={true} onBack={this.onBack} inputs={{ return <SendCustomEvent room={this.props.room} forceStateEvent={true} onBack={this.onBack} inputs={{
eventType: this.state.event.getType(), eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'), evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
stateKey: this.state.event.getStateKey(), stateKey: this.state.event.getStateKey(),
@ -442,15 +445,18 @@ class RoomStateExplorer extends DevtoolsComponent {
} }
} }
class AccountDataExplorer extends DevtoolsComponent { class AccountDataExplorer extends React.PureComponent {
static getLabel() { return _t('Explore Account Data'); } static getLabel() { return _t('Explore Account Data'); }
static propTypes = { static propTypes = {
onBack: PropTypes.func.isRequired, onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
}; };
constructor(props, context) { static contextType = MatrixClientContext;
super(props, context);
constructor(props) {
super(props);
this.onBack = this.onBack.bind(this); this.onBack = this.onBack.bind(this);
this.editEv = this.editEv.bind(this); this.editEv = this.editEv.bind(this);
@ -467,11 +473,10 @@ class AccountDataExplorer extends DevtoolsComponent {
} }
getData() { getData() {
const cli = MatrixClientPeg.get();
if (this.state.isRoomAccountData) { if (this.state.isRoomAccountData) {
return cli.getRoom(this.context.roomId).accountData; return this.props.room.accountData;
} }
return cli.store.accountData; return this.context.store.accountData;
} }
onViewSourceClick(event) { onViewSourceClick(event) {
@ -505,10 +510,14 @@ 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
eventType: this.state.event.getType(), room={this.props.room}
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'), isRoomAccountData={this.state.isRoomAccountData}
}} forceMode={true} />; onBack={this.onBack}
inputs={{
eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
}} forceMode={true} />;
} }
return <div className="mx_ViewSource"> return <div className="mx_ViewSource">
@ -553,17 +562,20 @@ class AccountDataExplorer extends DevtoolsComponent {
} }
} }
class ServersInRoomList extends DevtoolsComponent { class ServersInRoomList extends React.PureComponent {
static getLabel() { return _t('View Servers in Room'); } static getLabel() { return _t('View Servers in Room'); }
static propTypes = { static propTypes = {
onBack: PropTypes.func.isRequired, onBack: PropTypes.func.isRequired,
room: PropTypes.instanceOf(Room).isRequired,
}; };
constructor(props, context) { static contextType = MatrixClientContext;
super(props, context);
const room = MatrixClientPeg.get().getRoom(this.context.roomId); constructor(props) {
super(props);
const room = this.props.room;
const servers = new Set(); const servers = new Set();
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1])); room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
this.servers = Array.from(servers).map(s => this.servers = Array.from(servers).map(s =>
@ -602,19 +614,14 @@ const Entries = [
ServersInRoomList, ServersInRoomList,
]; ];
export default class DevtoolsDialog extends React.Component { export default class DevtoolsDialog extends React.PureComponent {
static childContextTypes = {
roomId: PropTypes.string.isRequired,
// client: PropTypes.instanceOf(MatixClient),
};
static propTypes = { static propTypes = {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}; };
constructor(props, context) { constructor(props) {
super(props, context); super(props);
this.onBack = this.onBack.bind(this); this.onBack = this.onBack.bind(this);
this.onCancel = this.onCancel.bind(this); this.onCancel = this.onCancel.bind(this);
@ -627,10 +634,6 @@ export default class DevtoolsDialog extends React.Component {
this._unmounted = true; this._unmounted = true;
} }
getChildContext() {
return { roomId: this.props.roomId };
}
_setMode(mode) { _setMode(mode) {
return () => { return () => {
this.setState({ mode }); this.setState({ mode });
@ -654,15 +657,17 @@ export default class DevtoolsDialog extends React.Component {
let body; let body;
if (this.state.mode) { if (this.state.mode) {
body = <div> body = <MatrixClientContext.Consumer>
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div> {(cli) => <React.Fragment>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div> <div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
<div className="mx_DevTools_label_bottom" /> <div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<this.state.mode onBack={this.onBack} /> <div className="mx_DevTools_label_bottom" />
</div>; <this.state.mode onBack={this.onBack} room={cli.getRoom(this.props.roomId)} />
</React.Fragment>}
</MatrixClientContext.Consumer>;
} else { } else {
const classes = "mx_DevTools_RoomStateExplorer_button"; const classes = "mx_DevTools_RoomStateExplorer_button";
body = <div> body = <React.Fragment>
<div> <div>
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</div> <div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div> <div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
@ -679,7 +684,7 @@ export default class DevtoolsDialog extends React.Component {
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>{ _t('Cancel') }</button> <button onClick={this.onCancel}>{ _t('Cancel') }</button>
</div> </div>
</div>; </React.Fragment>;
} }
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

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,11 @@ 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";
import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab";
import sdk from "../../../index"; import sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg"; import MatrixClientPeg from "../../../MatrixClientPeg";
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 +54,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 +78,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

@ -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,18 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../../index'; import sdk from '../../../../index';
import MatrixClientPeg from '../../../../MatrixClientPeg'; import MatrixClientPeg from '../../../../MatrixClientPeg';
import Modal from '../../../../Modal'; import Modal from '../../../../Modal';
import { MatrixClient } from 'matrix-js-sdk';
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 +37,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 +76,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
onFinished: () => { onFinished: () => {
this._loadBackupStatus(); this._loadBackupStatus();
}, },
}, }, null, /* priority = */ false, /* static = */ true,
); );
} }
@ -148,6 +151,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 +184,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

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

View file

@ -25,13 +25,13 @@ import sdk from '../../../index';
* Parent components should supply an 'onSubmit' callback which returns a * Parent components should supply an 'onSubmit' callback which returns a
* promise; a spinner is shown until the promise resolves. * promise; a spinner is shown until the promise resolves.
* *
* The parent can also supply a 'getIntialValue' callback, which works in a * The parent can also supply a 'getInitialValue' callback, which works in a
* similarly asynchronous way. If this is not provided, the initial value is * similarly asynchronous way. If this is not provided, the initial value is
* taken from the 'initialValue' property. * taken from the 'initialValue' property.
*/ */
export default class EditableTextContainer extends React.Component { export default class EditableTextContainer extends React.Component {
constructor(props, context) { constructor(props) {
super(props, context); super(props);
this._unmounted = false; this._unmounted = false;
this.state = { this.state = {

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

@ -1,37 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const GroupsButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton className="mx_GroupsButton" action="toggle_my_groups"
label={_t("Communities")}
size={props.size}
tooltip={true}
/>
);
};
GroupsButton.propTypes = {
size: PropTypes.string,
};
export default GroupsButton;

View file

@ -20,12 +20,13 @@ import createReactClass from 'create-react-class';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import classNames from 'classnames'; import classNames from 'classnames';
import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk'; import { Room, RoomMember } from 'matrix-js-sdk';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import { getDisplayAliasForRoom } from '../../../Rooms'; import { getDisplayAliasForRoom } from '../../../Rooms';
import FlairStore from "../../../stores/FlairStore"; import FlairStore from "../../../stores/FlairStore";
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks"; import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
// For URLs of matrix.to links in the timeline which have been reformatted by // For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`) // HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
@ -66,17 +67,6 @@ const Pill = createReactClass({
isSelected: PropTypes.bool, isSelected: PropTypes.bool,
}, },
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getChildContext() {
return {
matrixClient: this._matrixClient,
};
},
getInitialState() { getInitialState() {
return { return {
// ID/alias of the room/user // ID/alias of the room/user
@ -127,7 +117,7 @@ const Pill = createReactClass({
} }
break; break;
case Pill.TYPE_USER_MENTION: { case Pill.TYPE_USER_MENTION: {
const localMember = nextProps.room ? nextProps.room.getMember(resourceId) : null; const localMember = nextProps.room ? nextProps.room.getMember(resourceId) : undefined;
member = localMember; member = localMember;
if (!localMember) { if (!localMember) {
member = new RoomMember(null, resourceId); member = new RoomMember(null, resourceId);
@ -276,15 +266,17 @@ const Pill = createReactClass({
}); });
if (this.state.pillType) { if (this.state.pillType) {
return this.props.inMessage ? return <MatrixClientContext.Provider value={this._matrixClient}>
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}> { this.props.inMessage ?
{ avatar } <a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
{ linkText } { avatar }
</a> : { linkText }
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}> </a> :
{ avatar } <span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
{ linkText } { avatar }
</span>; { linkText }
</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 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,16 @@ 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';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index'; import sdk from '../../../index';
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 +45,8 @@ export default createReactClass({
tag: PropTypes.string, tag: PropTypes.string,
}, },
contextTypes: { statics: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, contextType: MatrixClientContext,
}, },
getInitialState() { getInitialState() {
@ -83,7 +82,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;
@ -114,12 +113,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});
}, },
@ -142,12 +139,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;
@ -166,9 +162,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}>
@ -186,14 +179,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}
@ -202,11 +190,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>;

View file

@ -16,54 +16,12 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import EMOJIBASE from 'emojibase-data/en/compact.json';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as recent from './recent'; import * as recent from './recent';
import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji";
const EMOJIBASE_CATEGORY_IDS = [
"people", // smileys
"people", // actually people
"control", // modifiers and such, not displayed in picker
"nature",
"foods",
"places",
"activity",
"objects",
"symbols",
"flags",
];
const DATA_BY_CATEGORY = {
"people": [],
"nature": [],
"foods": [],
"places": [],
"activity": [],
"objects": [],
"symbols": [],
"flags": [],
};
const DATA_BY_EMOJI = {};
const VARIATION_SELECTOR = String.fromCharCode(0xFE0F);
EMOJIBASE.forEach(emoji => {
if (emoji.unicode.includes(VARIATION_SELECTOR)) {
// Clone data into variation-less version
emoji = Object.assign({}, emoji, {
unicode: emoji.unicode.replace(VARIATION_SELECTOR, ""),
});
}
DATA_BY_EMOJI[emoji.unicode] = emoji;
const categoryId = EMOJIBASE_CATEGORY_IDS[emoji.group];
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
DATA_BY_CATEGORY[categoryId].push(emoji);
}
// This is used as the string to match the query against when filtering emojis.
emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`.toLowerCase();
});
export const CATEGORY_HEADER_HEIGHT = 22; export const CATEGORY_HEADER_HEIGHT = 22;
export const EMOJI_HEIGHT = 37; export const EMOJI_HEIGHT = 37;
@ -91,7 +49,7 @@ class EmojiPicker extends React.Component {
// Convert recent emoji characters to emoji data, removing unknowns. // Convert recent emoji characters to emoji data, removing unknowns.
this.recentlyUsed = recent.get() this.recentlyUsed = recent.get()
.map(unicode => DATA_BY_EMOJI[unicode]) .map(unicode => getEmojiFromUnicode(unicode))
.filter(data => !!data); .filter(data => !!data);
this.memoizedDataByCategory = { this.memoizedDataByCategory = {
recent: this.recentlyUsed, recent: this.recentlyUsed,

View file

@ -19,15 +19,15 @@ import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { findEmojiData } from '../../../HtmlUtils'; import {getEmojiFromUnicode} from "../../../emoji";
// We use the variation-selector Heart in Quick Reactions for some reason
const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => { const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => {
const data = findEmojiData(emoji); const data = getEmojiFromUnicode(emoji);
if (!data) { if (!data) {
throw new Error(`Emoji ${emoji} doesn't exist in emojibase`); throw new Error(`Emoji ${emoji} doesn't exist in emojibase`);
} }
// Prefer our unicode value for quick reactions (which does not have // Prefer our unicode value for quick reactions as we sometimes use variation selectors.
// variation selectors).
return Object.assign({}, data, { unicode: emoji }); return Object.assign({}, data, { unicode: emoji });
}); });

View file

@ -34,7 +34,7 @@ module.exports = createReactClass({
render: function() { render: function() {
return ( return (
<div className="mx_MatrixToolbar"> <div className="mx_MatrixToolbar">
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" /> <img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
<div className="mx_MatrixToolbar_content"> <div className="mx_MatrixToolbar_content">
{ _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a> { _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a>
</div> </div>

View file

@ -97,7 +97,7 @@ export default createReactClass({
} }
return ( return (
<div className="mx_MatrixToolbar"> <div className="mx_MatrixToolbar">
<img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" /> <img className="mx_MatrixToolbar_warning" src={require("../../../../res/img/warning.svg")} width="24" height="23" alt="" />
<div className="mx_MatrixToolbar_content"> <div className="mx_MatrixToolbar_content">
{_t("A new version of Riot is available.")} {_t("A new version of Riot is available.")}
</div> </div>

View file

@ -19,13 +19,13 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import {_t} from '../../../languageHandler'; import {_t} from '../../../languageHandler';
import classNames from 'classnames'; import classNames from 'classnames';
import MatrixClientPeg from "../../../MatrixClientPeg"; import MatrixClientPeg from "../../../MatrixClientPeg";
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu"; import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
// XXX this class copies a lot from RoomTile.js // XXX this class copies a lot from RoomTile.js
export default createReactClass({ export default createReactClass({
@ -35,8 +35,8 @@ export default createReactClass({
group: PropTypes.object.isRequired, group: PropTypes.object.isRequired,
}, },
contextTypes: { statics: {
matrixClient: PropTypes.instanceOf(MatrixClient), contextType: MatrixClientContext,
}, },
getInitialState: function() { getInitialState: function() {
@ -58,7 +58,7 @@ export default createReactClass({
onMouseEnter: function() { onMouseEnter: function() {
const state = {hover: true}; const state = {hover: true};
// Only allow non-guests to access the context menu // Only allow non-guests to access the context menu
if (!this.context.matrixClient.isGuest()) { if (!this.context.isGuest()) {
state.badgeHover = true; state.badgeHover = true;
} }
this.setState(state); this.setState(state);
@ -118,7 +118,7 @@ export default createReactClass({
const groupName = this.props.group.name || this.props.group.groupId; const groupName = this.props.group.name || this.props.group.groupId;
const httpAvatarUrl = this.props.group.avatarUrl ? const httpAvatarUrl = this.props.group.avatarUrl ?
this.context.matrixClient.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null; this.context.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null;
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />; const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;

View file

@ -18,7 +18,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import sdk from '../../../index'; import sdk from '../../../index';
@ -26,12 +25,13 @@ import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups'; import { GroupMemberType } from '../../../groups';
import GroupStore from '../../../stores/GroupStore'; import GroupStore from '../../../stores/GroupStore';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
module.exports = createReactClass({ module.exports = createReactClass({
displayName: 'GroupMemberInfo', displayName: 'GroupMemberInfo',
contextTypes: { statics: {
matrixClient: PropTypes.instanceOf(MatrixClient), contextType: MatrixClientContext,
}, },
propTypes: { propTypes: {
@ -85,7 +85,7 @@ module.exports = createReactClass({
_onKick: function() { _onKick: function() {
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createDialog(ConfirmUserActionDialog, { Modal.createDialog(ConfirmUserActionDialog, {
matrixClient: this.context.matrixClient, matrixClient: this.context,
groupMember: this.props.groupMember, groupMember: this.props.groupMember,
action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'), action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'),
title: this.state.isUserInvited ? _t('Disinvite this user from community?') title: this.state.isUserInvited ? _t('Disinvite this user from community?')
@ -95,7 +95,7 @@ module.exports = createReactClass({
if (!proceed) return; if (!proceed) return;
this.setState({removingUser: true}); this.setState({removingUser: true});
this.context.matrixClient.removeUserFromGroup( this.context.removeUserFromGroup(
this.props.groupId, this.props.groupMember.userId, this.props.groupId, this.props.groupMember.userId,
).then(() => { ).then(() => {
// return to the user list // return to the user list
@ -171,7 +171,7 @@ module.exports = createReactClass({
const avatarUrl = this.props.groupMember.avatarUrl; const avatarUrl = this.props.groupMember.avatarUrl;
let avatarElement; let avatarElement;
if (avatarUrl) { if (avatarUrl) {
const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800); const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);
avatarElement = (<div className="mx_MemberInfo_avatar"> avatarElement = (<div className="mx_MemberInfo_avatar">
<img src={httpUrl} /> <img src={httpUrl} />
</div>); </div>);

View file

@ -19,10 +19,10 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import { GroupMemberType } from '../../../groups'; import { GroupMemberType } from '../../../groups';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default createReactClass({ export default createReactClass({
displayName: 'GroupMemberTile', displayName: 'GroupMemberTile',
@ -36,8 +36,8 @@ export default createReactClass({
return {}; return {};
}, },
contextTypes: { statics: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, contextType: MatrixClientContext,
}, },
onClick: function(e) { onClick: function(e) {
@ -53,7 +53,7 @@ export default createReactClass({
const EntityTile = sdk.getComponent('rooms.EntityTile'); const EntityTile = sdk.getComponent('rooms.EntityTile');
const name = this.props.member.displayname || this.props.member.userId; const name = this.props.member.displayname || this.props.member.userId;
const avatarUrl = this.context.matrixClient.mxcUrlToHttp( const avatarUrl = this.context.mxcUrlToHttp(
this.props.member.avatarUrl, this.props.member.avatarUrl,
36, 36, 'crop', 36, 36, 'crop',
); );

View file

@ -17,18 +17,18 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import { MatrixClient } from 'matrix-js-sdk';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import GroupStore from '../../../stores/GroupStore'; import GroupStore from '../../../stores/GroupStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
module.exports = createReactClass({ module.exports = createReactClass({
displayName: 'GroupRoomInfo', displayName: 'GroupRoomInfo',
contextTypes: { statics: {
matrixClient: PropTypes.instanceOf(MatrixClient), contextType: MatrixClientContext,
}, },
propTypes: { propTypes: {
@ -206,7 +206,7 @@ module.exports = createReactClass({
const avatarUrl = this.state.groupRoom.avatarUrl; const avatarUrl = this.state.groupRoom.avatarUrl;
let avatarElement; let avatarElement;
if (avatarUrl) { if (avatarUrl) {
const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800); const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);
avatarElement = (<div className="mx_MemberInfo_avatar"> avatarElement = (<div className="mx_MemberInfo_avatar">
<img src={httpUrl} /> <img src={httpUrl} />
</div>); </div>);

View file

@ -17,10 +17,10 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import { GroupRoomType } from '../../../groups'; import { GroupRoomType } from '../../../groups';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
const GroupRoomTile = createReactClass({ const GroupRoomTile = createReactClass({
displayName: 'GroupRoomTile', displayName: 'GroupRoomTile',
@ -41,7 +41,7 @@ const GroupRoomTile = createReactClass({
render: function() { render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const avatarUrl = this.context.matrixClient.mxcUrlToHttp( const avatarUrl = this.context.mxcUrlToHttp(
this.props.groupRoom.avatarUrl, this.props.groupRoom.avatarUrl,
36, 36, 'crop', 36, 36, 'crop',
); );
@ -66,9 +66,7 @@ const GroupRoomTile = createReactClass({
}, },
}); });
GroupRoomTile.contextTypes = { GroupRoomTile.contextType = MatrixClientContext;
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};
export default GroupRoomTile; export default GroupRoomTile;

View file

@ -17,11 +17,11 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import {MatrixClient} from 'matrix-js-sdk';
import { Draggable, Droppable } from 'react-beautiful-dnd'; import { Draggable, Droppable } from 'react-beautiful-dnd';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import FlairStore from '../../../stores/FlairStore'; import FlairStore from '../../../stores/FlairStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
function nop() {} function nop() {}
@ -37,8 +37,8 @@ const GroupTile = createReactClass({
draggable: PropTypes.bool, draggable: PropTypes.bool,
}, },
contextTypes: { statics: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, contextType: MatrixClientContext,
}, },
getInitialState() { getInitialState() {
@ -56,7 +56,7 @@ const GroupTile = createReactClass({
}, },
componentWillMount: function() { componentWillMount: function() {
FlairStore.getGroupProfileCached(this.context.matrixClient, this.props.groupId).then((profile) => { FlairStore.getGroupProfileCached(this.context, this.props.groupId).then((profile) => {
this.setState({profile}); this.setState({profile});
}).catch((err) => { }).catch((err) => {
console.error('Error whilst getting cached profile for GroupTile', err); console.error('Error whilst getting cached profile for GroupTile', err);
@ -80,7 +80,7 @@ const GroupTile = createReactClass({
const descElement = this.props.showDescription ? const descElement = this.props.showDescription ?
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> : <div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
<div />; <div />;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp( const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null; profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null;
let avatarElement = ( let avatarElement = (

View file

@ -15,17 +15,16 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import sdk from '../../../index'; import sdk from '../../../index';
import { MatrixClient } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default createReactClass({ export default createReactClass({
displayName: 'GroupUserSettings', displayName: 'GroupUserSettings',
contextTypes: { statics: {
matrixClient: PropTypes.instanceOf(MatrixClient), contextType: MatrixClientContext,
}, },
getInitialState() { getInitialState() {
@ -36,7 +35,7 @@ export default createReactClass({
}, },
componentWillMount: function() { componentWillMount: 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) => {
console.error(err); console.error(err);

View file

@ -26,6 +26,7 @@ import {decryptFile} from '../../../utils/DecryptFile';
import Tinter from '../../../Tinter'; import Tinter from '../../../Tinter';
import request from 'browser-request'; import request from 'browser-request';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
// A cached tinted copy of require("../../../../res/img/download.svg") // A cached tinted copy of require("../../../../res/img/download.svg")
@ -214,10 +215,6 @@ module.exports = createReactClass({
tileShape: PropTypes.string, tileShape: PropTypes.string,
}, },
contextTypes: {
appConfig: PropTypes.object,
},
/** /**
* Extracts a human readable label for the file attachment to use as * Extracts a human readable label for the file attachment to use as
* link text. * link text.
@ -360,8 +357,9 @@ module.exports = createReactClass({
// If the attachment is encryped then put the link inside an iframe. // If the attachment is encryped then put the link inside an iframe.
let renderer_url = DEFAULT_CROSS_ORIGIN_RENDERER; let renderer_url = DEFAULT_CROSS_ORIGIN_RENDERER;
if (this.context.appConfig && this.context.appConfig.cross_origin_renderer_url) { const appConfig = SdkConfig.get();
renderer_url = this.context.appConfig.cross_origin_renderer_url; if (appConfig && appConfig.cross_origin_renderer_url) {
renderer_url = appConfig.cross_origin_renderer_url;
} }
renderer_url += "?origin=" + encodeURIComponent(window.location.origin); renderer_url += "?origin=" + encodeURIComponent(window.location.origin);
return ( return (

View file

@ -18,7 +18,6 @@ limitations under the License.
import React, {createRef} from 'react'; import React, {createRef} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import MFileBody from './MFileBody'; import MFileBody from './MFileBody';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -26,6 +25,7 @@ import sdk from '../../../index';
import { decryptFile } from '../../../utils/DecryptFile'; import { decryptFile } from '../../../utils/DecryptFile';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default class MImageBody extends React.Component { export default class MImageBody extends React.Component {
static propTypes = { static propTypes = {
@ -39,9 +39,7 @@ export default class MImageBody extends React.Component {
maxImageHeight: PropTypes.number, maxImageHeight: PropTypes.number,
}; };
static contextTypes = { static contextType = MatrixClientContext;
matrixClient: PropTypes.instanceOf(MatrixClient),
};
constructor(props) { constructor(props) {
super(props); super(props);
@ -71,7 +69,7 @@ export default class MImageBody extends React.Component {
componentWillMount() { componentWillMount() {
this.unmounted = false; this.unmounted = false;
this.context.matrixClient.on('sync', this.onClientSync); this.context.on('sync', this.onClientSync);
} }
// FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too! // FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too!
@ -174,7 +172,7 @@ export default class MImageBody extends React.Component {
if (content.file !== undefined) { if (content.file !== undefined) {
return this.state.decryptedUrl; return this.state.decryptedUrl;
} else { } else {
return this.context.matrixClient.mxcUrlToHttp(content.url); return this.context.mxcUrlToHttp(content.url);
} }
} }
@ -198,7 +196,7 @@ export default class MImageBody extends React.Component {
// special case to return clientside sender-generated thumbnails for SVGs, if any, // special case to return clientside sender-generated thumbnails for SVGs, if any,
// given we deliberately don't thumbnail them serverside to prevent // given we deliberately don't thumbnail them serverside to prevent
// billion lol attacks and similar // billion lol attacks and similar
return this.context.matrixClient.mxcUrlToHttp( return this.context.mxcUrlToHttp(
content.info.thumbnail_url, content.info.thumbnail_url,
thumbWidth, thumbWidth,
thumbHeight, thumbHeight,
@ -221,7 +219,7 @@ export default class MImageBody extends React.Component {
pixelRatio === 1.0 || pixelRatio === 1.0 ||
(!info || !info.w || !info.h || !info.size) (!info || !info.w || !info.h || !info.size)
) { ) {
return this.context.matrixClient.mxcUrlToHttp(content.url, thumbWidth, thumbHeight); return this.context.mxcUrlToHttp(content.url, thumbWidth, thumbHeight);
} else { } else {
// we should only request thumbnails if the image is bigger than 800x600 // we should only request thumbnails if the image is bigger than 800x600
// (or 1600x1200 on retina) otherwise the image in the timeline will just // (or 1600x1200 on retina) otherwise the image in the timeline will just
@ -242,7 +240,7 @@ export default class MImageBody extends React.Component {
// image is too large physically and bytewise to clutter our timeline so // image is too large physically and bytewise to clutter our timeline so
// we ask for a thumbnail, despite knowing that it will be max 800x600 // we ask for a thumbnail, despite knowing that it will be max 800x600
// despite us being retina (as synapse doesn't do 1600x1200 thumbs yet). // despite us being retina (as synapse doesn't do 1600x1200 thumbs yet).
return this.context.matrixClient.mxcUrlToHttp( return this.context.mxcUrlToHttp(
content.url, content.url,
thumbWidth, thumbWidth,
thumbHeight, thumbHeight,
@ -251,7 +249,7 @@ export default class MImageBody extends React.Component {
// download the original image otherwise, so we can scale it client side // download the original image otherwise, so we can scale it client side
// to take pixelRatio into account. // to take pixelRatio into account.
// ( no width/height means we want the original image) // ( no width/height means we want the original image)
return this.context.matrixClient.mxcUrlToHttp( return this.context.mxcUrlToHttp(
content.url, content.url,
); );
} }
@ -308,7 +306,7 @@ export default class MImageBody extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this.unmounted = true; this.unmounted = true;
this.context.matrixClient.removeListener('sync', this.onClientSync); this.context.removeListener('sync', this.onClientSync);
this._afterComponentWillUnmount(); this._afterComponentWillUnmount();
if (this.state.decryptedUrl) { if (this.state.decryptedUrl) {

View file

@ -25,7 +25,7 @@ import dis from '../../../dispatcher';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu'; import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
import {RoomContext} from "../../structures/RoomView"; import RoomContext from "../../../contexts/RoomContext";
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@ -88,7 +88,7 @@ const ReactButton = ({mxEvent, reactions, onFocusChange}) => {
if (menuDisplayed) { if (menuDisplayed) {
const buttonRect = button.current.getBoundingClientRect(); const buttonRect = button.current.getBoundingClientRect();
const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker'); const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker');
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} catchTab={false}> contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} /> <ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
</ContextMenu>; </ContextMenu>;
} }
@ -117,9 +117,7 @@ export default class MessageActionBar extends React.PureComponent {
onFocusChange: PropTypes.func, onFocusChange: PropTypes.func,
}; };
static contextTypes = { static contextType = RoomContext;
room: RoomContext,
};
componentDidMount() { componentDidMount() {
this.props.mxEvent.on("Event.decrypted", this.onDecrypted); this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
@ -164,12 +162,12 @@ export default class MessageActionBar extends React.PureComponent {
let editButton; let editButton;
if (isContentActionable(this.props.mxEvent)) { if (isContentActionable(this.props.mxEvent)) {
if (this.context.room.canReact) { if (this.context.canReact) {
reactButton = ( reactButton = (
<ReactButton mxEvent={this.props.mxEvent} reactions={this.props.reactions} onFocusChange={this.onFocusChange} /> <ReactButton mxEvent={this.props.mxEvent} reactions={this.props.reactions} onFocusChange={this.onFocusChange} />
); );
} }
if (this.context.room.canReply) { if (this.context.canReply) {
replyButton = <AccessibleButton replyButton = <AccessibleButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton" className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
title={_t("Reply")} title={_t("Reply")}

View file

@ -17,11 +17,11 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import {MatrixClient} from 'matrix-js-sdk';
import Flair from '../elements/Flair.js'; import Flair from '../elements/Flair.js';
import FlairStore from '../../../stores/FlairStore'; import FlairStore from '../../../stores/FlairStore';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {getUserNameColorClass} from '../../../utils/FormattingUtils'; import {getUserNameColorClass} from '../../../utils/FormattingUtils';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
export default createReactClass({ export default createReactClass({
displayName: 'SenderProfile', displayName: 'SenderProfile',
@ -31,8 +31,8 @@ export default createReactClass({
onClick: PropTypes.func, onClick: PropTypes.func,
}, },
contextTypes: { statics: {
matrixClient: PropTypes.instanceOf(MatrixClient), contextType: MatrixClientContext,
}, },
getInitialState() { getInitialState() {
@ -47,18 +47,18 @@ export default createReactClass({
this._updateRelatedGroups(); this._updateRelatedGroups();
FlairStore.getPublicisedGroupsCached( FlairStore.getPublicisedGroupsCached(
this.context.matrixClient, this.props.mxEvent.getSender(), this.context, this.props.mxEvent.getSender(),
).then((userGroups) => { ).then((userGroups) => {
if (this.unmounted) return; if (this.unmounted) return;
this.setState({userGroups}); this.setState({userGroups});
}); });
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents); this.context.on('RoomState.events', this.onRoomStateEvents);
}, },
componentWillUnmount() { componentWillUnmount() {
this.unmounted = true; this.unmounted = true;
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents); this.context.removeListener('RoomState.events', this.onRoomStateEvents);
}, },
onRoomStateEvents(event) { onRoomStateEvents(event) {
@ -71,7 +71,7 @@ export default createReactClass({
_updateRelatedGroups() { _updateRelatedGroups() {
if (this.unmounted) return; if (this.unmounted) return;
const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId()); const room = this.context.getRoom(this.props.mxEvent.getRoomId());
if (!room) return; if (!room) return;
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', ''); const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');

View file

@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useCallback, useMemo, useState, useEffect} from 'react'; import React, {useCallback, useMemo, useState, useEffect, useContext} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import {Group, RoomMember, User} from 'matrix-js-sdk'; import {Group, RoomMember, User} from 'matrix-js-sdk';
@ -37,9 +37,9 @@ import MultiInviter from "../../../utils/MultiInviter";
import GroupStore from "../../../stores/GroupStore"; import GroupStore from "../../../stores/GroupStore";
import MatrixClientPeg from "../../../MatrixClientPeg"; import MatrixClientPeg from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon"; import E2EIcon from "../rooms/E2EIcon";
import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient";
import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {textualPowerLevel} from '../../../Roles'; import {textualPowerLevel} from '../../../Roles';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
const _disambiguateDevices = (devices) => { const _disambiguateDevices = (devices) => {
const names = Object.create(null); const names = Object.create(null);
@ -74,17 +74,6 @@ const _getE2EStatus = (cli, userId, devices) => {
return "warning"; return "warning";
}; };
async function unverifyUser(matrixClient, userId) {
const devices = await matrixClient.getStoredDevicesForUser(userId);
for (const device of devices) {
if (device.isVerified()) {
matrixClient.setDeviceVerified(
userId, device.deviceId, false,
);
}
}
}
function openDMForUser(matrixClient, userId) { function openDMForUser(matrixClient, userId) {
const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
@ -129,17 +118,20 @@ function verifyDevice(userId, device) {
} }
function DeviceItem({userId, device}) { function DeviceItem({userId, device}) {
const cli = useContext(MatrixClientContext);
const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId);
const classes = classNames("mx_UserInfo_device", { const classes = classNames("mx_UserInfo_device", {
mx_UserInfo_device_verified: device.isVerified(), mx_UserInfo_device_verified: deviceTrust.isVerified(),
mx_UserInfo_device_unverified: !device.isVerified(), mx_UserInfo_device_unverified: !deviceTrust.isVerified(),
}); });
const iconClasses = classNames("mx_E2EIcon", { const iconClasses = classNames("mx_E2EIcon", {
mx_E2EIcon_verified: device.isVerified(), mx_E2EIcon_verified: deviceTrust.isVerified(),
mx_E2EIcon_warning: !device.isVerified(), mx_E2EIcon_warning: !deviceTrust.isVerified(),
}); });
const onDeviceClick = () => { const onDeviceClick = () => {
if (!device.isVerified()) { if (!deviceTrust.isVerified()) {
verifyDevice(userId, device); verifyDevice(userId, device);
} }
}; };
@ -147,7 +139,7 @@ function DeviceItem({userId, device}) {
const deviceName = device.ambiguous ? const deviceName = device.ambiguous ?
(device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" : (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" :
device.getDisplayName(); device.getDisplayName();
const trustedLabel = device.isVerified() ? _t("Trusted") : _t("Not trusted"); const trustedLabel = deviceTrust.isVerified() ? _t("Trusted") : _t("Not trusted");
return (<AccessibleButton className={classes} onClick={onDeviceClick}> return (<AccessibleButton className={classes} onClick={onDeviceClick}>
<div className={iconClasses} /> <div className={iconClasses} />
<div className="mx_UserInfo_device_name">{deviceName}</div> <div className="mx_UserInfo_device_name">{deviceName}</div>
@ -157,6 +149,7 @@ function DeviceItem({userId, device}) {
function DevicesSection({devices, userId, loading}) { function DevicesSection({devices, userId, loading}) {
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
const cli = useContext(MatrixClientContext);
const [isExpanded, setExpanded] = useState(false); const [isExpanded, setExpanded] = useState(false);
@ -167,9 +160,21 @@ function DevicesSection({devices, userId, loading}) {
if (devices === null) { if (devices === null) {
return _t("Unable to load device list"); return _t("Unable to load device list");
} }
const deviceTrusts = devices.map(d => cli.checkDeviceTrust(userId, d.deviceId));
const unverifiedDevices = devices.filter(d => !d.isVerified()); const unverifiedDevices = [];
const verifiedDevices = devices.filter(d => d.isVerified()); const verifiedDevices = [];
for (let i = 0; i < devices.length; ++i) {
const device = devices[i];
const deviceTrust = deviceTrusts[i];
if (deviceTrust.isVerified()) {
verifiedDevices.push(device);
} else {
unverifiedDevices.push(device);
}
}
let expandButton; let expandButton;
if (verifiedDevices.length) { if (verifiedDevices.length) {
@ -203,7 +208,9 @@ function DevicesSection({devices, userId, loading}) {
); );
} }
const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => { const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => {
const cli = useContext(MatrixClientContext);
let ignoreButton = null; let ignoreButton = null;
let insertPillButton = null; let insertPillButton = null;
let inviteUserButton = null; let inviteUserButton = null;
@ -313,14 +320,6 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
</AccessibleButton> </AccessibleButton>
); );
} }
let unverifyButton;
if (devices && devices.some(device => device.isVerified())) {
unverifyButton = (
<AccessibleButton onClick={() => unverifyUser(cli, member.userId)} className="mx_UserInfo_field mx_UserInfo_destructive">
{ _t('Unverify user') }
</AccessibleButton>
);
}
return ( return (
<div className="mx_UserInfo_container"> <div className="mx_UserInfo_container">
@ -332,11 +331,10 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
{ insertPillButton } { insertPillButton }
{ inviteUserButton } { inviteUserButton }
{ ignoreButton } { ignoreButton }
{ unverifyButton }
</div> </div>
</div> </div>
); );
}); };
const _warnSelfDemote = async () => { const _warnSelfDemote = async () => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
@ -404,7 +402,12 @@ const useRoomPowerLevels = (cli, room) => {
return powerLevels; return powerLevels;
}; };
const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, startUpdating, stopUpdating}) => { const RoomKickButton = ({member, startUpdating, stopUpdating}) => {
const cli = useContext(MatrixClientContext);
// check if user can be kicked/disinvited
if (member.membership !== "invite" && member.membership !== "join") return null;
const onKick = async () => { const onKick = async () => {
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
const {finished} = Modal.createTrackedDialog( const {finished} = Modal.createTrackedDialog(
@ -444,9 +447,11 @@ const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, start
return <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onKick}> return <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onKick}>
{ kickLabel } { kickLabel }
</AccessibleButton>; </AccessibleButton>;
}); };
const RedactMessagesButton = ({member}) => {
const cli = useContext(MatrixClientContext);
const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member}) => {
const onRedactAllMessages = async () => { const onRedactAllMessages = async () => {
const {roomId, userId} = member; const {roomId, userId} = member;
const room = cli.getRoom(roomId); const room = cli.getRoom(roomId);
@ -517,9 +522,11 @@ const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member}
return <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onRedactAllMessages}> return <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onRedactAllMessages}>
{ _t("Remove recent messages") } { _t("Remove recent messages") }
</AccessibleButton>; </AccessibleButton>;
}); };
const BanToggleButton = ({member, startUpdating, stopUpdating}) => {
const cli = useContext(MatrixClientContext);
const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, startUpdating, stopUpdating}) => {
const onBanOrUnban = async () => { const onBanOrUnban = async () => {
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
const {finished} = Modal.createTrackedDialog( const {finished} = Modal.createTrackedDialog(
@ -573,207 +580,209 @@ const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, star
return <AccessibleButton className={classes} onClick={onBanOrUnban}> return <AccessibleButton className={classes} onClick={onBanOrUnban}>
{ label } { label }
</AccessibleButton>; </AccessibleButton>;
}); };
const MuteToggleButton = withLegacyMatrixClient( const MuteToggleButton = ({member, room, powerLevels, startUpdating, stopUpdating}) => {
({matrixClient: cli, member, room, powerLevels, startUpdating, stopUpdating}) => { const cli = useContext(MatrixClientContext);
const isMuted = _isMuted(member, powerLevels);
const onMuteToggle = async () => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomId = member.roomId;
const target = member.userId;
// if muting self, warn as it may be irreversible // Don't show the mute/unmute option if the user is not in the room
if (target === cli.getUserId()) { if (member.membership !== "join") return null;
try {
if (!(await _warnSelfDemote())) return; const isMuted = _isMuted(member, powerLevels);
} catch (e) { const onMuteToggle = async () => {
console.error("Failed to warn about self demotion: ", e); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
return; const roomId = member.roomId;
} const target = member.userId;
// if muting self, warn as it may be irreversible
if (target === cli.getUserId()) {
try {
if (!(await _warnSelfDemote())) return;
} catch (e) {
console.error("Failed to warn about self demotion: ", e);
return;
} }
}
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return; if (!powerLevelEvent) return;
const powerLevels = powerLevelEvent.getContent(); const powerLevels = powerLevelEvent.getContent();
const levelToSend = ( const levelToSend = (
(powerLevels.events ? powerLevels.events["m.room.message"] : null) || (powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
powerLevels.events_default powerLevels.events_default
); );
let level; let level;
if (isMuted) { // unmute if (isMuted) { // unmute
level = levelToSend; level = levelToSend;
} else { // mute } else { // mute
level = levelToSend - 1; level = levelToSend - 1;
} }
level = parseInt(level); level = parseInt(level);
if (!isNaN(level)) { if (!isNaN(level)) {
startUpdating(); startUpdating();
cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => { cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Mute toggle success"); console.log("Mute toggle success");
}, function(err) { }, function(err) {
console.error("Mute error: " + err); console.error("Mute error: " + err);
Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, { Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, {
title: _t("Error"), title: _t("Error"),
description: _t("Failed to mute user"), description: _t("Failed to mute user"),
});
}).finally(() => {
stopUpdating();
}); });
} }).finally(() => {
stopUpdating();
});
}
};
const classes = classNames("mx_UserInfo_field", {
mx_UserInfo_destructive: !isMuted,
});
const muteLabel = isMuted ? _t("Unmute") : _t("Mute");
return <AccessibleButton className={classes} onClick={onMuteToggle}>
{ muteLabel }
</AccessibleButton>;
};
const RoomAdminToolsContainer = ({room, children, member, startUpdating, stopUpdating, powerLevels}) => {
const cli = useContext(MatrixClientContext);
let kickButton;
let banButton;
let muteButton;
let redactButton;
const editPowerLevel = (
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
powerLevels.state_default
);
const me = room.getMember(cli.getUserId());
const isMe = me.userId === member.userId;
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
if (canAffectUser && me.powerLevel >= powerLevels.kick) {
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
}
if (me.powerLevel >= powerLevels.redact) {
redactButton = (
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
);
}
if (canAffectUser && me.powerLevel >= powerLevels.ban) {
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
}
if (canAffectUser && me.powerLevel >= editPowerLevel) {
muteButton = (
<MuteToggleButton
member={member}
room={room}
powerLevels={powerLevels}
startUpdating={startUpdating}
stopUpdating={stopUpdating}
/>
);
}
if (kickButton || banButton || muteButton || redactButton || children) {
return <GenericAdminToolsContainer>
{ muteButton }
{ kickButton }
{ banButton }
{ redactButton }
{ children }
</GenericAdminToolsContainer>;
}
return <div />;
};
const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating, stopUpdating}) => {
const cli = useContext(MatrixClientContext);
const [isPrivileged, setIsPrivileged] = useState(false);
const [isInvited, setIsInvited] = useState(false);
// Listen to group store changes
useEffect(() => {
let unmounted = false;
const onGroupStoreUpdated = () => {
if (unmounted) return;
setIsPrivileged(GroupStore.isUserPrivileged(groupId));
setIsInvited(GroupStore.getGroupInvitedMembers(groupId).some(
(m) => m.userId === groupMember.userId,
));
}; };
const classes = classNames("mx_UserInfo_field", { GroupStore.registerListener(groupId, onGroupStoreUpdated);
mx_UserInfo_destructive: !isMuted, onGroupStoreUpdated();
}); // Handle unmount
return () => {
unmounted = true;
GroupStore.unregisterListener(onGroupStoreUpdated);
};
}, [groupId, groupMember.userId]);
const muteLabel = isMuted ? _t("Unmute") : _t("Mute"); if (isPrivileged) {
return <AccessibleButton className={classes} onClick={onMuteToggle}> const _onKick = async () => {
{ muteLabel } const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
</AccessibleButton>; const {finished} = Modal.createDialog(ConfirmUserActionDialog, {
}, matrixClient: cli,
); groupMember,
action: isInvited ? _t('Disinvite') : _t('Remove from community'),
title: isInvited ? _t('Disinvite this user from community?')
: _t('Remove this user from community?'),
danger: true,
});
const RoomAdminToolsContainer = withLegacyMatrixClient( const [proceed] = await finished;
({matrixClient: cli, room, children, member, startUpdating, stopUpdating, powerLevels}) => { if (!proceed) return;
let kickButton;
let banButton;
let muteButton;
let redactButton;
const editPowerLevel = ( startUpdating();
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || cli.removeUserFromGroup(groupId, groupMember.userId).then(() => {
powerLevels.state_default // return to the user list
dis.dispatch({
action: "view_user",
member: null,
});
}).catch((e) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
title: _t('Error'),
description: isInvited ?
_t('Failed to withdraw invitation') :
_t('Failed to remove user from community'),
});
console.log(e);
}).finally(() => {
stopUpdating();
});
};
const kickButton = (
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={_onKick}>
{ isInvited ? _t('Disinvite') : _t('Remove from community') }
</AccessibleButton>
); );
const me = room.getMember(cli.getUserId()); // No make/revoke admin API yet
const isMe = me.userId === member.userId; /*const opLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
const canAffectUser = member.powerLevel < me.powerLevel || isMe; giveModButton = <AccessibleButton className="mx_UserInfo_field" onClick={this.onModToggle}>
{giveOpLabel}
</AccessibleButton>;*/
if (canAffectUser && me.powerLevel >= powerLevels.kick) { return <GenericAdminToolsContainer>
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />; { kickButton }
} { children }
if (me.powerLevel >= powerLevels.redact) { </GenericAdminToolsContainer>;
redactButton = ( }
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
);
}
if (canAffectUser && me.powerLevel >= powerLevels.ban) {
banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
}
if (canAffectUser && me.powerLevel >= editPowerLevel) {
muteButton = (
<MuteToggleButton
member={member}
room={room}
powerLevels={powerLevels}
startUpdating={startUpdating}
stopUpdating={stopUpdating}
/>
);
}
if (kickButton || banButton || muteButton || redactButton || children) { return <div />;
return <GenericAdminToolsContainer> };
{ muteButton }
{ kickButton }
{ banButton }
{ redactButton }
{ children }
</GenericAdminToolsContainer>;
}
return <div />;
},
);
const GroupAdminToolsSection = withLegacyMatrixClient(
({matrixClient: cli, children, groupId, groupMember, startUpdating, stopUpdating}) => {
const [isPrivileged, setIsPrivileged] = useState(false);
const [isInvited, setIsInvited] = useState(false);
// Listen to group store changes
useEffect(() => {
let unmounted = false;
const onGroupStoreUpdated = () => {
if (unmounted) return;
setIsPrivileged(GroupStore.isUserPrivileged(groupId));
setIsInvited(GroupStore.getGroupInvitedMembers(groupId).some(
(m) => m.userId === groupMember.userId,
));
};
GroupStore.registerListener(groupId, onGroupStoreUpdated);
onGroupStoreUpdated();
// Handle unmount
return () => {
unmounted = true;
GroupStore.unregisterListener(onGroupStoreUpdated);
};
}, [groupId, groupMember.userId]);
if (isPrivileged) {
const _onKick = async () => {
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
const {finished} = Modal.createDialog(ConfirmUserActionDialog, {
matrixClient: cli,
groupMember,
action: isInvited ? _t('Disinvite') : _t('Remove from community'),
title: isInvited ? _t('Disinvite this user from community?')
: _t('Remove this user from community?'),
danger: true,
});
const [proceed] = await finished;
if (!proceed) return;
startUpdating();
cli.removeUserFromGroup(groupId, groupMember.userId).then(() => {
// return to the user list
dis.dispatch({
action: "view_user",
member: null,
});
}).catch((e) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
title: _t('Error'),
description: isInvited ?
_t('Failed to withdraw invitation') :
_t('Failed to remove user from community'),
});
console.log(e);
}).finally(() => {
stopUpdating();
});
};
const kickButton = (
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={_onKick}>
{ isInvited ? _t('Disinvite') : _t('Remove from community') }
</AccessibleButton>
);
// No make/revoke admin API yet
/*const opLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
giveModButton = <AccessibleButton className="mx_UserInfo_field" onClick={this.onModToggle}>
{giveOpLabel}
</AccessibleButton>;*/
return <GenericAdminToolsContainer>
{ kickButton }
{ children }
</GenericAdminToolsContainer>;
}
return <div />;
},
);
const GroupMember = PropTypes.shape({ const GroupMember = PropTypes.shape({
userId: PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
@ -849,7 +858,7 @@ function useRoomPermissions(cli, room, user) {
return roomPermissions; return roomPermissions;
} }
const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => { const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => {
const [isEditing, setEditing] = useState(false); const [isEditing, setEditing] = useState(false);
if (room && user.roomId) { // is in room if (room && user.roomId) { // is in room
if (isEditing) { if (isEditing) {
@ -876,9 +885,11 @@ const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room
} else { } else {
return null; return null;
} }
}); };
const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
const cli = useContext(MatrixClientContext);
const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, onFinished}) => {
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10)); const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10));
const [isDirty, setIsDirty] = useState(false); const [isDirty, setIsDirty] = useState(false);
@ -953,7 +964,7 @@ const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room,
}); });
const [confirmed] = await finished; const [confirmed] = await finished;
if (confirmed) return; if (!confirmed) return;
} }
await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
} finally { } finally {
@ -982,10 +993,11 @@ const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room,
{buttonOrSpinner} {buttonOrSpinner}
</div> </div>
); );
}); };
const UserInfo = ({user, groupId, roomId, onClose}) => {
const cli = useContext(MatrixClientContext);
// cli is injected by withLegacyMatrixClient
const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => {
// Load room if we are given a room id and memoize it // Load room if we are given a room id and memoize it
const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]); const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]);
@ -1260,11 +1272,20 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
const devicesSection = isRoomEncrypted ? const devicesSection = isRoomEncrypted ?
(<DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />) : null; (<DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />) : null;
const userVerified = cli.checkUserTrust(user.userId).isVerified();
let verifyButton;
if (!userVerified) {
verifyButton = <AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyDevice(user.userId, null)}>
{_t("Verify")}
</AccessibleButton>;
}
const securitySection = ( const securitySection = (
<div className="mx_UserInfo_container"> <div className="mx_UserInfo_container">
<h3>{ _t("Security") }</h3> <h3>{ _t("Security") }</h3>
<p>{ text }</p> <p>{ text }</p>
<AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyDevice(user.userId, null)}>{_t("Verify")}</AccessibleButton> {verifyButton}
{ devicesSection } { devicesSection }
</div> </div>
); );
@ -1316,7 +1337,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
</AutoHideScrollbar> </AutoHideScrollbar>
</div> </div>
); );
}); };
UserInfo.propTypes = { UserInfo.propTypes = {
user: PropTypes.oneOfType([ user: PropTypes.oneOfType([

View file

@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import EditableItemList from "../elements/EditableItemList";
const React = require('react'); const React = require('react');
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const MatrixClientPeg = require('../../../MatrixClientPeg'); const MatrixClientPeg = require('../../../MatrixClientPeg');
@ -22,8 +24,33 @@ const sdk = require("../../../index");
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Field from "../elements/Field"; import Field from "../elements/Field";
import ErrorDialog from "../dialogs/ErrorDialog"; import ErrorDialog from "../dialogs/ErrorDialog";
import AccessibleButton from "../elements/AccessibleButton";
const Modal = require("../../../Modal"); const Modal = require("../../../Modal");
class EditableAliasesList extends EditableItemList {
_renderNewItemField() {
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
const onChange = (alias) => this._onNewItemChanged({target: {value: alias}});
return (
<form
onSubmit={this._onItemAdded}
autoComplete="off"
noValidate={true}
className="mx_EditableItemList_newItem"
>
<RoomAliasField
id={`mx_EditableItemList_new_${this.props.id}`}
onChange={onChange}
value={this.props.newItem || ""}
domain={this.props.domain} />
<AccessibleButton onClick={this._onItemAdded} kind="primary">
{ _t("Add") }
</AccessibleButton>
</form>
);
}
}
export default class AliasSettings extends React.Component { export default class AliasSettings extends React.Component {
static propTypes = { static propTypes = {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
@ -47,7 +74,6 @@ export default class AliasSettings extends React.Component {
remoteDomains: [], // [ domain.com, foobar.com ] remoteDomains: [], // [ domain.com, foobar.com ]
canonicalAlias: null, // #canonical:domain.com canonicalAlias: null, // #canonical:domain.com
updatingCanonicalAlias: false, updatingCanonicalAlias: false,
newItem: "",
}; };
const localDomain = MatrixClientPeg.get().getDomain(); const localDomain = MatrixClientPeg.get().getDomain();
@ -181,7 +207,6 @@ export default class AliasSettings extends React.Component {
}; };
render() { render() {
const EditableItemList = sdk.getComponent("elements.EditableItemList");
const localDomain = MatrixClientPeg.get().getDomain(); const localDomain = MatrixClientPeg.get().getDomain();
let found = false; let found = false;
@ -233,7 +258,7 @@ export default class AliasSettings extends React.Component {
return ( return (
<div className='mx_AliasSettings'> <div className='mx_AliasSettings'>
{canonicalAliasSection} {canonicalAliasSection}
<EditableItemList <EditableAliasesList
id="roomAliases" id="roomAliases"
className={"mx_RoomSettings_localAliases"} className={"mx_RoomSettings_localAliases"}
items={this.state.domainToAliases[localDomain] || []} items={this.state.domainToAliases[localDomain] || []}
@ -248,6 +273,7 @@ export default class AliasSettings extends React.Component {
placeholder={_t( placeholder={_t(
'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain}, 'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
)} )}
domain={localDomain}
/> />
{remoteAliasesSection} {remoteAliasesSection}
</div> </div>

View file

@ -16,11 +16,12 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk'; import {MatrixEvent} from 'matrix-js-sdk';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import ErrorDialog from "../dialogs/ErrorDialog"; import ErrorDialog from "../dialogs/ErrorDialog";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
const GROUP_ID_REGEX = /\+\S+:\S+/; const GROUP_ID_REGEX = /\+\S+:\S+/;
@ -31,9 +32,7 @@ export default class RelatedGroupSettings extends React.Component {
relatedGroupsEvent: PropTypes.instanceOf(MatrixEvent), relatedGroupsEvent: PropTypes.instanceOf(MatrixEvent),
}; };
static contextTypes = { static contextType = MatrixClientContext;
matrixClient: PropTypes.instanceOf(MatrixClient),
};
static defaultProps = { static defaultProps = {
canSetRelatedGroups: false, canSetRelatedGroups: false,
@ -49,7 +48,7 @@ export default class RelatedGroupSettings extends React.Component {
} }
updateGroups(newGroupsList) { updateGroups(newGroupsList) {
this.context.matrixClient.sendStateEvent(this.props.roomId, 'm.room.related_groups', { this.context.sendStateEvent(this.props.roomId, 'm.room.related_groups', {
groups: newGroupsList, groups: newGroupsList,
}, '').catch((err) => { }, '').catch((err) => {
console.error(err); console.error(err);
@ -99,7 +98,7 @@ export default class RelatedGroupSettings extends React.Component {
}; };
render() { render() {
const localDomain = this.context.matrixClient.getDomain(); const localDomain = this.context.getDomain();
const EditableItemList = sdk.getComponent('elements.EditableItemList'); const EditableItemList = sdk.getComponent('elements.EditableItemList');
return <div> return <div>
<EditableItemList <EditableItemList

View file

@ -34,11 +34,11 @@ import {parsePlainTextMessage} from '../../../editor/deserialize';
import {renderModel} from '../../../editor/render'; import {renderModel} from '../../../editor/render';
import {Room} from 'matrix-js-sdk'; import {Room} from 'matrix-js-sdk';
import TypingStore from "../../../stores/TypingStore"; import TypingStore from "../../../stores/TypingStore";
import EMOJIBASE from 'emojibase-data/en/compact.json';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import sdk from '../../../index'; import sdk from '../../../index';
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
import {EMOTICON_TO_EMOJI} from "../../../emoji";
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
@ -80,8 +80,8 @@ export default class BasicMessageEditor extends React.Component {
initialCaret: PropTypes.object, // See DocumentPosition in editor/model.js initialCaret: PropTypes.object, // See DocumentPosition in editor/model.js
}; };
constructor(props, context) { constructor(props) {
super(props, context); super(props);
this.state = { this.state = {
autoComplete: null, autoComplete: null,
}; };
@ -108,7 +108,8 @@ export default class BasicMessageEditor extends React.Component {
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text); const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
if (emoticonMatch) { if (emoticonMatch) {
const query = emoticonMatch[1].toLowerCase().replace("-", ""); const query = emoticonMatch[1].toLowerCase().replace("-", "");
const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false); const data = EMOTICON_TO_EMOJI.get(query);
if (data) { if (data) {
const {partCreator} = model; const {partCreator} = model;
const hasPrecedingSpace = emoticonMatch[0][0] === " "; const hasPrecedingSpace = emoticonMatch[0][0] === " ";

View file

@ -26,11 +26,11 @@ import {findEditableEvent} from '../../../utils/EventUtils';
import {parseEvent} from '../../../editor/deserialize'; import {parseEvent} from '../../../editor/deserialize';
import {PartCreator} from '../../../editor/parts'; import {PartCreator} from '../../../editor/parts';
import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import EditorStateTransfer from '../../../utils/EditorStateTransfer';
import {MatrixClient} from 'matrix-js-sdk';
import classNames from 'classnames'; import classNames from 'classnames';
import {EventStatus} from 'matrix-js-sdk'; import {EventStatus} from 'matrix-js-sdk';
import BasicMessageComposer from "./BasicMessageComposer"; import BasicMessageComposer from "./BasicMessageComposer";
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
function _isReply(mxEvent) { function _isReply(mxEvent) {
const relatesTo = mxEvent.getContent()["m.relates_to"]; const relatesTo = mxEvent.getContent()["m.relates_to"];
@ -105,12 +105,10 @@ export default class EditMessageComposer extends React.Component {
editState: PropTypes.instanceOf(EditorStateTransfer).isRequired, editState: PropTypes.instanceOf(EditorStateTransfer).isRequired,
}; };
static contextTypes = { static contextType = MatrixClientContext;
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
};
constructor(props, context) { constructor(props) {
super(props, context); super(props);
this.model = null; this.model = null;
this._editorRef = null; this._editorRef = null;
@ -124,7 +122,7 @@ export default class EditMessageComposer extends React.Component {
}; };
_getRoom() { _getRoom() {
return this.context.matrixClient.getRoom(this.props.editState.getEvent().getRoomId()); return this.context.getRoom(this.props.editState.getEvent().getRoomId());
} }
_onKeyDown = (event) => { _onKeyDown = (event) => {
@ -190,7 +188,7 @@ export default class EditMessageComposer extends React.Component {
if (this._isContentModified(newContent)) { if (this._isContentModified(newContent)) {
const roomId = editedEvent.getRoomId(); const roomId = editedEvent.getRoomId();
this._cancelPreviousPendingEdit(); this._cancelPreviousPendingEdit();
this.context.matrixClient.sendMessage(roomId, editContent); this.context.sendMessage(roomId, editContent);
} }
// close the event editing and focus composer // close the event editing and focus composer
@ -205,7 +203,7 @@ export default class EditMessageComposer extends React.Component {
previousEdit.status === EventStatus.QUEUED || previousEdit.status === EventStatus.QUEUED ||
previousEdit.status === EventStatus.NOT_SENT previousEdit.status === EventStatus.NOT_SENT
)) { )) {
this.context.matrixClient.cancelPendingEvent(previousEdit); this.context.cancelPendingEvent(previousEdit);
} }
} }
@ -232,7 +230,7 @@ export default class EditMessageComposer extends React.Component {
_createEditorModel() { _createEditorModel() {
const {editState} = this.props; const {editState} = this.props;
const room = this._getRoom(); const room = this._getRoom();
const partCreator = new PartCreator(room, this.context.matrixClient); const partCreator = new PartCreator(room, this.context);
let parts; let parts;
if (editState.hasEditorState()) { if (editState.hasEditorState()) {
// if restoring state from a previous editor, // if restoring state from a previous editor,

View file

@ -1,8 +1,8 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd Copyright 2017 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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.
@ -24,17 +24,17 @@ import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
const classNames = require("classnames"); const classNames = require("classnames");
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
const Modal = require('../../../Modal');
const sdk = require('../../../index'); const sdk = require('../../../index');
const TextForEvent = require('../../../TextForEvent'); const TextForEvent = require('../../../TextForEvent');
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {EventStatus, MatrixClient} from 'matrix-js-sdk'; import {EventStatus} from 'matrix-js-sdk';
import {formatTime} from "../../../DateUtils"; import {formatTime} from "../../../DateUtils";
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList"; import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
const ObjectUtils = require('../../../ObjectUtils'); const ObjectUtils = require('../../../ObjectUtils');
@ -222,8 +222,8 @@ module.exports = createReactClass({
}; };
}, },
contextTypes: { statics: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, contextType: MatrixClientContext,
}, },
componentWillMount: function() { componentWillMount: function() {
@ -237,7 +237,7 @@ module.exports = createReactClass({
componentDidMount: function() { componentDidMount: function() {
this._suppressReadReceiptAnimation = false; this._suppressReadReceiptAnimation = false;
const client = this.context.matrixClient; const client = this.context;
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged); client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.props.mxEvent.on("Event.decrypted", this._onDecrypted); this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
if (this.props.showReactions) { if (this.props.showReactions) {
@ -262,7 +262,7 @@ module.exports = createReactClass({
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
const client = this.context.matrixClient; const client = this.context;
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted); this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
if (this.props.showReactions) { if (this.props.showReactions) {
@ -291,7 +291,7 @@ module.exports = createReactClass({
return; return;
} }
const verified = await this.context.matrixClient.isEventSenderVerified(mxEvent); const verified = await this.context.isEventSenderVerified(mxEvent);
this.setState({ this.setState({
verified: verified, verified: verified,
}, () => { }, () => {
@ -349,11 +349,11 @@ module.exports = createReactClass({
}, },
shouldHighlight: function() { shouldHighlight: function() {
const actions = this.context.matrixClient.getPushActionsForEvent(this.props.mxEvent); const actions = this.context.getPushActionsForEvent(this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; } if (!actions || !actions.tweaks) { return false; }
// don't show self-highlights from another of our clients // don't show self-highlights from another of our clients
if (this.props.mxEvent.getSender() === this.context.matrixClient.credentials.userId) { if (this.props.mxEvent.getSender() === this.context.credentials.userId) {
return false; return false;
} }
@ -442,15 +442,6 @@ module.exports = createReactClass({
}); });
}, },
onCryptoClick: function(e) {
const event = this.props.mxEvent;
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
{event},
);
},
onRequestKeysClick: function() { onRequestKeysClick: function() {
this.setState({ this.setState({
// Indicate in the UI that the keys have been requested (this is expected to // Indicate in the UI that the keys have been requested (this is expected to
@ -461,7 +452,7 @@ module.exports = createReactClass({
// Cancel any outgoing key request for this event and resend it. If a response // Cancel any outgoing key request for this event and resend it. If a response
// is received for the request with the required keys, the event could be // is received for the request with the required keys, the event could be
// decrypted successfully. // decrypted successfully.
this.context.matrixClient.cancelAndResendEventRoomKeyRequest(this.props.mxEvent); this.context.cancelAndResendEventRoomKeyRequest(this.props.mxEvent);
}, },
onPermalinkClicked: function(e) { onPermalinkClicked: function(e) {
@ -478,11 +469,10 @@ module.exports = createReactClass({
_renderE2EPadlock: function() { _renderE2EPadlock: function() {
const ev = this.props.mxEvent; const ev = this.props.mxEvent;
const props = {onClick: this.onCryptoClick};
// event could not be decrypted // event could not be decrypted
if (ev.getContent().msgtype === 'm.bad.encrypted') { if (ev.getContent().msgtype === 'm.bad.encrypted') {
return <E2ePadlockUndecryptable {...props} />; return <E2ePadlockUndecryptable />;
} }
// event is encrypted, display padlock corresponding to whether or not it is verified // event is encrypted, display padlock corresponding to whether or not it is verified
@ -490,11 +480,11 @@ module.exports = createReactClass({
if (this.state.verified) { if (this.state.verified) {
return; // no icon for verified return; // no icon for verified
} else { } else {
return (<E2ePadlockUnverified {...props} />); return (<E2ePadlockUnverified />);
} }
} }
if (this.context.matrixClient.isRoomEncrypted(ev.getRoomId())) { if (this.context.isRoomEncrypted(ev.getRoomId())) {
// else if room is encrypted // else if room is encrypted
// and event is being encrypted or is not_sent (Unknown Devices/Network Error) // and event is being encrypted or is not_sent (Unknown Devices/Network Error)
if (ev.status === EventStatus.ENCRYPTING) { if (ev.status === EventStatus.ENCRYPTING) {
@ -507,7 +497,7 @@ module.exports = createReactClass({
return; // we expect this to be unencrypted return; // we expect this to be unencrypted
} }
// if the event is not encrypted, but it's an e2e room, show the open padlock // if the event is not encrypted, but it's an e2e room, show the open padlock
return <E2ePadlockUnencrypted {...props} />; return <E2ePadlockUnencrypted />;
} }
// no padlock needed // no padlock needed
@ -741,7 +731,7 @@ module.exports = createReactClass({
switch (this.props.tileShape) { switch (this.props.tileShape) {
case 'notif': { case 'notif': {
const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId()); const room = this.context.getRoom(this.props.mxEvent.getRoomId());
return ( return (
<div className={classes}> <div className={classes}>
<div className="mx_EventTile_roomName"> <div className="mx_EventTile_roomName">
@ -919,7 +909,6 @@ class E2ePadlock extends React.Component {
static propTypes = { static propTypes = {
icon: PropTypes.string.isRequired, icon: PropTypes.string.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
onClick: PropTypes.func,
}; };
constructor() { constructor() {
@ -930,10 +919,6 @@ class E2ePadlock extends React.Component {
}; };
} }
onClick = (e) => {
if (this.props.onClick) this.props.onClick(e);
};
onHoverStart = () => { onHoverStart = () => {
this.setState({hover: true}); this.setState({hover: true});
}; };

View file

@ -23,6 +23,8 @@ import classNames from 'classnames';
export default class MemberDeviceInfo extends React.Component { export default class MemberDeviceInfo extends React.Component {
render() { render() {
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons'); const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
// XXX: These checks are not cross-signing aware but this component is only used
// from the old, pre-cross-signing memberinfopanel
const iconClasses = classNames({ const iconClasses = classNames({
mx_MemberDeviceInfo_icon: true, mx_MemberDeviceInfo_icon: true,
mx_MemberDeviceInfo_icon_blacklisted: this.props.device.isBlocked(), mx_MemberDeviceInfo_icon_blacklisted: this.props.device.isBlocked(),

View file

@ -31,7 +31,6 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import classNames from 'classnames'; import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import sdk from '../../../index'; import sdk from '../../../index';
@ -48,7 +47,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import E2EIcon from "./E2EIcon"; import E2EIcon from "./E2EIcon";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import MatrixClientPeg from "../../../MatrixClientPeg"; import MatrixClientPeg from "../../../MatrixClientPeg";
import {EventTimeline} from "matrix-js-sdk"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
module.exports = createReactClass({ module.exports = createReactClass({
displayName: 'MemberInfo', displayName: 'MemberInfo',
@ -76,13 +75,13 @@ module.exports = createReactClass({
}; };
}, },
contextTypes: { statics: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, contextType: MatrixClientContext,
}, },
componentWillMount: function() { componentWillMount: function() {
this._cancelDeviceList = null; this._cancelDeviceList = null;
const cli = this.context.matrixClient; const cli = this.context;
// only display the devices list if our client supports E2E // only display the devices list if our client supports E2E
this._enableDevices = cli.isCryptoEnabled(); this._enableDevices = cli.isCryptoEnabled();
@ -112,7 +111,7 @@ module.exports = createReactClass({
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
const client = this.context.matrixClient; const client = this.context;
if (client) { if (client) {
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
client.removeListener("Room", this.onRoom); client.removeListener("Room", this.onRoom);
@ -131,7 +130,7 @@ module.exports = createReactClass({
}, },
_checkIgnoreState: function() { _checkIgnoreState: function() {
const isIgnoring = this.context.matrixClient.isUserIgnored(this.props.member.userId); const isIgnoring = this.context.isUserIgnored(this.props.member.userId);
this.setState({isIgnoring: isIgnoring}); this.setState({isIgnoring: isIgnoring});
}, },
@ -163,7 +162,7 @@ module.exports = createReactClass({
// Promise.resolve to handle transition from static result to promise; can be removed // Promise.resolve to handle transition from static result to promise; can be removed
// in future // in future
Promise.resolve(this.context.matrixClient.getStoredDevicesForUser(userId)).then((devices) => { Promise.resolve(this.context.getStoredDevicesForUser(userId)).then((devices) => {
this.setState({ this.setState({
devices: devices, devices: devices,
e2eStatus: this._getE2EStatus(devices), e2eStatus: this._getE2EStatus(devices),
@ -197,7 +196,7 @@ module.exports = createReactClass({
onRoomReceipt: function(receiptEvent, room) { onRoomReceipt: function(receiptEvent, room) {
// because if we read a notification, it will affect notification count // because if we read a notification, it will affect notification count
// only bother updating if there's a receipt from us // only bother updating if there's a receipt from us
if (findReadReceiptFromUserId(receiptEvent, this.context.matrixClient.credentials.userId)) { if (findReadReceiptFromUserId(receiptEvent, this.context.credentials.userId)) {
this.forceUpdate(); this.forceUpdate();
} }
}, },
@ -242,7 +241,7 @@ module.exports = createReactClass({
let cancelled = false; let cancelled = false;
this._cancelDeviceList = function() { cancelled = true; }; this._cancelDeviceList = function() { cancelled = true; };
const client = this.context.matrixClient; const client = this.context;
const self = this; const self = this;
client.downloadKeys([member.userId], true).then(() => { client.downloadKeys([member.userId], true).then(() => {
return client.getStoredDevicesForUser(member.userId); return client.getStoredDevicesForUser(member.userId);
@ -267,7 +266,7 @@ module.exports = createReactClass({
}, },
onIgnoreToggle: function() { onIgnoreToggle: function() {
const ignoredUsers = this.context.matrixClient.getIgnoredUsers(); const ignoredUsers = this.context.getIgnoredUsers();
if (this.state.isIgnoring) { if (this.state.isIgnoring) {
const index = ignoredUsers.indexOf(this.props.member.userId); const index = ignoredUsers.indexOf(this.props.member.userId);
if (index !== -1) ignoredUsers.splice(index, 1); if (index !== -1) ignoredUsers.splice(index, 1);
@ -275,7 +274,7 @@ module.exports = createReactClass({
ignoredUsers.push(this.props.member.userId); ignoredUsers.push(this.props.member.userId);
} }
this.context.matrixClient.setIgnoredUsers(ignoredUsers).then(() => { this.context.setIgnoredUsers(ignoredUsers).then(() => {
return this.setState({isIgnoring: !this.state.isIgnoring}); return this.setState({isIgnoring: !this.state.isIgnoring});
}); });
}, },
@ -293,7 +292,7 @@ module.exports = createReactClass({
if (!proceed) return; if (!proceed) return;
this.setState({ updating: this.state.updating + 1 }); this.setState({ updating: this.state.updating + 1 });
this.context.matrixClient.kick( this.context.kick(
this.props.member.roomId, this.props.member.userId, this.props.member.roomId, this.props.member.userId,
reason || undefined, reason || undefined,
).then(function() { ).then(function() {
@ -329,11 +328,11 @@ module.exports = createReactClass({
this.setState({ updating: this.state.updating + 1 }); this.setState({ updating: this.state.updating + 1 });
let promise; let promise;
if (this.props.member.membership === 'ban') { if (this.props.member.membership === 'ban') {
promise = this.context.matrixClient.unban( promise = this.context.unban(
this.props.member.roomId, this.props.member.userId, this.props.member.roomId, this.props.member.userId,
); );
} else { } else {
promise = this.context.matrixClient.ban( promise = this.context.ban(
this.props.member.roomId, this.props.member.userId, this.props.member.roomId, this.props.member.userId,
reason || undefined, reason || undefined,
); );
@ -360,7 +359,7 @@ module.exports = createReactClass({
onRedactAllMessages: async function() { onRedactAllMessages: async function() {
const {roomId, userId} = this.props.member; const {roomId, userId} = this.props.member;
const room = this.context.matrixClient.getRoom(roomId); const room = this.context.getRoom(roomId);
if (!room) { if (!room) {
return; return;
} }
@ -414,7 +413,7 @@ module.exports = createReactClass({
console.info(`Started redacting recent ${count} messages for ${user} in ${roomId}`); console.info(`Started redacting recent ${count} messages for ${user} in ${roomId}`);
await Promise.all(eventsToRedact.map(async event => { await Promise.all(eventsToRedact.map(async event => {
try { try {
await this.context.matrixClient.redactEvent(roomId, event.getId()); await this.context.redactEvent(roomId, event.getId());
} catch (err) { } catch (err) {
// log and swallow errors // log and swallow errors
console.error("Could not redact", event.getId()); console.error("Could not redact", event.getId());
@ -446,11 +445,11 @@ module.exports = createReactClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomId = this.props.member.roomId; const roomId = this.props.member.roomId;
const target = this.props.member.userId; const target = this.props.member.userId;
const room = this.context.matrixClient.getRoom(roomId); const room = this.context.getRoom(roomId);
if (!room) return; if (!room) return;
// if muting self, warn as it may be irreversible // if muting self, warn as it may be irreversible
if (target === this.context.matrixClient.getUserId()) { if (target === this.context.getUserId()) {
try { try {
if (!(await this._warnSelfDemote())) return; if (!(await this._warnSelfDemote())) return;
} catch (e) { } catch (e) {
@ -478,7 +477,7 @@ module.exports = createReactClass({
if (!isNaN(level)) { if (!isNaN(level)) {
this.setState({ updating: this.state.updating + 1 }); this.setState({ updating: this.state.updating + 1 });
this.context.matrixClient.setPowerLevel(roomId, target, level, powerLevelEvent).then( this.context.setPowerLevel(roomId, target, level, powerLevelEvent).then(
function() { function() {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
@ -500,13 +499,13 @@ module.exports = createReactClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomId = this.props.member.roomId; const roomId = this.props.member.roomId;
const target = this.props.member.userId; const target = this.props.member.userId;
const room = this.context.matrixClient.getRoom(roomId); const room = this.context.getRoom(roomId);
if (!room) return; if (!room) return;
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return; if (!powerLevelEvent) return;
const me = room.getMember(this.context.matrixClient.credentials.userId); const me = room.getMember(this.context.credentials.userId);
if (!me) return; if (!me) return;
const defaultLevel = powerLevelEvent.getContent().users_default; const defaultLevel = powerLevelEvent.getContent().users_default;
@ -515,7 +514,7 @@ module.exports = createReactClass({
// toggle the level // toggle the level
const newLevel = this.state.isTargetMod ? defaultLevel : modLevel; const newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
this.setState({ updating: this.state.updating + 1 }); this.setState({ updating: this.state.updating + 1 });
this.context.matrixClient.setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).then( this.context.setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).then(
function() { function() {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
@ -550,7 +549,7 @@ module.exports = createReactClass({
danger: true, danger: true,
onFinished: (accepted) => { onFinished: (accepted) => {
if (!accepted) return; if (!accepted) return;
this.context.matrixClient.deactivateSynapseUser(this.props.member.userId).catch(e => { this.context.deactivateSynapseUser(this.props.member.userId).catch(e => {
console.error("Failed to deactivate user"); console.error("Failed to deactivate user");
console.error(e); console.error(e);
@ -566,7 +565,7 @@ module.exports = createReactClass({
_applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) { _applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) {
this.setState({ updating: this.state.updating + 1 }); this.setState({ updating: this.state.updating + 1 });
this.context.matrixClient.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( this.context.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
function() { function() {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
@ -587,7 +586,7 @@ module.exports = createReactClass({
onPowerChange: async function(powerLevel) { onPowerChange: async function(powerLevel) {
const roomId = this.props.member.roomId; const roomId = this.props.member.roomId;
const target = this.props.member.userId; const target = this.props.member.userId;
const room = this.context.matrixClient.getRoom(roomId); const room = this.context.getRoom(roomId);
if (!room) return; if (!room) return;
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
@ -598,7 +597,7 @@ module.exports = createReactClass({
return; return;
} }
const myUserId = this.context.matrixClient.getUserId(); const myUserId = this.context.getUserId();
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse. // If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
@ -650,9 +649,9 @@ module.exports = createReactClass({
_calculateOpsPermissions: async function(member) { _calculateOpsPermissions: async function(member) {
let canDeactivate = false; let canDeactivate = false;
if (this.context.matrixClient) { if (this.context) {
try { try {
canDeactivate = await this.context.matrixClient.isSynapseAdministrator(); canDeactivate = await this.context.isSynapseAdministrator();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@ -665,13 +664,13 @@ module.exports = createReactClass({
}, },
muted: false, muted: false,
}; };
const room = this.context.matrixClient.getRoom(member.roomId); const room = this.context.getRoom(member.roomId);
if (!room) return defaultPerms; if (!room) return defaultPerms;
const powerLevels = room.currentState.getStateEvents("m.room.power_levels", ""); const powerLevels = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevels) return defaultPerms; if (!powerLevels) return defaultPerms;
const me = room.getMember(this.context.matrixClient.credentials.userId); const me = room.getMember(this.context.credentials.userId);
if (!me) return defaultPerms; if (!me) return defaultPerms;
const them = member; const them = member;
@ -738,7 +737,7 @@ module.exports = createReactClass({
const avatarUrl = member.getMxcAvatarUrl(); const avatarUrl = member.getMxcAvatarUrl();
if (!avatarUrl) return; if (!avatarUrl) return;
const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl); const httpUrl = this.context.mxcUrlToHttp(avatarUrl);
const ImageView = sdk.getComponent("elements.ImageView"); const ImageView = sdk.getComponent("elements.ImageView");
const params = { const params = {
src: httpUrl, src: httpUrl,
@ -797,7 +796,7 @@ module.exports = createReactClass({
}, },
_renderUserOptions: function() { _renderUserOptions: function() {
const cli = this.context.matrixClient; const cli = this.context;
const member = this.props.member; const member = this.props.member;
let ignoreButton = null; let ignoreButton = null;
@ -905,9 +904,9 @@ module.exports = createReactClass({
let synapseDeactivateButton; let synapseDeactivateButton;
let spinner; let spinner;
if (this.props.member.userId !== this.context.matrixClient.credentials.userId) { if (this.props.member.userId !== this.context.credentials.userId) {
// TODO: Immutable DMs replaces a lot of this // TODO: Immutable DMs replaces a lot of this
const dmRoomMap = new DMRoomMap(this.context.matrixClient); const dmRoomMap = new DMRoomMap(this.context);
// dmRooms will not include dmRooms that we have been invited into but did not join. // dmRooms will not include dmRooms that we have been invited into but did not join.
// Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room. // Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room.
// XXX: we potentially want DMs we have been invited to, to also show up here :L // XXX: we potentially want DMs we have been invited to, to also show up here :L
@ -918,7 +917,7 @@ module.exports = createReactClass({
const tiles = []; const tiles = [];
for (const roomId of dmRooms) { for (const roomId of dmRooms) {
const room = this.context.matrixClient.getRoom(roomId); const room = this.context.getRoom(roomId);
if (room) { if (room) {
const myMembership = room.getMyMembership(); const myMembership = room.getMyMembership();
// not a DM room if we have are not joined // not a DM room if we have are not joined
@ -1064,12 +1063,12 @@ module.exports = createReactClass({
} }
} }
const room = this.context.matrixClient.getRoom(this.props.member.roomId); const room = this.context.getRoom(this.props.member.roomId);
const powerLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null; const powerLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null;
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"]; const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
const hsUrl = this.context.matrixClient.baseUrl; const hsUrl = this.context.baseUrl;
let showPresence = true; let showPresence = true;
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) { if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
showPresence = enablePresenceByHsUrl[hsUrl]; showPresence = enablePresenceByHsUrl[hsUrl];
@ -1108,7 +1107,7 @@ module.exports = createReactClass({
</div> </div>
</div>; </div>;
const isEncrypted = this.context.matrixClient.isRoomEncrypted(this.props.member.roomId); const isEncrypted = this.context.isRoomEncrypted(this.props.member.roomId);
if (this.state.e2eStatus && isEncrypted) { if (this.state.e2eStatus && isEncrypted) {
e2eIconElement = (<E2EIcon status={this.state.e2eStatus} isUser={true} />); e2eIconElement = (<E2EIcon status={this.state.e2eStatus} isUser={true} />);
} }
@ -1117,7 +1116,7 @@ module.exports = createReactClass({
const avatarUrl = this.props.member.getMxcAvatarUrl(); const avatarUrl = this.props.member.getMxcAvatarUrl();
let avatarElement; let avatarElement;
if (avatarUrl) { if (avatarUrl) {
const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800); const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);
avatarElement = <div className="mx_MemberInfo_avatar"> avatarElement = <div className="mx_MemberInfo_avatar">
<img src={httpUrl} /> <img src={httpUrl} />
</div>; </div>;

View file

@ -32,6 +32,10 @@ const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5; const INITIAL_LOAD_NUM_INVITED = 5;
const SHOW_MORE_INCREMENT = 100; const SHOW_MORE_INCREMENT = 100;
// Regex applied to filter our punctuation in member names before applying sort, to fuzzy it a little
// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
module.exports = createReactClass({ module.exports = createReactClass({
displayName: 'MemberList', displayName: 'MemberList',
@ -336,10 +340,13 @@ module.exports = createReactClass({
} }
// Fourth by name (alphabetical) // Fourth by name (alphabetical)
const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name; const nameA = (memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name).replace(SORT_REGEX, "");
const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name; const nameB = (memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name).replace(SORT_REGEX, "");
// console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`); // console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`);
return nameA.localeCompare(nameB); return nameA.localeCompare(nameB, {
ignorePunctuation: true,
sensitivity: "base",
});
}, },
onSearchQueryChanged: function(searchQuery) { onSearchQueryChanged: function(searchQuery) {

View file

@ -107,8 +107,8 @@ class UploadButton extends React.Component {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
} }
constructor(props, context) { constructor(props) {
super(props, context); super(props);
this.onUploadClick = this.onUploadClick.bind(this); this.onUploadClick = this.onUploadClick.bind(this);
this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this); this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this);
@ -165,8 +165,8 @@ class UploadButton extends React.Component {
} }
export default class MessageComposer extends React.Component { export default class MessageComposer extends React.Component {
constructor(props, context) { constructor(props) {
super(props, context); super(props);
this.onInputStateChanged = this.onInputStateChanged.bind(this); this.onInputStateChanged = this.onInputStateChanged.bind(this);
this.onEvent = this.onEvent.bind(this); this.onEvent = this.onEvent.bind(this);
this._onRoomStateEvents = this._onRoomStateEvents.bind(this); this._onRoomStateEvents = this._onRoomStateEvents.bind(this);

View file

@ -48,7 +48,6 @@ import Markdown from '../../../Markdown';
import MessageComposerStore from '../../../stores/MessageComposerStore'; import MessageComposerStore from '../../../stores/MessageComposerStore';
import ContentMessages from '../../../ContentMessages'; import ContentMessages from '../../../ContentMessages';
import EMOJIBASE from 'emojibase-data/en/compact.json';
import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
@ -61,6 +60,7 @@ import AccessibleButton from '../elements/AccessibleButton';
import {findEditableEvent} from '../../../utils/EventUtils'; import {findEditableEvent} from '../../../utils/EventUtils';
import SlateComposerHistoryManager from "../../../SlateComposerHistoryManager"; import SlateComposerHistoryManager from "../../../SlateComposerHistoryManager";
import TypingStore from "../../../stores/TypingStore"; import TypingStore from "../../../stores/TypingStore";
import {EMOTICON_TO_EMOJI} from "../../../emoji";
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
@ -141,8 +141,8 @@ export default class MessageComposerInput extends React.Component {
autocomplete: Autocomplete; autocomplete: Autocomplete;
historyManager: SlateComposerHistoryManager; historyManager: SlateComposerHistoryManager;
constructor(props, context) { constructor(props) {
super(props, context); super(props);
const isRichTextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'); const isRichTextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled');
@ -464,7 +464,7 @@ export default class MessageComposerInput extends React.Component {
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(text.slice(0, currentStartOffset)); const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(text.slice(0, currentStartOffset));
if (emoticonMatch) { if (emoticonMatch) {
const query = emoticonMatch[1].toLowerCase().replace("-", ""); const query = emoticonMatch[1].toLowerCase().replace("-", "");
const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false); const data = EMOTICON_TO_EMOJI.get(query);
// only perform replacement if we found a match, otherwise we would be not letting user type // only perform replacement if we found a match, otherwise we would be not letting user type
if (data) { if (data) {

Some files were not shown because too many files have changed in this diff Show more