From fc546d4a436519ca6e8f92e29cfa41ad4f28fc2c Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 11 Nov 2023 17:57:03 +0100 Subject: [PATCH] Add notifications view --- src/CMakeLists.txt | 3 + src/models/notificationsmodel.cpp | 161 ++++++++++++++++++++++++++++++ src/models/notificationsmodel.h | 67 +++++++++++++ src/qml/NotificationsView.qml | 93 +++++++++++++++++ src/qml/RoomListPage.qml | 18 ++++ 5 files changed, 342 insertions(+) create mode 100644 src/models/notificationsmodel.cpp create mode 100644 src/models/notificationsmodel.h create mode 100644 src/qml/NotificationsView.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 49efc74c2..396815c3a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -139,6 +139,8 @@ add_library(neochat STATIC chatbarcache.h colorschemer.cpp colorschemer.h + models/notificationsmodel.cpp + models/notificationsmodel.h ) qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN @@ -290,6 +292,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN qml/QrCodeMaximizeComponent.qml qml/SelectSpacesDialog.qml qml/AttachDialog.qml + qml/NotificationsView.qml RESOURCES qml/confetti.png qml/glowdot.png diff --git a/src/models/notificationsmodel.cpp b/src/models/notificationsmodel.cpp new file mode 100644 index 000000000..6ee4603d2 --- /dev/null +++ b/src/models/notificationsmodel.cpp @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "notificationsmodel.h" + +#include +#include + +#include "eventhandler.h" +#include "neochatroom.h" + +using namespace Quotient; + +NotificationsModel::NotificationsModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +int NotificationsModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_notifications.count(); +} + +QVariant NotificationsModel::data(const QModelIndex &index, int role) const +{ + auto row = index.row(); + if (row < 0 || row >= m_notifications.count()) { + return {}; + } + if (role == TextRole) { + return m_notifications[row].text; + } + if (role == RoomIdRole) { + return m_notifications[row].roomId; + } + if (role == AuthorName) { + return m_notifications[row].authorName; + } + if (role == AuthorAvatar) { + return m_notifications[row].authorAvatar; + } + if (role == RoomRole) { + return QVariant::fromValue(m_connection->room(m_notifications[row].roomId)); + } + if (role == EventIdRole) { + return m_notifications[row].eventId; + } + if (role == RoomDisplayNameRole) { + return m_notifications[row].roomDisplayName; + } + return {}; +} + +QHash NotificationsModel::roleNames() const +{ + return { + {TextRole, "text"}, + {RoomIdRole, "roomId"}, + {AuthorName, "authorName"}, + {AuthorAvatar, "authorAvatar"}, + {RoomRole, "room"}, + {EventIdRole, "eventId"}, + {RoomDisplayNameRole, "roomDisplayName"}, + }; +} + +NeoChatConnection *NotificationsModel::connection() const +{ + return m_connection; +} + +void NotificationsModel::setConnection(NeoChatConnection *connection) +{ + if (m_connection) { + // disconnect things... + } + if (!connection) { + return; + } + m_connection = connection; + Q_EMIT connectionChanged(); + connect(connection, &Connection::syncDone, this, [=]() { + loadData(); + }); + loadData(); +} + +void NotificationsModel::loadData() +{ + Q_ASSERT(m_connection); + if (m_job || (m_notifications.size() && m_nextToken.isEmpty())) { + return; + } + m_job = m_connection->callApi(m_nextToken); + Q_EMIT loadingChanged(); + connect(m_job, &BaseJob::finished, this, [this]() { + m_nextToken = m_job->nextToken(); + Q_EMIT nextTokenChanged(); + for (const auto ¬ification : m_job->notifications()) { + if (std::any_of(notification.actions.constBegin(), notification.actions.constEnd(), [](const QVariant &it) { + if (it.canConvert()) { + auto map = it.toMap(); + if (map["set_tweak"_ls] == "highlight"_ls) { + return true; + } + } + return false; + })) { + const auto &authorId = notification.event->fullJson()["sender"_ls].toString(); + const auto &room = m_connection->room(notification.roomId); + if (!room) { + continue; + } + auto u = room->memberAvatarUrl(authorId); + auto avatar = u.isEmpty() ? QUrl() : connection()->makeMediaUrl(u); + const auto &authorAvatar = avatar.isValid() && avatar.scheme() == QStringLiteral("mxc") ? avatar : QUrl(); + + const auto &roomEvent = eventCast(notification.event.get()); + EventHandler eventHandler; + eventHandler.setRoom(dynamic_cast(room)); + eventHandler.setEvent(roomEvent); + beginInsertRows({}, m_notifications.length(), m_notifications.length()); + m_notifications += Notification{ + .roomId = notification.roomId, + .text = room->htmlSafeMemberName(authorId) + (roomEvent->is() ? QStringLiteral(" ") : QStringLiteral(": ")) + + eventHandler.getPlainBody(true), + .authorName = room->htmlSafeMemberName(authorId), + .authorAvatar = authorAvatar, + .eventId = roomEvent->id(), + .roomDisplayName = room->displayName(), + }; + endInsertRows(); + } + } + m_job = nullptr; + Q_EMIT loadingChanged(); + }); +} + +bool NotificationsModel::canFetchMore(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return !m_nextToken.isEmpty(); +} + +void NotificationsModel::fetchMore(const QModelIndex &parent) +{ + Q_UNUSED(parent); + loadData(); +} + +bool NotificationsModel::loading() const +{ + return m_job; +} + +QString NotificationsModel::nextToken() const +{ + return m_nextToken; +} diff --git a/src/models/notificationsmodel.h b/src/models/notificationsmodel.h new file mode 100644 index 000000000..f2215c428 --- /dev/null +++ b/src/models/notificationsmodel.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include "neochatconnection.h" +#include +#include +#include +#include +#include + +class NotificationsModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged) + Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged) + Q_PROPERTY(QString nextToken READ nextToken NOTIFY nextTokenChanged) + +public: + enum Roles { + TextRole = Qt::DisplayRole, + RoomIdRole, + AuthorName, + AuthorAvatar, + RoomRole, + EventIdRole, + RoomDisplayNameRole, + }; + Q_ENUM(Roles); + + struct Notification { + QString roomId; + QString text; + QString authorName; + QUrl authorAvatar; + QString eventId; + QString roomDisplayName; + }; + + NotificationsModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; + + NeoChatConnection *connection() const; + void setConnection(NeoChatConnection *connection); + bool loading() const; + QString nextToken() const; + +Q_SIGNALS: + void connectionChanged(); + void loadingChanged(); + void nextTokenChanged(); + +private: + QPointer m_connection; + void loadData(); + QList m_notifications; + QString m_nextToken; + QPointer m_job; +}; diff --git a/src/qml/NotificationsView.qml b/src/qml/NotificationsView.qml new file mode 100644 index 000000000..011b4ecbd --- /dev/null +++ b/src/qml/NotificationsView.qml @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.kirigami.delegates as KD +import org.kde.kirigamiaddons.components as Components + +import org.kde.neochat + +Kirigami.ScrollablePage { + id: root + + required property NeoChatConnection connection + + title: i18nc("@title", "Notifications") + + ListView { + id: listView + + anchors.fill: parent + verticalLayoutDirection: ListView.BottomToTop + model: NotificationsModel { + id: notificationsModel + connection: root.connection + } + + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + visible: listView.count === 0 + text: notificationsModel.loading ? i18n("Loading…") : i18n("No Notifications") + } + + footer: Kirigami.PlaceholderMessage { + anchors.horizontalCenter: parent.horizontalCenter + text: i18n("Loading…") + visible: notificationsModel.nextToken.length > 0 && listView.count > 0 + } + + delegate: QQC2.ItemDelegate { + width: parent?.width ?? 0 + + onClicked: RoomManager.visitRoom(model.room, model.eventId) + contentItem: RowLayout { + spacing: Kirigami.Units.largeSpacing + + Components.Avatar { + source: model.authorAvatar + name: model.authorName + implicitHeight: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2 + implicitWidth: implicitHeight + + Layout.fillHeight: true + Layout.preferredWidth: height + } + + ColumnLayout { + spacing: 0 + + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + + QQC2.Label { + id: label + + text: model.roomDisplayName + elide: Text.ElideRight + font.weight: Font.Normal + textFormat: Text.PlainText + + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignBottom + } + + QQC2.Label { + id: subtitle + + text: model.text + elide: Text.ElideRight + font: Kirigami.Theme.smallFont + opacity: root.hasNotifications ? 0.9 : 0.7 + + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + } + } + } + } + } +} diff --git a/src/qml/RoomListPage.qml b/src/qml/RoomListPage.qml index 4f44c3df8..19136c612 100644 --- a/src/qml/RoomListPage.qml +++ b/src/qml/RoomListPage.qml @@ -8,6 +8,7 @@ import QtQuick.Layouts import QtQml.Models import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.components as KirigamiComponents import org.kde.neochat import org.kde.neochat.config @@ -128,6 +129,23 @@ Kirigami.Page { topMargin: Math.round(Kirigami.Units.smallSpacing / 2) + KirigamiComponents.FloatingButton { + icon.name: "notifications" + text: i18n("View notifications") + anchors.right: parent.right + anchors.rightMargin: Kirigami.Units.largeSpacing + anchors.bottom: parent.bottom + anchors.bottomMargin: Kirigami.Units.largeSpacing + width: Kirigami.Units.gridUnit * 2 + height: width + QQC2.ToolTip.text: text + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + QQC2.ToolTip.visible: hovered + onClicked: pageStack.pushDialogLayer("qrc:/org/kde/neochat/qml/NotificationsView.qml", {connection: root.connection}, { + title: i18nc("@title", "Notifications") + }); + } + header: QQC2.ItemDelegate { width: visible ? ListView.view.width : 0 height: visible ? Kirigami.Units.gridUnit * 2 : 0