Add profile/verification UI for room members

This commit is contained in:
miruka 2020-07-08 11:33:05 -04:00
parent 4ccb774411
commit 9b43bef935
12 changed files with 452 additions and 31 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Controls 2.12
StackView {
}

View 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
}
}

View File

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

View File

@ -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
},
)
}

View 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()
}

View File

@ -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()

View File

@ -3,6 +3,7 @@
import QtQuick 2.12
import "../../../Base"
import "../../.."
import "MemberView"
MultiviewPane {
id: roomPane

View File

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

View 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