diff --git a/TODO.md b/TODO.md index 95fe9cf9..9003ed6a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,11 @@ # TODO -- issue templates +- trust/blacklist buttons +- reload devices when needed +- get devices for members with no shared E2E room? +- keyboard controls +- remove useless Base imports in Base components +- HTile enter trigger leftClicked() ## Refactoring diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py index b4b7c8c6..270bc464 100644 --- a/src/backend/matrix_client.py +++ b/src/backend/matrix_client.py @@ -1286,10 +1286,10 @@ class MatrixClient(nio.AsyncClient): if device_id == self.device_id: return "current" - if device_id not in self.olm.device_store[self.user_id]: + if device_id not in self.device_store[self.user_id]: return "no_keys" - trust = self.olm.device_store[self.user_id][device_id].trust_state + trust = self.device_store[self.user_id][device_id].trust_state return trust.name def get_ed25519(device_id: str) -> str: @@ -1297,8 +1297,8 @@ class MatrixClient(nio.AsyncClient): if device_id == self.device_id: key = self.olm.account.identity_keys["ed25519"] - elif device_id in self.olm.device_store[self.user_id]: - key = self.olm.device_store[self.user_id][device_id].ed25519 + elif device_id in self.device_store[self.user_id]: + key = self.device_store[self.user_id][device_id].ed25519 return " ".join(textwrap.wrap(key, 4)) @@ -1333,6 +1333,32 @@ class MatrixClient(nio.AsyncClient): ) + async def member_devices(self, user_id: str) -> List[Dict[str, Any]]: + """Get list of E2E-aware devices for a user we share a room with.""" + + devices = [ + # types: "verified", "blacklisted", "ignored" or "unset" + { + "id": device.id, + "display_name": device.display_name or "", + "type": device.trust_state.name, + "ed25519_key": device.ed25519, + } + for device in self.device_store.active_user_devices(user_id) + ] + + types_order = { + "unset": 0, "verified": 1, "ignored": 2, "blacklisted": 3, + } + + # Sort by type, then by display name, then by ID + return sorted( + devices, + key = lambda d: + (types_order[d["type"]], d["display_name"], d["id"]), + ) + + async def rename_device(self, device_id: str, name: str) -> bool: """Rename one of our device, return `False` if it doesn't exist.""" @@ -1346,13 +1372,13 @@ class MatrixClient(nio.AsyncClient): async def verify_device_id(self, user_id: str, device_id: str) -> None: """Mark a device as verified.""" - self.verify_device(self.olm.device_store[user_id][device_id]) + self.verify_device(self.device_store[user_id][device_id]) async def blacklist_device_id(self, user_id: str, device_id: str) -> None: """Mark a device as blacklisted.""" - self.blacklist_device(self.olm.device_store[user_id][device_id]) + self.blacklist_device(self.device_store[user_id][device_id]) async def delete_devices_with_password( diff --git a/src/gui/Base/HButton.qml b/src/gui/Base/HButton.qml index 7915d830..69706125 100644 --- a/src/gui/Base/HButton.qml +++ b/src/gui/Base/HButton.qml @@ -8,9 +8,10 @@ Button { id: button enabled: ! button.loading spacing: theme.spacing - topPadding: padded ? spacing / (circle ? 1.75 : 2) : 0 + topPadding: + padded ? spacing * (circle ? (iconItem.small ? 1.5 : 1.8) : 0.5) : 0 bottomPadding: topPadding - leftPadding: padded ? spacing / (circle ? 1.5 : 1) : 0 + leftPadding: padded ? spacing : 0 rightPadding: leftPadding icon.color: theme.icons.colorize @@ -31,7 +32,7 @@ Button { background: HButtonBackground { button: button buttonTheme: theme.controls.button - radius: circle ? height : enableRadius ? theme.radius : 0 + radius: circle ? height / 2 : enableRadius ? theme.radius : 0 color: backgroundColor } diff --git a/src/gui/Base/HStackView.qml b/src/gui/Base/HStackView.qml new file mode 100644 index 00000000..c4061fef --- /dev/null +++ b/src/gui/Base/HStackView.qml @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +StackView { +} diff --git a/src/gui/Pages/Chat/RoomPane/MemberView/DeviceVerification.qml b/src/gui/Pages/Chat/RoomPane/MemberView/DeviceVerification.qml new file mode 100644 index 00000000..fe861154 --- /dev/null +++ b/src/gui/Pages/Chat/RoomPane/MemberView/DeviceVerification.qml @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import "../../../../Base" +import "../../../../Base/ButtonLayout" + +HFlickableColumnPage { + id: page + + + property string userId + property string deviceOwner + property string deviceOwnerDisplayName + property string deviceId + property string deviceName + property string ed25519Key + property HStackView stackView + + + footer: ButtonLayout { + ApplyButton { + text: qsTr("They're the same") + icon.name: "device-verified" + onClicked: { + loading = true + + py.callClientCoro( + userId, + "verify_device_id", + [deviceOwner, deviceId], + () => { + loading = false + page.verified() + } + ) + } + } + + CancelButton { + text: qsTr("They differ") + icon.name: "device-blacklisted" + onClicked: { + loading = true + + py.callClientCoro( + userId, + "blacklist_device_id", + [deviceOwner, deviceId], + () => { + loading = false + page.blacklisted() + } + ) + } + } + + CancelButton { + id: cancelButton + onClicked: stackView.pop() + Component.onCompleted: forceActiveFocus() + } + } + + onKeyboardCancel: stackView.pop() + + + HRowLayout { + HButton { + id: closeButton + circle: true + icon.name: "close-view" + iconItem.small: true + onClicked: page.stackView.pop() + + Layout.rightMargin: theme.spacing + } + + HLabel { + text: qsTr("Verification") + font.bold: true + elide: HLabel.ElideRight + horizontalAlignment: Qt.AlignHCenter + + Layout.fillWidth: true + } + + Item { + Layout.preferredWidth: closeButton.width + } + } + + HLabel { + wrapMode: HLabel.Wrap + textFormat: HLabel.StyledText + text: qsTr( + "Does %1 sees the same info in their session's account settings?" + ).arg(utils.coloredNameHtml(deviceOwnerDisplayName, deviceOwner)) + + Layout.fillWidth: true + } + + HTextArea { + function formatInfo(info, value) { + return ( + `

` + + info + + `
` + + value + + `

` + ) + } + + readOnly: true + wrapMode: HSelectableLabel.Wrap + textFormat: Qt.RichText + text: ( + formatInfo(qsTr("Session name: "), page.deviceName) + + formatInfo(qsTr("Session ID: "), page.deviceId) + + formatInfo(qsTr("Session key: "), ""+page.ed25519Key+"") + ) + + Layout.fillWidth: true + } + + HLabel { + wrapMode: HLabel.Wrap + text: + qsTr( + "If you already know this user, exchange these info by using" + + " a trusted contact method, such as email or a phone call." + ) + + Layout.fillWidth: true + } +} diff --git a/src/gui/Pages/Chat/RoomPane/MemberDelegate.qml b/src/gui/Pages/Chat/RoomPane/MemberView/MemberDelegate.qml similarity index 97% rename from src/gui/Pages/Chat/RoomPane/MemberDelegate.qml rename to src/gui/Pages/Chat/RoomPane/MemberView/MemberDelegate.qml index 6c593bf3..362623f3 100644 --- a/src/gui/Pages/Chat/RoomPane/MemberDelegate.qml +++ b/src/gui/Pages/Chat/RoomPane/MemberView/MemberDelegate.qml @@ -2,9 +2,9 @@ import QtQuick 2.12 import Clipboard 0.1 -import "../../../Base" -import "../../../Base/HTile" -import "../../../Popups" +import "../../../../Base" +import "../../../../Base/HTile" +import "../../../../Popups" HTile { id: member diff --git a/src/gui/Pages/Chat/RoomPane/MemberView/MemberDeviceDelegate.qml b/src/gui/Pages/Chat/RoomPane/MemberView/MemberDeviceDelegate.qml new file mode 100644 index 00000000..66e99f36 --- /dev/null +++ b/src/gui/Pages/Chat/RoomPane/MemberView/MemberDeviceDelegate.qml @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import "../../../../Base" +import "../../../../Base/ButtonLayout" +import "../../../../Base/HTile" + +HTile { + id: deviceTile + + + property string userId + property string deviceOwner + property string deviceOwnerDisplayName + property HStackView stackView + + signal trustChanged() + + + backgroundColor: "transparent" + rightPadding: theme.spacing / 2 + compact: false + + contentItem: ContentRow { + tile: deviceTile + spacing: 0 + + HColumnLayout { + HRowLayout { + spacing: theme.spacing + + TitleLabel { + text: model.display_name || qsTr("Unnamed") + } + } + + SubtitleLabel { + tile: deviceTile + font.family: theme.fontFamily.mono + text: model.id + } + } + + HIcon { + svgName: "device-action-menu" + + Layout.fillHeight: true + } + } + + onClicked: stackView.push( + "DeviceVerification.qml", + { + userId: deviceTile.userId, + deviceOwner: deviceTile.deviceOwner, + deviceOwnerDisplayName: deviceTile.deviceOwnerDisplayName, + deviceId: model.id, + deviceName: model.display_name, + ed25519Key: model.ed25519_key, + stackView: deviceTile.stackView + }, + ) +} diff --git a/src/gui/Pages/Chat/RoomPane/MemberView/MemberProfile.qml b/src/gui/Pages/Chat/RoomPane/MemberView/MemberProfile.qml new file mode 100644 index 00000000..79116f68 --- /dev/null +++ b/src/gui/Pages/Chat/RoomPane/MemberView/MemberProfile.qml @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 +import "../../../.." +import "../../../../Base" + +HListView { + id: profile + + + property string userId + property string roomId + property QtObject member // RoomMember model item + property HStackView stackView + + + function loadDevices() { + py.callClientCoro(userId, "member_devices", [member.id], devices => { + profile.model.clear() + + for (const device of devices) + profile.model.append(device) + }) + } + + + clip: true + bottomMargin: theme.spacing + model: ListModel {} + delegate: MemberDeviceDelegate { + width: profile.width + userId: profile.userId + deviceOwner: member.id + deviceOwnerDisplayName: member.display_name + stackView: profile.stackView + } + + section.property: "type" + section.delegate: RowLayout { + width: profile.width + spacing: theme.spacing / 2 + + HIcon { + svgName: "device-" + section + colorize: + section === "verified" ? theme.colors.positiveText : + section === "blacklisted" ? theme.colors.errorText : + theme.colors.warningText + + Layout.preferredHeight: dimension + Layout.leftMargin: theme.spacing / 2 + } + + HLabel { + wrapMode: HLabel.Wrap + verticalAlignment: Qt.AlignVCenter + + text: + section === "unset" ? qsTr("Unverified sessions") : + section === "verified" ? qsTr("Verified sessions") : + section === "ignored" ? qsTr("Ignored sessions") : + qsTr("Blacklisted sessions") + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: theme.spacing + Layout.bottomMargin: theme.spacing + Layout.rightMargin: theme.spacing / 2 + } + } + + header: HColumnLayout { + x: theme.spacing + width: profile.width - x * 2 + spacing: theme.spacing * 1.5 + + HUserAvatar { + userId: member.id + displayName: member.display_name + mxc: member.avatar_url + + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.preferredHeight: width + Layout.topMargin: theme.spacing + + HButton { + x: -theme.spacing * 0.75 + y: x + z: 999 + circle: true + icon.name: "close-view" + iconItem.small: true + onClicked: profile.stackView.pop() + } + + } + + HLabel { + textFormat: HLabel.StyledText + wrapMode: HLabel.Wrap + horizontalAlignment: Qt.AlignHCenter + text: + utils.coloredNameHtml(member.display_name, member.user_id) + + (member.display_name.trim() ? + `
${member.id}` : + "") + + Layout.fillWidth: true + Layout.bottomMargin: theme.spacing + } + + // TODO + // HColumnLayout { + // spacing: theme.spacing / 2 + + // HLabel { + // text: qsTr("Power level:") + // wrapMode: HLabel.Wrap + // horizontalAlignment: Qt.AlignHCenter + + // Layout.fillWidth: true + // } + + // HRowLayout { + // spacing: theme.spacing + + // HSpacer {} + + // Row { + // HButton { + // text: qsTr("Default") + // checked: levelBox.value >= 0 && levelBox.value < 50 + // onClicked: levelBox.value = 0 + // } + // HButton { + // text: qsTr("Moderator") + // checked: levelBox.value >= 50 && levelBox.value < 100 + // onClicked: levelBox.value = 50 + // } + // HButton { + // text: qsTr("Admin") + // checked: levelBox.value === 100 + // onClicked: levelBox.value = 100 + // } + // } + + // HSpinBox { + // id: levelBox + // from: -999 + // to: 100 + // defaultValue: member.power_level + // } + + // HSpacer {} + // } + // } + } + + Component.onCompleted: loadDevices() + + Keys.onEscapePressed: stackView.pop() +} diff --git a/src/gui/Pages/Chat/RoomPane/MemberView.qml b/src/gui/Pages/Chat/RoomPane/MemberView/MemberView.qml similarity index 77% rename from src/gui/Pages/Chat/RoomPane/MemberView.qml rename to src/gui/Pages/Chat/RoomPane/MemberView/MemberView.qml index 0cba188b..36a6d383 100644 --- a/src/gui/Pages/Chat/RoomPane/MemberView.qml +++ b/src/gui/Pages/Chat/RoomPane/MemberView/MemberView.qml @@ -2,33 +2,45 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 -import "../../.." -import "../../../Base" +import "../../../.." +import "../../../../Base" HColumnLayout { readonly property alias keybindFocusItem: filterField readonly property var modelSyncId: [chat.userId, chat.roomId, "filtered_members"] - HListView { - id: memberList - clip: true + HStackView { + id: stackView - model: ModelStore.get(modelSyncId) + background: Rectangle { + color: theme.chat.roomPane.listView.background + } - delegate: MemberDelegate { - id: member - width: memberList.width + initialItem: HListView { + id: memberList + clip: true + + model: ModelStore.get(modelSyncId) + + delegate: MemberDelegate { + id: member + width: memberList.width + + onLeftClicked: stackView.push( + "MemberProfile.qml", + { + userId: chat.userId, + roomId: chat.roomId, + member: model, + stackView: stackView, + }, + ) + } } Layout.fillWidth: true Layout.fillHeight: true - - Rectangle { - anchors.fill: parent - z: -100 - color: theme.chat.roomPane.listView.background - } } Rectangle { @@ -58,8 +70,10 @@ HColumnLayout { // declared normally Component.onCompleted: placeholderText = qsTr("Filter members") - onTextChanged: + onTextChanged: { + stackView.pop(stackView.initialItem) py.callCoro("set_substring_filter", [modelSyncId, text]) + } Keys.onEscapePressed: { roomPane.toggleFocus() diff --git a/src/gui/Pages/Chat/RoomPane/RoomPane.qml b/src/gui/Pages/Chat/RoomPane/RoomPane.qml index 15637c47..09ae81e0 100644 --- a/src/gui/Pages/Chat/RoomPane/RoomPane.qml +++ b/src/gui/Pages/Chat/RoomPane/RoomPane.qml @@ -3,6 +3,7 @@ import QtQuick 2.12 import "../../../Base" import "../../.." +import "MemberView" MultiviewPane { id: roomPane diff --git a/src/gui/Utils.qml b/src/gui/Utils.qml index 94d4c4c6..98d7c697 100644 --- a/src/gui/Utils.qml +++ b/src/gui/Utils.qml @@ -49,8 +49,8 @@ QtObject { function makePopup(urlComponent, properties={}, callback=null, - autoDestruct=true) { - makeObject(urlComponent, window, properties, (popup) => { + autoDestruct=true, parent=window) { + makeObject(urlComponent, parent, properties, (popup) => { popup.open() if (autoDestruct) popup.closed.connect(() => { popup.destroy() }) if (callback) callback(popup) diff --git a/src/icons/thin/close-view.svg b/src/icons/thin/close-view.svg new file mode 100644 index 00000000..e4728028 --- /dev/null +++ b/src/icons/thin/close-view.svg @@ -0,0 +1,3 @@ + + +