Show rooms you share with someone

This commit is contained in:
Nicolas Werner 2023-02-24 02:40:14 +01:00
parent d46a67f64b
commit aae3300860
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
8 changed files with 320 additions and 139 deletions

View file

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

View file

@ -49,30 +49,10 @@ ApplicationWindow {
width: parent.width width: parent.width
palette: Nheko.colors palette: Nheko.colors
component TabB : TabButton { NhekoTabButton {
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 {
text: qsTr("Roles") text: qsTr("Roles")
} }
TabB { NhekoTabButton {
text: qsTr("Users") text: qsTr("Users")
} }
} }

View file

@ -5,10 +5,12 @@
import ".." import ".."
import "../device-verification" import "../device-verification"
import "../ui" import "../ui"
import "../components"
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
import QtQuick.Window 2.13 import QtQuick.Window 2.13
import QtQml.Models 2.2
import im.nheko 1.0 import im.nheko 1.0
ApplicationWindow { ApplicationWindow {
@ -34,12 +36,13 @@ ApplicationWindow {
ListView { ListView {
id: devicelist id: devicelist
property int selectedTab: 0
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
clip: true clip: true
spacing: 8 spacing: 8
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
model: profile.deviceList
anchors.fill: parent anchors.fill: parent
anchors.margins: 10 anchors.margins: 10
footerPositioning: ListView.OverlayFooter 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 { model: (selectedTab == 0) ? devicesModel : sharedRoomsModel
required property int verificationStatus
required property string deviceId
required property string deviceName
required property string lastIp
required property var lastTs
width: devicelist.width DelegateModel {
spacing: 4 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 { width: devicelist.width
spacing: 0 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 { Text {
visible: profile.isSelf
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
elide: Text.ElideRight elide: Text.ElideRight
font.bold: true
color: Nheko.colors.text 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 Image {
visible: profile.isSelf && verificationStatus != VerificationStatus.NOT_APPLICABLE Layout.preferredHeight: 16
sourceSize.height: 16 * Screen.devicePixelRatio Layout.preferredWidth: 16
sourceSize.width: 16 * Screen.devicePixelRatio visible: !profile.isSelf && verificationStatus != VerificationStatus.NOT_APPLICABLE
source: { source: {
switch (verificationStatus) { switch (verificationStatus) {
case VerificationStatus.VERIFIED: 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: 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: 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: 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 { Button {
id: deviceNameRow id: verifyButton
property bool isEditingAllowed visible: verificationStatus == VerificationStatus.UNVERIFIED && (profile.isSelf || !profile.userVerificationEnabled)
text: (verificationStatus != VerificationStatus.VERIFIED) ? qsTr("Verify") : qsTr("Unverify")
TextInput { onClicked: {
id: deviceNameField if (verificationStatus == VerificationStatus.VERIFIED)
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)
profile.unverify(deviceId); profile.unverify(deviceId);
else else
profile.verify(deviceId); 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 { footer: DialogButtonBox {

View file

@ -129,6 +129,7 @@
<file>qml/components/AvatarListTile.qml</file> <file>qml/components/AvatarListTile.qml</file>
<file>qml/components/FlatButton.qml</file> <file>qml/components/FlatButton.qml</file>
<file>qml/components/MainWindowDialog.qml</file> <file>qml/components/MainWindowDialog.qml</file>
<file>qml/components/NhekoTabButton.qml</file>
<file>qml/components/NotificationBubble.qml</file> <file>qml/components/NotificationBubble.qml</file>
<file>qml/components/ReorderableListview.qml</file> <file>qml/components/ReorderableListview.qml</file>
<file>qml/components/SpaceMenuLevel.qml</file> <file>qml/components/SpaceMenuLevel.qml</file>

View file

@ -3146,6 +3146,36 @@ Cache::joinedRooms()
return room_ids; 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> std::optional<MemberInfo>
Cache::getMember(const std::string &room_id, const std::string &user_id) Cache::getMember(const std::string &room_id, const std::string &user_id)
{ {

View file

@ -64,6 +64,7 @@ public:
crypto::Trust roomVerificationStatus(const std::string &room_id); crypto::Trust roomVerificationStatus(const std::string &room_id);
std::vector<std::string> joinedRooms(); std::vector<std::string> joinedRooms();
std::map<std::string, RoomInfo> getCommonRooms(const std::string &user_id);
QMap<QString, RoomInfo> roomInfo(bool withInvites = true); QMap<QString, RoomInfo> roomInfo(bool withInvites = true);
QHash<QString, RoomInfo> invites(); QHash<QString, RoomInfo> invites();

View file

@ -58,6 +58,12 @@ UserProfile::UserProfile(const QString &roomid,
emit verificationStatiChanged(); emit verificationStatiChanged();
}); });
fetchDeviceList(this->userid_); fetchDeviceList(this->userid_);
if (userid != utils::localUser())
sharedRooms_ =
new RoomInfoModel(cache::client()->getCommonRooms(userid.toStdString()), this);
else
sharedRooms_ = new RoomInfoModel({}, this);
} }
QHash<int, QByteArray> QHash<int, QByteArray>
@ -102,12 +108,53 @@ DeviceInfoModel::reset(const std::vector<DeviceInfo> &deviceList)
endResetModel(); 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 * DeviceInfoModel *
UserProfile::deviceList() UserProfile::deviceList()
{ {
return &this->deviceList_; return &this->deviceList_;
} }
RoomInfoModel *
UserProfile::sharedRooms()
{
return this->sharedRooms_;
}
QString QString
UserProfile::userid() UserProfile::userid()
{ {

View file

@ -119,6 +119,30 @@ private:
friend class UserProfile; 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 class UserProfile final : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -126,6 +150,7 @@ class UserProfile final : public QObject
Q_PROPERTY(QString userid READ userid CONSTANT) Q_PROPERTY(QString userid READ userid CONSTANT)
Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged)
Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList NOTIFY devicesChanged) Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList NOTIFY devicesChanged)
Q_PROPERTY(RoomInfoModel *sharedRooms READ sharedRooms CONSTANT)
Q_PROPERTY(bool isGlobalUserProfile READ isGlobalUserProfile CONSTANT) Q_PROPERTY(bool isGlobalUserProfile READ isGlobalUserProfile CONSTANT)
Q_PROPERTY(int userVerified READ getUserStatus NOTIFY userStatusChanged) Q_PROPERTY(int userVerified READ getUserStatus NOTIFY userStatusChanged)
Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged) Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged)
@ -139,6 +164,7 @@ public:
TimelineModel *parent = nullptr); TimelineModel *parent = nullptr);
DeviceInfoModel *deviceList(); DeviceInfoModel *deviceList();
RoomInfoModel *sharedRooms();
QString userid(); QString userid();
QString displayName(); QString displayName();
@ -198,4 +224,5 @@ private:
bool isLoading_ = false; bool isLoading_ = false;
TimelineViewManager *manager; TimelineViewManager *manager;
TimelineModel *model; TimelineModel *model;
RoomInfoModel *sharedRooms_ = nullptr;
}; };