diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b26602cfc0c02b1ad7d70d129fe1976a86e3a74..80ea628fcc0edc73d76e5c7c6ec92d940bef2731 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -286,7 +286,6 @@ set(SRC_FILES src/dialogs/Logout.cpp src/dialogs/PreviewUploadOverlay.cpp src/dialogs/ReCaptcha.cpp - src/dialogs/ReadReceipts.cpp # Emoji src/emoji/EmojiModel.cpp @@ -305,7 +304,6 @@ set(SRC_FILES src/timeline/RoomlistModel.cpp # UI components - src/ui/Avatar.cpp src/ui/Badge.cpp src/ui/DropShadow.cpp src/ui/FlatButton.cpp @@ -352,6 +350,7 @@ set(SRC_FILES src/MemberList.cpp src/MxcImageProvider.cpp src/Olm.cpp + src/ReadReceiptsModel.cpp src/RegisterPage.cpp src/SSOHandler.cpp src/CombinedImagePackModel.cpp @@ -499,7 +498,6 @@ qt5_wrap_cpp(MOC_HEADERS src/dialogs/PreviewUploadOverlay.h src/dialogs/RawMessage.h src/dialogs/ReCaptcha.h - src/dialogs/ReadReceipts.h # Emoji src/emoji/EmojiModel.h @@ -517,7 +515,6 @@ qt5_wrap_cpp(MOC_HEADERS src/timeline/RoomlistModel.h # UI components - src/ui/Avatar.h src/ui/Badge.h src/ui/FlatButton.h src/ui/FloatingButton.h @@ -558,6 +555,7 @@ qt5_wrap_cpp(MOC_HEADERS src/MainWindow.h src/MemberList.h src/MxcImageProvider.h + src/ReadReceiptsModel.h src/RegisterPage.h src/SSOHandler.h src/CombinedImagePackModel.h diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 07feec8c6be00ca25be13fe408ed7f06d00bb1ea..b6f2b909373409f2f495b7d5ec389faf7847d447 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -580,7 +580,7 @@ ScrollView { Platform.MenuItem { text: qsTr("Read receip&ts") - onTriggered: room.readReceiptsAction(messageContextMenu.eventId) + onTriggered: room.showReadReceipts(messageContextMenu.eventId) } Platform.MenuItem { diff --git a/resources/qml/ReadReceipts.qml b/resources/qml/ReadReceipts.qml new file mode 100644 index 0000000000000000000000000000000000000000..8869d8131564a224ed84159abd8b7a21c89707c3 --- /dev/null +++ b/resources/qml/ReadReceipts.qml @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import im.nheko 1.0 + +ApplicationWindow { + id: readReceiptsRoot + + property ReadReceiptsProxy readReceipts + property Room room + + x: MainWindow.x + (MainWindow.width / 2) - (width / 2) + y: MainWindow.y + (MainWindow.height / 2) - (height / 2) + height: 380 + width: 340 + minimumHeight: 380 + minimumWidth: headerTitle.width + 2 * Nheko.paddingMedium + palette: Nheko.colors + color: Nheko.colors.window + flags: Qt.Dialog + + Shortcut { + sequence: StandardKey.Cancel + onActivated: readReceiptsRoot.close() + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + spacing: Nheko.paddingMedium + + Label { + id: headerTitle + + color: Nheko.colors.text + Layout.alignment: Qt.AlignCenter + text: qsTr("Read receipts") + font.pointSize: fontMetrics.font.pointSize * 1.5 + } + + ScrollView { + palette: Nheko.colors + padding: Nheko.paddingMedium + ScrollBar.horizontal.visible: false + Layout.fillHeight: true + Layout.minimumHeight: 200 + Layout.fillWidth: true + + ListView { + id: readReceiptsList + + clip: true + spacing: Nheko.paddingMedium + boundsBehavior: Flickable.StopAtBounds + model: readReceipts + + delegate: RowLayout { + spacing: Nheko.paddingMedium + + Avatar { + width: Nheko.avatarSize + height: Nheko.avatarSize + userid: model.mxid + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: model.displayName + onClicked: room.openUserProfile(model.mxid) + ToolTip.visible: avatarHover.hovered + ToolTip.text: model.mxid + + HoverHandler { + id: avatarHover + } + + } + + ColumnLayout { + spacing: Nheko.paddingSmall + + Label { + text: model.displayName + color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window) + font.pointSize: fontMetrics.font.pointSize + ToolTip.visible: displayNameHover.hovered + ToolTip.text: model.mxid + + TapHandler { + onSingleTapped: room.openUserProfile(userId) + } + + CursorShape { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + } + + HoverHandler { + id: displayNameHover + } + + } + + Label { + text: model.timestamp + color: Nheko.colors.buttonText + font.pointSize: fontMetrics.font.pointSize * 0.9 + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + + } + + } + + } + + } + + } + + footer: DialogButtonBox { + standardButtons: DialogButtonBox.Ok + onAccepted: readReceiptsRoot.close() + } + +} diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index e80ff764dd123660baaf91ad589366d26f6c123e..7d91beaefc99dc8b3d3548a5a044e079c3086b6e 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -96,6 +96,14 @@ Page { } + Component { + id: readReceiptsDialog + + ReadReceipts { + } + + } + Shortcut { sequence: "Ctrl+K" onActivated: { diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index 7e471d69232a38ddf9d37a6fe382a4e4b127f9ef..0af02b3c9055c3a5e0f87e05c315ca83e0a7a9dc 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -34,7 +34,7 @@ ImageButton { } onClicked: { if (status == MtxEvent.Read) - room.readReceiptsAction(eventId); + room.showReadReceipts(eventId); } image: { diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index c5cc69a6636be12d971b5c828e6cf4a4a7dc3901..d19f2cc90deac3b647046591392624569d8c5975 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -249,4 +249,16 @@ Item { roomid: room ? room.roomId : "" } + Connections { + function onOpenReadReceiptsDialog(rr) { + var dialog = readReceiptsDialog.createObject(timelineRoot, { + "readReceipts": rr, + "room": room + }); + dialog.show(); + } + + target: room + } + } diff --git a/resources/res.qrc b/resources/res.qrc index 5d37c3976a9206963a9c4f3013e65ca9f213618c..2b655b9e1329743d6a42c3bea367539cd8209e87 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -112,7 +112,6 @@ </qresource> <qresource prefix="/"> <file>qtquickcontrols2.conf</file> - <file>qml/Root.qml</file> <file>qml/ChatPage.qml</file> <file>qml/CommunitiesList.qml</file> @@ -177,6 +176,7 @@ <file>qml/components/FlatButton.qml</file> <file>qml/RoomMembers.qml</file> <file>qml/InviteDialog.qml</file> + <file>qml/ReadReceipts.qml</file> </qresource> <qresource prefix="/media"> <file>media/ring.ogg</file> diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index a76756ae64de8765302110d63f605e747879439b..42e3bc7b49969203550c4cf048d6e3252ea67497 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -31,7 +31,6 @@ #include "notifications/Manager.h" -#include "dialogs/ReadReceipts.h" #include "timeline/TimelineViewManager.h" #include "blurhash.hpp" diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index c0486d01d821b7094228aff6664d9f4b892a2122..8bc90f29eae3319cb1b023648eab3dcd793408e0 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -36,7 +36,6 @@ #include "dialogs/JoinRoom.h" #include "dialogs/LeaveRoom.h" #include "dialogs/Logout.h" -#include "dialogs/ReadReceipts.h" MainWindow *MainWindow::instance_ = nullptr; @@ -398,27 +397,6 @@ MainWindow::openLogoutDialog() showDialog(dialog); } -void -MainWindow::openReadReceiptsDialog(const QString &event_id) -{ - auto dialog = new dialogs::ReadReceipts(this); - - const auto room_id = chat_page_->currentRoom(); - - try { - dialog->addUsers(cache::readReceipts(event_id, room_id)); - } catch (const lmdb::error &) { - nhlog::db()->warn("failed to retrieve read receipts for {} {}", - event_id.toStdString(), - chat_page_->currentRoom().toStdString()); - dialog->deleteLater(); - - return; - } - - showDialog(dialog); -} - bool MainWindow::hasActiveDialogs() const { diff --git a/src/MainWindow.h b/src/MainWindow.h index 6d62545c4462a77509abb6cc244b4d9d5110bbcd..d423af9fd008d61264e7dce28aafd731e946ce11 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -65,7 +65,6 @@ public: std::function<void(const mtx::requests::CreateRoom &request)> callback); void openJoinRoomDialog(std::function<void(const QString &room_id)> callback); void openLogoutDialog(); - void openReadReceiptsDialog(const QString &event_id); void hideOverlay(); void showSolidOverlayModal(QWidget *content, diff --git a/src/MemberList.cpp b/src/MemberList.cpp index 0ef3b696743dcbfdc8e7cd5964ecb1ad4ee42b42..196647fead8cd139cbba42bad5046b8f29cf2924 100644 --- a/src/MemberList.cpp +++ b/src/MemberList.cpp @@ -2,16 +2,6 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -#include <QAbstractSlider> -#include <QLabel> -#include <QListWidgetItem> -#include <QPainter> -#include <QPushButton> -#include <QScrollBar> -#include <QShortcut> -#include <QStyleOption> -#include <QVBoxLayout> - #include "MemberList.h" #include "Cache.h" @@ -20,7 +10,6 @@ #include "Logging.h" #include "Utils.h" #include "timeline/TimelineViewManager.h" -#include "ui/Avatar.h" MemberList::MemberList(const QString &room_id, QObject *parent) : QAbstractListModel{parent} diff --git a/src/MemberList.h b/src/MemberList.h index 9932f6a43f3cffbbbc1948fab5c938f38bc475b7..e6522694d6b4f3c0b9434a748bb978a9dfa41288 100644 --- a/src/MemberList.h +++ b/src/MemberList.h @@ -4,9 +4,10 @@ #pragma once -#include "CacheStructs.h" #include <QAbstractListModel> +#include "CacheStructs.h" + class MemberList : public QAbstractListModel { Q_OBJECT diff --git a/src/ReadReceiptsModel.cpp b/src/ReadReceiptsModel.cpp new file mode 100644 index 0000000000000000000000000000000000000000..25262c59fd37c998f39d3858e2dec02ac03b1c01 --- /dev/null +++ b/src/ReadReceiptsModel.cpp @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ReadReceiptsModel.h" + +#include <QLocale> + +#include "Cache.h" +#include "Cache_p.h" +#include "Logging.h" +#include "Utils.h" + +ReadReceiptsModel::ReadReceiptsModel(QString event_id, QString room_id, QObject *parent) + : QAbstractListModel{parent} + , event_id_{event_id} + , room_id_{room_id} +{ + try { + addUsers(cache::readReceipts(event_id_, room_id_)); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve read receipts for {} {}", + event_id_.toStdString(), + room_id_.toStdString()); + + return; + } + + connect(cache::client(), &Cache::newReadReceipts, this, &ReadReceiptsModel::update); +} + +void +ReadReceiptsModel::update() +{ + try { + addUsers(cache::readReceipts(event_id_, room_id_)); + } catch (const lmdb::error &) { + nhlog::db()->warn("failed to retrieve read receipts for {} {}", + event_id_.toStdString(), + room_id_.toStdString()); + + return; + } +} + +QHash<int, QByteArray> +ReadReceiptsModel::roleNames() const +{ + // Note: RawTimestamp is purposely not included here + return { + {Mxid, "mxid"}, + {DisplayName, "displayName"}, + {AvatarUrl, "avatarUrl"}, + {Timestamp, "timestamp"}, + }; +} + +QVariant +ReadReceiptsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= (int)readReceipts_.size() || index.row() < 0) + return {}; + + switch (role) { + case Mxid: + return readReceipts_[index.row()].first; + case DisplayName: + return cache::displayName(room_id_, readReceipts_[index.row()].first); + case AvatarUrl: + return cache::avatarUrl(room_id_, readReceipts_[index.row()].first); + case Timestamp: + return dateFormat(readReceipts_[index.row()].second); + case RawTimestamp: + return readReceipts_[index.row()].second; + default: + return {}; + } +} + +void +ReadReceiptsModel::addUsers( + const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users) +{ + auto newReceipts = users.size() - readReceipts_.size(); + + if (newReceipts > 0) { + beginInsertRows( + QModelIndex{}, readReceipts_.size(), readReceipts_.size() + newReceipts - 1); + + for (const auto &user : users) { + QPair<QString, QDateTime> item = { + QString::fromStdString(user.second), + QDateTime::fromMSecsSinceEpoch(user.first)}; + if (!readReceipts_.contains(item)) + readReceipts_.push_back(item); + } + + endInsertRows(); + } +} + +QString +ReadReceiptsModel::dateFormat(const QDateTime &then) const +{ + auto now = QDateTime::currentDateTime(); + auto days = then.daysTo(now); + + if (days == 0) + return QLocale::system().toString(then.time(), QLocale::ShortFormat); + else if (days < 2) + return tr("Yesterday, %1") + .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + else if (days < 7) + //: %1 is the name of the current day, %2 is the time the read receipt was read. The + //: result may look like this: Monday, 7:15 + return QString("%1, %2") + .arg(then.toString("dddd")) + .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); + + return QLocale::system().toString(then.time(), QLocale::ShortFormat); +} + +ReadReceiptsProxy::ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent) + : QSortFilterProxyModel{parent} + , model_{event_id, room_id, this} +{ + setSourceModel(&model_); + setSortRole(ReadReceiptsModel::RawTimestamp); + sort(0, Qt::DescendingOrder); + setDynamicSortFilter(true); +} diff --git a/src/ReadReceiptsModel.h b/src/ReadReceiptsModel.h new file mode 100644 index 0000000000000000000000000000000000000000..3b45716c555d47a541635644f1fbfa6562344011 --- /dev/null +++ b/src/ReadReceiptsModel.h @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef READRECEIPTSMODEL_H +#define READRECEIPTSMODEL_H + +#include <QAbstractListModel> +#include <QDateTime> +#include <QObject> +#include <QSortFilterProxyModel> +#include <QString> + +class ReadReceiptsModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles + { + Mxid, + DisplayName, + AvatarUrl, + Timestamp, + RawTimestamp, + }; + + explicit ReadReceiptsModel(QString event_id, QString room_id, QObject *parent = nullptr); + + QString eventId() const { return event_id_; } + QString roomId() const { return room_id_; } + + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent) const override + { + Q_UNUSED(parent) + return readReceipts_.size(); + } + QVariant data(const QModelIndex &index, int role) const override; + +public slots: + void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users); + void update(); + +private: + QString dateFormat(const QDateTime &then) const; + + QString event_id_; + QString room_id_; + QVector<QPair<QString, QDateTime>> readReceipts_; +}; + +class ReadReceiptsProxy : public QSortFilterProxyModel +{ + Q_OBJECT + + Q_PROPERTY(QString eventId READ eventId CONSTANT) + Q_PROPERTY(QString roomId READ roomId CONSTANT) + +public: + explicit ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent = nullptr); + + QString eventId() const { return event_id_; } + QString roomId() const { return room_id_; } + +private: + QString event_id_; + QString room_id_; + + ReadReceiptsModel model_; +}; + +#endif // READRECEIPTSMODEL_H diff --git a/src/dialogs/ReadReceipts.cpp b/src/dialogs/ReadReceipts.cpp deleted file mode 100644 index fa7132fd4da7e1aea5dd1880f4a7a10da4419c44..0000000000000000000000000000000000000000 --- a/src/dialogs/ReadReceipts.cpp +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QDebug> -#include <QIcon> -#include <QLabel> -#include <QListWidgetItem> -#include <QPainter> -#include <QPushButton> -#include <QShortcut> -#include <QStyleOption> -#include <QTimer> -#include <QVBoxLayout> - -#include "dialogs/ReadReceipts.h" - -#include "AvatarProvider.h" -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "Utils.h" -#include "ui/Avatar.h" - -using namespace dialogs; - -ReceiptItem::ReceiptItem(QWidget *parent, - const QString &user_id, - uint64_t timestamp, - const QString &room_id) - : QWidget(parent) -{ - topLayout_ = new QHBoxLayout(this); - topLayout_->setMargin(0); - - textLayout_ = new QVBoxLayout; - textLayout_->setMargin(0); - textLayout_->setSpacing(conf::modals::TEXT_SPACING); - - QFont nameFont; - nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); - - auto displayName = cache::displayName(room_id, user_id); - - avatar_ = new Avatar(this, 44); - avatar_->setLetter(utils::firstChar(displayName)); - - // If it's a matrix id we use the second letter. - if (displayName.size() > 1 && displayName.at(0) == '@') - avatar_->setLetter(QChar(displayName.at(1))); - - userName_ = new QLabel(displayName, this); - userName_->setFont(nameFont); - - timestamp_ = new QLabel(dateFormat(QDateTime::fromMSecsSinceEpoch(timestamp)), this); - - textLayout_->addWidget(userName_); - textLayout_->addWidget(timestamp_); - - topLayout_->addWidget(avatar_); - topLayout_->addLayout(textLayout_, 1); - - avatar_->setImage(ChatPage::instance()->currentRoom(), user_id); -} - -void -ReceiptItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -QString -ReceiptItem::dateFormat(const QDateTime &then) const -{ - auto now = QDateTime::currentDateTime(); - auto days = then.daysTo(now); - - if (days == 0) - return tr("Today %1") - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); - else if (days < 2) - return tr("Yesterday %1") - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); - else if (days < 7) - return QString("%1 %2") - .arg(then.toString("dddd")) - .arg(QLocale::system().toString(then.time(), QLocale::ShortFormat)); - - return QLocale::system().toString(then.time(), QLocale::ShortFormat); -} - -ReadReceipts::ReadReceipts(QWidget *parent) - : QFrame(parent) -{ - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - setAttribute(Qt::WA_DeleteOnClose, true); - - auto layout = new QVBoxLayout(this); - layout->setSpacing(conf::modals::WIDGET_SPACING); - layout->setMargin(conf::modals::WIDGET_MARGIN); - - userList_ = new QListWidget; - userList_->setFrameStyle(QFrame::NoFrame); - userList_->setSelectionMode(QAbstractItemView::NoSelection); - userList_->setSpacing(conf::modals::TEXT_SPACING); - - QFont largeFont; - largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5); - - setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - setMinimumHeight(userList_->sizeHint().height() * 2); - setMinimumWidth(std::max(userList_->sizeHint().width() + 4 * conf::modals::WIDGET_MARGIN, - QFontMetrics(largeFont).averageCharWidth() * 30 - - 2 * conf::modals::WIDGET_MARGIN)); - - QFont font; - font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO); - - topLabel_ = new QLabel(tr("Read receipts"), this); - topLabel_->setAlignment(Qt::AlignCenter); - topLabel_->setFont(font); - - auto okBtn = new QPushButton(tr("Close"), this); - - auto buttonLayout = new QHBoxLayout(); - buttonLayout->setSpacing(15); - buttonLayout->addStretch(1); - buttonLayout->addWidget(okBtn); - - layout->addWidget(topLabel_); - layout->addWidget(userList_); - layout->addLayout(buttonLayout); - - auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); - connect(closeShortcut, &QShortcut::activated, this, &ReadReceipts::close); - connect(okBtn, &QPushButton::clicked, this, &ReadReceipts::close); -} - -void -ReadReceipts::addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &receipts) -{ - // We want to remove any previous items that have been set. - userList_->clear(); - - for (const auto &receipt : receipts) { - auto user = new ReceiptItem(this, - QString::fromStdString(receipt.second), - receipt.first, - ChatPage::instance()->currentRoom()); - auto item = new QListWidgetItem(userList_); - - item->setSizeHint(user->minimumSizeHint()); - item->setFlags(Qt::NoItemFlags); - item->setTextAlignment(Qt::AlignCenter); - - userList_->setItemWidget(item, user); - } -} - -void -ReadReceipts::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -ReadReceipts::hideEvent(QHideEvent *event) -{ - userList_->clear(); - QFrame::hideEvent(event); -} diff --git a/src/dialogs/ReadReceipts.h b/src/dialogs/ReadReceipts.h deleted file mode 100644 index 5c6c5d2bdbdcc604566302421f6872e006cdba5b..0000000000000000000000000000000000000000 --- a/src/dialogs/ReadReceipts.h +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QDateTime> -#include <QFrame> - -class Avatar; -class QLabel; -class QListWidget; -class QHBoxLayout; -class QVBoxLayout; - -namespace dialogs { - -class ReceiptItem : public QWidget -{ - Q_OBJECT - -public: - ReceiptItem(QWidget *parent, - const QString &user_id, - uint64_t timestamp, - const QString &room_id); - -protected: - void paintEvent(QPaintEvent *) override; - -private: - QString dateFormat(const QDateTime &then) const; - - QHBoxLayout *topLayout_; - QVBoxLayout *textLayout_; - - Avatar *avatar_; - - QLabel *userName_; - QLabel *timestamp_; -}; - -class ReadReceipts : public QFrame -{ - Q_OBJECT -public: - explicit ReadReceipts(QWidget *parent = nullptr); - -public slots: - void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users); - -protected: - void paintEvent(QPaintEvent *event) override; - void hideEvent(QHideEvent *event) override; - -private: - QLabel *topLabel_; - - QListWidget *userList_; -}; -} // dialogs diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index ee5564a5f5dee77f3b7f6de55ce2cd7fe1dbdfb3..6ae0c4d1a6df2cebbc771e3008f626d731354401 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -28,6 +28,7 @@ #include "MemberList.h" #include "MxcImageProvider.h" #include "Olm.h" +#include "ReadReceiptsModel.h" #include "TimelineViewManager.h" #include "Utils.h" #include "dialogs/RawMessage.h" @@ -1089,9 +1090,9 @@ TimelineModel::relatedInfo(QString id) } void -TimelineModel::readReceiptsAction(QString id) const +TimelineModel::showReadReceipts(QString id) { - MainWindow::instance()->openReadReceiptsDialog(id); + emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this}); } void diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 0e2ce153c66be5daa344abeac30b57ebd4e22f69..0d5f710982e5166e1c797db54f706d26bacf3359 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -20,6 +20,7 @@ #include "InviteesModel.h" #include "MemberList.h" #include "Permissions.h" +#include "ReadReceiptsModel.h" #include "ui/RoomSettings.h" #include "ui/UserProfile.h" @@ -241,7 +242,7 @@ public: Q_INVOKABLE void openUserProfile(QString userid); Q_INVOKABLE void editAction(QString id); Q_INVOKABLE void replyAction(QString id); - Q_INVOKABLE void readReceiptsAction(QString id) const; + Q_INVOKABLE void showReadReceipts(QString id); Q_INVOKABLE void redactEvent(QString id); Q_INVOKABLE int idToIndex(QString id) const; Q_INVOKABLE QString indexToId(int index) const; @@ -348,6 +349,7 @@ signals: void typingUsersChanged(std::vector<QString> users); void replyChanged(QString reply); void editChanged(QString reply); + void openReadReceiptsDialog(ReadReceiptsProxy *rr); void paginationInProgressChanged(const bool); void newCallEvent(const mtx::events::collections::TimelineEvents &event); void scrollToIndex(int index); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index a6922be7a1d9417cd92ca5ff9e5f38fa4f6b2e25..76bc127e197599fb4351cddfc1aaa07080f81775 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -26,6 +26,7 @@ #include "MainWindow.h" #include "MatrixClient.h" #include "MxcImageProvider.h" +#include "ReadReceiptsModel.h" #include "RoomsModel.h" #include "SingleImagePackModel.h" #include "UserSettingsPage.h" @@ -205,6 +206,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par 0, "InviteesModel", "InviteesModel needs to be instantiated on the C++ side"); + qmlRegisterUncreatableType<ReadReceiptsProxy>( + "im.nheko", + 1, + 0, + "ReadReceiptsProxy", + "ReadReceiptsProxy needs to be instantiated on the C++ side"); static auto self = this; qmlRegisterSingletonType<MainWindow>( diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp deleted file mode 100644 index 154a0e2c305ba3327e9ae97ec696aadb583f3a30..0000000000000000000000000000000000000000 --- a/src/ui/Avatar.cpp +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QPainter> -#include <QPainterPath> -#include <QSettings> - -#include "AvatarProvider.h" -#include "Utils.h" -#include "ui/Avatar.h" - -Avatar::Avatar(QWidget *parent, int size) - : QWidget(parent) - , size_(size) -{ - type_ = ui::AvatarType::Letter; - letter_ = "A"; - - QFont _font(font()); - _font.setPointSizeF(ui::FontSize); - setFont(_font); - - QSizePolicy policy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); - setSizePolicy(policy); -} - -QColor -Avatar::textColor() const -{ - if (!text_color_.isValid()) - return QColor("black"); - - return text_color_; -} - -QColor -Avatar::backgroundColor() const -{ - if (!text_color_.isValid()) - return QColor("white"); - - return background_color_; -} - -QSize -Avatar::sizeHint() const -{ - return QSize(size_ + 2, size_ + 2); -} - -void -Avatar::setTextColor(const QColor &color) -{ - text_color_ = color; -} - -void -Avatar::setBackgroundColor(const QColor &color) -{ - background_color_ = color; -} - -void -Avatar::setLetter(const QString &letter) -{ - letter_ = letter; - type_ = ui::AvatarType::Letter; - update(); -} - -void -Avatar::setImage(const QString &avatar_url) -{ - avatar_url_ = avatar_url; - AvatarProvider::resolve(avatar_url, - static_cast<int>(size_ * pixmap_.devicePixelRatio()), - this, - [this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) { - if (pm.isNull()) - return; - type_ = ui::AvatarType::Image; - pixmap_ = pm; - pixmap_.setDevicePixelRatio(requestedRatio); - update(); - }); -} - -void -Avatar::setImage(const QString &room, const QString &user) -{ - room_ = room; - user_ = user; - AvatarProvider::resolve(room, - user, - static_cast<int>(size_ * pixmap_.devicePixelRatio()), - this, - [this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) { - if (pm.isNull()) - return; - type_ = ui::AvatarType::Image; - pixmap_ = pm; - pixmap_.setDevicePixelRatio(requestedRatio); - update(); - }); -} - -void -Avatar::setDevicePixelRatio(double ratio) -{ - if (type_ == ui::AvatarType::Image && abs(pixmap_.devicePixelRatio() - ratio) > 0.01) { - pixmap_ = pixmap_.scaled(QSize(size_, size_) * ratio); - pixmap_.setDevicePixelRatio(ratio); - - if (!avatar_url_.isEmpty()) - setImage(avatar_url_); - else - setImage(room_, user_); - } -} - -void -Avatar::paintEvent(QPaintEvent *) -{ - bool rounded = QSettings().value(QStringLiteral("user/avatar_circles"), true).toBool(); - - QPainter painter(this); - - painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | - QPainter::TextAntialiasing); - - QRectF r = rect(); - const int hs = size_ / 2; - - if (type_ != ui::AvatarType::Image) { - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(backgroundColor()); - - painter.setPen(Qt::NoPen); - painter.setBrush(brush); - rounded ? painter.drawEllipse(r) : painter.drawRoundedRect(r, 3, 3); - } else if (painter.isActive()) { - setDevicePixelRatio(painter.device()->devicePixelRatioF()); - } - - switch (type_) { - case ui::AvatarType::Image: { - QPainterPath ppath; - - rounded ? ppath.addEllipse(width() / 2 - hs, height() / 2 - hs, size_, size_) - : ppath.addRoundedRect(r, 3, 3); - - painter.setClipPath(ppath); - painter.drawPixmap(QRect(width() / 2 - hs, height() / 2 - hs, size_, size_), - pixmap_); - break; - } - case ui::AvatarType::Letter: { - painter.setPen(textColor()); - painter.setBrush(Qt::NoBrush); - painter.drawText(r.translated(0, -1), Qt::AlignCenter, letter_); - break; - } - default: - break; - } -} diff --git a/src/ui/Avatar.h b/src/ui/Avatar.h deleted file mode 100644 index bbf05be3e7e6b03dc1c25387aa9b46f79a0f9402..0000000000000000000000000000000000000000 --- a/src/ui/Avatar.h +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QImage> -#include <QPixmap> -#include <QWidget> - -#include "Theme.h" - -class Avatar : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - explicit Avatar(QWidget *parent = nullptr, int size = ui::AvatarSize); - - void setBackgroundColor(const QColor &color); - void setImage(const QString &avatar_url); - void setImage(const QString &room, const QString &user); - void setLetter(const QString &letter); - void setTextColor(const QColor &color); - void setDevicePixelRatio(double ratio); - - QColor backgroundColor() const; - QColor textColor() const; - - QSize sizeHint() const override; - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - void init(); - - ui::AvatarType type_; - QString letter_; - QString avatar_url_, room_, user_; - QColor background_color_; - QColor text_color_; - QPixmap pixmap_; - int size_; -};