From fd28cf7a4c9e284ffe3bca8921e7402aacdd2924 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 12 Nov 2019 15:12:54 -0700 Subject: [PATCH 01/46] Move notification count to in front of the room name in the page title Fixes https://github.com/vector-im/riot-web/issues/10943 --- src/components/structures/MatrixChat.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 6cc86bf6d7..cd5b27f2b9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1767,10 +1767,12 @@ export default createReactClass({ const client = MatrixClientPeg.get(); const room = client && client.getRoom(this.state.currentRoomId); if (room) { - subtitle = `| ${ room.name } ${subtitle}`; + subtitle = `${this.subTitleStatus} | ${ room.name } ${subtitle}`; } + } else { + subtitle = `${this.subTitleStatus} ${subtitle}`; } - document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle} ${this.subTitleStatus}`; + document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle}`; }, updateStatusIndicator: function(state, prevState) { From 1aa0ab13e682fd73c2c9b7d46d77202339593a1e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 12 Nov 2019 15:38:24 -0700 Subject: [PATCH 02/46] Add some logging/recovery for lost rooms Zero inserts is not normal, so we apply the same recovery technique from the categorization logic above this block: insert it to be the very first room and hope that someone complains that the room is ordered incorrectly. There's some additional logging to try and identify what went wrong because it should definitely be inserted. The `!== 1` check is not supposed to be called, ever. Logging for https://github.com/vector-im/riot-web/issues/11303 --- src/stores/RoomListStore.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 980753551a..134870398f 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -515,7 +515,21 @@ class RoomListStore extends Store { } if (count !== 1) { - console.warn(`!! Room ${room.roomId} inserted ${count} times`); + console.warn(`!! Room ${room.roomId} inserted ${count} times to ${targetTag}`); + } + + // This is a workaround for https://github.com/vector-im/riot-web/issues/11303 + // The logging is to try and identify what happened exactly. + if (count === 0) { + // Something went very badly wrong - try to recover the room. + // We don't bother checking how the target list is ordered - we're expecting + // to just insert it. + console.warn(`!! Recovering ${room.roomId} for tag ${targetTag} at position 0`); + if (!listsClone[targetTag]) { + console.warn(`!! List for tag ${targetTag} does not exist - creating`); + listsClone[targetTag] = []; + } + listsClone[targetTag].splice(0, 0, {room, category}); } } From fa6e02fafb30b44b0ac0d833335780b18fc80851 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 12 Nov 2019 15:38:24 -0700 Subject: [PATCH 03/46] Revert "Add some logging/recovery for lost rooms" This reverts commit 1aa0ab13e682fd73c2c9b7d46d77202339593a1e. --- src/stores/RoomListStore.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 134870398f..980753551a 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -515,21 +515,7 @@ class RoomListStore extends Store { } if (count !== 1) { - console.warn(`!! Room ${room.roomId} inserted ${count} times to ${targetTag}`); - } - - // This is a workaround for https://github.com/vector-im/riot-web/issues/11303 - // The logging is to try and identify what happened exactly. - if (count === 0) { - // Something went very badly wrong - try to recover the room. - // We don't bother checking how the target list is ordered - we're expecting - // to just insert it. - console.warn(`!! Recovering ${room.roomId} for tag ${targetTag} at position 0`); - if (!listsClone[targetTag]) { - console.warn(`!! List for tag ${targetTag} does not exist - creating`); - listsClone[targetTag] = []; - } - listsClone[targetTag].splice(0, 0, {room, category}); + console.warn(`!! Room ${room.roomId} inserted ${count} times`); } } From 3dcc92b79da7458e25b6511f6d0a478da746714b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 12 Nov 2019 15:38:24 -0700 Subject: [PATCH 04/46] Add some logging/recovery for lost rooms Zero inserts is not normal, so we apply the same recovery technique from the categorization logic above this block: insert it to be the very first room and hope that someone complains that the room is ordered incorrectly. There's some additional logging to try and identify what went wrong because it should definitely be inserted. The `!== 1` check is not supposed to be called, ever. Logging for https://github.com/vector-im/riot-web/issues/11303 --- src/stores/RoomListStore.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 980753551a..134870398f 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -515,7 +515,21 @@ class RoomListStore extends Store { } if (count !== 1) { - console.warn(`!! Room ${room.roomId} inserted ${count} times`); + console.warn(`!! Room ${room.roomId} inserted ${count} times to ${targetTag}`); + } + + // This is a workaround for https://github.com/vector-im/riot-web/issues/11303 + // The logging is to try and identify what happened exactly. + if (count === 0) { + // Something went very badly wrong - try to recover the room. + // We don't bother checking how the target list is ordered - we're expecting + // to just insert it. + console.warn(`!! Recovering ${room.roomId} for tag ${targetTag} at position 0`); + if (!listsClone[targetTag]) { + console.warn(`!! List for tag ${targetTag} does not exist - creating`); + listsClone[targetTag] = []; + } + listsClone[targetTag].splice(0, 0, {room, category}); } } From 56ad164c69ab497efed2949de62b7282fe86da2e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 13 Nov 2019 14:01:07 -0700 Subject: [PATCH 05/46] Add a function to get the "base" theme for a theme Useful for trying to load the right assets first. See https://github.com/vector-im/riot-web/pull/11381 --- src/theme.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/theme.js b/src/theme.js index d479170792..8a15c606d7 100644 --- a/src/theme.js +++ b/src/theme.js @@ -60,6 +60,22 @@ function getCustomTheme(themeName) { return customTheme; } +/** + * Gets the underlying theme name for the given theme. This is usually the theme or + * CSS resource that the theme relies upon to load. + * @param {string} theme The theme name to get the base of. + * @returns {string} The base theme (typically "light" or "dark"). + */ +export function getBaseTheme(theme) { + if (!theme) return "light"; + if (theme.startsWith("custom-")) { + const customTheme = getCustomTheme(theme.substr(7)); + return customTheme.is_dark ? "dark-custom" : "light-custom"; + } + + return theme; // it's probably a base theme +} + /** * Called whenever someone changes the theme * From 413b90328fe3fc916045c194522cb0ba1721d4a4 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 14 Nov 2019 15:23:04 +0000 Subject: [PATCH 06/46] Show server details on login for unreachable homeserver This fixes the login page to be more helpful when the current homeserver is unreachable: it reveals the server change field, so you have some chance to progress forward. Fixes https://github.com/vector-im/riot-web/issues/11077 --- src/components/structures/auth/Login.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index ad77ed49a5..b35110bf6b 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -386,7 +386,11 @@ module.exports = createReactClass({ ...AutoDiscoveryUtils.authComponentStateForError(e), }); if (this.state.serverErrorIsFatal) { - return; // Server is dead - do not continue. + // Server is dead: show server details prompt instead + this.setState({ + phase: PHASE_SERVER_DETAILS, + }); + return; } } From b05dabe2b7746975ba285d6e52b7594e27a4b8b6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 Nov 2019 12:02:16 -0700 Subject: [PATCH 07/46] Add better error handling to Synapse user deactivation Also clearly flag it as a Synapse user deactivation in the analytics, so we don't get confused. Fixes https://github.com/vector-im/riot-web/issues/10986 --- src/components/views/right_panel/UserInfo.js | 7 +++++-- src/components/views/rooms/MemberInfo.js | 11 ++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 207bf29998..7a88c80ce5 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -842,10 +842,13 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room const [accepted] = await finished; if (!accepted) return; try { - cli.deactivateSynapseUser(user.userId); + await cli.deactivateSynapseUser(user.userId); } catch (err) { + console.error("Failed to deactivate user"); + console.error(err); + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createTrackedDialog('Failed to deactivate user', '', ErrorDialog, { + Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, { title: _t('Failed to deactivate user'), description: ((err && err.message) ? err.message : _t("Operation failed")), }); diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 2ea6392e96..9364f2f49d 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -550,7 +550,16 @@ module.exports = createReactClass({ danger: true, onFinished: (accepted) => { if (!accepted) return; - this.context.matrixClient.deactivateSynapseUser(this.props.member.userId); + this.context.matrixClient.deactivateSynapseUser(this.props.member.userId).catch(e => { + console.error("Failed to deactivate user"); + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, { + title: _t('Failed to deactivate user'), + description: ((e && e.message) ? e.message : _t("Operation failed")), + }); + }); }, }); }, From 0f2f500a16078b23e818340f4bc2491e2d0e3d56 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 Nov 2019 12:08:04 -0700 Subject: [PATCH 08/46] i18n update --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dc9773ad21..8f1344d5c9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -867,6 +867,7 @@ "Deactivate user?": "Deactivate user?", "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?", "Deactivate user": "Deactivate user", + "Failed to deactivate user": "Failed to deactivate user", "Failed to change power level": "Failed to change power level", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "Are you sure?": "Are you sure?", @@ -1073,7 +1074,6 @@ "Remove this user from community?": "Remove this user from community?", "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", - "Failed to deactivate user": "Failed to deactivate user", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Sunday": "Sunday", From af4ad488bdf753edb514e13b88957a2ef103dcda Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 11 Nov 2019 16:54:12 +0100 Subject: [PATCH 09/46] Restyle Avatar Make it a circle with the profile picture centered, with a max height/width of 30vh --- res/css/views/right_panel/_UserInfo.scss | 27 +++++++++++++++----- src/components/views/right_panel/UserInfo.js | 18 +++++++++++-- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index df536a7388..db08fe18bf 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -38,6 +38,7 @@ limitations under the License. mask-repeat: no-repeat; mask-position: 16px center; background-color: $rightpanel-button-color; + position: absolute; } .mx_UserInfo_profile h2 { @@ -47,7 +48,7 @@ limitations under the License. } .mx_UserInfo h2 { - font-size: 16px; + font-size: 18px; font-weight: 600; margin: 16px 0 8px 0; } @@ -74,15 +75,27 @@ limitations under the License. } .mx_UserInfo_avatar { - background: $tagpanel-bg-color; + margin: 24px 32px 0 32px; } -.mx_UserInfo_avatar > img { - height: auto; - width: 100%; +.mx_UserInfo_avatar > div { + max-width: 30vh; + margin: 0 auto; +} + +.mx_UserInfo_avatar > div > div { + /* use padding-top instead of height to make this element square, + as the % in padding is a % of the width (including margin, + that's why we had to put the margin to center on a parent div), + and not a % of the parent height. */ + padding-top: 100%; + height: 0; + border-radius: 100%; max-height: 30vh; - object-fit: contain; - display: block; + box-sizing: content-box; + background-repeat: no-repeat; + background-size: cover; + background-position: center; } .mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 207bf29998..e55aae74f5 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -40,6 +40,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import {ContentRepo} from 'matrix-js-sdk'; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -917,6 +918,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); }, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line + const onMemberAvatarKey = e => { + if (e.key === "Enter") { + onMemberAvatarClick(); + } + }; + const onMemberAvatarClick = useCallback(() => { const member = user; const avatarUrl = member.getMxcAvatarUrl(); @@ -1045,8 +1052,15 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room let avatarElement; if (avatarUrl) { const httpUrl = cli.mxcUrlToHttp(avatarUrl, 800, 800); - avatarElement =
- {_t("Profile + avatarElement =
+
; } From f4988392f9cdec443ddd01c09bfb931f1beead7f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 16:25:38 +0100 Subject: [PATCH 10/46] restyle e2e icons --- res/css/views/rooms/_E2EIcon.scss | 53 +++++++++++++++++++++++---- res/img/e2e/verified.svg | 13 ++++++- res/img/e2e/warning.svg | 16 +++++--- src/components/views/rooms/E2EIcon.js | 8 +++- 4 files changed, 75 insertions(+), 15 deletions(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index 84a16611de..c609d70f4c 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -17,17 +17,56 @@ limitations under the License. .mx_E2EIcon { width: 25px; height: 25px; - mask-repeat: no-repeat; - mask-position: center 0; margin: 0 9px; + position: relative; + display: block; } -.mx_E2EIcon_verified { - mask-image: url('$(res)/img/e2e/lock-verified.svg'); - background-color: $accent-color; +.mx_E2EIcon_verified::before, .mx_E2EIcon_warning::before { + content: ""; + display: block; + /* the symbols in the shield icons are cut out the make the themeable with css masking. + if they appear on a different background than white, the symbol wouldn't be white though, so we + add a rectangle here below the masked element to shine through the symbol cutout. + hardcoding white and not using a theme variable as this would probably be white for any theme. */ + background-color: white; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; } -.mx_E2EIcon_warning { - mask-image: url('$(res)/img/e2e/lock-warning.svg'); +.mx_E2EIcon_verified::after, .mx_E2EIcon_warning::after { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-size: contain; +} + +.mx_E2EIcon_verified::before { + /* white rectangle below checkmark of shield */ + margin: 25% 28% 38% 25%; +} + + +.mx_E2EIcon_verified::after { + mask-image: url('$(res)/img/e2e/verified.svg'); + background-color: $warning-color; +} + + +.mx_E2EIcon_warning::before { + /* white rectangle below "!" of shield */ + margin: 18% 40% 25% 40%; +} + +.mx_E2EIcon_warning::after { + mask-image: url('$(res)/img/e2e/warning.svg'); background-color: $warning-color; } diff --git a/res/img/e2e/verified.svg b/res/img/e2e/verified.svg index 459a552a40..af6bb92297 100644 --- a/res/img/e2e/verified.svg +++ b/res/img/e2e/verified.svg @@ -1,3 +1,12 @@ - - + + + diff --git a/res/img/e2e/warning.svg b/res/img/e2e/warning.svg index 3d5fba550c..2501da6ab3 100644 --- a/res/img/e2e/warning.svg +++ b/res/img/e2e/warning.svg @@ -1,6 +1,12 @@ - - - - - + + + diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js index 54260e4ee2..d6baa30c8e 100644 --- a/src/components/views/rooms/E2EIcon.js +++ b/src/components/views/rooms/E2EIcon.js @@ -36,7 +36,13 @@ export default function(props) { _t("All devices for this user are trusted") : _t("All devices in this encrypted room are trusted"); } - const icon = (
); + + let style = null; + if (props.size) { + style = {width: `${props.size}px`, height: `${props.size}px`}; + } + + const icon = (
); if (props.onClick) { return ({ icon }); } else { From 3e356756aae08459f22e71e25e33fcbc50d880b7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 16:26:26 +0100 Subject: [PATCH 11/46] style profile info --- res/css/views/right_panel/_UserInfo.scss | 40 +++++++++++--------- src/components/views/right_panel/UserInfo.js | 12 +++--- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index db08fe18bf..aee0252c4e 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -22,13 +22,6 @@ limitations under the License. overflow-y: auto; } -.mx_UserInfo_profile .mx_E2EIcon { - display: inline; - margin: auto; - padding-right: 25px; - mask-size: contain; -} - .mx_UserInfo_cancel { height: 16px; width: 16px; @@ -41,16 +34,10 @@ limitations under the License. position: absolute; } -.mx_UserInfo_profile h2 { - flex: 1; - overflow-x: auto; - max-height: 50px; -} - .mx_UserInfo h2 { font-size: 18px; font-weight: 600; - margin: 16px 0 8px 0; + margin: 0; } .mx_UserInfo_container { @@ -76,6 +63,7 @@ limitations under the License. .mx_UserInfo_avatar { margin: 24px 32px 0 32px; + cursor: pointer; } .mx_UserInfo_avatar > div { @@ -110,12 +98,30 @@ limitations under the License. margin: 4px 0; } -.mx_UserInfo_profileField { - font-size: 15px; - position: relative; +.mx_UserInfo_profile { + font-size: 12px; text-align: center; + + h2 { + flex: 1; + overflow-x: auto; + max-height: 50px; + display: flex; + justify-self: ; + justify-content: center; + align-items: center; + + .mx_E2EIcon { + margin: 5px; + } + } + + .mx_UserInfo_profileStatus { + margin-top: 12px; + } } + .mx_UserInfo_memberDetails { text-align: center; } diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index e55aae74f5..43c5833faa 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1157,7 +1157,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room let e2eIcon; if (isRoomEncrypted && devices) { - e2eIcon = ; + e2eIcon = ; } return ( @@ -1167,16 +1167,14 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
-
-

+
+

{ e2eIcon } { displayName }

-
- { user.userId } -
-
+
{ user.userId }
+
{presenceLabel} {statusLabel}
From b475bc9e912f0764f6a759e2434ea806fedf1e5b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 16:40:07 +0100 Subject: [PATCH 12/46] Add direct message button While we don't have canonical DMs yet, it takes you to the most recently active DM room --- src/components/views/right_panel/UserInfo.js | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 43c5833faa..0c058a8859 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -186,6 +186,26 @@ const DirectChatsSection = withLegacyMatrixClient(({matrixClient: cli, userId, s ); }); +function openDMForUser(cli, userId) { + const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); + const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { + const room = cli.getRoom(roomId); + if (!lastActiveRoom || (room && lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp())) { + return room; + } + return lastActiveRoom; + }, null); + + if (lastActiveRoom) { + dis.dispatch({ + action: 'view_room', + room_id: lastActiveRoom.roomId, + }); + } else { + createRoom({dmUserId: userId}); + } +} + const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite}) => { let ignoreButton = null; let insertPillButton = null; @@ -286,10 +306,20 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i ); + let directMessageButton; + if (!isMe) { + directMessageButton = ( + openDMForUser(cli, member.userId)} className="mx_UserInfo_field"> + { _t('Direct message') } + + ); + } + return (

{ _t("User Options") }

+ { directMessageButton } { readReceiptButton } { shareUserButton } { insertPillButton } From 0a2255ce7303d0fbdcfefb6f7a107b168c0c533a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 17:32:34 +0100 Subject: [PATCH 13/46] fixup: bring back margin above display name --- res/css/views/right_panel/_UserInfo.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index aee0252c4e..07b8ed2879 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -37,7 +37,7 @@ limitations under the License. .mx_UserInfo h2 { font-size: 18px; font-weight: 600; - margin: 0; + margin: 18px 0 0 0; } .mx_UserInfo_container { From 8dd7d8e5c0fdb9f4166a85a15e8bf3a0e2b62ab8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 17:33:24 +0100 Subject: [PATCH 14/46] fixup: don't consider left DM rooms --- src/components/views/right_panel/UserInfo.js | 7 +++++-- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 0c058a8859..c008cfe1f0 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -190,7 +190,10 @@ function openDMForUser(cli, userId) { const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { const room = cli.getRoom(roomId); - if (!lastActiveRoom || (room && lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp())) { + if (!room || room.getMyMembership() === "leave") { + return lastActiveRoom; + } + if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) { return room; } return lastActiveRoom; @@ -317,7 +320,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i return (
-

{ _t("User Options") }

+

{ _t("Options") }

{ directMessageButton } { readReceiptButton } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dc9773ad21..4b86c399a4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1068,6 +1068,8 @@ "Files": "Files", "Trust & Devices": "Trust & Devices", "Direct messages": "Direct messages", + "Direct message": "Direct message", + "Options": "Options", "Remove from community": "Remove from community", "Disinvite this user from community?": "Disinvite this user from community?", "Remove this user from community?": "Remove this user from community?", @@ -1091,7 +1093,6 @@ "Reply": "Reply", "Edit": "Edit", "Message Actions": "Message Actions", - "Options": "Options", "Attachment": "Attachment", "Error decrypting attachment": "Error decrypting attachment", "Decrypt %(text)s": "Decrypt %(text)s", From 238555f4ec20ed6fa60be311e18d52cf0d447a4e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 17:34:35 +0100 Subject: [PATCH 15/46] fixup: isMe --- src/components/views/right_panel/UserInfo.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index c008cfe1f0..1964b5601c 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -215,6 +215,10 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i let inviteUserButton = null; let readReceiptButton = null; + const isMe = member.userId === cli.getUserId(); + + + const onShareUserClick = () => { const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share room member dialog', '', ShareDialog, { @@ -224,7 +228,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i // Only allow the user to ignore the user if its not ourselves // same goes for jumping to read receipt - if (member.userId !== cli.getUserId()) { + if (!isMe) { const onIgnoreToggle = () => { const ignoredUsers = cli.getIgnoredUsers(); if (isIgnored) { From bd2bf4500adf8d753b857b210c15e849faf8b682 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Nov 2019 17:35:38 +0100 Subject: [PATCH 16/46] remove direct message list from UserInfo --- src/components/views/right_panel/UserInfo.js | 107 ------------------- 1 file changed, 107 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 1964b5601c..412bf92831 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -89,103 +89,6 @@ const DevicesSection = ({devices, userId, loading}) => { ); }; -const onRoomTileClick = (roomId) => { - dis.dispatch({ - action: 'view_room', - room_id: roomId, - }); -}; - -const DirectChatsSection = withLegacyMatrixClient(({matrixClient: cli, userId, startUpdating, stopUpdating}) => { - const onNewDMClick = async () => { - startUpdating(); - await createRoom({dmUserId: userId}); - stopUpdating(); - }; - - // TODO: Immutable DMs replaces a lot of this - // 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. - // XXX: we potentially want DMs we have been invited to, to also show up here :L - // especially as logic below concerns specially if we haven't joined but have been invited - const [dmRooms, setDmRooms] = useState(new DMRoomMap(cli).getDMRoomsForUserId(userId)); - - // TODO bind the below - // cli.on("Room", this.onRoom); - // cli.on("Room.name", this.onRoomName); - // cli.on("deleteRoom", this.onDeleteRoom); - - const accountDataHandler = useCallback((ev) => { - if (ev.getType() === "m.direct") { - const dmRoomMap = new DMRoomMap(cli); - setDmRooms(dmRoomMap.getDMRoomsForUserId(userId)); - } - }, [cli, userId]); - useEventEmitter(cli, "accountData", accountDataHandler); - - const RoomTile = sdk.getComponent("rooms.RoomTile"); - - const tiles = []; - for (const roomId of dmRooms) { - const room = cli.getRoom(roomId); - if (room) { - const myMembership = room.getMyMembership(); - // not a DM room if we have are not joined - if (myMembership !== 'join') continue; - - const them = room.getMember(userId); - // not a DM room if they are not joined - if (!them || !them.membership || them.membership !== 'join') continue; - - const highlight = room.getUnreadNotificationCount('highlight') > 0; - - tiles.push( - , - ); - } - } - - const labelClasses = classNames({ - mx_UserInfo_createRoom_label: true, - mx_RoomTile_name: true, - }); - - let body = tiles; - if (!body) { - body = ( - -
- {_t("Start -
-
{ _t("Start a chat") }
-
- ); - } - - return ( -
-
-

{ _t("Direct messages") }

- -
- { body } -
- ); -}); - function openDMForUser(cli, userId) { const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { @@ -217,8 +120,6 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i const isMe = member.userId === cli.getUserId(); - - const onShareUserClick = () => { const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share room member dialog', '', ShareDialog, { @@ -979,11 +880,6 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room let synapseDeactivateButton; let spinner; - let directChatsSection; - if (user.userId !== cli.getUserId()) { - directChatsSection = ; - } - // We don't need a perfect check here, just something to pass as "probably not our homeserver". If // someone does figure out how to bypass this check the worst that happens is an error. // FIXME this should be using cli instead of MatrixClientPeg.matrixClient @@ -1226,9 +1122,6 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room { devicesSection } - - { directChatsSection } - Date: Wed, 13 Nov 2019 12:09:20 +0100 Subject: [PATCH 17/46] update when room encryption is turned on also don't download devices as long as room is not encrypted --- src/components/views/right_panel/UserInfo.js | 31 +++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 412bf92831..28b5af358a 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -64,6 +64,18 @@ const _getE2EStatus = (devices) => { return hasUnverifiedDevice ? "warning" : "verified"; }; +function useIsEncrypted(cli, room) { + const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId)); + + const update = useCallback((event) => { + if (event.getType() === "m.room.encryption") { + setIsEncrypted(cli.isRoomEncrypted(room.roomId)); + } + }, [cli, room]); + useEventEmitter(room.currentState, "RoomState.events", update); + return isEncrypted; +} + const DevicesSection = ({devices, userId, loading}) => { const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); const Spinner = sdk.getComponent("elements.Spinner"); @@ -1005,6 +1017,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room title={_t('Close')} />; } + const isRoomEncrypted = useIsEncrypted(cli, room); // undefined means yet to be loaded, null means failed to load, otherwise list of devices const [devices, setDevices] = useState(undefined); // Download device lists @@ -1029,14 +1042,15 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room setDevices(null); } } - - _downloadDeviceList(); + if (isRoomEncrypted) { + _downloadDeviceList(); + } // Handle being unmounted return () => { cancelled = true; }; - }, [cli, user.userId]); + }, [cli, user.userId, isRoomEncrypted]); // Listen to changes useEffect(() => { @@ -1053,16 +1067,19 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room } }; - cli.on("deviceVerificationChanged", onDeviceVerificationChanged); + if (isRoomEncrypted) { + cli.on("deviceVerificationChanged", onDeviceVerificationChanged); + } // Handle being unmounted return () => { cancel = true; - cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged); + if (isRoomEncrypted) { + cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged); + } }; - }, [cli, user.userId]); + }, [cli, user.userId, isRoomEncrypted]); let devicesSection; - const isRoomEncrypted = _enableDevices && room && cli.isRoomEncrypted(room.roomId); if (isRoomEncrypted) { devicesSection = ; } else { From e32a948d5d15a44b6dd2cb6a1615a69e8fa8fd8e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 12:10:30 +0100 Subject: [PATCH 18/46] add "unverify user" action to user info --- src/components/views/right_panel/UserInfo.js | 23 +++++++++++++++++++- src/i18n/strings/en_EN.json | 1 + 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 28b5af358a..c61746293e 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -64,6 +64,17 @@ const _getE2EStatus = (devices) => { return hasUnverifiedDevice ? "warning" : "verified"; }; +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 useIsEncrypted(cli, room) { const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId)); @@ -124,7 +135,7 @@ function openDMForUser(cli, userId) { } } -const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite}) => { +const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => { let ignoreButton = null; let insertPillButton = null; let inviteUserButton = null; @@ -234,6 +245,14 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i ); } + let unverifyButton; + if (devices && devices.some(device => device.isVerified())) { + unverifyButton = ( + unverifyUser(cli, member.userId)} className="mx_UserInfo_field"> + { _t('Unverify user') } + + ); + } return (
@@ -245,6 +264,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i { insertPillButton } { ignoreButton } { inviteUserButton } + { unverifyButton }
); @@ -1140,6 +1160,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room { devicesSection } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4b86c399a4..fcf43af31f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1069,6 +1069,7 @@ "Trust & Devices": "Trust & Devices", "Direct messages": "Direct messages", "Direct message": "Direct message", + "Unverify user": "Unverify user", "Options": "Options", "Remove from community": "Remove from community", "Disinvite this user from community?": "Disinvite this user from community?", From 4a1dc5567341c478fefd275010dabf81329f3efb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 12:11:06 +0100 Subject: [PATCH 19/46] fixup: rearrange openDMForUser --- src/components/views/right_panel/UserInfo.js | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index c61746293e..e7277a52e2 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -75,6 +75,29 @@ async function unverifyUser(matrixClient, userId) { } } +function openDMForUser(matrixClient, userId) { + const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); + const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { + const room = matrixClient.getRoom(roomId); + if (!room || room.getMyMembership() === "leave") { + return lastActiveRoom; + } + if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) { + return room; + } + return lastActiveRoom; + }, null); + + if (lastActiveRoom) { + dis.dispatch({ + action: 'view_room', + room_id: lastActiveRoom.roomId, + }); + } else { + createRoom({dmUserId: userId}); + } +} + function useIsEncrypted(cli, room) { const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId)); @@ -112,29 +135,6 @@ const DevicesSection = ({devices, userId, loading}) => { ); }; -function openDMForUser(cli, userId) { - const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); - const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { - const room = cli.getRoom(roomId); - if (!room || room.getMyMembership() === "leave") { - return lastActiveRoom; - } - if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) { - return room; - } - return lastActiveRoom; - }, null); - - if (lastActiveRoom) { - dis.dispatch({ - action: 'view_room', - room_id: lastActiveRoom.roomId, - }); - } else { - createRoom({dmUserId: userId}); - } -} - const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => { let ignoreButton = null; let insertPillButton = null; From 6afeeddb36cba22823b815d37a20fb6de158ca27 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 15:59:49 +0100 Subject: [PATCH 20/46] hide verified devices by default with expand button --- src/components/views/right_panel/UserInfo.js | 66 +++++++++++++------- src/i18n/strings/en_EN.json | 6 +- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index e7277a52e2..1e59ec2c44 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -114,6 +114,8 @@ const DevicesSection = ({devices, userId, loading}) => { const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); const Spinner = sdk.getComponent("elements.Spinner"); + const [isExpanded, setExpanded] = useState(false); + if (loading) { // still loading return ; @@ -121,16 +123,37 @@ const DevicesSection = ({devices, userId, loading}) => { if (devices === null) { return _t("Unable to load device list"); } - if (devices.length === 0) { - return _t("No devices with registered encryption keys"); + + const unverifiedDevices = devices.filter(d => !d.isVerified()); + const verifiedDevices = devices.filter(d => d.isVerified()); + + let expandButton; + if (verifiedDevices.length) { + if (isExpanded) { + expandButton = ( setExpanded(false)}> + {_t("Hide verified Sign-In's")} + ); + } else { + expandButton = ( setExpanded(true)}> + {_t("%(count)s verified Sign-In's", {count: verifiedDevices.length})} + ); + } + } + + let deviceList = unverifiedDevices.map((device, i) => { + return (); + }); + if (isExpanded) { + const keyStart = unverifiedDevices.length; + deviceList = deviceList.concat(verifiedDevices.map((device, i) => { + return (); + })); } return ( -
-

{ _t("Trust & Devices") }

-
- { devices.map((device, i) => ) } -
+
+
{deviceList}
+
{expandButton}
); }; @@ -1099,12 +1122,8 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room }; }, [cli, user.userId, isRoomEncrypted]); - let devicesSection; - if (isRoomEncrypted) { - devicesSection = ; - } else { - let text; - + let text; + if (!isRoomEncrypted) { if (!_enableDevices) { text = _t("This client does not support end-to-end encryption."); } else if (room) { @@ -1112,19 +1131,18 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room } else { // TODO what to render for GroupMember } - - if (text) { - devicesSection = ( -
-

{ _t("Trust & Devices") }

-
- { text } -
-
- ); - } + } else { + text = _t("Messages in this room are end-to-end encrypted."); } + const devicesSection = ( +
+

{ _t("Security") }

+

{ text }

+ +
+ ); + let e2eIcon; if (isRoomEncrypted && devices) { e2eIcon = ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fcf43af31f..9322e71b19 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1066,8 +1066,10 @@ "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", "Members": "Members", "Files": "Files", - "Trust & Devices": "Trust & Devices", "Direct messages": "Direct messages", + "Hide verified Sign-In's": "Hide verified Sign-In's", + "%(count)s verified Sign-In's|one": "1 verified Sign-In", + "%(count)s verified Sign-In's|other": "%(count)s verified Sign-In's", "Direct message": "Direct message", "Unverify user": "Unverify user", "Options": "Options", @@ -1079,6 +1081,8 @@ "Failed to deactivate user": "Failed to deactivate user", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", + "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", + "Security": "Security", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", From 04731d0ae331420123683ba6cbaa366ffd99c44b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 16:00:13 +0100 Subject: [PATCH 21/46] RoomState.events fired on RoomState object, not room --- src/components/views/right_panel/UserInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 1e59ec2c44..c1a6442409 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -346,7 +346,7 @@ const useRoomPowerLevels = (room) => { }; }, [room]); - useEventEmitter(room, "RoomState.events", update); + useEventEmitter(room.currentState, "RoomState.events", update); useEffect(() => { update(); return () => { From 73b6575082820216f0d62ff70d634224a3aa6ae2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 17:56:04 +0100 Subject: [PATCH 22/46] fixup: verified shield should be green --- res/css/views/rooms/_E2EIcon.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index c609d70f4c..c8b1be47f9 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -57,7 +57,7 @@ limitations under the License. .mx_E2EIcon_verified::after { mask-image: url('$(res)/img/e2e/verified.svg'); - background-color: $warning-color; + background-color: $accent-color; } From 0bd1e7112df59a3dabde0946c4926347d2c6779f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 17:59:22 +0100 Subject: [PATCH 23/46] style security section as per design --- res/css/views/right_panel/_UserInfo.scss | 325 ++++++++++--------- src/components/views/right_panel/UserInfo.js | 53 ++- src/i18n/strings/en_EN.json | 5 +- 3 files changed, 213 insertions(+), 170 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 07b8ed2879..79211bb38a 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -20,175 +20,182 @@ limitations under the License. flex-direction: column; flex: 1; overflow-y: auto; -} - -.mx_UserInfo_cancel { - height: 16px; - width: 16px; - padding: 10px 0 10px 10px; - cursor: pointer; - mask-image: url('$(res)/img/minimise.svg'); - mask-repeat: no-repeat; - mask-position: 16px center; - background-color: $rightpanel-button-color; - position: absolute; -} - -.mx_UserInfo h2 { - font-size: 18px; - font-weight: 600; - margin: 18px 0 0 0; -} - -.mx_UserInfo_container { - padding: 0 16px 16px 16px; - border-bottom: 1px solid lightgray; -} - -.mx_UserInfo_memberDetailsContainer { - padding-bottom: 0; -} - -.mx_UserInfo .mx_RoomTile_nameContainer { - width: 154px; -} - -.mx_UserInfo .mx_RoomTile_badge { - display: none; -} - -.mx_UserInfo .mx_RoomTile_name { - width: 160px; -} - -.mx_UserInfo_avatar { - margin: 24px 32px 0 32px; - cursor: pointer; -} - -.mx_UserInfo_avatar > div { - max-width: 30vh; - margin: 0 auto; -} - -.mx_UserInfo_avatar > div > div { - /* use padding-top instead of height to make this element square, - as the % in padding is a % of the width (including margin, - that's why we had to put the margin to center on a parent div), - and not a % of the parent height. */ - padding-top: 100%; - height: 0; - border-radius: 100%; - max-height: 30vh; - box-sizing: content-box; - background-repeat: no-repeat; - background-size: cover; - background-position: center; -} - -.mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { - cursor: zoom-in; -} - -.mx_UserInfo h3 { - text-transform: uppercase; - color: $input-darker-fg-color; - font-weight: bold; font-size: 12px; - margin: 4px 0; -} -.mx_UserInfo_profile { - font-size: 12px; - text-align: center; + .mx_UserInfo_cancel { + height: 16px; + width: 16px; + padding: 10px 0 10px 10px; + cursor: pointer; + mask-image: url('$(res)/img/minimise.svg'); + mask-repeat: no-repeat; + mask-position: 16px center; + background-color: $rightpanel-button-color; + position: absolute; + } h2 { - flex: 1; - overflow-x: auto; - max-height: 50px; - display: flex; - justify-self: ; - justify-content: center; - align-items: center; + font-size: 18px; + font-weight: 600; + margin: 18px 0 0 0; + } - .mx_E2EIcon { - margin: 5px; + .mx_UserInfo_container { + padding: 0 16px 16px 16px; + border-bottom: 1px solid lightgray; + } + + .mx_UserInfo_memberDetailsContainer { + padding-bottom: 0; + } + + .mx_RoomTile_nameContainer { + width: 154px; + } + + .mx_RoomTile_badge { + display: none; + } + + .mx_RoomTile_name { + width: 160px; + } + + .mx_UserInfo_avatar { + margin: 24px 32px 0 32px; + cursor: pointer; + } + + .mx_UserInfo_avatar > div { + max-width: 30vh; + margin: 0 auto; + } + + .mx_UserInfo_avatar > div > div { + /* use padding-top instead of height to make this element square, + as the % in padding is a % of the width (including margin, + that's why we had to put the margin to center on a parent div), + and not a % of the parent height. */ + padding-top: 100%; + height: 0; + border-radius: 100%; + max-height: 30vh; + box-sizing: content-box; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + } + + .mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { + cursor: zoom-in; + } + + h3 { + text-transform: uppercase; + color: $notice-secondary-color; + font-weight: bold; + font-size: 12px; + margin: 4px 0; + } + + p { + margin: 5px 0; + } + + .mx_UserInfo_profile { + text-align: center; + + h2 { + font-size: 18px; + line-height: 25px; + flex: 1; + overflow-x: auto; + max-height: 50px; + display: flex; + justify-self: ; + justify-content: center; + align-items: center; + + .mx_E2EIcon { + margin: 5px; + } + } + + .mx_UserInfo_profileStatus { + margin-top: 12px; } } - .mx_UserInfo_profileStatus { - margin-top: 12px; + .mx_UserInfo_memberDetails { + text-align: center; } -} + .mx_UserInfo_field { + cursor: pointer; + color: $accent-color; + line-height: 16px; + margin: 8px 0; -.mx_UserInfo_memberDetails { - text-align: center; -} - -.mx_UserInfo_field { - cursor: pointer; - font-size: 15px; - color: $primary-fg-color; - margin-left: 8px; - line-height: 23px; -} - -.mx_UserInfo_createRoom { - cursor: pointer; - display: flex; - align-items: center; - padding: 0 8px; -} - -.mx_UserInfo_createRoom_label { - width: initial !important; - cursor: pointer; -} - -.mx_UserInfo_statusMessage { - font-size: 11px; - opacity: 0.5; - overflow: hidden; - white-space: nowrap; - text-overflow: clip; -} -.mx_UserInfo .mx_UserInfo_scrollContainer { - flex: 1; - padding-bottom: 16px; -} - -.mx_UserInfo .mx_UserInfo_scrollContainer .mx_UserInfo_container { - padding-top: 16px; - padding-bottom: 0; - border-bottom: none; -} - -.mx_UserInfo_container_header { - display: flex; -} - -.mx_UserInfo_container_header_right { - position: relative; - margin-left: auto; -} - -.mx_UserInfo_newDmButton { - background-color: $roomheader-addroom-bg-color; - border-radius: 10px; // 16/2 + 2 padding - height: 16px; - flex: 0 0 16px; - - &::before { - background-color: $roomheader-addroom-fg-color; - mask: url('$(res)/img/icons-room-add.svg'); - mask-repeat: no-repeat; - mask-position: center; - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; } + + .mx_UserInfo_statusMessage { + font-size: 11px; + opacity: 0.5; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; + } + + .mx_UserInfo_scrollContainer { + flex: 1; + padding-bottom: 16px; + } + + .mx_UserInfo_scrollContainer .mx_UserInfo_container { + padding-top: 16px; + padding-bottom: 0; + border-bottom: none; + + >:not(h3) { + margin-left: 8px; + } + } + + .mx_UserInfo_devices { + .mx_UserInfo_device { + display: flex; + + &.mx_UserInfo_device_verified { + .mx_UserInfo_device_trusted { + color: $accent-color; + } + } + &.mx_UserInfo_device_unverified { + .mx_UserInfo_device_trusted { + color: $warning-color; + } + } + + .mx_UserInfo_device_name { + flex: 1; + margin-right: 5px; + } + } + + // both for icon in expand button and device item + .mx_E2EIcon { + // don't squeeze + flex: 0 0 auto; + margin: 2px 5px 0 0; + width: 12px; + height: 12px; + } + + .mx_UserInfo_expand { + display: flex; + margin-top: 11px; + color: $accent-color; + } + } + } diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index c1a6442409..12a38c468e 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -110,8 +110,42 @@ function useIsEncrypted(cli, room) { return isEncrypted; } -const DevicesSection = ({devices, userId, loading}) => { - const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); +function verifyDevice(userId, device) { + const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); + Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, { + userId: userId, + device: device, + }); +} + +function DeviceItem({userId, device}) { + const classes = classNames("mx_UserInfo_device", { + mx_UserInfo_device_verified: device.isVerified(), + mx_UserInfo_device_unverified: !device.isVerified(), + }); + const iconClasses = classNames("mx_E2EIcon", { + mx_E2EIcon_verified: device.isVerified(), + mx_E2EIcon_warning: !device.isVerified(), + }); + + const onDeviceClick = () => { + if (!device.isVerified()) { + verifyDevice(userId, device); + } + }; + + const deviceName = device.ambiguous ? + (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" : + device.getDisplayName(); + const trustedLabel = device.isVerified() ? _t("Trusted") : _t("Not trusted"); + return ( +
+
{deviceName}
+
{trustedLabel}
+ ); +} + +function DevicesSection({devices, userId, loading}) { const Spinner = sdk.getComponent("elements.Spinner"); const [isExpanded, setExpanded] = useState(false); @@ -130,23 +164,24 @@ const DevicesSection = ({devices, userId, loading}) => { let expandButton; if (verifiedDevices.length) { if (isExpanded) { - expandButton = ( setExpanded(false)}> - {_t("Hide verified Sign-In's")} + expandButton = ( setExpanded(false)}> +
{_t("Hide verified Sign-In's")}
); } else { - expandButton = ( setExpanded(true)}> - {_t("%(count)s verified Sign-In's", {count: verifiedDevices.length})} + expandButton = ( setExpanded(true)}> +
+
{_t("%(count)s verified Sign-In's", {count: verifiedDevices.length})}
); } } let deviceList = unverifiedDevices.map((device, i) => { - return (); + return (); }); if (isExpanded) { const keyStart = unverifiedDevices.length; deviceList = deviceList.concat(verifiedDevices.map((device, i) => { - return (); + return (); })); } @@ -156,7 +191,7 @@ const DevicesSection = ({devices, userId, loading}) => {
{expandButton}
); -}; +} const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => { let ignoreButton = null; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9322e71b19..a7bcf29407 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1066,10 +1066,11 @@ "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", "Members": "Members", "Files": "Files", - "Direct messages": "Direct messages", + "Trusted": "Trusted", + "Not trusted": "Not trusted", "Hide verified Sign-In's": "Hide verified Sign-In's", - "%(count)s verified Sign-In's|one": "1 verified Sign-In", "%(count)s verified Sign-In's|other": "%(count)s verified Sign-In's", + "%(count)s verified Sign-In's|one": "1 verified Sign-In", "Direct message": "Direct message", "Unverify user": "Unverify user", "Options": "Options", From 030827f77d78eead4f9c613e93299ac4b7ad75eb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:00:35 +0100 Subject: [PATCH 24/46] mark destructive actions in red --- res/css/views/right_panel/_UserInfo.scss | 3 +++ src/components/views/right_panel/UserInfo.js | 24 +++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 79211bb38a..49f52d3387 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -136,6 +136,9 @@ limitations under the License. line-height: 16px; margin: 8px 0; + &.mx_UserInfo_destructive { + color: $warning-color; + } } .mx_UserInfo_statusMessage { diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 12a38c468e..0b8fb8782c 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -224,7 +224,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i }; ignoreButton = ( - + { isIgnored ? _t("Unignore") : _t("Ignore") } ); @@ -306,7 +306,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i let unverifyButton; if (devices && devices.some(device => device.isVerified())) { unverifyButton = ( - unverifyUser(cli, member.userId)} className="mx_UserInfo_field"> + unverifyUser(cli, member.userId)} className="mx_UserInfo_field mx_UserInfo_destructive"> { _t('Unverify user') } ); @@ -428,7 +428,7 @@ const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, start }; const kickLabel = member.membership === "invite" ? _t("Disinvite") : _t("Kick"); - return + return { kickLabel } ; }); @@ -501,7 +501,7 @@ const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member} } }; - return + return { _t("Remove recent messages") } ; }); @@ -553,7 +553,11 @@ const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, star label = _t("Unban"); } - return + const classes = classNames("mx_UserInfo_field", { + mx_UserInfo_destructive: member.membership !== 'ban', + }); + + return { label } ; }); @@ -610,8 +614,12 @@ const MuteToggleButton = withLegacyMatrixClient( } }; + const classes = classNames("mx_UserInfo_field", { + mx_UserInfo_destructive: !isMuted, + }); + const muteLabel = isMuted ? _t("Unmute") : _t("Mute"); - return + return { muteLabel } ; }, @@ -734,7 +742,7 @@ const GroupAdminToolsSection = withLegacyMatrixClient( }; const kickButton = ( - + { isInvited ? _t('Disinvite') : _t('Remove from community') } ); @@ -975,7 +983,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room // FIXME this should be using cli instead of MatrixClientPeg.matrixClient if (isSynapseAdmin && user.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) { synapseDeactivateButton = ( - + {_t("Deactivate user")} ); From 9e8a2eda1fd77c29fad4067bf3192464aa876069 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:00:57 +0100 Subject: [PATCH 25/46] small fixes --- src/components/views/right_panel/UserInfo.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 0b8fb8782c..52caa69fcf 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -40,7 +40,6 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; -import {ContentRepo} from 'matrix-js-sdk'; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -315,13 +314,13 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i return (

{ _t("Options") }

-
+
{ directMessageButton } { readReceiptButton } { shareUserButton } { insertPillButton } - { ignoreButton } { inviteUserButton } + { ignoreButton } { unverifyButton }
From ca12e6c0106942747f5923b625561788d7a1e9ac Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:37:25 +0100 Subject: [PATCH 26/46] don't render unverified state on bubbles as they are only used for verification right now, and verification events will be unverified by definition, so no need to alarm users needlessly. Also, this breaks the bubble layout on hover due to e2e icons and verified left border style. --- src/components/views/rooms/EventTile.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 22f1f914b6..5fcf1e4491 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -606,8 +606,8 @@ module.exports = createReactClass({ mx_EventTile_last: this.props.last, mx_EventTile_contextual: this.props.contextual, mx_EventTile_actionBarFocused: this.state.actionBarFocused, - mx_EventTile_verified: this.state.verified === true, - mx_EventTile_unverified: this.state.verified === false, + mx_EventTile_verified: !isBubbleMessage && this.state.verified === true, + mx_EventTile_unverified: !isBubbleMessage && this.state.verified === false, mx_EventTile_bad: isEncryptionFailure, mx_EventTile_emote: msgtype === 'm.emote', mx_EventTile_redacted: isRedacted, @@ -800,7 +800,7 @@ module.exports = createReactClass({ { timestamp } - { this._renderE2EPadlock() } + { !isBubbleMessage && this._renderE2EPadlock() } { thread } { sender } -
+
{ timestamp } - { this._renderE2EPadlock() } + { !isBubbleMessage && this._renderE2EPadlock() } { thread } Date: Wed, 13 Nov 2019 18:38:55 +0100 Subject: [PATCH 27/46] don't need this, as it takes height from the constrained width --- res/css/views/right_panel/_UserInfo.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 49f52d3387..a5dae148f4 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -79,7 +79,6 @@ limitations under the License. padding-top: 100%; height: 0; border-radius: 100%; - max-height: 30vh; box-sizing: content-box; background-repeat: no-repeat; background-size: cover; From e3f7fe51dc6be1768d76c707ef95b7dd71ab795c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:45:12 +0100 Subject: [PATCH 28/46] use normal shield for verification requests --- res/css/views/messages/_MKeyVerificationRequest.scss | 3 ++- res/img/e2e/normal.svg | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 res/img/e2e/normal.svg diff --git a/res/css/views/messages/_MKeyVerificationRequest.scss b/res/css/views/messages/_MKeyVerificationRequest.scss index aff44e4109..b4cde4e7ef 100644 --- a/res/css/views/messages/_MKeyVerificationRequest.scss +++ b/res/css/views/messages/_MKeyVerificationRequest.scss @@ -25,7 +25,7 @@ limitations under the License. width: 12px; height: 16px; content: ""; - mask: url("$(res)/img/e2e/verified.svg"); + mask: url("$(res)/img/e2e/normal.svg"); mask-repeat: no-repeat; mask-size: 100%; margin-top: 4px; @@ -33,6 +33,7 @@ limitations under the License. } &.mx_KeyVerification_icon_verified::after { + mask: url("$(res)/img/e2e/verified.svg"); background-color: $accent-color; } diff --git a/res/img/e2e/normal.svg b/res/img/e2e/normal.svg new file mode 100644 index 0000000000..5b848bc27f --- /dev/null +++ b/res/img/e2e/normal.svg @@ -0,0 +1,3 @@ + + + From 942a1c9a56dcef794db5d8c2ce4e9650a551d035 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 13 Nov 2019 18:55:11 +0100 Subject: [PATCH 29/46] fix e2e icon in composer having wrong colors --- res/css/views/rooms/_MessageComposer.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index e9f33183f5..14562fe7ed 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -78,7 +78,10 @@ limitations under the License. .mx_MessageComposer_e2eIcon.mx_E2EIcon { position: absolute; left: 60px; - background-color: $composer-e2e-icon-color; + + &::after { + background-color: $composer-e2e-icon-color; + } } .mx_MessageComposer_noperm_error { From b278531f2fbabfdee31bd46434f30f9e462e56ea Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 14 Nov 2019 16:58:07 +0100 Subject: [PATCH 30/46] add IconButton as in design --- res/css/_components.scss | 1 + res/css/views/elements/_IconButton.scss | 55 +++++++++++++++++++++ res/img/feather-customised/edit.svg | 4 ++ src/components/views/elements/IconButton.js | 34 +++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 res/css/views/elements/_IconButton.scss create mode 100644 res/img/feather-customised/edit.svg create mode 100644 src/components/views/elements/IconButton.js diff --git a/res/css/_components.scss b/res/css/_components.scss index c8ea237dcd..40a2c576d0 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -90,6 +90,7 @@ @import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_Field.scss"; +@import "./views/elements/_IconButton.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_InteractiveTooltip.scss"; diff --git a/res/css/views/elements/_IconButton.scss b/res/css/views/elements/_IconButton.scss new file mode 100644 index 0000000000..d8ebbeb65e --- /dev/null +++ b/res/css/views/elements/_IconButton.scss @@ -0,0 +1,55 @@ +/* +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_IconButton { + width: 32px; + height: 32px; + border-radius: 100%; + background-color: $accent-bg-color; + // don't shrink or grow if in a flex container + flex: 0 0 auto; + + &.mx_AccessibleButton_disabled { + background-color: none; + + &::before { + background-color: lightgrey; + } + } + + &:hover { + opacity: 90%; + } + + &::before { + content: ""; + display: block; + width: 100%; + height: 100%; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 55%; + background-color: $accent-color; + } + + &.mx_IconButton_icon_check::before { + mask-image: url('$(res)/img/feather-customised/check.svg'); + } + + &.mx_IconButton_icon_edit::before { + mask-image: url('$(res)/img/feather-customised/edit.svg'); + } +} diff --git a/res/img/feather-customised/edit.svg b/res/img/feather-customised/edit.svg new file mode 100644 index 0000000000..f511aa1477 --- /dev/null +++ b/res/img/feather-customised/edit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/views/elements/IconButton.js b/src/components/views/elements/IconButton.js new file mode 100644 index 0000000000..9f5bf77426 --- /dev/null +++ b/src/components/views/elements/IconButton.js @@ -0,0 +1,34 @@ +/* + Copyright 2016 Jani Mustonen + + 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 AccessibleButton from "./AccessibleButton"; + +export default function IconButton(props) { + const {icon, className, ...restProps} = props; + + let newClassName = (className || "") + " mx_IconButton"; + newClassName = newClassName + " mx_IconButton_icon_" + icon; + + const allProps = Object.assign({}, restProps, {className: newClassName}); + + return React.createElement(AccessibleButton, allProps); +} + +IconButton.propTypes = Object.assign({ + icon: PropTypes.string, +}, AccessibleButton.propTypes); From d0914f9208f44e624eb91bff5bc654c4a9f25bfe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 14 Nov 2019 16:58:37 +0100 Subject: [PATCH 31/46] allow label to be empty on power selector --- src/components/views/elements/PowerSelector.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index 5bc8eeba58..e6babded32 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -129,10 +129,11 @@ module.exports = createReactClass({ render: function() { let picker; + const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label; if (this.state.custom) { picker = ( ); @@ -151,7 +152,7 @@ module.exports = createReactClass({ picker = ( {options} From 91e02aa623e91f1d09d8cea2ad447f8cd5222dd0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 14 Nov 2019 16:58:56 +0100 Subject: [PATCH 32/46] hide PL numbers on labels --- src/Roles.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Roles.js b/src/Roles.js index 10c4ceaf1e..4c0d2ab4e6 100644 --- a/src/Roles.js +++ b/src/Roles.js @@ -28,8 +28,8 @@ export function levelRoleMap(usersDefault) { export function textualPowerLevel(level, usersDefault) { const LEVEL_ROLE_MAP = levelRoleMap(usersDefault); if (LEVEL_ROLE_MAP[level]) { - return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`); + return LEVEL_ROLE_MAP[level]; } else { - return level; + return _t("Custom %(level)s", {level}); } } From 6db162a3a7a5a13e99d4e31b43091c25831a0223 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 14 Nov 2019 16:59:09 +0100 Subject: [PATCH 33/46] add PL edit mode, don't show selector by default still saves when changing the selector though --- res/css/views/right_panel/_UserInfo.scss | 30 ++- src/components/views/right_panel/UserInfo.js | 192 +++++++++++-------- src/i18n/strings/en_EN.json | 2 + 3 files changed, 141 insertions(+), 83 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index a5dae148f4..9e4d4dc471 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -125,8 +125,34 @@ limitations under the License. } } - .mx_UserInfo_memberDetails { - text-align: center; + .mx_UserInfo_memberDetails .mx_UserInfo_profileField { + display: flex; + justify-content: center; + align-items: center; + + margin: 6px 0; + + .mx_IconButton { + margin-left: 6px; + width: 16px; + height: 16px; + + &::before { + mask-size: 80%; + } + } + + .mx_UserInfo_roleDescription { + display: flex; + justify-content: center; + align-items: center; + // try to make it the same height as the dropdown + margin: 11px 0 12px 0; + } + + .mx_Field { + margin: 0; + } } .mx_UserInfo_field { diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 52caa69fcf..379292c152 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -27,7 +27,6 @@ import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import createRoom from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; -import Unread from '../../../Unread'; import AccessibleButton from '../elements/AccessibleButton'; import SdkConfig from '../../../SdkConfig'; import SettingsStore from "../../../settings/SettingsStore"; @@ -40,6 +39,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import {textualPowerLevel} from '../../../Roles'; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -780,43 +780,11 @@ const useIsSynapseAdmin = (cli) => { return isAdmin; }; -// 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 - const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]); - - // only display the devices list if our client supports E2E - const _enableDevices = cli.isCryptoEnabled(); - - // Load whether or not we are a Synapse Admin - const isSynapseAdmin = useIsSynapseAdmin(cli); - - // Check whether the user is ignored - const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId)); - // Recheck if the user or client changes - useEffect(() => { - setIsIgnored(cli.isUserIgnored(user.userId)); - }, [cli, user.userId]); - // Recheck also if we receive new accountData m.ignored_user_list - const accountDataHandler = useCallback((ev) => { - if (ev.getType() === "m.ignored_user_list") { - setIsIgnored(cli.isUserIgnored(user.userId)); - } - }, [cli, user.userId]); - useEventEmitter(cli, "accountData", accountDataHandler); - - // Count of how many operations are currently in progress, if > 0 then show a Spinner - const [pendingUpdateCount, setPendingUpdateCount] = useState(0); - const startUpdating = useCallback(() => { - setPendingUpdateCount(pendingUpdateCount + 1); - }, [pendingUpdateCount]); - const stopUpdating = useCallback(() => { - setPendingUpdateCount(pendingUpdateCount - 1); - }, [pendingUpdateCount]); - +function useRoomPermissions(cli, room, user) { const [roomPermissions, setRoomPermissions] = useState({ // modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL modifyLevelMax: -1, + canAffectUser: false, canInvite: false, }); const updateRoomPermissions = useCallback(async () => { @@ -847,6 +815,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room setRoomPermissions({ canInvite: me.powerLevel >= powerLevels.invite, + canAffectUser, modifyLevelMax, }); }, [cli, user, room]); @@ -856,38 +825,16 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room return () => { setRoomPermissions({ maximalPowerLevel: -1, + canAffectUser: false, canInvite: false, }); }; }, [updateRoomPermissions]); - const onSynapseDeactivate = useCallback(async () => { - const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog'); - const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, { - title: _t("Deactivate user?"), - description: -
{ _t( - "Deactivating this user will log them out and prevent them from logging back in. Additionally, " + - "they will leave all the rooms they are in. This action cannot be reversed. Are you sure you " + - "want to deactivate this user?", - ) }
, - button: _t("Deactivate user"), - danger: true, - }); - - const [accepted] = await finished; - if (!accepted) return; - try { - cli.deactivateSynapseUser(user.userId); - } catch (err) { - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createTrackedDialog('Failed to deactivate user', '', ErrorDialog, { - title: _t('Failed to deactivate user'), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - } - }, [cli, user.userId]); + return roomPermissions; +} +const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, startUpdating, stopUpdating, roomPermissions}) => { const onPowerChange = useCallback(async (powerLevel) => { const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { startUpdating(); @@ -953,6 +900,104 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); }, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line + + const [isEditingPL, setEditingPL] = useState(false); + if (room && user.roomId) { // is in room + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; + const powerLevel = parseInt(user.powerLevel); + const IconButton = sdk.getComponent('elements.IconButton'); + if (isEditingPL) { + const PowerSelector = sdk.getComponent('elements.PowerSelector'); + return ( +
+ + setEditingPL(false)} /> +
+ ); + } else { + const modifyButton = roomPermissions.canAffectUser ? + ( setEditingPL(true)} />) : null; + const role = textualPowerLevel(powerLevel, powerLevelUsersDefault); + const label = _t("%(role)s in %(roomName)s", + {role, roomName: room.name}, + {strong: label => {label}}, + ); + return (
{label}{modifyButton}
); + } + } +}); + +// 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 + const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]); + + // only display the devices list if our client supports E2E + const _enableDevices = cli.isCryptoEnabled(); + + // Load whether or not we are a Synapse Admin + const isSynapseAdmin = useIsSynapseAdmin(cli); + + // Check whether the user is ignored + const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId)); + // Recheck if the user or client changes + useEffect(() => { + setIsIgnored(cli.isUserIgnored(user.userId)); + }, [cli, user.userId]); + // Recheck also if we receive new accountData m.ignored_user_list + const accountDataHandler = useCallback((ev) => { + if (ev.getType() === "m.ignored_user_list") { + setIsIgnored(cli.isUserIgnored(user.userId)); + } + }, [cli, user.userId]); + useEventEmitter(cli, "accountData", accountDataHandler); + + // Count of how many operations are currently in progress, if > 0 then show a Spinner + const [pendingUpdateCount, setPendingUpdateCount] = useState(0); + const startUpdating = useCallback(() => { + setPendingUpdateCount(pendingUpdateCount + 1); + }, [pendingUpdateCount]); + const stopUpdating = useCallback(() => { + setPendingUpdateCount(pendingUpdateCount - 1); + }, [pendingUpdateCount]); + + const roomPermissions = useRoomPermissions(cli, room, user); + + const onSynapseDeactivate = useCallback(async () => { + const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog'); + const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, { + title: _t("Deactivate user?"), + description: +
{ _t( + "Deactivating this user will log them out and prevent them from logging back in. Additionally, " + + "they will leave all the rooms they are in. This action cannot be reversed. Are you sure you " + + "want to deactivate this user?", + ) }
, + button: _t("Deactivate user"), + danger: true, + }); + + const [accepted] = await finished; + if (!accepted) return; + try { + cli.deactivateSynapseUser(user.userId); + } catch (err) { + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('Failed to deactivate user', '', ErrorDialog, { + title: _t('Failed to deactivate user'), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } + }, [cli, user.userId]); + + const onMemberAvatarKey = e => { if (e.key === "Enter") { onMemberAvatarClick(); @@ -1058,26 +1103,6 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room statusLabel = { statusMessage }; } - let memberDetails = null; - - if (room && user.roomId) { // is in room - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; - - const PowerSelector = sdk.getComponent('elements.PowerSelector'); - memberDetails =
-
- -
- -
; - } - const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl; let avatarElement; if (avatarUrl) { @@ -1102,6 +1127,11 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room title={_t('Close')} />; } + const memberDetails = ; + const isRoomEncrypted = useIsEncrypted(cli, room); // undefined means yet to be loaded, null means failed to load, otherwise list of devices const [devices, setDevices] = useState(undefined); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a7bcf29407..46ad7d5135 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -118,6 +118,7 @@ "Restricted": "Restricted", "Moderator": "Moderator", "Admin": "Admin", + "Custom %(level)s": "Custom %(level)s", "Start a chat": "Start a chat", "Who would you like to communicate with?": "Who would you like to communicate with?", "Email, name or Matrix ID": "Email, name or Matrix ID", @@ -1080,6 +1081,7 @@ "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", "Failed to deactivate user": "Failed to deactivate user", + "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", From bd853b3102a3da670a705aae338b11cf4b4da9f8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 12:10:56 +0100 Subject: [PATCH 34/46] listen for RoomState.members instead of RoomState.events as the powerlevel on the member is not yet updated at the time RoomState.events is emitted. Also listen on the client for this event as the currentState object can change when the timeline is reset. --- src/components/views/right_panel/UserInfo.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 379292c152..8931b3ed1f 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -365,10 +365,13 @@ const _isMuted = (member, powerLevelContent) => { return member.powerLevel < levelToSend; }; -const useRoomPowerLevels = (room) => { +const useRoomPowerLevels = (cli, room) => { const [powerLevels, setPowerLevels] = useState({}); const update = useCallback(() => { + if (!room) { + return; + } const event = room.currentState.getStateEvents("m.room.power_levels", ""); if (event) { setPowerLevels(event.getContent()); @@ -380,7 +383,7 @@ const useRoomPowerLevels = (room) => { }; }, [room]); - useEventEmitter(room.currentState, "RoomState.events", update); + useEventEmitter(cli, "RoomState.members", update); useEffect(() => { update(); return () => { @@ -819,7 +822,7 @@ function useRoomPermissions(cli, room, user) { modifyLevelMax, }); }, [cli, user, room]); - useEventEmitter(cli, "RoomState.events", updateRoomPermissions); + useEventEmitter(cli, "RoomState.members", updateRoomPermissions); useEffect(() => { updateRoomPermissions(); return () => { From e86ceb986fc913e958c6eb72cb0f237b86ebeb07 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:13:22 +0100 Subject: [PATCH 35/46] pass powerlevels state to power level section and admin section --- src/components/views/right_panel/UserInfo.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 8931b3ed1f..a0a256e4c8 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -628,13 +628,12 @@ const MuteToggleButton = withLegacyMatrixClient( ); const RoomAdminToolsContainer = withLegacyMatrixClient( - ({matrixClient: cli, room, children, member, startUpdating, stopUpdating}) => { + ({matrixClient: cli, room, children, member, startUpdating, stopUpdating, powerLevels}) => { let kickButton; let banButton; let muteButton; let redactButton; - const powerLevels = useRoomPowerLevels(room); const editPowerLevel = ( (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || powerLevels.state_default @@ -837,8 +836,7 @@ function useRoomPermissions(cli, room, user) { return roomPermissions; } -const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, startUpdating, stopUpdating, roomPermissions}) => { - const onPowerChange = useCallback(async (powerLevel) => { +const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => { const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { startUpdating(); cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( @@ -945,6 +943,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room // only display the devices list if our client supports E2E const _enableDevices = cli.isCryptoEnabled(); + const powerLevels = useRoomPowerLevels(cli, room); // Load whether or not we are a Synapse Admin const isSynapseAdmin = useIsSynapseAdmin(cli); @@ -1040,6 +1039,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room if (room && user.roomId) { adminToolsContainer = ( ; } - const memberDetails = ; const isRoomEncrypted = useIsEncrypted(cli, room); From 48b1207c6ed2ea07d764f96db5429c2a6c72f24d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:16:23 +0100 Subject: [PATCH 36/46] split up PowerLevelEditor into two components one while editing (PowerLevelEditor) and one while not editing (PowerLevelSection). Also save when pressing the button, not when changing the power dropdown. Also show the spinner next to the dropdown when saving, not at the bottom of the component. --- res/css/views/right_panel/_UserInfo.scss | 8 +- src/Roles.js | 2 +- src/components/views/right_panel/UserInfo.js | 190 +++++++++++-------- src/i18n/strings/en_EN.json | 4 +- 4 files changed, 122 insertions(+), 82 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 9e4d4dc471..2b2add49ee 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -132,8 +132,8 @@ limitations under the License. margin: 6px 0; - .mx_IconButton { - margin-left: 6px; + .mx_IconButton, .mx_Spinner { + margin-left: 20px; width: 16px; height: 16px; @@ -148,6 +148,10 @@ limitations under the License. align-items: center; // try to make it the same height as the dropdown margin: 11px 0 12px 0; + + .mx_IconButton { + margin-left: 6px; + } } .mx_Field { diff --git a/src/Roles.js b/src/Roles.js index 4c0d2ab4e6..7cc3c880d7 100644 --- a/src/Roles.js +++ b/src/Roles.js @@ -30,6 +30,6 @@ export function textualPowerLevel(level, usersDefault) { if (LEVEL_ROLE_MAP[level]) { return LEVEL_ROLE_MAP[level]; } else { - return _t("Custom %(level)s", {level}); + return _t("Custom (%(level)s)", {level}); } } diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index a0a256e4c8..589eca9a08 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -837,9 +837,46 @@ function useRoomPermissions(cli, room, user) { } const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => { + const [isEditing, setEditing] = useState(false); + if (room && user.roomId) { // is in room + if (isEditing) { + return ( setEditing(false)} />); + } else { + const IconButton = sdk.getComponent('elements.IconButton'); + const powerLevelUsersDefault = powerLevels.users_default || 0; + const powerLevel = parseInt(user.powerLevel, 10); + const modifyButton = roomPermissions.canEdit ? + ( setEditing(true)} />) : null; + const role = textualPowerLevel(powerLevel, powerLevelUsersDefault); + const label = _t("%(role)s in %(roomName)s", + {role, roomName: room.name}, + {strong: label => {label}}, + ); + return ( +
+
{label}{modifyButton}
+
+ ); + } + } else { + return null; + } +}); + +const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, onFinished}) => { + const [isUpdating, setIsUpdating] = useState(false); + const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10)); + const [isDirty, setIsDirty] = useState(false); + const onPowerChange = useCallback((powerLevel) => { + setIsDirty(true); + setSelectedPowerLevel(parseInt(powerLevel, 10)); + }, [setSelectedPowerLevel, setIsDirty]); + + const changePowerLevel = useCallback(async () => { const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { - startUpdating(); - cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( + return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -852,87 +889,86 @@ const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room description: _t("Failed to change power level"), }); }, - ).finally(() => { - stopUpdating(); - }).done(); + ); }; - const roomId = user.roomId; - const target = user.userId; - - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevelEvent) return; - - if (!powerLevelEvent.getContent().users) { - _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - return; - } - - const myUserId = cli.getUserId(); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - - // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. - if (myUserId === target) { - try { - if (!(await _warnSelfDemote())) return; - _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - } catch (e) { - console.error("Failed to warn about self demotion: ", e); + try { + if (!isDirty) { + return; } - return; + + setIsUpdating(true); + + const powerLevel = selectedPowerLevel; + + const roomId = user.roomId; + const target = user.userId; + + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (!powerLevelEvent) return; + + if (!powerLevelEvent.getContent().users) { + _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + return; + } + + const myUserId = cli.getUserId(); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. + if (myUserId === target) { + try { + if (!(await _warnSelfDemote())) return; + } catch (e) { + console.error("Failed to warn about self demotion: ", e); + } + await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + return; + } + + const myPower = powerLevelEvent.getContent().users[myUserId]; + if (parseInt(myPower) === parseInt(powerLevel)) { + const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, { + title: _t("Warning!"), + description: +
+ { _t("You will not be able to undo this change as you are promoting the user " + + "to have the same power level as yourself.") }
+ { _t("Are you sure?") } +
, + button: _t("Continue"), + }); + + const [confirmed] = await finished; + if (confirmed) return; + } + await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + } finally { + onFinished(); } + }, [user.roomId, user.userId, cli, selectedPowerLevel, isDirty, setIsUpdating, onFinished, room]); - const myPower = powerLevelEvent.getContent().users[myUserId]; - if (parseInt(myPower) === parseInt(powerLevel)) { - const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, { - title: _t("Warning!"), - description: -
- { _t("You will not be able to undo this change as you are promoting the user " + - "to have the same power level as yourself.") }
- { _t("Are you sure?") } -
, - button: _t("Continue"), - }); + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; + const IconButton = sdk.getComponent('elements.IconButton'); + const Spinner = sdk.getComponent("elements.Spinner"); + const buttonOrSpinner = isUpdating ? : + ; - const [confirmed] = await finished; - if (confirmed) return; - } - _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - }, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line - - - const [isEditingPL, setEditingPL] = useState(false); - if (room && user.roomId) { // is in room - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; - const powerLevel = parseInt(user.powerLevel); - const IconButton = sdk.getComponent('elements.IconButton'); - if (isEditingPL) { - const PowerSelector = sdk.getComponent('elements.PowerSelector'); - return ( -
- - setEditingPL(false)} /> -
- ); - } else { - const modifyButton = roomPermissions.canAffectUser ? - ( setEditingPL(true)} />) : null; - const role = textualPowerLevel(powerLevel, powerLevelUsersDefault); - const label = _t("%(role)s in %(roomName)s", - {role, roomName: room.name}, - {strong: label => {label}}, - ); - return (
{label}{modifyButton}
); - } - } + const PowerSelector = sdk.getComponent('elements.PowerSelector'); + return ( +
+ + {buttonOrSpinner} +
+ ); }); // cli is injected by withLegacyMatrixClient diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 46ad7d5135..029000e9d2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -118,7 +118,7 @@ "Restricted": "Restricted", "Moderator": "Moderator", "Admin": "Admin", - "Custom %(level)s": "Custom %(level)s", + "Custom (%(level)s)": "Custom (%(level)s)", "Start a chat": "Start a chat", "Who would you like to communicate with?": "Who would you like to communicate with?", "Email, name or Matrix ID": "Email, name or Matrix ID", @@ -1080,8 +1080,8 @@ "Remove this user from community?": "Remove this user from community?", "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", - "Failed to deactivate user": "Failed to deactivate user", "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", + "Failed to deactivate user": "Failed to deactivate user", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", From 264c8181c2f6c6b52d673e16bbf76708f06c3f30 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:18:04 +0100 Subject: [PATCH 37/46] add canEdit to permission state, more explicit than maxLevel >= 0 --- src/components/views/right_panel/UserInfo.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 589eca9a08..681336df7c 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -786,7 +786,7 @@ function useRoomPermissions(cli, room, user) { const [roomPermissions, setRoomPermissions] = useState({ // modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL modifyLevelMax: -1, - canAffectUser: false, + canEdit: false, canInvite: false, }); const updateRoomPermissions = useCallback(async () => { @@ -817,7 +817,7 @@ function useRoomPermissions(cli, room, user) { setRoomPermissions({ canInvite: me.powerLevel >= powerLevels.invite, - canAffectUser, + canEdit: modifyLevelMax >= 0, modifyLevelMax, }); }, [cli, user, room]); @@ -827,7 +827,7 @@ function useRoomPermissions(cli, room, user) { return () => { setRoomPermissions({ maximalPowerLevel: -1, - canAffectUser: false, + canEdit: false, canInvite: false, }); }; From 92237f10452eb77b077f3e49373a497791b6bb7e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:18:36 +0100 Subject: [PATCH 38/46] cleanup --- src/components/views/right_panel/UserInfo.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 681336df7c..a194b89eee 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -789,8 +789,10 @@ function useRoomPermissions(cli, room, user) { canEdit: false, canInvite: false, }); - const updateRoomPermissions = useCallback(async () => { - if (!room) return; + const updateRoomPermissions = useCallback(() => { + if (!room) { + return; + } const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); if (!powerLevelEvent) return; From 53019c5e9135b2ad0bc45b23438407fff2763272 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:19:16 +0100 Subject: [PATCH 39/46] don't show devices section when not encrypted, as it just shows spinner --- src/components/views/right_panel/UserInfo.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index a194b89eee..88cc41766d 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1248,11 +1248,13 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room text = _t("Messages in this room are end-to-end encrypted."); } - const devicesSection = ( + const devicesSection = isRoomEncrypted ? + () : null; + const securitySection = (

{ _t("Security") }

{ text }

- + { devicesSection }
); @@ -1289,7 +1291,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
} - { devicesSection } + { securitySection } Date: Fri, 15 Nov 2019 15:23:23 +0100 Subject: [PATCH 40/46] prevent https://github.com/vector-im/riot-web/issues/11338 --- src/components/views/right_panel/UserInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 88cc41766d..7355153ec7 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1115,7 +1115,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room let presenceCurrentlyActive; let statusMessage; - if (user instanceof RoomMember) { + if (user instanceof RoomMember && user.user) { presenceState = user.user.presence; presenceLastActiveAgo = user.user.lastActiveAgo; presenceCurrentlyActive = user.user.currentlyActive; From 1162d1ee43862b994245641f118c2d2bade438e2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:29:23 +0100 Subject: [PATCH 41/46] Fix user info scroll container growing larger than available height making whole room view grow taller than viewport --- res/css/views/right_panel/_UserInfo.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 2b2add49ee..c5c0d9f42f 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -179,7 +179,7 @@ limitations under the License. } .mx_UserInfo_scrollContainer { - flex: 1; + flex: 1 1 0; padding-bottom: 16px; } From edd5d3c9150679a5b7c0b08e06da3a886ac16a80 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:37:43 +0100 Subject: [PATCH 42/46] make custom power level not grow too wide --- res/css/views/elements/_Field.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index 4d012a136e..b260d4b097 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -49,6 +49,7 @@ limitations under the License. color: $primary-fg-color; background-color: $primary-bg-color; flex: 1; + min-width: 0; } .mx_Field select { From ecc842629a989ff824d8114c22acd73affd4f243 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 15:48:04 +0100 Subject: [PATCH 43/46] fix css lint errors --- res/css/views/right_panel/_UserInfo.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index c5c0d9f42f..7fda114a79 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -111,7 +111,6 @@ limitations under the License. overflow-x: auto; max-height: 50px; display: flex; - justify-self: ; justify-content: center; align-items: center; @@ -188,7 +187,7 @@ limitations under the License. padding-bottom: 0; border-bottom: none; - >:not(h3) { + > :not(h3) { margin-left: 8px; } } @@ -229,5 +228,4 @@ limitations under the License. color: $accent-color; } } - } From d416ba2c0ceea8f7e8eca9c0f871552ec8a6220c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 16:04:00 +0100 Subject: [PATCH 44/46] add verify button while we don't have the verification in right panel --- res/css/views/right_panel/_UserInfo.scss | 10 ++++++++++ src/components/views/right_panel/UserInfo.js | 1 + src/i18n/strings/en_EN.json | 1 + 3 files changed, 12 insertions(+) diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 7fda114a79..c68f3ffd37 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -228,4 +228,14 @@ limitations under the License. color: $accent-color; } } + + .mx_UserInfo_verify { + display: block; + background-color: $accent-color; + color: $accent-fg-color; + border-radius: 4px; + padding: 7px 1.5em; + text-align: center; + margin: 16px 0; + } } diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 7355153ec7..53a87ed1c6 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1254,6 +1254,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room

{ _t("Security") }

{ text }

+ verifyDevice(user.userId, null)}>{_t("Verify")} { devicesSection }
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 029000e9d2..a90af471c2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1086,6 +1086,7 @@ "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", "Security": "Security", + "Verify": "Verify", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", From 0f39a9f72dd20f4e4dfa372b9356c0f468fbfcbd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 15 Nov 2019 17:29:38 +0100 Subject: [PATCH 45/46] fix pr feedback --- res/css/views/rooms/_E2EIcon.scss | 4 ++-- src/components/views/elements/IconButton.js | 22 ++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index c8b1be47f9..bc11ac6e1c 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -25,9 +25,9 @@ limitations under the License. .mx_E2EIcon_verified::before, .mx_E2EIcon_warning::before { content: ""; display: block; - /* the symbols in the shield icons are cut out the make the themeable with css masking. + /* the symbols in the shield icons are cut out to make it themeable with css masking. if they appear on a different background than white, the symbol wouldn't be white though, so we - add a rectangle here below the masked element to shine through the symbol cutout. + add a rectangle here below the masked element to shine through the symbol cut-out. hardcoding white and not using a theme variable as this would probably be white for any theme. */ background-color: white; position: absolute; diff --git a/src/components/views/elements/IconButton.js b/src/components/views/elements/IconButton.js index 9f5bf77426..ef7b4a8399 100644 --- a/src/components/views/elements/IconButton.js +++ b/src/components/views/elements/IconButton.js @@ -1,18 +1,18 @@ /* - Copyright 2016 Jani Mustonen +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 +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 +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. - */ +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'; From 61454bcf32d6d547bbcce8b3466778f87cf30c24 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 15 Nov 2019 10:25:58 -0700 Subject: [PATCH 46/46] Fix i18n after merge --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f0ff0275f7..655c7030c4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1082,7 +1082,6 @@ "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", - "Failed to deactivate user": "Failed to deactivate user", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.",