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
- width: devicelist.width
- spacing: 4
+ 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
- ColumnLayout {
- spacing: 0
+ 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
+ }
+
+ 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;
+ }
+ }
+ }
+
+ 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
- }
-
}
- RowLayout {
- id: deviceNameRow
+ Button {
+ id: verifyButton
- 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();
- }
- }
- }
-
- }
-
- 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 : "???")
- }
-
- }
-
- 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;
- }
- }
- }
-
- Button {
- id: verifyButton
-
- visible: verificationStatus == VerificationStatus.UNVERIFIED && (profile.isSelf || !profile.userVerificationEnabled)
- text: (verificationStatus != VerificationStatus.VERIFIED) ? qsTr("Verify") : qsTr("Unverify")
- onClicked: {
- if (verificationStatus == VerificationStatus.VERIFIED)
+ 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
+ else
profile.verify(deviceId);
+ }
+ }
+
+ }
+ }
+
+ DelegateModel {
+ id: sharedRoomsModel
+ model: profile.sharedRooms
+ delegate: RowLayout {
+ required property string roomId
+ required property string roomName
+ required property string avatarUrl
+
+ width: devicelist.width
+ spacing: 4
+
+
+ Avatar {
+ id: avatar
+
+ enabled: false
+ Layout.alignment: Qt.AlignVCenter
+ Layout.leftMargin: Nheko.paddingMedium
+
+ property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 1.6)
+ height: avatarSize
+ width: avatarSize
+ url: avatarUrl.replace("mxc://", "image://MxcImage/")
+ roomid: roomId
+ displayName: roomName
+ }
+
+ ElidedLabel {
+ Layout.alignment: Qt.AlignVCenter
+ color: Nheko.colors.text
+ Layout.fillWidth: true
+ elideWidth: width
+ fullText: roomName
+ textFormat: Text.PlainText
+ Layout.rightMargin: Nheko.paddingMedium
+ }
+
+ 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 @@
qml/components/AvatarListTile.qml
qml/components/FlatButton.qml
qml/components/MainWindowDialog.qml
+ qml/components/NhekoTabButton.qml
qml/components/NotificationBubble.qml
qml/components/ReorderableListview.qml
qml/components/SpaceMenuLevel.qml
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
+Cache::getCommonRooms(const std::string &user_id)
+{
+ std::map 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();
+ 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
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 joinedRooms();
+ std::map getCommonRooms(const std::string &user_id);
QMap roomInfo(bool withInvites = true);
QHash 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
@@ -102,12 +108,53 @@ DeviceInfoModel::reset(const std::vector &deviceList)
endResetModel();
}
+RoomInfoModel::RoomInfoModel(const std::map &raw, QObject *parent)
+ : QAbstractListModel(parent)
+{
+ for (const auto &e : raw)
+ roomInfos_.push_back(e);
+}
+
+QHash
+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 &, QObject *parent = nullptr);
+ QHash 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> 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;
};