Merge remote-tracking branch 'nheko-im/master' into video_player_enhancements

This commit is contained in:
Joseph Donofry 2021-08-16 17:16:17 -04:00
commit 093f9f9e33
No known key found for this signature in database
GPG key ID: E8A1D78EF044B0CB
39 changed files with 795 additions and 295 deletions

View file

@ -6,7 +6,7 @@ set -eux
#TAG=$(git tag -l --points-at HEAD) #TAG=$(git tag -l --points-at HEAD)
# Add Qt binaries to path # Add Qt binaries to path
PATH=/usr/local/opt/qt/bin/:${PATH} PATH=/usr/local/opt/qt@5/bin/:${PATH}
( cd build ( cd build
# macdeployqt does not copy symlinks over. # macdeployqt does not copy symlinks over.

View file

@ -1,61 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
### Describe the bug
A clear and concise description of what the bug is.
### To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
### Expected behavior
A clear and concise description of what you expected to happen.
### Screenshots
If applicable, add screenshots to help explain your problem.
### System:
- Nheko version: <!-- Get the version from the settings menu (bottom left corner) -->
- Profile used: <!-- If you are not using the default profile, mention it here -->
- Installation method: <!-- AppImage, some repository, local build etc -->
- Operating System:
- Qt version: <!-- If you compiled it yourself -->
- C++ compiler: <!-- if you compiled it yourself -->
- Desktop Environment: <!-- for Linux -->
### Logs
<!-- If applicable -->
<!-- The log file is located in
Linux: ~/.cache/nheko/
macOS: ~/Library/Caches/nheko or /Library/Caches/nheko
Windows: C:/Users/<USER>/AppData/Local/nheko/cache
-->
### Debugger backtrace
<!--
If the program crashed send a backtrace:
You can retrieve a backtrace by building nheko with -DCMAKE_BUILD_TYPE=Debug
and running it through gdb or lldb.
gdb ./build/nheko
>> run
... Make the program crash
>> bt
... Paste a link of the output below (Use a pastebin, don't paste directly in the github issue).
-->

150
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View file

@ -0,0 +1,150 @@
name: Bug Report
description: Create a report to help us improve
#title: "[Bug]: "
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Please try to fill out all fields to the best of your ability.
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
placeholder: Enter your description here.
validations:
required: true
- type: textarea
id: reproduction-steps
attributes:
label: To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
value: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: behaviour
attributes:
label: What happened?
description: A clear and concise description of what actually happened.
validations:
required: false
- type: textarea
id: expected-behaviour
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
placeholder: Upload your screenshots here. You can paste them or click on "Attach files".
validations:
required: false
- type: input
id: version
attributes:
label: Version
description: Get the version from the settings menu (bottom left corner)
placeholder: 0.0.1-deafbeef
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating system
multiple: true
options:
- Linux
- macOS
- Windows
- BSD
- Haiku
- Other
- type: dropdown
id: install-method
attributes:
label: Installation method
multiple: true
options:
- Flathub
- Flatpak nightly repo or download
- AppImage
- Windows download
- macOS DMG file
- Some repository (AUR, homebrew, distribution repository, PPA, etc)
- Local build
- type: input
id: qt-version
attributes:
label: Qt version
description: What version of Qt does your system use? (If you compiled Nheko yourself.)
placeholder: 5.15.2.
validations:
required: false
- type: input
id: compiler
attributes:
label: C++ compiler
description: What compiler (and version) did you use (if you compiled Nheko yourself)?
placeholder: gcc-9000
validations:
required: false
- type: input
id: de
attributes:
label: Desktop Environment
description: If you are on Linux, describe your desktop environment.
placeholder: KDE with i3 as the window manager
validations:
required: false
- type: checkboxes
id: profiles
attributes:
label: Did you use profiles?
description: Usually by passing the --profile command line parameter. If you don't know, answer 'no'.
options:
- label: Profiles used?
required: false
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
placeholder: |
The log file is located in
Linux: ~/.cache/nheko/
macOS: ~/Library/Caches/nheko or /Library/Caches/nheko
Windows: C:/Users/<USER>/AppData/Local/nheko/cache
render: shell
- type: textarea
id: backtrace
attributes:
label: Backtrace
description: If the program crashed send a backtrace.
placeholder: |
You can retrieve a backtrace by building nheko with -DCMAKE_BUILD_TYPE=Debug and running it through gdb or lldb.
gdb ./build/nheko
>> run
... Make the program crash
>> bt
render: shell

View file

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -0,0 +1,49 @@
name: Feature request
description: Suggest an idea for this project
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Please verify that there is no feature request for this already!
- type: textarea
id: problem
attributes:
label: The Problem
description: Is your feature request related to a problem? Please describe.
placeholder: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]"
validations:
required: true
- type: textarea
id: solution
attributes:
label: The Solution
description: Describe the solution you'd like
placeholder: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives
description: Describe alternatives you've considered.
placeholder: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
id: context
attributes:
label: Additional context
description: Describe alternatives you've considered.
placeholder: Add any other context or screenshots about the feature request here.
validations:
required: false
- type: checkboxes
id: version-check
attributes:
label: Happens in the latest version
description: Please verify that this is still missing in the latest version.
options:
- label: Yes, this feature is still missing.
required: true

View file

@ -381,7 +381,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare( FetchContent_Declare(
MatrixClient MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG bcf363cb5e6c423f40c96123e227bc8c5f6d6f80 GIT_TAG deb51ef1d6df870098069312f0a1999550e1eb85
) )
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@ -676,7 +676,7 @@ if(USE_BUNDLED_COEURL)
FetchContent_Declare( FetchContent_Declare(
coeurl coeurl
GIT_REPOSITORY https://nheko.im/Nheko-Reborn/coeurl.git GIT_REPOSITORY https://nheko.im/Nheko-Reborn/coeurl.git
GIT_TAG e9010d1ce14e7163d1cb5407ed27b23303781796 GIT_TAG 3901507db25cf3f9364b58cd8c7880640900c992
) )
FetchContent_MakeAvailable(coeurl) FetchContent_MakeAvailable(coeurl)
target_link_libraries(nheko PUBLIC coeurl::coeurl) target_link_libraries(nheko PUBLIC coeurl::coeurl)

View file

@ -213,7 +213,7 @@ sudo emerge -a ">=dev-qt/qtgui-5.10.0" media-libs/fontconfig dev-libs/qtkeychain
```bash ```bash
# Build requirements + qml modules needed at runtime (you may not need all of them, but the following seem to work according to reports): # Build requirements + qml modules needed at runtime (you may not need all of them, but the following seem to work according to reports):
sudo apt install g++ cmake zlib1g-dev libssl-dev qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt5svg5-dev libboost-system-dev libboost-thread-dev libboost-iostreams-dev libolm-dev liblmdb++-dev libcmark-dev nlohmann-json3-dev libspdlog-dev libgtest-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,-labs-platform,graphicaleffects,quick-controls2} qt5keychain-dev sudo apt install g++ cmake zlib1g-dev libssl-dev qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt5svg5-dev libboost-system-dev libboost-thread-dev libboost-iostreams-dev libolm-dev liblmdb++-dev libcmark-dev nlohmann-json3-dev libspdlog-dev libgtest-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,-labs-platform,graphicaleffects,quick-controls2} qt5keychain-dev libevent-dev libcurl-dev
``` ```
This will install all dependencies, except for tweeny (use bundled tweeny) This will install all dependencies, except for tweeny (use bundled tweeny)
and mtxclient (needs to be build separately). and mtxclient (needs to be build separately).

View file

@ -152,7 +152,7 @@ modules:
- -Ddefault_library=static - -Ddefault_library=static
name: coeurl name: coeurl
sources: sources:
- commit: 417821a07cfe4429b08a2efed5e480a498087afd - commit: 3901507db25cf3f9364b58cd8c7880640900c992
type: git type: git
url: https://nheko.im/nheko-reborn/coeurl.git url: https://nheko.im/nheko-reborn/coeurl.git
- config-opts: - config-opts:
@ -163,7 +163,7 @@ modules:
buildsystem: cmake-ninja buildsystem: cmake-ninja
name: mtxclient name: mtxclient
sources: sources:
- commit: bcf363cb5e6c423f40c96123e227bc8c5f6d6f80 - commit: deb51ef1d6df870098069312f0a1999550e1eb85
type: git type: git
url: https://github.com/Nheko-Reborn/mtxclient.git url: https://github.com/Nheko-Reborn/mtxclient.git
- config-opts: - config-opts:

View file

@ -3,7 +3,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import "./ui" import "./ui"
import QtGraphicalEffects 1.0
import QtQuick 2.6 import QtQuick 2.6
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import im.nheko 1.0 import im.nheko 1.0
@ -21,7 +20,7 @@ Rectangle {
width: 48 width: 48
height: 48 height: 48
radius: Settings.avatarCircles ? height / 2 : 3 radius: Settings.avatarCircles ? height / 2 : height / 8
color: Nheko.colors.alternateBase color: Nheko.colors.alternateBase
Component.onCompleted: { Component.onCompleted: {
mouseArea.clicked.connect(clicked); mouseArea.clicked.connect(clicked);
@ -50,8 +49,7 @@ Rectangle {
smooth: true smooth: true
sourceSize.width: avatar.width sourceSize.width: avatar.width
sourceSize.height: avatar.height sourceSize.height: avatar.height
layer.enabled: true source: avatar.url ? (avatar.url + "?radius=" + (Settings.avatarCircles ? 100.0 : 25.0) + ((avatar.crop) ? "" : "&scale")) : ""
source: avatar.url + ((avatar.crop || !avatar.url) ? "" : "?scale")
MouseArea { MouseArea {
id: mouseArea id: mouseArea
@ -65,18 +63,6 @@ Rectangle {
} }
layer.effect: OpacityMask {
cached: true
maskSource: Rectangle {
anchors.fill: parent
width: avatar.width
height: avatar.height
radius: Settings.avatarCircles ? height / 2 : 3
}
}
} }
Rectangle { Rectangle {
@ -85,7 +71,7 @@ Rectangle {
visible: !!userid visible: !!userid
height: avatar.height / 6 height: avatar.height / 6
width: height width: height
radius: Settings.avatarCircles ? height / 2 : height / 4 radius: Settings.avatarCircles ? height / 2 : height / 8
color: { color: {
switch (TimelineManager.userPresence(userid)) { switch (TimelineManager.userPresence(userid)) {
case "online": case "online":

View file

@ -68,6 +68,7 @@ Popup {
isOnlyEmoji: modelData.isOnlyEmoji ?? false isOnlyEmoji: modelData.isOnlyEmoji ?? false
userId: modelData.userId ?? "" userId: modelData.userId ?? ""
userName: modelData.userName ?? "" userName: modelData.userName ?? ""
encryptionError: modelData.encryptionError ?? ""
} }
MatrixTextField { MatrixTextField {
@ -85,6 +86,9 @@ Popup {
} else if (event.key == Qt.Key_Down && completerPopup.opened) { } else if (event.key == Qt.Key_Down && completerPopup.opened) {
event.accepted = true; event.accepted = true;
completerPopup.down(); completerPopup.down();
} else if (event.key == Qt.Key_Tab && completerPopup.opened) {
event.accepted = true;
completerPopup.down();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) { } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
completerPopup.finishCompletion(); completerPopup.finishCompletion();
event.accepted = true; event.accepted = true;

View file

@ -6,7 +6,6 @@ import "./delegates"
import "./emoji" import "./emoji"
import "./ui" import "./ui"
import Qt.labs.platform 1.1 as Platform import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2

View file

@ -45,6 +45,9 @@ Popup {
} else if (event.key == Qt.Key_Down && completerPopup.opened) { } else if (event.key == Qt.Key_Down && completerPopup.opened) {
event.accepted = true; event.accepted = true;
completerPopup.down(); completerPopup.down();
} else if (event.key == Qt.Key_Tab && completerPopup.opened) {
event.accepted = true;
completerPopup.down();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) { } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
completerPopup.finishCompletion(); completerPopup.finishCompletion();
event.accepted = true; event.accepted = true;

View file

@ -45,6 +45,7 @@ Rectangle {
isOnlyEmoji: modelData.isOnlyEmoji ?? false isOnlyEmoji: modelData.isOnlyEmoji ?? false
userId: modelData.userId ?? "" userId: modelData.userId ?? ""
userName: modelData.userName ?? "" userName: modelData.userName ?? ""
encryptionError: modelData.encryptionError ?? ""
} }
ImageButton { ImageButton {

View file

@ -33,8 +33,9 @@ Page {
Connections { Connections {
function onCurrentRoomChanged() { function onCurrentRoomChanged() {
if (Rooms.currentRoom)
roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain); roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain);
console.log("Test" + Rooms.currentRoom.roomId + " " + Rooms.roomidToIndex(Rooms.currentRoom.roomId));
} }
target: Rooms target: Rooms
@ -190,7 +191,12 @@ Page {
TapHandler { TapHandler {
margin: -Nheko.paddingSmall margin: -Nheko.paddingSmall
onSingleTapped: Rooms.setCurrentRoom(roomId) onSingleTapped: {
if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId)
Rooms.setCurrentRoom(roomId);
else
Rooms.resetCurrentRoom();
}
onLongPressed: { onLongPressed: {
if (!isInvite) if (!isInvite)
roomContextMenu.show(roomId, tags); roomContextMenu.show(roomId, tags);

View file

@ -13,6 +13,7 @@ ApplicationWindow {
id: roomMembersRoot id: roomMembersRoot
property MemberList members property MemberList members
property Room room
title: qsTr("Members of %1").arg(members.roomName) title: qsTr("Members of %1").arg(members.roomName)
height: 650 height: 650
@ -83,9 +84,14 @@ ApplicationWindow {
} }
delegate: RowLayout { delegate: RowLayout {
id: del
width: ListView.view.width
spacing: Nheko.paddingMedium spacing: Nheko.paddingMedium
Avatar { Avatar {
id: avatar
width: Nheko.avatarSize width: Nheko.avatarSize
height: Nheko.avatarSize height: Nheko.avatarSize
userid: model.mxid userid: model.mxid
@ -97,16 +103,18 @@ ApplicationWindow {
ColumnLayout { ColumnLayout {
spacing: Nheko.paddingSmall spacing: Nheko.paddingSmall
Label { ElidedLabel {
text: model.displayName fullText: model.displayName
color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window) color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window)
font.pointSize: fontMetrics.font.pointSize font.pixelSize: fontMetrics.font.pixelSize
elideWidth: del.width - Nheko.paddingMedium * 2 - avatar.width - encryptInd.width
} }
Label { ElidedLabel {
text: model.mxid fullText: model.mxid
color: Nheko.colors.buttonText color: Nheko.colors.buttonText
font.pointSize: fontMetrics.font.pointSize * 0.9 font.pixelSize: Math.ceil(fontMetrics.font.pixelSize * 0.9)
elideWidth: del.width - Nheko.paddingMedium * 2 - avatar.width - encryptInd.width
} }
Item { Item {
@ -116,6 +124,28 @@ ApplicationWindow {
} }
EncryptionIndicator {
id: encryptInd
Layout.alignment: Qt.AlignRight
visible: room.isEncrypted
encrypted: room.isEncrypted
trust: encrypted ? model.trustlevel : Crypto.Unverified
ToolTip.text: {
if (!encrypted)
return qsTr("This room is not encrypted!");
switch (trust) {
case Crypto.Verified:
return qsTr("This user is verified.");
case Crypto.TOFU:
return qsTr("This user isn't verified, but is still using the same master key from the first time you met.");
default:
return qsTr("This user has unverified devices!");
}
}
}
} }
footer: Item { footer: Item {

View file

@ -15,8 +15,8 @@ ApplicationWindow {
property var roomSettings property var roomSettings
minimumWidth: 420 minimumWidth: 450
minimumHeight: 650 minimumHeight: 680
palette: Nheko.colors palette: Nheko.colors
color: Nheko.colors.window color: Nheko.colors.window
modality: Qt.NonModal modality: Qt.NonModal

View file

@ -8,7 +8,6 @@ import "./dialogs"
import "./emoji" import "./emoji"
import "./voip" import "./voip"
import Qt.labs.platform 1.1 as Platform import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
@ -153,10 +152,10 @@ Page {
packSet.show(); packSet.show();
} }
function onOpenRoomMembersDialog(members) { function onOpenRoomMembersDialog(members, room) {
var membersDialog = roomMembersComponent.createObject(timelineRoot, { var membersDialog = roomMembersComponent.createObject(timelineRoot, {
"members": members, "members": members,
"roomName": Rooms.currentRoom.roomName "room": room
}); });
membersDialog.show(); membersDialog.show();
} }

View file

@ -9,7 +9,6 @@ import "./emoji"
import "./ui" import "./ui"
import "./voip" import "./voip"
import Qt.labs.platform 1.1 as Platform import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.5 import QtQuick.Controls 2.5
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
@ -85,11 +84,16 @@ Item {
target: timelineView target: timelineView
} }
MessageView { Loader {
active: room || roomPreview
Layout.fillWidth: true Layout.fillWidth: true
sourceComponent: MessageView {
implicitHeight: msgView.height - typingIndicator.height implicitHeight: msgView.height - typingIndicator.height
} }
}
Loader { Loader {
source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : "" source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : ""
onLoaded: TimelineManager.setVideoCallItem() onLoaded: TimelineManager.setVideoCallItem()

View file

@ -15,6 +15,8 @@ Rectangle {
property string roomName: room ? room.roomName : qsTr("No room selected") property string roomName: room ? room.roomName : qsTr("No room selected")
property string avatarUrl: room ? room.roomAvatarUrl : "" property string avatarUrl: room ? room.roomAvatarUrl : ""
property string roomTopic: room ? room.roomTopic : "" property string roomTopic: room ? room.roomTopic : ""
property bool isEncrypted: room ? room.isEncrypted : false
property int trustlevel: room ? room.trustlevel : Crypto.Unverified
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: topLayout.height + Nheko.paddingMedium * 2 implicitHeight: topLayout.height + Nheko.paddingMedium * 2
@ -92,11 +94,33 @@ Rectangle {
text: roomTopic text: roomTopic
} }
EncryptionIndicator {
Layout.column: 3
Layout.row: 0
Layout.rowSpan: 2
visible: isEncrypted
encrypted: isEncrypted
trust: trustlevel
ToolTip.text: {
if (!encrypted)
return qsTr("This room is not encrypted!");
switch (trust) {
case Crypto.Verified:
return qsTr("This room contains only verified devices.");
case Crypto.TOFU:
return qsTr("This rooms contain verified devices and devices which have never changed their master key.");
default:
return qsTr("This room contains unverified devices!");
}
}
}
ImageButton { ImageButton {
id: roomOptionsButton id: roomOptionsButton
visible: !!room visible: !!room
Layout.column: 3 Layout.column: 4
Layout.row: 0 Layout.row: 0
Layout.rowSpan: 2 Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
@ -116,7 +140,7 @@ Rectangle {
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Members") text: qsTr("Members")
onTriggered: TimelineManager.openRoomMembers(room.roomId) onTriggered: TimelineManager.openRoomMembers(room)
} }
Platform.MenuItem { Platform.MenuItem {

View file

@ -171,7 +171,7 @@ ApplicationWindow {
} }
MatrixText { MatrixText {
text: qsTr("Attrbution") text: qsTr("Attribution")
} }
MatrixTextField { MatrixTextField {

View file

@ -114,7 +114,13 @@ ro_txn(lmdb::env &env)
txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY); txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
reuse_counter = 0; reuse_counter = 0;
} else if (reuse_counter > 0) { } else if (reuse_counter > 0) {
try {
txn.renew(); txn.renew();
} catch (...) {
txn.abort();
txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
reuse_counter = 0;
}
} }
reuse_counter++; reuse_counter++;
@ -290,6 +296,8 @@ Cache::setup()
// What rooms are encrypted // What rooms are encrypted
encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
[[maybe_unused]] auto verificationDb = getVerificationDb(txn);
[[maybe_unused]] auto userKeysDb = getUserKeysDb(txn);
txn.commit(); txn.commit();
@ -720,20 +728,35 @@ Cache::storeSecret(const std::string name, const std::string secret)
{ {
auto settings = UserSettings::instance(); auto settings = UserSettings::instance();
auto job = new QKeychain::WritePasswordJob(QCoreApplication::applicationName()); auto job = new QKeychain::WritePasswordJob(QCoreApplication::applicationName());
job->setAutoDelete(true);
job->setInsecureFallback(true); job->setInsecureFallback(true);
job->setKey("matrix." + job->setSettings(UserSettings::instance()->qsettings());
QString(QCryptographicHash::hash(settings->profile().toUtf8(),
QCryptographicHash::Sha256)) + job->setKey(
"." + name.c_str()); "matrix." +
QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
.toBase64()) +
"." + QString::fromStdString(name));
job->setTextData(QString::fromStdString(secret)); job->setTextData(QString::fromStdString(secret));
QObject::connect(job, &QKeychain::Job::finished, job, [name, this](QKeychain::Job *job) {
QObject::connect(
job,
&QKeychain::WritePasswordJob::finished,
this,
[name, this](QKeychain::Job *job) {
if (job->error()) { if (job->error()) {
nhlog::db()->warn( nhlog::db()->warn("Storing secret '{}' failed: {}",
"Storing secret '{}' failed: {}", name, job->errorString().toStdString()); name,
job->errorString().toStdString());
} else { } else {
emit secretChanged(name); // if we emit the signal directly, qtkeychain breaks and won't execute new
// jobs. You can't start a job from the finish signal of a job.
QTimer::singleShot(100, [this, name] { emit secretChanged(name); });
nhlog::db()->info("Storing secret '{}' successful", name);
} }
}); },
Qt::ConnectionType::DirectConnection);
job->start(); job->start();
} }
@ -744,10 +767,14 @@ Cache::deleteSecret(const std::string name)
QKeychain::DeletePasswordJob job(QCoreApplication::applicationName()); QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
job.setAutoDelete(false); job.setAutoDelete(false);
job.setInsecureFallback(true); job.setInsecureFallback(true);
job.setKey("matrix." + job.setSettings(UserSettings::instance()->qsettings());
QString(QCryptographicHash::hash(settings->profile().toUtf8(),
QCryptographicHash::Sha256)) + job.setKey(
"." + name.c_str()); "matrix." +
QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
.toBase64()) +
"." + QString::fromStdString(name));
// FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
// time! // time!
QEventLoop loop; QEventLoop loop;
@ -765,10 +792,14 @@ Cache::secret(const std::string name)
QKeychain::ReadPasswordJob job(QCoreApplication::applicationName()); QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
job.setAutoDelete(false); job.setAutoDelete(false);
job.setInsecureFallback(true); job.setInsecureFallback(true);
job.setKey("matrix." + job.setSettings(UserSettings::instance()->qsettings());
QString(QCryptographicHash::hash(settings->profile().toUtf8(),
QCryptographicHash::Sha256)) + job.setKey(
"." + name.c_str()); "matrix." +
QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
.toBase64()) +
"." + QString::fromStdString(name));
// FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean // FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
// time! // time!
QEventLoop loop; QEventLoop loop;
@ -838,6 +869,9 @@ Cache::setNextBatchToken(lmdb::txn &txn, const QString &token)
bool bool
Cache::isInitialized() Cache::isInitialized()
{ {
if (!env_.handle())
return false;
auto txn = ro_txn(env_); auto txn = ro_txn(env_);
std::string_view token; std::string_view token;
@ -1564,6 +1598,8 @@ RoomInfo
Cache::singleRoomInfo(const std::string &room_id) Cache::singleRoomInfo(const std::string &room_id)
{ {
auto txn = ro_txn(env_); auto txn = ro_txn(env_);
try {
auto statesdb = getStatesDb(txn, room_id); auto statesdb = getStatesDb(txn, room_id);
std::string_view data; std::string_view data;
@ -1584,6 +1620,10 @@ Cache::singleRoomInfo(const std::string &room_id)
e.what()); e.what());
} }
} }
} catch (const lmdb::error &e) {
nhlog::db()->warn(
"failed to read room info from db: room_id ({}), {}", room_id, e.what());
}
return RoomInfo(); return RoomInfo();
} }
@ -3541,6 +3581,44 @@ Cache::roomMembers(const std::string &room_id)
return members; return members;
} }
crypto::Trust
Cache::roomVerificationStatus(const std::string &room_id)
{
crypto::Trust trust = crypto::Verified;
try {
auto txn = lmdb::txn::begin(env_);
auto db = getMembersDb(txn, room_id);
auto keysDb = getUserKeysDb(txn);
std::vector<std::string> keysToRequest;
std::string_view user_id, unused;
auto cursor = lmdb::cursor::open(txn, db);
while (cursor.get(user_id, unused, MDB_NEXT)) {
auto verif = verificationStatus_(std::string(user_id), txn);
if (verif.unverified_device_count) {
trust = crypto::Unverified;
if (verif.verified_devices.empty() && verif.no_keys) {
// we probably don't have the keys yet, so query them
keysToRequest.push_back(std::string(user_id));
}
} else if (verif.user_verified == crypto::TOFU && trust == crypto::Verified)
trust = crypto::TOFU;
}
if (!keysToRequest.empty())
markUserKeysOutOfDate(txn, keysDb, keysToRequest, "");
} catch (std::exception &e) {
nhlog::db()->error(
"Failed to calculate verification status for {}: {}", room_id, e.what());
trust = crypto::Unverified;
}
return trust;
}
std::map<std::string, std::optional<UserKeyCache>> std::map<std::string, std::optional<UserKeyCache>>
Cache::getMembersWithKeys(const std::string &room_id, bool verified_only) Cache::getMembersWithKeys(const std::string &room_id, bool verified_only)
{ {
@ -3722,11 +3800,17 @@ from_json(const json &j, UserKeyCache &info)
std::optional<UserKeyCache> std::optional<UserKeyCache>
Cache::userKeys(const std::string &user_id) Cache::userKeys(const std::string &user_id)
{
auto txn = ro_txn(env_);
return userKeys_(user_id, txn);
}
std::optional<UserKeyCache>
Cache::userKeys_(const std::string &user_id, lmdb::txn &txn)
{ {
std::string_view keys; std::string_view keys;
try { try {
auto txn = ro_txn(env_);
auto db = getUserKeysDb(txn); auto db = getUserKeysDb(txn);
auto res = db.get(txn, user_id, keys); auto res = db.get(txn, user_id, keys);
@ -3735,7 +3819,8 @@ Cache::userKeys(const std::string &user_id)
} else { } else {
return {}; return {};
} }
} catch (std::exception &) { } catch (std::exception &e) {
nhlog::db()->error("Failed to retrieve user keys for {}: {}", user_id, e.what());
return {}; return {};
} }
} }
@ -3770,8 +3855,14 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
auto last_changed = updateToWrite.last_changed; auto last_changed = updateToWrite.last_changed;
// skip if we are tracking this and expect it to be up to date with the last // skip if we are tracking this and expect it to be up to date with the last
// sync token // sync token
if (!last_changed.empty() && last_changed != sync_token) if (!last_changed.empty() && last_changed != sync_token) {
nhlog::db()->debug("Not storing update for user {}, because "
"last_changed {}, but we fetched update for {}",
user,
last_changed,
sync_token);
continue; continue;
}
if (!updateToWrite.master_keys.keys.empty() && if (!updateToWrite.master_keys.keys.empty() &&
update.master_keys.keys != updateToWrite.master_keys.keys) { update.master_keys.keys != updateToWrite.master_keys.keys) {
@ -3819,9 +3910,44 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
} }
} }
if (!keyReused && !oldDeviceKeys.count(device_id)) if (!keyReused && !oldDeviceKeys.count(device_id)) {
// ensure the key has a valid signature from itself
std::string device_signing_key =
"ed25519:" + device_keys.device_id;
if (device_id != device_keys.device_id) {
nhlog::crypto()->warn(
"device {}:{} has a different device id "
"in the body: {}",
user,
device_id,
device_keys.device_id);
continue;
}
if (!device_keys.signatures.count(user) ||
!device_keys.signatures.at(user).count(
device_signing_key)) {
nhlog::crypto()->warn(
"device {}:{} has no signature",
user,
device_id);
continue;
}
if (!mtx::crypto::ed25519_verify_signature(
device_keys.keys.at(device_signing_key),
json(device_keys),
device_keys.signatures.at(user).at(
device_signing_key))) {
nhlog::crypto()->warn(
"device {}:{} has an invalid signature",
user,
device_id);
continue;
}
updateToWrite.device_keys[device_id] = device_keys; updateToWrite.device_keys[device_id] = device_keys;
} }
}
for (const auto &[key_id, key] : device_keys.keys) { for (const auto &[key_id, key] : device_keys.keys) {
(void)key_id; (void)key_id;
@ -3830,6 +3956,7 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
updateToWrite.seen_device_ids.insert(device_id); updateToWrite.seen_device_ids.insert(device_id);
} }
} }
updateToWrite.updated_at = sync_token;
db.put(txn, user, json(updateToWrite).dump()); db.put(txn, user, json(updateToWrite).dump());
} }
@ -3882,14 +4009,15 @@ Cache::markUserKeysOutOfDate(lmdb::txn &txn,
nhlog::db()->debug("Marking user keys out of date: {}", user); nhlog::db()->debug("Marking user keys out of date: {}", user);
std::string_view oldKeys; std::string_view oldKeys;
UserKeyCache cacheEntry;
auto res = db.get(txn, user, oldKeys); auto res = db.get(txn, user, oldKeys);
if (res) {
if (!res) cacheEntry = json::parse(std::string_view(oldKeys.data(), oldKeys.size()))
continue; .get<UserKeyCache>();
}
auto cacheEntry =
json::parse(std::string_view(oldKeys.data(), oldKeys.size())).get<UserKeyCache>();
cacheEntry.last_changed = sync_token; cacheEntry.last_changed = sync_token;
db.put(txn, user, json(cacheEntry).dump()); db.put(txn, user, json(cacheEntry).dump());
query.device_keys[user] = {}; query.device_keys[user] = {};
@ -3915,35 +4043,46 @@ void
Cache::query_keys(const std::string &user_id, Cache::query_keys(const std::string &user_id,
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb) std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb)
{ {
auto cache_ = cache::userKeys(user_id); mtx::requests::QueryKeys req;
std::string last_changed;
{
auto txn = ro_txn(env_);
auto cache_ = userKeys_(user_id, txn);
if (cache_.has_value()) { if (cache_.has_value()) {
if (!cache_->updated_at.empty() && cache_->updated_at == cache_->last_changed) { if (cache_->updated_at == cache_->last_changed) {
cb(cache_.value(), {}); cb(cache_.value(), {});
return; return;
} } else
} nhlog::db()->info("Keys outdated for {}: {} vs {}",
user_id,
cache_->updated_at,
cache_->last_changed);
} else
nhlog::db()->info("No keys found for {}", user_id);
mtx::requests::QueryKeys req;
req.device_keys[user_id] = {}; req.device_keys[user_id] = {};
std::string last_changed;
if (cache_) if (cache_)
last_changed = cache_->last_changed; last_changed = cache_->last_changed;
req.token = last_changed; req.token = last_changed;
}
// use context object so that we can disconnect again // use context object so that we can disconnect again
QObject *context{new QObject(this)}; QObject *context{new QObject(this)};
QObject::connect(this, QObject::connect(
this,
&Cache::verificationStatusChanged, &Cache::verificationStatusChanged,
context, context,
[cb, user_id, context_ = context](std::string updated_user) mutable { [cb, user_id, context_ = context, this](std::string updated_user) mutable {
if (user_id == updated_user) { if (user_id == updated_user) {
context_->deleteLater(); context_->deleteLater();
auto keys = cache::userKeys(user_id); auto txn = ro_txn(env_);
auto keys = this->userKeys_(user_id, txn);
cb(keys.value_or(UserKeyCache{}), {}); cb(keys.value_or(UserKeyCache{}), {});
} }
}); },
Qt::QueuedConnection);
http::client()->query_keys( http::client()->query_keys(
req, req,
@ -3971,16 +4110,15 @@ to_json(json &j, const VerificationCache &info)
void void
from_json(const json &j, VerificationCache &info) from_json(const json &j, VerificationCache &info)
{ {
info.device_verified = j.at("device_verified").get<std::vector<std::string>>(); info.device_verified = j.at("device_verified").get<std::set<std::string>>();
info.device_blocked = j.at("device_blocked").get<std::vector<std::string>>(); info.device_blocked = j.at("device_blocked").get<std::set<std::string>>();
} }
std::optional<VerificationCache> std::optional<VerificationCache>
Cache::verificationCache(const std::string &user_id) Cache::verificationCache(const std::string &user_id, lmdb::txn &txn)
{ {
std::string_view verifiedVal; std::string_view verifiedVal;
auto txn = lmdb::txn::begin(env_);
auto db = getVerificationDb(txn); auto db = getVerificationDb(txn);
try { try {
@ -3999,6 +4137,7 @@ Cache::verificationCache(const std::string &user_id)
void void
Cache::markDeviceVerified(const std::string &user_id, const std::string &key) Cache::markDeviceVerified(const std::string &user_id, const std::string &key)
{
{ {
std::string_view val; std::string_view val;
@ -4016,11 +4155,12 @@ Cache::markDeviceVerified(const std::string &user_id, const std::string &key)
if (device == key) if (device == key)
return; return;
verified_state.device_verified.push_back(key); verified_state.device_verified.insert(key);
db.put(txn, user_id, json(verified_state).dump()); db.put(txn, user_id, json(verified_state).dump());
txn.commit(); txn.commit();
} catch (std::exception &) { } catch (std::exception &) {
} }
}
const auto local_user = utils::localUser().toStdString(); const auto local_user = utils::localUser().toStdString();
std::map<std::string, VerificationStatus> tmp; std::map<std::string, VerificationStatus> tmp;
@ -4057,11 +4197,7 @@ Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
verified_state = json::parse(val); verified_state = json::parse(val);
} }
verified_state.device_verified.erase( verified_state.device_verified.erase(key);
std::remove(verified_state.device_verified.begin(),
verified_state.device_verified.end(),
key),
verified_state.device_verified.end());
db.put(txn, user_id, json(verified_state).dump()); db.put(txn, user_id, json(verified_state).dump());
txn.commit(); txn.commit();
@ -4090,6 +4226,13 @@ Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
VerificationStatus VerificationStatus
Cache::verificationStatus(const std::string &user_id) Cache::verificationStatus(const std::string &user_id)
{
auto txn = ro_txn(env_);
return verificationStatus_(user_id, txn);
}
VerificationStatus
Cache::verificationStatus_(const std::string &user_id, lmdb::txn &txn)
{ {
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx); std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
if (verification_storage.status.count(user_id)) if (verification_storage.status.count(user_id))
@ -4097,7 +4240,12 @@ Cache::verificationStatus(const std::string &user_id)
VerificationStatus status; VerificationStatus status;
if (auto verifCache = verificationCache(user_id)) { // assume there is at least one unverified device until we have checked we have the device
// list for that user.
status.unverified_device_count = 1;
status.no_keys = true;
if (auto verifCache = verificationCache(user_id, txn)) {
status.verified_devices = verifCache->device_verified; status.verified_devices = verifCache->device_verified;
} }
@ -4105,12 +4253,10 @@ Cache::verificationStatus(const std::string &user_id)
crypto::Trust trustlevel = crypto::Trust::Unverified; crypto::Trust trustlevel = crypto::Trust::Unverified;
if (user_id == local_user) { if (user_id == local_user) {
status.verified_devices.push_back(http::client()->device_id()); status.verified_devices.insert(http::client()->device_id());
trustlevel = crypto::Trust::Verified; trustlevel = crypto::Trust::Verified;
} }
verification_storage.status[user_id] = status;
auto verifyAtLeastOneSig = [](const auto &toVerif, auto verifyAtLeastOneSig = [](const auto &toVerif,
const std::map<std::string, std::string> &keys, const std::map<std::string, std::string> &keys,
const std::string &keyOwner) { const std::string &keyOwner) {
@ -4128,6 +4274,16 @@ Cache::verificationStatus(const std::string &user_id)
return false; return false;
}; };
auto updateUnverifiedDevices = [&status](auto &theirDeviceKeys) {
int currentVerifiedDevices = 0;
for (auto device_id : status.verified_devices) {
if (theirDeviceKeys.count(device_id))
currentVerifiedDevices++;
}
status.unverified_device_count =
static_cast<int>(theirDeviceKeys.size()) - currentVerifiedDevices;
};
try { try {
// for local user verify this device_key -> our master_key -> our self_signing_key // for local user verify this device_key -> our master_key -> our self_signing_key
// -> our device_keys // -> our device_keys
@ -4137,17 +4293,27 @@ Cache::verificationStatus(const std::string &user_id)
// //
// This means verifying the other user adds 2 extra steps,verifying our user_signing // This means verifying the other user adds 2 extra steps,verifying our user_signing
// key and their master key // key and their master key
auto ourKeys = userKeys(local_user); auto ourKeys = userKeys_(local_user, txn);
auto theirKeys = userKeys(user_id); auto theirKeys = userKeys_(user_id, txn);
if (!ourKeys || !theirKeys) if (theirKeys)
status.no_keys = false;
if (!ourKeys || !theirKeys) {
verification_storage.status[user_id] = status;
return status; return status;
}
// Update verified devices count to count without cross-signing
updateUnverifiedDevices(theirKeys->device_keys);
if (!mtx::crypto::ed25519_verify_signature( if (!mtx::crypto::ed25519_verify_signature(
olm::client()->identity_keys().ed25519, olm::client()->identity_keys().ed25519,
json(ourKeys->master_keys), json(ourKeys->master_keys),
ourKeys->master_keys.signatures.at(local_user) ourKeys->master_keys.signatures.at(local_user)
.at("ed25519:" + http::client()->device_id()))) .at("ed25519:" + http::client()->device_id()))) {
verification_storage.status[user_id] = status;
return status; return status;
}
auto master_keys = ourKeys->master_keys.keys; auto master_keys = ourKeys->master_keys.keys;
@ -4162,14 +4328,17 @@ Cache::verificationStatus(const std::string &user_id)
trustlevel = crypto::Trust::Verified; trustlevel = crypto::Trust::Verified;
else if (!theirKeys->master_key_changed) else if (!theirKeys->master_key_changed)
trustlevel = crypto::Trust::TOFU; trustlevel = crypto::Trust::TOFU;
else else {
verification_storage.status[user_id] = status;
return status; return status;
}
master_keys = theirKeys->master_keys.keys; master_keys = theirKeys->master_keys.keys;
} }
status.user_verified = trustlevel; status.user_verified = trustlevel;
verification_storage.status[user_id] = status;
if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id)) if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id))
return status; return status;
@ -4180,16 +4349,19 @@ Cache::verificationStatus(const std::string &user_id)
device_key.keys.at("curve25519:" + device_key.device_id); device_key.keys.at("curve25519:" + device_key.device_id);
if (verifyAtLeastOneSig( if (verifyAtLeastOneSig(
device_key, theirKeys->self_signing_keys.keys, user_id)) { device_key, theirKeys->self_signing_keys.keys, user_id)) {
status.verified_devices.push_back(device_key.device_id); status.verified_devices.insert(device_key.device_id);
status.verified_device_keys[identkey] = trustlevel; status.verified_device_keys[identkey] = trustlevel;
} }
} catch (...) { } catch (...) {
} }
} }
updateUnverifiedDevices(theirKeys->device_keys);
verification_storage.status[user_id] = status; verification_storage.status[user_id] = status;
return status; return status;
} catch (std::exception &) { } catch (std::exception &e) {
nhlog::db()->error(
"Failed to calculate verification status of {}: {}", user_id, e.what());
return status; return status;
} }
} }

View file

@ -112,9 +112,13 @@ struct VerificationStatus
//! True, if the users master key is verified //! True, if the users master key is verified
crypto::Trust user_verified = crypto::Trust::Unverified; crypto::Trust user_verified = crypto::Trust::Unverified;
//! List of all devices marked as verified //! List of all devices marked as verified
std::vector<std::string> verified_devices; std::set<std::string> verified_devices;
//! Map from sender key/curve25519 to trust status //! Map from sender key/curve25519 to trust status
std::map<std::string, crypto::Trust> verified_device_keys; std::map<std::string, crypto::Trust> verified_device_keys;
//! Count of unverified devices
int unverified_device_count = 0;
// if the keys are not in cache
bool no_keys = false;
}; };
//! In memory cache of verification status //! In memory cache of verification status
@ -154,9 +158,9 @@ from_json(const nlohmann::json &j, UserKeyCache &info);
struct VerificationCache struct VerificationCache
{ {
//! list of verified device_ids with device-verification //! list of verified device_ids with device-verification
std::vector<std::string> device_verified; std::set<std::string> device_verified;
//! list of devices the user blocks //! list of devices the user blocks
std::vector<std::string> device_blocked; std::set<std::string> device_blocked;
}; };
void void

View file

@ -46,7 +46,6 @@ public:
std::string statusMessage(const std::string &user_id); std::string statusMessage(const std::string &user_id);
// user cache stores user keys // user cache stores user keys
std::optional<UserKeyCache> userKeys(const std::string &user_id);
std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys( std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys(
const std::string &room_id, const std::string &room_id,
bool verified_only); bool verified_only);
@ -63,9 +62,11 @@ public:
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb); std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb);
// device & user verification cache // device & user verification cache
std::optional<UserKeyCache> userKeys(const std::string &user_id);
VerificationStatus verificationStatus(const std::string &user_id); VerificationStatus verificationStatus(const std::string &user_id);
void markDeviceVerified(const std::string &user_id, const std::string &device); void markDeviceVerified(const std::string &user_id, const std::string &device);
void markDeviceUnverified(const std::string &user_id, const std::string &device); void markDeviceUnverified(const std::string &user_id, const std::string &device);
crypto::Trust roomVerificationStatus(const std::string &room_id);
std::vector<std::string> joinedRooms(); std::vector<std::string> joinedRooms();
@ -414,8 +415,10 @@ private:
if constexpr (isStateEvent_<decltype(e)>) { if constexpr (isStateEvent_<decltype(e)>) {
eventsDb.put(txn, e.event_id, json(e).dump()); eventsDb.put(txn, e.event_id, json(e).dump());
if (e.type != EventType::Unsupported) {
if (std::is_same_v< if (std::is_same_v<
std::remove_cv_t<std::remove_reference_t<decltype(e)>>, std::remove_cv_t<
std::remove_reference_t<decltype(e)>>,
StateEvent<mtx::events::msg::Redacted>>) { StateEvent<mtx::events::msg::Redacted>>) {
if (e.type == EventType::RoomMember) if (e.type == EventType::RoomMember)
membersdb.del(txn, e.state_key, ""); membersdb.del(txn, e.state_key, "");
@ -430,8 +433,7 @@ private:
{"id", e.event_id}, {"id", e.event_id},
}) })
.dump()); .dump());
} else if (e.type != EventType::Unsupported) { } else if (e.state_key.empty())
if (e.state_key.empty())
statesdb.put( statesdb.put(
txn, to_string(e.type), json(e).dump()); txn, to_string(e.type), json(e).dump());
else else
@ -680,7 +682,10 @@ private:
return QString::fromStdString(event.state_key); return QString::fromStdString(event.state_key);
} }
std::optional<VerificationCache> verificationCache(const std::string &user_id); std::optional<VerificationCache> verificationCache(const std::string &user_id,
lmdb::txn &txn);
VerificationStatus verificationStatus_(const std::string &user_id, lmdb::txn &txn);
std::optional<UserKeyCache> userKeys_(const std::string &user_id, lmdb::txn &txn);
void setNextBatchToken(lmdb::txn &txn, const std::string &token); void setNextBatchToken(lmdb::txn &txn, const std::string &token);
void setNextBatchToken(lmdb::txn &txn, const QString &token); void setNextBatchToken(lmdb::txn &txn, const QString &token);

View file

@ -4,11 +4,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#include <QApplication> #include <QApplication>
#include <QImageReader>
#include <QInputDialog> #include <QInputDialog>
#include <QMessageBox> #include <QMessageBox>
#include <QSettings> #include <QSettings>
#include <QShortcut>
#include <mtx/responses.hpp> #include <mtx/responses.hpp>

View file

@ -17,10 +17,8 @@
#include <mtx/events/presence.hpp> #include <mtx/events/presence.hpp>
#include <mtx/secret_storage.hpp> #include <mtx/secret_storage.hpp>
#include <QFrame>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMap> #include <QMap>
#include <QPixmap>
#include <QPoint> #include <QPoint>
#include <QTimer> #include <QTimer>
#include <QWidget> #include <QWidget>

View file

@ -53,6 +53,7 @@ MemberList::roleNames() const
{Mxid, "mxid"}, {Mxid, "mxid"},
{DisplayName, "displayName"}, {DisplayName, "displayName"},
{AvatarUrl, "avatarUrl"}, {AvatarUrl, "avatarUrl"},
{Trustlevel, "trustlevel"},
}; };
} }
@ -69,6 +70,17 @@ MemberList::data(const QModelIndex &index, int role) const
return m_memberList[index.row()].first.display_name; return m_memberList[index.row()].first.display_name;
case AvatarUrl: case AvatarUrl:
return m_memberList[index.row()].second; return m_memberList[index.row()].second;
case Trustlevel: {
auto stat =
cache::verificationStatus(m_memberList[index.row()].first.user_id.toStdString());
if (!stat)
return crypto::Unverified;
if (stat->unverified_device_count)
return crypto::Unverified;
else
return stat->user_verified;
}
default: default:
return {}; return {};
} }

View file

@ -25,6 +25,7 @@ public:
Mxid, Mxid,
DisplayName, DisplayName,
AvatarUrl, AvatarUrl,
Trustlevel,
}; };
MemberList(const QString &room_id, QObject *parent = nullptr); MemberList(const QString &room_id, QObject *parent = nullptr);

View file

@ -11,6 +11,8 @@
#include <QByteArray> #include <QByteArray>
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QPainter>
#include <QPainterPath>
#include <QStandardPaths> #include <QStandardPaths>
#include "Logging.h" #include "Logging.h"
@ -24,12 +26,24 @@ MxcImageProvider::requestImageResponse(const QString &id, const QSize &requested
{ {
auto id_ = id; auto id_ = id;
bool crop = true; bool crop = true;
if (id.endsWith("?scale")) { double radius = 0;
auto queryStart = id.lastIndexOf('?');
if (queryStart != -1) {
id_ = id.left(queryStart);
auto query = id.midRef(queryStart + 1);
auto queryBits = query.split('&');
for (auto b : queryBits) {
if (b == "scale") {
crop = false; crop = false;
id_.remove("?scale"); } else if (b.startsWith("radius=")) {
radius = b.mid(7).toDouble();
}
}
} }
MxcImageResponse *response = new MxcImageResponse(id_, crop, requestedSize); MxcImageResponse *response = new MxcImageResponse(id_, crop, radius, requestedSize);
pool.start(response); pool.start(response);
return response; return response;
} }
@ -53,14 +67,35 @@ MxcImageResponse::run()
} }
emit finished(); emit finished();
}, },
m_crop); m_crop,
m_radius);
}
static QImage
clipRadius(QImage img, double radius)
{
QImage out(img.size(), QImage::Format_ARGB32_Premultiplied);
out.fill(Qt::transparent);
QPainter painter(&out);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
QPainterPath ppath;
ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize);
painter.setClipPath(ppath);
painter.drawImage(img.rect(), img);
return out;
} }
void void
MxcImageProvider::download(const QString &id, MxcImageProvider::download(const QString &id,
const QSize &requestedSize, const QSize &requestedSize,
std::function<void(QString, QSize, QImage, QString)> then, std::function<void(QString, QSize, QImage, QString)> then,
bool crop) bool crop,
double radius)
{ {
std::optional<mtx::crypto::EncryptedFile> encryptionInfo; std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
auto temp = infos.find("mxc://" + id); auto temp = infos.find("mxc://" + id);
@ -69,12 +104,13 @@ MxcImageProvider::download(const QString &id,
if (requestedSize.isValid() && !encryptionInfo) { if (requestedSize.isValid() && !encryptionInfo) {
QString fileName = QString fileName =
QString("%1_%2x%3_%4") QString("%1_%2x%3_%4_radius%5")
.arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding | .arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
QByteArray::OmitTrailingEquals))) QByteArray::OmitTrailingEquals)))
.arg(requestedSize.width()) .arg(requestedSize.width())
.arg(requestedSize.height()) .arg(requestedSize.height())
.arg(crop ? "crop" : "scale"); .arg(crop ? "crop" : "scale")
.arg(radius);
QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache", "/media_cache",
fileName); fileName);
@ -86,6 +122,10 @@ MxcImageProvider::download(const QString &id,
image = image.scaled( image = image.scaled(
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
if (radius != 0) {
image = clipRadius(std::move(image), radius);
}
if (!image.isNull()) { if (!image.isNull()) {
then(id, requestedSize, image, fileInfo.absoluteFilePath()); then(id, requestedSize, image, fileInfo.absoluteFilePath());
return; return;
@ -100,7 +140,7 @@ MxcImageProvider::download(const QString &id,
opts.method = crop ? "crop" : "scale"; opts.method = crop ? "crop" : "scale";
http::client()->get_thumbnail( http::client()->get_thumbnail(
opts, opts,
[fileInfo, requestedSize, then, id](const std::string &res, [fileInfo, requestedSize, radius, then, id](const std::string &res,
mtx::http::RequestErr err) { mtx::http::RequestErr err) {
if (err || res.empty()) { if (err || res.empty()) {
then(id, QSize(), {}, ""); then(id, QSize(), {}, "");
@ -113,6 +153,10 @@ MxcImageProvider::download(const QString &id,
if (!image.isNull()) { if (!image.isNull()) {
image = image.scaled( image = image.scaled(
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
if (radius != 0) {
image = clipRadius(std::move(image), radius);
}
} }
image.setText("mxc url", "mxc://" + id); image.setText("mxc url", "mxc://" + id);
if (image.save(fileInfo.absoluteFilePath(), "png")) if (image.save(fileInfo.absoluteFilePath(), "png"))
@ -126,8 +170,12 @@ MxcImageProvider::download(const QString &id,
}); });
} else { } else {
try { try {
QString fileName = QString::fromUtf8(id.toUtf8().toBase64( QString fileName =
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); QString("%1_radius%2")
.arg(QString::fromUtf8(id.toUtf8().toBase64(
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)))
.arg(radius);
QFileInfo fileInfo( QFileInfo fileInfo(
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache", "/media_cache",
@ -148,6 +196,11 @@ MxcImageProvider::download(const QString &id,
QImage image = utils::readImage(data); QImage image = utils::readImage(data);
image.setText("mxc url", "mxc://" + id); image.setText("mxc url", "mxc://" + id);
if (!image.isNull()) { if (!image.isNull()) {
if (radius != 0) {
image =
clipRadius(std::move(image), radius);
}
then(id, then(id,
requestedSize, requestedSize,
image, image,
@ -158,6 +211,11 @@ MxcImageProvider::download(const QString &id,
QImage image = QImage image =
utils::readImageFromFile(fileInfo.absoluteFilePath()); utils::readImageFromFile(fileInfo.absoluteFilePath());
if (!image.isNull()) { if (!image.isNull()) {
if (radius != 0) {
image =
clipRadius(std::move(image), radius);
}
then(id, then(id,
requestedSize, requestedSize,
image, image,
@ -169,7 +227,7 @@ MxcImageProvider::download(const QString &id,
http::client()->download( http::client()->download(
"mxc://" + id.toStdString(), "mxc://" + id.toStdString(),
[fileInfo, requestedSize, then, id, encryptionInfo]( [fileInfo, requestedSize, then, id, radius, encryptionInfo](
const std::string &res, const std::string &res,
const std::string &, const std::string &,
const std::string &originalFilename, const std::string &originalFilename,
@ -195,6 +253,10 @@ MxcImageProvider::download(const QString &id,
auto data = auto data =
QByteArray(tempData.data(), (int)tempData.size()); QByteArray(tempData.data(), (int)tempData.size());
QImage image = utils::readImage(data); QImage image = utils::readImage(data);
if (radius != 0) {
image = clipRadius(std::move(image), radius);
}
image.setText("original filename", image.setText("original filename",
QString::fromStdString(originalFilename)); QString::fromStdString(originalFilename));
image.setText("mxc url", "mxc://" + id); image.setText("mxc url", "mxc://" + id);
@ -205,6 +267,10 @@ MxcImageProvider::download(const QString &id,
QImage image = QImage image =
utils::readImageFromFile(fileInfo.absoluteFilePath()); utils::readImageFromFile(fileInfo.absoluteFilePath());
if (radius != 0) {
image = clipRadius(std::move(image), radius);
}
image.setText("original filename", image.setText("original filename",
QString::fromStdString(originalFilename)); QString::fromStdString(originalFilename));
image.setText("mxc url", "mxc://" + id); image.setText("mxc url", "mxc://" + id);

View file

@ -19,10 +19,11 @@ class MxcImageResponse
, public QRunnable , public QRunnable
{ {
public: public:
MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize) MxcImageResponse(const QString &id, bool crop, double radius, const QSize &requestedSize)
: m_id(id) : m_id(id)
, m_requestedSize(requestedSize) , m_requestedSize(requestedSize)
, m_crop(crop) , m_crop(crop)
, m_radius(radius)
{ {
setAutoDelete(false); setAutoDelete(false);
} }
@ -39,6 +40,7 @@ public:
QSize m_requestedSize; QSize m_requestedSize;
QImage m_image; QImage m_image;
bool m_crop; bool m_crop;
double m_radius;
}; };
class MxcImageProvider class MxcImageProvider
@ -54,7 +56,8 @@ public slots:
static void download(const QString &id, static void download(const QString &id,
const QSize &requestedSize, const QSize &requestedSize,
std::function<void(QString, QSize, QImage, QString)> then, std::function<void(QString, QSize, QImage, QString)> then,
bool crop = true); bool crop = true,
double radius = 0);
private: private:
QThreadPool pool; QThreadPool pool;

View file

@ -425,6 +425,8 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
} }
}); });
nhlog::crypto()->info("Storing secret {}",
secret_name->second);
cache::client()->storeSecret(secret_name->second, cache::client()->storeSecret(secret_name->second,
e->content.secret); e->content.secret);
@ -1110,6 +1112,8 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
const mtx::events::collections::DeviceEvents &event, const mtx::events::collections::DeviceEvents &event,
bool force_new_session) bool force_new_session)
{ {
static QMap<QPair<std::string, std::string>, qint64> rateLimit;
nlohmann::json ev_json = std::visit([](const auto &e) { return json(e); }, event); nlohmann::json ev_json = std::visit([](const auto &e) { return json(e); }, event);
std::map<std::string, std::vector<std::string>> keysToQuery; std::map<std::string, std::vector<std::string>> keysToQuery;
@ -1162,7 +1166,6 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
auto session = cache::getLatestOlmSession(device_curve); auto session = cache::getLatestOlmSession(device_curve);
if (!session || force_new_session) { if (!session || force_new_session) {
static QMap<QPair<std::string, std::string>, qint64> rateLimit;
auto currentTime = QDateTime::currentSecsSinceEpoch(); auto currentTime = QDateTime::currentSecsSinceEpoch();
if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 < if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 <
currentTime) { currentTime) {
@ -1318,6 +1321,7 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
}; };
}; };
if (!claims.one_time_keys.empty())
http::client()->claim_keys(claims, BindPks(pks)); http::client()->claim_keys(claims, BindPks(pks));
if (!keysToQuery.empty()) { if (!keysToQuery.empty()) {
@ -1395,16 +1399,33 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
continue; continue;
} }
auto currentTime = QDateTime::currentSecsSinceEpoch();
if (rateLimit.value(QPair(user.first, device_id.get())) +
60 * 60 * 10 <
currentTime) {
deviceKeys[user_id].emplace(device_id, pks); deviceKeys[user_id].emplace(device_id, pks);
claim_keys.one_time_keys[user.first][device_id] = claim_keys.one_time_keys[user.first][device_id] =
mtx::crypto::SIGNED_CURVE25519; mtx::crypto::SIGNED_CURVE25519;
rateLimit.insert(
QPair(user.first, device_id.get()),
currentTime);
} else {
nhlog::crypto()->warn(
"Not creating new session with {}:{} "
"because of rate limit",
user.first,
device_id.get());
continue;
}
nhlog::net()->info("{}", device_id.get()); nhlog::net()->info("{}", device_id.get());
nhlog::net()->info(" curve25519 {}", pks.curve25519); nhlog::net()->info(" curve25519 {}", pks.curve25519);
nhlog::net()->info(" ed25519 {}", pks.ed25519); nhlog::net()->info(" ed25519 {}", pks.ed25519);
} }
} }
if (!claim_keys.one_time_keys.empty())
http::client()->claim_keys(claim_keys, BindPks(deviceKeys)); http::client()->claim_keys(claim_keys, BindPks(deviceKeys));
}); });
} }

View file

@ -3,6 +3,7 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#include <QInputDialog>
#include <QLabel> #include <QLabel>
#include <QMetaType> #include <QMetaType>
#include <QPainter> #include <QPainter>
@ -481,6 +482,23 @@ RegisterPage::doUIA(const mtx::user_interactive::Unauthorized &unauthorized)
doRegistrationWithAuth( doRegistrationWithAuth(
mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Dummy{}}); mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Dummy{}});
} else if (current_stage == mtx::user_interactive::auth_types::registration_token) {
bool ok;
QString token =
QInputDialog::getText(this,
tr("Registration token"),
tr("Please enter a valid registration token."),
QLineEdit::Normal,
QString(),
&ok);
if (ok) {
emit registrationWithAuth(mtx::user_interactive::Auth{
session,
mtx::user_interactive::auth::RegistrationToken{token.toStdString()}});
} else {
emit errorOccurred();
}
} else { } else {
// use fallback // use fallback
auto dialog = new dialogs::FallbackAuth( auto dialog = new dialogs::FallbackAuth(

View file

@ -19,7 +19,6 @@
#include <QResizeEvent> #include <QResizeEvent>
#include <QScrollArea> #include <QScrollArea>
#include <QScroller> #include <QScroller>
#include <QSettings>
#include <QSpinBox> #include <QSpinBox>
#include <QStandardPaths> #include <QStandardPaths>
#include <QString> #include <QString>
@ -63,7 +62,6 @@ UserSettings::initialize(std::optional<QString> profile)
void void
UserSettings::load(std::optional<QString> profile) UserSettings::load(std::optional<QString> profile)
{ {
QSettings settings;
tray_ = settings.value("user/window/tray", false).toBool(); tray_ = settings.value("user/window/tray", false).toBool();
startInTray_ = settings.value("user/window/start_in_tray", false).toBool(); startInTray_ = settings.value("user/window/start_in_tray", false).toBool();
@ -601,7 +599,6 @@ UserSettings::applyTheme()
void void
UserSettings::save() UserSettings::save()
{ {
QSettings settings;
settings.beginGroup("user"); settings.beginGroup("user");
settings.beginGroup("window"); settings.beginGroup("window");

View file

@ -8,6 +8,7 @@
#include <QFontDatabase> #include <QFontDatabase>
#include <QFrame> #include <QFrame>
#include <QProcessEnvironment> #include <QProcessEnvironment>
#include <QSettings>
#include <QSharedPointer> #include <QSharedPointer>
#include <QWidget> #include <QWidget>
@ -107,6 +108,8 @@ public:
static QSharedPointer<UserSettings> instance(); static QSharedPointer<UserSettings> instance();
static void initialize(std::optional<QString> profile); static void initialize(std::optional<QString> profile);
QSettings *qsettings() { return &settings; }
enum class Presence enum class Presence
{ {
AutomaticPresence, AutomaticPresence,
@ -316,6 +319,8 @@ private:
QString homeserver_; QString homeserver_;
QStringList hiddenTags_; QStringList hiddenTags_;
QSettings settings;
static QSharedPointer<UserSettings> instance_; static QSharedPointer<UserSettings> instance_;
}; };

View file

@ -28,8 +28,10 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent)
setAttribute(Qt::WA_TranslucentBackground, true); setAttribute(Qt::WA_TranslucentBackground, true);
setAttribute(Qt::WA_DeleteOnClose, true); setAttribute(Qt::WA_DeleteOnClose, true);
setWindowState(Qt::WindowFullScreen); setWindowState(Qt::WindowFullScreen);
close_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Escape), this);
connect(this, SIGNAL(closing()), this, SLOT(close())); connect(close_shortcut_, &QShortcut::activated, this, &ImageOverlay::closing);
connect(this, &ImageOverlay::closing, this, &ImageOverlay::close);
raise(); raise();
} }

View file

@ -8,6 +8,7 @@
#include <QDialog> #include <QDialog>
#include <QMouseEvent> #include <QMouseEvent>
#include <QPixmap> #include <QPixmap>
#include <QShortcut>
namespace dialogs { namespace dialogs {
@ -32,5 +33,6 @@ private:
QRect content_; QRect content_;
QRect close_button_; QRect close_button_;
QRect save_button_; QRect save_button_;
QShortcut *close_shortcut_;
}; };
} // dialogs } // dialogs

View file

@ -310,7 +310,7 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t)
return mtx::events::EventType::RoomMessage; return mtx::events::EventType::RoomMessage;
//! m.image_pack, currently im.ponies.room_emotes //! m.image_pack, currently im.ponies.room_emotes
case qml_mtx_events::ImagePackInRoom: case qml_mtx_events::ImagePackInRoom:
return mtx::events::EventType::ImagePackRooms; return mtx::events::EventType::ImagePackInRoom;
//! m.image_pack, currently im.ponies.user_emotes //! m.image_pack, currently im.ponies.user_emotes
case qml_mtx_events::ImagePackInAccountData: case qml_mtx_events::ImagePackInAccountData:
return mtx::events::EventType::ImagePackInAccountData; return mtx::events::EventType::ImagePackInAccountData;
@ -418,6 +418,14 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
&events, &events,
&EventStore::enableKeyRequests); &EventStore::enableKeyRequests);
connect(this, &TimelineModel::encryptionChanged, this, &TimelineModel::trustlevelChanged);
connect(
this, &TimelineModel::roomMemberCountChanged, this, &TimelineModel::trustlevelChanged);
connect(cache::client(),
&Cache::verificationStatusChanged,
this,
&TimelineModel::trustlevelChanged);
showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent); showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent);
} }
@ -1993,6 +2001,15 @@ TimelineModel::roomTopic() const
QString::fromStdString(info[room_id_].topic).toHtmlEscaped())); QString::fromStdString(info[room_id_].topic).toHtmlEscaped()));
} }
crypto::Trust
TimelineModel::trustlevel() const
{
if (!isEncrypted_)
return crypto::Trust::Unverified;
return cache::client()->roomVerificationStatus(room_id_.toStdString());
}
int int
TimelineModel::roomMemberCount() const TimelineModel::roomMemberCount() const
{ {

View file

@ -175,6 +175,7 @@ class TimelineModel : public QAbstractListModel
Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged) Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged)
Q_PROPERTY(bool isEncrypted READ isEncrypted NOTIFY encryptionChanged) Q_PROPERTY(bool isEncrypted READ isEncrypted NOTIFY encryptionChanged)
Q_PROPERTY(bool isSpace READ isSpace CONSTANT) Q_PROPERTY(bool isSpace READ isSpace CONSTANT)
Q_PROPERTY(int trustlevel READ trustlevel NOTIFY trustlevelChanged)
Q_PROPERTY(InputBar *input READ input CONSTANT) Q_PROPERTY(InputBar *input READ input CONSTANT)
Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged) Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged)
@ -287,6 +288,7 @@ public:
DescInfo lastMessage() const { return lastMessage_; } DescInfo lastMessage() const { return lastMessage_; }
bool isSpace() const { return isSpace_; } bool isSpace() const { return isSpace_; }
bool isEncrypted() const { return isEncrypted_; } bool isEncrypted() const { return isEncrypted_; }
crypto::Trust trustlevel() const;
int roomMemberCount() const; int roomMemberCount() const;
public slots: public slots:
@ -372,6 +374,7 @@ signals:
void updateFlowEventId(std::string event_id); void updateFlowEventId(std::string event_id);
void encryptionChanged(); void encryptionChanged();
void trustlevelChanged();
void roomNameChanged(); void roomNameChanged();
void plainRoomNameChanged(); void plainRoomNameChanged();
void roomTopicChanged(); void roomTopicChanged();

View file

@ -375,10 +375,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
} }
void void
TimelineViewManager::openRoomMembers(QString room_id) TimelineViewManager::openRoomMembers(TimelineModel *room)
{ {
MemberList *memberList = new MemberList(room_id, this); if (!room)
emit openRoomMembersDialog(memberList); return;
MemberList *memberList = new MemberList(room->roomId(), this);
emit openRoomMembersDialog(memberList, room);
} }
void void

View file

@ -66,7 +66,7 @@ public:
Q_INVOKABLE QString userPresence(QString id) const; Q_INVOKABLE QString userPresence(QString id) const;
Q_INVOKABLE QString userStatus(QString id) const; Q_INVOKABLE QString userStatus(QString id) const;
Q_INVOKABLE void openRoomMembers(QString room_id); Q_INVOKABLE void openRoomMembers(TimelineModel *room);
Q_INVOKABLE void openRoomSettings(QString room_id); Q_INVOKABLE void openRoomSettings(QString room_id);
Q_INVOKABLE void openInviteUsers(QString roomId); Q_INVOKABLE void openInviteUsers(QString roomId);
Q_INVOKABLE void openGlobalUserProfile(QString userId); Q_INVOKABLE void openGlobalUserProfile(QString userId);
@ -92,7 +92,7 @@ signals:
void focusChanged(); void focusChanged();
void focusInput(); void focusInput();
void openImageOverlayInternalCb(QString eventId, QImage img); void openImageOverlayInternalCb(QString eventId, QImage img);
void openRoomMembersDialog(MemberList *members); void openRoomMembersDialog(MemberList *members, TimelineModel *room);
void openRoomSettingsDialog(RoomSettings *settings); void openRoomSettingsDialog(RoomSettings *settings);
void openInviteUsersDialog(InviteesModel *invitees); void openInviteUsersDialog(InviteesModel *invitees);
void openProfile(UserProfile *profile); void openProfile(UserProfile *profile);