From af4ad488bdf753edb514e13b88957a2ef103dcda Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 11 Nov 2019 16:54:12 +0100 Subject: [PATCH 01/37] 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 02/37] 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 03/37] 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 04/37] 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 05/37] 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 06/37] 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 07/37] 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 08/37] 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 09/37] 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 10/37] 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 11/37] 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 12/37] 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 13/37] 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 14/37] 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 15/37] 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 16/37] 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 17/37] 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 18/37] 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 19/37] 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 20/37] 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 21/37] 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 22/37] 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 23/37] 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 24/37] 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 25/37] 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 26/37] 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 27/37] 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 28/37] 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 29/37] 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 30/37] 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 31/37] 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 32/37] 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 33/37] 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 34/37] 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 35/37] 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 36/37] 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 37/37] 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';