diff --git a/src/models/roomlistmodel.cpp b/src/models/roomlistmodel.cpp index edd5c6e08..70fca7232 100644 --- a/src/models/roomlistmodel.cpp +++ b/src/models/roomlistmodel.cpp @@ -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 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: diff --git a/src/models/roomlistmodel.h b/src/models/roomlistmodel.h index 329098cf0..dab8b1a86 100644 --- a/src/models/roomlistmodel.h +++ b/src/models/roomlistmodel.h @@ -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) diff --git a/src/models/sortfilterroomlistmodel.cpp b/src/models/sortfilterroomlistmodel.cpp index 15e94365a..044b36c9a 100644 --- a/src/models/sortfilterroomlistmodel.cpp +++ b/src/models/sortfilterroomlistmodel.cpp @@ -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(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" diff --git a/src/models/sortfilterroomlistmodel.h b/src/models/sortfilterroomlistmodel.h index dd9f222f6..534ae6743 100644 --- a/src/models/sortfilterroomlistmodel.h +++ b/src/models/sortfilterroomlistmodel.h @@ -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; }; diff --git a/src/neochatconnection.cpp b/src/neochatconnection.cpp index d144e219e..7c12fae99 100644 --- a/src/neochatconnection.cpp +++ b/src/neochatconnection.cpp @@ -10,6 +10,8 @@ #include "jobs/neochatdeactivateaccountjob.h" #include "roommanager.h" +#include +#include #include #include @@ -19,6 +21,7 @@ #include #include #include +#include #include #include @@ -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 NeoChatConnection::setupPushNotifications(QString endpoint) { #ifdef HAVE_KUNIFIEDPUSH diff --git a/src/neochatconnection.h b/src/neochatconnection.h index 863eb54ca..4760fff95 100644 --- a/src/neochatconnection.h +++ b/src/neochatconnection.h @@ -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 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); diff --git a/src/qml/RoomListPage.qml b/src/qml/RoomListPage.qml index fa8826fcf..ef928253e 100644 --- a/src/qml/RoomListPage.qml +++ b/src/qml/RoomListPage.qml @@ -196,6 +196,7 @@ Kirigami.Page { listView.currentIndex = sortFilterRoomListModel.mapFromSource(itemSelection.currentIndex).row } activeSpaceId: spaceDrawer.selectedSpaceId + mode: spaceDrawer.showDirectChats ? SortFilterRoomListModel.DirectChats : SortFilterRoomListModel.Rooms } section { diff --git a/src/qml/SpaceDrawer.qml b/src/qml/SpaceDrawer.qml index 1bed19a5f..8c23aa786 100644 --- a/src/qml/SpaceDrawer.qml +++ b/src/qml/SpaceDrawer.qml @@ -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) } diff --git a/src/qml/UserDetailDialog.qml b/src/qml/UserDetailDialog.qml index c61bb1dc6..c7a14652c 100644 --- a/src/qml/UserDetailDialog.qml +++ b/src/qml/UserDetailDialog.qml @@ -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)