diff options
author | Nicolas Werner <nicolas.werner@hotmail.de> | 2023-02-24 02:40:14 +0100 |
---|---|---|
committer | Nicolas Werner <nicolas.werner@hotmail.de> | 2023-02-24 02:40:14 +0100 |
commit | aae3300860ebe2fac39a156a31f9cddeefb4bf92 (patch) | |
tree | de63d71abe28e9e38e4cfb2c877ce5840e9b6a13 | |
parent | Reenable the nosync options for the database (diff) | |
download | nheko-aae3300860ebe2fac39a156a31f9cddeefb4bf92.tar.xz |
Show rooms you share with someone
-rw-r--r-- | resources/qml/components/NhekoTabButton.qml | 25 | ||||
-rw-r--r-- | resources/qml/dialogs/PowerLevelEditor.qml | 24 | ||||
-rw-r--r-- | resources/qml/dialogs/UserProfile.qml | 286 | ||||
-rw-r--r-- | resources/res.qrc | 1 | ||||
-rw-r--r-- | src/Cache.cpp | 30 | ||||
-rw-r--r-- | src/Cache_p.h | 1 | ||||
-rw-r--r-- | src/ui/UserProfile.cpp | 47 | ||||
-rw-r--r-- | src/ui/UserProfile.h | 27 |
8 files changed, 311 insertions, 130 deletions
diff --git a/resources/qml/components/NhekoTabButton.qml b/resources/qml/components/NhekoTabButton.qml new file mode 100644 index 00000000..5ae8748b --- /dev/null +++ b/resources/qml/components/NhekoTabButton.qml @@ -0,0 +1,25 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import im.nheko 1.0 + +TabButton { + id: control + + contentItem: Text { + text: control.text + font: control.font + opacity: enabled ? 1.0 : 0.3 + color: control.down ? Nheko.colors.highlightedText : Nheko.colors.text + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + border.color: control.down ? Nheko.colors.highlight : Nheko.theme.separator + color: control.checked ? Nheko.colors.highlight : Nheko.colors.base + border.width: 1 + radius: 2 + } +} + diff --git a/resources/qml/dialogs/PowerLevelEditor.qml b/resources/qml/dialogs/PowerLevelEditor.qml index 12458f62..048672e4 100644 --- a/resources/qml/dialogs/PowerLevelEditor.qml +++ b/resources/qml/dialogs/PowerLevelEditor.qml @@ -49,30 +49,10 @@ ApplicationWindow { width: parent.width palette: Nheko.colors - component TabB : TabButton { - id: control - - contentItem: Text { - text: control.text - font: control.font - opacity: enabled ? 1.0 : 0.3 - color: control.down ? Nheko.colors.highlightedText : Nheko.colors.text - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - background: Rectangle { - border.color: control.down ? Nheko.colors.highlight : Nheko.theme.separator - color: control.checked ? Nheko.colors.highlight : Nheko.colors.base - border.width: 1 - radius: 2 - } - } - TabB { + NhekoTabButton { text: qsTr("Roles") } - TabB { + NhekoTabButton { text: qsTr("Users") } } diff --git a/resources/qml/dialogs/UserProfile.qml b/resources/qml/dialogs/UserProfile.qml index c0d4905b..792dec00 100644 --- a/resources/qml/dialogs/UserProfile.qml +++ b/resources/qml/dialogs/UserProfile.qml @@ -5,10 +5,12 @@ import ".." import "../device-verification" import "../ui" +import "../components" import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.2 import QtQuick.Window 2.13 +import QtQml.Models 2.2 import im.nheko 1.0 ApplicationWindow { @@ -34,12 +36,13 @@ ApplicationWindow { ListView { id: devicelist + property int selectedTab: 0 + Layout.fillHeight: true Layout.fillWidth: true clip: true spacing: 8 boundsBehavior: Flickable.StopAtBounds - model: profile.deviceList anchors.fill: parent anchors.margins: 10 footerPositioning: ListView.OverlayFooter @@ -297,147 +300,214 @@ ApplicationWindow { } + TabBar { + id: tabbar + visible: !profile.isSelf + Layout.fillWidth: true + + onCurrentIndexChanged: devicelist.selectedTab = currentIndex + + palette: Nheko.colors + + NhekoTabButton { + text: qsTr("Devices") + } + NhekoTabButton { + text: qsTr("Shared Rooms") + } + + Layout.bottomMargin: Nheko.paddingMedium + } } - delegate: RowLayout { - required property int verificationStatus - required property string deviceId - required property string deviceName - required property string lastIp - required property var lastTs + model: (selectedTab == 0) ? devicesModel : sharedRoomsModel + + DelegateModel { + id: devicesModel + model: profile.deviceList + delegate: RowLayout { + required property int verificationStatus + required property string deviceId + required property string deviceName + required property string lastIp + required property var lastTs + + width: devicelist.width + spacing: 4 + + ColumnLayout { + spacing: 0 + + Layout.leftMargin: Nheko.paddingMedium + Layout.rightMargin: Nheko.paddingMedium + RowLayout { + Text { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + elide: Text.ElideRight + font.bold: true + color: Nheko.colors.text + text: deviceId + } - width: devicelist.width - spacing: 4 + Image { + Layout.preferredHeight: 16 + Layout.preferredWidth: 16 + visible: profile.isSelf && verificationStatus != VerificationStatus.NOT_APPLICABLE + sourceSize.height: 16 * Screen.devicePixelRatio + sourceSize.width: 16 * Screen.devicePixelRatio + source: { + switch (verificationStatus) { + case VerificationStatus.VERIFIED: + return "image://colorimage/:/icons/icons/ui/shield-filled-checkmark.svg?" + Nheko.theme.green; + case VerificationStatus.UNVERIFIED: + return "image://colorimage/:/icons/icons/ui/shield-filled-exclamation-mark.svg?" + Nheko.theme.orange; + case VerificationStatus.SELF: + return "image://colorimage/:/icons/icons/ui/checkmark.svg?" + Nheko.theme.green; + default: + return "image://colorimage/:/icons/icons/ui/shield-filled-cross.svg?" + Nheko.theme.orange; + } + } + } - ColumnLayout { - spacing: 0 + ImageButton { + Layout.alignment: Qt.AlignTop + image: ":/icons/icons/ui/power-off.svg" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Sign out this device.") + onClicked: profile.signOutDevice(deviceId) + visible: profile.isSelf + } + + } + + RowLayout { + id: deviceNameRow + + property bool isEditingAllowed + + TextInput { + id: deviceNameField + + readOnly: !deviceNameRow.isEditingAllowed + text: deviceName + color: Nheko.colors.text + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + selectByMouse: true + onAccepted: { + profile.changeDeviceName(deviceId, deviceNameField.text); + deviceNameRow.isEditingAllowed = false; + } + } + + ImageButton { + visible: profile.isSelf + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Change device name.") + image: deviceNameRow.isEditingAllowed ? ":/icons/icons/ui/checkmark.svg" : ":/icons/icons/ui/edit.svg" + onClicked: { + if (deviceNameRow.isEditingAllowed) { + profile.changeDeviceName(deviceId, deviceNameField.text); + deviceNameRow.isEditingAllowed = false; + } else { + deviceNameRow.isEditingAllowed = true; + deviceNameField.focus = true; + deviceNameField.selectAll(); + } + } + } + + } - Layout.leftMargin: Nheko.paddingMedium - Layout.rightMargin: Nheko.paddingMedium - RowLayout { Text { + visible: profile.isSelf Layout.fillWidth: true Layout.alignment: Qt.AlignLeft elide: Text.ElideRight - font.bold: true color: Nheko.colors.text - text: deviceId + text: qsTr("Last seen %1 from %2").arg(new Date(lastTs).toLocaleString(Locale.ShortFormat)).arg(lastIp ? lastIp : "???") } - Image { - Layout.preferredHeight: 16 - Layout.preferredWidth: 16 - visible: profile.isSelf && verificationStatus != VerificationStatus.NOT_APPLICABLE - sourceSize.height: 16 * Screen.devicePixelRatio - sourceSize.width: 16 * Screen.devicePixelRatio - source: { - switch (verificationStatus) { + } + + Image { + Layout.preferredHeight: 16 + Layout.preferredWidth: 16 + visible: !profile.isSelf && verificationStatus != VerificationStatus.NOT_APPLICABLE + source: { + switch (verificationStatus) { case VerificationStatus.VERIFIED: - return "image://colorimage/:/icons/icons/ui/shield-filled-checkmark.svg?" + Nheko.theme.green; + return "image://colorimage/:/icons/icons/ui/shield-filled-checkmark.svg?" + Nheko.theme.green; case VerificationStatus.UNVERIFIED: - return "image://colorimage/:/icons/icons/ui/shield-filled-exclamation-mark.svg?" + Nheko.theme.orange; + return "image://colorimage/:/icons/icons/ui/shield-filled-exclamation-mark.svg?" + Nheko.theme.orange; case VerificationStatus.SELF: - return "image://colorimage/:/icons/icons/ui/checkmark.svg?" + Nheko.theme.green; + return "image://colorimage/:/icons/icons/ui/checkmark.svg?" + Nheko.theme.green; default: - return "image://colorimage/:/icons/icons/ui/shield-filled-cross.svg?" + Nheko.theme.orange; - } + return "image://colorimage/:/icons/icons/ui/shield-filled.svg?" + Nheko.theme.red; } } + } - ImageButton { - Layout.alignment: Qt.AlignTop - image: ":/icons/icons/ui/power-off.svg" - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("Sign out this device.") - onClicked: profile.signOutDevice(deviceId) - visible: profile.isSelf - } + Button { + id: verifyButton + visible: verificationStatus == VerificationStatus.UNVERIFIED && (profile.isSelf || !profile.userVerificationEnabled) + text: (verificationStatus != VerificationStatus.VERIFIED) ? qsTr("Verify") : qsTr("Unverify") + onClicked: { + if (verificationStatus == VerificationStatus.VERIFIED) + profile.unverify(deviceId); + else + profile.verify(deviceId); + } } - RowLayout { - id: deviceNameRow + } + } - property bool isEditingAllowed + DelegateModel { + id: sharedRoomsModel + model: profile.sharedRooms + delegate: RowLayout { + required property string roomId + required property string roomName + required property string avatarUrl - TextInput { - id: deviceNameField + width: devicelist.width + spacing: 4 - readOnly: !deviceNameRow.isEditingAllowed - text: deviceName - color: Nheko.colors.text - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - selectByMouse: true - onAccepted: { - profile.changeDeviceName(deviceId, deviceNameField.text); - deviceNameRow.isEditingAllowed = false; - } - } - ImageButton { - visible: profile.isSelf - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("Change device name.") - image: deviceNameRow.isEditingAllowed ? ":/icons/icons/ui/checkmark.svg" : ":/icons/icons/ui/edit.svg" - onClicked: { - if (deviceNameRow.isEditingAllowed) { - profile.changeDeviceName(deviceId, deviceNameField.text); - deviceNameRow.isEditingAllowed = false; - } else { - deviceNameRow.isEditingAllowed = true; - deviceNameField.focus = true; - deviceNameField.selectAll(); - } - } - } + Avatar { + id: avatar - } + enabled: false + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Nheko.paddingMedium - Text { - visible: profile.isSelf - Layout.fillWidth: true - Layout.alignment: Qt.AlignLeft - elide: Text.ElideRight - color: Nheko.colors.text - text: qsTr("Last seen %1 from %2").arg(new Date(lastTs).toLocaleString(Locale.ShortFormat)).arg(lastIp ? lastIp : "???") + property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 1.6) + height: avatarSize + width: avatarSize + url: avatarUrl.replace("mxc://", "image://MxcImage/") + roomid: roomId + displayName: roomName } - } - - Image { - Layout.preferredHeight: 16 - Layout.preferredWidth: 16 - visible: !profile.isSelf && verificationStatus != VerificationStatus.NOT_APPLICABLE - source: { - switch (verificationStatus) { - case VerificationStatus.VERIFIED: - return "image://colorimage/:/icons/icons/ui/shield-filled-checkmark.svg?" + Nheko.theme.green; - case VerificationStatus.UNVERIFIED: - return "image://colorimage/:/icons/icons/ui/shield-filled-exclamation-mark.svg?" + Nheko.theme.orange; - case VerificationStatus.SELF: - return "image://colorimage/:/icons/icons/ui/checkmark.svg?" + Nheko.theme.green; - default: - return "image://colorimage/:/icons/icons/ui/shield-filled.svg?" + Nheko.theme.red; - } + ElidedLabel { + Layout.alignment: Qt.AlignVCenter + color: Nheko.colors.text + Layout.fillWidth: true + elideWidth: width + fullText: roomName + textFormat: Text.PlainText + Layout.rightMargin: Nheko.paddingMedium } - } - - Button { - id: verifyButton - visible: verificationStatus == VerificationStatus.UNVERIFIED && (profile.isSelf || !profile.userVerificationEnabled) - text: (verificationStatus != VerificationStatus.VERIFIED) ? qsTr("Verify") : qsTr("Unverify") - onClicked: { - if (verificationStatus == VerificationStatus.VERIFIED) - profile.unverify(deviceId); - else - profile.verify(deviceId); + Item { + Layout.fillWidth: true } } - } footer: DialogButtonBox { diff --git a/resources/res.qrc b/resources/res.qrc index 9663b5a3..88159d40 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -129,6 +129,7 @@ <file>qml/components/AvatarListTile.qml</file> <file>qml/components/FlatButton.qml</file> <file>qml/components/MainWindowDialog.qml</file> + <file>qml/components/NhekoTabButton.qml</file> <file>qml/components/NotificationBubble.qml</file> <file>qml/components/ReorderableListview.qml</file> <file>qml/components/SpaceMenuLevel.qml</file> diff --git a/src/Cache.cpp b/src/Cache.cpp index b27a8b37..6c746d4b 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -3146,6 +3146,36 @@ Cache::joinedRooms() return room_ids; } +std::map<std::string, RoomInfo> +Cache::getCommonRooms(const std::string &user_id) +{ + std::map<std::string, RoomInfo> result; + + auto txn = ro_txn(env_); + + std::string_view room_id; + std::string_view room_data; + std::string_view member_info; + + auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); + while (roomsCursor.get(room_id, room_data, MDB_NEXT)) { + try { + if (getMembersDb(txn, std::string(room_id)).get(txn, user_id, member_info)) { + RoomInfo tmp = nlohmann::json::parse(std::move(room_data)).get<RoomInfo>(); + result.emplace(std::string(room_id), std::move(tmp)); + } + } catch (std::exception &e) { + nhlog::db()->warn("Failed to read common room for member ({}) in room ({}): {}", + user_id, + room_id, + e.what()); + } + } + roomsCursor.close(); + + return result; +} + std::optional<MemberInfo> Cache::getMember(const std::string &room_id, const std::string &user_id) { diff --git a/src/Cache_p.h b/src/Cache_p.h index 5a4f9afb..38cadfc9 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -64,6 +64,7 @@ public: crypto::Trust roomVerificationStatus(const std::string &room_id); std::vector<std::string> joinedRooms(); + std::map<std::string, RoomInfo> getCommonRooms(const std::string &user_id); QMap<QString, RoomInfo> roomInfo(bool withInvites = true); QHash<QString, RoomInfo> invites(); diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp index 66a68bb8..80def409 100644 --- a/src/ui/UserProfile.cpp +++ b/src/ui/UserProfile.cpp @@ -58,6 +58,12 @@ UserProfile::UserProfile(const QString &roomid, emit verificationStatiChanged(); }); fetchDeviceList(this->userid_); + + if (userid != utils::localUser()) + sharedRooms_ = + new RoomInfoModel(cache::client()->getCommonRooms(userid.toStdString()), this); + else + sharedRooms_ = new RoomInfoModel({}, this); } QHash<int, QByteArray> @@ -102,12 +108,53 @@ DeviceInfoModel::reset(const std::vector<DeviceInfo> &deviceList) endResetModel(); } +RoomInfoModel::RoomInfoModel(const std::map<std::string, RoomInfo> &raw, QObject *parent) + : QAbstractListModel(parent) +{ + for (const auto &e : raw) + roomInfos_.push_back(e); +} + +QHash<int, QByteArray> +RoomInfoModel::roleNames() const +{ + return { + {RoomId, "roomId"}, + {RoomName, "roomName"}, + {AvatarUrl, "avatarUrl"}, + }; +} + +QVariant +RoomInfoModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= (int)roomInfos_.size() || index.row() < 0) + return {}; + + switch (role) { + case RoomId: + return QString::fromStdString(roomInfos_[index.row()].first); + case RoomName: + return QString::fromStdString(roomInfos_[index.row()].second.name); + case AvatarUrl: + return QString::fromStdString(roomInfos_[index.row()].second.avatar_url); + default: + return {}; + } +} + DeviceInfoModel * UserProfile::deviceList() { return &this->deviceList_; } +RoomInfoModel * +UserProfile::sharedRooms() +{ + return this->sharedRooms_; +} + QString UserProfile::userid() { diff --git a/src/ui/UserProfile.h b/src/ui/UserProfile.h index d8423ffa..a880f320 100644 --- a/src/ui/UserProfile.h +++ b/src/ui/UserProfile.h @@ -119,6 +119,30 @@ private: friend class UserProfile; }; +class RoomInfoModel final : public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles + { + RoomId, + RoomName, + AvatarUrl, + }; + + explicit RoomInfoModel(const std::map<std::string, RoomInfo> &, QObject *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return (int)roomInfos_.size(); + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +private: + std::vector<std::pair<std::string, RoomInfo>> roomInfos_; +}; + class UserProfile final : public QObject { Q_OBJECT @@ -126,6 +150,7 @@ class UserProfile final : public QObject Q_PROPERTY(QString userid READ userid CONSTANT) Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList NOTIFY devicesChanged) + Q_PROPERTY(RoomInfoModel *sharedRooms READ sharedRooms CONSTANT) Q_PROPERTY(bool isGlobalUserProfile READ isGlobalUserProfile CONSTANT) Q_PROPERTY(int userVerified READ getUserStatus NOTIFY userStatusChanged) Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged) @@ -139,6 +164,7 @@ public: TimelineModel *parent = nullptr); DeviceInfoModel *deviceList(); + RoomInfoModel *sharedRooms(); QString userid(); QString displayName(); @@ -198,4 +224,5 @@ private: bool isLoading_ = false; TimelineViewManager *manager; TimelineModel *model; + RoomInfoModel *sharedRooms_ = nullptr; }; |