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
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")
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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