From 1c9cc33902d8242ec25b4416a1662ee6c9902583 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 28 Jul 2019 12:50:10 +0200 Subject: Try to localise timestamps I'm not sure, if that is the right way, but Qt doesn't really have a way to format custom localised dates, so I tried to find the closest approximations to what we currently have. Relates to #69 --- src/dialogs/MemberList.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/dialogs/MemberList.cpp') diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp index f4167143..3b957c15 100644 --- a/src/dialogs/MemberList.cpp +++ b/src/dialogs/MemberList.cpp @@ -97,7 +97,7 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) topLabel_->setAlignment(Qt::AlignCenter); topLabel_->setFont(font); - auto okBtn = new QPushButton("OK", this); + auto okBtn = new QPushButton(tr("OK"), this); auto buttonLayout = new QHBoxLayout(); buttonLayout->setSpacing(15); @@ -126,7 +126,7 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) qCritical() << e.what(); } - auto closeShortcut = new QShortcut(QKeySequence(tr("ESC")), this); + auto closeShortcut = new QShortcut(QKeySequence("ESC"), this); connect(closeShortcut, &QShortcut::activated, this, &MemberList::close); connect(okBtn, &QPushButton::clicked, this, &MemberList::close); } -- cgit 1.5.1 From dbb8eecf292626a59b52d3c524d41f102716f235 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 28 Jul 2019 16:16:02 +0200 Subject: media->file in error message, when image upload fails --- resources/langs/nheko_de.ts | 23 ++++------------------- resources/langs/nheko_el.ts | 23 ++++------------------- resources/langs/nheko_en.ts | 23 ++++------------------- resources/langs/nheko_fi.ts | 23 ++++------------------- resources/langs/nheko_fr.ts | 23 ++++------------------- resources/langs/nheko_nl.ts | 23 ++++------------------- resources/langs/nheko_pl.ts | 27 ++++++--------------------- resources/langs/nheko_ru.ts | 27 ++++++--------------------- resources/langs/nheko_zh_CN.ts | 27 ++++++--------------------- src/dialogs/MemberList.cpp | 2 +- src/dialogs/ReadReceipts.cpp | 2 +- src/dialogs/RoomSettings.cpp | 6 +++--- src/dialogs/UserProfile.cpp | 2 +- 13 files changed, 48 insertions(+), 183 deletions(-) (limited to 'src/dialogs/MemberList.cpp') diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts index a3acae8e..1f856b80 100644 --- a/resources/langs/nheko_de.ts +++ b/resources/langs/nheko_de.ts @@ -835,16 +835,11 @@ Medien-Größe: %2 Close - - - ESC - - dialogs::ReceiptItem - + Today %1 @@ -952,12 +947,7 @@ Medien-Größe: %2 - - ESC - - - - + Failed to enable encryption: %1 @@ -973,12 +963,12 @@ Medien-Größe: %2 - The selected media is not an image + The selected file is not an image - Error while reading media: %1 + Error while reading file: %1 @@ -1015,11 +1005,6 @@ Medien-Größe: %2 Devices - - - ESC - - emoji::Panel diff --git a/resources/langs/nheko_el.ts b/resources/langs/nheko_el.ts index 462f523e..e50aadad 100644 --- a/resources/langs/nheko_el.ts +++ b/resources/langs/nheko_el.ts @@ -833,16 +833,11 @@ Media size: %2 Close - - - ESC - - dialogs::ReceiptItem - + Today %1 @@ -950,12 +945,7 @@ Media size: %2 - - ESC - - - - + Failed to enable encryption: %1 @@ -971,12 +961,12 @@ Media size: %2 - The selected media is not an image + The selected file is not an image - Error while reading media: %1 + Error while reading file: %1 @@ -1013,11 +1003,6 @@ Media size: %2 Devices - - - ESC - - emoji::Panel diff --git a/resources/langs/nheko_en.ts b/resources/langs/nheko_en.ts index afd4c815..e031c4ec 100644 --- a/resources/langs/nheko_en.ts +++ b/resources/langs/nheko_en.ts @@ -833,16 +833,11 @@ Media size: %2 Close - - - ESC - - dialogs::ReceiptItem - + Today %1 @@ -950,12 +945,7 @@ Media size: %2 - - ESC - - - - + Failed to enable encryption: %1 @@ -971,12 +961,12 @@ Media size: %2 - The selected media is not an image + The selected file is not an image - Error while reading media: %1 + Error while reading file: %1 @@ -1013,11 +1003,6 @@ Media size: %2 Devices - - - ESC - - emoji::Panel diff --git a/resources/langs/nheko_fi.ts b/resources/langs/nheko_fi.ts index 789a4ebd..2baba55d 100644 --- a/resources/langs/nheko_fi.ts +++ b/resources/langs/nheko_fi.ts @@ -833,16 +833,11 @@ Media size: %2 Close - - - ESC - - dialogs::ReceiptItem - + Today %1 @@ -950,12 +945,7 @@ Media size: %2 - - ESC - - - - + Failed to enable encryption: %1 @@ -971,12 +961,12 @@ Media size: %2 - The selected media is not an image + The selected file is not an image - Error while reading media: %1 + Error while reading file: %1 @@ -1013,11 +1003,6 @@ Media size: %2 Devices - - - ESC - - emoji::Panel diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts index b8c0d6d9..355c4325 100644 --- a/resources/langs/nheko_fr.ts +++ b/resources/langs/nheko_fr.ts @@ -836,16 +836,11 @@ Taille du média : %2 Close - - - ESC - - dialogs::ReceiptItem - + Today %1 @@ -953,12 +948,7 @@ Taille du média : %2 - - ESC - - - - + Failed to enable encryption: %1 @@ -974,12 +964,12 @@ Taille du média : %2 - The selected media is not an image + The selected file is not an image - Error while reading media: %1 + Error while reading file: %1 @@ -1016,11 +1006,6 @@ Taille du média : %2 Devices - - - ESC - - emoji::Panel diff --git a/resources/langs/nheko_nl.ts b/resources/langs/nheko_nl.ts index b1be0135..db56dc0f 100644 --- a/resources/langs/nheko_nl.ts +++ b/resources/langs/nheko_nl.ts @@ -835,16 +835,11 @@ Mediagrootte: %2 Close - - - ESC - - dialogs::ReceiptItem - + Today %1 @@ -952,12 +947,7 @@ Mediagrootte: %2 - - ESC - - - - + Failed to enable encryption: %1 @@ -973,12 +963,12 @@ Mediagrootte: %2 - The selected media is not an image + The selected file is not an image - Error while reading media: %1 + Error while reading file: %1 @@ -1015,11 +1005,6 @@ Mediagrootte: %2 Devices - - - ESC - - emoji::Panel diff --git a/resources/langs/nheko_pl.ts b/resources/langs/nheko_pl.ts index 2f7c903c..34a4ed26 100644 --- a/resources/langs/nheko_pl.ts +++ b/resources/langs/nheko_pl.ts @@ -835,16 +835,11 @@ Rozmiar multimediów: %2 Close - - - ESC - - dialogs::ReceiptItem - + Today %1 @@ -955,12 +950,7 @@ Rozmiar multimediów: %2 - - ESC - - - - + Failed to enable encryption: %1 Nie udało się włączyć szyfrowania: %1 @@ -976,13 +966,13 @@ Rozmiar multimediów: %2 - The selected media is not an image - Wybrany plik multimedialny nie jest obrazem + The selected file is not an image + - Error while reading media: %1 - Błąd odczytywania pliku: %1 + Error while reading file: %1 + @@ -1018,11 +1008,6 @@ Rozmiar multimediów: %2 Devices Urządzenia - - - ESC - - emoji::Panel diff --git a/resources/langs/nheko_ru.ts b/resources/langs/nheko_ru.ts index 097067db..afe36ef9 100644 --- a/resources/langs/nheko_ru.ts +++ b/resources/langs/nheko_ru.ts @@ -836,16 +836,11 @@ Media size: %2 Close Закрыть - - - ESC - - dialogs::ReceiptItem - + Today %1 @@ -954,12 +949,7 @@ Media size: %2 - - ESC - - - - + Failed to enable encryption: %1 Не удалось включить шифрование: %1 @@ -975,13 +965,13 @@ Media size: %2 - The selected media is not an image - Выбранное медия не является изображением + The selected file is not an image + - Error while reading media: %1 - Ошибка при чтении медия: %1 + Error while reading file: %1 + @@ -1017,11 +1007,6 @@ Media size: %2 Devices Устройства - - - ESC - - emoji::Panel diff --git a/resources/langs/nheko_zh_CN.ts b/resources/langs/nheko_zh_CN.ts index 2481f989..607b2ba9 100644 --- a/resources/langs/nheko_zh_CN.ts +++ b/resources/langs/nheko_zh_CN.ts @@ -835,16 +835,11 @@ Media size: %2 Close - - - ESC - - dialogs::ReceiptItem - + Today %1 @@ -953,12 +948,7 @@ Media size: %2 - - ESC - - - - + Failed to enable encryption: %1 启用加密失败:%1 @@ -974,13 +964,13 @@ Media size: %2 - The selected media is not an image - 选择的媒体不是一个图像 + The selected file is not an image + - Error while reading media: %1 - 读取媒体时失败:%1 + Error while reading file: %1 + @@ -1016,11 +1006,6 @@ Media size: %2 Devices 设备 - - - ESC - - emoji::Panel diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp index 3b957c15..88a95403 100644 --- a/src/dialogs/MemberList.cpp +++ b/src/dialogs/MemberList.cpp @@ -126,7 +126,7 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) qCritical() << e.what(); } - auto closeShortcut = new QShortcut(QKeySequence("ESC"), this); + auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); connect(closeShortcut, &QShortcut::activated, this, &MemberList::close); connect(okBtn, &QPushButton::clicked, this, &MemberList::close); } diff --git a/src/dialogs/ReadReceipts.cpp b/src/dialogs/ReadReceipts.cpp index 03ce3068..5a0d98c7 100644 --- a/src/dialogs/ReadReceipts.cpp +++ b/src/dialogs/ReadReceipts.cpp @@ -133,7 +133,7 @@ ReadReceipts::ReadReceipts(QWidget *parent) layout->addWidget(userList_); layout->addLayout(buttonLayout); - auto closeShortcut = new QShortcut(QKeySequence(tr("ESC")), this); + auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); connect(closeShortcut, &QShortcut::activated, this, &ReadReceipts::close); connect(okBtn, &QPushButton::clicked, this, &ReadReceipts::close); } diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index b2344f23..1fe5904b 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -438,7 +438,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) resetErrorLabel(); }); - auto closeShortcut = new QShortcut(QKeySequence(tr("ESC")), this); + auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); connect(closeShortcut, &QShortcut::activated, this, &RoomSettings::close); connect(okBtn, &QPushButton::clicked, this, &RoomSettings::close); } @@ -668,12 +668,12 @@ RoomSettings::updateAvatar() QFile file{fileName, this}; if (format != "image") { - displayErrorMessage(tr("The selected media is not an image")); + displayErrorMessage(tr("The selected file is not an image")); return; } if (!file.open(QIODevice::ReadOnly)) { - displayErrorMessage(tr("Error while reading media: %1").arg(file.errorString())); + displayErrorMessage(tr("Error while reading file: %1").arg(file.errorString())); return; } diff --git a/src/dialogs/UserProfile.cpp b/src/dialogs/UserProfile.cpp index b8040f9f..6aea96a8 100644 --- a/src/dialogs/UserProfile.cpp +++ b/src/dialogs/UserProfile.cpp @@ -183,7 +183,7 @@ UserProfile::UserProfile(QWidget *parent) qRegisterMetaType>(); - auto closeShortcut = new QShortcut(QKeySequence(tr("ESC")), this); + auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); connect(closeShortcut, &QShortcut::activated, this, &UserProfile::close); connect(okBtn, &QPushButton::clicked, this, &UserProfile::close); } -- cgit 1.5.1 From 52056a79fa94117ce0fc8f388458ceb48cc3be54 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 26 Aug 2019 01:24:56 +0200 Subject: Try to reduce memory usage by reusing avatar pixmaps --- src/AvatarProvider.cpp | 55 +++++++++++++++++++++++++++++++------ src/AvatarProvider.h | 12 ++++++-- src/Cache.cpp | 5 +--- src/Cache.h | 1 - src/ChatPage.cpp | 38 +++---------------------- src/ChatPage.h | 4 +-- src/Logging.cpp | 2 ++ src/Logging.h | 2 ++ src/RoomInfoListItem.cpp | 9 ++++-- src/RoomInfoListItem.h | 2 +- src/RoomList.cpp | 39 ++------------------------ src/RoomList.h | 6 ++-- src/TopRoomBar.cpp | 5 ++-- src/TopRoomBar.h | 2 +- src/UserInfoWidget.cpp | 19 ++++++------- src/UserInfoWidget.h | 2 +- src/dialogs/MemberList.cpp | 12 ++------ src/dialogs/ReadReceipts.cpp | 8 ++---- src/dialogs/RoomSettings.cpp | 17 ++++++------ src/dialogs/RoomSettings.h | 4 +-- src/dialogs/UserProfile.cpp | 6 ++-- src/popups/PopupItem.cpp | 26 ++++-------------- src/timeline/.TimelineItem.cpp.swp | Bin 0 -> 114688 bytes src/timeline/TimelineItem.cpp | 22 ++++++--------- src/timeline/TimelineItem.h | 8 ++---- src/ui/Avatar.cpp | 39 ++++++++++++-------------- src/ui/Avatar.h | 10 ++++--- 27 files changed, 148 insertions(+), 207 deletions(-) create mode 100644 src/timeline/.TimelineItem.cpp.swp (limited to 'src/dialogs/MemberList.cpp') diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp index 57b61c75..277a4030 100644 --- a/src/AvatarProvider.cpp +++ b/src/AvatarProvider.cpp @@ -16,30 +16,44 @@ */ #include +#include #include +#include #include "AvatarProvider.h" #include "Cache.h" #include "Logging.h" #include "MatrixClient.h" -namespace AvatarProvider { +static QPixmapCache avatar_cache; +namespace AvatarProvider { void -resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback callback) +resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback callback) { - const auto key = QString("%1 %2").arg(room_id).arg(user_id); - const auto avatarUrl = Cache::avatarUrl(room_id, user_id); + avatar_cache.setCacheLimit(1024 * 1024); - if (!Cache::AvatarUrls.contains(key) || !cache::client()) + const auto cacheKey = avatarUrl + "_size_" + size; + + if (!cache::client()) return; if (avatarUrl.isEmpty()) return; + QPixmap pixmap; + if (avatar_cache.find(cacheKey, pixmap)) { + nhlog::net()->info("cached pixmap {}", avatarUrl.toStdString()); + callback(pixmap); + return; + } + auto data = cache::client()->image(avatarUrl); if (!data.isNull()) { - callback(QImage::fromData(data)); + pixmap.loadFromData(data); + avatar_cache.insert(cacheKey, pixmap); + nhlog::net()->info("loaded pixmap from disk cache {}", avatarUrl.toStdString()); + callback(pixmap); return; } @@ -47,7 +61,12 @@ resolve(const QString &room_id, const QString &user_id, QObject *receiver, Avata QObject::connect(proxy.get(), &AvatarProxy::avatarDownloaded, receiver, - [callback](const QByteArray &data) { callback(QImage::fromData(data)); }); + [callback, cacheKey](const QByteArray &data) { + QPixmap pm; + pm.loadFromData(data); + avatar_cache.insert(cacheKey, pm); + callback(pm); + }); mtx::http::ThumbOpts opts; opts.width = 256; @@ -67,8 +86,26 @@ resolve(const QString &room_id, const QString &user_id, QObject *receiver, Avata cache::client()->saveImage(opts.mxc_url, res); - auto data = QByteArray(res.data(), res.size()); - emit proxy->avatarDownloaded(data); + nhlog::net()->info("downloaded pixmap {}", opts.mxc_url); + + emit proxy->avatarDownloaded(QByteArray(res.data(), res.size())); }); } + +void +resolve(const QString &room_id, + const QString &user_id, + int size, + QObject *receiver, + AvatarCallback callback) +{ + const auto key = QString("%1 %2").arg(room_id).arg(user_id); + const auto avatarUrl = Cache::avatarUrl(room_id, user_id); + const auto cacheKey = avatarUrl + "_size_" + size; + + if (!Cache::AvatarUrls.contains(key) || !cache::client()) + return; + + resolve(avatarUrl, size, receiver, callback); +} } diff --git a/src/AvatarProvider.h b/src/AvatarProvider.h index 4b4e15e9..47ed028e 100644 --- a/src/AvatarProvider.h +++ b/src/AvatarProvider.h @@ -17,7 +17,7 @@ #pragma once -#include +#include #include class AvatarProxy : public QObject @@ -28,9 +28,15 @@ signals: void avatarDownloaded(const QByteArray &data); }; -using AvatarCallback = std::function; +using AvatarCallback = std::function; namespace AvatarProvider { void -resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback cb); +resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback cb); +void +resolve(const QString &room_id, + const QString &user_id, + int size, + QObject *receiver, + AvatarCallback cb); } diff --git a/src/Cache.cpp b/src/Cache.cpp index ef0f951e..083dbe89 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -1830,10 +1830,7 @@ Cache::searchRooms(const std::string &query, std::uint8_t max_items) std::vector results; for (auto it = items.begin(); it != end; it++) { - results.push_back( - RoomSearchResult{it->second.first, - it->second.second, - QImage::fromData(image(txn, it->second.second.avatar_url))}); + results.push_back(RoomSearchResult{it->second.first, it->second.second}); } txn.commit(); diff --git a/src/Cache.h b/src/Cache.h index 302bb65b..0da49793 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -153,7 +153,6 @@ struct RoomSearchResult { std::string room_id; RoomInfo info; - QImage img; }; Q_DECLARE_METATYPE(RoomSearchResult) diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 2ed64b6b..21ded4b3 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -774,12 +774,12 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) } void -ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img) +ChatPage::updateTopBarAvatar(const QString &roomid, const QString &img) { if (current_room_ != roomid) return; - top_bar_->updateRoomAvatar(img.toImage()); + top_bar_->updateRoomAvatar(img); } void @@ -807,7 +807,7 @@ ChatPage::changeTopRoomInfo(const QString &room_id) if (img.isNull()) top_bar_->updateRoomAvatarFromName(name); else - top_bar_->updateRoomAvatar(img); + top_bar_->updateRoomAvatar(avatar_url); } catch (const lmdb::error &e) { nhlog::ui()->error("failed to change top bar room info: {}", e.what()); @@ -1337,37 +1337,7 @@ ChatPage::getProfileInfo() emit setUserDisplayName(QString::fromStdString(res.display_name)); - if (cache::client()) { - auto data = cache::client()->image(res.avatar_url); - if (!data.isNull()) { - emit setUserAvatar(QImage::fromData(data)); - return; - } - } - - if (res.avatar_url.empty()) - return; - - http::client()->download( - res.avatar_url, - [this, res](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to download user avatar: {} - {}", - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - if (cache::client()) - cache::client()->saveImage(res.avatar_url, data); - - emit setUserAvatar( - QImage::fromData(QByteArray(data.data(), data.size()))); - }); + emit setUserAvatar(QString::fromStdString(res.avatar_url)); }); http::client()->joined_groups( diff --git a/src/ChatPage.h b/src/ChatPage.h index 189af387..e41ae1ae 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -129,7 +129,7 @@ signals: void ownProfileOk(); void setUserDisplayName(const QString &name); - void setUserAvatar(const QImage &avatar); + void setUserAvatar(const QString &avatar); void loggedOut(); void trySyncCb(); @@ -159,7 +159,7 @@ signals: private slots: void showUnreadMessageNotification(int count); - void updateTopBarAvatar(const QString &roomid, const QPixmap &img); + void updateTopBarAvatar(const QString &roomid, const QString &img); void changeTopRoomInfo(const QString &room_id); void logout(); void removeRoom(const QString &room_id); diff --git a/src/Logging.cpp b/src/Logging.cpp index 686274d8..32287582 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -16,6 +16,8 @@ constexpr auto MAX_LOG_FILES = 3; } namespace nhlog { +bool enable_debug_log_from_commandline = false; + void init(const std::string &file_path) { diff --git a/src/Logging.h b/src/Logging.h index 2feae60d..e54f3c3f 100644 --- a/src/Logging.h +++ b/src/Logging.h @@ -18,4 +18,6 @@ db(); std::shared_ptr crypto(); + +extern bool enable_debug_log_from_commandline; } diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp index 9bcce134..dbcd6806 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp @@ -21,6 +21,7 @@ #include #include +#include "AvatarProvider.h" #include "Cache.h" #include "Config.h" #include "RoomInfoListItem.h" @@ -434,10 +435,12 @@ RoomInfoListItem::mousePressEvent(QMouseEvent *event) } void -RoomInfoListItem::setAvatar(const QImage &img) +RoomInfoListItem::setAvatar(const QString &avatar_url) { - roomAvatar_ = utils::scaleImageToPixmap(img, IconSize); - update(); + AvatarProvider::resolve(avatar_url, IconSize, this, [this](const QPixmap &img) { + roomAvatar_ = img; + update(); + }); } void diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h index 40c938c1..54e02a76 100644 --- a/src/RoomInfoListItem.h +++ b/src/RoomInfoListItem.h @@ -73,7 +73,7 @@ public: bool isPressed() const { return isPressed_; } int unreadMessageCount() const { return unreadMsgCount_; } - void setAvatar(const QImage &avatar_image); + void setAvatar(const QString &avatar_url); void setDescriptionMessage(const DescInfo &info); DescInfo lastMessageInfo() const { return lastMsgInfo_; } diff --git a/src/RoomList.cpp b/src/RoomList.cpp index 1abf3533..c5e05621 100644 --- a/src/RoomList.cpp +++ b/src/RoomList.cpp @@ -89,40 +89,7 @@ RoomList::updateAvatar(const QString &room_id, const QString &url) if (url.isEmpty()) return; - QByteArray savedImgData; - - if (cache::client()) - savedImgData = cache::client()->image(url); - - if (savedImgData.isEmpty()) { - mtx::http::ThumbOpts opts; - opts.mxc_url = url.toStdString(); - http::client()->get_thumbnail( - opts, [room_id, opts, this](const std::string &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to download room avatar: {} {} {}", - opts.mxc_url, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - if (cache::client()) - cache::client()->saveImage(opts.mxc_url, res); - - auto data = QByteArray(res.data(), res.size()); - QPixmap pixmap; - pixmap.loadFromData(data); - - emit updateRoomAvatarCb(room_id, pixmap); - }); - } else { - QPixmap img; - img.loadFromData(savedImgData); - - updateRoomAvatar(room_id, img); - } + emit updateRoomAvatarCb(room_id, url); } void @@ -252,7 +219,7 @@ RoomList::highlightSelectedRoom(const QString &room_id) } void -RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img) +RoomList::updateRoomAvatar(const QString &roomid, const QString &img) { if (!roomExists(roomid)) { nhlog::ui()->warn("avatar update on non-existent room_id: {}", @@ -260,7 +227,7 @@ RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img) return; } - rooms_[roomid]->setAvatar(img.toImage()); + rooms_[roomid]->setAvatar(img); // Used to inform other widgets for the new image data. emit roomAvatarChanged(roomid, img); diff --git a/src/RoomList.h b/src/RoomList.h index 155a969c..95fc0d9b 100644 --- a/src/RoomList.h +++ b/src/RoomList.h @@ -61,12 +61,12 @@ signals: void totalUnreadMessageCountUpdated(int count); void acceptInvite(const QString &room_id); void declineInvite(const QString &room_id); - void roomAvatarChanged(const QString &room_id, const QPixmap &img); + void roomAvatarChanged(const QString &room_id, const QString &img); void joinRoom(const QString &room_id); - void updateRoomAvatarCb(const QString &room_id, const QPixmap &img); + void updateRoomAvatarCb(const QString &room_id, const QString &img); public slots: - void updateRoomAvatar(const QString &roomid, const QPixmap &img); + void updateRoomAvatar(const QString &roomid, const QString &img); void highlightSelectedRoom(const QString &room_id); void updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount); void updateRoomDescription(const QString &roomid, const DescInfo &info); diff --git a/src/TopRoomBar.cpp b/src/TopRoomBar.cpp index a8049e3a..712fe9aa 100644 --- a/src/TopRoomBar.cpp +++ b/src/TopRoomBar.cpp @@ -46,9 +46,8 @@ TopRoomBar::TopRoomBar(QWidget *parent) topLayout_->setContentsMargins( 2 * widgetMargin, widgetMargin, 2 * widgetMargin, widgetMargin); - avatar_ = new Avatar(this); + avatar_ = new Avatar(this, fontHeight * 2); avatar_->setLetter(""); - avatar_->setSize(fontHeight * 2); textLayout_ = new QVBoxLayout(); textLayout_->setSpacing(0); @@ -183,7 +182,7 @@ TopRoomBar::reset() } void -TopRoomBar::updateRoomAvatar(const QImage &avatar_image) +TopRoomBar::updateRoomAvatar(const QString &avatar_image) { avatar_->setImage(avatar_image); update(); diff --git a/src/TopRoomBar.h b/src/TopRoomBar.h index 5f2c936e..3243064e 100644 --- a/src/TopRoomBar.h +++ b/src/TopRoomBar.h @@ -44,7 +44,7 @@ class TopRoomBar : public QWidget public: TopRoomBar(QWidget *parent = 0); - void updateRoomAvatar(const QImage &avatar_image); + void updateRoomAvatar(const QString &avatar_image); void updateRoomAvatar(const QIcon &icon); void updateRoomName(const QString &name); void updateRoomTopic(QString topic); diff --git a/src/UserInfoWidget.cpp b/src/UserInfoWidget.cpp index 5345fb2a..19d7478e 100644 --- a/src/UserInfoWidget.cpp +++ b/src/UserInfoWidget.cpp @@ -52,10 +52,9 @@ UserInfoWidget::UserInfoWidget(QWidget *parent) textLayout_->setSpacing(widgetMargin / 2); textLayout_->setContentsMargins(widgetMargin * 2, widgetMargin, widgetMargin, widgetMargin); - userAvatar_ = new Avatar(this); + userAvatar_ = new Avatar(this, fontHeight * 2.5); userAvatar_->setObjectName("userAvatar"); userAvatar_->setLetter(QChar('?')); - userAvatar_->setSize(fontHeight * 2.5); QFont nameFont; nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); @@ -134,14 +133,6 @@ UserInfoWidget::reset() userAvatar_->setLetter(QChar('?')); } -void -UserInfoWidget::setAvatar(const QImage &img) -{ - avatar_image_ = img; - userAvatar_->setImage(img); - update(); -} - void UserInfoWidget::setDisplayName(const QString &name) { @@ -160,6 +151,14 @@ UserInfoWidget::setUserId(const QString &userid) { user_id_ = userid; userIdLabel_->setText(userid); + update(); +} + +void +UserInfoWidget::setAvatar(const QString &url) +{ + userAvatar_->setImage(url); + update(); } void diff --git a/src/UserInfoWidget.h b/src/UserInfoWidget.h index 65de7be9..263dd0c2 100644 --- a/src/UserInfoWidget.h +++ b/src/UserInfoWidget.h @@ -33,9 +33,9 @@ class UserInfoWidget : public QWidget public: UserInfoWidget(QWidget *parent = 0); - void setAvatar(const QImage &img); void setDisplayName(const QString &name); void setUserId(const QString &userid); + void setAvatar(const QString &url); void reset(); diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp index 88a95403..9e973efa 100644 --- a/src/dialogs/MemberList.cpp +++ b/src/dialogs/MemberList.cpp @@ -9,7 +9,6 @@ #include "dialogs/MemberList.h" -#include "AvatarProvider.h" #include "Cache.h" #include "ChatPage.h" #include "Config.h" @@ -28,17 +27,10 @@ MemberItem::MemberItem(const RoomMember &member, QWidget *parent) textLayout_->setMargin(0); textLayout_->setSpacing(0); - avatar_ = new Avatar(this); - avatar_->setSize(44); + avatar_ = new Avatar(this, 44); avatar_->setLetter(utils::firstChar(member.display_name)); - if (!member.avatar.isNull()) - avatar_->setImage(member.avatar); - else - AvatarProvider::resolve(ChatPage::instance()->currentRoom(), - member.user_id, - this, - [this](const QImage &img) { avatar_->setImage(img); }); + avatar_->setImage(ChatPage::instance()->currentRoom(), member.user_id); QFont nameFont; nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); diff --git a/src/dialogs/ReadReceipts.cpp b/src/dialogs/ReadReceipts.cpp index 5a0d98c7..58ad59c3 100644 --- a/src/dialogs/ReadReceipts.cpp +++ b/src/dialogs/ReadReceipts.cpp @@ -37,8 +37,7 @@ ReceiptItem::ReceiptItem(QWidget *parent, auto displayName = Cache::displayName(room_id, user_id); - avatar_ = new Avatar(this); - avatar_->setSize(44); + avatar_ = new Avatar(this, 44); avatar_->setLetter(utils::firstChar(displayName)); // If it's a matrix id we use the second letter. @@ -56,10 +55,7 @@ ReceiptItem::ReceiptItem(QWidget *parent, topLayout_->addWidget(avatar_); topLayout_->addLayout(textLayout_, 1); - AvatarProvider::resolve(ChatPage::instance()->currentRoom(), - user_id, - this, - [this](const QImage &img) { avatar_->setImage(img); }); + avatar_->setImage(ChatPage::instance()->currentRoom(), user_id); } void diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index 1fe5904b..00b034cc 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -350,12 +350,12 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) keyRequestsToggle_->hide(); } - avatar_ = new Avatar(this); - avatar_->setSize(128); + avatar_ = new Avatar(this, 128); if (avatarImg_.isNull()) avatar_->setLetter(utils::firstChar(QString::fromStdString(info_.name))); else - avatar_->setImage(avatarImg_); + avatar_->setImage(room_id_, + QString::fromStdString(http::client()->user_id().to_string())); if (canChangeAvatar(room_id_.toStdString(), utils::localUser().toStdString())) { auto filter = new ClickableFilter(this); @@ -487,7 +487,7 @@ RoomSettings::retrieveRoomInfo() try { usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString()); info_ = cache::client()->singleRoomInfo(room_id_.toStdString()); - setAvatar(QImage::fromData(cache::client()->image(info_.avatar_url))); + setAvatar(); } catch (const lmdb::error &e) { nhlog::db()->warn("failed to retrieve room info from cache: {}", room_id_.toStdString()); @@ -633,14 +633,13 @@ RoomSettings::displayErrorMessage(const QString &msg) } void -RoomSettings::setAvatar(const QImage &img) +RoomSettings::setAvatar() { stopLoadingSpinner(); - avatarImg_ = img; - if (avatar_) - avatar_->setImage(img); + avatar_->setImage(room_id_, + QString::fromStdString(http::client()->user_id().to_string())); } void @@ -733,7 +732,7 @@ RoomSettings::updateAvatar() return; } - emit proxy->avatarChanged(QImage::fromData(content)); + emit proxy->avatarChanged(); }); }); } diff --git a/src/dialogs/RoomSettings.h b/src/dialogs/RoomSettings.h index 6667b68b..e1807ba1 100644 --- a/src/dialogs/RoomSettings.h +++ b/src/dialogs/RoomSettings.h @@ -52,7 +52,7 @@ class ThreadProxy : public QObject signals: void error(const QString &msg); - void avatarChanged(const QImage &img); + void avatarChanged(); void nameEventSent(const QString &); void topicEventSent(); }; @@ -140,7 +140,7 @@ private: void resetErrorLabel(); void displayErrorMessage(const QString &msg); - void setAvatar(const QImage &img); + void setAvatar(); void setupEditButton(); //! Retrieve the current room information from cache. void retrieveRoomInfo(); diff --git a/src/dialogs/UserProfile.cpp b/src/dialogs/UserProfile.cpp index 6aea96a8..5ad3afa2 100644 --- a/src/dialogs/UserProfile.cpp +++ b/src/dialogs/UserProfile.cpp @@ -114,9 +114,8 @@ UserProfile::UserProfile(QWidget *parent) btnLayout->setSpacing(8); btnLayout->setMargin(0); - avatar_ = new Avatar(this); + avatar_ = new Avatar(this, 128); avatar_->setLetter("X"); - avatar_->setSize(128); QFont font; font.setPointSizeF(font.pointSizeF() * 2); @@ -210,8 +209,7 @@ UserProfile::init(const QString &userId, const QString &roomId) displayNameLabel_->setText(displayName); avatar_->setLetter(utils::firstChar(displayName)); - AvatarProvider::resolve( - roomId, userId, this, [this](const QImage &img) { avatar_->setImage(img); }); + avatar_->setImage(roomId, userId); auto localUser = utils::localUser(); diff --git a/src/popups/PopupItem.cpp b/src/popups/PopupItem.cpp index f905983a..c4d4327f 100644 --- a/src/popups/PopupItem.cpp +++ b/src/popups/PopupItem.cpp @@ -11,7 +11,7 @@ constexpr int PopupItemMargin = 3; PopupItem::PopupItem(QWidget *parent) : QWidget(parent) - , avatar_{new Avatar(this)} + , avatar_{new Avatar(this, conf::popup::avatar)} , hovering_{false} { setMouseTracking(true); @@ -40,7 +40,6 @@ UserItem::UserItem(QWidget *parent) : PopupItem(parent) { userName_ = new QLabel("Placeholder", this); - avatar_->setSize(conf::popup::avatar); avatar_->setLetter("P"); topLayout_->addWidget(avatar_); topLayout_->addWidget(userName_, 1); @@ -52,7 +51,6 @@ UserItem::UserItem(QWidget *parent, const QString &user_id) { auto displayName = Cache::displayName(ChatPage::instance()->currentRoom(), userId_); - avatar_->setSize(conf::popup::avatar); avatar_->setLetter(utils::firstChar(displayName)); // If it's a matrix id we use the second letter. @@ -87,16 +85,7 @@ UserItem::updateItem(const QString &user_id) void UserItem::resolveAvatar(const QString &user_id) { - AvatarProvider::resolve( - ChatPage::instance()->currentRoom(), userId_, this, [this, user_id](const QImage &img) { - // The user on the widget when the avatar is resolved, - // might be different from the user that made the call. - if (user_id == userId_) - avatar_->setImage(img); - else - // We try to resolve the avatar again. - resolveAvatar(userId_); - }); + avatar_->setImage(ChatPage::instance()->currentRoom(), user_id); } void @@ -116,7 +105,6 @@ RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res) auto name = QFontMetrics(QFont()).elidedText( QString::fromStdString(res.info.name), Qt::ElideRight, parentWidget()->width() - 10); - avatar_->setSize(conf::popup::avatar + 6); avatar_->setLetter(utils::firstChar(name)); roomName_ = new QLabel(name, this); @@ -125,8 +113,7 @@ RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res) topLayout_->addWidget(avatar_); topLayout_->addWidget(roomName_, 1); - if (!res.img.isNull()) - avatar_->setImage(res.img); + avatar_->setImage(QString::fromStdString(res.info.avatar_url)); } void @@ -141,10 +128,7 @@ RoomItem::updateItem(const RoomSearchResult &result) roomName_->setText(name); - if (!result.img.isNull()) - avatar_->setImage(result.img); - else - avatar_->setLetter(utils::firstChar(name)); + avatar_->setImage(QString::fromStdString(result.info.avatar_url)); } void @@ -154,4 +138,4 @@ RoomItem::mousePressEvent(QMouseEvent *event) emit clicked(selectedText()); QWidget::mousePressEvent(event); -} \ No newline at end of file +} diff --git a/src/timeline/.TimelineItem.cpp.swp b/src/timeline/.TimelineItem.cpp.swp new file mode 100644 index 00000000..75e03aeb Binary files /dev/null and b/src/timeline/.TimelineItem.cpp.swp differ diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index dd3b48c3..7916bd80 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -326,8 +326,7 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty, generateBody(userid, displayName, formatted_body); setupAvatarLayout(displayName); - AvatarProvider::resolve( - room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); + setUserAvatar(userid); } else { generateBody(formatted_body); setupSimpleLayout(); @@ -509,8 +508,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent generateBody(sender, displayName, formatted_body); setupAvatarLayout(displayName); - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); + setUserAvatar(sender); } else { generateBody(formatted_body); setupSimpleLayout(); @@ -607,8 +604,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent generateBody(sender, displayName, formatted_body); setupAvatarLayout(displayName); - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); + setUserAvatar(sender); } else { generateBody(formatted_body); setupSimpleLayout(); @@ -793,9 +789,8 @@ TimelineItem::setupAvatarLayout(const QString &userName) QFont f; f.setPointSizeF(f.pointSizeF()); - userAvatar_ = new Avatar(this); + userAvatar_ = new Avatar(this, QFontMetrics(f).height() * 2); userAvatar_->setLetter(QChar(userName[0]).toUpper()); - userAvatar_->setSize(QFontMetrics(f).height() * 2); // TODO: The provided user name should be a UserId class if (userName[0] == '@' && userName.size() > 1) @@ -822,12 +817,12 @@ TimelineItem::setupSimpleLayout() } void -TimelineItem::setUserAvatar(const QImage &avatar) +TimelineItem::setUserAvatar(const QString &userid) { if (userAvatar_ == nullptr) return; - userAvatar_->setImage(avatar); + userAvatar_->setImage(room_id_, userid); } void @@ -911,8 +906,7 @@ TimelineItem::addAvatar() setupAvatarLayout(displayName); - AvatarProvider::resolve( - room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); + setUserAvatar(userid); } void diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h index fe354000..356976e5 100644 --- a/src/timeline/TimelineItem.h +++ b/src/timeline/TimelineItem.h @@ -215,7 +215,7 @@ public: void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } QColor backgroundColor() const { return backgroundColor_; } - void setUserAvatar(const QImage &pixmap); + void setUserAvatar(const QString &userid); DescInfo descriptionMessage() const { return descriptionMsg_; } QString eventId() const { return event_id_; } void setEventId(const QString &event_id) { event_id_ = event_id; } @@ -336,8 +336,7 @@ TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool generateBody(userid, displayName, ""); setupAvatarLayout(displayName); - AvatarProvider::resolve( - room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); + setUserAvatar(userid); } else { setupSimpleLayout(); } @@ -381,8 +380,7 @@ TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSen generateBody(sender, displayName, ""); setupAvatarLayout(displayName); - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); + setUserAvatar(sender); } else { setupSimpleLayout(); } diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp index 4b4cd272..98bf21c6 100644 --- a/src/ui/Avatar.cpp +++ b/src/ui/Avatar.cpp @@ -1,12 +1,13 @@ #include +#include "AvatarProvider.h" #include "Utils.h" #include "ui/Avatar.h" -Avatar::Avatar(QWidget *parent) +Avatar::Avatar(QWidget *parent, int size) : QWidget(parent) + , size_(size) { - size_ = ui::AvatarSize; type_ = ui::AvatarType::Letter; letter_ = "A"; @@ -61,35 +62,31 @@ Avatar::setBackgroundColor(const QColor &color) } void -Avatar::setSize(int size) +Avatar::setLetter(const QString &letter) { - size_ = size; - - if (!image_.isNull()) - pixmap_ = utils::scaleImageToPixmap(image_, size_); - - QFont _font(font()); - _font.setPointSizeF(size_ * (ui::FontSize) / 40); - - setFont(_font); + letter_ = letter; + type_ = ui::AvatarType::Letter; update(); } void -Avatar::setLetter(const QString &letter) +Avatar::setImage(const QString &avatar_url) { - letter_ = letter; - type_ = ui::AvatarType::Letter; - update(); + AvatarProvider::resolve(avatar_url, size_, this, [this](QPixmap pm) { + type_ = ui::AvatarType::Image; + pixmap_ = pm; + update(); + }); } void -Avatar::setImage(const QImage &image) +Avatar::setImage(const QString &room, const QString &user) { - image_ = image; - type_ = ui::AvatarType::Image; - pixmap_ = utils::scaleImageToPixmap(image_, size_); - update(); + AvatarProvider::resolve(room, user, size_, this, [this](QPixmap pm) { + type_ = ui::AvatarType::Image; + pixmap_ = pm; + update(); + }); } void diff --git a/src/ui/Avatar.h b/src/ui/Avatar.h index 41967af5..b9225dd1 100644 --- a/src/ui/Avatar.h +++ b/src/ui/Avatar.h @@ -15,13 +15,14 @@ class Avatar : public QWidget Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) public: - explicit Avatar(QWidget *parent = 0); + explicit Avatar(QWidget *parent = 0, int size = ui::AvatarSize); void setBackgroundColor(const QColor &color); void setIcon(const QIcon &icon); - void setImage(const QImage &image); + void setImage(const QString &avatar_url); + void setImage(const QString &room, const QString &user); void setLetter(const QString &letter); - void setSize(int size); + // void setSize(int size); void setTextColor(const QColor &color); QColor backgroundColor() const; @@ -41,7 +42,8 @@ private: QColor background_color_; QColor text_color_; QIcon icon_; - QImage image_; QPixmap pixmap_; + const std::string room; + const std::string user; int size_; }; -- cgit 1.5.1 From 91d1f19058a31cc35ca1212f042a9dd6f501a7b7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 9 Nov 2019 03:06:10 +0100 Subject: Remove old timeline --- CMakeLists.txt | 26 +- src/ChatPage.cpp | 2 +- src/Utils.h | 14 +- src/dialogs/MemberList.cpp | 1 + src/timeline/DelegateChooser.cpp | 138 +++ src/timeline/DelegateChooser.h | 82 ++ src/timeline/TimelineItem.cpp | 960 ------------------- src/timeline/TimelineItem.h | 389 -------- src/timeline/TimelineModel.cpp | 1220 ++++++++++++++++++++++++ src/timeline/TimelineModel.h | 258 ++++++ src/timeline/TimelineView.cpp | 1627 --------------------------------- src/timeline/TimelineView.h | 449 --------- src/timeline/TimelineViewManager.cpp | 564 +++++++----- src/timeline/TimelineViewManager.h | 111 ++- src/timeline/widgets/AudioItem.cpp | 236 ----- src/timeline/widgets/AudioItem.h | 104 --- src/timeline/widgets/FileItem.cpp | 221 ----- src/timeline/widgets/FileItem.h | 79 -- src/timeline/widgets/ImageItem.cpp | 267 ------ src/timeline/widgets/ImageItem.h | 104 --- src/timeline/widgets/VideoItem.cpp | 65 -- src/timeline/widgets/VideoItem.h | 51 -- src/timeline2/DelegateChooser.cpp | 138 --- src/timeline2/DelegateChooser.h | 82 -- src/timeline2/TimelineModel.cpp | 1220 ------------------------ src/timeline2/TimelineModel.h | 258 ------ src/timeline2/TimelineViewManager.cpp | 400 -------- src/timeline2/TimelineViewManager.h | 117 --- 28 files changed, 2088 insertions(+), 7095 deletions(-) create mode 100644 src/timeline/DelegateChooser.cpp create mode 100644 src/timeline/DelegateChooser.h delete mode 100644 src/timeline/TimelineItem.cpp delete mode 100644 src/timeline/TimelineItem.h create mode 100644 src/timeline/TimelineModel.cpp create mode 100644 src/timeline/TimelineModel.h delete mode 100644 src/timeline/TimelineView.cpp delete mode 100644 src/timeline/TimelineView.h delete mode 100644 src/timeline/widgets/AudioItem.cpp delete mode 100644 src/timeline/widgets/AudioItem.h delete mode 100644 src/timeline/widgets/FileItem.cpp delete mode 100644 src/timeline/widgets/FileItem.h delete mode 100644 src/timeline/widgets/ImageItem.cpp delete mode 100644 src/timeline/widgets/ImageItem.h delete mode 100644 src/timeline/widgets/VideoItem.cpp delete mode 100644 src/timeline/widgets/VideoItem.h delete mode 100644 src/timeline2/DelegateChooser.cpp delete mode 100644 src/timeline2/DelegateChooser.h delete mode 100644 src/timeline2/TimelineModel.cpp delete mode 100644 src/timeline2/TimelineModel.h delete mode 100644 src/timeline2/TimelineViewManager.cpp delete mode 100644 src/timeline2/TimelineViewManager.h (limited to 'src/dialogs/MemberList.cpp') diff --git a/CMakeLists.txt b/CMakeLists.txt index a7cddc50..e07df88d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -192,16 +192,9 @@ set(SRC_FILES src/emoji/Provider.cpp # Timeline - src/timeline2/TimelineViewManager.cpp - src/timeline2/TimelineModel.cpp - src/timeline2/DelegateChooser.cpp - #src/timeline/TimelineViewManager.cpp - #src/timeline/TimelineItem.cpp - #src/timeline/TimelineView.cpp - #src/timeline/widgets/AudioItem.cpp - #src/timeline/widgets/FileItem.cpp - #src/timeline/widgets/ImageItem.cpp - #src/timeline/widgets/VideoItem.cpp + src/timeline/TimelineViewManager.cpp + src/timeline/TimelineModel.cpp + src/timeline/DelegateChooser.cpp # UI components src/ui/Avatar.cpp @@ -339,16 +332,9 @@ qt5_wrap_cpp(MOC_HEADERS src/emoji/PickButton.h # Timeline - src/timeline2/TimelineViewManager.h - src/timeline2/TimelineModel.h - src/timeline2/DelegateChooser.h - #src/timeline/TimelineItem.h - #src/timeline/TimelineView.h - #src/timeline/TimelineViewManager.h - #src/timeline/widgets/AudioItem.h - #src/timeline/widgets/FileItem.h - #src/timeline/widgets/ImageItem.h - #src/timeline/widgets/VideoItem.h + src/timeline/TimelineViewManager.h + src/timeline/TimelineModel.h + src/timeline/DelegateChooser.h # UI components src/ui/Avatar.h diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index b8f312ac..091a9fa0 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -44,7 +44,7 @@ #include "dialogs/ReadReceipts.h" #include "popups/UserMentions.h" -#include "timeline2/TimelineViewManager.h" +#include "timeline/TimelineViewManager.h" // TODO: Needs to be updated with an actual secret. static const std::string STORAGE_SECRET_KEY("secret"); diff --git a/src/Utils.h b/src/Utils.h index 007126c3..bdb51844 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -4,10 +4,6 @@ #include "Cache.h" #include "RoomInfoListItem.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" #include #include @@ -94,7 +90,7 @@ messageDescription(const QString &username = "", using Video = mtx::events::RoomEvent; using Encrypted = mtx::events::EncryptedEvent; - if (std::is_same::value || std::is_same::value) { + if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent an audio clip"); @@ -102,7 +98,7 @@ messageDescription(const QString &username = "", return QCoreApplication::translate("message-description sent:", "%1 sent an audio clip") .arg(username); - } else if (std::is_same::value || std::is_same::value) { + } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent an image"); @@ -110,7 +106,7 @@ messageDescription(const QString &username = "", return QCoreApplication::translate("message-description sent:", "%1 sent an image") .arg(username); - } else if (std::is_same::value || std::is_same::value) { + } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a file"); @@ -118,7 +114,7 @@ messageDescription(const QString &username = "", return QCoreApplication::translate("message-description sent:", "%1 sent a file") .arg(username); - } else if (std::is_same::value || std::is_same::value) { + } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a video"); @@ -126,7 +122,7 @@ messageDescription(const QString &username = "", return QCoreApplication::translate("message-description sent:", "%1 sent a video") .arg(username); - } else if (std::is_same::value || std::is_same::value) { + } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a sticker"); diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp index 9e973efa..f62cf9fe 100644 --- a/src/dialogs/MemberList.cpp +++ b/src/dialogs/MemberList.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp new file mode 100644 index 00000000..632a2a64 --- /dev/null +++ b/src/timeline/DelegateChooser.cpp @@ -0,0 +1,138 @@ +#include "DelegateChooser.h" + +#include "Logging.h" + +// uses private API, which moved between versions +#include +#include + +QQmlComponent * +DelegateChoice::delegate() const +{ + return delegate_; +} + +void +DelegateChoice::setDelegate(QQmlComponent *delegate) +{ + if (delegate != delegate_) { + delegate_ = delegate; + emit delegateChanged(); + emit changed(); + } +} + +QVariant +DelegateChoice::roleValue() const +{ + return roleValue_; +} + +void +DelegateChoice::setRoleValue(const QVariant &value) +{ + if (value != roleValue_) { + roleValue_ = value; + emit roleValueChanged(); + emit changed(); + } +} + +QVariant +DelegateChooser::roleValue() const +{ + return roleValue_; +} + +void +DelegateChooser::setRoleValue(const QVariant &value) +{ + if (value != roleValue_) { + roleValue_ = value; + recalcChild(); + emit roleValueChanged(); + } +} + +QQmlListProperty +DelegateChooser::choices() +{ + return QQmlListProperty(this, + this, + &DelegateChooser::appendChoice, + &DelegateChooser::choiceCount, + &DelegateChooser::choice, + &DelegateChooser::clearChoices); +} + +void +DelegateChooser::appendChoice(QQmlListProperty *p, DelegateChoice *c) +{ + DelegateChooser *dc = static_cast(p->object); + dc->choices_.append(c); +} + +int +DelegateChooser::choiceCount(QQmlListProperty *p) +{ + return static_cast(p->object)->choices_.count(); +} +DelegateChoice * +DelegateChooser::choice(QQmlListProperty *p, int index) +{ + return static_cast(p->object)->choices_.at(index); +} +void +DelegateChooser::clearChoices(QQmlListProperty *p) +{ + static_cast(p->object)->choices_.clear(); +} + +void +DelegateChooser::recalcChild() +{ + for (const auto choice : choices_) { + auto choiceValue = choice->roleValue(); + if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) { + if (child) { + child->setParentItem(nullptr); + child = nullptr; + } + + choice->delegate()->create(incubator, QQmlEngine::contextForObject(this)); + return; + } + } +} + +void +DelegateChooser::componentComplete() +{ + QQuickItem::componentComplete(); + recalcChild(); +} + +void +DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) +{ + if (status == QQmlIncubator::Ready) { + chooser.child = dynamic_cast(object()); + if (chooser.child == nullptr) { + nhlog::ui()->error("Delegate has to be derived of Item!"); + return; + } + + chooser.child->setParentItem(&chooser); + connect(chooser.child, &QQuickItem::heightChanged, &chooser, [this]() { + chooser.setHeight(chooser.child->height()); + }); + chooser.setHeight(chooser.child->height()); + QQmlEngine::setObjectOwnership(chooser.child, + QQmlEngine::ObjectOwnership::JavaScriptOwnership); + + } else if (status == QQmlIncubator::Error) { + for (const auto &e : errors()) + nhlog::ui()->error("Error instantiating delegate: {}", + e.toString().toStdString()); + } +} diff --git a/src/timeline/DelegateChooser.h b/src/timeline/DelegateChooser.h new file mode 100644 index 00000000..68ebeb04 --- /dev/null +++ b/src/timeline/DelegateChooser.h @@ -0,0 +1,82 @@ +// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt +// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent + +#pragma once + +#include +#include +#include +#include +#include +#include + +class QQmlAdaptorModel; + +class DelegateChoice : public QObject +{ + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "delegate") + +public: + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) + + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); + + QVariant roleValue() const; + void setRoleValue(const QVariant &value); + +signals: + void delegateChanged(); + void roleValueChanged(); + void changed(); + +private: + QVariant roleValue_; + QQmlComponent *delegate_ = nullptr; +}; + +class DelegateChooser : public QQuickItem +{ + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "choices") + +public: + Q_PROPERTY(QQmlListProperty choices READ choices CONSTANT) + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + + QQmlListProperty choices(); + + QVariant roleValue() const; + void setRoleValue(const QVariant &value); + + void recalcChild(); + void componentComplete() override; + +signals: + void roleChanged(); + void roleValueChanged(); + +private: + struct DelegateIncubator : public QQmlIncubator + { + DelegateIncubator(DelegateChooser &parent) + : QQmlIncubator(QQmlIncubator::AsynchronousIfNested) + , chooser(parent) + {} + void statusChanged(QQmlIncubator::Status status) override; + + DelegateChooser &chooser; + }; + + QVariant roleValue_; + QList choices_; + QQuickItem *child = nullptr; + DelegateIncubator incubator{*this}; + + static void appendChoice(QQmlListProperty *, DelegateChoice *); + static int choiceCount(QQmlListProperty *); + static DelegateChoice *choice(QQmlListProperty *, int index); + static void clearChoices(QQmlListProperty *); +}; diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp deleted file mode 100644 index 7916bd80..00000000 --- a/src/timeline/TimelineItem.cpp +++ /dev/null @@ -1,960 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#include - -#include -#include -#include -#include -#include -#include - -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "MainWindow.h" -#include "Olm.h" -#include "ui/Avatar.h" -#include "ui/Painter.h" -#include "ui/TextLabel.h" - -#include "timeline/TimelineItem.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -#include "dialogs/RawMessage.h" -#include "mtx/identifiers.hpp" - -constexpr int MSG_RIGHT_MARGIN = 7; -constexpr int MSG_PADDING = 20; - -StatusIndicator::StatusIndicator(QWidget *parent) - : QWidget(parent) -{ - lockIcon_.addFile(":/icons/icons/ui/lock.png"); - clockIcon_.addFile(":/icons/icons/ui/clock.png"); - checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png"); - doubleCheckmarkIcon_.addFile(":/icons/icons/ui/double-tick-indicator.png"); -} - -void -StatusIndicator::paintIcon(QPainter &p, QIcon &icon) -{ - auto pixmap = icon.pixmap(width()); - - QPainter painter(&pixmap); - painter.setCompositionMode(QPainter::CompositionMode_SourceIn); - painter.fillRect(pixmap.rect(), p.pen().color()); - - QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal); -} - -void -StatusIndicator::paintEvent(QPaintEvent *) -{ - if (state_ == StatusIndicatorState::Empty) - return; - - Painter p(this); - PainterHighQualityEnabler hq(p); - - p.setPen(iconColor_); - - switch (state_) { - case StatusIndicatorState::Sent: { - paintIcon(p, clockIcon_); - break; - } - case StatusIndicatorState::Encrypted: - paintIcon(p, lockIcon_); - break; - case StatusIndicatorState::Received: { - paintIcon(p, checkmarkIcon_); - break; - } - case StatusIndicatorState::Read: { - paintIcon(p, doubleCheckmarkIcon_); - break; - } - case StatusIndicatorState::Empty: - break; - } -} - -void -StatusIndicator::setState(StatusIndicatorState state) -{ - state_ = state; - - switch (state) { - case StatusIndicatorState::Encrypted: - setToolTip(tr("Encrypted")); - break; - case StatusIndicatorState::Received: - setToolTip(tr("Delivered")); - break; - case StatusIndicatorState::Read: - setToolTip(tr("Seen")); - break; - case StatusIndicatorState::Sent: - setToolTip(tr("Sent")); - break; - case StatusIndicatorState::Empty: - setToolTip(""); - break; - } - - update(); -} - -void -TimelineItem::adjustMessageLayoutForWidget() -{ - messageLayout_->addLayout(widgetLayout_, 1); - actionLayout_->addWidget(replyBtn_); - actionLayout_->addWidget(contextBtn_); - messageLayout_->addLayout(actionLayout_); - messageLayout_->addWidget(statusIndicator_); - messageLayout_->addWidget(timestamp_); - - actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight); - actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight); - messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); - messageLayout_->setAlignment(timestamp_, Qt::AlignTop); - messageLayout_->setAlignment(actionLayout_, Qt::AlignTop); - - mainLayout_->addLayout(messageLayout_); -} - -void -TimelineItem::adjustMessageLayout() -{ - messageLayout_->addWidget(body_, 1); - actionLayout_->addWidget(replyBtn_); - actionLayout_->addWidget(contextBtn_); - messageLayout_->addLayout(actionLayout_); - messageLayout_->addWidget(statusIndicator_); - messageLayout_->addWidget(timestamp_); - - actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight); - actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight); - messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); - messageLayout_->setAlignment(timestamp_, Qt::AlignTop); - messageLayout_->setAlignment(actionLayout_, Qt::AlignTop); - - mainLayout_->addLayout(messageLayout_); -} - -void -TimelineItem::init() -{ - userAvatar_ = nullptr; - timestamp_ = nullptr; - userName_ = nullptr; - body_ = nullptr; - auto buttonSize_ = 32; - - contextMenu_ = new QMenu(this); - showReadReceipts_ = new QAction("Read receipts", this); - markAsRead_ = new QAction("Mark as read", this); - viewRawMessage_ = new QAction("View raw message", this); - redactMsg_ = new QAction("Redact message", this); - contextMenu_->addAction(showReadReceipts_); - contextMenu_->addAction(viewRawMessage_); - contextMenu_->addAction(markAsRead_); - contextMenu_->addAction(redactMsg_); - - connect(showReadReceipts_, &QAction::triggered, this, [this]() { - if (!event_id_.isEmpty()) - MainWindow::instance()->openReadReceiptsDialog(event_id_); - }); - - connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) { - emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id); - }); - connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) { - emit ChatPage::instance()->showNotification(msg); - }); - connect(redactMsg_, &QAction::triggered, this, [this]() { - if (!event_id_.isEmpty()) - http::client()->redact_event( - room_id_.toStdString(), - event_id_.toStdString(), - [this](const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - emit redactionFailed(tr("Message redaction failed: %1") - .arg(QString::fromStdString( - err->matrix_error.error))); - return; - } - - emit eventRedacted(event_id_); - }); - }); - connect( - ChatPage::instance(), &ChatPage::themeChanged, this, &TimelineItem::refreshAuthorColor); - connect(markAsRead_, &QAction::triggered, this, &TimelineItem::sendReadReceipt); - connect(viewRawMessage_, &QAction::triggered, this, &TimelineItem::openRawMessageViewer); - - colorGenerating_ = new QFutureWatcher(this); - connect(colorGenerating_, - &QFutureWatcher::finished, - this, - &TimelineItem::finishedGeneratingColor); - - topLayout_ = new QHBoxLayout(this); - mainLayout_ = new QVBoxLayout; - messageLayout_ = new QHBoxLayout; - actionLayout_ = new QHBoxLayout; - messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0); - messageLayout_->setSpacing(MSG_PADDING); - - actionLayout_->setContentsMargins(13, 1, 13, 0); - actionLayout_->setSpacing(0); - - topLayout_->setContentsMargins( - conf::timeline::msgLeftMargin, conf::timeline::msgTopMargin, 0, 0); - topLayout_->setSpacing(0); - topLayout_->addLayout(mainLayout_); - - mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0); - mainLayout_->setSpacing(0); - - replyBtn_ = new FlatButton(this); - replyBtn_->setToolTip(tr("Reply")); - replyBtn_->setFixedSize(buttonSize_, buttonSize_); - replyBtn_->setCornerRadius(buttonSize_ / 2); - - QIcon reply_icon; - reply_icon.addFile(":/icons/icons/ui/mail-reply.png"); - replyBtn_->setIcon(reply_icon); - replyBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); - connect(replyBtn_, &FlatButton::clicked, this, &TimelineItem::replyAction); - - contextBtn_ = new FlatButton(this); - contextBtn_->setToolTip(tr("Options")); - contextBtn_->setFixedSize(buttonSize_, buttonSize_); - contextBtn_->setCornerRadius(buttonSize_ / 2); - - QIcon context_icon; - context_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png"); - contextBtn_->setIcon(context_icon); - contextBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); - contextBtn_->setMenu(contextMenu_); - - timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9); - timestampFont_.setFamily("Monospace"); - timestampFont_.setStyleHint(QFont::Monospace); - - QFontMetrics tsFm(timestampFont_); - - statusIndicator_ = new StatusIndicator(this); - statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading()); - statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading()); - - parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); -} - -/* - * For messages created locally. - */ -TimelineItem::TimelineItem(mtx::events::MessageType ty, - const QString &userid, - QString body, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(ty) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - auto displayName = Cache::displayName(room_id_, userid); - auto timestamp = QDateTime::currentDateTime(); - - // Generate the html body to be rendered. - auto formatted_body = utils::markdownToHtml(body); - - // Escape html if the input is not formatted. - if (formatted_body == body.trimmed().toHtmlEscaped()) - formatted_body = body.toHtmlEscaped(); - - QString emptyEventId; - - if (ty == mtx::events::MessageType::Emote) { - formatted_body = QString("%1").arg(formatted_body); - descriptionMsg_ = {emptyEventId, - "", - userid, - QString("* %1 %2").arg(displayName).arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - } else { - descriptionMsg_ = {emptyEventId, - "You: ", - userid, - body, - utils::descriptiveTime(timestamp), - timestamp}; - } - - formatted_body = utils::linkifyMessage(formatted_body); - formatted_body.replace("mx-reply", "div"); - - generateTimestamp(timestamp); - - if (withSender) { - generateBody(userid, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(userid); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -TimelineItem::TimelineItem(ImageItem *image, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::Image) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout(image, userid, withSender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(FileItem *file, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::File) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout(file, userid, withSender); -} - -TimelineItem::TimelineItem(AudioItem *audio, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::Audio) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout(audio, userid, withSender); -} - -TimelineItem::TimelineItem(VideoItem *video, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::Video) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout(video, userid, withSender); -} - -TimelineItem::TimelineItem(ImageItem *image, - const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Image) - , room_id_{room_id} -{ - setupWidgetLayout, ImageItem>( - image, event, with_sender); - - markOwnMessagesAsReceived(event.sender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(StickerItem *image, - const mtx::events::Sticker &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout(image, event, with_sender); - - markOwnMessagesAsReceived(event.sender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(FileItem *file, - const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::File) - , room_id_{room_id} -{ - setupWidgetLayout, FileItem>( - file, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -TimelineItem::TimelineItem(AudioItem *audio, - const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Audio) - , room_id_{room_id} -{ - setupWidgetLayout, AudioItem>( - audio, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -TimelineItem::TimelineItem(VideoItem *video, - const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Video) - , room_id_{room_id} -{ - setupWidgetLayout, VideoItem>( - video, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -/* - * Used to display remote notice messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Notice) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - descriptionMsg_ = {event_id_, - Cache::displayName(room_id_, sender), - sender, - " sent a notification", - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - auto displayName = Cache::displayName(room_id_, sender); - - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -/* - * Used to display remote emote messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Emote) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - formatted_body = QString("%1").arg(formatted_body); - - descriptionMsg_ = {event_id_, - "", - sender, - QString("* %1 %2").arg(displayName).arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -/* - * Used to display remote text messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Text) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - - QSettings settings; - descriptionMsg_ = {event_id_, - sender == settings.value("auth/user_id") ? "You" : displayName, - sender, - QString(": %1").arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -TimelineItem::~TimelineItem() -{ - colorGenerating_->cancel(); - colorGenerating_->waitForFinished(); -} - -void -TimelineItem::markSent() -{ - statusIndicator_->setState(StatusIndicatorState::Sent); -} - -void -TimelineItem::markOwnMessagesAsReceived(const std::string &sender) -{ - QSettings settings; - if (sender == settings.value("auth/user_id").toString().toStdString()) - statusIndicator_->setState(StatusIndicatorState::Received); -} - -void -TimelineItem::markRead() -{ - if (statusIndicator_->state() != StatusIndicatorState::Encrypted) - statusIndicator_->setState(StatusIndicatorState::Read); -} - -void -TimelineItem::markReceived(bool isEncrypted) -{ - isReceived_ = true; - - if (isEncrypted) - statusIndicator_->setState(StatusIndicatorState::Encrypted); - else - statusIndicator_->setState(StatusIndicatorState::Received); - - sendReadReceipt(); -} - -// Only the body is displayed. -void -TimelineItem::generateBody(const QString &body) -{ - body_ = new TextLabel(utils::replaceEmoji(body), this); - body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - - connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) { - MainWindow::instance()->openUserProfile(user_id, - ChatPage::instance()->currentRoom()); - }); -} - -void -TimelineItem::refreshAuthorColor() -{ - // Cancel and wait if we are already generating the color. - if (colorGenerating_->isRunning()) { - colorGenerating_->cancel(); - colorGenerating_->waitForFinished(); - } - if (userName_) { - // generate user's unique color. - std::function generate = [this]() { - QString userColor = utils::generateContrastingHexColor( - userName_->toolTip(), backgroundColor().name()); - return userColor; - }; - - QString userColor = Cache::userColor(userName_->toolTip()); - - // If the color is empty, then generate it asynchronously - if (userColor.isEmpty()) { - colorGenerating_->setFuture(QtConcurrent::run(generate)); - } else { - userName_->setStyleSheet("QLabel { color : " + userColor + "; }"); - } - } -} - -void -TimelineItem::finishedGeneratingColor() -{ - nhlog::ui()->debug("finishedGeneratingColor for: {}", userName_->toolTip().toStdString()); - QString userColor = colorGenerating_->result(); - - if (!userColor.isEmpty()) { - // another TimelineItem might have inserted in the meantime. - if (Cache::userColor(userName_->toolTip()).isEmpty()) { - Cache::insertUserColor(userName_->toolTip(), userColor); - } - userName_->setStyleSheet("QLabel { color : " + userColor + "; }"); - } -} -// The username/timestamp is displayed along with the message body. -void -TimelineItem::generateBody(const QString &user_id, const QString &displayname, const QString &body) -{ - generateUserName(user_id, displayname); - generateBody(body); -} - -void -TimelineItem::generateUserName(const QString &user_id, const QString &displayname) -{ - auto sender = displayname; - - if (displayname.startsWith("@")) { - // TODO: Fix this by using a UserId type. - if (displayname.split(":")[0].split("@").size() > 1) - sender = displayname.split(":")[0].split("@")[1]; - } - - QFont usernameFont; - usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1); - usernameFont.setWeight(QFont::Medium); - - QFontMetrics fm(usernameFont); - - userName_ = new QLabel(this); - userName_->setFont(usernameFont); - userName_->setText(utils::replaceEmoji(fm.elidedText(sender, Qt::ElideRight, 500))); - userName_->setToolTip(user_id); - userName_->setToolTipDuration(1500); - userName_->setAttribute(Qt::WA_Hover); - userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - // width deprecated in 5.13: - userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text())); -#else - userName_->setFixedWidth( - QFontMetrics(userName_->font()).horizontalAdvance(userName_->text())); -#endif - // Set the user color asynchronously if it hasn't been generated yet, - // otherwise this will just set it. - refreshAuthorColor(); - - auto filter = new UserProfileFilter(user_id, userName_); - userName_->installEventFilter(filter); - userName_->setCursor(Qt::PointingHandCursor); - - connect(filter, &UserProfileFilter::hoverOn, this, [this]() { - QFont f = userName_->font(); - f.setUnderline(true); - userName_->setFont(f); - }); - - connect(filter, &UserProfileFilter::hoverOff, this, [this]() { - QFont f = userName_->font(); - f.setUnderline(false); - userName_->setFont(f); - }); - - connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() { - MainWindow::instance()->openUserProfile(user_id, room_id_); - }); -} - -void -TimelineItem::generateTimestamp(const QDateTime &time) -{ - timestamp_ = new QLabel(this); - timestamp_->setFont(timestampFont_); - timestamp_->setText( - QString(" %1 ").arg(time.toString("HH:mm"))); -} - -void -TimelineItem::setupAvatarLayout(const QString &userName) -{ - topLayout_->setContentsMargins( - conf::timeline::msgLeftMargin, conf::timeline::msgAvatarTopMargin, 0, 0); - - QFont f; - f.setPointSizeF(f.pointSizeF()); - - userAvatar_ = new Avatar(this, QFontMetrics(f).height() * 2); - userAvatar_->setLetter(QChar(userName[0]).toUpper()); - - // TODO: The provided user name should be a UserId class - if (userName[0] == '@' && userName.size() > 1) - userAvatar_->setLetter(QChar(userName[1]).toUpper()); - - topLayout_->insertWidget(0, userAvatar_); - topLayout_->setAlignment(userAvatar_, Qt::AlignTop | Qt::AlignLeft); - - if (userName_) - mainLayout_->insertWidget(0, userName_, Qt::AlignTop | Qt::AlignLeft); -} - -void -TimelineItem::setupSimpleLayout() -{ - QFont f; - f.setPointSizeF(f.pointSizeF()); - - topLayout_->setContentsMargins(conf::timeline::msgLeftMargin + - QFontMetrics(f).height() * 2 + 2, - conf::timeline::msgTopMargin, - 0, - 0); -} - -void -TimelineItem::setUserAvatar(const QString &userid) -{ - if (userAvatar_ == nullptr) - return; - - userAvatar_->setImage(room_id_, userid); -} - -void -TimelineItem::contextMenuEvent(QContextMenuEvent *event) -{ - if (contextMenu_) - contextMenu_->exec(event->globalPos()); -} - -void -TimelineItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -TimelineItem::addSaveImageAction(ImageItem *image) -{ - if (contextMenu_) { - auto saveImage = new QAction("Save image", this); - contextMenu_->addAction(saveImage); - - connect(saveImage, &QAction::triggered, image, &ImageItem::saveAs); - } -} - -void -TimelineItem::addReplyAction() -{ - if (contextMenu_) { - auto replyAction = new QAction("Reply", this); - contextMenu_->addAction(replyAction); - - connect(replyAction, &QAction::triggered, this, &TimelineItem::replyAction); - } -} - -void -TimelineItem::replyAction() -{ - if (!body_) - return; - - RelatedInfo related; - related.type = message_type_; - related.quoted_body = body_->toPlainText(); - related.quoted_user = descriptionMsg_.userid; - related.related_event = eventId().toStdString(); - related.room = room_id_; - - emit ChatPage::instance()->messageReply(related); -} - -void -TimelineItem::addKeyRequestAction() -{ - if (contextMenu_) { - auto requestKeys = new QAction("Request encryption keys", this); - contextMenu_->addAction(requestKeys); - - connect(requestKeys, &QAction::triggered, this, [this]() { - olm::request_keys(room_id_.toStdString(), event_id_.toStdString()); - }); - } -} - -void -TimelineItem::addAvatar() -{ - if (userAvatar_) - return; - - // TODO: should be replaced with the proper event struct. - auto userid = descriptionMsg_.userid; - auto displayName = Cache::displayName(room_id_, userid); - - generateUserName(userid, displayName); - - setupAvatarLayout(displayName); - - setUserAvatar(userid); -} - -void -TimelineItem::sendReadReceipt() const -{ - if (!event_id_.isEmpty()) - http::client()->read_event(room_id_.toStdString(), - event_id_.toStdString(), - [this](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read_event ({}, {})", - room_id_.toStdString(), - event_id_.toStdString()); - } - }); -} - -void -TimelineItem::openRawMessageViewer() const -{ - const auto event_id = event_id_.toStdString(); - const auto room_id = room_id_.toStdString(); - - auto proxy = std::make_shared(); - connect(proxy.get(), &EventProxy::eventRetrieved, this, [](const nlohmann::json &obj) { - auto dialog = new dialogs::RawMessage{QString::fromStdString(obj.dump(4))}; - Q_UNUSED(dialog); - }); - - http::client()->get_event( - room_id, - event_id, - [event_id, room_id, proxy = std::move(proxy)]( - const mtx::events::collections::TimelineEvents &res, mtx::http::RequestErr err) { - using namespace mtx::events; - - if (err) { - nhlog::net()->warn( - "failed to retrieve event {} from {}", event_id, room_id); - return; - } - - try { - emit proxy->eventRetrieved(utils::serialize_event(res)); - } catch (const nlohmann::json::exception &e) { - nhlog::net()->warn( - "failed to serialize event ({}, {})", room_id, event_id); - } - }); -} diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h deleted file mode 100644 index 356976e5..00000000 --- a/src/timeline/TimelineItem.h +++ /dev/null @@ -1,389 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "mtx/events.hpp" - -#include "AvatarProvider.h" -#include "RoomInfoListItem.h" -#include "Utils.h" - -#include "Cache.h" -#include "MatrixClient.h" - -#include "ui/FlatButton.h" - -class ImageItem; -class StickerItem; -class AudioItem; -class VideoItem; -class FileItem; -class Avatar; -class TextLabel; - -enum class StatusIndicatorState -{ - //! The encrypted message was received by the server. - Encrypted, - //! The plaintext message was received by the server. - Received, - //! At least one of the participants has read the message. - Read, - //! The client sent the message. Not yet received. - Sent, - //! When the message is loaded from cache or backfill. - Empty, -}; - -//! -//! Used to notify the user about the status of a message. -//! -class StatusIndicator : public QWidget -{ - Q_OBJECT - -public: - explicit StatusIndicator(QWidget *parent); - void setState(StatusIndicatorState state); - StatusIndicatorState state() const { return state_; } - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - void paintIcon(QPainter &p, QIcon &icon); - - QIcon lockIcon_; - QIcon clockIcon_; - QIcon checkmarkIcon_; - QIcon doubleCheckmarkIcon_; - - QColor iconColor_ = QColor("#999"); - - StatusIndicatorState state_ = StatusIndicatorState::Empty; - - static constexpr int MaxWidth = 24; -}; - -class EventProxy : public QObject -{ - Q_OBJECT - -signals: - void eventRetrieved(const nlohmann::json &); -}; - -class UserProfileFilter : public QObject -{ - Q_OBJECT - -public: - explicit UserProfileFilter(const QString &user_id, QLabel *parent) - : QObject(parent) - , user_id_{user_id} - {} - -signals: - void hoverOff(); - void hoverOn(); - void clicked(); - -protected: - bool eventFilter(QObject *obj, QEvent *event) - { - if (event->type() == QEvent::MouseButtonRelease) { - emit clicked(); - return true; - } else if (event->type() == QEvent::HoverLeave) { - emit hoverOff(); - return true; - } else if (event->type() == QEvent::HoverEnter) { - emit hoverOn(); - return true; - } - - return QObject::eventFilter(obj, event); - } - -private: - QString user_id_; -}; - -class TimelineItem : public QWidget -{ - Q_OBJECT - Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) - -public: - TimelineItem(const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - - // For local messages. - // m.text & m.emote - TimelineItem(mtx::events::MessageType ty, - const QString &userid, - QString body, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - // m.image - TimelineItem(ImageItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(FileItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(AudioItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(VideoItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - - TimelineItem(ImageItem *img, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(StickerItem *img, - const mtx::events::Sticker &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(FileItem *file, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(AudioItem *audio, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(VideoItem *video, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - - ~TimelineItem(); - - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - QColor backgroundColor() const { return backgroundColor_; } - - void setUserAvatar(const QString &userid); - DescInfo descriptionMessage() const { return descriptionMsg_; } - QString eventId() const { return event_id_; } - void setEventId(const QString &event_id) { event_id_ = event_id; } - void markReceived(bool isEncrypted); - void markRead(); - void markSent(); - bool isReceived() { return isReceived_; }; - void setRoomId(QString room_id) { room_id_ = room_id; } - void sendReadReceipt() const; - void openRawMessageViewer() const; - void replyAction(); - - //! Add a user avatar for this event. - void addAvatar(); - void addKeyRequestAction(); - -signals: - void eventRedacted(const QString &event_id); - void redactionFailed(const QString &msg); - -public slots: - void refreshAuthorColor(); - void finishedGeneratingColor(); - -protected: - void paintEvent(QPaintEvent *event) override; - void contextMenuEvent(QContextMenuEvent *event) override; - -private: - //! If we are the sender of the message the event wil be marked as received by the server. - void markOwnMessagesAsReceived(const std::string &sender); - void init(); - //! Add a context menu option to save the image of the timeline item. - void addSaveImageAction(ImageItem *image); - //! Add the reply action in the context menu for widgets that support it. - void addReplyAction(); - - template - void setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender); - - template - void setupWidgetLayout(Widget *widget, const Event &event, bool withSender); - - void generateBody(const QString &body); - void generateBody(const QString &user_id, const QString &displayname, const QString &body); - void generateTimestamp(const QDateTime &time); - void generateUserName(const QString &userid, const QString &displayname); - - void setupAvatarLayout(const QString &userName); - void setupSimpleLayout(); - - void adjustMessageLayout(); - void adjustMessageLayoutForWidget(); - - //! Whether or not the event associated with the widget - //! has been acknowledged by the server. - bool isReceived_ = false; - - QFutureWatcher *colorGenerating_; - - QString event_id_; - mtx::events::MessageType message_type_ = mtx::events::MessageType::Unknown; - QString room_id_; - - DescInfo descriptionMsg_; - - QMenu *contextMenu_; - QAction *showReadReceipts_; - QAction *markAsRead_; - QAction *redactMsg_; - QAction *viewRawMessage_; - QAction *replyMsg_; - - QHBoxLayout *topLayout_ = nullptr; - QHBoxLayout *messageLayout_ = nullptr; - QHBoxLayout *actionLayout_ = nullptr; - QVBoxLayout *mainLayout_ = nullptr; - QHBoxLayout *widgetLayout_ = nullptr; - - Avatar *userAvatar_; - - QFont timestampFont_; - - StatusIndicator *statusIndicator_; - - QLabel *timestamp_; - QLabel *userName_; - TextLabel *body_; - - QColor backgroundColor_; - - FlatButton *replyBtn_; - FlatButton *contextBtn_; -}; - -template -void -TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender) -{ - auto displayName = Cache::displayName(room_id_, userid); - auto timestamp = QDateTime::currentDateTime(); - - descriptionMsg_ = {"", // No event_id up until this point. - "You", - userid, - QString(" %1").arg(utils::messageDescription()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout; - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(userid, displayName, ""); - setupAvatarLayout(displayName); - - setUserAvatar(userid); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} - -template -void -TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSender) -{ - init(); - - // if (event.type == mtx::events::EventType::RoomMessage) { - // message_type_ = mtx::events::getMessageType(event.content.msgtype); - //} - // TODO: Fix this. - message_type_ = mtx::events::MessageType::Unknown; - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - - QSettings settings; - descriptionMsg_ = {event_id_, - sender == settings.value("auth/user_id") ? "You" : displayName, - sender, - QString(" %1").arg(utils::messageDescription()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout(); - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(sender, displayName, ""); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp new file mode 100644 index 00000000..ab7d3d47 --- /dev/null +++ b/src/timeline/TimelineModel.cpp @@ -0,0 +1,1220 @@ +#include "TimelineModel.h" + +#include +#include + +#include + +#include "ChatPage.h" +#include "Logging.h" +#include "MainWindow.h" +#include "Olm.h" +#include "TimelineViewManager.h" +#include "Utils.h" +#include "dialogs/RawMessage.h" + +Q_DECLARE_METATYPE(QModelIndex) + +namespace { +template +QString +eventId(const mtx::events::RoomEvent &event) +{ + return QString::fromStdString(event.event_id); +} +template +QString +roomId(const mtx::events::Event &event) +{ + return QString::fromStdString(event.room_id); +} +template +QString +senderId(const mtx::events::RoomEvent &event) +{ + return QString::fromStdString(event.sender); +} + +template +QDateTime +eventTimestamp(const mtx::events::RoomEvent &event) +{ + return QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); +} + +template +std::string +eventMsgType(const mtx::events::Event &) +{ + return ""; +} +template +auto +eventMsgType(const mtx::events::RoomEvent &e) -> decltype(e.content.msgtype) +{ + return e.content.msgtype; +} + +template +QString +eventBody(const mtx::events::Event &) +{ + return QString(""); +} +template +auto +eventBody(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + return QString::fromStdString(e.content.body); +} + +template +QString +eventFormattedBody(const mtx::events::Event &) +{ + return QString(""); +} +template +auto +eventFormattedBody(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + auto temp = e.content.formatted_body; + if (!temp.empty()) { + return QString::fromStdString(temp); + } else { + return QString::fromStdString(e.content.body).toHtmlEscaped().replace("\n", "
"); + } +} + +template +QString +eventUrl(const mtx::events::Event &) +{ + return ""; +} +template +auto +eventUrl(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + return QString::fromStdString(e.content.url); +} + +template +QString +eventThumbnailUrl(const mtx::events::Event &) +{ + return ""; +} +template +auto +eventThumbnailUrl(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, + QString> +{ + return QString::fromStdString(e.content.info.thumbnail_url); +} + +template +QString +eventFilename(const mtx::events::Event &) +{ + return ""; +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + if (!e.content.filename.empty()) + return QString::fromStdString(e.content.filename); + return QString::fromStdString(e.content.body); +} + +template +auto +eventFilesize(const mtx::events::RoomEvent &e) -> decltype(e.content.info.size) +{ + return e.content.info.size; +} + +template +int64_t +eventFilesize(const mtx::events::Event &) +{ + return 0; +} + +template +QString +eventMimeType(const mtx::events::Event &) +{ + return QString(); +} +template +auto +eventMimeType(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + return QString::fromStdString(e.content.info.mimetype); +} + +template +QString +eventRelatesTo(const mtx::events::Event &) +{ + return QString(); +} +template +auto +eventRelatesTo(const mtx::events::RoomEvent &e) -> std::enable_if_t< + std::is_same::value, + QString> +{ + return QString::fromStdString(e.content.relates_to.in_reply_to.event_id); +} + +template +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &e) +{ + using mtx::events::EventType; + switch (e.type) { + case EventType::RoomKeyRequest: + return qml_mtx_events::EventType::KeyRequest; + case EventType::RoomAliases: + return qml_mtx_events::EventType::Aliases; + case EventType::RoomAvatar: + return qml_mtx_events::EventType::Avatar; + case EventType::RoomCanonicalAlias: + return qml_mtx_events::EventType::CanonicalAlias; + case EventType::RoomCreate: + return qml_mtx_events::EventType::Create; + case EventType::RoomEncrypted: + return qml_mtx_events::EventType::Encrypted; + case EventType::RoomEncryption: + return qml_mtx_events::EventType::Encryption; + case EventType::RoomGuestAccess: + return qml_mtx_events::EventType::GuestAccess; + case EventType::RoomHistoryVisibility: + return qml_mtx_events::EventType::HistoryVisibility; + case EventType::RoomJoinRules: + return qml_mtx_events::EventType::JoinRules; + case EventType::RoomMember: + return qml_mtx_events::EventType::Member; + case EventType::RoomMessage: + return qml_mtx_events::EventType::UnknownMessage; + case EventType::RoomName: + return qml_mtx_events::EventType::Name; + case EventType::RoomPowerLevels: + return qml_mtx_events::EventType::PowerLevels; + case EventType::RoomTopic: + return qml_mtx_events::EventType::Topic; + case EventType::RoomTombstone: + return qml_mtx_events::EventType::Tombstone; + case EventType::RoomRedaction: + return qml_mtx_events::EventType::Redaction; + case EventType::RoomPinnedEvents: + return qml_mtx_events::EventType::PinnedEvents; + case EventType::Sticker: + return qml_mtx_events::EventType::Sticker; + case EventType::Tag: + return qml_mtx_events::EventType::Tag; + case EventType::Unsupported: + default: + return qml_mtx_events::EventType::Unsupported; + } +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::AudioMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::EmoteMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::FileMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::ImageMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::NoticeMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::TextMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::VideoMessage; +} + +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::Redacted; +} +// ::EventType::Type toRoomEventType(const Event &e) { return +// ::EventType::LocationMessage; } + +template +uint64_t +eventHeight(const mtx::events::Event &) +{ + return -1; +} +template +auto +eventHeight(const mtx::events::RoomEvent &e) -> decltype(e.content.info.h) +{ + return e.content.info.h; +} +template +uint64_t +eventWidth(const mtx::events::Event &) +{ + return -1; +} +template +auto +eventWidth(const mtx::events::RoomEvent &e) -> decltype(e.content.info.w) +{ + return e.content.info.w; +} + +template +double +eventPropHeight(const mtx::events::RoomEvent &e) +{ + auto w = eventWidth(e); + if (w == 0) + w = 1; + return eventHeight(e) / (double)w; +} +} + +TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent) + : QAbstractListModel(parent) + , room_id_(room_id) + , manager_(manager) +{ + connect( + this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); + connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) { + pending.remove(txn_id); + failed.insert(txn_id); + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Failed index out of range"); + return; + } + emit dataChanged(index(idx, 0), index(idx, 0)); + }); + connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) { + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Sent index out of range"); + return; + } + eventOrder[idx] = event_id; + auto ev = events.value(txn_id); + ev = boost::apply_visitor( + [event_id](const auto &e) -> mtx::events::collections::TimelineEvents { + auto eventCopy = e; + eventCopy.event_id = event_id.toStdString(); + return eventCopy; + }, + ev); + events.remove(txn_id); + events.insert(event_id, ev); + + // mark our messages as read + readEvent(event_id.toStdString()); + + // ask to be notified for read receipts + cache::client()->addPendingReceipt(room_id_, event_id); + + emit dataChanged(index(idx, 0), index(idx, 0)); + }); + connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { + emit ChatPage::instance()->showNotification(msg); + }); +} + +QHash +TimelineModel::roleNames() const +{ + return { + {Section, "section"}, + {Type, "type"}, + {Body, "body"}, + {FormattedBody, "formattedBody"}, + {UserId, "userId"}, + {UserName, "userName"}, + {Timestamp, "timestamp"}, + {Url, "url"}, + {ThumbnailUrl, "thumbnailUrl"}, + {Filename, "filename"}, + {Filesize, "filesize"}, + {MimeType, "mimetype"}, + {Height, "height"}, + {Width, "width"}, + {ProportionalHeight, "proportionalHeight"}, + {Id, "id"}, + {State, "state"}, + {IsEncrypted, "isEncrypted"}, + {ReplyTo, "replyTo"}, + }; +} +int +TimelineModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return (int)this->eventOrder.size(); +} + +QVariant +TimelineModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 && index.row() >= (int)eventOrder.size()) + return QVariant(); + + QString id = eventOrder[index.row()]; + + mtx::events::collections::TimelineEvents event = events.value(id); + + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + switch (role) { + case Section: { + QDateTime date = boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event); + date.setTime(QTime()); + + QString userId = + boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, event); + + for (int r = index.row() - 1; r > 0; r--) { + QDateTime prevDate = boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, + events.value(eventOrder[r])); + prevDate.setTime(QTime()); + if (prevDate != date) + return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId); + + QString prevUserId = + boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, + events.value(eventOrder[r])); + if (userId != prevUserId) + break; + } + + return QString("%1").arg(userId); + } + case UserId: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, event)); + case UserName: + return QVariant(displayName(boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, event))); + + case Timestamp: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event)); + case Type: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, + event)); + case Body: + return QVariant(utils::replaceEmoji(boost::apply_visitor( + [](const auto &e) -> QString { return eventBody(e); }, event))); + case FormattedBody: + return QVariant( + utils::replaceEmoji( + boost::apply_visitor( + [](const auto &e) -> QString { return eventFormattedBody(e); }, event)) + .remove("") + .remove("")); + case Url: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventUrl(e); }, event)); + case ThumbnailUrl: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventThumbnailUrl(e); }, event)); + case Filename: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventFilename(e); }, event)); + case Filesize: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { + return utils::humanReadableFileSize(eventFilesize(e)); + }, + event)); + case MimeType: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventMimeType(e); }, event)); + case Height: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qulonglong { return eventHeight(e); }, event)); + case Width: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qulonglong { return eventWidth(e); }, event)); + case ProportionalHeight: + return QVariant(boost::apply_visitor( + [](const auto &e) -> double { return eventPropHeight(e); }, event)); + case Id: + return id; + case State: + // only show read receipts for messages not from us + if (boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, + event) + .toStdString() != http::client()->user_id().to_string()) + return qml_mtx_events::Empty; + else if (failed.contains(id)) + return qml_mtx_events::Failed; + else if (pending.contains(id)) + return qml_mtx_events::Sent; + else if (read.contains(id) || + cache::client()->readReceipts(id, room_id_).size() > 1) + return qml_mtx_events::Read; + else + return qml_mtx_events::Received; + case IsEncrypted: { + auto tempEvent = events[id]; + return boost::get>( + &tempEvent) != nullptr; + } + case ReplyTo: { + QString evId = boost::apply_visitor( + [](const auto &e) -> QString { return eventRelatesTo(e); }, event); + return QVariant(evId); + } + default: + return QVariant(); + } +} + +void +TimelineModel::addEvents(const mtx::responses::Timeline &timeline) +{ + if (isInitialSync) { + prev_batch_token_ = QString::fromStdString(timeline.prev_batch); + isInitialSync = false; + } + + if (timeline.events.empty()) + return; + + std::vector ids = internalAddEvents(timeline.events); + + if (ids.empty()) + return; + + beginInsertRows(QModelIndex(), + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size() + ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); + endInsertRows(); + + updateLastMessage(); +} + +void +TimelineModel::updateLastMessage() +{ + auto event = events.value(eventOrder.back()); + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + auto description = utils::getMessageDescription( + event, QString::fromStdString(http::client()->user_id().to_string()), room_id_); + emit manager_->updateRoomsLastMessage(room_id_, description); +} + +std::vector +TimelineModel::internalAddEvents( + const std::vector &timeline) +{ + std::vector ids; + for (const auto &e : timeline) { + QString id = + boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); + + if (this->events.contains(id)) { + this->events.insert(id, e); + int idx = idToIndex(id); + emit dataChanged(index(idx, 0), index(idx, 0)); + continue; + } + + if (auto redaction = + boost::get>(&e)) { + QString redacts = QString::fromStdString(redaction->redacts); + auto redacted = std::find(eventOrder.begin(), eventOrder.end(), redacts); + + if (redacted != eventOrder.end()) { + auto redactedEvent = boost::apply_visitor( + [](const auto &ev) + -> mtx::events::RoomEvent { + mtx::events::RoomEvent + replacement = {}; + replacement.event_id = ev.event_id; + replacement.room_id = ev.room_id; + replacement.sender = ev.sender; + replacement.origin_server_ts = ev.origin_server_ts; + replacement.type = ev.type; + return replacement; + }, + e); + events.insert(redacts, redactedEvent); + + int row = (int)std::distance(eventOrder.begin(), redacted); + emit dataChanged(index(row, 0), index(row, 0)); + } + + continue; // don't insert redaction into timeline + } + + this->events.insert(id, e); + ids.push_back(id); + } + return ids; +} + +void +TimelineModel::fetchHistory() +{ + if (paginationInProgress) { + nhlog::ui()->warn("Already loading older messages"); + return; + } + + paginationInProgress = true; + mtx::http::MessagesOpts opts; + opts.room_id = room_id_.toStdString(); + opts.from = prev_batch_token_.toStdString(); + + nhlog::ui()->info("Paginationg room {}", opts.room_id); + + http::client()->messages( + opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("failed to call /messages ({}): {} - {}", + opts.room_id, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + paginationInProgress = false; + return; + } + + emit oldMessagesRetrieved(std::move(res)); + paginationInProgress = false; + }); +} + +void +TimelineModel::setCurrentIndex(int index) +{ + auto oldIndex = idToIndex(currentId); + currentId = indexToId(index); + emit currentIndexChanged(index); + + if (oldIndex < index && !pending.contains(currentId)) { + readEvent(currentId.toStdString()); + } +} + +void +TimelineModel::readEvent(const std::string &id) +{ + http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to read_event ({}, {})", + room_id_.toStdString(), + currentId.toStdString()); + } + }); +} + +void +TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) +{ + std::vector ids = internalAddEvents(msgs.chunk); + + if (!ids.empty()) { + beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); + endInsertRows(); + } + + prev_batch_token_ = QString::fromStdString(msgs.end); +} + +QColor +TimelineModel::userColor(QString id, QColor background) +{ + if (!userColors.contains(id)) + userColors.insert( + id, QColor(utils::generateContrastingHexColor(id, background.name()))); + return userColors.value(id); +} + +QString +TimelineModel::displayName(QString id) const +{ + return Cache::displayName(room_id_, id); +} + +QString +TimelineModel::avatarUrl(QString id) const +{ + return Cache::avatarUrl(room_id_, id); +} + +QString +TimelineModel::formatDateSeparator(QDate date) const +{ + auto now = QDateTime::currentDateTime(); + + QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); + + if (now.date().year() == date.year()) { + QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); + fmt = fmt.remove(rx); + } + + return date.toString(fmt); +} + +QString +TimelineModel::escapeEmoji(QString str) const +{ + return utils::replaceEmoji(str); +} + +void +TimelineModel::viewRawMessage(QString id) const +{ + std::string ev = utils::serialize_event(events.value(id)).dump(4); + auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); + Q_UNUSED(dialog); +} + +void + +TimelineModel::openUserProfile(QString userid) const +{ + MainWindow::instance()->openUserProfile(userid, room_id_); +} + +DecryptionResult +TimelineModel::decryptEvent(const mtx::events::EncryptedEvent &e) const +{ + MegolmSessionIndex index; + index.room_id = room_id_.toStdString(); + index.session_id = e.content.session_id; + index.sender_key = e.content.sender_key; + + mtx::events::RoomEvent dummy; + dummy.origin_server_ts = e.origin_server_ts; + dummy.event_id = e.event_id; + dummy.sender = e.sender; + dummy.content.body = + tr("-- Encrypted Event (No keys found for decryption) --", + "Placeholder, when the message was not decrypted yet or can't be decrypted") + .toStdString(); + + try { + if (!cache::client()->inboundMegolmSessionExists(index)) { + nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", + index.room_id, + index.session_id, + e.sender); + // TODO: request megolm session_id & session_key from the sender. + return {dummy, false}; + } + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); + dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --", + "Placeholder, when the message can't be decrypted, because " + "the DB access failed when trying to lookup the session.") + .toStdString(); + return {dummy, false}; + } + + std::string msg_str; + try { + auto session = cache::client()->getInboundMegolmSession(index); + auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); + msg_str = std::string((char *)res.data.data(), res.data.size()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (failed to retrieve megolm keys from db) --", + "Placeholder, when the message can't be decrypted, because the DB access " + "failed.") + .toStdString(); + return {dummy, false}; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (%1) --", + "Placeholder, when the message can't be decrypted. In this case, the Olm " + "decrytion returned an error, which is passed ad %1") + .arg(e.what()) + .toStdString(); + return {dummy, false}; + } + + // Add missing fields for the event. + json body = json::parse(msg_str); + body["event_id"] = e.event_id; + body["sender"] = e.sender; + body["origin_server_ts"] = e.origin_server_ts; + body["unsigned"] = e.unsigned_data; + + json event_array = json::array(); + event_array.push_back(body); + + std::vector temp_events; + mtx::responses::utils::parse_timeline_events(event_array, temp_events); + + if (temp_events.size() == 1) + return {temp_events.at(0), true}; + + dummy.content.body = + tr("-- Encrypted Event (Unknown event type) --", + "Placeholder, when the message was decrypted, but we couldn't parse it, because " + "Nheko/mtxclient don't support that event type yet") + .toStdString(); + return {dummy, false}; +} + +void +TimelineModel::replyAction(QString id) +{ + auto event = events.value(id); + RelatedInfo related = boost::apply_visitor( + [](const auto &ev) -> RelatedInfo { + RelatedInfo related_ = {}; + related_.quoted_user = QString::fromStdString(ev.sender); + related_.related_event = ev.event_id; + return related_; + }, + event); + related.type = mtx::events::getMessageType(boost::apply_visitor( + [](const auto &e) -> std::string { return eventMsgType(e); }, event)); + related.quoted_body = boost::apply_visitor( + [](const auto &e) -> QString { return eventFormattedBody(e); }, event); + related.quoted_body.remove(QRegularExpression( + ".*", QRegularExpression::DotMatchesEverythingOption)); + nhlog::ui()->debug("after replacement: {}", related.quoted_body.toStdString()); + related.room = room_id_; + + if (related.quoted_body.isEmpty()) + return; + + ChatPage::instance()->messageReply(related); +} + +void +TimelineModel::readReceiptsAction(QString id) const +{ + MainWindow::instance()->openReadReceiptsDialog(id); +} + +void +TimelineModel::redactEvent(QString id) +{ + if (!id.isEmpty()) + http::client()->redact_event( + room_id_.toStdString(), + id.toStdString(), + [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit redactionFailed( + tr("Message redaction failed: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit eventRedacted(id); + }); +} + +int +TimelineModel::idToIndex(QString id) const +{ + if (id.isEmpty()) + return -1; + for (int i = 0; i < (int)eventOrder.size(); i++) + if (id == eventOrder[i]) + return i; + return -1; +} + +QString +TimelineModel::indexToId(int index) const +{ + if (index < 0 || index >= (int)eventOrder.size()) + return ""; + return eventOrder[index]; +} + +// Note: this will only be called for our messages +void +TimelineModel::markEventsAsRead(const std::vector &event_ids) +{ + for (const auto &id : event_ids) { + read.insert(id); + int idx = idToIndex(id); + if (idx < 0) { + nhlog::ui()->warn("Read index out of range"); + return; + } + emit dataChanged(index(idx, 0), index(idx, 0)); + } +} + +void +TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json content) +{ + const auto room_id = room_id_.toStdString(); + + using namespace mtx::events; + using namespace mtx::identifiers; + + json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; + + try { + // Check if we have already an outbound megolm session then we can use. + if (cache::client()->outboundMegolmSessionExists(room_id)) { + auto data = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc.dump()); + + http::client()->send_room_message( + room_id, + txn_id, + data, + [this, txn_id](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed(QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + return; + } + + nhlog::ui()->debug("creating new outbound megolm session"); + + // Create a new outbound megolm session. + auto outbound_session = olm::client()->init_outbound_group_session(); + const auto session_id = mtx::crypto::session_id(outbound_session.get()); + const auto session_key = mtx::crypto::session_key(outbound_session.get()); + + // TODO: needs to be moved in the lib. + auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, + {"room_id", room_id}, + {"session_id", session_id}, + {"session_key", session_key}}; + + // Saving the new megolm session. + // TODO: Maybe it's too early to save. + OutboundGroupSessionData session_data; + session_data.session_id = session_id; + session_data.session_key = session_key; + session_data.message_index = 0; // TODO Update me + cache::client()->saveOutboundMegolmSession( + room_id, session_data, std::move(outbound_session)); + + const auto members = cache::client()->roomMembers(room_id); + nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); + + auto keeper = + std::make_shared([megolm_payload, room_id, doc, txn_id, this]() { + try { + auto data = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc.dump()); + + http::client() + ->send_room_message( + room_id, + txn_id, + data, + [this, txn_id](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast(err->status_code); + nhlog::net()->warn( + "[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed( + QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to save megolm outbound session: {}", e.what()); + } + }); + + mtx::requests::QueryKeys req; + for (const auto &member : members) + req.device_keys[member] = {}; + + http::client()->query_keys( + req, + [keeper = std::move(keeper), megolm_payload, this]( + const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {} {}", + err->matrix_error.error, + static_cast(err->status_code)); + // TODO: Mark the event as failed. Communicate with the UI. + return; + } + + for (const auto &user : res.device_keys) { + // Mapping from a device_id with valid identity keys to the + // generated room_key event used for sharing the megolm session. + std::map room_key_msgs; + std::map deviceKeys; + + room_key_msgs.clear(); + deviceKeys.clear(); + + for (const auto &dev : user.second) { + const auto user_id = ::UserId(dev.second.user_id); + const auto device_id = DeviceId(dev.second.device_id); + + const auto device_keys = dev.second.keys; + const auto curveKey = "curve25519:" + device_id.get(); + const auto edKey = "ed25519:" + device_id.get(); + + if ((device_keys.find(curveKey) == device_keys.end()) || + (device_keys.find(edKey) == device_keys.end())) { + nhlog::net()->debug( + "ignoring malformed keys for device {}", + device_id.get()); + continue; + } + + DevicePublicKeys pks; + pks.ed25519 = device_keys.at(edKey); + pks.curve25519 = device_keys.at(curveKey); + + try { + if (!mtx::crypto::verify_identity_signature( + json(dev.second), device_id, user_id)) { + nhlog::crypto()->warn( + "failed to verify identity keys: {}", + json(dev.second).dump(2)); + continue; + } + } catch (const json::exception &e) { + nhlog::crypto()->warn( + "failed to parse device key json: {}", + e.what()); + continue; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->warn( + "failed to verify device key json: {}", + e.what()); + continue; + } + + auto room_key = olm::client() + ->create_room_key_event( + user_id, pks.ed25519, megolm_payload) + .dump(); + + room_key_msgs.emplace(device_id, room_key); + deviceKeys.emplace(device_id, pks); + } + + std::vector valid_devices; + valid_devices.reserve(room_key_msgs.size()); + for (auto const &d : room_key_msgs) { + valid_devices.push_back(d.first); + + nhlog::net()->info("{}", d.first); + nhlog::net()->info(" curve25519 {}", + deviceKeys.at(d.first).curve25519); + nhlog::net()->info(" ed25519 {}", + deviceKeys.at(d.first).ed25519); + } + + nhlog::net()->info( + "sending claim request for user {} with {} devices", + user.first, + valid_devices.size()); + + http::client()->claim_keys( + user.first, + valid_devices, + std::bind(&TimelineModel::handleClaimedKeys, + this, + keeper, + room_key_msgs, + deviceKeys, + user.first, + std::placeholders::_1, + std::placeholders::_2)); + + // TODO: Wait before sending the next batch of requests. + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + }); + + // TODO: Let the user know about the errors. + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + } +} + +void +TimelineModel::handleClaimedKeys(std::shared_ptr keeper, + const std::map &room_keys, + const std::map &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err) +{ + if (err) { + nhlog::net()->warn("claim keys error: {} {} {}", + err->matrix_error.error, + err->parse_error, + static_cast(err->status_code)); + return; + } + + nhlog::net()->debug("claimed keys for {}", user_id); + + if (res.one_time_keys.size() == 0) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + auto retrieved_devices = res.one_time_keys.at(user_id); + + // Payload with all the to_device message to be sent. + json body; + body["messages"][user_id] = json::object(); + + for (const auto &rd : retrieved_devices) { + const auto device_id = rd.first; + nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); + + // TODO: Verify signatures + auto otk = rd.second.begin()->at("key"); + + if (pks.find(device_id) == pks.end()) { + nhlog::net()->critical("couldn't find public key for device: {}", + device_id); + continue; + } + + auto id_key = pks.at(device_id).curve25519; + auto s = olm::client()->create_outbound_session(id_key, otk); + + if (room_keys.find(device_id) == room_keys.end()) { + nhlog::net()->critical("couldn't find m.room_key for device: {}", + device_id); + continue; + } + + auto device_msg = olm::client()->create_olm_encrypted_content( + s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); + + try { + cache::client()->saveOlmSession(id_key, std::move(s)); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to pickle outbound olm session: {}", + e.what()); + } + + body["messages"][user_id][device_id] = device_msg; + } + + nhlog::net()->info("send_to_device: {}", user_id); + + http::client()->send_to_device( + "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send " + "send_to_device " + "message: {}", + err->matrix_error.error); + } + + (void)keeper; + }); +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h new file mode 100644 index 00000000..31e41315 --- /dev/null +++ b/src/timeline/TimelineModel.h @@ -0,0 +1,258 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" + +namespace qml_mtx_events { +Q_NAMESPACE + +enum EventType +{ + // Unsupported event + Unsupported, + /// m.room_key_request + KeyRequest, + /// m.room.aliases + Aliases, + /// m.room.avatar + Avatar, + /// m.room.canonical_alias + CanonicalAlias, + /// m.room.create + Create, + /// m.room.encrypted. + Encrypted, + /// m.room.encryption. + Encryption, + /// m.room.guest_access + GuestAccess, + /// m.room.history_visibility + HistoryVisibility, + /// m.room.join_rules + JoinRules, + /// m.room.member + Member, + /// m.room.name + Name, + /// m.room.power_levels + PowerLevels, + /// m.room.tombstone + Tombstone, + /// m.room.topic + Topic, + /// m.room.redaction + Redaction, + /// m.room.pinned_events + PinnedEvents, + // m.sticker + Sticker, + // m.tag + Tag, + /// m.room.message + AudioMessage, + EmoteMessage, + FileMessage, + ImageMessage, + LocationMessage, + NoticeMessage, + TextMessage, + VideoMessage, + Redacted, + UnknownMessage, +}; +Q_ENUM_NS(EventType) + +enum EventState +{ + //! The plaintext message was received by the server. + Received, + //! At least one of the participants has read the message. + Read, + //! The client sent the message. Not yet received. + Sent, + //! When the message is loaded from cache or backfill. + Empty, + //! When the message failed to send + Failed, +}; +Q_ENUM_NS(EventState) +} + +class StateKeeper +{ +public: + StateKeeper(std::function &&fn) + : fn_(std::move(fn)) + {} + + ~StateKeeper() { fn_(); } + +private: + std::function fn_; +}; + +struct DecryptionResult +{ + //! The decrypted content as a normal plaintext event. + mtx::events::collections::TimelineEvents event; + //! Whether or not the decryption was successful. + bool isDecrypted = false; +}; + +class TimelineViewManager; + +class TimelineModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY( + int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + +public: + explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = 0); + + enum Roles + { + Section, + Type, + Body, + FormattedBody, + UserId, + UserName, + Timestamp, + Url, + ThumbnailUrl, + Filename, + Filesize, + MimeType, + Height, + Width, + ProportionalHeight, + Id, + State, + IsEncrypted, + ReplyTo, + }; + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + Q_INVOKABLE QColor userColor(QString id, QColor background); + Q_INVOKABLE QString displayName(QString id) const; + Q_INVOKABLE QString avatarUrl(QString id) const; + Q_INVOKABLE QString formatDateSeparator(QDate date) const; + + Q_INVOKABLE QString escapeEmoji(QString str) const; + Q_INVOKABLE void viewRawMessage(QString id) const; + Q_INVOKABLE void openUserProfile(QString userid) const; + Q_INVOKABLE void replyAction(QString id); + Q_INVOKABLE void readReceiptsAction(QString id) const; + Q_INVOKABLE void redactEvent(QString id); + Q_INVOKABLE int idToIndex(QString id) const; + Q_INVOKABLE QString indexToId(int index) const; + + void addEvents(const mtx::responses::Timeline &events); + template + void sendMessage(const T &msg); + +public slots: + void fetchHistory(); + void setCurrentIndex(int index); + int currentIndex() const { return idToIndex(currentId); } + void markEventsAsRead(const std::vector &event_ids); + +private slots: + // Add old events at the top of the timeline. + void addBackwardsEvents(const mtx::responses::Messages &msgs); + +signals: + void oldMessagesRetrieved(const mtx::responses::Messages &res); + void messageFailed(QString txn_id); + void messageSent(QString txn_id, QString event_id); + void currentIndexChanged(int index); + void redactionFailed(QString id); + void eventRedacted(QString id); + +private: + DecryptionResult decryptEvent( + const mtx::events::EncryptedEvent &e) const; + std::vector internalAddEvents( + const std::vector &timeline); + void sendEncryptedMessage(const std::string &txn_id, nlohmann::json content); + void handleClaimedKeys(std::shared_ptr keeper, + const std::map &room_key, + const std::map &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err); + void updateLastMessage(); + void readEvent(const std::string &id); + + QHash events; + QSet pending, failed, read; + std::vector eventOrder; + + QString room_id_; + QString prev_batch_token_; + + bool isInitialSync = true; + bool paginationInProgress = false; + + QHash userColors; + QString currentId; + + TimelineViewManager *manager_; +}; + +template +void +TimelineModel::sendMessage(const T &msg) +{ + auto txn_id = http::client()->generate_txn_id(); + mtx::events::RoomEvent msgCopy = {}; + msgCopy.content = msg; + msgCopy.type = mtx::events::EventType::RoomMessage; + msgCopy.event_id = txn_id; + msgCopy.sender = http::client()->user_id().to_string(); + msgCopy.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); + internalAddEvents({msgCopy}); + + QString txn_id_qstr = QString::fromStdString(txn_id); + beginInsertRows(QModelIndex(), + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size())); + pending.insert(txn_id_qstr); + this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); + endInsertRows(); + updateLastMessage(); + + if (cache::client()->isRoomEncrypted(room_id_.toStdString())) + sendEncryptedMessage(txn_id, nlohmann::json(msg)); + else + http::client()->send_room_message( + room_id_.toStdString(), + txn_id, + msg, + [this, txn_id, txn_id_qstr](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = static_cast(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed(txn_id_qstr); + } + emit messageSent(txn_id_qstr, + QString::fromStdString(res.event_id.to_string())); + }); +} diff --git a/src/timeline/TimelineView.cpp b/src/timeline/TimelineView.cpp deleted file mode 100644 index ed783e90..00000000 --- a/src/timeline/TimelineView.cpp +++ /dev/null @@ -1,1627 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include - -#include -#include -#include -#include - -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "Olm.h" -#include "UserSettingsPage.h" -#include "Utils.h" -#include "ui/FloatingButton.h" -#include "ui/InfoMessage.h" - -#include "timeline/TimelineView.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -using TimelineEvent = mtx::events::collections::TimelineEvents; - -//! Maximum number of widgets to keep in the timeline layout. -constexpr int MAX_RETAINED_WIDGETS = 100; -constexpr int MIN_SCROLLBAR_HANDLE = 60; - -//! Retrieve the timestamp of the event represented by the given widget. -QDateTime -getDate(QWidget *widget) -{ - auto item = qobject_cast(widget); - if (item) - return item->descriptionMessage().datetime; - - auto infoMsg = qobject_cast(widget); - if (infoMsg) - return infoMsg->datetime(); - - return QDateTime(); -} - -TimelineView::TimelineView(const mtx::responses::Timeline &timeline, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addEvents(timeline); -} - -TimelineView::TimelineView(const QString &room_id, QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - getMessages(); -} - -void -TimelineView::sliderRangeChanged(int min, int max) -{ - Q_UNUSED(min); - - if (!scroll_area_->verticalScrollBar()->isVisible()) { - scroll_area_->verticalScrollBar()->setValue(max); - return; - } - - // If the scrollbar is close to the bottom and a new message - // is added we move the scrollbar. - if (max - scroll_area_->verticalScrollBar()->value() < SCROLL_BAR_GAP) { - scroll_area_->verticalScrollBar()->setValue(max); - return; - } - - int currentHeight = scroll_widget_->size().height(); - int diff = currentHeight - oldHeight_; - int newPosition = oldPosition_ + diff; - - // Keep the scroll bar to the bottom if it hasn't been activated yet. - if (oldPosition_ == 0 && !scroll_area_->verticalScrollBar()->isVisible()) - newPosition = max; - - if (lastMessageDirection_ == TimelineDirection::Top) - scroll_area_->verticalScrollBar()->setValue(newPosition); -} - -void -TimelineView::fetchHistory() -{ - if (!isScrollbarActivated() && !isTimelineFinished) { - if (!isVisible()) - return; - - isPaginationInProgress_ = true; - getMessages(); - paginationTimer_->start(2000); - - return; - } - - paginationTimer_->stop(); -} - -void -TimelineView::scrollDown() -{ - int current = scroll_area_->verticalScrollBar()->value(); - int max = scroll_area_->verticalScrollBar()->maximum(); - - // The first time we enter the room move the scroll bar to the bottom. - if (!isInitialized) { - scroll_area_->verticalScrollBar()->setValue(max); - isInitialized = true; - return; - } - - // If the gap is small enough move the scroll bar down. e.g when a new - // message appears. - if (max - current < SCROLL_BAR_GAP) - scroll_area_->verticalScrollBar()->setValue(max); -} - -void -TimelineView::sliderMoved(int position) -{ - if (!scroll_area_->verticalScrollBar()->isVisible()) - return; - - toggleScrollDownButton(); - - // The scrollbar is high enough so we can start retrieving old events. - if (position < SCROLL_BAR_GAP) { - if (isTimelineFinished) - return; - - // Prevent user from moving up when there is pagination in - // progress. - if (isPaginationInProgress_) - return; - - isPaginationInProgress_ = true; - - getMessages(); - } -} - -bool -TimelineView::isStartOfTimeline(const mtx::responses::Messages &msgs) -{ - return (msgs.chunk.size() == 0 && (msgs.end.empty() || msgs.end == msgs.start)); -} - -void -TimelineView::addBackwardsEvents(const mtx::responses::Messages &msgs) -{ - // We've reached the start of the timline and there're no more messages. - if (isStartOfTimeline(msgs)) { - nhlog::ui()->info("[{}] start of timeline reached, no more messages to fetch", - room_id_.toStdString()); - isTimelineFinished = true; - return; - } - - isTimelineFinished = false; - - // Queue incoming messages to be rendered later. - topMessages_.insert(topMessages_.end(), - std::make_move_iterator(msgs.chunk.begin()), - std::make_move_iterator(msgs.chunk.end())); - - // The RoomList message preview will be updated only if this - // is the first batch of messages received through /messages - // i.e there are no other messages currently present. - if (!topMessages_.empty() && scroll_layout_->count() == 0) - notifyForLastEvent(findFirstViewableEvent(topMessages_)); - - if (isVisible()) { - renderTopEvents(topMessages_); - - // Free up space for new messages. - topMessages_.clear(); - - // Send a read receipt for the last event. - if (isActiveWindow()) - readLastEvent(); - } - - prev_batch_token_ = QString::fromStdString(msgs.end); - isPaginationInProgress_ = false; -} - -QWidget * -TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event, - TimelineDirection direction) -{ - using namespace mtx::events; - - using AudioEvent = RoomEvent; - using EmoteEvent = RoomEvent; - using FileEvent = RoomEvent; - using ImageEvent = RoomEvent; - using NoticeEvent = RoomEvent; - using TextEvent = RoomEvent; - using VideoEvent = RoomEvent; - - if (boost::get>(&event) != nullptr) { - auto redaction_event = boost::get>(event); - const auto event_id = QString::fromStdString(redaction_event.redacts); - - QTimer::singleShot(0, this, [event_id, this]() { - if (eventIds_.contains(event_id)) - removeEvent(event_id); - }); - - return nullptr; - } else if (boost::get>(&event) != nullptr) { - auto msg = boost::get>(event); - auto event_id = QString::fromStdString(msg.event_id); - - if (eventIds_.contains(event_id)) - return nullptr; - - auto item = new InfoMessage(tr("Encryption is enabled"), this); - item->saveDatetime(QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts)); - eventIds_[event_id] = item; - - // Force the next message to have avatar by not providing the current username. - saveMessageInfo("", msg.origin_server_ts, direction); - - return item; - } else if (boost::get>(&event) != nullptr) { - auto audio = boost::get>(event); - return processMessageEvent(audio, direction); - } else if (boost::get>(&event) != nullptr) { - auto emote = boost::get>(event); - return processMessageEvent(emote, direction); - } else if (boost::get>(&event) != nullptr) { - auto file = boost::get>(event); - return processMessageEvent(file, direction); - } else if (boost::get>(&event) != nullptr) { - auto image = boost::get>(event); - return processMessageEvent(image, direction); - } else if (boost::get>(&event) != nullptr) { - auto notice = boost::get>(event); - return processMessageEvent(notice, direction); - } else if (boost::get>(&event) != nullptr) { - auto text = boost::get>(event); - return processMessageEvent(text, direction); - } else if (boost::get>(&event) != nullptr) { - auto video = boost::get>(event); - return processMessageEvent(video, direction); - } else if (boost::get(&event) != nullptr) { - return processMessageEvent(boost::get(event), - direction); - } else if (boost::get>(&event) != nullptr) { - auto res = parseEncryptedEvent(boost::get>(event)); - auto widget = parseMessageEvent(res.event, direction); - - if (widget == nullptr) - return nullptr; - - auto item = qobject_cast(widget); - - if (item && res.isDecrypted) - item->markReceived(true); - else if (item && !res.isDecrypted) - item->addKeyRequestAction(); - - return widget; - } - - return nullptr; -} - -DecryptionResult -TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent &e) -{ - MegolmSessionIndex index; - index.room_id = room_id_.toStdString(); - index.session_id = e.content.session_id; - index.sender_key = e.content.sender_key; - - mtx::events::RoomEvent dummy; - dummy.origin_server_ts = e.origin_server_ts; - dummy.event_id = e.event_id; - dummy.sender = e.sender; - dummy.content.body = - tr("-- Encrypted Event (No keys found for decryption) --", - "Placeholder, when the message was not decrypted yet or can't be decrypted") - .toStdString(); - - try { - if (!cache::client()->inboundMegolmSessionExists(index)) { - nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", - index.room_id, - index.session_id, - e.sender); - // TODO: request megolm session_id & session_key from the sender. - return {dummy, false}; - } - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); - dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --", - "Placeholder, when the message can't be decrypted, because " - "the DB access failed when trying to lookup the session.") - .toStdString(); - return {dummy, false}; - } - - std::string msg_str; - try { - auto session = cache::client()->getInboundMegolmSession(index); - auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (failed to retrieve megolm keys from db) --", - "Placeholder, when the message can't be decrypted, because the DB access " - "failed.") - .toStdString(); - return {dummy, false}; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (%1) --", - "Placeholder, when the message can't be decrypted. In this case, the Olm " - "decrytion returned an error, which is passed ad %1") - .arg(e.what()) - .toStdString(); - return {dummy, false}; - } - - // Add missing fields for the event. - json body = json::parse(msg_str); - body["event_id"] = e.event_id; - body["sender"] = e.sender; - body["origin_server_ts"] = e.origin_server_ts; - body["unsigned"] = e.unsigned_data; - - nhlog::crypto()->debug("decrypted event: {}", e.event_id); - - json event_array = json::array(); - event_array.push_back(body); - - std::vector events; - mtx::responses::utils::parse_timeline_events(event_array, events); - - if (events.size() == 1) - return {events.at(0), true}; - - dummy.content.body = - tr("-- Encrypted Event (Unknown event type) --", - "Placeholder, when the message was decrypted, but we couldn't parse it, because " - "Nheko/mtxclient don't support that event type yet") - .toStdString(); - return {dummy, false}; -} - -void -TimelineView::displayReadReceipts(std::vector events) -{ - QtConcurrent::run( - [events = std::move(events), room_id = room_id_, local_user = local_user_, this]() { - std::vector event_ids; - - for (const auto &e : events) { - if (utils::event_sender(e) == local_user) - event_ids.emplace_back( - QString::fromStdString(utils::event_id(e))); - } - - auto readEvents = - cache::client()->filterReadEvents(room_id, event_ids, local_user.toStdString()); - - if (!readEvents.empty()) - emit markReadEvents(readEvents); - }); -} - -void -TimelineView::renderBottomEvents(const std::vector &events) -{ - int counter = 0; - - for (const auto &event : events) { - QWidget *item = parseMessageEvent(event, TimelineDirection::Bottom); - - if (item != nullptr) { - addTimelineItem(item, TimelineDirection::Bottom); - counter++; - - // Prevent blocking of the event-loop - // by calling processEvents every 10 items we render. - if (counter % 4 == 0) - QApplication::processEvents(); - } - } - - lastMessageDirection_ = TimelineDirection::Bottom; - - displayReadReceipts(events); - - QApplication::processEvents(); -} - -void -TimelineView::renderTopEvents(const std::vector &events) -{ - std::vector items; - - // Reset the sender of the first message in the timeline - // cause we're about to insert a new one. - firstSender_.clear(); - firstMsgTimestamp_ = QDateTime(); - - // Parse in reverse order to determine where we should not show sender's name. - for (auto it = events.rbegin(); it != events.rend(); ++it) { - auto item = parseMessageEvent(*it, TimelineDirection::Top); - - if (item != nullptr) - items.push_back(item); - } - - // Reverse again to render them. - std::reverse(items.begin(), items.end()); - - oldPosition_ = scroll_area_->verticalScrollBar()->value(); - oldHeight_ = scroll_widget_->size().height(); - - for (const auto &item : items) - addTimelineItem(item, TimelineDirection::Top); - - lastMessageDirection_ = TimelineDirection::Top; - - QApplication::processEvents(); - - displayReadReceipts(events); - - // If this batch is the first being rendered (i.e the first and the last - // events originate from this batch), set the last sender. - if (lastSender_.isEmpty() && !items.empty()) { - for (const auto &w : items) { - auto timelineItem = qobject_cast(w); - if (timelineItem) { - saveLastMessageInfo(timelineItem->descriptionMessage().userid, - timelineItem->descriptionMessage().datetime); - break; - } - } - } -} - -void -TimelineView::addEvents(const mtx::responses::Timeline &timeline) -{ - if (isInitialSync) { - prev_batch_token_ = QString::fromStdString(timeline.prev_batch); - isInitialSync = false; - } - - bottomMessages_.insert(bottomMessages_.end(), - std::make_move_iterator(timeline.events.begin()), - std::make_move_iterator(timeline.events.end())); - - if (!bottomMessages_.empty()) - notifyForLastEvent(findLastViewableEvent(bottomMessages_)); - - // If the current timeline is open and there are messages to be rendered. - if (isVisible() && !bottomMessages_.empty()) { - renderBottomEvents(bottomMessages_); - - // Free up space for new messages. - bottomMessages_.clear(); - - // Send a read receipt for the last event. - if (isActiveWindow()) - readLastEvent(); - } -} - -void -TimelineView::init() -{ - local_user_ = utils::localUser(); - - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-arrow-down.png"); - scrollDownBtn_ = new FloatingButton(icon, this); - scrollDownBtn_->hide(); - - connect(scrollDownBtn_, &QPushButton::clicked, this, [this]() { - const int max = scroll_area_->verticalScrollBar()->maximum(); - scroll_area_->verticalScrollBar()->setValue(max); - }); - top_layout_ = new QVBoxLayout(this); - top_layout_->setSpacing(0); - top_layout_->setMargin(0); - - scroll_area_ = new QScrollArea(this); - scroll_area_->setWidgetResizable(true); - scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scroll_widget_ = new QWidget(this); - scroll_widget_->setObjectName("scroll_widget"); - - // Height of the typing display. - QFont f; - f.setPointSizeF(f.pointSizeF() * 0.9); - const int bottomMargin = QFontMetrics(f).height() + 6; - - scroll_layout_ = new QVBoxLayout(scroll_widget_); - scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); - scroll_layout_->setSpacing(0); - scroll_layout_->setObjectName("timelinescrollarea"); - - scroll_area_->setWidget(scroll_widget_); - scroll_area_->setAlignment(Qt::AlignBottom); - - top_layout_->addWidget(scroll_area_); - - setLayout(top_layout_); - - paginationTimer_ = new QTimer(this); - connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory); - - connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents); - - connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage); - connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage); - - connect( - this, &TimelineView::markReadEvents, this, [this](const std::vector &event_ids) { - for (const auto &event : event_ids) { - if (eventIds_.contains(event)) { - auto widget = eventIds_[event]; - if (!widget) - return; - - auto item = qobject_cast(widget); - if (!item) - return; - - item->markRead(); - } - } - }); - - connect(scroll_area_->verticalScrollBar(), - SIGNAL(valueChanged(int)), - this, - SLOT(sliderMoved(int))); - connect(scroll_area_->verticalScrollBar(), - SIGNAL(rangeChanged(int, int)), - this, - SLOT(sliderRangeChanged(int, int))); -} - -void -TimelineView::getMessages() -{ - mtx::http::MessagesOpts opts; - opts.room_id = room_id_.toStdString(); - opts.from = prev_batch_token_.toStdString(); - - http::client()->messages( - opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error("failed to call /messages ({}): {} - {}", - opts.room_id, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - emit messagesRetrieved(std::move(res)); - }); -} - -void -TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction) -{ - if (direction == TimelineDirection::Bottom) - lastSender_ = user_id; - else - firstSender_ = user_id; -} - -bool -TimelineView::isSenderRendered(const QString &user_id, - uint64_t origin_server_ts, - TimelineDirection direction) -{ - if (direction == TimelineDirection::Bottom) { - return (lastSender_ != user_id) || - isDateDifference(lastMsgTimestamp_, - QDateTime::fromMSecsSinceEpoch(origin_server_ts)); - } else { - return (firstSender_ != user_id) || - isDateDifference(firstMsgTimestamp_, - QDateTime::fromMSecsSinceEpoch(origin_server_ts)); - } -} - -void -TimelineView::addTimelineItem(QWidget *item, TimelineDirection direction) -{ - const auto newDate = getDate(item); - - if (direction == TimelineDirection::Bottom) { - QWidget *lastItem = nullptr; - int lastItemPosition = 0; - - if (scroll_layout_->count() > 0) { - lastItemPosition = scroll_layout_->count() - 1; - lastItem = scroll_layout_->itemAt(lastItemPosition)->widget(); - } - - if (lastItem) { - const auto oldDate = getDate(lastItem); - - if (oldDate.daysTo(newDate) != 0) { - auto separator = new DateSeparator(newDate, this); - - if (separator) - pushTimelineItem(separator, direction); - } - } - - pushTimelineItem(item, direction); - } else { - if (scroll_layout_->count() > 0) { - const auto firstItem = scroll_layout_->itemAt(0)->widget(); - - if (firstItem) { - const auto oldDate = getDate(firstItem); - - if (newDate.daysTo(oldDate) != 0) { - auto separator = new DateSeparator(oldDate); - - if (separator) - pushTimelineItem(separator, direction); - } - } - } - - pushTimelineItem(item, direction); - } -} - -void -TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id) -{ - nhlog::ui()->debug("[{}] message was received by the server", txn_id); - if (!pending_msgs_.isEmpty() && - pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet - auto msg = pending_msgs_.dequeue(); - msg.event_id = event_id; - - if (msg.widget) { - msg.widget->setEventId(event_id); - eventIds_[event_id] = msg.widget; - - // If the response comes after we have received the event from sync - // we've already marked the widget as received. - if (!msg.widget->isReceived()) { - msg.widget->markReceived(msg.is_encrypted); - cache::client()->addPendingReceipt(room_id_, event_id); - pending_sent_msgs_.append(msg); - } - } else { - nhlog::ui()->warn("[{}] received message response for invalid widget", - txn_id); - } - } - - sendNextPendingMessage(); -} - -void -TimelineView::addUserMessage(mtx::events::MessageType ty, - const QString &body, - const RelatedInfo &related = RelatedInfo()) -{ - auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); - - QString full_body; - if (related.related_event.empty()) { - full_body = body; - } else { - full_body = utils::getFormattedQuoteBody(related, body); - } - TimelineItem *view_item = - new TimelineItem(ty, local_user_, full_body, with_sender, room_id_, scroll_widget_); - - PendingMessage message; - message.ty = ty; - message.txn_id = http::client()->generate_txn_id(); - message.body = body; - message.related = related; - message.widget = view_item; - - try { - message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check encryption status of room {}", e.what()); - view_item->deleteLater(); - - // TODO: Send a notification to the user. - - return; - } - - addTimelineItem(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - handleNewUserMessage(message); -} - -void -TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) -{ - addUserMessage(ty, body, RelatedInfo()); -} - -void -TimelineView::handleNewUserMessage(PendingMessage msg) -{ - pending_msgs_.enqueue(msg); - if (pending_msgs_.size() == 1 && pending_sent_msgs_.isEmpty()) - sendNextPendingMessage(); -} - -void -TimelineView::sendNextPendingMessage() -{ - if (pending_msgs_.size() == 0) - return; - - using namespace mtx::events; - - PendingMessage &m = pending_msgs_.head(); - - nhlog::ui()->debug("[{}] sending next queued message", m.txn_id); - - if (m.widget) - m.widget->markSent(); - - if (m.is_encrypted) { - nhlog::ui()->debug("[{}] sending encrypted event", m.txn_id); - prepareEncryptedMessage(std::move(m)); - return; - } - - switch (m.ty) { - case mtx::events::MessageType::Audio: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Image: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Video: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::File: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Text: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Emote: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - break; - } - default: - nhlog::ui()->warn("cannot send unknown message type: {}", m.body.toStdString()); - break; - } -} - -void -TimelineView::notifyForLastEvent() -{ - if (scroll_layout_->count() == 0) { - nhlog::ui()->error("notifyForLastEvent called with empty timeline"); - return; - } - - auto lastItem = scroll_layout_->itemAt(scroll_layout_->count() - 1); - - if (!lastItem) - return; - - auto *lastTimelineItem = qobject_cast(lastItem->widget()); - - if (lastTimelineItem) - emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage()); - else - nhlog::ui()->warn("cast to TimelineItem failed: {}", room_id_.toStdString()); -} - -void -TimelineView::notifyForLastEvent(const TimelineEvent &event) -{ - auto descInfo = utils::getMessageDescription(event, local_user_, room_id_); - - if (!descInfo.timestamp.isEmpty()) - emit updateLastTimelineMessage(room_id_, descInfo); -} - -bool -TimelineView::isPendingMessage(const std::string &txn_id, - const QString &sender, - const QString &local_userid) -{ - if (sender != local_userid) - return false; - - auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; }; - - return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) || - std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid); -} - -void -TimelineView::removePendingMessage(const std::string &txn_id) -{ - if (txn_id.empty()) - return; - - for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) { - if (it->txn_id == txn_id) { - int index = std::distance(pending_sent_msgs_.begin(), it); - pending_sent_msgs_.removeAt(index); - - if (pending_sent_msgs_.isEmpty()) - sendNextPendingMessage(); - - nhlog::ui()->debug("[{}] removed message with sync", txn_id); - } - } - for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) { - if (it->txn_id == txn_id) { - if (it->widget) { - it->widget->markReceived(it->is_encrypted); - - // TODO: update when a solution for encrypted messages is available. - if (!it->is_encrypted) - cache::client()->addPendingReceipt(room_id_, it->event_id); - } - - nhlog::ui()->debug("[{}] received sync before message response", txn_id); - return; - } - } -} - -void -TimelineView::handleFailedMessage(const std::string &txn_id) -{ - Q_UNUSED(txn_id); - // Note: We do this even if the message has already been echoed. - QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage())); -} - -void -TimelineView::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -TimelineView::readLastEvent() const -{ - if (!ChatPage::instance()->userSettings()->isReadReceiptsEnabled()) - return; - - const auto eventId = getLastEventId(); - - if (!eventId.isEmpty()) - http::client()->read_event(room_id_.toStdString(), - eventId.toStdString(), - [this, eventId](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read event ({}, {})", - room_id_.toStdString(), - eventId.toStdString()); - } - }); -} - -QString -TimelineView::getLastEventId() const -{ - auto index = scroll_layout_->count(); - - // Search backwards for the first event that has a valid event id. - while (index > 0) { - --index; - - auto lastItem = scroll_layout_->itemAt(index); - auto *lastTimelineItem = qobject_cast(lastItem->widget()); - - if (lastTimelineItem && !lastTimelineItem->eventId().isEmpty()) - return lastTimelineItem->eventId(); - } - - return QString(""); -} - -void -TimelineView::showEvent(QShowEvent *event) -{ - if (!topMessages_.empty()) { - renderTopEvents(topMessages_); - topMessages_.clear(); - } - - if (!bottomMessages_.empty()) { - renderBottomEvents(bottomMessages_); - bottomMessages_.clear(); - scrollDown(); - } - - toggleScrollDownButton(); - - readLastEvent(); - - QWidget::showEvent(event); -} - -void -TimelineView::hideEvent(QHideEvent *event) -{ - const auto handleHeight = scroll_area_->verticalScrollBar()->sizeHint().height(); - const auto widgetsNum = scroll_layout_->count(); - - // Remove widgets from the timeline to reduce the memory footprint. - if (handleHeight < MIN_SCROLLBAR_HANDLE && widgetsNum > MAX_RETAINED_WIDGETS) - clearTimeline(); - - QWidget::hideEvent(event); -} - -bool -TimelineView::event(QEvent *event) -{ - if (event->type() == QEvent::WindowActivate) - readLastEvent(); - - return QWidget::event(event); -} - -void -TimelineView::clearTimeline() -{ - // Delete all widgets. - QLayoutItem *item; - while ((item = scroll_layout_->takeAt(0)) != nullptr) { - delete item->widget(); - delete item; - } - - // The next call to /messages will be without a prev token. - prev_batch_token_.clear(); - eventIds_.clear(); - - // Clear queues with pending messages to be rendered. - bottomMessages_.clear(); - topMessages_.clear(); - - firstSender_.clear(); - lastSender_.clear(); -} - -void -TimelineView::toggleScrollDownButton() -{ - const int maxScroll = scroll_area_->verticalScrollBar()->maximum(); - const int currentScroll = scroll_area_->verticalScrollBar()->value(); - - if (maxScroll - currentScroll > SCROLL_BAR_GAP) { - scrollDownBtn_->show(); - scrollDownBtn_->raise(); - } else { - scrollDownBtn_->hide(); - } -} - -void -TimelineView::removeEvent(const QString &event_id) -{ - if (!eventIds_.contains(event_id)) { - nhlog::ui()->warn("cannot remove widget with unknown event_id: {}", - event_id.toStdString()); - return; - } - - auto removedItem = eventIds_[event_id]; - - // Find the next and the previous widgets in the timeline - auto prevWidget = relativeWidget(removedItem, -1); - auto nextWidget = relativeWidget(removedItem, 1); - - // See if they are timeline items - auto prevItem = qobject_cast(prevWidget); - auto nextItem = qobject_cast(nextWidget); - - // ... or a date separator - auto prevLabel = qobject_cast(prevWidget); - - // If it's a TimelineItem add an avatar. - if (prevItem) { - prevItem->addAvatar(); - } - - if (nextItem) { - nextItem->addAvatar(); - } else if (prevLabel) { - // If there's no chat message after this, and we have a label before us, delete the - // label. - prevLabel->deleteLater(); - } - - // If we deleted the last item in the timeline... - if (!nextItem && prevItem) - saveLastMessageInfo(prevItem->descriptionMessage().userid, - prevItem->descriptionMessage().datetime); - - // If we deleted the first item in the timeline... - if (!prevItem && nextItem) - saveFirstMessageInfo(nextItem->descriptionMessage().userid, - nextItem->descriptionMessage().datetime); - - // If we deleted the only item in the timeline... - if (!prevItem && !nextItem) { - firstSender_.clear(); - firstMsgTimestamp_ = QDateTime(); - lastSender_.clear(); - lastMsgTimestamp_ = QDateTime(); - } - - // Finally remove the event. - removedItem->deleteLater(); - eventIds_.remove(event_id); - - // Update the room list with a view of the last message after - // all events have been processed. - QTimer::singleShot(0, this, [this]() { notifyForLastEvent(); }); -} - -QWidget * -TimelineView::relativeWidget(QWidget *item, int dt) const -{ - int pos = scroll_layout_->indexOf(item); - - if (pos == -1) - return nullptr; - - pos = pos + dt; - - bool isOutOfBounds = (pos < 0 || pos > scroll_layout_->count() - 1); - - return isOutOfBounds ? nullptr : scroll_layout_->itemAt(pos)->widget(); -} - -TimelineEvent -TimelineView::findFirstViewableEvent(const std::vector &events) -{ - auto it = std::find_if(events.begin(), events.end(), [](const auto &event) { - return mtx::events::EventType::RoomMessage == utils::event_type(event); - }); - - return (it == std::end(events)) ? events.front() : *it; -} - -TimelineEvent -TimelineView::findLastViewableEvent(const std::vector &events) -{ - auto it = std::find_if(events.rbegin(), events.rend(), [](const auto &event) { - return (mtx::events::EventType::RoomMessage == utils::event_type(event)) || - (mtx::events::EventType::RoomEncrypted == utils::event_type(event)); - }); - - return (it == std::rend(events)) ? events.back() : *it; -} - -void -TimelineView::saveMessageInfo(const QString &sender, - uint64_t origin_server_ts, - TimelineDirection direction) -{ - updateLastSender(sender, direction); - - if (direction == TimelineDirection::Bottom) - lastMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts); - else - firstMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts); -} - -bool -TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second) const -{ - // Check if the dates are in a different day. - if (std::abs(first.daysTo(second)) != 0) - return true; - - const uint64_t diffInSeconds = std::abs(first.msecsTo(second)) / 1000; - constexpr uint64_t fifteenMins = 15 * 60; - - return diffInSeconds > fifteenMins; -} - -void -TimelineView::sendRoomMessageHandler(const std::string &txn_id, - const mtx::responses::EventId &res, - mtx::http::RequestErr err) -{ - if (err) { - const int status_code = static_cast(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(txn_id); - return; - } - - emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string())); -} - -template<> -mtx::events::msg::Audio -toRoomMessage(const PendingMessage &m) -{ - mtx::events::msg::Audio audio; - audio.info.mimetype = m.mime.toStdString(); - audio.info.size = m.media_size; - audio.body = m.filename.toStdString(); - audio.url = m.body.toStdString(); - return audio; -} - -template<> -mtx::events::msg::Image -toRoomMessage(const PendingMessage &m) -{ - mtx::events::msg::Image image; - image.info.mimetype = m.mime.toStdString(); - image.info.size = m.media_size; - image.body = m.filename.toStdString(); - image.url = m.body.toStdString(); - image.info.h = m.dimensions.height(); - image.info.w = m.dimensions.width(); - return image; -} - -template<> -mtx::events::msg::Video -toRoomMessage(const PendingMessage &m) -{ - mtx::events::msg::Video video; - video.info.mimetype = m.mime.toStdString(); - video.info.size = m.media_size; - video.body = m.filename.toStdString(); - video.url = m.body.toStdString(); - return video; -} - -template<> -mtx::events::msg::Emote -toRoomMessage(const PendingMessage &m) -{ - auto html = utils::markdownToHtml(m.body); - - mtx::events::msg::Emote emote; - emote.body = m.body.trimmed().toStdString(); - - if (html != m.body.trimmed().toHtmlEscaped()) - emote.formatted_body = html.toStdString(); - - return emote; -} - -template<> -mtx::events::msg::File -toRoomMessage(const PendingMessage &m) -{ - mtx::events::msg::File file; - file.info.mimetype = m.mime.toStdString(); - file.info.size = m.media_size; - file.body = m.filename.toStdString(); - file.url = m.body.toStdString(); - return file; -} - -template<> -mtx::events::msg::Text -toRoomMessage(const PendingMessage &m) -{ - auto html = utils::markdownToHtml(m.body); - - mtx::events::msg::Text text; - - text.body = m.body.trimmed().toStdString(); - - if (html != m.body.trimmed().toHtmlEscaped()) { - if (!m.related.quoted_body.isEmpty()) { - text.formatted_body = - utils::getFormattedQuoteBody(m.related, html).toStdString(); - } else { - text.formatted_body = html.toStdString(); - } - } - - if (!m.related.related_event.empty()) { - text.relates_to.in_reply_to.event_id = m.related.related_event; - } - - return text; -} - -void -TimelineView::prepareEncryptedMessage(const PendingMessage &msg) -{ - const auto room_id = room_id_.toStdString(); - - using namespace mtx::events; - using namespace mtx::identifiers; - - json content; - - // Serialize the message to the plaintext that will be encrypted. - switch (msg.ty) { - case MessageType::Audio: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::Emote: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::File: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::Image: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::Text: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::Video: { - content = json(toRoomMessage(msg)); - break; - } - default: - break; - } - - json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; - - try { - // Check if we have already an outbound megolm session then we can use. - if (cache::client()->outboundMegolmSessionExists(room_id)) { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client()->send_room_message( - room_id, - msg.txn_id, - data, - std::bind(&TimelineView::sendRoomMessageHandler, - this, - msg.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - return; - } - - nhlog::ui()->debug("creating new outbound megolm session"); - - // Create a new outbound megolm session. - auto outbound_session = olm::client()->init_outbound_group_session(); - const auto session_id = mtx::crypto::session_id(outbound_session.get()); - const auto session_key = mtx::crypto::session_key(outbound_session.get()); - - // TODO: needs to be moved in the lib. - auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, - {"room_id", room_id}, - {"session_id", session_id}, - {"session_key", session_key}}; - - // Saving the new megolm session. - // TODO: Maybe it's too early to save. - OutboundGroupSessionData session_data; - session_data.session_id = session_id; - session_data.session_key = session_key; - session_data.message_index = 0; // TODO Update me - cache::client()->saveOutboundMegolmSession( - room_id, session_data, std::move(outbound_session)); - - const auto members = cache::client()->roomMembers(room_id); - nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); - - auto keeper = std::make_shared( - [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() { - try { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client() - ->send_room_message( - room_id, - txn_id, - data, - std::bind(&TimelineView::sendRoomMessageHandler, - this, - txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to save megolm outbound session: {}", e.what()); - } - }); - - mtx::requests::QueryKeys req; - for (const auto &member : members) - req.device_keys[member] = {}; - - http::client()->query_keys( - req, - [keeper = std::move(keeper), megolm_payload, this]( - const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to query device keys: {} {}", - err->matrix_error.error, - static_cast(err->status_code)); - // TODO: Mark the event as failed. Communicate with the UI. - return; - } - - for (const auto &user : res.device_keys) { - // Mapping from a device_id with valid identity keys to the - // generated room_key event used for sharing the megolm session. - std::map room_key_msgs; - std::map deviceKeys; - - room_key_msgs.clear(); - deviceKeys.clear(); - - for (const auto &dev : user.second) { - const auto user_id = UserId(dev.second.user_id); - const auto device_id = DeviceId(dev.second.device_id); - - const auto device_keys = dev.second.keys; - const auto curveKey = "curve25519:" + device_id.get(); - const auto edKey = "ed25519:" + device_id.get(); - - if ((device_keys.find(curveKey) == device_keys.end()) || - (device_keys.find(edKey) == device_keys.end())) { - nhlog::net()->debug( - "ignoring malformed keys for device {}", - device_id.get()); - continue; - } - - DevicePublicKeys pks; - pks.ed25519 = device_keys.at(edKey); - pks.curve25519 = device_keys.at(curveKey); - - try { - if (!mtx::crypto::verify_identity_signature( - json(dev.second), device_id, user_id)) { - nhlog::crypto()->warn( - "failed to verify identity keys: {}", - json(dev.second).dump(2)); - continue; - } - } catch (const json::exception &e) { - nhlog::crypto()->warn( - "failed to parse device key json: {}", - e.what()); - continue; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->warn( - "failed to verify device key json: {}", - e.what()); - continue; - } - - auto room_key = olm::client() - ->create_room_key_event( - user_id, pks.ed25519, megolm_payload) - .dump(); - - room_key_msgs.emplace(device_id, room_key); - deviceKeys.emplace(device_id, pks); - } - - std::vector valid_devices; - valid_devices.reserve(room_key_msgs.size()); - for (auto const &d : room_key_msgs) { - valid_devices.push_back(d.first); - - nhlog::net()->info("{}", d.first); - nhlog::net()->info(" curve25519 {}", - deviceKeys.at(d.first).curve25519); - nhlog::net()->info(" ed25519 {}", - deviceKeys.at(d.first).ed25519); - } - - nhlog::net()->info( - "sending claim request for user {} with {} devices", - user.first, - valid_devices.size()); - - http::client()->claim_keys( - user.first, - valid_devices, - std::bind(&TimelineView::handleClaimedKeys, - this, - keeper, - room_key_msgs, - deviceKeys, - user.first, - std::placeholders::_1, - std::placeholders::_2)); - - // TODO: Wait before sending the next batch of requests. - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - }); - - // TODO: Let the user know about the errors. - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } -} - -void -TimelineView::handleClaimedKeys(std::shared_ptr keeper, - const std::map &room_keys, - const std::map &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err) -{ - if (err) { - nhlog::net()->warn("claim keys error: {} {} {}", - err->matrix_error.error, - err->parse_error, - static_cast(err->status_code)); - return; - } - - nhlog::net()->debug("claimed keys for {}", user_id); - - if (res.one_time_keys.size() == 0) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - auto retrieved_devices = res.one_time_keys.at(user_id); - - // Payload with all the to_device message to be sent. - json body; - body["messages"][user_id] = json::object(); - - for (const auto &rd : retrieved_devices) { - const auto device_id = rd.first; - nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); - - // TODO: Verify signatures - auto otk = rd.second.begin()->at("key"); - - if (pks.find(device_id) == pks.end()) { - nhlog::net()->critical("couldn't find public key for device: {}", - device_id); - continue; - } - - auto id_key = pks.at(device_id).curve25519; - auto s = olm::client()->create_outbound_session(id_key, otk); - - if (room_keys.find(device_id) == room_keys.end()) { - nhlog::net()->critical("couldn't find m.room_key for device: {}", - device_id); - continue; - } - - auto device_msg = olm::client()->create_olm_encrypted_content( - s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); - - try { - cache::client()->saveOlmSession(id_key, std::move(s)); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to pickle outbound olm session: {}", - e.what()); - } - - body["messages"][user_id][device_id] = device_msg; - } - - nhlog::net()->info("send_to_device: {}", user_id); - - http::client()->send_to_device( - "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - - (void)keeper; - }); -} diff --git a/src/timeline/TimelineView.h b/src/timeline/TimelineView.h deleted file mode 100644 index 35796efd..00000000 --- a/src/timeline/TimelineView.h +++ /dev/null @@ -1,449 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "../Utils.h" -#include "MatrixClient.h" -#include "timeline/TimelineItem.h" - -class StateKeeper -{ -public: - StateKeeper(std::function &&fn) - : fn_(std::move(fn)) - {} - - ~StateKeeper() { fn_(); } - -private: - std::function fn_; -}; - -struct DecryptionResult -{ - //! The decrypted content as a normal plaintext event. - utils::TimelineEvent event; - //! Whether or not the decryption was successful. - bool isDecrypted = false; -}; - -class FloatingButton; -struct DescInfo; - -// Contains info about a message shown in the history view -// but not yet confirmed by the homeserver through sync. -struct PendingMessage -{ - mtx::events::MessageType ty; - std::string txn_id; - RelatedInfo related; - QString body; - QString filename; - QString mime; - uint64_t media_size; - QString event_id; - TimelineItem *widget; - QSize dimensions; - bool is_encrypted = false; -}; - -template -MessageT -toRoomMessage(const PendingMessage &) = delete; - -template<> -mtx::events::msg::Audio -toRoomMessage(const PendingMessage &m); - -template<> -mtx::events::msg::Emote -toRoomMessage(const PendingMessage &m); - -template<> -mtx::events::msg::File -toRoomMessage(const PendingMessage &); - -template<> -mtx::events::msg::Image -toRoomMessage(const PendingMessage &m); - -template<> -mtx::events::msg::Text -toRoomMessage(const PendingMessage &); - -template<> -mtx::events::msg::Video -toRoomMessage(const PendingMessage &m); - -// In which place new TimelineItems should be inserted. -enum class TimelineDirection -{ - Top, - Bottom, -}; - -class TimelineView : public QWidget -{ - Q_OBJECT - -public: - TimelineView(const mtx::responses::Timeline &timeline, - const QString &room_id, - QWidget *parent = 0); - TimelineView(const QString &room_id, QWidget *parent = 0); - - // Add new events at the end of the timeline. - void addEvents(const mtx::responses::Timeline &timeline); - void addUserMessage(mtx::events::MessageType ty, - const QString &body, - const RelatedInfo &related); - void addUserMessage(mtx::events::MessageType ty, const QString &msg); - - template - void addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions = QSize()); - void updatePendingMessage(const std::string &txn_id, const QString &event_id); - void scrollDown(); - - //! Remove an item from the timeline with the given Event ID. - void removeEvent(const QString &event_id); - void setPrevBatchToken(const QString &token) { prev_batch_token_ = token; } - -public slots: - void sliderRangeChanged(int min, int max); - void sliderMoved(int position); - void fetchHistory(); - - // Add old events at the top of the timeline. - void addBackwardsEvents(const mtx::responses::Messages &msgs); - - // Whether or not the initial batch has been loaded. - bool hasLoaded() { return scroll_layout_->count() > 0 || isTimelineFinished; } - - void handleFailedMessage(const std::string &txn_id); - -private slots: - void sendNextPendingMessage(); - -signals: - void updateLastTimelineMessage(const QString &user, const DescInfo &info); - void messagesRetrieved(const mtx::responses::Messages &res); - void messageFailed(const std::string &txn_id); - void messageSent(const std::string &txn_id, const QString &event_id); - void markReadEvents(const std::vector &event_ids); - -protected: - void paintEvent(QPaintEvent *event) override; - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - bool event(QEvent *event) override; - -private: - using TimelineEvent = mtx::events::collections::TimelineEvents; - - //! Mark our own widgets as read if they have more than one receipt. - void displayReadReceipts(std::vector events); - //! Determine if the start of the timeline is reached from the response of /messages. - bool isStartOfTimeline(const mtx::responses::Messages &msgs); - - QWidget *relativeWidget(QWidget *item, int dt) const; - - DecryptionResult parseEncryptedEvent( - const mtx::events::EncryptedEvent &e); - - void handleClaimedKeys(std::shared_ptr keeper, - const std::map &room_key, - const std::map &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err); - - //! Callback for all message sending. - void sendRoomMessageHandler(const std::string &txn_id, - const mtx::responses::EventId &res, - mtx::http::RequestErr err); - void prepareEncryptedMessage(const PendingMessage &msg); - - //! Call the /messages endpoint to fill the timeline. - void getMessages(); - //! HACK: Fixing layout flickering when adding to the bottom - //! of the timeline. - void pushTimelineItem(QWidget *item, TimelineDirection dir) - { - setUpdatesEnabled(false); - item->hide(); - - if (dir == TimelineDirection::Top) - scroll_layout_->insertWidget(0, item); - else - scroll_layout_->addWidget(item); - - QTimer::singleShot(0, this, [item, this]() { - item->show(); - item->adjustSize(); - setUpdatesEnabled(true); - }); - } - - //! Decides whether or not to show or hide the scroll down button. - void toggleScrollDownButton(); - void init(); - void addTimelineItem(QWidget *item, - TimelineDirection direction = TimelineDirection::Bottom); - void updateLastSender(const QString &user_id, TimelineDirection direction); - void notifyForLastEvent(); - void notifyForLastEvent(const TimelineEvent &event); - //! Keep track of the sender and the timestamp of the current message. - void saveLastMessageInfo(const QString &sender, const QDateTime &datetime) - { - lastSender_ = sender; - lastMsgTimestamp_ = datetime; - } - void saveFirstMessageInfo(const QString &sender, const QDateTime &datetime) - { - firstSender_ = sender; - firstMsgTimestamp_ = datetime; - } - //! Keep track of the sender and the timestamp of the current message. - void saveMessageInfo(const QString &sender, - uint64_t origin_server_ts, - TimelineDirection direction); - - TimelineEvent findFirstViewableEvent(const std::vector &events); - TimelineEvent findLastViewableEvent(const std::vector &events); - - //! Mark the last event as read. - void readLastEvent() const; - //! Whether or not the scrollbar is visible (non-zero height). - bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; } - //! Retrieve the event id of the last item. - QString getLastEventId() const; - - template - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // TODO: Remove this eventually. - template - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // For events with custom display widgets. - template - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // For events without custom display widgets. - // TODO: All events should have custom widgets. - template - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // Used to determine whether or not we should prefix a message with the - // sender's name. - bool isSenderRendered(const QString &user_id, - uint64_t origin_server_ts, - TimelineDirection direction); - - bool isPendingMessage(const std::string &txn_id, - const QString &sender, - const QString &userid); - void removePendingMessage(const std::string &txn_id); - - bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); } - - void handleNewUserMessage(PendingMessage msg); - bool isDateDifference(const QDateTime &first, - const QDateTime &second = QDateTime::currentDateTime()) const; - - // Return nullptr if the event couldn't be parsed. - QWidget *parseMessageEvent(const mtx::events::collections::TimelineEvents &event, - TimelineDirection direction); - - //! Store the event id associated with the given widget. - void saveEventId(QWidget *widget); - //! Remove all widgets from the timeline layout. - void clearTimeline(); - - QVBoxLayout *top_layout_; - QVBoxLayout *scroll_layout_; - - QScrollArea *scroll_area_; - QWidget *scroll_widget_; - - QString firstSender_; - QDateTime firstMsgTimestamp_; - QString lastSender_; - QDateTime lastMsgTimestamp_; - - QString room_id_; - QString prev_batch_token_; - QString local_user_; - - bool isPaginationInProgress_ = false; - - // Keeps track whether or not the user has visited the view. - bool isInitialized = false; - bool isTimelineFinished = false; - bool isInitialSync = true; - - const int SCROLL_BAR_GAP = 200; - - QTimer *paginationTimer_; - - int scroll_height_ = 0; - int previous_max_height_ = 0; - - int oldPosition_; - int oldHeight_; - - FloatingButton *scrollDownBtn_; - - TimelineDirection lastMessageDirection_; - - //! Messages received by sync not added to the timeline. - std::vector bottomMessages_; - //! Messages received by /messages not added to the timeline. - std::vector topMessages_; - - //! Render the given timeline events to the bottom of the timeline. - void renderBottomEvents(const std::vector &events); - //! Render the given timeline events to the top of the timeline. - void renderTopEvents(const std::vector &events); - - // The events currently rendered. Used for duplicate detection. - QMap eventIds_; - QQueue pending_msgs_; - QList pending_sent_msgs_; -}; - -template -void -TimelineView::addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions) -{ - auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); - auto trimmed = QFileInfo{filename}.fileName(); // Trim file path. - - auto widget = new Widget(url, trimmed, size, this); - - TimelineItem *view_item = - new TimelineItem(widget, local_user_, with_sender, room_id_, scroll_widget_); - - addTimelineItem(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - // Keep track of the sender and the timestamp of the current message. - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - - PendingMessage message; - message.ty = MsgType; - message.txn_id = http::client()->generate_txn_id(); - message.body = url; - message.filename = trimmed; - message.mime = mime; - message.media_size = size; - message.widget = view_item; - message.dimensions = dimensions; - - handleNewUserMessage(message); -} - -template -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - TimelineItem *item = new TimelineItem(event, withSender, room_id_, scroll_widget_); - return item; -} - -template -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - auto eventWidget = new Widget(event); - auto item = new TimelineItem(eventWidget, event, withSender, room_id_, scroll_widget_); - - return item; -} - -template -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} - -template -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 86505481..d733ad90 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -1,340 +1,400 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include - -#include -#include -#include - -#include "Cache.h" +#include "TimelineViewManager.h" + +#include +#include +#include +#include +#include +#include + +#include "ChatPage.h" +#include "ColorImageProvider.h" +#include "DelegateChooser.h" #include "Logging.h" -#include "Utils.h" -#include "timeline/TimelineView.h" -#include "timeline/TimelineViewManager.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -TimelineViewManager::TimelineViewManager(QWidget *parent) - : QStackedWidget(parent) -{} +#include "MxcImageProvider.h" +#include "UserSettingsPage.h" +#include "dialogs/ImageOverlay.h" void -TimelineViewManager::updateReadReceipts(const QString &room_id, - const std::vector &event_ids) +TimelineViewManager::updateColorPalette() { - if (timelineViewExists(room_id)) { - auto view = views_[room_id]; - if (view) - emit view->markReadEvents(event_ids); + UserSettings settings; + if (settings.theme() == "light") { + QPalette lightActive(/*windowText*/ QColor("#333"), + /*button*/ QColor("#333"), + /*light*/ QColor(), + /*dark*/ QColor(220, 220, 220, 120), + /*mid*/ QColor(), + /*text*/ QColor("#333"), + /*bright_text*/ QColor(), + /*base*/ QColor("white"), + /*window*/ QColor("white")); + view->rootContext()->setContextProperty("currentActivePalette", lightActive); + view->rootContext()->setContextProperty("currentInactivePalette", lightActive); + } else if (settings.theme() == "dark") { + QPalette darkActive(/*windowText*/ QColor("#caccd1"), + /*button*/ QColor("#caccd1"), + /*light*/ QColor(), + /*dark*/ QColor(45, 49, 57, 120), + /*mid*/ QColor(), + /*text*/ QColor("#caccd1"), + /*bright_text*/ QColor(), + /*base*/ QColor("#202228"), + /*window*/ QColor("#202228")); + darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9")); + view->rootContext()->setContextProperty("currentActivePalette", darkActive); + view->rootContext()->setContextProperty("currentInactivePalette", darkActive); + } else { + view->rootContext()->setContextProperty("currentActivePalette", QPalette()); + view->rootContext()->setContextProperty("currentInactivePalette", nullptr); } } -void -TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id) +TimelineViewManager::TimelineViewManager(QWidget *parent) + : imgProvider(new MxcImageProvider()) + , colorImgProvider(new ColorImageProvider()) { - auto view = views_[room_id]; - - if (view) - view->removeEvent(event_id); + qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, + "com.github.nheko", + 1, + 0, + "MtxEvent", + "Can't instantiate enum!"); + qmlRegisterType("com.github.nheko", 1, 0, "DelegateChoice"); + qmlRegisterType("com.github.nheko", 1, 0, "DelegateChooser"); + +#ifdef USE_QUICK_VIEW + view = new QQuickView(); + container = QWidget::createWindowContainer(view, parent); +#else + view = new QQuickWidget(parent); + container = view; + view->setResizeMode(QQuickWidget::SizeRootObjectToView); + container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { + nhlog::ui()->debug("Status changed to {}", status); + }); +#endif + container->setMinimumSize(200, 200); + view->rootContext()->setContextProperty("timelineManager", this); + updateColorPalette(); + view->engine()->addImageProvider("MxcImage", imgProvider); + view->engine()->addImageProvider("colorimage", colorImgProvider); + view->setSource(QUrl("qrc:///qml/TimelineView.qml")); + + connect(dynamic_cast(parent), + &ChatPage::themeChanged, + this, + &TimelineViewManager::updateColorPalette); } void -TimelineViewManager::queueTextMessage(const QString &msg) +TimelineViewManager::sync(const mtx::responses::Rooms &rooms) { - if (active_room_.isEmpty()) - return; - - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Text, msg); + for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { + // addRoom will only add the room, if it doesn't exist + addRoom(QString::fromStdString(it->first)); + models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline); + } } void -TimelineViewManager::queueEmoteMessage(const QString &msg) +TimelineViewManager::addRoom(const QString &room_id) { - if (active_room_.isEmpty()) - return; - - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Emote, msg); + if (!models.contains(room_id)) + models.insert(room_id, + QSharedPointer(new TimelineModel(this, room_id))); } void -TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) +TimelineViewManager::setHistoryView(const QString &room_id) { - if (active_room_.isEmpty()) - return; + nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Text, reply, related); + auto room = models.find(room_id); + if (room != models.end()) { + timeline_ = room.value().data(); + emit activeTimelineChanged(timeline_); + nhlog::ui()->info("Activated room {}", room_id.toStdString()); + } } void -TimelineViewManager::queueImageMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size, - const QSize &dimensions) +TimelineViewManager::openImageOverlay(QString mxcUrl, + QString originalFilename, + QString mimeType, + qml_mtx_events::EventType eventType) const { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("Cannot send m.image message to a non-managed view"); - return; - } - - auto view = views_[roomid]; - - view->addUserMessage( - url, filename, mime, size, dimensions); + QQuickImageResponse *imgResponse = + imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize()); + connect(imgResponse, + &QQuickImageResponse::finished, + this, + [this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() { + if (!imgResponse->errorString().isEmpty()) { + nhlog::ui()->error("Error when retrieving image for overlay: {}", + imgResponse->errorString().toStdString()); + return; + } + auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); + + auto imgDialog = new dialogs::ImageOverlay(pixmap); + imgDialog->show(); + connect(imgDialog, + &dialogs::ImageOverlay::saving, + this, + [this, mxcUrl, originalFilename, mimeType, eventType]() { + saveMedia(mxcUrl, originalFilename, mimeType, eventType); + }); + }); } void -TimelineViewManager::queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::saveMedia(QString mxcUrl, + QString originalFilename, + QString mimeType, + qml_mtx_events::EventType eventType) const { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.file message to a non-managed view"); - return; + QString dialogTitle; + if (eventType == qml_mtx_events::EventType::ImageMessage) { + dialogTitle = tr("Save image"); + } else if (eventType == qml_mtx_events::EventType::VideoMessage) { + dialogTitle = tr("Save video"); + } else if (eventType == qml_mtx_events::EventType::AudioMessage) { + dialogTitle = tr("Save audio"); + } else { + dialogTitle = tr("Save file"); } - auto view = views_[roomid]; + QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); + + auto filename = + QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString); + + if (filename.isEmpty()) + return; - view->addUserMessage(url, filename, mime, size); + const auto url = mxcUrl.toStdString(); + + http::client()->download( + url, + [filename, url](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(data.data(), data.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); } void -TimelineViewManager::queueAudioMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.audio message to a non-managed view"); + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + emit mediaCached(mxcUrl, mxcUrl); return; } - auto view = views_[roomid]; - - view->addUserMessage(url, filename, mime, size); -} + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); -void -TimelineViewManager::queueVideoMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) -{ - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.video message to a non-managed view"); + const auto url = mxcUrl.toStdString(); + QFileInfo filename(QString("%1/media_cache/%2.%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString(mxcUrl).remove("mxc://")) + .arg(suffix)); + if (QDir::cleanPath(filename.path()) != filename.path()) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); return; } - auto view = views_[roomid]; + QDir().mkpath(filename.path()); - view->addUserMessage(url, filename, mime, size); + if (filename.isReadable()) { + emit mediaCached(mxcUrl, filename.filePath()); + return; + } + + http::client()->download( + url, + [this, mxcUrl, filename, url](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(data.data(), data.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + + emit mediaCached(mxcUrl, filename.filePath()); + }); } void -TimelineViewManager::initialize(const mtx::responses::Rooms &rooms) +TimelineViewManager::updateReadReceipts(const QString &room_id, + const std::vector &event_ids) { - for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { - addRoom(it->second, QString::fromStdString(it->first)); + auto room = models.find(room_id); + if (room != models.end()) { + room.value()->markEventsAsRead(event_ids); } - - sync(rooms); } void TimelineViewManager::initWithMessages(const std::map &msgs) { - for (auto it = msgs.cbegin(); it != msgs.cend(); ++it) { - if (timelineViewExists(it->first)) - return; - - // Create a history view with the room events. - TimelineView *view = new TimelineView(it->second, it->first); - views_.emplace(it->first, QSharedPointer(view)); + for (const auto &e : msgs) { + addRoom(e.first); - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); - - // Add the view in the widget stack. - addWidget(view); + models.value(e.first)->addEvents(e.second); } } void -TimelineViewManager::initialize(const std::vector &rooms) +TimelineViewManager::queueTextMessage(const QString &msg) { - for (const auto &roomid : rooms) - addRoom(QString::fromStdString(roomid)); + mtx::events::msg::Text text = {}; + text.body = msg.trimmed().toStdString(); + text.format = "org.matrix.custom.html"; + text.formatted_body = utils::markdownToHtml(msg).toStdString(); + + if (timeline_) + timeline_->sendMessage(text); } void -TimelineViewManager::addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id) +TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) { - if (timelineViewExists(room_id)) - return; - - // Create a history view with the room events. - TimelineView *view = new TimelineView(room.timeline, room_id); - views_.emplace(room_id, QSharedPointer(view)); + mtx::events::msg::Text text = {}; + + QString body; + bool firstLine = true; + for (const auto &line : related.quoted_body.split("\n")) { + if (firstLine) { + firstLine = false; + body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line); + } else { + body = QString("%1\n> %2\n").arg(body).arg(line); + } + } - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); + text.body = QString("%1\n%2").arg(body).arg(reply).toStdString(); + text.format = "org.matrix.custom.html"; + text.formatted_body = + utils::getFormattedQuoteBody(related, utils::markdownToHtml(reply)).toStdString(); + text.relates_to.in_reply_to.event_id = related.related_event; - // Add the view in the widget stack. - addWidget(view); + if (timeline_) + timeline_->sendMessage(text); } void -TimelineViewManager::addRoom(const QString &room_id) +TimelineViewManager::queueEmoteMessage(const QString &msg) { - if (timelineViewExists(room_id)) - return; + auto html = utils::markdownToHtml(msg); - // Create a history view without any events. - TimelineView *view = new TimelineView(room_id); - views_.emplace(room_id, QSharedPointer(view)); + mtx::events::msg::Emote emote; + emote.body = msg.trimmed().toStdString(); - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); + if (html != msg.trimmed().toHtmlEscaped()) + emote.formatted_body = html.toStdString(); - // Add the view in the widget stack. - addWidget(view); + if (timeline_) + timeline_->sendMessage(emote); } void -TimelineViewManager::sync(const mtx::responses::Rooms &rooms) +TimelineViewManager::queueImageMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize, + const QSize &dimensions) { - for (const auto &room : rooms.join) { - auto roomid = QString::fromStdString(room.first); - - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("ignoring event from unknown room: {}", - roomid.toStdString()); - continue; - } - - auto view = views_.at(roomid); - - view->addEvents(room.second.timeline); - } + mtx::events::msg::Image image; + image.info.mimetype = mime.toStdString(); + image.info.size = dsize; + image.body = filename.toStdString(); + image.url = url.toStdString(); + image.info.h = dimensions.height(); + image.info.w = dimensions.width(); + models.value(roomid)->sendMessage(image); } void -TimelineViewManager::setHistoryView(const QString &room_id) +TimelineViewManager::queueFileMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) { - if (!timelineViewExists(room_id)) { - nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}", - room_id.toStdString()); - return; - } - - active_room_ = room_id; - auto view = views_.at(room_id); - - setCurrentWidget(view.data()); - - view->fetchHistory(); - view->scrollDown(); + mtx::events::msg::File file; + file.info.mimetype = mime.toStdString(); + file.info.size = dsize; + file.body = filename.toStdString(); + file.url = url.toStdString(); + models.value(roomid)->sendMessage(file); } -QString -TimelineViewManager::chooseRandomColor() +void +TimelineViewManager::queueAudioMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) { - std::random_device random_device; - std::mt19937 engine{random_device()}; - std::uniform_real_distribution dist(0, 1); - - float hue = dist(engine); - float saturation = 0.9; - float value = 0.7; - - int hue_i = hue * 6; - - float f = hue * 6 - hue_i; - - float p = value * (1 - saturation); - float q = value * (1 - f * saturation); - float t = value * (1 - (1 - f) * saturation); - - float r = 0; - float g = 0; - float b = 0; - - if (hue_i == 0) { - r = value; - g = t; - b = p; - } else if (hue_i == 1) { - r = q; - g = value; - b = p; - } else if (hue_i == 2) { - r = p; - g = value; - b = t; - } else if (hue_i == 3) { - r = p; - g = q; - b = value; - } else if (hue_i == 4) { - r = t; - g = p; - b = value; - } else if (hue_i == 5) { - r = value; - g = p; - b = q; - } - - int ri = r * 256; - int gi = g * 256; - int bi = b * 256; - - QColor color(ri, gi, bi); - - return color.name(); + mtx::events::msg::Audio audio; + audio.info.mimetype = mime.toStdString(); + audio.info.size = dsize; + audio.body = filename.toStdString(); + audio.url = url.toStdString(); + models.value(roomid)->sendMessage(audio); } -bool -TimelineViewManager::hasLoaded() const +void +TimelineViewManager::queueVideoMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) { - return std::all_of(views_.cbegin(), views_.cend(), [](const auto &view) { - return view.second->hasLoaded(); - }); + mtx::events::msg::Video video; + video.info.mimetype = mime.toStdString(); + video.info.size = dsize; + video.body = filename.toStdString(); + video.url = url.toStdString(); + models.value(roomid)->sendMessage(video); } diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index b52136d9..691c8ddb 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -1,69 +1,80 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - #pragma once +#include +#include #include -#include +#include -#include +#include +#include "Cache.h" +#include "Logging.h" +#include "TimelineModel.h" #include "Utils.h" -class QFile; +// temporary for stubs +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" -class RoomInfoListItem; -class TimelineView; -struct DescInfo; -struct SavedMessages; +class MxcImageProvider; +class ColorImageProvider; -class TimelineViewManager : public QStackedWidget +class TimelineViewManager : public QObject { Q_OBJECT -public: - TimelineViewManager(QWidget *parent); - - // Initialize with timeline events. - void initialize(const mtx::responses::Rooms &rooms); - // Empty initialization. - void initialize(const std::vector &rooms); + Q_PROPERTY( + TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) - void addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id); - void addRoom(const QString &room_id); +public: + TimelineViewManager(QWidget *parent = 0); + QWidget *getWidget() const { return container; } void sync(const mtx::responses::Rooms &rooms); - void clearAll() { views_.clear(); } - - // Check if all the timelines have been loaded. - bool hasLoaded() const; + void addRoom(const QString &room_id); - static QString chooseRandomColor(); + void clearAll() { models.clear(); } + + Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } + void openImageOverlay(QString mxcUrl, + QString originalFilename, + QString mimeType, + qml_mtx_events::EventType eventType) const; + void saveMedia(QString mxcUrl, + QString originalFilename, + QString mimeType, + qml_mtx_events::EventType eventType) const; + Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType); + // Qml can only pass enum as int + Q_INVOKABLE void openImageOverlay(QString mxcUrl, + QString originalFilename, + QString mimeType, + int eventType) const + { + openImageOverlay( + mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); + } + Q_INVOKABLE void saveMedia(QString mxcUrl, + QString originalFilename, + QString mimeType, + int eventType) const + { + saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); + } signals: void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(const QString &user, const DescInfo &info); + void updateRoomsLastMessage(QString roomid, const DescInfo &info); + void activeTimelineChanged(TimelineModel *timeline); + void mediaCached(QString mxcUrl, QString cacheUrl); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); - void removeTimelineEvent(const QString &room_id, const QString &event_id); void initWithMessages(const std::map &msgs); void setHistoryView(const QString &room_id); + void updateColorPalette(); + void queueTextMessage(const QString &msg); void queueReplyMessage(const QString &reply, const RelatedInfo &related); void queueEmoteMessage(const QString &msg); @@ -90,9 +101,17 @@ public slots: uint64_t dsize); private: - //! Check if the given room id is managed by a TimelineView. - bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); } - - QString active_room_; - std::map> views_; +#ifdef USE_QUICK_VIEW + QQuickView *view; +#else + QQuickWidget *view; +#endif + QWidget *container; + TimelineModel *timeline_ = nullptr; + MxcImageProvider *imgProvider; + ColorImageProvider *colorImgProvider; + + QHash> models; }; + +#pragma GCC diagnostic pop diff --git a/src/timeline/widgets/AudioItem.cpp b/src/timeline/widgets/AudioItem.cpp deleted file mode 100644 index 5d6431ee..00000000 --- a/src/timeline/widgets/AudioItem.cpp +++ /dev/null @@ -1,236 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include - -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" - -#include "timeline/widgets/AudioItem.h" - -constexpr int MaxWidth = 400; -constexpr int Height = 70; -constexpr int IconRadius = 22; -constexpr int IconDiameter = IconRadius * 2; -constexpr int HorizontalPadding = 12; -constexpr int TextPadding = 15; -constexpr int ActionIconRadius = IconRadius - 4; - -constexpr double VerticalPadding = Height - 2 * IconRadius; -constexpr double IconYCenter = Height / 2; -constexpr double IconXCenter = HorizontalPadding + IconRadius; - -void -AudioItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - playIcon_.addFile(":/icons/icons/ui/play-sign.png"); - pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png"); - - player_ = new QMediaPlayer; - player_->setMedia(QUrl(url_)); - player_->setVolume(100); - player_->setNotifyInterval(1000); - - connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) { - if (state == QMediaPlayer::StoppedState) { - state_ = AudioState::Play; - player_->setMedia(QUrl(url_)); - update(); - } - }); - - setFixedHeight(Height); -} - -AudioItem::AudioItem(const mtx::events::RoomEvent &event, QWidget *parent) - : QWidget(parent) - , url_{QUrl(QString::fromStdString(event.content.url))} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); -} - -AudioItem::AudioItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} - -QSize -AudioItem::sizeHint() const -{ - return QSize(MaxWidth, Height); -} - -void -AudioItem::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton) - return; - - auto point = event->pos(); - - // Click on the download icon. - if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter) - .contains(point)) { - if (state_ == AudioState::Play) { - state_ = AudioState::Pause; - player_->play(); - } else { - state_ = AudioState::Play; - player_->pause(); - } - - update(); - } else { - filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_); - - if (filenameToSave_.isEmpty()) - return; - - auto proxy = std::make_shared(); - connect(proxy.get(), &MediaProxy::fileDownloaded, this, &AudioItem::fileDownloaded); - - http::client()->download( - url_.toString().toStdString(), - [proxy = std::move(proxy), url = url_](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->info("failed to retrieve m.audio content: {}", - url.toString().toStdString()); - return; - } - - emit proxy->fileDownloaded(QByteArray(data.data(), data.size())); - }); - } -} - -void -AudioItem::fileDownloaded(const QByteArray &data) -{ - try { - QFile file(filenameToSave_); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("error while saving file: {}", e.what()); - } -} - -void -AudioItem::resizeEvent(QResizeEvent *event) -{ - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); -#else - const int computedWidth = - std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, - (double)MaxWidth); -#endif - resize(computedWidth, Height); - - event->accept(); -} - -void -AudioItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, backgroundColor_); - painter.drawPath(path); - - QPainterPath circle; - circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius); - - painter.setPen(Qt::NoPen); - painter.fillPath(circle, iconColor_); - painter.drawPath(circle); - - QIcon icon_; - if (state_ == AudioState::Play) - icon_ = playIcon_; - else - icon_ = pauseIcon_; - - icon_.paint(&painter, - QRect(IconXCenter - ActionIconRadius / 2, - IconYCenter - ActionIconRadius / 2, - ActionIconRadius, - ActionIconRadius), - Qt::AlignCenter, - QIcon::Normal); - - const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding; - const int textStartY = VerticalPadding + fm.ascent() / 2; - - // Draw the filename. - QString elidedText = fm.elidedText( - text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius); - - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY), elidedText); - - // Draw the filesize. - font.setWeight(QFont::Normal); - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_); -} diff --git a/src/timeline/widgets/AudioItem.h b/src/timeline/widgets/AudioItem.h deleted file mode 100644 index c32b7731..00000000 --- a/src/timeline/widgets/AudioItem.h +++ /dev/null @@ -1,104 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include - -class AudioItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - - Q_PROPERTY(QColor durationBackgroundColor WRITE setDurationBackgroundColor READ - durationBackgroundColor) - Q_PROPERTY(QColor durationForegroundColor WRITE setDurationForegroundColor READ - durationForegroundColor) - -public: - AudioItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - AudioItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - void setDurationBackgroundColor(const QColor &color) { durationBgColor_ = color; } - void setDurationForegroundColor(const QColor &color) { durationFgColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - - QColor durationBackgroundColor() const { return durationBgColor_; } - QColor durationForegroundColor() const { return durationFgColor_; } - -protected: - void paintEvent(QPaintEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void init(); - - enum class AudioState - { - Play, - Pause, - }; - - AudioState state_ = AudioState::Play; - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent event_; - - QMediaPlayer *player_; - - QIcon playIcon_; - QIcon pauseIcon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); - - QColor durationBgColor_ = QColor("black"); - QColor durationFgColor_ = QColor("blue"); -}; diff --git a/src/timeline/widgets/FileItem.cpp b/src/timeline/widgets/FileItem.cpp deleted file mode 100644 index 1a555d1c..00000000 --- a/src/timeline/widgets/FileItem.cpp +++ /dev/null @@ -1,221 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include - -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" - -#include "timeline/widgets/FileItem.h" - -constexpr int MaxWidth = 400; -constexpr int Height = 70; -constexpr int IconRadius = 22; -constexpr int IconDiameter = IconRadius * 2; -constexpr int HorizontalPadding = 12; -constexpr int TextPadding = 15; -constexpr int DownloadIconRadius = IconRadius - 4; - -constexpr double VerticalPadding = Height - 2 * IconRadius; -constexpr double IconYCenter = Height / 2; -constexpr double IconXCenter = HorizontalPadding + IconRadius; - -void -FileItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png"); - - setFixedHeight(Height); -} - -FileItem::FileItem(const mtx::events::RoomEvent &event, QWidget *parent) - : QWidget(parent) - , url_{QString::fromStdString(event.content.url)} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); -} - -FileItem::FileItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} - -void -FileItem::openUrl() -{ - if (url_.toString().isEmpty()) - return; - - auto urlToOpen = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); - - if (!QDesktopServices::openUrl(urlToOpen)) - nhlog::ui()->warn("Could not open url: {}", urlToOpen.toStdString()); -} - -QSize -FileItem::sizeHint() const -{ - return QSize(MaxWidth, Height); -} - -void -FileItem::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton) - return; - - auto point = event->pos(); - - // Click on the download icon. - if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter) - .contains(point)) { - filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_); - - if (filenameToSave_.isEmpty()) - return; - - auto proxy = std::make_shared(); - connect(proxy.get(), &MediaProxy::fileDownloaded, this, &FileItem::fileDownloaded); - - http::client()->download( - url_.toString().toStdString(), - [proxy = std::move(proxy), url = url_](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->warn("failed to retrieve m.file content: {}", - url.toString().toStdString()); - return; - } - - emit proxy->fileDownloaded(QByteArray(data.data(), data.size())); - }); - } else { - openUrl(); - } -} - -void -FileItem::fileDownloaded(const QByteArray &data) -{ - try { - QFile file(filenameToSave_); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } -} - -void -FileItem::resizeEvent(QResizeEvent *event) -{ - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); -#else - const int computedWidth = - std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, - (double)MaxWidth); -#endif - resize(computedWidth, Height); - - event->accept(); -} - -void -FileItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, backgroundColor_); - painter.drawPath(path); - - QPainterPath circle; - circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius); - - painter.setPen(Qt::NoPen); - painter.fillPath(circle, iconColor_); - painter.drawPath(circle); - - icon_.paint(&painter, - QRect(IconXCenter - DownloadIconRadius / 2, - IconYCenter - DownloadIconRadius / 2, - DownloadIconRadius, - DownloadIconRadius), - Qt::AlignCenter, - QIcon::Normal); - - const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding; - const int textStartY = VerticalPadding + fm.ascent() / 2; - - // Draw the filename. - QString elidedText = fm.elidedText( - text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius); - - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY), elidedText); - - // Draw the filesize. - font.setWeight(QFont::Normal); - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_); -} diff --git a/src/timeline/widgets/FileItem.h b/src/timeline/widgets/FileItem.h deleted file mode 100644 index d63cce88..00000000 --- a/src/timeline/widgets/FileItem.h +++ /dev/null @@ -1,79 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include - -class FileItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - FileItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - FileItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void openUrl(); - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent event_; - - QIcon icon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); -}; diff --git a/src/timeline/widgets/ImageItem.cpp b/src/timeline/widgets/ImageItem.cpp deleted file mode 100644 index 26c569d7..00000000 --- a/src/timeline/widgets/ImageItem.cpp +++ /dev/null @@ -1,267 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Config.h" -#include "ImageItem.h" -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" -#include "dialogs/ImageOverlay.h" - -void -ImageItem::downloadMedia(const QUrl &url) -{ - auto proxy = std::make_shared(); - connect(proxy.get(), &MediaProxy::imageDownloaded, this, &ImageItem::setImage); - - http::client()->download(url.toString().toStdString(), - [proxy = std::move(proxy), url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to retrieve image {}: {} {}", - url.toString().toStdString(), - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - QPixmap img; - img.loadFromData(QByteArray(data.data(), data.size())); - - emit proxy->imageDownloaded(img); - }); -} - -void -ImageItem::saveImage(const QString &filename, const QByteArray &data) -{ - try { - QFile file(filename); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } -} - -void -ImageItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - downloadMedia(url_); -} - -ImageItem::ImageItem(const mtx::events::RoomEvent &event, QWidget *parent) - : QWidget(parent) - , event_{event} -{ - url_ = QString::fromStdString(event.content.url); - text_ = QString::fromStdString(event.content.body); - - init(); -} - -ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - Q_UNUSED(size); - init(); -} - -void -ImageItem::openUrl() -{ - if (url_.toString().isEmpty()) - return; - - auto urlToOpen = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); - - if (!QDesktopServices::openUrl(urlToOpen)) - nhlog::ui()->warn("could not open url: {}", urlToOpen.toStdString()); -} - -QSize -ImageItem::sizeHint() const -{ - if (image_.isNull()) - return QSize(max_width_, bottom_height_); - - return QSize(width_, height_); -} - -void -ImageItem::setImage(const QPixmap &image) -{ - image_ = image; - scaled_image_ = utils::scaleDown(max_width_, max_height_, image_); - - width_ = scaled_image_.width(); - height_ = scaled_image_.height(); - - setFixedSize(width_, height_); - update(); -} - -void -ImageItem::mousePressEvent(QMouseEvent *event) -{ - if (!isInteractive_) { - event->accept(); - return; - } - - if (event->button() != Qt::LeftButton) - return; - - if (image_.isNull()) { - openUrl(); - return; - } - - if (textRegion_.contains(event->pos())) { - openUrl(); - } else { - auto imgDialog = new dialogs::ImageOverlay(image_); - imgDialog->show(); - connect(imgDialog, &dialogs::ImageOverlay::saving, this, &ImageItem::saveAs); - } -} - -void -ImageItem::resizeEvent(QResizeEvent *event) -{ - if (!image_) - return QWidget::resizeEvent(event); - - scaled_image_ = utils::scaleDown(max_width_, max_height_, image_); - - width_ = scaled_image_.width(); - height_ = scaled_image_.height(); - - setFixedSize(width_, height_); -} - -void -ImageItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - - QFontMetrics metrics(font); - const int fontHeight = metrics.height() + metrics.ascent(); - - if (image_.isNull()) { - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - setFixedSize(metrics.width(elidedText), fontHeight); -#else - setFixedSize(metrics.horizontalAdvance(elidedText), fontHeight); -#endif - painter.setFont(font); - painter.setPen(QPen(QColor(66, 133, 244))); - painter.drawText(QPoint(0, fontHeight / 2), elidedText); - - return; - } - - imageRegion_ = QRectF(0, 0, width_, height_); - - QPainterPath path; - path.addRoundedRect(imageRegion_, 5, 5); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, scaled_image_); - painter.drawPath(path); - - // Bottom text section - if (isInteractive_ && underMouse()) { - const int textBoxHeight = fontHeight / 2 + 6; - - textRegion_ = QRectF(0, height_ - textBoxHeight, width_, textBoxHeight); - - QPainterPath textPath; - textPath.addRoundedRect(textRegion_, 0, 0); - - painter.fillPath(textPath, QColor(40, 40, 40, 140)); - - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, width_ - 10); - - font.setWeight(QFont::Medium); - painter.setFont(font); - painter.setPen(QPen(QColor(Qt::white))); - - textRegion_.adjust(5, 0, 5, 0); - painter.drawText(textRegion_, Qt::AlignVCenter, elidedText); - } -} - -void -ImageItem::saveAs() -{ - auto filename = QFileDialog::getSaveFileName(this, tr("Save image"), text_); - - if (filename.isEmpty()) - return; - - const auto url = url_.toString().toStdString(); - - auto proxy = std::make_shared(); - connect(proxy.get(), &MediaProxy::imageSaved, this, &ImageItem::saveImage); - - http::client()->download( - url, - [proxy = std::move(proxy), filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - emit proxy->imageSaved(filename, QByteArray(data.data(), data.size())); - }); -} diff --git a/src/timeline/widgets/ImageItem.h b/src/timeline/widgets/ImageItem.h deleted file mode 100644 index 65bd962d..00000000 --- a/src/timeline/widgets/ImageItem.h +++ /dev/null @@ -1,104 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include - -#include - -namespace dialogs { -class ImageOverlay; -} - -class ImageItem : public QWidget -{ - Q_OBJECT -public: - ImageItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - ImageItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - -public slots: - //! Show a save as dialog for the image. - void saveAs(); - void setImage(const QPixmap &image); - void saveImage(const QString &filename, const QByteArray &data); - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - - //! Whether the user can interact with the displayed image. - bool isInteractive_ = true; - -private: - void init(); - void openUrl(); - void downloadMedia(const QUrl &url); - - int max_width_ = 500; - int max_height_ = 300; - - int width_; - int height_; - - QPixmap scaled_image_; - QPixmap image_; - - QUrl url_; - QString text_; - - int bottom_height_ = 30; - - QRectF textRegion_; - QRectF imageRegion_; - - mtx::events::RoomEvent event_; -}; - -class StickerItem : public ImageItem -{ - Q_OBJECT - -public: - StickerItem(const mtx::events::Sticker &event, QWidget *parent = nullptr) - : ImageItem{QString::fromStdString(event.content.url), - QString::fromStdString(event.content.body), - event.content.info.size, - parent} - , event_{event} - { - isInteractive_ = false; - setCursor(Qt::ArrowCursor); - setMouseTracking(false); - setAttribute(Qt::WA_Hover, false); - } - -private: - mtx::events::Sticker event_; -}; diff --git a/src/timeline/widgets/VideoItem.cpp b/src/timeline/widgets/VideoItem.cpp deleted file mode 100644 index 4b5dc022..00000000 --- a/src/timeline/widgets/VideoItem.cpp +++ /dev/null @@ -1,65 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include - -#include "Config.h" -#include "MatrixClient.h" -#include "Utils.h" -#include "timeline/widgets/VideoItem.h" - -void -VideoItem::init() -{ - url_ = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); -} - -VideoItem::VideoItem(const mtx::events::RoomEvent &event, QWidget *parent) - : QWidget(parent) - , url_{QString::fromStdString(event.content.url)} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); - - auto layout = new QVBoxLayout(this); - layout->setMargin(0); - layout->setSpacing(0); - - QString link = QString("%2").arg(url_.toString()).arg(text_); - - label_ = new QLabel(link, this); - label_->setMargin(0); - label_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - label_->setOpenExternalLinks(true); - - layout->addWidget(label_); -} - -VideoItem::VideoItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} diff --git a/src/timeline/widgets/VideoItem.h b/src/timeline/widgets/VideoItem.h deleted file mode 100644 index 26fa1c35..00000000 --- a/src/timeline/widgets/VideoItem.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include - -class VideoItem : public QWidget -{ - Q_OBJECT - -public: - VideoItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - VideoItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - -private: - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - - QLabel *label_; - - mtx::events::RoomEvent event_; -}; diff --git a/src/timeline2/DelegateChooser.cpp b/src/timeline2/DelegateChooser.cpp deleted file mode 100644 index 632a2a64..00000000 --- a/src/timeline2/DelegateChooser.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include "DelegateChooser.h" - -#include "Logging.h" - -// uses private API, which moved between versions -#include -#include - -QQmlComponent * -DelegateChoice::delegate() const -{ - return delegate_; -} - -void -DelegateChoice::setDelegate(QQmlComponent *delegate) -{ - if (delegate != delegate_) { - delegate_ = delegate; - emit delegateChanged(); - emit changed(); - } -} - -QVariant -DelegateChoice::roleValue() const -{ - return roleValue_; -} - -void -DelegateChoice::setRoleValue(const QVariant &value) -{ - if (value != roleValue_) { - roleValue_ = value; - emit roleValueChanged(); - emit changed(); - } -} - -QVariant -DelegateChooser::roleValue() const -{ - return roleValue_; -} - -void -DelegateChooser::setRoleValue(const QVariant &value) -{ - if (value != roleValue_) { - roleValue_ = value; - recalcChild(); - emit roleValueChanged(); - } -} - -QQmlListProperty -DelegateChooser::choices() -{ - return QQmlListProperty(this, - this, - &DelegateChooser::appendChoice, - &DelegateChooser::choiceCount, - &DelegateChooser::choice, - &DelegateChooser::clearChoices); -} - -void -DelegateChooser::appendChoice(QQmlListProperty *p, DelegateChoice *c) -{ - DelegateChooser *dc = static_cast(p->object); - dc->choices_.append(c); -} - -int -DelegateChooser::choiceCount(QQmlListProperty *p) -{ - return static_cast(p->object)->choices_.count(); -} -DelegateChoice * -DelegateChooser::choice(QQmlListProperty *p, int index) -{ - return static_cast(p->object)->choices_.at(index); -} -void -DelegateChooser::clearChoices(QQmlListProperty *p) -{ - static_cast(p->object)->choices_.clear(); -} - -void -DelegateChooser::recalcChild() -{ - for (const auto choice : choices_) { - auto choiceValue = choice->roleValue(); - if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) { - if (child) { - child->setParentItem(nullptr); - child = nullptr; - } - - choice->delegate()->create(incubator, QQmlEngine::contextForObject(this)); - return; - } - } -} - -void -DelegateChooser::componentComplete() -{ - QQuickItem::componentComplete(); - recalcChild(); -} - -void -DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) -{ - if (status == QQmlIncubator::Ready) { - chooser.child = dynamic_cast(object()); - if (chooser.child == nullptr) { - nhlog::ui()->error("Delegate has to be derived of Item!"); - return; - } - - chooser.child->setParentItem(&chooser); - connect(chooser.child, &QQuickItem::heightChanged, &chooser, [this]() { - chooser.setHeight(chooser.child->height()); - }); - chooser.setHeight(chooser.child->height()); - QQmlEngine::setObjectOwnership(chooser.child, - QQmlEngine::ObjectOwnership::JavaScriptOwnership); - - } else if (status == QQmlIncubator::Error) { - for (const auto &e : errors()) - nhlog::ui()->error("Error instantiating delegate: {}", - e.toString().toStdString()); - } -} diff --git a/src/timeline2/DelegateChooser.h b/src/timeline2/DelegateChooser.h deleted file mode 100644 index 68ebeb04..00000000 --- a/src/timeline2/DelegateChooser.h +++ /dev/null @@ -1,82 +0,0 @@ -// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt -// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent - -#pragma once - -#include -#include -#include -#include -#include -#include - -class QQmlAdaptorModel; - -class DelegateChoice : public QObject -{ - Q_OBJECT - Q_CLASSINFO("DefaultProperty", "delegate") - -public: - Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) - Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) - - QQmlComponent *delegate() const; - void setDelegate(QQmlComponent *delegate); - - QVariant roleValue() const; - void setRoleValue(const QVariant &value); - -signals: - void delegateChanged(); - void roleValueChanged(); - void changed(); - -private: - QVariant roleValue_; - QQmlComponent *delegate_ = nullptr; -}; - -class DelegateChooser : public QQuickItem -{ - Q_OBJECT - Q_CLASSINFO("DefaultProperty", "choices") - -public: - Q_PROPERTY(QQmlListProperty choices READ choices CONSTANT) - Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) - - QQmlListProperty choices(); - - QVariant roleValue() const; - void setRoleValue(const QVariant &value); - - void recalcChild(); - void componentComplete() override; - -signals: - void roleChanged(); - void roleValueChanged(); - -private: - struct DelegateIncubator : public QQmlIncubator - { - DelegateIncubator(DelegateChooser &parent) - : QQmlIncubator(QQmlIncubator::AsynchronousIfNested) - , chooser(parent) - {} - void statusChanged(QQmlIncubator::Status status) override; - - DelegateChooser &chooser; - }; - - QVariant roleValue_; - QList choices_; - QQuickItem *child = nullptr; - DelegateIncubator incubator{*this}; - - static void appendChoice(QQmlListProperty *, DelegateChoice *); - static int choiceCount(QQmlListProperty *); - static DelegateChoice *choice(QQmlListProperty *, int index); - static void clearChoices(QQmlListProperty *); -}; diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp deleted file mode 100644 index ab7d3d47..00000000 --- a/src/timeline2/TimelineModel.cpp +++ /dev/null @@ -1,1220 +0,0 @@ -#include "TimelineModel.h" - -#include -#include - -#include - -#include "ChatPage.h" -#include "Logging.h" -#include "MainWindow.h" -#include "Olm.h" -#include "TimelineViewManager.h" -#include "Utils.h" -#include "dialogs/RawMessage.h" - -Q_DECLARE_METATYPE(QModelIndex) - -namespace { -template -QString -eventId(const mtx::events::RoomEvent &event) -{ - return QString::fromStdString(event.event_id); -} -template -QString -roomId(const mtx::events::Event &event) -{ - return QString::fromStdString(event.room_id); -} -template -QString -senderId(const mtx::events::RoomEvent &event) -{ - return QString::fromStdString(event.sender); -} - -template -QDateTime -eventTimestamp(const mtx::events::RoomEvent &event) -{ - return QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); -} - -template -std::string -eventMsgType(const mtx::events::Event &) -{ - return ""; -} -template -auto -eventMsgType(const mtx::events::RoomEvent &e) -> decltype(e.content.msgtype) -{ - return e.content.msgtype; -} - -template -QString -eventBody(const mtx::events::Event &) -{ - return QString(""); -} -template -auto -eventBody(const mtx::events::RoomEvent &e) - -> std::enable_if_t::value, QString> -{ - return QString::fromStdString(e.content.body); -} - -template -QString -eventFormattedBody(const mtx::events::Event &) -{ - return QString(""); -} -template -auto -eventFormattedBody(const mtx::events::RoomEvent &e) - -> std::enable_if_t::value, QString> -{ - auto temp = e.content.formatted_body; - if (!temp.empty()) { - return QString::fromStdString(temp); - } else { - return QString::fromStdString(e.content.body).toHtmlEscaped().replace("\n", "
"); - } -} - -template -QString -eventUrl(const mtx::events::Event &) -{ - return ""; -} -template -auto -eventUrl(const mtx::events::RoomEvent &e) - -> std::enable_if_t::value, QString> -{ - return QString::fromStdString(e.content.url); -} - -template -QString -eventThumbnailUrl(const mtx::events::Event &) -{ - return ""; -} -template -auto -eventThumbnailUrl(const mtx::events::RoomEvent &e) - -> std::enable_if_t::value, - QString> -{ - return QString::fromStdString(e.content.info.thumbnail_url); -} - -template -QString -eventFilename(const mtx::events::Event &) -{ - return ""; -} -QString -eventFilename(const mtx::events::RoomEvent &e) -{ - // body may be the original filename - return QString::fromStdString(e.content.body); -} -QString -eventFilename(const mtx::events::RoomEvent &e) -{ - // body may be the original filename - return QString::fromStdString(e.content.body); -} -QString -eventFilename(const mtx::events::RoomEvent &e) -{ - // body may be the original filename - return QString::fromStdString(e.content.body); -} -QString -eventFilename(const mtx::events::RoomEvent &e) -{ - // body may be the original filename - if (!e.content.filename.empty()) - return QString::fromStdString(e.content.filename); - return QString::fromStdString(e.content.body); -} - -template -auto -eventFilesize(const mtx::events::RoomEvent &e) -> decltype(e.content.info.size) -{ - return e.content.info.size; -} - -template -int64_t -eventFilesize(const mtx::events::Event &) -{ - return 0; -} - -template -QString -eventMimeType(const mtx::events::Event &) -{ - return QString(); -} -template -auto -eventMimeType(const mtx::events::RoomEvent &e) - -> std::enable_if_t::value, QString> -{ - return QString::fromStdString(e.content.info.mimetype); -} - -template -QString -eventRelatesTo(const mtx::events::Event &) -{ - return QString(); -} -template -auto -eventRelatesTo(const mtx::events::RoomEvent &e) -> std::enable_if_t< - std::is_same::value, - QString> -{ - return QString::fromStdString(e.content.relates_to.in_reply_to.event_id); -} - -template -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &e) -{ - using mtx::events::EventType; - switch (e.type) { - case EventType::RoomKeyRequest: - return qml_mtx_events::EventType::KeyRequest; - case EventType::RoomAliases: - return qml_mtx_events::EventType::Aliases; - case EventType::RoomAvatar: - return qml_mtx_events::EventType::Avatar; - case EventType::RoomCanonicalAlias: - return qml_mtx_events::EventType::CanonicalAlias; - case EventType::RoomCreate: - return qml_mtx_events::EventType::Create; - case EventType::RoomEncrypted: - return qml_mtx_events::EventType::Encrypted; - case EventType::RoomEncryption: - return qml_mtx_events::EventType::Encryption; - case EventType::RoomGuestAccess: - return qml_mtx_events::EventType::GuestAccess; - case EventType::RoomHistoryVisibility: - return qml_mtx_events::EventType::HistoryVisibility; - case EventType::RoomJoinRules: - return qml_mtx_events::EventType::JoinRules; - case EventType::RoomMember: - return qml_mtx_events::EventType::Member; - case EventType::RoomMessage: - return qml_mtx_events::EventType::UnknownMessage; - case EventType::RoomName: - return qml_mtx_events::EventType::Name; - case EventType::RoomPowerLevels: - return qml_mtx_events::EventType::PowerLevels; - case EventType::RoomTopic: - return qml_mtx_events::EventType::Topic; - case EventType::RoomTombstone: - return qml_mtx_events::EventType::Tombstone; - case EventType::RoomRedaction: - return qml_mtx_events::EventType::Redaction; - case EventType::RoomPinnedEvents: - return qml_mtx_events::EventType::PinnedEvents; - case EventType::Sticker: - return qml_mtx_events::EventType::Sticker; - case EventType::Tag: - return qml_mtx_events::EventType::Tag; - case EventType::Unsupported: - default: - return qml_mtx_events::EventType::Unsupported; - } -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::AudioMessage; -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::EmoteMessage; -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::FileMessage; -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::ImageMessage; -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::NoticeMessage; -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::TextMessage; -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::VideoMessage; -} - -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::Redacted; -} -// ::EventType::Type toRoomEventType(const Event &e) { return -// ::EventType::LocationMessage; } - -template -uint64_t -eventHeight(const mtx::events::Event &) -{ - return -1; -} -template -auto -eventHeight(const mtx::events::RoomEvent &e) -> decltype(e.content.info.h) -{ - return e.content.info.h; -} -template -uint64_t -eventWidth(const mtx::events::Event &) -{ - return -1; -} -template -auto -eventWidth(const mtx::events::RoomEvent &e) -> decltype(e.content.info.w) -{ - return e.content.info.w; -} - -template -double -eventPropHeight(const mtx::events::RoomEvent &e) -{ - auto w = eventWidth(e); - if (w == 0) - w = 1; - return eventHeight(e) / (double)w; -} -} - -TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent) - : QAbstractListModel(parent) - , room_id_(room_id) - , manager_(manager) -{ - connect( - this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); - connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) { - pending.remove(txn_id); - failed.insert(txn_id); - int idx = idToIndex(txn_id); - if (idx < 0) { - nhlog::ui()->warn("Failed index out of range"); - return; - } - emit dataChanged(index(idx, 0), index(idx, 0)); - }); - connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) { - int idx = idToIndex(txn_id); - if (idx < 0) { - nhlog::ui()->warn("Sent index out of range"); - return; - } - eventOrder[idx] = event_id; - auto ev = events.value(txn_id); - ev = boost::apply_visitor( - [event_id](const auto &e) -> mtx::events::collections::TimelineEvents { - auto eventCopy = e; - eventCopy.event_id = event_id.toStdString(); - return eventCopy; - }, - ev); - events.remove(txn_id); - events.insert(event_id, ev); - - // mark our messages as read - readEvent(event_id.toStdString()); - - // ask to be notified for read receipts - cache::client()->addPendingReceipt(room_id_, event_id); - - emit dataChanged(index(idx, 0), index(idx, 0)); - }); - connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { - emit ChatPage::instance()->showNotification(msg); - }); -} - -QHash -TimelineModel::roleNames() const -{ - return { - {Section, "section"}, - {Type, "type"}, - {Body, "body"}, - {FormattedBody, "formattedBody"}, - {UserId, "userId"}, - {UserName, "userName"}, - {Timestamp, "timestamp"}, - {Url, "url"}, - {ThumbnailUrl, "thumbnailUrl"}, - {Filename, "filename"}, - {Filesize, "filesize"}, - {MimeType, "mimetype"}, - {Height, "height"}, - {Width, "width"}, - {ProportionalHeight, "proportionalHeight"}, - {Id, "id"}, - {State, "state"}, - {IsEncrypted, "isEncrypted"}, - {ReplyTo, "replyTo"}, - }; -} -int -TimelineModel::rowCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent); - return (int)this->eventOrder.size(); -} - -QVariant -TimelineModel::data(const QModelIndex &index, int role) const -{ - if (index.row() < 0 && index.row() >= (int)eventOrder.size()) - return QVariant(); - - QString id = eventOrder[index.row()]; - - mtx::events::collections::TimelineEvents event = events.value(id); - - if (auto e = boost::get>(&event)) { - event = decryptEvent(*e).event; - } - - switch (role) { - case Section: { - QDateTime date = boost::apply_visitor( - [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event); - date.setTime(QTime()); - - QString userId = - boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, event); - - for (int r = index.row() - 1; r > 0; r--) { - QDateTime prevDate = boost::apply_visitor( - [](const auto &e) -> QDateTime { return eventTimestamp(e); }, - events.value(eventOrder[r])); - prevDate.setTime(QTime()); - if (prevDate != date) - return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId); - - QString prevUserId = - boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, - events.value(eventOrder[r])); - if (userId != prevUserId) - break; - } - - return QString("%1").arg(userId); - } - case UserId: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { return senderId(e); }, event)); - case UserName: - return QVariant(displayName(boost::apply_visitor( - [](const auto &e) -> QString { return senderId(e); }, event))); - - case Timestamp: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event)); - case Type: - return QVariant(boost::apply_visitor( - [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, - event)); - case Body: - return QVariant(utils::replaceEmoji(boost::apply_visitor( - [](const auto &e) -> QString { return eventBody(e); }, event))); - case FormattedBody: - return QVariant( - utils::replaceEmoji( - boost::apply_visitor( - [](const auto &e) -> QString { return eventFormattedBody(e); }, event)) - .remove("") - .remove("")); - case Url: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { return eventUrl(e); }, event)); - case ThumbnailUrl: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { return eventThumbnailUrl(e); }, event)); - case Filename: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { return eventFilename(e); }, event)); - case Filesize: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { - return utils::humanReadableFileSize(eventFilesize(e)); - }, - event)); - case MimeType: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { return eventMimeType(e); }, event)); - case Height: - return QVariant(boost::apply_visitor( - [](const auto &e) -> qulonglong { return eventHeight(e); }, event)); - case Width: - return QVariant(boost::apply_visitor( - [](const auto &e) -> qulonglong { return eventWidth(e); }, event)); - case ProportionalHeight: - return QVariant(boost::apply_visitor( - [](const auto &e) -> double { return eventPropHeight(e); }, event)); - case Id: - return id; - case State: - // only show read receipts for messages not from us - if (boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, - event) - .toStdString() != http::client()->user_id().to_string()) - return qml_mtx_events::Empty; - else if (failed.contains(id)) - return qml_mtx_events::Failed; - else if (pending.contains(id)) - return qml_mtx_events::Sent; - else if (read.contains(id) || - cache::client()->readReceipts(id, room_id_).size() > 1) - return qml_mtx_events::Read; - else - return qml_mtx_events::Received; - case IsEncrypted: { - auto tempEvent = events[id]; - return boost::get>( - &tempEvent) != nullptr; - } - case ReplyTo: { - QString evId = boost::apply_visitor( - [](const auto &e) -> QString { return eventRelatesTo(e); }, event); - return QVariant(evId); - } - default: - return QVariant(); - } -} - -void -TimelineModel::addEvents(const mtx::responses::Timeline &timeline) -{ - if (isInitialSync) { - prev_batch_token_ = QString::fromStdString(timeline.prev_batch); - isInitialSync = false; - } - - if (timeline.events.empty()) - return; - - std::vector ids = internalAddEvents(timeline.events); - - if (ids.empty()) - return; - - beginInsertRows(QModelIndex(), - static_cast(this->eventOrder.size()), - static_cast(this->eventOrder.size() + ids.size() - 1)); - this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); - endInsertRows(); - - updateLastMessage(); -} - -void -TimelineModel::updateLastMessage() -{ - auto event = events.value(eventOrder.back()); - if (auto e = boost::get>(&event)) { - event = decryptEvent(*e).event; - } - - auto description = utils::getMessageDescription( - event, QString::fromStdString(http::client()->user_id().to_string()), room_id_); - emit manager_->updateRoomsLastMessage(room_id_, description); -} - -std::vector -TimelineModel::internalAddEvents( - const std::vector &timeline) -{ - std::vector ids; - for (const auto &e : timeline) { - QString id = - boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); - - if (this->events.contains(id)) { - this->events.insert(id, e); - int idx = idToIndex(id); - emit dataChanged(index(idx, 0), index(idx, 0)); - continue; - } - - if (auto redaction = - boost::get>(&e)) { - QString redacts = QString::fromStdString(redaction->redacts); - auto redacted = std::find(eventOrder.begin(), eventOrder.end(), redacts); - - if (redacted != eventOrder.end()) { - auto redactedEvent = boost::apply_visitor( - [](const auto &ev) - -> mtx::events::RoomEvent { - mtx::events::RoomEvent - replacement = {}; - replacement.event_id = ev.event_id; - replacement.room_id = ev.room_id; - replacement.sender = ev.sender; - replacement.origin_server_ts = ev.origin_server_ts; - replacement.type = ev.type; - return replacement; - }, - e); - events.insert(redacts, redactedEvent); - - int row = (int)std::distance(eventOrder.begin(), redacted); - emit dataChanged(index(row, 0), index(row, 0)); - } - - continue; // don't insert redaction into timeline - } - - this->events.insert(id, e); - ids.push_back(id); - } - return ids; -} - -void -TimelineModel::fetchHistory() -{ - if (paginationInProgress) { - nhlog::ui()->warn("Already loading older messages"); - return; - } - - paginationInProgress = true; - mtx::http::MessagesOpts opts; - opts.room_id = room_id_.toStdString(); - opts.from = prev_batch_token_.toStdString(); - - nhlog::ui()->info("Paginationg room {}", opts.room_id); - - http::client()->messages( - opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error("failed to call /messages ({}): {} - {}", - opts.room_id, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - paginationInProgress = false; - return; - } - - emit oldMessagesRetrieved(std::move(res)); - paginationInProgress = false; - }); -} - -void -TimelineModel::setCurrentIndex(int index) -{ - auto oldIndex = idToIndex(currentId); - currentId = indexToId(index); - emit currentIndexChanged(index); - - if (oldIndex < index && !pending.contains(currentId)) { - readEvent(currentId.toStdString()); - } -} - -void -TimelineModel::readEvent(const std::string &id) -{ - http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to read_event ({}, {})", - room_id_.toStdString(), - currentId.toStdString()); - } - }); -} - -void -TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) -{ - std::vector ids = internalAddEvents(msgs.chunk); - - if (!ids.empty()) { - beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1)); - this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); - endInsertRows(); - } - - prev_batch_token_ = QString::fromStdString(msgs.end); -} - -QColor -TimelineModel::userColor(QString id, QColor background) -{ - if (!userColors.contains(id)) - userColors.insert( - id, QColor(utils::generateContrastingHexColor(id, background.name()))); - return userColors.value(id); -} - -QString -TimelineModel::displayName(QString id) const -{ - return Cache::displayName(room_id_, id); -} - -QString -TimelineModel::avatarUrl(QString id) const -{ - return Cache::avatarUrl(room_id_, id); -} - -QString -TimelineModel::formatDateSeparator(QDate date) const -{ - auto now = QDateTime::currentDateTime(); - - QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); - - if (now.date().year() == date.year()) { - QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); - fmt = fmt.remove(rx); - } - - return date.toString(fmt); -} - -QString -TimelineModel::escapeEmoji(QString str) const -{ - return utils::replaceEmoji(str); -} - -void -TimelineModel::viewRawMessage(QString id) const -{ - std::string ev = utils::serialize_event(events.value(id)).dump(4); - auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); - Q_UNUSED(dialog); -} - -void - -TimelineModel::openUserProfile(QString userid) const -{ - MainWindow::instance()->openUserProfile(userid, room_id_); -} - -DecryptionResult -TimelineModel::decryptEvent(const mtx::events::EncryptedEvent &e) const -{ - MegolmSessionIndex index; - index.room_id = room_id_.toStdString(); - index.session_id = e.content.session_id; - index.sender_key = e.content.sender_key; - - mtx::events::RoomEvent dummy; - dummy.origin_server_ts = e.origin_server_ts; - dummy.event_id = e.event_id; - dummy.sender = e.sender; - dummy.content.body = - tr("-- Encrypted Event (No keys found for decryption) --", - "Placeholder, when the message was not decrypted yet or can't be decrypted") - .toStdString(); - - try { - if (!cache::client()->inboundMegolmSessionExists(index)) { - nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", - index.room_id, - index.session_id, - e.sender); - // TODO: request megolm session_id & session_key from the sender. - return {dummy, false}; - } - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); - dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --", - "Placeholder, when the message can't be decrypted, because " - "the DB access failed when trying to lookup the session.") - .toStdString(); - return {dummy, false}; - } - - std::string msg_str; - try { - auto session = cache::client()->getInboundMegolmSession(index); - auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (failed to retrieve megolm keys from db) --", - "Placeholder, when the message can't be decrypted, because the DB access " - "failed.") - .toStdString(); - return {dummy, false}; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (%1) --", - "Placeholder, when the message can't be decrypted. In this case, the Olm " - "decrytion returned an error, which is passed ad %1") - .arg(e.what()) - .toStdString(); - return {dummy, false}; - } - - // Add missing fields for the event. - json body = json::parse(msg_str); - body["event_id"] = e.event_id; - body["sender"] = e.sender; - body["origin_server_ts"] = e.origin_server_ts; - body["unsigned"] = e.unsigned_data; - - json event_array = json::array(); - event_array.push_back(body); - - std::vector temp_events; - mtx::responses::utils::parse_timeline_events(event_array, temp_events); - - if (temp_events.size() == 1) - return {temp_events.at(0), true}; - - dummy.content.body = - tr("-- Encrypted Event (Unknown event type) --", - "Placeholder, when the message was decrypted, but we couldn't parse it, because " - "Nheko/mtxclient don't support that event type yet") - .toStdString(); - return {dummy, false}; -} - -void -TimelineModel::replyAction(QString id) -{ - auto event = events.value(id); - RelatedInfo related = boost::apply_visitor( - [](const auto &ev) -> RelatedInfo { - RelatedInfo related_ = {}; - related_.quoted_user = QString::fromStdString(ev.sender); - related_.related_event = ev.event_id; - return related_; - }, - event); - related.type = mtx::events::getMessageType(boost::apply_visitor( - [](const auto &e) -> std::string { return eventMsgType(e); }, event)); - related.quoted_body = boost::apply_visitor( - [](const auto &e) -> QString { return eventFormattedBody(e); }, event); - related.quoted_body.remove(QRegularExpression( - ".*", QRegularExpression::DotMatchesEverythingOption)); - nhlog::ui()->debug("after replacement: {}", related.quoted_body.toStdString()); - related.room = room_id_; - - if (related.quoted_body.isEmpty()) - return; - - ChatPage::instance()->messageReply(related); -} - -void -TimelineModel::readReceiptsAction(QString id) const -{ - MainWindow::instance()->openReadReceiptsDialog(id); -} - -void -TimelineModel::redactEvent(QString id) -{ - if (!id.isEmpty()) - http::client()->redact_event( - room_id_.toStdString(), - id.toStdString(), - [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - emit redactionFailed( - tr("Message redaction failed: %1") - .arg(QString::fromStdString(err->matrix_error.error))); - return; - } - - emit eventRedacted(id); - }); -} - -int -TimelineModel::idToIndex(QString id) const -{ - if (id.isEmpty()) - return -1; - for (int i = 0; i < (int)eventOrder.size(); i++) - if (id == eventOrder[i]) - return i; - return -1; -} - -QString -TimelineModel::indexToId(int index) const -{ - if (index < 0 || index >= (int)eventOrder.size()) - return ""; - return eventOrder[index]; -} - -// Note: this will only be called for our messages -void -TimelineModel::markEventsAsRead(const std::vector &event_ids) -{ - for (const auto &id : event_ids) { - read.insert(id); - int idx = idToIndex(id); - if (idx < 0) { - nhlog::ui()->warn("Read index out of range"); - return; - } - emit dataChanged(index(idx, 0), index(idx, 0)); - } -} - -void -TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json content) -{ - const auto room_id = room_id_.toStdString(); - - using namespace mtx::events; - using namespace mtx::identifiers; - - json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; - - try { - // Check if we have already an outbound megolm session then we can use. - if (cache::client()->outboundMegolmSessionExists(room_id)) { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client()->send_room_message( - room_id, - txn_id, - data, - [this, txn_id](const mtx::responses::EventId &res, - mtx::http::RequestErr err) { - if (err) { - const int status_code = - static_cast(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(QString::fromStdString(txn_id)); - } - emit messageSent( - QString::fromStdString(txn_id), - QString::fromStdString(res.event_id.to_string())); - }); - return; - } - - nhlog::ui()->debug("creating new outbound megolm session"); - - // Create a new outbound megolm session. - auto outbound_session = olm::client()->init_outbound_group_session(); - const auto session_id = mtx::crypto::session_id(outbound_session.get()); - const auto session_key = mtx::crypto::session_key(outbound_session.get()); - - // TODO: needs to be moved in the lib. - auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, - {"room_id", room_id}, - {"session_id", session_id}, - {"session_key", session_key}}; - - // Saving the new megolm session. - // TODO: Maybe it's too early to save. - OutboundGroupSessionData session_data; - session_data.session_id = session_id; - session_data.session_key = session_key; - session_data.message_index = 0; // TODO Update me - cache::client()->saveOutboundMegolmSession( - room_id, session_data, std::move(outbound_session)); - - const auto members = cache::client()->roomMembers(room_id); - nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); - - auto keeper = - std::make_shared([megolm_payload, room_id, doc, txn_id, this]() { - try { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client() - ->send_room_message( - room_id, - txn_id, - data, - [this, txn_id](const mtx::responses::EventId &res, - mtx::http::RequestErr err) { - if (err) { - const int status_code = - static_cast(err->status_code); - nhlog::net()->warn( - "[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed( - QString::fromStdString(txn_id)); - } - emit messageSent( - QString::fromStdString(txn_id), - QString::fromStdString(res.event_id.to_string())); - }); - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to save megolm outbound session: {}", e.what()); - } - }); - - mtx::requests::QueryKeys req; - for (const auto &member : members) - req.device_keys[member] = {}; - - http::client()->query_keys( - req, - [keeper = std::move(keeper), megolm_payload, this]( - const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to query device keys: {} {}", - err->matrix_error.error, - static_cast(err->status_code)); - // TODO: Mark the event as failed. Communicate with the UI. - return; - } - - for (const auto &user : res.device_keys) { - // Mapping from a device_id with valid identity keys to the - // generated room_key event used for sharing the megolm session. - std::map room_key_msgs; - std::map deviceKeys; - - room_key_msgs.clear(); - deviceKeys.clear(); - - for (const auto &dev : user.second) { - const auto user_id = ::UserId(dev.second.user_id); - const auto device_id = DeviceId(dev.second.device_id); - - const auto device_keys = dev.second.keys; - const auto curveKey = "curve25519:" + device_id.get(); - const auto edKey = "ed25519:" + device_id.get(); - - if ((device_keys.find(curveKey) == device_keys.end()) || - (device_keys.find(edKey) == device_keys.end())) { - nhlog::net()->debug( - "ignoring malformed keys for device {}", - device_id.get()); - continue; - } - - DevicePublicKeys pks; - pks.ed25519 = device_keys.at(edKey); - pks.curve25519 = device_keys.at(curveKey); - - try { - if (!mtx::crypto::verify_identity_signature( - json(dev.second), device_id, user_id)) { - nhlog::crypto()->warn( - "failed to verify identity keys: {}", - json(dev.second).dump(2)); - continue; - } - } catch (const json::exception &e) { - nhlog::crypto()->warn( - "failed to parse device key json: {}", - e.what()); - continue; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->warn( - "failed to verify device key json: {}", - e.what()); - continue; - } - - auto room_key = olm::client() - ->create_room_key_event( - user_id, pks.ed25519, megolm_payload) - .dump(); - - room_key_msgs.emplace(device_id, room_key); - deviceKeys.emplace(device_id, pks); - } - - std::vector valid_devices; - valid_devices.reserve(room_key_msgs.size()); - for (auto const &d : room_key_msgs) { - valid_devices.push_back(d.first); - - nhlog::net()->info("{}", d.first); - nhlog::net()->info(" curve25519 {}", - deviceKeys.at(d.first).curve25519); - nhlog::net()->info(" ed25519 {}", - deviceKeys.at(d.first).ed25519); - } - - nhlog::net()->info( - "sending claim request for user {} with {} devices", - user.first, - valid_devices.size()); - - http::client()->claim_keys( - user.first, - valid_devices, - std::bind(&TimelineModel::handleClaimedKeys, - this, - keeper, - room_key_msgs, - deviceKeys, - user.first, - std::placeholders::_1, - std::placeholders::_2)); - - // TODO: Wait before sending the next batch of requests. - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - }); - - // TODO: Let the user know about the errors. - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } -} - -void -TimelineModel::handleClaimedKeys(std::shared_ptr keeper, - const std::map &room_keys, - const std::map &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err) -{ - if (err) { - nhlog::net()->warn("claim keys error: {} {} {}", - err->matrix_error.error, - err->parse_error, - static_cast(err->status_code)); - return; - } - - nhlog::net()->debug("claimed keys for {}", user_id); - - if (res.one_time_keys.size() == 0) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - auto retrieved_devices = res.one_time_keys.at(user_id); - - // Payload with all the to_device message to be sent. - json body; - body["messages"][user_id] = json::object(); - - for (const auto &rd : retrieved_devices) { - const auto device_id = rd.first; - nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); - - // TODO: Verify signatures - auto otk = rd.second.begin()->at("key"); - - if (pks.find(device_id) == pks.end()) { - nhlog::net()->critical("couldn't find public key for device: {}", - device_id); - continue; - } - - auto id_key = pks.at(device_id).curve25519; - auto s = olm::client()->create_outbound_session(id_key, otk); - - if (room_keys.find(device_id) == room_keys.end()) { - nhlog::net()->critical("couldn't find m.room_key for device: {}", - device_id); - continue; - } - - auto device_msg = olm::client()->create_olm_encrypted_content( - s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); - - try { - cache::client()->saveOlmSession(id_key, std::move(s)); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to pickle outbound olm session: {}", - e.what()); - } - - body["messages"][user_id][device_id] = device_msg; - } - - nhlog::net()->info("send_to_device: {}", user_id); - - http::client()->send_to_device( - "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - - (void)keeper; - }); -} diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h deleted file mode 100644 index 31e41315..00000000 --- a/src/timeline2/TimelineModel.h +++ /dev/null @@ -1,258 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include - -#include "Cache.h" -#include "Logging.h" -#include "MatrixClient.h" - -namespace qml_mtx_events { -Q_NAMESPACE - -enum EventType -{ - // Unsupported event - Unsupported, - /// m.room_key_request - KeyRequest, - /// m.room.aliases - Aliases, - /// m.room.avatar - Avatar, - /// m.room.canonical_alias - CanonicalAlias, - /// m.room.create - Create, - /// m.room.encrypted. - Encrypted, - /// m.room.encryption. - Encryption, - /// m.room.guest_access - GuestAccess, - /// m.room.history_visibility - HistoryVisibility, - /// m.room.join_rules - JoinRules, - /// m.room.member - Member, - /// m.room.name - Name, - /// m.room.power_levels - PowerLevels, - /// m.room.tombstone - Tombstone, - /// m.room.topic - Topic, - /// m.room.redaction - Redaction, - /// m.room.pinned_events - PinnedEvents, - // m.sticker - Sticker, - // m.tag - Tag, - /// m.room.message - AudioMessage, - EmoteMessage, - FileMessage, - ImageMessage, - LocationMessage, - NoticeMessage, - TextMessage, - VideoMessage, - Redacted, - UnknownMessage, -}; -Q_ENUM_NS(EventType) - -enum EventState -{ - //! The plaintext message was received by the server. - Received, - //! At least one of the participants has read the message. - Read, - //! The client sent the message. Not yet received. - Sent, - //! When the message is loaded from cache or backfill. - Empty, - //! When the message failed to send - Failed, -}; -Q_ENUM_NS(EventState) -} - -class StateKeeper -{ -public: - StateKeeper(std::function &&fn) - : fn_(std::move(fn)) - {} - - ~StateKeeper() { fn_(); } - -private: - std::function fn_; -}; - -struct DecryptionResult -{ - //! The decrypted content as a normal plaintext event. - mtx::events::collections::TimelineEvents event; - //! Whether or not the decryption was successful. - bool isDecrypted = false; -}; - -class TimelineViewManager; - -class TimelineModel : public QAbstractListModel -{ - Q_OBJECT - Q_PROPERTY( - int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) - -public: - explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = 0); - - enum Roles - { - Section, - Type, - Body, - FormattedBody, - UserId, - UserName, - Timestamp, - Url, - ThumbnailUrl, - Filename, - Filesize, - MimeType, - Height, - Width, - ProportionalHeight, - Id, - State, - IsEncrypted, - ReplyTo, - }; - - QHash roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - Q_INVOKABLE QColor userColor(QString id, QColor background); - Q_INVOKABLE QString displayName(QString id) const; - Q_INVOKABLE QString avatarUrl(QString id) const; - Q_INVOKABLE QString formatDateSeparator(QDate date) const; - - Q_INVOKABLE QString escapeEmoji(QString str) const; - Q_INVOKABLE void viewRawMessage(QString id) const; - Q_INVOKABLE void openUserProfile(QString userid) const; - Q_INVOKABLE void replyAction(QString id); - Q_INVOKABLE void readReceiptsAction(QString id) const; - Q_INVOKABLE void redactEvent(QString id); - Q_INVOKABLE int idToIndex(QString id) const; - Q_INVOKABLE QString indexToId(int index) const; - - void addEvents(const mtx::responses::Timeline &events); - template - void sendMessage(const T &msg); - -public slots: - void fetchHistory(); - void setCurrentIndex(int index); - int currentIndex() const { return idToIndex(currentId); } - void markEventsAsRead(const std::vector &event_ids); - -private slots: - // Add old events at the top of the timeline. - void addBackwardsEvents(const mtx::responses::Messages &msgs); - -signals: - void oldMessagesRetrieved(const mtx::responses::Messages &res); - void messageFailed(QString txn_id); - void messageSent(QString txn_id, QString event_id); - void currentIndexChanged(int index); - void redactionFailed(QString id); - void eventRedacted(QString id); - -private: - DecryptionResult decryptEvent( - const mtx::events::EncryptedEvent &e) const; - std::vector internalAddEvents( - const std::vector &timeline); - void sendEncryptedMessage(const std::string &txn_id, nlohmann::json content); - void handleClaimedKeys(std::shared_ptr keeper, - const std::map &room_key, - const std::map &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err); - void updateLastMessage(); - void readEvent(const std::string &id); - - QHash events; - QSet pending, failed, read; - std::vector eventOrder; - - QString room_id_; - QString prev_batch_token_; - - bool isInitialSync = true; - bool paginationInProgress = false; - - QHash userColors; - QString currentId; - - TimelineViewManager *manager_; -}; - -template -void -TimelineModel::sendMessage(const T &msg) -{ - auto txn_id = http::client()->generate_txn_id(); - mtx::events::RoomEvent msgCopy = {}; - msgCopy.content = msg; - msgCopy.type = mtx::events::EventType::RoomMessage; - msgCopy.event_id = txn_id; - msgCopy.sender = http::client()->user_id().to_string(); - msgCopy.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - internalAddEvents({msgCopy}); - - QString txn_id_qstr = QString::fromStdString(txn_id); - beginInsertRows(QModelIndex(), - static_cast(this->eventOrder.size()), - static_cast(this->eventOrder.size())); - pending.insert(txn_id_qstr); - this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); - endInsertRows(); - updateLastMessage(); - - if (cache::client()->isRoomEncrypted(room_id_.toStdString())) - sendEncryptedMessage(txn_id, nlohmann::json(msg)); - else - http::client()->send_room_message( - room_id_.toStdString(), - txn_id, - msg, - [this, txn_id, txn_id_qstr](const mtx::responses::EventId &res, - mtx::http::RequestErr err) { - if (err) { - const int status_code = static_cast(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(txn_id_qstr); - } - emit messageSent(txn_id_qstr, - QString::fromStdString(res.event_id.to_string())); - }); -} diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp deleted file mode 100644 index d733ad90..00000000 --- a/src/timeline2/TimelineViewManager.cpp +++ /dev/null @@ -1,400 +0,0 @@ -#include "TimelineViewManager.h" - -#include -#include -#include -#include -#include -#include - -#include "ChatPage.h" -#include "ColorImageProvider.h" -#include "DelegateChooser.h" -#include "Logging.h" -#include "MxcImageProvider.h" -#include "UserSettingsPage.h" -#include "dialogs/ImageOverlay.h" - -void -TimelineViewManager::updateColorPalette() -{ - UserSettings settings; - if (settings.theme() == "light") { - QPalette lightActive(/*windowText*/ QColor("#333"), - /*button*/ QColor("#333"), - /*light*/ QColor(), - /*dark*/ QColor(220, 220, 220, 120), - /*mid*/ QColor(), - /*text*/ QColor("#333"), - /*bright_text*/ QColor(), - /*base*/ QColor("white"), - /*window*/ QColor("white")); - view->rootContext()->setContextProperty("currentActivePalette", lightActive); - view->rootContext()->setContextProperty("currentInactivePalette", lightActive); - } else if (settings.theme() == "dark") { - QPalette darkActive(/*windowText*/ QColor("#caccd1"), - /*button*/ QColor("#caccd1"), - /*light*/ QColor(), - /*dark*/ QColor(45, 49, 57, 120), - /*mid*/ QColor(), - /*text*/ QColor("#caccd1"), - /*bright_text*/ QColor(), - /*base*/ QColor("#202228"), - /*window*/ QColor("#202228")); - darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9")); - view->rootContext()->setContextProperty("currentActivePalette", darkActive); - view->rootContext()->setContextProperty("currentInactivePalette", darkActive); - } else { - view->rootContext()->setContextProperty("currentActivePalette", QPalette()); - view->rootContext()->setContextProperty("currentInactivePalette", nullptr); - } -} - -TimelineViewManager::TimelineViewManager(QWidget *parent) - : imgProvider(new MxcImageProvider()) - , colorImgProvider(new ColorImageProvider()) -{ - qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, - "com.github.nheko", - 1, - 0, - "MtxEvent", - "Can't instantiate enum!"); - qmlRegisterType("com.github.nheko", 1, 0, "DelegateChoice"); - qmlRegisterType("com.github.nheko", 1, 0, "DelegateChooser"); - -#ifdef USE_QUICK_VIEW - view = new QQuickView(); - container = QWidget::createWindowContainer(view, parent); -#else - view = new QQuickWidget(parent); - container = view; - view->setResizeMode(QQuickWidget::SizeRootObjectToView); - container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - - connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { - nhlog::ui()->debug("Status changed to {}", status); - }); -#endif - container->setMinimumSize(200, 200); - view->rootContext()->setContextProperty("timelineManager", this); - updateColorPalette(); - view->engine()->addImageProvider("MxcImage", imgProvider); - view->engine()->addImageProvider("colorimage", colorImgProvider); - view->setSource(QUrl("qrc:///qml/TimelineView.qml")); - - connect(dynamic_cast(parent), - &ChatPage::themeChanged, - this, - &TimelineViewManager::updateColorPalette); -} - -void -TimelineViewManager::sync(const mtx::responses::Rooms &rooms) -{ - for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { - // addRoom will only add the room, if it doesn't exist - addRoom(QString::fromStdString(it->first)); - models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline); - } -} - -void -TimelineViewManager::addRoom(const QString &room_id) -{ - if (!models.contains(room_id)) - models.insert(room_id, - QSharedPointer(new TimelineModel(this, room_id))); -} - -void -TimelineViewManager::setHistoryView(const QString &room_id) -{ - nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); - - auto room = models.find(room_id); - if (room != models.end()) { - timeline_ = room.value().data(); - emit activeTimelineChanged(timeline_); - nhlog::ui()->info("Activated room {}", room_id.toStdString()); - } -} - -void -TimelineViewManager::openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const -{ - QQuickImageResponse *imgResponse = - imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize()); - connect(imgResponse, - &QQuickImageResponse::finished, - this, - [this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() { - if (!imgResponse->errorString().isEmpty()) { - nhlog::ui()->error("Error when retrieving image for overlay: {}", - imgResponse->errorString().toStdString()); - return; - } - auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); - - auto imgDialog = new dialogs::ImageOverlay(pixmap); - imgDialog->show(); - connect(imgDialog, - &dialogs::ImageOverlay::saving, - this, - [this, mxcUrl, originalFilename, mimeType, eventType]() { - saveMedia(mxcUrl, originalFilename, mimeType, eventType); - }); - }); -} - -void -TimelineViewManager::saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const -{ - QString dialogTitle; - if (eventType == qml_mtx_events::EventType::ImageMessage) { - dialogTitle = tr("Save image"); - } else if (eventType == qml_mtx_events::EventType::VideoMessage) { - dialogTitle = tr("Save video"); - } else if (eventType == qml_mtx_events::EventType::AudioMessage) { - dialogTitle = tr("Save audio"); - } else { - dialogTitle = tr("Save file"); - } - - QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); - - auto filename = - QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString); - - if (filename.isEmpty()) - return; - - const auto url = mxcUrl.toStdString(); - - http::client()->download( - url, - [filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - try { - QFile file(filename); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(QByteArray(data.data(), data.size())); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - }); -} - -void -TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType) -{ - // If the message is a link to a non mxcUrl, don't download it - if (!mxcUrl.startsWith("mxc://")) { - emit mediaCached(mxcUrl, mxcUrl); - return; - } - - QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); - - const auto url = mxcUrl.toStdString(); - QFileInfo filename(QString("%1/media_cache/%2.%3") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString(mxcUrl).remove("mxc://")) - .arg(suffix)); - if (QDir::cleanPath(filename.path()) != filename.path()) { - nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); - return; - } - - QDir().mkpath(filename.path()); - - if (filename.isReadable()) { - emit mediaCached(mxcUrl, filename.filePath()); - return; - } - - http::client()->download( - url, - [this, mxcUrl, filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - try { - QFile file(filename.filePath()); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(QByteArray(data.data(), data.size())); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - - emit mediaCached(mxcUrl, filename.filePath()); - }); -} - -void -TimelineViewManager::updateReadReceipts(const QString &room_id, - const std::vector &event_ids) -{ - auto room = models.find(room_id); - if (room != models.end()) { - room.value()->markEventsAsRead(event_ids); - } -} - -void -TimelineViewManager::initWithMessages(const std::map &msgs) -{ - for (const auto &e : msgs) { - addRoom(e.first); - - models.value(e.first)->addEvents(e.second); - } -} - -void -TimelineViewManager::queueTextMessage(const QString &msg) -{ - mtx::events::msg::Text text = {}; - text.body = msg.trimmed().toStdString(); - text.format = "org.matrix.custom.html"; - text.formatted_body = utils::markdownToHtml(msg).toStdString(); - - if (timeline_) - timeline_->sendMessage(text); -} - -void -TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) -{ - mtx::events::msg::Text text = {}; - - QString body; - bool firstLine = true; - for (const auto &line : related.quoted_body.split("\n")) { - if (firstLine) { - firstLine = false; - body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line); - } else { - body = QString("%1\n> %2\n").arg(body).arg(line); - } - } - - text.body = QString("%1\n%2").arg(body).arg(reply).toStdString(); - text.format = "org.matrix.custom.html"; - text.formatted_body = - utils::getFormattedQuoteBody(related, utils::markdownToHtml(reply)).toStdString(); - text.relates_to.in_reply_to.event_id = related.related_event; - - if (timeline_) - timeline_->sendMessage(text); -} - -void -TimelineViewManager::queueEmoteMessage(const QString &msg) -{ - auto html = utils::markdownToHtml(msg); - - mtx::events::msg::Emote emote; - emote.body = msg.trimmed().toStdString(); - - if (html != msg.trimmed().toHtmlEscaped()) - emote.formatted_body = html.toStdString(); - - if (timeline_) - timeline_->sendMessage(emote); -} - -void -TimelineViewManager::queueImageMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize, - const QSize &dimensions) -{ - mtx::events::msg::Image image; - image.info.mimetype = mime.toStdString(); - image.info.size = dsize; - image.body = filename.toStdString(); - image.url = url.toStdString(); - image.info.h = dimensions.height(); - image.info.w = dimensions.width(); - models.value(roomid)->sendMessage(image); -} - -void -TimelineViewManager::queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::File file; - file.info.mimetype = mime.toStdString(); - file.info.size = dsize; - file.body = filename.toStdString(); - file.url = url.toStdString(); - models.value(roomid)->sendMessage(file); -} - -void -TimelineViewManager::queueAudioMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::Audio audio; - audio.info.mimetype = mime.toStdString(); - audio.info.size = dsize; - audio.body = filename.toStdString(); - audio.url = url.toStdString(); - models.value(roomid)->sendMessage(audio); -} - -void -TimelineViewManager::queueVideoMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::Video video; - video.info.mimetype = mime.toStdString(); - video.info.size = dsize; - video.body = filename.toStdString(); - video.url = url.toStdString(); - models.value(roomid)->sendMessage(video); -} diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h deleted file mode 100644 index 691c8ddb..00000000 --- a/src/timeline2/TimelineViewManager.h +++ /dev/null @@ -1,117 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#include "Cache.h" -#include "Logging.h" -#include "TimelineModel.h" -#include "Utils.h" - -// temporary for stubs -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - -class MxcImageProvider; -class ColorImageProvider; - -class TimelineViewManager : public QObject -{ - Q_OBJECT - - Q_PROPERTY( - TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) - -public: - TimelineViewManager(QWidget *parent = 0); - QWidget *getWidget() const { return container; } - - void sync(const mtx::responses::Rooms &rooms); - void addRoom(const QString &room_id); - - void clearAll() { models.clear(); } - - Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } - void openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const; - void saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const; - Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType); - // Qml can only pass enum as int - Q_INVOKABLE void openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - int eventType) const - { - openImageOverlay( - mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); - } - Q_INVOKABLE void saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - int eventType) const - { - saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); - } - -signals: - void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(QString roomid, const DescInfo &info); - void activeTimelineChanged(TimelineModel *timeline); - void mediaCached(QString mxcUrl, QString cacheUrl); - -public slots: - void updateReadReceipts(const QString &room_id, const std::vector &event_ids); - void initWithMessages(const std::map &msgs); - - void setHistoryView(const QString &room_id); - void updateColorPalette(); - - void queueTextMessage(const QString &msg); - void queueReplyMessage(const QString &reply, const RelatedInfo &related); - void queueEmoteMessage(const QString &msg); - void queueImageMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize, - const QSize &dimensions); - void queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize); - void queueAudioMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize); - void queueVideoMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize); - -private: -#ifdef USE_QUICK_VIEW - QQuickView *view; -#else - QQuickWidget *view; -#endif - QWidget *container; - TimelineModel *timeline_ = nullptr; - MxcImageProvider *imgProvider; - ColorImageProvider *colorImgProvider; - - QHash> models; -}; - -#pragma GCC diagnostic pop -- cgit 1.5.1 From 5fc1f3bd678cca690268eafbb7e4595657a6e133 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 15 Dec 2019 02:56:04 +0100 Subject: Reduce overhead of Cache.h --- .gitignore | 1 + CMakeLists.txt | 2 +- src/AvatarProvider.cpp | 14 +- src/Cache.cpp | 561 +++++++++++++++++++++++++++++-- src/Cache.h | 744 +++++++++++++++-------------------------- src/Cache_p.h | 494 +++++++++++++++++++++++++++ src/ChatPage.cpp | 87 +++-- src/CommunitiesList.cpp | 4 +- src/MainWindow.cpp | 6 +- src/MxcImageProvider.cpp | 10 +- src/Olm.cpp | 27 +- src/QuickSwitcher.cpp | 3 +- src/TextInputWidget.cpp | 4 +- src/UserSettingsPage.cpp | 4 +- src/Utils.cpp | 2 +- src/Utils.h | 2 +- src/dialogs/MemberList.cpp | 4 +- src/dialogs/ReadReceipts.cpp | 2 +- src/dialogs/RoomSettings.cpp | 12 +- src/dialogs/RoomSettings.h | 4 +- src/dialogs/UserProfile.cpp | 8 +- src/popups/PopupItem.cpp | 6 +- src/popups/PopupItem.h | 3 +- src/popups/ReplyPopup.cpp | 1 + src/popups/ReplyPopup.h | 9 +- src/popups/SuggestionsPopup.h | 7 +- src/popups/UserMentions.cpp | 2 +- src/timeline/TimelineModel.cpp | 23 +- 28 files changed, 1403 insertions(+), 643 deletions(-) create mode 100644 src/Cache_p.h (limited to 'src/dialogs/MemberList.cpp') diff --git a/.gitignore b/.gitignore index 2d772e58..6d178679 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ moc_*.cpp qrc_*.cpp ui_*.h *-build-* +/.clangd/ # QtCreator diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e86ee14..5c68d614 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -357,7 +357,7 @@ qt5_wrap_cpp(MOC_HEADERS src/notifications/Manager.h src/AvatarProvider.h - src/Cache.h + src/Cache_p.h src/ChatPage.h src/CommunitiesListItem.h src/CommunitiesList.h diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp index 68b6901e..1587a9a1 100644 --- a/src/AvatarProvider.cpp +++ b/src/AvatarProvider.cpp @@ -35,9 +35,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca const auto cacheKey = avatarUrl + "_size_" + size; - if (!cache::client()) - return; - if (avatarUrl.isEmpty()) return; @@ -47,7 +44,7 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca return; } - auto data = cache::client()->image(avatarUrl); + auto data = cache::image(avatarUrl); if (!data.isNull()) { pixmap.loadFromData(data); avatar_cache.insert(cacheKey, pixmap); @@ -82,7 +79,7 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca return; } - cache::client()->saveImage(opts.mxc_url, res); + cache::saveImage(opts.mxc_url, res); emit proxy->avatarDownloaded(QByteArray(res.data(), res.size())); }); @@ -95,12 +92,7 @@ resolve(const QString &room_id, QObject *receiver, AvatarCallback callback) { - const auto key = QString("%1 %2").arg(room_id).arg(user_id); - const auto avatarUrl = Cache::avatarUrl(room_id, user_id); - const auto cacheKey = avatarUrl + "_size_" + size; - - if (!Cache::AvatarUrls.contains(key) || !cache::client()) - return; + const auto avatarUrl = cache::avatarUrl(room_id, user_id); resolve(avatarUrl, size, receiver, callback); } diff --git a/src/Cache.cpp b/src/Cache.cpp index ee53ffbd..79425fa1 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -30,6 +30,7 @@ #include #include "Cache.h" +#include "Cache_p.h" #include "Utils.h" //! Should be changed when a breaking change occurs in the cache format. @@ -89,29 +90,20 @@ namespace { std::unique_ptr instance_ = nullptr; } -namespace cache { -void -init(const QString &user_id) +int +numeric_key_comparison(const MDB_val *a, const MDB_val *b) { - qRegisterMetaType(); - qRegisterMetaType>(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType>(); - qRegisterMetaType>(); - qRegisterMetaType>(); - qRegisterMetaType>(); + auto lhs = std::stoull(std::string((char *)a->mv_data, a->mv_size)); + auto rhs = std::stoull(std::string((char *)b->mv_data, b->mv_size)); - instance_ = std::make_unique(user_id); -} + if (lhs < rhs) + return 1; + else if (lhs == rhs) + return 0; -Cache * -client() -{ - return instance_.get(); + return -1; } -} // namespace cache + Cache::Cache(const QString &userId, QObject *parent) : QObject{parent} @@ -2321,20 +2313,6 @@ from_json(const json &j, RoomInfo &info) info.tags = j.at("tags").get>(); } -int -numeric_key_comparison(const MDB_val *a, const MDB_val *b) -{ - auto lhs = std::stoull(std::string((char *)a->mv_data, a->mv_size)); - auto rhs = std::stoull(std::string((char *)b->mv_data, b->mv_size)); - - if (lhs < rhs) - return 1; - else if (lhs == rhs) - return 0; - - return -1; -} - void to_json(json &j, const ReadReceiptKey &key) { @@ -2407,3 +2385,520 @@ from_json(const nlohmann::json &obj, MegolmSessionIndex &msg) msg.session_id = obj.at("session_id"); msg.sender_key = obj.at("sender_key"); } + +namespace cache { +void +init(const QString &user_id) +{ + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType>(); + qRegisterMetaType>(); + qRegisterMetaType>(); + + instance_ = std::make_unique(user_id); +} + +Cache * +client() +{ + return instance_.get(); +} + +std::string +displayName(const std::string &room_id, const std::string &user_id) +{ + return instance_->displayName(room_id, user_id); +} + +QString +displayName(const QString &room_id, const QString &user_id) +{ + return instance_->displayName(room_id, user_id); +} +QString +avatarUrl(const QString &room_id, const QString &user_id) +{ + return instance_->avatarUrl(room_id, user_id); +} + +QString +userColor(const QString &user_id) +{ + return instance_->userColor(user_id); +} + +void +removeDisplayName(const QString &room_id, const QString &user_id) +{ + instance_->removeDisplayName(room_id, user_id); +} +void +removeAvatarUrl(const QString &room_id, const QString &user_id) +{ + instance_->removeAvatarUrl(room_id, user_id); +} +void +removeUserColor(const QString &user_id) +{ + instance_->removeUserColor(user_id); +} + +void +insertDisplayName(const QString &room_id, const QString &user_id, const QString &display_name) +{ + instance_->insertDisplayName(room_id, user_id, display_name); +} +void +insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url) +{ + instance_->insertAvatarUrl(room_id, user_id, avatar_url); +} +void +insertUserColor(const QString &user_id, const QString &color_name) +{ + instance_->insertUserColor(user_id, color_name); +} + +void +clearUserColors() +{ + instance_->clearUserColors(); +} + +//! Load saved data for the display names & avatars. +void +populateMembers() +{ + instance_->populateMembers(); +} + +std::vector +joinedRooms() +{ + return instance_->joinedRooms(); +} + +QMap +roomInfo(bool withInvites) +{ + return instance_->roomInfo(withInvites); +} +std::map +invites() +{ + return instance_->invites(); +} + +QString +getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) +{ + return instance_->getRoomName(txn, statesdb, membersdb); +} +mtx::events::state::JoinRule +getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + return instance_->getRoomJoinRule(txn, statesdb); +} +bool +getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + return instance_->getRoomGuestAccess(txn, statesdb); +} +QString +getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + return instance_->getRoomTopic(txn, statesdb); +} +QString +getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb, const QString &room_id) +{ + return instance_->getRoomAvatarUrl(txn, statesdb, membersdb, room_id); +} + +QString +getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + return instance_->getRoomVersion(txn, statesdb); +} + +std::vector +getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) +{ + return instance_->getMembers(room_id, startIndex, len); +} + +void +saveState(const mtx::responses::Sync &res) +{ + instance_->saveState(res); +} +bool +isInitialized() +{ + return instance_->isInitialized(); +} + +std::string +nextBatchToken() +{ + return instance_->nextBatchToken(); +} + +void +deleteData() +{ + instance_->deleteData(); +} + +void +removeInvite(lmdb::txn &txn, const std::string &room_id) +{ + instance_->removeInvite(txn, room_id); +} +void +removeInvite(const std::string &room_id) +{ + instance_->removeInvite(room_id); +} +void +removeRoom(lmdb::txn &txn, const std::string &roomid) +{ + instance_->removeRoom(txn, roomid); +} +void +removeRoom(const std::string &roomid) +{ + instance_->removeRoom(roomid); +} +void +removeRoom(const QString &roomid) +{ + instance_->removeRoom(roomid.toStdString()); +} +void +setup() +{ + instance_->setup(); +} + +bool +isFormatValid() +{ + return instance_->isFormatValid(); +} +void +setCurrentFormat() +{ + instance_->setCurrentFormat(); +} + +std::map +roomMessages() +{ + return instance_->roomMessages(); +} + +QMap +getTimelineMentions() +{ + return instance_->getTimelineMentions(); +} + +//! Retrieve all the user ids from a room. +std::vector +roomMembers(const std::string &room_id) +{ + return instance_->roomMembers(room_id); +} + +//! Check if the given user has power leve greater than than +//! lowest power level of the given events. +bool +hasEnoughPowerLevel(const std::vector &eventTypes, + const std::string &room_id, + const std::string &user_id) +{ + return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id); +} + +//! Retrieves the saved room avatar. +QImage +getRoomAvatar(const QString &id) +{ + return instance_->getRoomAvatar(id); +} +QImage +getRoomAvatar(const std::string &id) +{ + return instance_->getRoomAvatar(id); +} + +void +updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts) +{ + instance_->updateReadReceipt(txn, room_id, receipts); +} + +UserReceipts +readReceipts(const QString &event_id, const QString &room_id) +{ + return instance_->readReceipts(event_id, room_id); +} + +//! Filter the events that have at least one read receipt. +std::vector +filterReadEvents(const QString &room_id, + const std::vector &event_ids, + const std::string &excluded_user) +{ + return instance_->filterReadEvents(room_id, event_ids, excluded_user); +} +//! Add event for which we are expecting some read receipts. +void +addPendingReceipt(const QString &room_id, const QString &event_id) +{ + instance_->addPendingReceipt(room_id, event_id); +} +void +removePendingReceipt(lmdb::txn &txn, const std::string &room_id, const std::string &event_id) +{ + instance_->removePendingReceipt(txn, room_id, event_id); +} +void +notifyForReadReceipts(const std::string &room_id) +{ + instance_->notifyForReadReceipts(room_id); +} +std::vector +pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id) +{ + return instance_->pendingReceiptsEvents(txn, room_id); +} + +QByteArray +image(const QString &url) +{ + return instance_->image(url); +} +QByteArray +image(lmdb::txn &txn, const std::string &url) +{ + return instance_->image(txn, url); +} +void +saveImage(const std::string &url, const std::string &data) +{ + instance_->saveImage(url, data); +} +void +saveImage(const QString &url, const QByteArray &data) +{ + instance_->saveImage(url, data); +} + +RoomInfo +singleRoomInfo(const std::string &room_id) +{ + return instance_->singleRoomInfo(room_id); +} +std::vector +roomsWithStateUpdates(const mtx::responses::Sync &res) +{ + return instance_->roomsWithStateUpdates(res); +} +std::vector +roomsWithTagUpdates(const mtx::responses::Sync &res) +{ + return instance_->roomsWithTagUpdates(res); +} +std::map +getRoomInfo(const std::vector &rooms) +{ + return instance_->getRoomInfo(rooms); +} + +//! Calculates which the read status of a room. +//! Whether all the events in the timeline have been read. +bool +calculateRoomReadStatus(const std::string &room_id) +{ + return instance_->calculateRoomReadStatus(room_id); +} +void +calculateRoomReadStatus() +{ + instance_->calculateRoomReadStatus(); +} + +QVector +searchUsers(const std::string &room_id, const std::string &query, std::uint8_t max_items) +{ + return instance_->searchUsers(room_id, query, max_items); +} +std::vector +searchRooms(const std::string &query, std::uint8_t max_items) +{ + return instance_->searchRooms(query, max_items); +} + +void +markSentNotification(const std::string &event_id) +{ + instance_->markSentNotification(event_id); +} +//! Removes an event from the sent notifications. +void +removeReadNotification(const std::string &event_id) +{ + instance_->removeReadNotification(event_id); +} +//! Check if we have sent a desktop notification for the given event id. +bool +isNotificationSent(const std::string &event_id) +{ + return instance_->isNotificationSent(event_id); +} + +//! Add all notifications containing a user mention to the db. +void +saveTimelineMentions(const mtx::responses::Notifications &res) +{ + instance_->saveTimelineMentions(res); +} + +//! Remove old unused data. +void +deleteOldMessages() +{ + instance_->deleteOldMessages(); +} +void +deleteOldData() noexcept +{ + instance_->deleteOldData(); +} +//! Retrieve all saved room ids. +std::vector +getRoomIds(lmdb::txn &txn) +{ + return instance_->getRoomIds(txn); +} + +//! Mark a room that uses e2e encryption. +void +setEncryptedRoom(lmdb::txn &txn, const std::string &room_id) +{ + instance_->setEncryptedRoom(txn, room_id); +} +bool +isRoomEncrypted(const std::string &room_id) +{ + return instance_->isRoomEncrypted(room_id); +} + +//! Check if a user is a member of the room. +bool +isRoomMember(const std::string &user_id, const std::string &room_id) +{ + return instance_->isRoomMember(user_id, room_id); +} + +// +// Outbound Megolm Sessions +// +void +saveOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session) +{ + instance_->saveOutboundMegolmSession(room_id, data, std::move(session)); +} +OutboundGroupSessionDataRef +getOutboundMegolmSession(const std::string &room_id) +{ + return instance_->getOutboundMegolmSession(room_id); +} +bool +outboundMegolmSessionExists(const std::string &room_id) noexcept +{ + return instance_->outboundMegolmSessionExists(room_id); +} +void +updateOutboundMegolmSession(const std::string &room_id, int message_index) +{ + instance_->updateOutboundMegolmSession(room_id, message_index); +} + +void +importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys) +{ + instance_->importSessionKeys(keys); +} +mtx::crypto::ExportedSessionKeys +exportSessionKeys() +{ + return instance_->exportSessionKeys(); +} + +// +// Inbound Megolm Sessions +// +void +saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session) +{ + instance_->saveInboundMegolmSession(index, std::move(session)); +} +OlmInboundGroupSession * +getInboundMegolmSession(const MegolmSessionIndex &index) +{ + return instance_->getInboundMegolmSession(index); +} +bool +inboundMegolmSessionExists(const MegolmSessionIndex &index) +{ + return instance_->inboundMegolmSessionExists(index); +} + +// +// Olm Sessions +// +void +saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session) +{ + instance_->saveOlmSession(curve25519, std::move(session)); +} +std::vector +getOlmSessions(const std::string &curve25519) +{ + return instance_->getOlmSessions(curve25519); +} +std::optional +getOlmSession(const std::string &curve25519, const std::string &session_id) +{ + return instance_->getOlmSession(curve25519, session_id); +} + +void +saveOlmAccount(const std::string &pickled) +{ + instance_->saveOlmAccount(pickled); +} +std::string +restoreOlmAccount() +{ + return instance_->restoreOlmAccount(); +} + +void +restoreSessions() +{ + return instance_->restoreSessions(); +} +} // namespace cache + diff --git a/src/Cache.h b/src/Cache.h index 02346287..da4e2040 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -17,501 +17,285 @@ #pragma once -#include -#include - #include #include #include #include #include -#include #include -#include #include "CacheCryptoStructs.h" #include "CacheStructs.h" -#include "Logging.h" -#include "MatrixClient.h" -int -numeric_key_comparison(const MDB_val *a, const MDB_val *b); +namespace cache { +void +init(const QString &user_id); + +std::string +displayName(const std::string &room_id, const std::string &user_id); +QString +displayName(const QString &room_id, const QString &user_id); +QString +avatarUrl(const QString &room_id, const QString &user_id); +QString +userColor(const QString &user_id); + +void +removeDisplayName(const QString &room_id, const QString &user_id); +void +removeAvatarUrl(const QString &room_id, const QString &user_id); +void +removeUserColor(const QString &user_id); + +void +insertDisplayName(const QString &room_id, const QString &user_id, const QString &display_name); +void +insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url); +void +insertUserColor(const QString &user_id, const QString &color_name); + +void +clearUserColors(); + +//! Load saved data for the display names & avatars. +void +populateMembers(); +std::vector +joinedRooms(); + +QMap +roomInfo(bool withInvites = true); +std::map +invites(); + +//! Calculate & return the name of the room. +QString +getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); +//! Get room join rules +mtx::events::state::JoinRule +getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb); +bool +getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb); +//! Retrieve the topic of the room if any. +QString +getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); +//! Retrieve the room avatar's url if any. +QString +getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb, const QString &room_id); +//! Retrieve the version of the room if any. +QString +getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb); + +//! Retrieve member info from a room. +std::vector +getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30); -class Cache : public QObject +void +saveState(const mtx::responses::Sync &res); +bool +isInitialized(); + +std::string +nextBatchToken(); + +void +deleteData(); + +void +removeInvite(lmdb::txn &txn, const std::string &room_id); +void +removeInvite(const std::string &room_id); +void +removeRoom(lmdb::txn &txn, const std::string &roomid); +void +removeRoom(const std::string &roomid); +void +removeRoom(const QString &roomid); +void +setup(); + +bool +isFormatValid(); +void +setCurrentFormat(); + +std::map +roomMessages(); + +QMap +getTimelineMentions(); + +//! Retrieve all the user ids from a room. +std::vector +roomMembers(const std::string &room_id); + +//! Check if the given user has power leve greater than than +//! lowest power level of the given events. +bool +hasEnoughPowerLevel(const std::vector &eventTypes, + const std::string &room_id, + const std::string &user_id); + +//! Retrieves the saved room avatar. +QImage +getRoomAvatar(const QString &id); +QImage +getRoomAvatar(const std::string &id); + +//! Adds a user to the read list for the given event. +//! +//! There should be only one user id present in a receipt list per room. +//! The user id should be removed from any other lists. +using Receipts = std::map>; +void +updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts); + +//! Retrieve all the read receipts for the given event id and room. +//! +//! Returns a map of user ids and the time of the read receipt in milliseconds. +using UserReceipts = std::multimap>; +UserReceipts +readReceipts(const QString &event_id, const QString &room_id); + +//! Filter the events that have at least one read receipt. +std::vector +filterReadEvents(const QString &room_id, + const std::vector &event_ids, + const std::string &excluded_user); +//! Add event for which we are expecting some read receipts. +void +addPendingReceipt(const QString &room_id, const QString &event_id); +void +removePendingReceipt(lmdb::txn &txn, const std::string &room_id, const std::string &event_id); +void +notifyForReadReceipts(const std::string &room_id); +std::vector +pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id); + +QByteArray +image(const QString &url); +QByteArray +image(lmdb::txn &txn, const std::string &url); +inline QByteArray +image(const std::string &url) +{ + return image(QString::fromStdString(url)); +} +void +saveImage(const std::string &url, const std::string &data); +void +saveImage(const QString &url, const QByteArray &data); + +RoomInfo +singleRoomInfo(const std::string &room_id); +std::vector +roomsWithStateUpdates(const mtx::responses::Sync &res); +std::vector +roomsWithTagUpdates(const mtx::responses::Sync &res); +std::map +getRoomInfo(const std::vector &rooms); +inline std::map +roomUpdates(const mtx::responses::Sync &sync) { - Q_OBJECT - -public: - Cache(const QString &userId, QObject *parent = nullptr); - - static QHash DisplayNames; - static QHash AvatarUrls; - static QHash UserColors; - - static std::string displayName(const std::string &room_id, const std::string &user_id); - static QString displayName(const QString &room_id, const QString &user_id); - static QString avatarUrl(const QString &room_id, const QString &user_id); - static QString userColor(const QString &user_id); - - static void removeDisplayName(const QString &room_id, const QString &user_id); - static void removeAvatarUrl(const QString &room_id, const QString &user_id); - static void removeUserColor(const QString &user_id); - - static void insertDisplayName(const QString &room_id, - const QString &user_id, - const QString &display_name); - static void insertAvatarUrl(const QString &room_id, - const QString &user_id, - const QString &avatar_url); - static void insertUserColor(const QString &user_id, const QString &color_name); - - static void clearUserColors(); - - //! Load saved data for the display names & avatars. - void populateMembers(); - std::vector joinedRooms(); - - QMap roomInfo(bool withInvites = true); - std::map invites(); - - //! Calculate & return the name of the room. - QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); - //! Get room join rules - mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb); - bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb); - //! Retrieve the topic of the room if any. - QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); - //! Retrieve the room avatar's url if any. - QString getRoomAvatarUrl(lmdb::txn &txn, - lmdb::dbi &statesdb, - lmdb::dbi &membersdb, - const QString &room_id); - //! Retrieve the version of the room if any. - QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb); - - //! Retrieve member info from a room. - std::vector getMembers(const std::string &room_id, - std::size_t startIndex = 0, - std::size_t len = 30); - - void saveState(const mtx::responses::Sync &res); - bool isInitialized() const; - - std::string nextBatchToken() const; - - void deleteData(); - - void removeInvite(lmdb::txn &txn, const std::string &room_id); - void removeInvite(const std::string &room_id); - void removeRoom(lmdb::txn &txn, const std::string &roomid); - void removeRoom(const std::string &roomid); - void removeRoom(const QString &roomid) { removeRoom(roomid.toStdString()); }; - void setup(); - - bool isFormatValid(); - void setCurrentFormat(); - - std::map roomMessages(); - - QMap getTimelineMentions(); - - //! Retrieve all the user ids from a room. - std::vector roomMembers(const std::string &room_id); - - //! Check if the given user has power leve greater than than - //! lowest power level of the given events. - bool hasEnoughPowerLevel(const std::vector &eventTypes, - const std::string &room_id, - const std::string &user_id); - - //! Retrieves the saved room avatar. - QImage getRoomAvatar(const QString &id); - QImage getRoomAvatar(const std::string &id); - - //! Adds a user to the read list for the given event. - //! - //! There should be only one user id present in a receipt list per room. - //! The user id should be removed from any other lists. - using Receipts = std::map>; - void updateReadReceipt(lmdb::txn &txn, - const std::string &room_id, - const Receipts &receipts); - - //! Retrieve all the read receipts for the given event id and room. - //! - //! Returns a map of user ids and the time of the read receipt in milliseconds. - using UserReceipts = std::multimap>; - UserReceipts readReceipts(const QString &event_id, const QString &room_id); - - //! Filter the events that have at least one read receipt. - std::vector filterReadEvents(const QString &room_id, - const std::vector &event_ids, - const std::string &excluded_user); - //! Add event for which we are expecting some read receipts. - void addPendingReceipt(const QString &room_id, const QString &event_id); - void removePendingReceipt(lmdb::txn &txn, - const std::string &room_id, - const std::string &event_id); - void notifyForReadReceipts(const std::string &room_id); - std::vector pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id); - - QByteArray image(const QString &url) const; - QByteArray image(lmdb::txn &txn, const std::string &url) const; - QByteArray image(const std::string &url) const - { - return image(QString::fromStdString(url)); - } - void saveImage(const std::string &url, const std::string &data); - void saveImage(const QString &url, const QByteArray &data); - - RoomInfo singleRoomInfo(const std::string &room_id); - std::vector roomsWithStateUpdates(const mtx::responses::Sync &res); - std::vector roomsWithTagUpdates(const mtx::responses::Sync &res); - std::map getRoomInfo(const std::vector &rooms); - std::map roomUpdates(const mtx::responses::Sync &sync) - { - return getRoomInfo(roomsWithStateUpdates(sync)); - } - std::map roomTagUpdates(const mtx::responses::Sync &sync) - { - return getRoomInfo(roomsWithTagUpdates(sync)); - } - - //! Calculates which the read status of a room. - //! Whether all the events in the timeline have been read. - bool calculateRoomReadStatus(const std::string &room_id); - void calculateRoomReadStatus(); - - QVector searchUsers(const std::string &room_id, - const std::string &query, - std::uint8_t max_items = 5); - std::vector searchRooms(const std::string &query, - std::uint8_t max_items = 5); - - void markSentNotification(const std::string &event_id); - //! Removes an event from the sent notifications. - void removeReadNotification(const std::string &event_id); - //! Check if we have sent a desktop notification for the given event id. - bool isNotificationSent(const std::string &event_id); - - //! Add all notifications containing a user mention to the db. - void saveTimelineMentions(const mtx::responses::Notifications &res); - - //! Remove old unused data. - void deleteOldMessages(); - void deleteOldData() noexcept; - //! Retrieve all saved room ids. - std::vector getRoomIds(lmdb::txn &txn); - - //! Mark a room that uses e2e encryption. - void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); - bool isRoomEncrypted(const std::string &room_id); - - //! Save the public keys for a device. - void saveDeviceKeys(const std::string &device_id); - void getDeviceKeys(const std::string &device_id); - - //! Save the device list for a user. - void setDeviceList(const std::string &user_id, const std::vector &devices); - std::vector getDeviceList(const std::string &user_id); - - //! Check if a user is a member of the room. - bool isRoomMember(const std::string &user_id, const std::string &room_id); - - // - // Outbound Megolm Sessions - // - void saveOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr session); - OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); - bool outboundMegolmSessionExists(const std::string &room_id) noexcept; - void updateOutboundMegolmSession(const std::string &room_id, int message_index); - - void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); - mtx::crypto::ExportedSessionKeys exportSessionKeys(); - - // - // Inbound Megolm Sessions - // - void saveInboundMegolmSession(const MegolmSessionIndex &index, - mtx::crypto::InboundGroupSessionPtr session); - OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index); - bool inboundMegolmSessionExists(const MegolmSessionIndex &index); - - // - // Olm Sessions - // - void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); - std::vector getOlmSessions(const std::string &curve25519); - std::optional getOlmSession(const std::string &curve25519, - const std::string &session_id); - - void saveOlmAccount(const std::string &pickled); - std::string restoreOlmAccount(); - - void restoreSessions(); - - OlmSessionStorage session_storage; - -signals: - void newReadReceipts(const QString &room_id, const std::vector &event_ids); - void roomReadStatus(const std::map &status); - -private: - //! Save an invited room. - void saveInvite(lmdb::txn &txn, - lmdb::dbi &statesdb, - lmdb::dbi &membersdb, - const mtx::responses::InvitedRoom &room); - - //! Add a notification containing a user mention to the db. - void saveTimelineMentions(lmdb::txn &txn, - const std::string &room_id, - const QList &res); - - //! Get timeline items that a user was mentions in for a given room - mtx::responses::Notifications getTimelineMentionsForRoom(lmdb::txn &txn, - const std::string &room_id); - - QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); - QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); - QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); - - DescInfo getLastMessageInfo(lmdb::txn &txn, const std::string &room_id); - void saveTimelineMessages(lmdb::txn &txn, - const std::string &room_id, - const mtx::responses::Timeline &res); - - mtx::responses::Timeline getTimelineMessages(lmdb::txn &txn, const std::string &room_id); - - //! Remove a room from the cache. - // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id); - template - void saveStateEvents(lmdb::txn &txn, - const lmdb::dbi &statesdb, - const lmdb::dbi &membersdb, - const std::string &room_id, - const std::vector &events) - { - for (const auto &e : events) - saveStateEvent(txn, statesdb, membersdb, room_id, e); - } - - template - void saveStateEvent(lmdb::txn &txn, - const lmdb::dbi &statesdb, - const lmdb::dbi &membersdb, - const std::string &room_id, - const T &event) - { - using namespace mtx::events; - using namespace mtx::events::state; - - if (auto e = std::get_if>(&event); e != nullptr) { - switch (e->content.membership) { - // - // We only keep users with invite or join membership. - // - case Membership::Invite: - case Membership::Join: { - auto display_name = e->content.display_name.empty() - ? e->state_key - : e->content.display_name; - - // Lightweight representation of a member. - MemberInfo tmp{display_name, e->content.avatar_url}; - - lmdb::dbi_put(txn, - membersdb, - lmdb::val(e->state_key), - lmdb::val(json(tmp).dump())); - - insertDisplayName(QString::fromStdString(room_id), - QString::fromStdString(e->state_key), - QString::fromStdString(display_name)); - - insertAvatarUrl(QString::fromStdString(room_id), - QString::fromStdString(e->state_key), - QString::fromStdString(e->content.avatar_url)); - - break; - } - default: { - lmdb::dbi_del( - txn, membersdb, lmdb::val(e->state_key), lmdb::val("")); - - removeDisplayName(QString::fromStdString(room_id), - QString::fromStdString(e->state_key)); - removeAvatarUrl(QString::fromStdString(room_id), - QString::fromStdString(e->state_key)); - - break; - } - } - - return; - } else if (std::holds_alternative>(event)) { - setEncryptedRoom(txn, room_id); - return; - } - - if (!isStateEvent(event)) - return; - - std::visit( - [&txn, &statesdb](auto e) { - lmdb::dbi_put( - txn, statesdb, lmdb::val(to_string(e.type)), lmdb::val(json(e).dump())); - }, - event); - } - - template - bool isStateEvent(const T &e) - { - using namespace mtx::events; - using namespace mtx::events::state; - - return std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e); - } - - template - bool containsStateUpdates(const T &e) - { - using namespace mtx::events; - using namespace mtx::events::state; - - return std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e); - } - - bool containsStateUpdates(const mtx::events::collections::StrippedEvents &e) - { - using namespace mtx::events; - using namespace mtx::events::state; - - return std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e) || - std::holds_alternative>(e); - } - - void saveInvites(lmdb::txn &txn, - const std::map &rooms); - - //! Sends signals for the rooms that are removed. - void removeLeftRooms(lmdb::txn &txn, - const std::map &rooms) - { - for (const auto &room : rooms) { - removeRoom(txn, room.first); - - // Clean up leftover invites. - removeInvite(txn, room.first); - } - } - - lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn) - { - return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE); - } - - lmdb::dbi getMessagesDb(lmdb::txn &txn, const std::string &room_id) - { - auto db = - lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str(), MDB_CREATE); - lmdb::dbi_set_compare(txn, db, numeric_key_comparison); - - return db; - } - - lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE); - } - - lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE); - } - - lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE); - } - - lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); - } - - lmdb::dbi getMentionsDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE); - } - - //! Retrieves or creates the database that stores the open OLM sessions between our device - //! and the given curve25519 key which represents another device. - //! - //! Each entry is a map from the session_id to the pickled representation of the session. - lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key) - { - return lmdb::dbi::open( - txn, std::string("olm_sessions/" + curve25519_key).c_str(), MDB_CREATE); - } - - QString getDisplayName(const mtx::events::StateEvent &event) - { - if (!event.content.display_name.empty()) - return QString::fromStdString(event.content.display_name); - - return QString::fromStdString(event.state_key); - } - - void setNextBatchToken(lmdb::txn &txn, const std::string &token); - void setNextBatchToken(lmdb::txn &txn, const QString &token); - - lmdb::env env_; - lmdb::dbi syncStateDb_; - lmdb::dbi roomsDb_; - lmdb::dbi invitesDb_; - lmdb::dbi mediaDb_; - lmdb::dbi readReceiptsDb_; - lmdb::dbi notificationsDb_; - - lmdb::dbi devicesDb_; - lmdb::dbi deviceKeysDb_; - - lmdb::dbi inboundMegolmSessionDb_; - lmdb::dbi outboundMegolmSessionDb_; - - QString localUserId_; - QString cacheDirectory_; -}; + return getRoomInfo(roomsWithStateUpdates(sync)); +} +inline std::map +roomTagUpdates(const mtx::responses::Sync &sync) +{ + return getRoomInfo(roomsWithTagUpdates(sync)); +} -namespace cache { +//! Calculates which the read status of a room. +//! Whether all the events in the timeline have been read. +bool +calculateRoomReadStatus(const std::string &room_id); void -init(const QString &user_id); +calculateRoomReadStatus(); + +QVector +searchUsers(const std::string &room_id, const std::string &query, std::uint8_t max_items = 5); +std::vector +searchRooms(const std::string &query, std::uint8_t max_items = 5); + +void +markSentNotification(const std::string &event_id); +//! Removes an event from the sent notifications. +void +removeReadNotification(const std::string &event_id); +//! Check if we have sent a desktop notification for the given event id. +bool +isNotificationSent(const std::string &event_id); + +//! Add all notifications containing a user mention to the db. +void +saveTimelineMentions(const mtx::responses::Notifications &res); -Cache * -client(); +//! Remove old unused data. +void +deleteOldMessages(); +void +deleteOldData() noexcept; +//! Retrieve all saved room ids. +std::vector +getRoomIds(lmdb::txn &txn); + +//! Mark a room that uses e2e encryption. +void +setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); +bool +isRoomEncrypted(const std::string &room_id); + +//! Check if a user is a member of the room. +bool +isRoomMember(const std::string &user_id, const std::string &room_id); + +// +// Outbound Megolm Sessions +// +void +saveOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session); +OutboundGroupSessionDataRef +getOutboundMegolmSession(const std::string &room_id); +bool +outboundMegolmSessionExists(const std::string &room_id) noexcept; +void +updateOutboundMegolmSession(const std::string &room_id, int message_index); + +void +importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); +mtx::crypto::ExportedSessionKeys +exportSessionKeys(); + +// +// Inbound Megolm Sessions +// +void +saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session); +OlmInboundGroupSession * +getInboundMegolmSession(const MegolmSessionIndex &index); +bool +inboundMegolmSessionExists(const MegolmSessionIndex &index); + +// +// Olm Sessions +// +void +saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); +std::vector +getOlmSessions(const std::string &curve25519); +std::optional +getOlmSession(const std::string &curve25519, const std::string &session_id); + +void +saveOlmAccount(const std::string &pickled); +std::string +restoreOlmAccount(); + +void +restoreSessions(); } diff --git a/src/Cache_p.h b/src/Cache_p.h new file mode 100644 index 00000000..47dd945a --- /dev/null +++ b/src/Cache_p.h @@ -0,0 +1,494 @@ +/* + * nheko Copyright (C) 2019 The nheko authors + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "CacheCryptoStructs.h" +#include "CacheStructs.h" +#include "Logging.h" +#include "MatrixClient.h" + +int +numeric_key_comparison(const MDB_val *a, const MDB_val *b); + +class Cache : public QObject +{ + Q_OBJECT + +public: + Cache(const QString &userId, QObject *parent = nullptr); + + static std::string displayName(const std::string &room_id, const std::string &user_id); + static QString displayName(const QString &room_id, const QString &user_id); + static QString avatarUrl(const QString &room_id, const QString &user_id); + static QString userColor(const QString &user_id); + + static void removeDisplayName(const QString &room_id, const QString &user_id); + static void removeAvatarUrl(const QString &room_id, const QString &user_id); + static void removeUserColor(const QString &user_id); + + static void insertDisplayName(const QString &room_id, + const QString &user_id, + const QString &display_name); + static void insertAvatarUrl(const QString &room_id, + const QString &user_id, + const QString &avatar_url); + static void insertUserColor(const QString &user_id, const QString &color_name); + + static void clearUserColors(); + + //! Load saved data for the display names & avatars. + void populateMembers(); + std::vector joinedRooms(); + + QMap roomInfo(bool withInvites = true); + std::map invites(); + + //! Calculate & return the name of the room. + QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + //! Get room join rules + mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb); + bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Retrieve the topic of the room if any. + QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Retrieve the room avatar's url if any. + QString getRoomAvatarUrl(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const QString &room_id); + //! Retrieve the version of the room if any. + QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb); + + //! Retrieve member info from a room. + std::vector getMembers(const std::string &room_id, + std::size_t startIndex = 0, + std::size_t len = 30); + + void saveState(const mtx::responses::Sync &res); + bool isInitialized() const; + + std::string nextBatchToken() const; + + void deleteData(); + + void removeInvite(lmdb::txn &txn, const std::string &room_id); + void removeInvite(const std::string &room_id); + void removeRoom(lmdb::txn &txn, const std::string &roomid); + void removeRoom(const std::string &roomid); + void setup(); + + bool isFormatValid(); + void setCurrentFormat(); + + std::map roomMessages(); + + QMap getTimelineMentions(); + + //! Retrieve all the user ids from a room. + std::vector roomMembers(const std::string &room_id); + + //! Check if the given user has power leve greater than than + //! lowest power level of the given events. + bool hasEnoughPowerLevel(const std::vector &eventTypes, + const std::string &room_id, + const std::string &user_id); + + //! Retrieves the saved room avatar. + QImage getRoomAvatar(const QString &id); + QImage getRoomAvatar(const std::string &id); + + //! Adds a user to the read list for the given event. + //! + //! There should be only one user id present in a receipt list per room. + //! The user id should be removed from any other lists. + using Receipts = std::map>; + void updateReadReceipt(lmdb::txn &txn, + const std::string &room_id, + const Receipts &receipts); + + //! Retrieve all the read receipts for the given event id and room. + //! + //! Returns a map of user ids and the time of the read receipt in milliseconds. + using UserReceipts = std::multimap>; + UserReceipts readReceipts(const QString &event_id, const QString &room_id); + + //! Filter the events that have at least one read receipt. + std::vector filterReadEvents(const QString &room_id, + const std::vector &event_ids, + const std::string &excluded_user); + //! Add event for which we are expecting some read receipts. + void addPendingReceipt(const QString &room_id, const QString &event_id); + void removePendingReceipt(lmdb::txn &txn, + const std::string &room_id, + const std::string &event_id); + void notifyForReadReceipts(const std::string &room_id); + std::vector pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id); + + QByteArray image(const QString &url) const; + QByteArray image(lmdb::txn &txn, const std::string &url) const; + void saveImage(const std::string &url, const std::string &data); + void saveImage(const QString &url, const QByteArray &data); + + RoomInfo singleRoomInfo(const std::string &room_id); + std::vector roomsWithStateUpdates(const mtx::responses::Sync &res); + std::vector roomsWithTagUpdates(const mtx::responses::Sync &res); + std::map getRoomInfo(const std::vector &rooms); + + //! Calculates which the read status of a room. + //! Whether all the events in the timeline have been read. + bool calculateRoomReadStatus(const std::string &room_id); + void calculateRoomReadStatus(); + + QVector searchUsers(const std::string &room_id, + const std::string &query, + std::uint8_t max_items = 5); + std::vector searchRooms(const std::string &query, + std::uint8_t max_items = 5); + + void markSentNotification(const std::string &event_id); + //! Removes an event from the sent notifications. + void removeReadNotification(const std::string &event_id); + //! Check if we have sent a desktop notification for the given event id. + bool isNotificationSent(const std::string &event_id); + + //! Add all notifications containing a user mention to the db. + void saveTimelineMentions(const mtx::responses::Notifications &res); + + //! Remove old unused data. + void deleteOldMessages(); + void deleteOldData() noexcept; + //! Retrieve all saved room ids. + std::vector getRoomIds(lmdb::txn &txn); + + //! Mark a room that uses e2e encryption. + void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); + bool isRoomEncrypted(const std::string &room_id); + + //! Check if a user is a member of the room. + bool isRoomMember(const std::string &user_id, const std::string &room_id); + + // + // Outbound Megolm Sessions + // + void saveOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session); + OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); + bool outboundMegolmSessionExists(const std::string &room_id) noexcept; + void updateOutboundMegolmSession(const std::string &room_id, int message_index); + + void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); + mtx::crypto::ExportedSessionKeys exportSessionKeys(); + + // + // Inbound Megolm Sessions + // + void saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session); + OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index); + bool inboundMegolmSessionExists(const MegolmSessionIndex &index); + + // + // Olm Sessions + // + void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); + std::vector getOlmSessions(const std::string &curve25519); + std::optional getOlmSession(const std::string &curve25519, + const std::string &session_id); + + void saveOlmAccount(const std::string &pickled); + std::string restoreOlmAccount(); + + void restoreSessions(); + +signals: + void newReadReceipts(const QString &room_id, const std::vector &event_ids); + void roomReadStatus(const std::map &status); + +private: + //! Save an invited room. + void saveInvite(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const mtx::responses::InvitedRoom &room); + + //! Add a notification containing a user mention to the db. + void saveTimelineMentions(lmdb::txn &txn, + const std::string &room_id, + const QList &res); + + //! Get timeline items that a user was mentions in for a given room + mtx::responses::Notifications getTimelineMentionsForRoom(lmdb::txn &txn, + const std::string &room_id); + + QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); + QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + + DescInfo getLastMessageInfo(lmdb::txn &txn, const std::string &room_id); + void saveTimelineMessages(lmdb::txn &txn, + const std::string &room_id, + const mtx::responses::Timeline &res); + + mtx::responses::Timeline getTimelineMessages(lmdb::txn &txn, const std::string &room_id); + + //! Remove a room from the cache. + // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id); + template + void saveStateEvents(lmdb::txn &txn, + const lmdb::dbi &statesdb, + const lmdb::dbi &membersdb, + const std::string &room_id, + const std::vector &events) + { + for (const auto &e : events) + saveStateEvent(txn, statesdb, membersdb, room_id, e); + } + + template + void saveStateEvent(lmdb::txn &txn, + const lmdb::dbi &statesdb, + const lmdb::dbi &membersdb, + const std::string &room_id, + const T &event) + { + using namespace mtx::events; + using namespace mtx::events::state; + + if (auto e = std::get_if>(&event); e != nullptr) { + switch (e->content.membership) { + // + // We only keep users with invite or join membership. + // + case Membership::Invite: + case Membership::Join: { + auto display_name = e->content.display_name.empty() + ? e->state_key + : e->content.display_name; + + // Lightweight representation of a member. + MemberInfo tmp{display_name, e->content.avatar_url}; + + lmdb::dbi_put(txn, + membersdb, + lmdb::val(e->state_key), + lmdb::val(json(tmp).dump())); + + insertDisplayName(QString::fromStdString(room_id), + QString::fromStdString(e->state_key), + QString::fromStdString(display_name)); + + insertAvatarUrl(QString::fromStdString(room_id), + QString::fromStdString(e->state_key), + QString::fromStdString(e->content.avatar_url)); + + break; + } + default: { + lmdb::dbi_del( + txn, membersdb, lmdb::val(e->state_key), lmdb::val("")); + + removeDisplayName(QString::fromStdString(room_id), + QString::fromStdString(e->state_key)); + removeAvatarUrl(QString::fromStdString(room_id), + QString::fromStdString(e->state_key)); + + break; + } + } + + return; + } else if (std::holds_alternative>(event)) { + setEncryptedRoom(txn, room_id); + return; + } + + if (!isStateEvent(event)) + return; + + std::visit( + [&txn, &statesdb](auto e) { + lmdb::dbi_put( + txn, statesdb, lmdb::val(to_string(e.type)), lmdb::val(json(e).dump())); + }, + event); + } + + template + bool isStateEvent(const T &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e); + } + + template + bool containsStateUpdates(const T &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e); + } + + bool containsStateUpdates(const mtx::events::collections::StrippedEvents &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e) || + std::holds_alternative>(e); + } + + void saveInvites(lmdb::txn &txn, + const std::map &rooms); + + //! Sends signals for the rooms that are removed. + void removeLeftRooms(lmdb::txn &txn, + const std::map &rooms) + { + for (const auto &room : rooms) { + removeRoom(txn, room.first); + + // Clean up leftover invites. + removeInvite(txn, room.first); + } + } + + lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn) + { + return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE); + } + + lmdb::dbi getMessagesDb(lmdb::txn &txn, const std::string &room_id) + { + auto db = + lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str(), MDB_CREATE); + lmdb::dbi_set_compare(txn, db, numeric_key_comparison); + + return db; + } + + lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE); + } + + lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE); + } + + lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE); + } + + lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); + } + + lmdb::dbi getMentionsDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE); + } + + //! Retrieves or creates the database that stores the open OLM sessions between our device + //! and the given curve25519 key which represents another device. + //! + //! Each entry is a map from the session_id to the pickled representation of the session. + lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key) + { + return lmdb::dbi::open( + txn, std::string("olm_sessions/" + curve25519_key).c_str(), MDB_CREATE); + } + + QString getDisplayName(const mtx::events::StateEvent &event) + { + if (!event.content.display_name.empty()) + return QString::fromStdString(event.content.display_name); + + return QString::fromStdString(event.state_key); + } + + void setNextBatchToken(lmdb::txn &txn, const std::string &token); + void setNextBatchToken(lmdb::txn &txn, const QString &token); + + lmdb::env env_; + lmdb::dbi syncStateDb_; + lmdb::dbi roomsDb_; + lmdb::dbi invitesDb_; + lmdb::dbi mediaDb_; + lmdb::dbi readReceiptsDb_; + lmdb::dbi notificationsDb_; + + lmdb::dbi devicesDb_; + lmdb::dbi deviceKeysDb_; + + lmdb::dbi inboundMegolmSessionDb_; + lmdb::dbi outboundMegolmSessionDb_; + + QString localUserId_; + QString cacheDirectory_; + + static QHash DisplayNames; + static QHash AvatarUrls; + static QHash UserColors; + + OlmSessionStorage session_storage; +}; + +namespace cache { +Cache * +client(); +} diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index c496acab..ad07efdd 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -22,6 +22,7 @@ #include "AvatarProvider.h" #include "Cache.h" +#include "Cache_p.h" #include "ChatPage.h" #include "Logging.h" #include "MainWindow.h" @@ -319,7 +320,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) auto bin = dev->peek(dev->size()); auto payload = std::string(bin.data(), bin.size()); std::optional encryptedFile; - if (cache::client()->isRoomEncrypted(current_room_.toStdString())) { + if (cache::isRoomEncrypted(current_room_.toStdString())) { mtx::crypto::BinaryBuf buf; std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload); payload = mtx::crypto::to_string(buf); @@ -408,7 +409,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) this, [](const mtx::responses::Notifications ¬if) { try { - cache::client()->saveTimelineMentions(notif); + cache::saveTimelineMentions(notif); } catch (const lmdb::error &e) { nhlog::db()->error("failed to save mentions: {}", e.what()); } @@ -457,7 +458,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) &popups::UserMentions::initializeMentions); connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) { try { - room_list_->cleanupInvites(cache::client()->invites()); + room_list_->cleanupInvites(cache::invites()); } catch (const lmdb::error &e) { nhlog::db()->error("failed to retrieve invites: {}", e.what()); } @@ -575,7 +576,7 @@ ChatPage::deleteConfigs() settings.remove(""); settings.endGroup(); - cache::client()->deleteData(); + cache::deleteData(); http::client()->clear(); } @@ -610,18 +611,18 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) connect( cache::client(), &Cache::roomReadStatus, room_list_, &RoomList::updateReadStatus); - const bool isInitialized = cache::client()->isInitialized(); - const bool isValid = cache::client()->isFormatValid(); + const bool isInitialized = cache::isInitialized(); + const bool isValid = cache::isFormatValid(); if (!isInitialized) { - cache::client()->setCurrentFormat(); + cache::setCurrentFormat(); } else if (isInitialized && !isValid) { // TODO: Deleting session data but keep using the // same device doesn't work. - cache::client()->deleteData(); + cache::deleteData(); cache::init(userid); - cache::client()->setCurrentFormat(); + cache::setCurrentFormat(); } else if (isInitialized) { loadStateFromCache(); return; @@ -629,7 +630,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) } catch (const lmdb::error &e) { nhlog::db()->critical("failure during boot: {}", e.what()); - cache::client()->deleteData(); + cache::deleteData(); nhlog::net()->info("falling back to initial sync"); } @@ -638,7 +639,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) // There isn't a saved olm account to restore. nhlog::crypto()->info("creating new olm account"); olm::client()->create_new_account(); - cache::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); + cache::saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); } catch (const lmdb::error &e) { nhlog::crypto()->critical("failed to save olm account {}", e.what()); emit dropToLoginPageCb(QString::fromStdString(e.what())); @@ -671,7 +672,7 @@ ChatPage::changeTopRoomInfo(const QString &room_id) } try { - auto room_info = cache::client()->getRoomInfo({room_id.toStdString()}); + auto room_info = cache::getRoomInfo({room_id.toStdString()}); if (room_info.find(room_id) == room_info.end()) return; @@ -682,7 +683,7 @@ ChatPage::changeTopRoomInfo(const QString &room_id) top_bar_->updateRoomName(name); top_bar_->updateRoomTopic(QString::fromStdString(room_info[room_id].topic)); - auto img = cache::client()->getRoomAvatar(room_id); + auto img = cache::getRoomAvatar(room_id); if (img.isNull()) top_bar_->updateRoomAvatarFromName(name); @@ -719,18 +720,17 @@ ChatPage::loadStateFromCache() QtConcurrent::run([this]() { try { - cache::client()->restoreSessions(); - olm::client()->load(cache::client()->restoreOlmAccount(), - STORAGE_SECRET_KEY); + cache::restoreSessions(); + olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY); - cache::client()->populateMembers(); + cache::populateMembers(); - emit initializeEmptyViews(cache::client()->roomMessages()); - emit initializeRoomList(cache::client()->roomInfo()); - emit initializeMentions(cache::client()->getTimelineMentions()); - emit syncTags(cache::client()->roomInfo().toStdMap()); + emit initializeEmptyViews(cache::roomMessages()); + emit initializeRoomList(cache::roomInfo()); + emit initializeMentions(cache::getTimelineMentions()); + emit syncTags(cache::roomInfo().toStdMap()); - cache::client()->calculateRoomReadStatus(); + cache::calculateRoomReadStatus(); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->critical("failed to restore olm account: {}", e.what()); @@ -773,8 +773,8 @@ void ChatPage::removeRoom(const QString &room_id) { try { - cache::client()->removeRoom(room_id); - cache::client()->removeInvite(room_id.toStdString()); + cache::removeRoom(room_id); + cache::removeInvite(room_id.toStdString()); } catch (const lmdb::error &e) { nhlog::db()->critical("failure while removing room: {}", e.what()); // TODO: Notify the user. @@ -807,7 +807,7 @@ ChatPage::generateTypingUsers(const QString &room_id, const std::vectorremoveReadNotification(event_id); + cache::removeReadNotification(event_id); continue; } - if (!cache::client()->isNotificationSent(event_id)) { + if (!cache::isNotificationSent(event_id)) { const auto room_id = QString::fromStdString(item.room_id); const auto user_id = utils::event_sender(item.event); // We should only sent one notification per event. - cache::client()->markSentNotification(event_id); + cache::markSentNotification(event_id); // Don't send a notification when the current room is opened. if (isRoomActive(room_id)) @@ -871,11 +871,10 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) notificationsManager.postNotification( room_id, QString::fromStdString(event_id), - QString::fromStdString( - cache::client()->singleRoomInfo(item.room_id).name), - Cache::displayName(room_id, user_id), + QString::fromStdString(cache::singleRoomInfo(item.room_id).name), + cache::displayName(room_id, user_id), utils::event_body(item.event), - cache::client()->getRoomAvatar(room_id)); + cache::getRoomAvatar(room_id)); } } catch (const lmdb::error &e) { nhlog::db()->warn("error while sending desktop notification: {}", e.what()); @@ -962,7 +961,7 @@ ChatPage::trySync() connectivityTimer_.start(); try { - opts.since = cache::client()->nextBatchToken(); + opts.since = cache::nextBatchToken(); } catch (const lmdb::error &e) { nhlog::db()->error("failed to retrieve next batch token: {}", e.what()); return; @@ -1015,22 +1014,22 @@ ChatPage::trySync() // TODO: fine grained error handling try { - cache::client()->saveState(res); + cache::saveState(res); olm::handle_to_device_messages(res.to_device); emit syncUI(res.rooms); - auto updates = cache::client()->roomUpdates(res); + auto updates = cache::roomUpdates(res); emit syncTopBar(updates); emit syncRoomlist(updates); - emit syncTags(cache::client()->roomTagUpdates(res)); + emit syncTags(cache::roomTagUpdates(res)); - cache::client()->deleteOldData(); + cache::deleteOldData(); } catch (const lmdb::map_full_error &e) { nhlog::db()->error("lmdb is full: {}", e.what()); - cache::client()->deleteOldData(); + cache::deleteOldData(); } catch (const lmdb::error &e) { nhlog::db()->error("saving sync response: {}", e.what()); } @@ -1058,7 +1057,7 @@ ChatPage::joinRoom(const QString &room) // We remove any invites with the same room_id. try { - cache::client()->removeInvite(room_id); + cache::removeInvite(room_id); } catch (const lmdb::error &e) { emit showNotification( QString("Failed to remove invite: %1").arg(e.what())); @@ -1156,16 +1155,16 @@ ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::Request nhlog::net()->info("initial sync completed"); try { - cache::client()->saveState(res); + cache::saveState(res); olm::handle_to_device_messages(res.to_device); emit initializeViews(std::move(res.rooms)); - emit initializeRoomList(cache::client()->roomInfo()); - emit initializeMentions(cache::client()->getTimelineMentions()); + emit initializeRoomList(cache::roomInfo()); + emit initializeMentions(cache::getTimelineMentions()); - cache::client()->calculateRoomReadStatus(); - emit syncTags(cache::client()->roomInfo().toStdMap()); + cache::calculateRoomReadStatus(); + emit syncTags(cache::roomInfo().toStdMap()); } catch (const lmdb::error &e) { nhlog::db()->error("failed to save state after initial sync: {}", e.what()); startInitialSync(); diff --git a/src/CommunitiesList.cpp b/src/CommunitiesList.cpp index 6e46741b..4ea99408 100644 --- a/src/CommunitiesList.cpp +++ b/src/CommunitiesList.cpp @@ -215,7 +215,7 @@ CommunitiesList::highlightSelectedCommunity(const QString &community_id) void CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl) { - auto savedImgData = cache::client()->image(avatarUrl); + auto savedImgData = cache::image(avatarUrl); if (!savedImgData.isNull()) { QPixmap pix; pix.loadFromData(savedImgData); @@ -238,7 +238,7 @@ CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUr return; } - cache::client()->saveImage(opts.mxc_url, res); + cache::saveImage(opts.mxc_url, res); auto data = QByteArray(res.data(), res.size()); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 7d9a8902..b13f1b80 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -114,7 +114,7 @@ MainWindow::MainWindow(QWidget *parent) connect( userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool))); connect(userSettingsPage_, &UserSettingsPage::themeChanged, this, []() { - Cache::clearUserColors(); + cache::clearUserColors(); }); connect( userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged); @@ -444,7 +444,7 @@ MainWindow::openReadReceiptsDialog(const QString &event_id) const auto room_id = chat_page_->currentRoom(); try { - dialog->addUsers(cache::client()->readReceipts(event_id, room_id)); + dialog->addUsers(cache::readReceipts(event_id, room_id)); } catch (const lmdb::error &e) { nhlog::db()->warn("failed to retrieve read receipts for {} {}", event_id.toStdString(), @@ -507,4 +507,4 @@ MainWindow::loadJdenticonPlugin() nhlog::ui()->info("jdenticon plugin not found."); return false; -} \ No newline at end of file +} diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index edf6ceb5..02ca2806 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -1,6 +1,8 @@ #include "MxcImageProvider.h" #include "Cache.h" +#include "MatrixClient.h" +#include "Logging.h" void MxcImageResponse::run() @@ -11,7 +13,7 @@ MxcImageResponse::run() .arg(m_requestedSize.width()) .arg(m_requestedSize.height()); - auto data = cache::client()->image(fileName); + auto data = cache::image(fileName); if (!data.isNull() && m_image.loadFromData(data)) { m_image = m_image.scaled(m_requestedSize, Qt::KeepAspectRatio); m_image.setText("mxc url", "mxc://" + m_id); @@ -36,14 +38,14 @@ MxcImageResponse::run() } auto data = QByteArray(res.data(), res.size()); - cache::client()->saveImage(fileName, data); + cache::saveImage(fileName, data); m_image.loadFromData(data); m_image.setText("mxc url", "mxc://" + m_id); emit finished(); }); } else { - auto data = cache::client()->image(m_id); + auto data = cache::image(m_id); if (!data.isNull() && m_image.loadFromData(data)) { m_image.setText("mxc url", "mxc://" + m_id); emit finished(); @@ -75,7 +77,7 @@ MxcImageResponse::run() m_image.setText("original filename", QString::fromStdString(originalFilename)); m_image.setText("mxc url", "mxc://" + m_id); - cache::client()->saveImage(m_id, data); + cache::saveImage(m_id, data); emit finished(); }); diff --git a/src/Olm.cpp b/src/Olm.cpp index 9c1a25df..5859fe8e 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -121,7 +121,7 @@ handle_pre_key_olm_message(const std::string &sender, // We also remove the one time key used to establish that // session so we'll have to update our copy of the account object. - cache::client()->saveOlmAccount(olm::client()->save("secret")); + cache::saveOlmAccount(olm::client()->save("secret")); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->critical( "failed to create inbound session with {}: {}", sender, e.what()); @@ -149,7 +149,7 @@ handle_pre_key_olm_message(const std::string &sender, nhlog::crypto()->debug("decrypted message: \n {}", plaintext.dump(2)); try { - cache::client()->saveOlmSession(sender_key, std::move(inbound_session)); + cache::saveOlmSession(sender_key, std::move(inbound_session)); } catch (const lmdb::error &e) { nhlog::db()->warn( "failed to save inbound olm session from {}: {}", sender, e.what()); @@ -166,7 +166,7 @@ encrypt_group_message(const std::string &room_id, using namespace mtx::events; // Always chech before for existence. - auto res = cache::client()->getOutboundMegolmSession(room_id); + auto res = cache::getOutboundMegolmSession(room_id); auto payload = olm::client()->encrypt_group_message(res.session, body); // Prepare the m.room.encrypted event. @@ -181,7 +181,7 @@ encrypt_group_message(const std::string &room_id, nhlog::crypto()->info("next message_index {}", message_index); // We need to re-pickle the session after we send a message to save the new message_index. - cache::client()->updateOutboundMegolmSession(room_id, message_index); + cache::updateOutboundMegolmSession(room_id, message_index); return data; } @@ -189,13 +189,13 @@ encrypt_group_message(const std::string &room_id, nlohmann::json try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCipherContent &msg) { - auto session_ids = cache::client()->getOlmSessions(sender_key); + auto session_ids = cache::getOlmSessions(sender_key); nhlog::crypto()->info("attempt to decrypt message with {} known session_ids", session_ids.size()); for (const auto &id : session_ids) { - auto session = cache::client()->getOlmSession(sender_key, id); + auto session = cache::getOlmSession(sender_key, id); if (!session) continue; @@ -204,7 +204,7 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip try { text = olm::client()->decrypt_message(session->get(), msg.type, msg.body); - cache::client()->saveOlmSession(id, std::move(session.value())); + cache::saveOlmSession(id, std::move(session.value())); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->debug("failed to decrypt olm message ({}, {}) with {}: {}", msg.type, @@ -252,7 +252,7 @@ create_inbound_megolm_session(const std::string &sender, try { auto megolm_session = olm::client()->init_inbound_group_session(session_key); - cache::client()->saveInboundMegolmSession(index, std::move(megolm_session)); + cache::saveInboundMegolmSession(index, std::move(megolm_session)); } catch (const lmdb::error &e) { nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); return; @@ -268,7 +268,7 @@ void mark_keys_as_published() { olm::client()->mark_keys_as_published(); - cache::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); + cache::saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); } void @@ -355,13 +355,13 @@ handle_key_request_message(const mtx::events::msg::KeyRequest &req) } // Check if we have the keys for the requested session. - if (!cache::client()->outboundMegolmSessionExists(req.room_id)) { + if (!cache::outboundMegolmSessionExists(req.room_id)) { nhlog::crypto()->warn("requested session not found in room: {}", req.room_id); return; } // Check that the requested session_id and the one we have saved match. - const auto session = cache::client()->getOutboundMegolmSession(req.room_id); + const auto session = cache::getOutboundMegolmSession(req.room_id); if (req.session_id != session.data.session_id) { nhlog::crypto()->warn("session id of retrieved session doesn't match the request: " "requested({}), ours({})", @@ -370,7 +370,7 @@ handle_key_request_message(const mtx::events::msg::KeyRequest &req) return; } - if (!cache::client()->isRoomMember(req.sender, req.room_id)) { + if (!cache::isRoomMember(req.sender, req.room_id)) { nhlog::crypto()->warn( "user {} that requested the session key is not member of the room {}", req.sender, @@ -509,8 +509,7 @@ send_megolm_key_to_device(const std::string &user_id, device_msg = olm::client()->create_olm_encrypted_content( olm_session.get(), room_key, pks.curve25519); - cache::client()->saveOlmSession(pks.curve25519, - std::move(olm_session)); + cache::saveOlmSession(pks.curve25519, std::move(olm_session)); } catch (const json::exception &e) { nhlog::crypto()->warn("creating outbound session: {}", e.what()); diff --git a/src/QuickSwitcher.cpp b/src/QuickSwitcher.cpp index f8f6c001..29683bb3 100644 --- a/src/QuickSwitcher.cpp +++ b/src/QuickSwitcher.cpp @@ -93,8 +93,7 @@ QuickSwitcher::QuickSwitcher(QWidget *parent) QtConcurrent::run([this, query = query.toLower()]() { try { - emit queryResults( - cache::client()->searchRooms(query.toStdString())); + emit queryResults(cache::searchRooms(query.toStdString())); } catch (const lmdb::error &e) { qWarning() << "room search failed:" << e.what(); } diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 66700dbc..5e63eca5 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -529,12 +529,12 @@ TextInputWidget::TextInputWidget(QWidget *parent) emit heightChanged(widgetHeight); }); connect(input_, &FilteredTextEdit::showSuggestions, this, [this](const QString &q) { - if (q.isEmpty() || !cache::client()) + if (q.isEmpty()) return; QtConcurrent::run([this, q = q.toLower().toStdString()]() { try { - emit input_->resultsRetrieved(cache::client()->searchUsers( + emit input_->resultsRetrieved(cache::searchUsers( ChatPage::instance()->currentRoom().toStdString(), q)); } catch (const lmdb::error &e) { std::cout << e.what() << '\n'; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 1caea449..772a8d13 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -555,7 +555,7 @@ UserSettingsPage::importSessionKeys() try { auto sessions = mtx::crypto::decrypt_exported_sessions(payload, password.toStdString()); - cache::client()->importSessionKeys(std::move(sessions)); + cache::importSessionKeys(std::move(sessions)); } catch (const mtx::crypto::sodium_exception &e) { QMessageBox::warning(this, tr("Error"), e.what()); } catch (const lmdb::error &e) { @@ -597,7 +597,7 @@ UserSettingsPage::exportSessionKeys() // Export sessions & save to file. try { auto encrypted_blob = mtx::crypto::encrypt_exported_sessions( - cache::client()->exportSessionKeys(), password.toStdString()); + cache::exportSessionKeys(), password.toStdString()); QString b64 = QString::fromStdString(mtx::crypto::bin2base64(encrypted_blob)); diff --git a/src/Utils.cpp b/src/Utils.cpp index 3d69162f..918e1996 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -142,7 +142,7 @@ utils::getMessageDescription(const TimelineEvent &event, } else if (auto msg = std::get_if(&event); msg != nullptr) { const auto sender = QString::fromStdString(msg->sender); - const auto username = Cache::displayName(room_id, sender); + const auto username = cache::displayName(room_id, sender); const auto ts = QDateTime::fromMSecsSinceEpoch(msg->origin_server_ts); DescInfo info; diff --git a/src/Utils.h b/src/Utils.h index 6c0cf0dd..aa62b8e7 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -168,7 +168,7 @@ createDescriptionInfo(const Event &event, const QString &localUser, const QStrin const auto msg = std::get(event); const auto sender = QString::fromStdString(msg.sender); - const auto username = Cache::displayName(room_id, sender); + const auto username = cache::displayName(room_id, sender); const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); return DescInfo{QString::fromStdString(msg.event_id), diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp index f62cf9fe..dfb3d984 100644 --- a/src/dialogs/MemberList.cpp +++ b/src/dialogs/MemberList.cpp @@ -110,11 +110,11 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) const size_t numMembers = list_->count() - 1; if (numMembers > 0) - addUsers(cache::client()->getMembers(room_id_.toStdString(), numMembers)); + addUsers(cache::getMembers(room_id_.toStdString(), numMembers)); }); try { - addUsers(cache::client()->getMembers(room_id_.toStdString())); + addUsers(cache::getMembers(room_id_.toStdString())); } catch (const lmdb::error &e) { qCritical() << e.what(); } diff --git a/src/dialogs/ReadReceipts.cpp b/src/dialogs/ReadReceipts.cpp index 58ad59c3..0edd1ebf 100644 --- a/src/dialogs/ReadReceipts.cpp +++ b/src/dialogs/ReadReceipts.cpp @@ -35,7 +35,7 @@ ReceiptItem::ReceiptItem(QWidget *parent, QFont nameFont; nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); - auto displayName = Cache::displayName(room_id, user_id); + auto displayName = cache::displayName(room_id, user_id); avatar_ = new Avatar(this, 44); avatar_->setLetter(utils::firstChar(displayName)); diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index 1be33d33..fcaa4fdc 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -485,8 +485,8 @@ void RoomSettings::retrieveRoomInfo() { try { - usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString()); - info_ = cache::client()->singleRoomInfo(room_id_.toStdString()); + usesEncryption_ = cache::isRoomEncrypted(room_id_.toStdString()); + info_ = cache::singleRoomInfo(room_id_.toStdString()); setAvatar(); } catch (const lmdb::error &) { nhlog::db()->warn("failed to retrieve room info from cache: {}", @@ -529,8 +529,7 @@ bool RoomSettings::canChangeJoinRules(const std::string &room_id, const std::string &user_id) const { try { - return cache::client()->hasEnoughPowerLevel( - {EventType::RoomJoinRules}, room_id, user_id); + return cache::hasEnoughPowerLevel({EventType::RoomJoinRules}, room_id, user_id); } catch (const lmdb::error &e) { nhlog::db()->warn("lmdb error: {}", e.what()); } @@ -542,7 +541,7 @@ bool RoomSettings::canChangeNameAndTopic(const std::string &room_id, const std::string &user_id) const { try { - return cache::client()->hasEnoughPowerLevel( + return cache::hasEnoughPowerLevel( {EventType::RoomName, EventType::RoomTopic}, room_id, user_id); } catch (const lmdb::error &e) { nhlog::db()->warn("lmdb error: {}", e.what()); @@ -555,8 +554,7 @@ bool RoomSettings::canChangeAvatar(const std::string &room_id, const std::string &user_id) const { try { - return cache::client()->hasEnoughPowerLevel( - {EventType::RoomAvatar}, room_id, user_id); + return cache::hasEnoughPowerLevel({EventType::RoomAvatar}, room_id, user_id); } catch (const lmdb::error &e) { nhlog::db()->warn("lmdb error: {}", e.what()); } diff --git a/src/dialogs/RoomSettings.h b/src/dialogs/RoomSettings.h index e1807ba1..d71b70db 100644 --- a/src/dialogs/RoomSettings.h +++ b/src/dialogs/RoomSettings.h @@ -5,7 +5,9 @@ #include #include -#include "Cache.h" +#include + +#include "CacheStructs.h" class Avatar; class FlatButton; diff --git a/src/dialogs/UserProfile.cpp b/src/dialogs/UserProfile.cpp index 5ad3afa2..50c1c990 100644 --- a/src/dialogs/UserProfile.cpp +++ b/src/dialogs/UserProfile.cpp @@ -203,7 +203,7 @@ UserProfile::init(const QString &userId, const QString &roomId) { resetToDefaults(); - auto displayName = Cache::displayName(roomId, userId); + auto displayName = cache::displayName(roomId, userId); userIdLabel_->setText(userId); displayNameLabel_->setText(displayName); @@ -215,9 +215,9 @@ UserProfile::init(const QString &userId, const QString &roomId) try { bool hasMemberRights = - cache::client()->hasEnoughPowerLevel({mtx::events::EventType::RoomMember}, - roomId.toStdString(), - localUser.toStdString()); + cache::hasEnoughPowerLevel({mtx::events::EventType::RoomMember}, + roomId.toStdString(), + localUser.toStdString()); if (!hasMemberRights) { kickBtn_->hide(); banBtn_->hide(); diff --git a/src/popups/PopupItem.cpp b/src/popups/PopupItem.cpp index c4d4327f..db97e4a3 100644 --- a/src/popups/PopupItem.cpp +++ b/src/popups/PopupItem.cpp @@ -49,7 +49,7 @@ UserItem::UserItem(QWidget *parent, const QString &user_id) : PopupItem(parent) , userId_{user_id} { - auto displayName = Cache::displayName(ChatPage::instance()->currentRoom(), userId_); + auto displayName = cache::displayName(ChatPage::instance()->currentRoom(), userId_); avatar_->setLetter(utils::firstChar(displayName)); @@ -70,7 +70,7 @@ UserItem::updateItem(const QString &user_id) { userId_ = user_id; - auto displayName = Cache::displayName(ChatPage::instance()->currentRoom(), userId_); + auto displayName = cache::displayName(ChatPage::instance()->currentRoom(), userId_); // If it's a matrix id we use the second letter. if (displayName.size() > 1 && displayName.at(0) == '@') @@ -93,7 +93,7 @@ UserItem::mousePressEvent(QMouseEvent *event) { if (event->buttons() != Qt::RightButton) emit clicked( - Cache::displayName(ChatPage::instance()->currentRoom(), selectedText())); + cache::displayName(ChatPage::instance()->currentRoom(), selectedText())); QWidget::mousePressEvent(event); } diff --git a/src/popups/PopupItem.h b/src/popups/PopupItem.h index cab73a9d..7a710fdb 100644 --- a/src/popups/PopupItem.h +++ b/src/popups/PopupItem.h @@ -6,7 +6,6 @@ #include #include "../AvatarProvider.h" -#include "../Cache.h" #include "../ChatPage.h" class Avatar; @@ -81,4 +80,4 @@ private: QLabel *roomName_; QString roomId_; RoomSearchResult info_; -}; \ No newline at end of file +}; diff --git a/src/popups/ReplyPopup.cpp b/src/popups/ReplyPopup.cpp index 0ebf7c88..42a5a6d3 100644 --- a/src/popups/ReplyPopup.cpp +++ b/src/popups/ReplyPopup.cpp @@ -8,6 +8,7 @@ #include "../ui/Avatar.h" #include "../ui/DropShadow.h" #include "../ui/TextLabel.h" +#include "PopupItem.h" #include "ReplyPopup.h" ReplyPopup::ReplyPopup(QWidget *parent) diff --git a/src/popups/ReplyPopup.h b/src/popups/ReplyPopup.h index b28cd0cf..1fa3bb83 100644 --- a/src/popups/ReplyPopup.h +++ b/src/popups/ReplyPopup.h @@ -2,17 +2,14 @@ #include #include -#include #include #include -#include "../AvatarProvider.h" -#include "../Cache.h" -#include "../ChatPage.h" -#include "../Utils.h" #include "../ui/FlatButton.h" #include "../ui/TextLabel.h" -#include "PopupItem.h" + +struct RelatedInfo; +class UserItem; class ReplyPopup : public QWidget { diff --git a/src/popups/SuggestionsPopup.h b/src/popups/SuggestionsPopup.h index 536c82fb..de52760a 100644 --- a/src/popups/SuggestionsPopup.h +++ b/src/popups/SuggestionsPopup.h @@ -5,9 +5,8 @@ #include #include -#include "../AvatarProvider.h" -#include "../Cache.h" -#include "../ChatPage.h" +#include "CacheStructs.h" +#include "ChatPage.h" #include "PopupItem.h" Q_DECLARE_METATYPE(QVector) @@ -28,7 +27,7 @@ public: const auto &widget = qobject_cast(item->widget()); emit itemSelected( - Cache::displayName(ChatPage::instance()->currentRoom(), widget->selectedText())); + cache::displayName(ChatPage::instance()->currentRoom(), widget->selectedText())); resetSelection(); } diff --git a/src/popups/UserMentions.cpp b/src/popups/UserMentions.cpp index 3be5c462..763eeffc 100644 --- a/src/popups/UserMentions.cpp +++ b/src/popups/UserMentions.cpp @@ -103,7 +103,7 @@ UserMentions::showPopup() delete widget; } - auto notifs = cache::client()->getTimelineMentions(); + auto notifs = cache::getTimelineMentions(); initializeMentions(notifs); show(); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 1e069d50..d3d1ad34 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -422,7 +422,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj readEvent(event_id.toStdString()); // ask to be notified for read receipts - cache::client()->addPendingReceipt(room_id_, event_id); + cache::addPendingReceipt(room_id_, event_id); isProcessingPending = false; emit dataChanged(index(idx, 0), index(idx, 0)); @@ -575,8 +575,7 @@ TimelineModel::data(const QModelIndex &index, int role) const return qml_mtx_events::Failed; else if (pending.contains(id)) return qml_mtx_events::Sent; - else if (read.contains(id) || - cache::client()->readReceipts(id, room_id_).size() > 1) + else if (read.contains(id) || cache::readReceipts(id, room_id_).size() > 1) return qml_mtx_events::Read; else return qml_mtx_events::Received; @@ -805,13 +804,13 @@ TimelineModel::userColor(QString id, QColor background) QString TimelineModel::displayName(QString id) const { - return Cache::displayName(room_id_, id); + return cache::displayName(room_id_, id); } QString TimelineModel::avatarUrl(QString id) const { - return Cache::avatarUrl(room_id_, id); + return cache::avatarUrl(room_id_, id); } QString @@ -868,7 +867,7 @@ TimelineModel::decryptEvent(const mtx::events::EncryptedEventinboundMegolmSessionExists(index)) { + if (!cache::inboundMegolmSessionExists(index)) { nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", index.room_id, index.session_id, @@ -887,7 +886,7 @@ TimelineModel::decryptEvent(const mtx::events::EncryptedEventgetInboundMegolmSession(index); + auto session = cache::getInboundMegolmSession(index); auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); msg_str = std::string((char *)res.data.data(), res.data.size()); } catch (const lmdb::error &e) { @@ -1044,7 +1043,7 @@ TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json co try { // Check if we have already an outbound megolm session then we can use. - if (cache::client()->outboundMegolmSessionExists(room_id)) { + if (cache::outboundMegolmSessionExists(room_id)) { auto data = olm::encrypt_group_message( room_id, http::client()->device_id(), doc.dump()); @@ -1089,10 +1088,10 @@ TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json co session_data.session_id = session_id; session_data.session_key = session_key; session_data.message_index = 0; // TODO Update me - cache::client()->saveOutboundMegolmSession( + cache::saveOutboundMegolmSession( room_id, session_data, std::move(outbound_session)); - const auto members = cache::client()->roomMembers(room_id); + const auto members = cache::roomMembers(room_id); nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); auto keeper = @@ -1311,7 +1310,7 @@ TimelineModel::handleClaimedKeys(std::shared_ptr keeper, s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); try { - cache::client()->saveOlmSession(id_key, std::move(s)); + cache::saveOlmSession(id_key, std::move(s)); } catch (const lmdb::error &e) { nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); } catch (const mtx::crypto::olm_exception &e) { @@ -1353,7 +1352,7 @@ struct SendMessageVisitor void operator()(const mtx::events::RoomEvent &msg) { - if (cache::client()->isRoomEncrypted(model_->room_id_.toStdString())) { + if (cache::isRoomEncrypted(model_->room_id_.toStdString())) { model_->sendEncryptedMessage(txn_id_qstr_.toStdString(), nlohmann::json(msg.content)); } else { -- cgit 1.5.1 From 4cd260bfcfcbf88a6efb8bf5a1abf3d37fb06463 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 31 Jan 2020 06:12:02 +0100 Subject: Optimize includes a bit --- CMakeLists.txt | 1 + src/ChatPage.cpp | 4 +- src/CommunitiesList.cpp | 11 +++-- src/CommunitiesList.h | 5 ++ src/CommunitiesListItem.cpp | 3 ++ src/CommunitiesListItem.h | 7 +-- src/LoginPage.cpp | 1 + src/MainWindow.cpp | 3 +- src/RegisterPage.cpp | 1 + src/RoomInfoListItem.cpp | 6 +-- src/RoomList.cpp | 6 +-- src/SideBarActions.cpp | 8 ++-- src/SideBarActions.h | 2 +- src/Splitter.cpp | 22 +++++++-- src/Splitter.h | 16 ++++++- src/TextInputWidget.cpp | 5 +- src/TextInputWidget.h | 1 - src/TopRoomBar.cpp | 26 +++++++++- src/TopRoomBar.h | 35 +++++--------- src/TrayIcon.cpp | 2 + src/TrayIcon.h | 5 +- src/UserInfoWidget.cpp | 5 +- src/UserSettingsPage.cpp | 1 + src/Utils.cpp | 32 ++++++++----- src/Utils.h | 33 +------------ src/WelcomePage.cpp | 1 + src/dialogs/MemberList.cpp | 3 +- src/dialogs/UserProfile.cpp | 3 +- src/emoji/ItemDelegate.cpp | 1 - src/emoji/Panel.cpp | 1 + src/popups/UserMentions.h | 7 +-- src/timeline/TimelineModel.h | 8 ++-- src/ui/DropShadow.cpp | 110 +++++++++++++++++++++++++++++++++++++++++++ src/ui/DropShadow.h | 100 ++------------------------------------- src/ui/FlatButton.cpp | 2 + src/ui/FlatButton.h | 2 - src/ui/FloatingButton.cpp | 1 + src/ui/LoadingIndicator.cpp | 5 +- src/ui/LoadingIndicator.h | 6 +-- src/ui/OverlayWidget.cpp | 4 +- src/ui/OverlayWidget.h | 4 +- 41 files changed, 271 insertions(+), 228 deletions(-) create mode 100644 src/ui/DropShadow.cpp (limited to 'src/dialogs/MemberList.cpp') diff --git a/CMakeLists.txt b/CMakeLists.txt index 96a6aec6..fe00d570 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -256,6 +256,7 @@ set(SRC_FILES # UI components src/ui/Avatar.cpp src/ui/Badge.cpp + src/ui/DropShadow.cpp src/ui/LoadingIndicator.cpp src/ui/InfoMessage.cpp src/ui/FlatButton.cpp diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 2191c6de..e54892a6 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -84,7 +84,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) // SideBar sideBar_ = new QFrame(this); sideBar_->setObjectName("sideBar"); - sideBar_->setMinimumWidth(utils::calculateSidebarSizes(QFont{}).normal); + sideBar_->setMinimumWidth(::splitter::calculateSidebarSizes(QFont{}).normal); sideBarLayout_ = new QVBoxLayout(sideBar_); sideBarLayout_->setSpacing(0); sideBarLayout_->setMargin(0); @@ -1307,7 +1307,7 @@ ChatPage::timelineWidth() bool ChatPage::isSideBarExpanded() { - const auto sz = utils::calculateSidebarSizes(QFont{}); + const auto sz = splitter::calculateSidebarSizes(QFont{}); return sideBar_->size().width() > sz.normal; } diff --git a/src/CommunitiesList.cpp b/src/CommunitiesList.cpp index 4ea99408..2d97594a 100644 --- a/src/CommunitiesList.cpp +++ b/src/CommunitiesList.cpp @@ -2,7 +2,9 @@ #include "Cache.h" #include "Logging.h" #include "MatrixClient.h" -#include "Utils.h" +#include "Splitter.h" + +#include #include @@ -20,7 +22,7 @@ CommunitiesList::CommunitiesList(QWidget *parent) topLayout_->setSpacing(0); topLayout_->setMargin(0); - const auto sideBarSizes = utils::calculateSidebarSizes(QFont{}); + const auto sideBarSizes = splitter::calculateSidebarSizes(QFont{}); setFixedWidth(sideBarSizes.groups); scrollArea_ = new QScrollArea(this); @@ -185,7 +187,8 @@ void CommunitiesList::updateCommunityAvatar(const QString &community_id, const QPixmap &img) { if (!communityExists(community_id)) { - qWarning() << "Avatar update on nonexistent community" << community_id; + nhlog::ui()->warn("Avatar update on nonexistent community {}", + community_id.toStdString()); return; } @@ -196,7 +199,7 @@ void CommunitiesList::highlightSelectedCommunity(const QString &community_id) { if (!communityExists(community_id)) { - qDebug() << "CommunitiesList: clicked unknown community"; + nhlog::ui()->debug("CommunitiesList: clicked unknown community"); return; } diff --git a/src/CommunitiesList.h b/src/CommunitiesList.h index 49eaeaf6..e8042666 100644 --- a/src/CommunitiesList.h +++ b/src/CommunitiesList.h @@ -8,6 +8,11 @@ #include "CommunitiesListItem.h" #include "ui/Theme.h" +namespace mtx::responses { +struct GroupProfile; +struct JoinedGroups; +} + class CommunitiesList : public QWidget { Q_OBJECT diff --git a/src/CommunitiesListItem.cpp b/src/CommunitiesListItem.cpp index 324482d3..274271e5 100644 --- a/src/CommunitiesListItem.cpp +++ b/src/CommunitiesListItem.cpp @@ -1,4 +1,7 @@ #include "CommunitiesListItem.h" + +#include + #include "Utils.h" #include "ui/Painter.h" #include "ui/Ripple.h" diff --git a/src/CommunitiesListItem.h b/src/CommunitiesListItem.h index d4d7e9c6..0cc5d60c 100644 --- a/src/CommunitiesListItem.h +++ b/src/CommunitiesListItem.h @@ -1,17 +1,14 @@ #pragma once -#include -#include -#include #include #include -#include - #include "Config.h" #include "ui/Theme.h" class RippleOverlay; +class QPainter; +class QMouseEvent; class CommunitiesListItem : public QWidget { diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp index 0e7a18d4..c244db28 100644 --- a/src/LoginPage.cpp +++ b/src/LoginPage.cpp @@ -15,6 +15,7 @@ * along with this program. If not, see . */ +#include #include #include diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index a24266fa..d400ad8e 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -31,6 +31,7 @@ #include "MainWindow.h" #include "MatrixClient.h" #include "RegisterPage.h" +#include "Splitter.h" #include "TrayIcon.h" #include "UserSettingsPage.h" #include "Utils.h" @@ -191,7 +192,7 @@ MainWindow::resizeEvent(QResizeEvent *event) void MainWindow::adjustSideBars() { - const auto sz = utils::calculateSidebarSizes(QFont{}); + const auto sz = splitter::calculateSidebarSizes(QFont{}); const uint64_t timelineWidth = chat_page_->timelineWidth(); const uint64_t minAvailableWidth = sz.collapsePoint + sz.groups; diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp index fdb0f43a..942fd1b8 100644 --- a/src/RegisterPage.cpp +++ b/src/RegisterPage.cpp @@ -15,6 +15,7 @@ * along with this program. If not, see . */ +#include #include #include diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp index 926e1359..822a7a55 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp @@ -16,7 +16,6 @@ */ #include -#include #include #include #include @@ -26,6 +25,7 @@ #include "Cache.h" #include "Config.h" #include "RoomInfoListItem.h" +#include "Splitter.h" #include "Utils.h" #include "ui/Menu.h" #include "ui/Ripple.h" @@ -116,7 +116,7 @@ RoomInfoListItem::resizeEvent(QResizeEvent *) QPainterPath path; path.addRect(0, 0, width(), height()); - const auto sidebarSizes = utils::calculateSidebarSizes(QFont{}); + const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); if (width() > sidebarSizes.small) setToolTip(""); @@ -165,7 +165,7 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) // Description line with the default font. int bottom_y = wm.maxHeight - wm.padding - metrics.ascent() / 2; - const auto sidebarSizes = utils::calculateSidebarSizes(QFont{}); + const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); if (width() > sidebarSizes.small) { QFont headingFont; diff --git a/src/RoomList.cpp b/src/RoomList.cpp index 6434489e..b90c8fa4 100644 --- a/src/RoomList.cpp +++ b/src/RoomList.cpp @@ -17,18 +17,14 @@ #include -#include -#include #include +#include #include -#include "Cache.h" #include "Logging.h" #include "MainWindow.h" -#include "MatrixClient.h" #include "RoomInfoListItem.h" #include "RoomList.h" -#include "UserSettingsPage.h" #include "Utils.h" #include "ui/OverlayModal.h" diff --git a/src/SideBarActions.cpp b/src/SideBarActions.cpp index 2f447cd8..4934ec05 100644 --- a/src/SideBarActions.cpp +++ b/src/SideBarActions.cpp @@ -1,15 +1,15 @@ -#include #include +#include +#include #include #include "Config.h" #include "MainWindow.h" #include "SideBarActions.h" -#include "Utils.h" +#include "Splitter.h" #include "ui/FlatButton.h" #include "ui/Menu.h" -#include "ui/OverlayModal.h" SideBarActions::SideBarActions(QWidget *parent) : QWidget{parent} @@ -93,7 +93,7 @@ SideBarActions::resizeEvent(QResizeEvent *event) { Q_UNUSED(event); - const auto sidebarSizes = utils::calculateSidebarSizes(QFont{}); + const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); if (width() <= sidebarSizes.small) { roomDirectory_->hide(); diff --git a/src/SideBarActions.h b/src/SideBarActions.h index ce96cba8..662750b3 100644 --- a/src/SideBarActions.h +++ b/src/SideBarActions.h @@ -2,7 +2,6 @@ #include #include -#include #include namespace mtx { @@ -13,6 +12,7 @@ struct CreateRoom; class Menu; class FlatButton; +class QResizeEvent; class SideBarActions : public QWidget { diff --git a/src/Splitter.cpp b/src/Splitter.cpp index ddb1dc1c..32c67425 100644 --- a/src/Splitter.cpp +++ b/src/Splitter.cpp @@ -16,19 +16,20 @@ */ #include -#include #include #include #include #include "Config.h" +#include "Logging.h" #include "Splitter.h" +#include "Utils.h" constexpr auto MaxWidth = (1 << 24) - 1; Splitter::Splitter(QWidget *parent) : QSplitter(parent) - , sz_{utils::calculateSidebarSizes(QFont{})} + , sz_{splitter::calculateSidebarSizes(QFont{})} { connect(this, &QSplitter::splitterMoved, this, &Splitter::onSplitterMoved); setChildrenCollapsible(false); @@ -80,7 +81,7 @@ Splitter::onSplitterMoved(int pos, int index) auto s = sizes(); if (s.count() < 2) { - qWarning() << "Splitter needs at least two children"; + nhlog::ui()->warn("Splitter needs at least two children"); return; } @@ -165,3 +166,18 @@ Splitter::showFullRoomList() left->show(); left->setMaximumWidth(MaxWidth); } + +splitter::SideBarSizes +splitter::calculateSidebarSizes(const QFont &f) +{ + const auto height = static_cast(QFontMetrics{f}.lineSpacing()); + + SideBarSizes sz; + sz.small = std::ceil(3.5 * height + height / 4.0); + sz.normal = std::ceil(16 * height); + sz.groups = std::ceil(3 * height); + sz.collapsePoint = 2 * sz.normal; + + return sz; +} + diff --git a/src/Splitter.h b/src/Splitter.h index 14d6773e..36c9f4fb 100644 --- a/src/Splitter.h +++ b/src/Splitter.h @@ -17,9 +17,21 @@ #pragma once -#include "Utils.h" #include +namespace splitter { +struct SideBarSizes +{ + int small; + int normal; + int groups; + int collapsePoint; +}; + +SideBarSizes +calculateSidebarSizes(const QFont &f); +} + class Splitter : public QSplitter { Q_OBJECT @@ -45,5 +57,5 @@ private: int leftMoveCount_ = 0; int rightMoveCount_ = 0; - utils::SideBarSizes sz_; + splitter::SideBarSizes sz_; }; diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 502456bf..7be50ab5 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -16,12 +16,9 @@ */ #include -#include #include #include -#include #include -#include #include #include #include @@ -31,7 +28,7 @@ #include "Cache.h" #include "ChatPage.h" -#include "Config.h" +#include "Logging.h" #include "TextInputWidget.h" #include "Utils.h" #include "ui/FlatButton.h" diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index 4bdb2509..a430aa5c 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -23,7 +23,6 @@ #include #include -#include #include #include #include diff --git a/src/TopRoomBar.cpp b/src/TopRoomBar.cpp index 712fe9aa..ffd57d50 100644 --- a/src/TopRoomBar.cpp +++ b/src/TopRoomBar.cpp @@ -15,8 +15,16 @@ * along with this program. If not, see . */ -#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include "Config.h" #include "MainWindow.h" @@ -210,3 +218,19 @@ TopRoomBar::updateRoomTopic(QString topic) topicLabel_->setHtml(topic); update(); } + +void +TopRoomBar::mousePressEvent(QMouseEvent *) +{ + if (roomSettings_ != nullptr) + roomSettings_->trigger(); +} + +void +TopRoomBar::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} diff --git a/src/TopRoomBar.h b/src/TopRoomBar.h index 3243064e..5ab25f39 100644 --- a/src/TopRoomBar.h +++ b/src/TopRoomBar.h @@ -17,17 +17,9 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include class Avatar; class FlatButton; @@ -35,6 +27,12 @@ class Menu; class TextLabel; class OverlayModal; +class QPainter; +class QLabel; +class QIcon; +class QHBoxLayout; +class QVBoxLayout; + class TopRoomBar : public QWidget { Q_OBJECT @@ -67,19 +65,8 @@ signals: void mentionsClicked(const QPoint &pos); protected: - void mousePressEvent(QMouseEvent *) override - { - if (roomSettings_ != nullptr) - roomSettings_->trigger(); - } - - void paintEvent(QPaintEvent *) override - { - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - } + void mousePressEvent(QMouseEvent *) override; + void paintEvent(QPaintEvent *) override; private: QHBoxLayout *topLayout_ = nullptr; diff --git a/src/TrayIcon.cpp b/src/TrayIcon.cpp index 8f62e563..6ab011d1 100644 --- a/src/TrayIcon.cpp +++ b/src/TrayIcon.cpp @@ -15,9 +15,11 @@ * along with this program. If not, see . */ +#include #include #include #include +#include #include #include "TrayIcon.h" diff --git a/src/TrayIcon.h b/src/TrayIcon.h index a3536cc3..6cb26b87 100644 --- a/src/TrayIcon.h +++ b/src/TrayIcon.h @@ -17,13 +17,14 @@ #pragma once -#include #include #include -#include #include #include +class QAction; +class QPainter; + class MsgCountComposedIcon : public QIconEngine { public: diff --git a/src/UserInfoWidget.cpp b/src/UserInfoWidget.cpp index 7a910340..2e21d41f 100644 --- a/src/UserInfoWidget.cpp +++ b/src/UserInfoWidget.cpp @@ -16,14 +16,15 @@ * along with this program. If not, see . */ +#include #include #include #include "Config.h" #include "MainWindow.h" +#include "Splitter.h" #include "UserInfoWidget.h" -#include "Utils.h" #include "ui/Avatar.h" #include "ui/FlatButton.h" #include "ui/OverlayModal.h" @@ -108,7 +109,7 @@ UserInfoWidget::resizeEvent(QResizeEvent *event) { Q_UNUSED(event); - const auto sz = utils::calculateSidebarSizes(QFont{}); + const auto sz = splitter::calculateSidebarSizes(QFont{}); if (width() <= sz.small) { topLayout_->setContentsMargins(0, 0, logoutButtonSize_, 0); diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index b73f80a1..a0f37c26 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include diff --git a/src/Utils.cpp b/src/Utils.cpp index 00796a53..55eebbc9 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -22,6 +22,25 @@ using TimelineEvent = mtx::events::collections::TimelineEvents; QHash authorColors_; +template +static DescInfo +createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id) +{ + const auto msg = std::get(event); + const auto sender = QString::fromStdString(msg.sender); + + const auto username = cache::displayName(room_id, sender); + const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); + + return DescInfo{ + QString::fromStdString(msg.event_id), + sender, + utils::messageDescription( + username, QString::fromStdString(msg.content.body).trimmed(), sender == localUser), + utils::descriptiveTime(ts), + ts}; +} + QString utils::localUser() { @@ -633,16 +652,3 @@ utils::restoreCombobox(QComboBox *combo, const QString &value) } } -utils::SideBarSizes -utils::calculateSidebarSizes(const QFont &f) -{ - const auto height = static_cast(QFontMetrics{f}.lineSpacing()); - - SideBarSizes sz; - sz.small = std::ceil(3.5 * height + height / 4.0); - sz.normal = std::ceil(16 * height); - sz.groups = std::ceil(3 * height); - sz.collapsePoint = 2 * sz.normal; - - return sz; -} diff --git a/src/Utils.h b/src/Utils.h index ea0df21f..a3854dd8 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -2,8 +2,6 @@ #include -#include "RoomInfoListItem.h" - #include #include #include @@ -12,6 +10,8 @@ #include +struct DescInfo; + namespace cache { // Forward declarations to prevent dependency on Cache.h, since this header is included often! QString @@ -166,25 +166,6 @@ messageDescription(const QString &username = "", } } -template -DescInfo -createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id) -{ - const auto msg = std::get(event); - const auto sender = QString::fromStdString(msg.sender); - - const auto username = cache::displayName(room_id, sender); - const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); - - return DescInfo{QString::fromStdString(msg.event_id), - sender, - messageDescription(username, - QString::fromStdString(msg.content.body).trimmed(), - sender == localUser), - utils::descriptiveTime(ts), - ts}; -} - //! Scale down an image to fit to the given width & height limitations. QPixmap scaleDown(uint64_t maxWidth, uint64_t maxHeight, const QPixmap &source); @@ -326,14 +307,4 @@ centerWidget(QWidget *widget, QWidget *parent); void restoreCombobox(QComboBox *combo, const QString &value); -struct SideBarSizes -{ - int small; - int normal; - int groups; - int collapsePoint; -}; - -SideBarSizes -calculateSidebarSizes(const QFont &f); } diff --git a/src/WelcomePage.cpp b/src/WelcomePage.cpp index 8c3f6487..e4b0e1c6 100644 --- a/src/WelcomePage.cpp +++ b/src/WelcomePage.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include "Config.h" diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp index dfb3d984..54e7bf96 100644 --- a/src/dialogs/MemberList.cpp +++ b/src/dialogs/MemberList.cpp @@ -13,6 +13,7 @@ #include "Cache.h" #include "ChatPage.h" #include "Config.h" +#include "Logging.h" #include "Utils.h" #include "ui/Avatar.h" @@ -116,7 +117,7 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) try { addUsers(cache::getMembers(room_id_.toStdString())); } catch (const lmdb::error &e) { - qCritical() << e.what(); + nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what()); } auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); diff --git a/src/dialogs/UserProfile.cpp b/src/dialogs/UserProfile.cpp index 755e8395..273ffd54 100644 --- a/src/dialogs/UserProfile.cpp +++ b/src/dialogs/UserProfile.cpp @@ -1,13 +1,12 @@ #include #include #include -#include #include #include -#include "AvatarProvider.h" #include "Cache.h" #include "ChatPage.h" +#include "Logging.h" #include "MatrixClient.h" #include "Utils.h" #include "dialogs/UserProfile.h" diff --git a/src/emoji/ItemDelegate.cpp b/src/emoji/ItemDelegate.cpp index 890e334a..afa01625 100644 --- a/src/emoji/ItemDelegate.cpp +++ b/src/emoji/ItemDelegate.cpp @@ -15,7 +15,6 @@ * along with this program. If not, see . */ -#include #include #include diff --git a/src/emoji/Panel.cpp b/src/emoji/Panel.cpp index 49ff40c5..e3b966b4 100644 --- a/src/emoji/Panel.cpp +++ b/src/emoji/Panel.cpp @@ -15,6 +15,7 @@ * along with this program. If not, see . */ +#include #include #include #include diff --git a/src/popups/UserMentions.h b/src/popups/UserMentions.h index d3877462..42bdcd60 100644 --- a/src/popups/UserMentions.h +++ b/src/popups/UserMentions.h @@ -2,19 +2,14 @@ #include -#include -#include #include #include #include -#include #include #include #include #include -#include -#include "Logging.h" namespace popups { @@ -47,4 +42,4 @@ private: QScrollArea *all_scroll_area_; QWidget *all_scroll_widget_; }; -} \ No newline at end of file +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 44cf79f4..fb32f0fb 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -6,16 +6,18 @@ #include #include -#include -#include #include #include "CacheCryptoStructs.h" -#include "Logging.h" namespace mtx::http { using RequestErr = const std::optional &; } +namespace mtx::responses { +struct Timeline; +struct Messages; +struct ClaimKeys; +} namespace qml_mtx_events { Q_NAMESPACE diff --git a/src/ui/DropShadow.cpp b/src/ui/DropShadow.cpp new file mode 100644 index 00000000..93baa02d --- /dev/null +++ b/src/ui/DropShadow.cpp @@ -0,0 +1,110 @@ +#include "DropShadow.h" + +#include +#include + + + void DropShadow::draw(QPainter &painter, + qint16 margin, + qreal radius, + QColor start, + QColor end, + qreal startPosition, + qreal endPosition0, + qreal endPosition1, + qreal width, + qreal height) + { + painter.setPen(Qt::NoPen); + + QLinearGradient gradient; + gradient.setColorAt(startPosition, start); + gradient.setColorAt(endPosition0, end); + + // Right + QPointF right0(width - margin, height / 2); + QPointF right1(width, height / 2); + gradient.setStart(right0); + gradient.setFinalStop(right1); + painter.setBrush(QBrush(gradient)); + // Deprecated in 5.13: painter.drawRoundRect( + // QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - + // margin)), 0.0, 0.0); + painter.drawRoundedRect( + QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), + 0.0, + 0.0); + + // Left + QPointF left0(margin, height / 2); + QPointF left1(0, height / 2); + gradient.setStart(left0); + gradient.setFinalStop(left1); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect( + QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0); + + // Top + QPointF top0(width / 2, margin); + QPointF top1(width / 2, 0); + gradient.setStart(top0); + gradient.setFinalStop(top1); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect( + QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0); + + // Bottom + QPointF bottom0(width / 2, height - margin); + QPointF bottom1(width / 2, height); + gradient.setStart(bottom0); + gradient.setFinalStop(bottom1); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect( + QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), + 0.0, + 0.0); + + // BottomRight + QPointF bottomright0(width - margin, height - margin); + QPointF bottomright1(width, height); + gradient.setStart(bottomright0); + gradient.setFinalStop(bottomright1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(bottomright0, bottomright1), 0.0, 0.0); + + // BottomLeft + QPointF bottomleft0(margin, height - margin); + QPointF bottomleft1(0, height); + gradient.setStart(bottomleft0); + gradient.setFinalStop(bottomleft1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0); + + // TopLeft + QPointF topleft0(margin, margin); + QPointF topleft1(0, 0); + gradient.setStart(topleft0); + gradient.setFinalStop(topleft1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(topleft0, topleft1), 0.0, 0.0); + + // TopRight + QPointF topright0(width - margin, margin); + QPointF topright1(width, 0); + gradient.setStart(topright0); + gradient.setFinalStop(topright1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(topright0, topright1), 0.0, 0.0); + + // Widget + painter.setBrush(QBrush("#FFFFFF")); + painter.setRenderHint(QPainter::Antialiasing); + painter.drawRoundedRect( + QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)), + radius, + radius); + } diff --git a/src/ui/DropShadow.h b/src/ui/DropShadow.h index 60fc697b..6997e1a0 100644 --- a/src/ui/DropShadow.h +++ b/src/ui/DropShadow.h @@ -1,8 +1,8 @@ #pragma once #include -#include -#include + +class QPainter; class DropShadow { @@ -16,99 +16,5 @@ public: qreal endPosition0, qreal endPosition1, qreal width, - qreal height) - { - painter.setPen(Qt::NoPen); - - QLinearGradient gradient; - gradient.setColorAt(startPosition, start); - gradient.setColorAt(endPosition0, end); - - // Right - QPointF right0(width - margin, height / 2); - QPointF right1(width, height / 2); - gradient.setStart(right0); - gradient.setFinalStop(right1); - painter.setBrush(QBrush(gradient)); - // Deprecated in 5.13: painter.drawRoundRect( - // QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - - // margin)), 0.0, 0.0); - painter.drawRoundedRect( - QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), - 0.0, - 0.0); - - // Left - QPointF left0(margin, height / 2); - QPointF left1(0, height / 2); - gradient.setStart(left0); - gradient.setFinalStop(left1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect( - QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0); - - // Top - QPointF top0(width / 2, margin); - QPointF top1(width / 2, 0); - gradient.setStart(top0); - gradient.setFinalStop(top1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect( - QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0); - - // Bottom - QPointF bottom0(width / 2, height - margin); - QPointF bottom1(width / 2, height); - gradient.setStart(bottom0); - gradient.setFinalStop(bottom1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect( - QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), - 0.0, - 0.0); - - // BottomRight - QPointF bottomright0(width - margin, height - margin); - QPointF bottomright1(width, height); - gradient.setStart(bottomright0); - gradient.setFinalStop(bottomright1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect(QRectF(bottomright0, bottomright1), 0.0, 0.0); - - // BottomLeft - QPointF bottomleft0(margin, height - margin); - QPointF bottomleft1(0, height); - gradient.setStart(bottomleft0); - gradient.setFinalStop(bottomleft1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0); - - // TopLeft - QPointF topleft0(margin, margin); - QPointF topleft1(0, 0); - gradient.setStart(topleft0); - gradient.setFinalStop(topleft1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect(QRectF(topleft0, topleft1), 0.0, 0.0); - - // TopRight - QPointF topright0(width - margin, margin); - QPointF topright1(width, 0); - gradient.setStart(topright0); - gradient.setFinalStop(topright1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundedRect(QRectF(topright0, topright1), 0.0, 0.0); - - // Widget - painter.setBrush(QBrush("#FFFFFF")); - painter.setRenderHint(QPainter::Antialiasing); - painter.drawRoundedRect( - QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)), - radius, - radius); - } + qreal height); }; diff --git a/src/ui/FlatButton.cpp b/src/ui/FlatButton.cpp index a828f582..6660c58d 100644 --- a/src/ui/FlatButton.cpp +++ b/src/ui/FlatButton.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/src/ui/FlatButton.h b/src/ui/FlatButton.h index 9c2bf425..d29903c2 100644 --- a/src/ui/FlatButton.h +++ b/src/ui/FlatButton.h @@ -1,7 +1,5 @@ #pragma once -#include -#include #include #include diff --git a/src/ui/FloatingButton.cpp b/src/ui/FloatingButton.cpp index 74dcd482..f3a09ccd 100644 --- a/src/ui/FloatingButton.cpp +++ b/src/ui/FloatingButton.cpp @@ -1,3 +1,4 @@ +#include #include #include "FloatingButton.h" diff --git a/src/ui/LoadingIndicator.cpp b/src/ui/LoadingIndicator.cpp index c8337089..d2b1240d 100644 --- a/src/ui/LoadingIndicator.cpp +++ b/src/ui/LoadingIndicator.cpp @@ -1,7 +1,8 @@ #include "LoadingIndicator.h" -#include -#include +#include +#include +#include LoadingIndicator::LoadingIndicator(QWidget *parent) : QWidget(parent) diff --git a/src/ui/LoadingIndicator.h b/src/ui/LoadingIndicator.h index e8de0aec..1585098e 100644 --- a/src/ui/LoadingIndicator.h +++ b/src/ui/LoadingIndicator.h @@ -1,11 +1,11 @@ #pragma once #include -#include -#include -#include #include +class QPainter; +class QTimer; +class QPaintEvent; class LoadingIndicator : public QWidget { Q_OBJECT diff --git a/src/ui/OverlayWidget.cpp b/src/ui/OverlayWidget.cpp index ccac0116..a32d86b6 100644 --- a/src/ui/OverlayWidget.cpp +++ b/src/ui/OverlayWidget.cpp @@ -1,5 +1,7 @@ #include "OverlayWidget.h" -#include + +#include +#include OverlayWidget::OverlayWidget(QWidget *parent) : QWidget(parent) diff --git a/src/ui/OverlayWidget.h b/src/ui/OverlayWidget.h index 6662479d..ed3ef52d 100644 --- a/src/ui/OverlayWidget.h +++ b/src/ui/OverlayWidget.h @@ -1,10 +1,10 @@ #pragma once #include -#include -#include #include +class QPainter; + class OverlayWidget : public QWidget { Q_OBJECT -- cgit 1.5.1