diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9c33d96f9..f0a73c74e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -194,6 +194,8 @@ add_library(neochat STATIC models/messagemodel.h models/messagecontentfiltermodel.cpp models/messagecontentfiltermodel.h + models/pinnedmessagemodel.cpp + models/pinnedmessagemodel.h ) set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES @@ -249,6 +251,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE qml/MessageSourceSheet.qml qml/ConfirmEncryptionDialog.qml qml/RoomSearchPage.qml + qml/RoomPinnedMessagesPage.qml qml/LocationChooser.qml qml/TimelineView.qml qml/InvitationView.qml @@ -534,6 +537,7 @@ if(ANDROID) "list-remove-symbolic" "edit-delete" "user-home-symbolic" + "pin-symbolic" ) ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android) else() diff --git a/src/models/pinnedmessagemodel.cpp b/src/models/pinnedmessagemodel.cpp new file mode 100644 index 000000000..a47c16c8d --- /dev/null +++ b/src/models/pinnedmessagemodel.cpp @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2025 Joshua Goins +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "pinnedmessagemodel.h" + +#include "enums/delegatetype.h" +#include "eventhandler.h" +#include "models/messagecontentmodel.h" +#include "neochatroom.h" + +#include + +#include + +using namespace Quotient; + +PinnedMessageModel::PinnedMessageModel(QObject *parent) + : MessageModel(parent) +{ + connect(this, &MessageModel::roomChanged, this, &PinnedMessageModel::fill); +} + +bool PinnedMessageModel::loading() const +{ + return m_loading; +} + +int PinnedMessageModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_pinnedEvents.size(); +} + +std::optional> PinnedMessageModel::getEventForIndex(const QModelIndex index) const +{ + if (static_cast(index.row()) >= m_pinnedEvents.size() || index.row() < 0) { + return std::nullopt; + } + return std::reference_wrapper{*m_pinnedEvents[index.row()].get()}; +} + +void PinnedMessageModel::setLoading(bool loading) +{ + m_loading = loading; + Q_EMIT loadingChanged(); +} + +void PinnedMessageModel::fill() +{ + if (!m_room) { + return; + } + + const auto events = m_room->pinnedEventIds(); + + for (const auto &event : std::as_const(events)) { + auto job = m_room->connection()->callApi(m_room->id(), event); + connect(job, &BaseJob::success, this, [this, job] { + beginInsertRows({}, m_pinnedEvents.size(), m_pinnedEvents.size()); + m_pinnedEvents.push_back(std::move(fromJson>(job->jsonData()))); + Q_EMIT newEventAdded(m_pinnedEvents.back().get(), false); + endInsertRows(); + }); + } +} + +#include "moc_pinnedmessagemodel.cpp" diff --git a/src/models/pinnedmessagemodel.h b/src/models/pinnedmessagemodel.h new file mode 100644 index 000000000..c1f8862a6 --- /dev/null +++ b/src/models/pinnedmessagemodel.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2025 Joshua Goins +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include + +#include "messagemodel.h" +#include "neochatroommember.h" + +namespace Quotient +{ +class Connection; +} + +class NeoChatRoom; + +/** + * @class PinnedMessageModel + * + * This class defines the model for visualising a room's pinned messages. + */ +class PinnedMessageModel : public MessageModel +{ + Q_OBJECT + QML_ELEMENT + + /** + * @brief Whether the model is currently loading. + */ + Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged) + +public: + explicit PinnedMessageModel(QObject *parent = nullptr); + + /** + * @brief Number of rows in the model. + * + * @sa QAbstractItemModel::rowCount + */ + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + bool loading() const; + +Q_SIGNALS: + void loadingChanged(); + +protected: + std::optional> getEventForIndex(QModelIndex index) const override; + +private: + void setLoading(bool loading); + void fill(); + + bool m_loading = false; + + std::vector> m_pinnedEvents; +}; diff --git a/src/qml/RoomInformation.qml b/src/qml/RoomInformation.qml index 2bfef0e76..0d58ac3a8 100644 --- a/src/qml/RoomInformation.qml +++ b/src/qml/RoomInformation.qml @@ -130,6 +130,24 @@ QQC2.ScrollView { Layout.fillWidth: true } + Delegates.RoundedItemDelegate { + id: pinnedMessagesButton + visible: !root.room.isSpace + icon.name: "pin-symbolic" + text: i18nc("@action:button", "Pinned messages") + activeFocusOnTab: true + + Layout.fillWidth: true + + onClicked: { + pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomPinnedMessagesPage'), { + room: root.room + }, { + title: i18nc("@title", "Pinned Messages") + }); + } + } + Delegates.RoundedItemDelegate { id: leaveButton icon.name: "arrow-left-symbolic" diff --git a/src/qml/RoomPinnedMessagesPage.qml b/src/qml/RoomPinnedMessagesPage.qml new file mode 100644 index 000000000..feeee66d3 --- /dev/null +++ b/src/qml/RoomPinnedMessagesPage.qml @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2025 Joshua Goins +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami + +import org.kde.neochat +import org.kde.neochat.timeline + +/** + * @brief Component for showing the pinned messages in a room. + */ +Kirigami.ScrollablePage { + id: root + + /** + * @brief The room to show the pinned messages for. + */ + required property NeoChatRoom room + + title: i18nc("@title", "Pinned Messages") + + Kirigami.Theme.colorSet: Kirigami.Theme.Window + Kirigami.Theme.inherit: false + + ListView { + id: listView + spacing: 0 + + model: PinnedMessageModel { + id: pinModel + room: root.room + } + + delegate: EventDelegate { + room: root.room + } + + section.property: "section" + + Kirigami.PlaceholderMessage { + icon.name: "pin-symbolic" + anchors.centerIn: parent + text: i18nc("@info:placeholder", "No Pinned Messages") + visible: listView.count === 0 + } + + Kirigami.LoadingPlaceholder { + anchors.centerIn: parent + visible: listView.count === 0 && pinModel.loading + } + + Keys.onUpPressed: { + if (listView.currentIndex > 0) { + listView.decrementCurrentIndex(); + } else { + listView.currentIndex = -1; // This is so the list view doesn't appear to have two selected items + listView.headerItem.forceActiveFocus(Qt.TabFocusReason); + } + } + } +}