Why can't we be friends

Update the UX to refer to structure direct chats as friends. The direct chats are pulled into their own tab in the space drawer.

The `UserDetailDialog` is also updated to check whether a direct chat already exists and if not ask to invite as friend.

![image](/uploads/67f13fa8558e704e0acaf7c60e135bbc/image.png)
This commit is contained in:
James Graham
2024-01-13 21:38:43 +00:00
parent 981edc9cf7
commit 77e366b179
9 changed files with 222 additions and 9 deletions

View File

@@ -368,6 +368,9 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
if (role == ReplacementIdRole) {
return room->successorId();
}
if (role == IsDirectChat) {
return room->isDirectChat();
}
return QVariant();
}
@@ -401,6 +404,7 @@ QHash<int, QByteArray> RoomListModel::roleNames() const
roles[IsSpaceRole] = "isSpace";
roles[RoomIdRole] = "roomId";
roles[IsChildSpaceRole] = "isChildSpace";
roles[IsDirectChat] = "isDirectChat";
return roles;
}
@@ -412,7 +416,7 @@ QString RoomListModel::categoryName(int category)
case NeoChatRoomType::Favorite:
return i18n("Favorite");
case NeoChatRoomType::Direct:
return i18n("Direct Messages");
return i18n("Friends");
case NeoChatRoomType::Normal:
return i18n("Normal");
case NeoChatRoomType::Deprioritized:

View File

@@ -77,6 +77,7 @@ public:
IsSpaceRole, /**< Whether the room is a space. */
IsChildSpaceRole, /**< Whether this space is a child of a different space. */
ReplacementIdRole, /**< The room id of the room replacing this one, if any. */
IsDirectChat, /**< Whether this room is a direct chat. */
};
Q_ENUM(EventRoles)

View File

@@ -83,6 +83,21 @@ bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex
{
Q_UNUSED(source_parent);
bool acceptRoom =
sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() == false;
bool isDirectChat = sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsDirectChat).toBool();
// In `show direct chats` mode we only care about whether or not it's a direct chat or if the filter string matches.'
if (m_mode == DirectChats) {
return isDirectChat && acceptRoom;
}
// When not in `show direct chats` mode, filter them out.
if (isDirectChat && m_mode == Rooms) {
return false;
}
if (sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::JoinStateRole).toString() == QStringLiteral("upgraded")
&& dynamic_cast<RoomListModel *>(sourceModel())
->connection()
@@ -90,10 +105,6 @@ bool SortFilterRoomListModel::filterAcceptsRow(int source_row, const QModelIndex
return false;
}
bool acceptRoom =
sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
&& sourceModel()->data(sourceModel()->index(source_row, 0), RoomListModel::IsSpaceRole).toBool() == false;
if (m_activeSpaceId.isEmpty()) {
return acceptRoom;
} else {
@@ -116,4 +127,20 @@ void SortFilterRoomListModel::setActiveSpaceId(const QString &spaceId)
invalidate();
}
SortFilterRoomListModel::Mode SortFilterRoomListModel::mode() const
{
return m_mode;
}
void SortFilterRoomListModel::setMode(SortFilterRoomListModel::Mode mode)
{
if (m_mode == mode) {
return;
}
m_mode = mode;
Q_EMIT modeChanged();
invalidate();
}
#include "moc_sortfilterroomlistmodel.cpp"

View File

@@ -47,6 +47,11 @@ class SortFilterRoomListModel : public QSortFilterProxyModel
*/
Q_PROPERTY(QString activeSpaceId READ activeSpaceId WRITE setActiveSpaceId NOTIFY activeSpaceIdChanged)
/**
* @brief Whether only direct chats should be shown.
*/
Q_PROPERTY(Mode mode READ mode WRITE setMode NOTIFY modeChanged)
public:
enum RoomSortOrder {
Alphabetical,
@@ -55,6 +60,13 @@ public:
};
Q_ENUM(RoomSortOrder)
enum Mode {
Rooms,
DirectChats,
All,
};
Q_ENUM(Mode)
explicit SortFilterRoomListModel(QObject *parent = nullptr);
void setRoomSortOrder(RoomSortOrder sortOrder);
@@ -66,6 +78,9 @@ public:
QString activeSpaceId() const;
void setActiveSpaceId(const QString &spaceId);
Mode mode() const;
void setMode(Mode mode);
protected:
/**
* @brief Returns true if the value of source_left is less than source_right.
@@ -85,9 +100,11 @@ Q_SIGNALS:
void roomSortOrderChanged();
void filterTextChanged();
void activeSpaceIdChanged();
void modeChanged();
private:
RoomSortOrder m_sortOrder = Categories;
Mode m_mode = All;
QString m_filterText;
QString m_activeSpaceId;
};

View File

@@ -10,6 +10,8 @@
#include "jobs/neochatdeactivateaccountjob.h"
#include "roommanager.h"
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <qt6keychain/keychain.h>
#include <KLocalizedString>
@@ -19,6 +21,7 @@
#include <Quotient/database.h>
#include <Quotient/jobs/downloadfilejob.h>
#include <Quotient/qt_connection_util.h>
#include <Quotient/room.h>
#include <Quotient/settings.h>
#include <Quotient/user.h>
@@ -55,6 +58,34 @@ NeoChatConnection::NeoChatConnection(QObject *parent)
RoomManager::instance().warning(i18n("File too large to download."), i18n("Contact your matrix server administrator for support."));
}
});
connect(this, &NeoChatConnection::directChatsListChanged, this, [this](DirectChatsMap additions, DirectChatsMap removals) {
Q_EMIT directChatInvitesChanged();
for (const auto &chatId : additions) {
if (const auto chat = room(chatId)) {
connect(chat, &Room::unreadStatsChanged, this, [this]() {
Q_EMIT directChatNotificationsChanged();
});
}
}
for (const auto &chatId : removals) {
if (const auto chat = room(chatId)) {
disconnect(chat, &Room::unreadStatsChanged, this, nullptr);
}
}
});
connect(this, &NeoChatConnection::joinedRoom, this, [this](Room *room) {
if (room->isDirectChat()) {
connect(room, &Room::unreadStatsChanged, this, [this]() {
Q_EMIT directChatNotificationsChanged();
});
}
});
connect(this, &NeoChatConnection::leftRoom, this, [this](Room *room, Room *prev) {
Q_UNUSED(room)
if (prev && prev->isDirectChat()) {
Q_EMIT directChatInvitesChanged();
}
});
}
NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
@@ -65,6 +96,34 @@ NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
Q_EMIT labelChanged();
}
});
connect(this, &NeoChatConnection::directChatsListChanged, this, [this](DirectChatsMap additions, DirectChatsMap removals) {
Q_EMIT directChatInvitesChanged();
for (const auto &chatId : additions) {
if (const auto chat = room(chatId)) {
connect(chat, &Room::unreadStatsChanged, this, [this]() {
Q_EMIT directChatNotificationsChanged();
});
}
}
for (const auto &chatId : removals) {
if (const auto chat = room(chatId)) {
disconnect(chat, &Room::unreadStatsChanged, this, nullptr);
}
}
});
connect(this, &NeoChatConnection::joinedRoom, this, [this](Room *room) {
if (room->isDirectChat()) {
connect(room, &Room::unreadStatsChanged, this, [this]() {
Q_EMIT directChatNotificationsChanged();
});
}
});
connect(this, &NeoChatConnection::leftRoom, this, [this](Room *room, Room *prev) {
Q_UNUSED(room)
if (prev && prev->isDirectChat()) {
Q_EMIT directChatInvitesChanged();
}
});
}
void NeoChatConnection::logout(bool serverSideLogout)
@@ -244,6 +303,11 @@ void NeoChatConnection::createSpace(const QString &name, const QString &topic, c
});
}
bool NeoChatConnection::directChatExists(Quotient::User *user)
{
return directChats().contains(user);
}
void NeoChatConnection::openOrCreateDirectChat(User *user)
{
const auto existing = directChats();
@@ -258,6 +322,32 @@ void NeoChatConnection::openOrCreateDirectChat(User *user)
requestDirectChat(user);
}
qsizetype NeoChatConnection::directChatNotifications() const
{
qsizetype notifications = 0;
QStringList added; // The same ID can be in the list multiple times.
for (const auto &chatId : directChats()) {
if (!added.contains(chatId)) {
if (const auto chat = room(chatId)) {
notifications += chat->notificationCount();
added += chatId;
}
}
}
return notifications;
}
bool NeoChatConnection::directChatInvites() const
{
auto inviteRooms = rooms(JoinState::Invite);
for (const auto inviteRoom : inviteRooms) {
if (inviteRoom->isDirectChat()) {
return true;
}
}
return false;
}
QCoro::Task<void> NeoChatConnection::setupPushNotifications(QString endpoint)
{
#ifdef HAVE_KUNIFIEDPUSH

View File

@@ -27,6 +27,16 @@ class NeoChatConnection : public Quotient::Connection
Q_PROPERTY(QString deviceKey READ deviceKey CONSTANT)
Q_PROPERTY(QString encryptionKey READ encryptionKey CONSTANT)
/**
* @brief The total number of notifications for all direct chats.
*/
Q_PROPERTY(qsizetype directChatNotifications READ directChatNotifications NOTIFY directChatNotificationsChanged)
/**
* @brief Whether there is at least one invite to a direct chat.
*/
Q_PROPERTY(bool directChatInvites READ directChatInvites NOTIFY directChatInvitesChanged)
/**
* @brief Whether NeoChat is currently able to connect to the server.
*/
@@ -79,6 +89,11 @@ public:
*/
Q_INVOKABLE void createSpace(const QString &name, const QString &topic, const QString &parent = {}, bool setChildParent = false);
/**
* @brief Whether a direct chat with the user exists.
*/
Q_INVOKABLE bool directChatExists(Quotient::User *user);
/**
* @brief Join a direct chat with the given user.
*
@@ -86,6 +101,9 @@ public:
*/
Q_INVOKABLE void openOrCreateDirectChat(Quotient::User *user);
qsizetype directChatNotifications() const;
bool directChatInvites() const;
// note: this is intentionally a copied QString because
// the reference could be destroyed before the task is finished
QCoro::Task<void> setupPushNotifications(QString endpoint);
@@ -97,6 +115,8 @@ public:
Q_SIGNALS:
void labelChanged();
void directChatNotificationsChanged();
void directChatInvitesChanged();
void isOnlineChanged();
void passwordStatus(NeoChatConnection::PasswordStatus status);
void userConsentRequired(QUrl url);

View File

@@ -196,6 +196,7 @@ Kirigami.Page {
listView.currentIndex = sortFilterRoomListModel.mapFromSource(itemSelection.currentIndex).row
}
activeSpaceId: spaceDrawer.selectedSpaceId
mode: spaceDrawer.showDirectChats ? SortFilterRoomListModel.DirectChats : SortFilterRoomListModel.Rooms
}
section {

View File

@@ -23,6 +23,8 @@ QQC2.Control {
property string selectedSpaceId
property bool showDirectChats: false
contentItem: Loader {
id: sidebarColumn
z: 0
@@ -86,8 +88,56 @@ QQC2.Control {
source: "globe"
}
checked: root.selectedSpaceId === ""
onClicked: root.selectedSpaceId = ""
checked: root.selectedSpaceId === "" && root.showDirectChats === false
onClicked: {
root.showDirectChats = false
root.selectedSpaceId = ""
}
}
AvatarTabButton {
id: directChatButton
Layout.fillWidth: true
Layout.preferredHeight: width - Kirigami.Units.smallSpacing
Layout.maximumHeight: width - Kirigami.Units.smallSpacing
Layout.topMargin: Kirigami.Units.smallSpacing / 2
text: i18nc("@button View all one-on-one chats with your friends.", "Friends")
contentItem: Kirigami.Icon {
source: "system-users"
}
checked: root.showDirectChats === true
onClicked: {
root.showDirectChats = true
root.selectedSpaceId = ""
}
QQC2.Label {
id: notificationCountLabel
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: Kirigami.Units.smallSpacing / 2
z: 1
width: Math.max(notificationCountTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
height: Kirigami.Units.iconSizes.smallMedium
text: root.connection.directChatNotifications > 0 ? root.connection.directChatNotifications : ""
visible: root.connection.directChatNotifications > 0 || root.connection.directChatInvites
color: Kirigami.Theme.textColor
horizontalAlignment: Text.AlignHCenter
background: Rectangle {
visible: true
Kirigami.Theme.colorSet: Kirigami.Theme.Button
color: Kirigami.Theme.positiveTextColor
radius: height / 2
}
TextMetrics {
id: notificationCountTextMetrics
text: notificationCountLabel.text
}
}
}
Repeater {
@@ -117,7 +167,10 @@ QQC2.Control {
text: displayName
source: avatar ? ("image://mxc/" + avatar) : ""
onSelected: root.selectedSpaceId = roomId
onSelected: {
root.showDirectChats = false
root.selectedSpaceId = roomId
}
checked: root.selectedSpaceId === roomId
onContextMenuRequested: root.createContextMenu(currentRoom)
}

View File

@@ -215,7 +215,7 @@ Kirigami.Dialog {
FormCard.FormButtonDelegate {
visible: !root.user.isLocalUser
action: Kirigami.Action {
text: i18n("Open a private chat")
text: root.room.connection.directChatExists(root.user.object) ? i18nc("%1 is the name of the user.", "Chat with %1", root.user.displayName) : i18n("Invite to private chat")
icon.name: "document-send"
onTriggered: {
root.room.connection.openOrCreateDirectChat(root.user.object)