Add profile/verification UI for room members
This commit is contained in:
parent
4ccb774411
commit
9b43bef935
7
TODO.md
7
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
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
7
src/gui/Base/HStackView.qml
Normal file
7
src/gui/Base/HStackView.qml
Normal file
|
@ -0,0 +1,7 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
|
||||
StackView {
|
||||
}
|
136
src/gui/Pages/Chat/RoomPane/MemberView/DeviceVerification.qml
Normal file
136
src/gui/Pages/Chat/RoomPane/MemberView/DeviceVerification.qml
Normal file
|
@ -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 (
|
||||
`<p style="line-height: 115%">` +
|
||||
info +
|
||||
`<br><span style="font-family: ${theme.fontFamily.mono}">` +
|
||||
value +
|
||||
`</span></p>`
|
||||
)
|
||||
}
|
||||
|
||||
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: "), "<b>"+page.ed25519Key+"</b>")
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
164
src/gui/Pages/Chat/RoomPane/MemberView/MemberProfile.qml
Normal file
164
src/gui/Pages/Chat/RoomPane/MemberView/MemberProfile.qml
Normal file
|
@ -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() ?
|
||||
`<br><font color="${theme.colors.dimText}">${member.id}</font>` :
|
||||
"")
|
||||
|
||||
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()
|
||||
}
|
|
@ -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()
|
|
@ -3,6 +3,7 @@
|
|||
import QtQuick 2.12
|
||||
import "../../../Base"
|
||||
import "../../.."
|
||||
import "MemberView"
|
||||
|
||||
MultiviewPane {
|
||||
id: roomPane
|
||||
|
|
|
@ -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)
|
||||
|
|
3
src/icons/thin/close-view.svg
Normal file
3
src/icons/thin/close-view.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m23 20.168-8.185-8.187 8.185-8.174-2.832-2.807-8.182 8.179-8.176-8.179-2.81 2.81 8.186 8.196-8.186 8.184 2.81 2.81 8.203-8.192 8.18 8.192z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 246 B |
Loading…
Reference in New Issue
Block a user