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