diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp
index ec745c04..68b6901e 100644
--- a/src/AvatarProvider.cpp
+++ b/src/AvatarProvider.cpp
@@ -43,7 +43,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
QPixmap pixmap;
if (avatar_cache.find(cacheKey, &pixmap)) {
- nhlog::net()->info("cached pixmap {}", avatarUrl.toStdString());
callback(pixmap);
return;
}
@@ -52,7 +51,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
if (!data.isNull()) {
pixmap.loadFromData(data);
avatar_cache.insert(cacheKey, pixmap);
- nhlog::net()->info("loaded pixmap from disk cache {}", avatarUrl.toStdString());
callback(pixmap);
return;
}
@@ -69,8 +67,8 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
});
mtx::http::ThumbOpts opts;
- opts.width = 256;
- opts.height = 256;
+ opts.width = size;
+ opts.height = size;
opts.mxc_url = avatarUrl.toStdString();
http::client()->get_thumbnail(
@@ -86,8 +84,6 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
cache::client()->saveImage(opts.mxc_url, res);
- nhlog::net()->info("downloaded pixmap {}", opts.mxc_url);
-
emit proxy->avatarDownloaded(QByteArray(res.data(), res.size()));
});
}
diff --git a/src/Cache.h b/src/Cache.h
index 0da49793..f5e1cfa0 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -91,7 +91,6 @@ from_json(const json &j, ReadReceiptKey &key)
struct DescInfo
{
QString event_id;
- QString username;
QString userid;
QString body;
QString timestamp;
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 21ded4b3..d6f6940b 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -54,6 +54,8 @@ constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
constexpr int RETRY_TIMEOUT = 5'000;
constexpr size_t MAX_ONETIME_KEYS = 50;
+Q_DECLARE_METATYPE(boost::optional<mtx::crypto::EncryptedFile>)
+
ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
: QWidget(parent)
, isConnected_(true)
@@ -62,6 +64,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
{
setObjectName("chatPage");
+ qRegisterMetaType<boost::optional<mtx::crypto::EncryptedFile>>(
+ "boost::optional<mtx::crypto::EncryptedFile>");
+
topLayout_ = new QHBoxLayout(this);
topLayout_->setSpacing(0);
topLayout_->setMargin(0);
@@ -113,12 +118,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
view_manager_ = new TimelineViewManager(this);
contentLayout_->addWidget(top_bar_);
- contentLayout_->addWidget(view_manager_);
-
- connect(this,
- &ChatPage::removeTimelineEvent,
- view_manager_,
- &TimelineViewManager::removeTimelineEvent);
+ contentLayout_->addWidget(view_manager_->getWidget());
// Splitter
splitter->addWidget(sideBar_);
@@ -304,9 +304,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
connect(
text_input_,
- &TextInputWidget::uploadImage,
+ &TextInputWidget::uploadMedia,
this,
- [this](QSharedPointer<QIODevice> dev, const QString &fn) {
+ [this](QSharedPointer<QIODevice> dev, QString mimeClass, const QString &fn) {
QMimeDatabase db;
QMimeType mime = db.mimeTypeForData(dev.data());
@@ -316,9 +316,18 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
return;
}
- auto bin = dev->peek(dev->size());
- auto payload = std::string(bin.data(), bin.size());
- auto dimensions = QImageReader(dev.data()).size();
+ auto bin = dev->peek(dev->size());
+ auto payload = std::string(bin.data(), bin.size());
+ boost::optional<mtx::crypto::EncryptedFile> encryptedFile;
+ if (cache::client()->isRoomEncrypted(current_room_.toStdString())) {
+ mtx::crypto::BinaryBuf buf;
+ std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload);
+ payload = mtx::crypto::to_string(buf);
+ }
+
+ QSize dimensions;
+ if (mimeClass == "image")
+ dimensions = QImageReader(dev.data()).size();
http::client()->upload(
payload,
@@ -327,193 +336,61 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
[this,
room_id = current_room_,
filename = fn,
- mime = mime.name(),
- size = payload.size(),
+ encryptedFile,
+ mimeClass,
+ mime = mime.name(),
+ size = payload.size(),
dimensions](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) {
if (err) {
emit uploadFailed(
- tr("Failed to upload image. Please try again."));
- nhlog::net()->warn("failed to upload image: {} {} ({})",
+ tr("Failed to upload media. Please try again."));
+ nhlog::net()->warn("failed to upload media: {} {} ({})",
err->matrix_error.error,
to_string(err->matrix_error.errcode),
static_cast<int>(err->status_code));
return;
}
- emit imageUploaded(room_id,
+ emit mediaUploaded(room_id,
filename,
+ encryptedFile,
QString::fromStdString(res.content_uri),
+ mimeClass,
mime,
size,
dimensions);
});
});
- connect(text_input_,
- &TextInputWidget::uploadFile,
- this,
- [this](QSharedPointer<QIODevice> dev, const QString &fn) {
- QMimeDatabase db;
- QMimeType mime = db.mimeTypeForData(dev.data());
-
- if (!dev->open(QIODevice::ReadOnly)) {
- emit uploadFailed(
- QString("Error while reading media: %1").arg(dev->errorString()));
- return;
- }
-
- auto bin = dev->readAll();
- auto payload = std::string(bin.data(), bin.size());
-
- http::client()->upload(
- payload,
- mime.name().toStdString(),
- QFileInfo(fn).fileName().toStdString(),
- [this,
- room_id = current_room_,
- filename = fn,
- mime = mime.name(),
- size = payload.size()](const mtx::responses::ContentURI &res,
- mtx::http::RequestErr err) {
- if (err) {
- emit uploadFailed(
- tr("Failed to upload file. Please try again."));
- nhlog::net()->warn("failed to upload file: {} ({})",
- err->matrix_error.error,
- static_cast<int>(err->status_code));
- return;
- }
-
- emit fileUploaded(room_id,
- filename,
- QString::fromStdString(res.content_uri),
- mime,
- size);
- });
- });
-
- connect(text_input_,
- &TextInputWidget::uploadAudio,
- this,
- [this](QSharedPointer<QIODevice> dev, const QString &fn) {
- QMimeDatabase db;
- QMimeType mime = db.mimeTypeForData(dev.data());
-
- if (!dev->open(QIODevice::ReadOnly)) {
- emit uploadFailed(
- QString("Error while reading media: %1").arg(dev->errorString()));
- return;
- }
-
- auto bin = dev->readAll();
- auto payload = std::string(bin.data(), bin.size());
-
- http::client()->upload(
- payload,
- mime.name().toStdString(),
- QFileInfo(fn).fileName().toStdString(),
- [this,
- room_id = current_room_,
- filename = fn,
- mime = mime.name(),
- size = payload.size()](const mtx::responses::ContentURI &res,
- mtx::http::RequestErr err) {
- if (err) {
- emit uploadFailed(
- tr("Failed to upload audio. Please try again."));
- nhlog::net()->warn("failed to upload audio: {} ({})",
- err->matrix_error.error,
- static_cast<int>(err->status_code));
- return;
- }
-
- emit audioUploaded(room_id,
- filename,
- QString::fromStdString(res.content_uri),
- mime,
- size);
- });
- });
- connect(text_input_,
- &TextInputWidget::uploadVideo,
- this,
- [this](QSharedPointer<QIODevice> dev, const QString &fn) {
- QMimeDatabase db;
- QMimeType mime = db.mimeTypeForData(dev.data());
-
- if (!dev->open(QIODevice::ReadOnly)) {
- emit uploadFailed(
- QString("Error while reading media: %1").arg(dev->errorString()));
- return;
- }
-
- auto bin = dev->readAll();
- auto payload = std::string(bin.data(), bin.size());
-
- http::client()->upload(
- payload,
- mime.name().toStdString(),
- QFileInfo(fn).fileName().toStdString(),
- [this,
- room_id = current_room_,
- filename = fn,
- mime = mime.name(),
- size = payload.size()](const mtx::responses::ContentURI &res,
- mtx::http::RequestErr err) {
- if (err) {
- emit uploadFailed(
- tr("Failed to upload video. Please try again."));
- nhlog::net()->warn("failed to upload video: {} ({})",
- err->matrix_error.error,
- static_cast<int>(err->status_code));
- return;
- }
-
- emit videoUploaded(room_id,
- filename,
- QString::fromStdString(res.content_uri),
- mime,
- size);
- });
- });
-
connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) {
text_input_->hideUploadSpinner();
emit showNotification(msg);
});
connect(this,
- &ChatPage::imageUploaded,
+ &ChatPage::mediaUploaded,
this,
[this](QString roomid,
QString filename,
+ boost::optional<mtx::crypto::EncryptedFile> encryptedFile,
QString url,
+ QString mimeClass,
QString mime,
qint64 dsize,
QSize dimensions) {
text_input_->hideUploadSpinner();
- view_manager_->queueImageMessage(
- roomid, filename, url, mime, dsize, dimensions);
- });
- connect(this,
- &ChatPage::fileUploaded,
- this,
- [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) {
- text_input_->hideUploadSpinner();
- view_manager_->queueFileMessage(roomid, filename, url, mime, dsize);
- });
- connect(this,
- &ChatPage::audioUploaded,
- this,
- [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) {
- text_input_->hideUploadSpinner();
- view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize);
- });
- connect(this,
- &ChatPage::videoUploaded,
- this,
- [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) {
- text_input_->hideUploadSpinner();
- view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize);
+
+ if (mimeClass == "image")
+ view_manager_->queueImageMessage(
+ roomid, filename, encryptedFile, url, mime, dsize, dimensions);
+ else if (mimeClass == "audio")
+ view_manager_->queueAudioMessage(
+ roomid, filename, encryptedFile, url, mime, dsize);
+ else if (mimeClass == "video")
+ view_manager_->queueVideoMessage(
+ roomid, filename, encryptedFile, url, mime, dsize);
+ else
+ view_manager_->queueFileMessage(
+ roomid, filename, encryptedFile, url, mime, dsize);
});
connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar);
@@ -566,7 +443,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
connect(this,
&ChatPage::initializeViews,
view_manager_,
- [this](const mtx::responses::Rooms &rooms) { view_manager_->initialize(rooms); });
+ [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); });
connect(this,
&ChatPage::initializeEmptyViews,
view_manager_,
@@ -582,7 +459,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
nhlog::db()->error("failed to retrieve invites: {}", e.what());
}
- view_manager_->initialize(rooms);
+ view_manager_->sync(rooms);
removeLeftRooms(rooms.leave);
bool hasNotifications = false;
diff --git a/src/ChatPage.h b/src/ChatPage.h
index e41ae1ae..20e156af 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -18,7 +18,9 @@
#pragma once
#include <atomic>
+#include <boost/optional.hpp>
#include <boost/variant.hpp>
+#include <mtx/common.hpp>
#include <mtx/responses.hpp>
#include <QFrame>
@@ -94,27 +96,14 @@ signals:
const QPoint widgetPos);
void uploadFailed(const QString &msg);
- void imageUploaded(const QString &roomid,
+ void mediaUploaded(const QString &roomid,
const QString &filename,
+ const boost::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
+ const QString &mimeClass,
const QString &mime,
qint64 dsize,
const QSize &dimensions);
- void fileUploaded(const QString &roomid,
- const QString &filename,
- const QString &url,
- const QString &mime,
- qint64 dsize);
- void audioUploaded(const QString &roomid,
- const QString &filename,
- const QString &url,
- const QString &mime,
- qint64 dsize);
- void videoUploaded(const QString &roomid,
- const QString &filename,
- const QString &url,
- const QString &mime,
- qint64 dsize);
void contentLoaded();
void closing();
@@ -125,8 +114,6 @@ signals:
void showUserSettingsPage();
void showOverlayProgressBar();
- void removeTimelineEvent(const QString &room_id, const QString &event_id);
-
void ownProfileOk();
void setUserDisplayName(const QString &name);
void setUserAvatar(const QString &avatar);
diff --git a/src/ColorImageProvider.cpp b/src/ColorImageProvider.cpp
new file mode 100644
index 00000000..92e4732b
--- /dev/null
+++ b/src/ColorImageProvider.cpp
@@ -0,0 +1,30 @@
+#include "ColorImageProvider.h"
+
+#include "Logging.h"
+#include <QPainter>
+
+QPixmap
+ColorImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &)
+{
+ auto args = id.split('?');
+
+ nhlog::ui()->info("Loading {}, source is {}", id.toStdString(), args[0].toStdString());
+
+ QPixmap source(args[0]);
+
+ if (size)
+ *size = QSize(source.width(), source.height());
+
+ if (args.size() < 2)
+ return source;
+
+ QColor color(args[1]);
+
+ QPixmap colorized = source;
+ QPainter painter(&colorized);
+ painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
+ painter.fillRect(colorized.rect(), color);
+ painter.end();
+
+ return colorized;
+}
diff --git a/src/ColorImageProvider.h b/src/ColorImageProvider.h
new file mode 100644
index 00000000..21f36c12
--- /dev/null
+++ b/src/ColorImageProvider.h
@@ -0,0 +1,11 @@
+#include <QQuickImageProvider>
+
+class ColorImageProvider : public QQuickImageProvider
+{
+public:
+ ColorImageProvider()
+ : QQuickImageProvider(QQuickImageProvider::Pixmap)
+ {}
+
+ QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override;
+};
diff --git a/src/Logging.cpp b/src/Logging.cpp
index 32287582..126b3781 100644
--- a/src/Logging.cpp
+++ b/src/Logging.cpp
@@ -5,14 +5,43 @@
#include "spdlog/sinks/stdout_color_sinks.h"
#include <iostream>
+#include <QString>
+#include <QtGlobal>
+
namespace {
std::shared_ptr<spdlog::logger> db_logger = nullptr;
std::shared_ptr<spdlog::logger> net_logger = nullptr;
std::shared_ptr<spdlog::logger> crypto_logger = nullptr;
std::shared_ptr<spdlog::logger> ui_logger = nullptr;
+std::shared_ptr<spdlog::logger> qml_logger = nullptr;
constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6;
constexpr auto MAX_LOG_FILES = 3;
+
+void
+qmlMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
+{
+ std::string localMsg = msg.toStdString();
+ const char *file = context.file ? context.file : "";
+ const char *function = context.function ? context.function : "";
+ switch (type) {
+ case QtDebugMsg:
+ nhlog::qml()->debug("{} ({}:{}, {})", localMsg, file, context.line, function);
+ break;
+ case QtInfoMsg:
+ nhlog::qml()->info("{} ({}:{}, {})", localMsg, file, context.line, function);
+ break;
+ case QtWarningMsg:
+ nhlog::qml()->warn("{} ({}:{}, {})", localMsg, file, context.line, function);
+ break;
+ case QtCriticalMsg:
+ nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function);
+ break;
+ case QtFatalMsg:
+ nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function);
+ break;
+ }
+}
}
namespace nhlog {
@@ -35,12 +64,15 @@ init(const std::string &file_path)
db_logger = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks));
crypto_logger =
std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks));
+ qml_logger = std::make_shared<spdlog::logger>("qml", std::begin(sinks), std::end(sinks));
if (nheko::enable_debug_log) {
db_logger->set_level(spdlog::level::trace);
ui_logger->set_level(spdlog::level::trace);
crypto_logger->set_level(spdlog::level::trace);
}
+
+ qInstallMessageHandler(qmlMessageHandler);
}
std::shared_ptr<spdlog::logger>
@@ -66,4 +98,10 @@ crypto()
{
return crypto_logger;
}
+
+std::shared_ptr<spdlog::logger>
+qml()
+{
+ return qml_logger;
+}
}
diff --git a/src/Logging.h b/src/Logging.h
index e54f3c3f..f572afae 100644
--- a/src/Logging.h
+++ b/src/Logging.h
@@ -19,5 +19,8 @@ db();
std::shared_ptr<spdlog::logger>
crypto();
+std::shared_ptr<spdlog::logger>
+qml();
+
extern bool enable_debug_log_from_commandline;
}
diff --git a/src/MatrixClient.h b/src/MatrixClient.h
index 2af57267..c77b1183 100644
--- a/src/MatrixClient.h
+++ b/src/MatrixClient.h
@@ -20,16 +20,6 @@ Q_DECLARE_METATYPE(nlohmann::json)
Q_DECLARE_METATYPE(std::vector<std::string>)
Q_DECLARE_METATYPE(std::vector<QString>)
-class MediaProxy : public QObject
-{
- Q_OBJECT
-
-signals:
- void imageDownloaded(const QPixmap &);
- void imageSaved(const QString &, const QByteArray &);
- void fileDownloaded(const QByteArray &);
-};
-
namespace http {
mtx::http::Client *
client();
diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp
new file mode 100644
index 00000000..edf6ceb5
--- /dev/null
+++ b/src/MxcImageProvider.cpp
@@ -0,0 +1,83 @@
+#include "MxcImageProvider.h"
+
+#include "Cache.h"
+
+void
+MxcImageResponse::run()
+{
+ if (m_requestedSize.isValid() && !m_encryptionInfo) {
+ QString fileName = QString("%1_%2x%3_crop")
+ .arg(m_id)
+ .arg(m_requestedSize.width())
+ .arg(m_requestedSize.height());
+
+ auto data = cache::client()->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);
+ emit finished();
+ return;
+ }
+
+ mtx::http::ThumbOpts opts;
+ opts.mxc_url = "mxc://" + m_id.toStdString();
+ opts.width = m_requestedSize.width() > 0 ? m_requestedSize.width() : -1;
+ opts.height = m_requestedSize.height() > 0 ? m_requestedSize.height() : -1;
+ opts.method = "crop";
+ http::client()->get_thumbnail(
+ opts, [this, fileName](const std::string &res, mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->error("Failed to download image {}",
+ m_id.toStdString());
+ m_error = "Failed download";
+ emit finished();
+
+ return;
+ }
+
+ auto data = QByteArray(res.data(), res.size());
+ cache::client()->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);
+ if (!data.isNull() && m_image.loadFromData(data)) {
+ m_image.setText("mxc url", "mxc://" + m_id);
+ emit finished();
+ return;
+ }
+
+ http::client()->download(
+ "mxc://" + m_id.toStdString(),
+ [this](const std::string &res,
+ const std::string &,
+ const std::string &originalFilename,
+ mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->error("Failed to download image {}",
+ m_id.toStdString());
+ m_error = "Failed download";
+ emit finished();
+
+ return;
+ }
+
+ auto temp = res;
+ if (m_encryptionInfo)
+ temp = mtx::crypto::to_string(
+ mtx::crypto::decrypt_file(temp, m_encryptionInfo.value()));
+
+ auto data = QByteArray(temp.data(), temp.size());
+ m_image.loadFromData(data);
+ m_image.setText("original filename",
+ QString::fromStdString(originalFilename));
+ m_image.setText("mxc url", "mxc://" + m_id);
+ cache::client()->saveImage(m_id, data);
+
+ emit finished();
+ });
+ }
+}
diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h
new file mode 100644
index 00000000..2c197a13
--- /dev/null
+++ b/src/MxcImageProvider.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <QQuickAsyncImageProvider>
+#include <QQuickImageResponse>
+
+#include <QImage>
+#include <QThreadPool>
+
+#include <mtx/common.hpp>
+
+#include <boost/optional.hpp>
+
+class MxcImageResponse
+ : public QQuickImageResponse
+ , public QRunnable
+{
+public:
+ MxcImageResponse(const QString &id,
+ const QSize &requestedSize,
+ boost::optional<mtx::crypto::EncryptedFile> encryptionInfo)
+ : m_id(id)
+ , m_requestedSize(requestedSize)
+ , m_encryptionInfo(encryptionInfo)
+ {
+ setAutoDelete(false);
+ }
+
+ QQuickTextureFactory *textureFactory() const override
+ {
+ return QQuickTextureFactory::textureFactoryForImage(m_image);
+ }
+ QString errorString() const override { return m_error; }
+
+ void run() override;
+
+ QString m_id, m_error;
+ QSize m_requestedSize;
+ QImage m_image;
+ boost::optional<mtx::crypto::EncryptedFile> m_encryptionInfo;
+};
+
+class MxcImageProvider
+ : public QObject
+ , public QQuickAsyncImageProvider
+{
+ Q_OBJECT
+public slots:
+ QQuickImageResponse *requestImageResponse(const QString &id,
+ const QSize &requestedSize) override
+ {
+ boost::optional<mtx::crypto::EncryptedFile> info;
+ auto temp = infos.find("mxc://" + id);
+ if (temp != infos.end())
+ info = *temp;
+
+ MxcImageResponse *response = new MxcImageResponse(id, requestedSize, info);
+ pool.start(response);
+ return response;
+ }
+
+ void addEncryptionInfo(mtx::crypto::EncryptedFile info)
+ {
+ infos.insert(QString::fromStdString(info.url), info);
+ }
+
+private:
+ QThreadPool pool;
+ QHash<QString, mtx::crypto::EncryptedFile> infos;
+};
diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp
index 8aadbea2..8bebb0f5 100644
--- a/src/RoomInfoListItem.cpp
+++ b/src/RoomInfoListItem.cpp
@@ -118,7 +118,7 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *pare
// so we can't use them for sorting.
if (roomType_ == RoomType::Invited)
lastMsgInfo_ = {
- emptyEventId, "-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)};
+ emptyEventId, "-", "-", "-", QDateTime::currentDateTime().addYears(10)};
}
void
@@ -142,7 +142,7 @@ RoomInfoListItem::resizeEvent(QResizeEvent *)
void
RoomInfoListItem::paintEvent(QPaintEvent *event)
{
- bool rounded = QSettings().value("user/avatar/circles", true).toBool();
+ bool rounded = QSettings().value("user/avatar_circles", true).toBool();
Q_UNUSED(event);
@@ -210,33 +210,11 @@ RoomInfoListItem::paintEvent(QPaintEvent *event)
p.setFont(QFont{});
p.setPen(subtitlePen);
- // The limit is the space between the end of the avatar and the start of the
- // timestamp.
- int usernameLimit =
- std::max(0, width() - 3 * wm.padding - msgStampWidth - wm.iconSize - 20);
- auto userName =
- metrics.elidedText(lastMsgInfo_.username, Qt::ElideRight, usernameLimit);
-
- p.setFont(QFont{});
- p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), userName);
-
-#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
- int nameWidth = QFontMetrics(QFont{}).width(userName);
-#else
- int nameWidth = QFontMetrics(QFont{}).horizontalAdvance(userName);
-#endif
- p.setFont(QFont{});
-
- // The limit is the space between the end of the username and the start of
- // the timestamp.
- int descriptionLimit =
- std::max(0,
- width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize -
- nameWidth - 5);
+ int descriptionLimit = std::max(
+ 0, width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize);
auto description =
metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit);
- p.drawText(QPoint(2 * wm.padding + wm.iconSize + nameWidth, bottom_y),
- description);
+ p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), description);
// We show the last message timestamp.
p.save();
diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp
index f723c01a..66700dbc 100644
--- a/src/TextInputWidget.cpp
+++ b/src/TextInputWidget.cpp
@@ -458,21 +458,16 @@ FilteredTextEdit::textChanged()
}
void
-FilteredTextEdit::uploadData(const QByteArray data, const QString &media, const QString &filename)
+FilteredTextEdit::uploadData(const QByteArray data,
+ const QString &mediaType,
+ const QString &filename)
{
QSharedPointer<QBuffer> buffer{new QBuffer{this}};
buffer->setData(data);
emit startedUpload();
- if (media == "image")
- emit image(buffer, filename);
- else if (media == "audio")
- emit audio(buffer, filename);
- else if (media == "video")
- emit video(buffer, filename);
- else
- emit file(buffer, filename);
+ emit media(buffer, mediaType, filename);
}
void
@@ -580,10 +575,7 @@ TextInputWidget::TextInputWidget(QWidget *parent)
connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage);
connect(input_, &FilteredTextEdit::reply, this, &TextInputWidget::sendReplyMessage);
connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command);
- connect(input_, &FilteredTextEdit::image, this, &TextInputWidget::uploadImage);
- connect(input_, &FilteredTextEdit::audio, this, &TextInputWidget::uploadAudio);
- connect(input_, &FilteredTextEdit::video, this, &TextInputWidget::uploadVideo);
- connect(input_, &FilteredTextEdit::file, this, &TextInputWidget::uploadFile);
+ connect(input_, &FilteredTextEdit::media, this, &TextInputWidget::uploadMedia);
connect(emojiBtn_,
SIGNAL(emojiSelected(const QString &)),
this,
@@ -642,14 +634,8 @@ TextInputWidget::openFileSelection()
const auto format = mime.name().split("/")[0];
QSharedPointer<QFile> file{new QFile{fileName, this}};
- if (format == "image")
- emit uploadImage(file, fileName);
- else if (format == "audio")
- emit uploadAudio(file, fileName);
- else if (format == "video")
- emit uploadVideo(file, fileName);
- else
- emit uploadFile(file, fileName);
+
+ emit uploadMedia(file, format, fileName);
showUploadSpinner();
}
diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h
index 71f794d1..d498be72 100644
--- a/src/TextInputWidget.h
+++ b/src/TextInputWidget.h
@@ -63,10 +63,7 @@ signals:
void message(QString);
void reply(QString, const RelatedInfo &);
void command(QString name, QString args);
- void image(QSharedPointer<QIODevice> data, const QString &filename);
- void audio(QSharedPointer<QIODevice> data, const QString &filename);
- void video(QSharedPointer<QIODevice> data, const QString &filename);
- void file(QSharedPointer<QIODevice> data, const QString &filename);
+ void media(QSharedPointer<QIODevice> data, QString mimeClass, const QString &filename);
//! Trigger the suggestion popup.
void showSuggestions(const QString &query);
@@ -179,10 +176,9 @@ signals:
void sendEmoteMessage(QString msg);
void heightChanged(int height);
- void uploadImage(const QSharedPointer<QIODevice> data, const QString &filename);
- void uploadFile(const QSharedPointer<QIODevice> data, const QString &filename);
- void uploadAudio(const QSharedPointer<QIODevice> data, const QString &filename);
- void uploadVideo(const QSharedPointer<QIODevice> data, const QString &filename);
+ void uploadMedia(const QSharedPointer<QIODevice> data,
+ QString mimeClass,
+ const QString &filename);
void sendJoinRoomRequest(const QString &room);
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 9fd033e9..1caea449 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -53,7 +53,7 @@ UserSettings::load()
isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool();
theme_ = settings.value("user/theme", defaultTheme_).toString();
font_ = settings.value("user/font_family", "default").toString();
- avatarCircles_ = settings.value("user/avatar/circles", true).toBool();
+ avatarCircles_ = settings.value("user/avatar_circles", true).toBool();
emojiFont_ = settings.value("user/emoji_font_family", "default").toString();
baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
@@ -119,9 +119,7 @@ UserSettings::save()
settings.setValue("start_in_tray", isStartInTrayEnabled_);
settings.endGroup();
- settings.beginGroup("avatar");
- settings.setValue("circles", avatarCircles_);
- settings.endGroup();
+ settings.setValue("avatar_circles", avatarCircles_);
settings.setValue("font_size", baseFontSize_);
settings.setValue("typing_notifications", isTypingNotificationsEnabled_);
diff --git a/src/Utils.cpp b/src/Utils.cpp
index c60adb58..3e59d912 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -40,9 +40,8 @@ utils::replaceEmoji(const QString &body)
for (auto &code : utf32_string) {
// TODO: Be more precise here.
if (code > 9000)
- fmtBody +=
- QString("<span style=\"font-family: " + userFontFamily + ";\">") +
- QString::fromUcs4(&code, 1) + "</span>";
+ fmtBody += QString("<font face=\"" + userFontFamily + "\">") +
+ QString::fromUcs4(&code, 1) + "</font>";
else
fmtBody += QString::fromUcs4(&code, 1);
}
@@ -147,11 +146,6 @@ utils::getMessageDescription(const TimelineEvent &event,
const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts);
DescInfo info;
- if (sender == localUser)
- info.username = QCoreApplication::translate("utils", "You");
- else
- info.username = username;
-
info.userid = sender;
info.body = QString(" %1").arg(messageDescription<Encrypted>());
info.timestamp = utils::descriptiveTime(ts);
@@ -324,19 +318,29 @@ utils::linkifyMessage(const QString &body)
return doc;
}
-QByteArray escapeRawHtml(const QByteArray &data) {
- QByteArray buffer;
- const size_t length = data.size();
- buffer.reserve(length);
- for(size_t pos = 0; pos != length; ++pos) {
- switch(data.at(pos)) {
- case '&': buffer.append("&"); break;
- case '<': buffer.append("<"); break;
- case '>': buffer.append(">"); break;
- default: buffer.append(data.at(pos)); break;
- }
- }
- return buffer;
+QByteArray
+escapeRawHtml(const QByteArray &data)
+{
+ QByteArray buffer;
+ const size_t length = data.size();
+ buffer.reserve(length);
+ for (size_t pos = 0; pos != length; ++pos) {
+ switch (data.at(pos)) {
+ case '&':
+ buffer.append("&");
+ break;
+ case '<':
+ buffer.append("<");
+ break;
+ case '>':
+ buffer.append(">");
+ break;
+ default:
+ buffer.append(data.at(pos));
+ break;
+ }
+ }
+ return buffer;
}
QString
@@ -362,7 +366,7 @@ utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html)
{
return QString("<mx-reply><blockquote><a "
"href=\"https://matrix.to/#/%1/%2\">In reply "
- "to</a>* <a href=\"https://matrix.to/#/%3\">%4</a><br "
+ "to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br"
"/>%5</blockquote></mx-reply>")
.arg(related.room,
QString::fromStdString(related.related_event),
@@ -378,9 +382,6 @@ utils::getQuoteBody(const RelatedInfo &related)
using MsgType = mtx::events::MessageType;
switch (related.type) {
- case MsgType::Text: {
- return markdownToHtml(related.quoted_body);
- }
case MsgType::File: {
return QString(QCoreApplication::translate("utils", "sent a file."));
}
diff --git a/src/Utils.h b/src/Utils.h
index 225754be..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 <QCoreApplication>
#include <QDateTime>
@@ -94,38 +90,72 @@ messageDescription(const QString &username = "",
using Video = mtx::events::RoomEvent<mtx::events::msg::Video>;
using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;
- // Sometimes the verb form of sent changes in some languages depending on the actor.
- auto remoteSent = QCoreApplication::translate(
- "message-description: ", "sent", "For when you are the sender");
- auto localSent = QCoreApplication::translate(
- "message-description:", "sent", "For when someone else is the sender");
- QString sentVerb = isLocal ? localSent : remoteSent;
- if (std::is_same<T, AudioItem>::value || std::is_same<T, Audio>::value) {
- return QCoreApplication::translate("message-description sent:", "%1 an audio clip")
- .arg(sentVerb);
- } else if (std::is_same<T, ImageItem>::value || std::is_same<T, Image>::value) {
- return QCoreApplication::translate("message-description sent:", "%1 an image")
- .arg(sentVerb);
- } else if (std::is_same<T, FileItem>::value || std::is_same<T, File>::value) {
- return QCoreApplication::translate("message-description sent:", "%1 a file")
- .arg(sentVerb);
- } else if (std::is_same<T, VideoItem>::value || std::is_same<T, Video>::value) {
- return QCoreApplication::translate("message-description sent:", "%1 a video clip")
- .arg(sentVerb);
- } else if (std::is_same<T, StickerItem>::value || std::is_same<T, Sticker>::value) {
- return QCoreApplication::translate("message-description sent:", "%1 a sticker")
- .arg(sentVerb);
+ if (std::is_same<T, Audio>::value) {
+ if (isLocal)
+ return QCoreApplication::translate("message-description sent:",
+ "You sent an audio clip");
+ else
+ return QCoreApplication::translate("message-description sent:",
+ "%1 sent an audio clip")
+ .arg(username);
+ } else if (std::is_same<T, Image>::value) {
+ if (isLocal)
+ return QCoreApplication::translate("message-description sent:",
+ "You sent an image");
+ else
+ return QCoreApplication::translate("message-description sent:",
+ "%1 sent an image")
+ .arg(username);
+ } else if (std::is_same<T, File>::value) {
+ if (isLocal)
+ return QCoreApplication::translate("message-description sent:",
+ "You sent a file");
+ else
+ return QCoreApplication::translate("message-description sent:",
+ "%1 sent a file")
+ .arg(username);
+ } else if (std::is_same<T, Video>::value) {
+ if (isLocal)
+ return QCoreApplication::translate("message-description sent:",
+ "You sent a video");
+ else
+ return QCoreApplication::translate("message-description sent:",
+ "%1 sent a video")
+ .arg(username);
+ } else if (std::is_same<T, Sticker>::value) {
+ if (isLocal)
+ return QCoreApplication::translate("message-description sent:",
+ "You sent a sticker");
+ else
+ return QCoreApplication::translate("message-description sent:",
+ "%1 sent a sticker")
+ .arg(username);
} else if (std::is_same<T, Notice>::value) {
- return QCoreApplication::translate("message-description sent:", "%1 a notification")
- .arg(sentVerb);
+ if (isLocal)
+ return QCoreApplication::translate("message-description sent:",
+ "You sent a notification");
+ else
+ return QCoreApplication::translate("message-description sent:",
+ "%1 sent a notification")
+ .arg(username);
} else if (std::is_same<T, Text>::value) {
- return QString(": %1").arg(body);
+ if (isLocal)
+ return QCoreApplication::translate("message-description sent:", "You: %1")
+ .arg(body);
+ else
+ return QCoreApplication::translate("message-description sent:", "%1: %2")
+ .arg(username)
+ .arg(body);
} else if (std::is_same<T, Emote>::value) {
return QString("* %1 %2").arg(username).arg(body);
} else if (std::is_same<T, Encrypted>::value) {
- return QCoreApplication::translate("message-description sent:",
- "%1 an encrypted message")
- .arg(sentVerb);
+ if (isLocal)
+ return QCoreApplication::translate("message-description sent:",
+ "You sent an encrypted message");
+ else
+ return QCoreApplication::translate("message-description sent:",
+ "%1 sent an encrypted message")
+ .arg(username);
} else {
return QCoreApplication::translate("utils", "Unknown Message Type");
}
@@ -135,29 +165,19 @@ template<class T, class Event>
DescInfo
createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id)
{
- using Text = mtx::events::RoomEvent<mtx::events::msg::Text>;
- using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>;
-
const auto msg = boost::get<T>(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);
- bool isText = std::is_same<T, Text>::value;
- bool isEmote = std::is_same<T, Emote>::value;
-
- return DescInfo{
- QString::fromStdString(msg.event_id),
- isEmote ? ""
- : (sender == localUser ? QCoreApplication::translate("utils", "You") : username),
- sender,
- (isText || isEmote)
- ? messageDescription<T>(
- username, QString::fromStdString(msg.content.body).trimmed(), sender == localUser)
- : QString(" %1").arg(messageDescription<T>()),
- utils::descriptiveTime(ts),
- ts};
+ return DescInfo{QString::fromStdString(msg.event_id),
+ sender,
+ messageDescription<T>(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.
diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp
index dd9cd03a..cbdd351c 100644
--- a/src/dialogs/ImageOverlay.cpp
+++ b/src/dialogs/ImageOverlay.cpp
@@ -41,7 +41,6 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent)
setAttribute(Qt::WA_DeleteOnClose, true);
setWindowState(Qt::WindowFullScreen);
- // Deprecated in 5.13: screen_ = QApplication::desktop()->availableGeometry();
screen_ = QGuiApplication::primaryScreen()->availableGeometry();
move(QApplication::desktop()->mapToGlobal(screen_.topLeft()));
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 <QAbstractSlider>
+#include <QLabel>
#include <QListWidgetItem>
#include <QPainter>
#include <QPushButton>
diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp
index 00b034cc..25909cd8 100644
--- a/src/dialogs/RoomSettings.cpp
+++ b/src/dialogs/RoomSettings.cpp
@@ -488,7 +488,7 @@ RoomSettings::retrieveRoomInfo()
usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString());
info_ = cache::client()->singleRoomInfo(room_id_.toStdString());
setAvatar();
- } catch (const lmdb::error &e) {
+ } catch (const lmdb::error &) {
nhlog::db()->warn("failed to retrieve room info from cache: {}",
room_id_.toStdString());
}
diff --git a/src/popups/UserMentions.cpp b/src/popups/UserMentions.cpp
index 3480959a..3be5c462 100644
--- a/src/popups/UserMentions.cpp
+++ b/src/popups/UserMentions.cpp
@@ -7,7 +7,7 @@
#include "ChatPage.h"
#include "Logging.h"
#include "UserMentions.h"
-#include "timeline/TimelineItem.h"
+//#include "timeline/TimelineItem.h"
using namespace popups;
@@ -116,39 +116,46 @@ UserMentions::pushItem(const QString &event_id,
const QString &room_id,
const QString ¤t_room_id)
{
- setUpdatesEnabled(false);
-
- // Add to the 'all' section
- TimelineItem *view_item = new TimelineItem(
- mtx::events::MessageType::Text, user_id, body, true, room_id, all_scroll_widget_);
- view_item->setEventId(event_id);
- view_item->hide();
-
- all_scroll_layout_->addWidget(view_item);
- QTimer::singleShot(0, this, [view_item, this]() {
- view_item->show();
- view_item->adjustSize();
- setUpdatesEnabled(true);
- });
-
- // if it matches the current room... add it to the current room as well.
- if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) {
- // Add to the 'local' section
- TimelineItem *local_view_item = new TimelineItem(mtx::events::MessageType::Text,
- user_id,
- body,
- true,
- room_id,
- local_scroll_widget_);
- local_view_item->setEventId(event_id);
- local_view_item->hide();
- local_scroll_layout_->addWidget(local_view_item);
-
- QTimer::singleShot(0, this, [local_view_item]() {
- local_view_item->show();
- local_view_item->adjustSize();
- });
- }
+ (void)event_id;
+ (void)user_id;
+ (void)body;
+ (void)room_id;
+ (void)current_room_id;
+ // setUpdatesEnabled(false);
+ //
+ // // Add to the 'all' section
+ // TimelineItem *view_item = new TimelineItem(
+ // mtx::events::MessageType::Text, user_id, body, true, room_id,
+ // all_scroll_widget_);
+ // view_item->setEventId(event_id);
+ // view_item->hide();
+ //
+ // all_scroll_layout_->addWidget(view_item);
+ // QTimer::singleShot(0, this, [view_item, this]() {
+ // view_item->show();
+ // view_item->adjustSize();
+ // setUpdatesEnabled(true);
+ // });
+ //
+ // // if it matches the current room... add it to the current room as well.
+ // if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) {
+ // // Add to the 'local' section
+ // TimelineItem *local_view_item = new
+ // TimelineItem(mtx::events::MessageType::Text,
+ // user_id,
+ // body,
+ // true,
+ // room_id,
+ // local_scroll_widget_);
+ // local_view_item->setEventId(event_id);
+ // local_view_item->hide();
+ // local_scroll_layout_->addWidget(local_view_item);
+ //
+ // QTimer::singleShot(0, this, [local_view_item]() {
+ // local_view_item->show();
+ // local_view_item->adjustSize();
+ // });
+ // }
}
void
@@ -158,4 +165,4 @@ UserMentions::paintEvent(QPaintEvent *)
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
-}
\ No newline at end of file
+}
diff --git a/src/timeline/.TimelineItem.cpp.swp b/src/timeline/.TimelineItem.cpp.swp
deleted file mode 100644
index 75e03aeb..00000000
--- a/src/timeline/.TimelineItem.cpp.swp
+++ /dev/null
Binary files differdiff --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 <QQmlEngine>
+#include <QtGlobal>
+
+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<DelegateChoice>
+DelegateChooser::choices()
+{
+ return QQmlListProperty<DelegateChoice>(this,
+ this,
+ &DelegateChooser::appendChoice,
+ &DelegateChooser::choiceCount,
+ &DelegateChooser::choice,
+ &DelegateChooser::clearChoices);
+}
+
+void
+DelegateChooser::appendChoice(QQmlListProperty<DelegateChoice> *p, DelegateChoice *c)
+{
+ DelegateChooser *dc = static_cast<DelegateChooser *>(p->object);
+ dc->choices_.append(c);
+}
+
+int
+DelegateChooser::choiceCount(QQmlListProperty<DelegateChoice> *p)
+{
+ return static_cast<DelegateChooser *>(p->object)->choices_.count();
+}
+DelegateChoice *
+DelegateChooser::choice(QQmlListProperty<DelegateChoice> *p, int index)
+{
+ return static_cast<DelegateChooser *>(p->object)->choices_.at(index);
+}
+void
+DelegateChooser::clearChoices(QQmlListProperty<DelegateChoice> *p)
+{
+ static_cast<DelegateChooser *>(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<QQuickItem *>(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 <QQmlComponent>
+#include <QQmlIncubator>
+#include <QQmlListProperty>
+#include <QQuickItem>
+#include <QtCore/QObject>
+#include <QtCore/QVariant>
+
+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<DelegateChoice> choices READ choices CONSTANT)
+ Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged)
+
+ QQmlListProperty<DelegateChoice> 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<DelegateChoice *> choices_;
+ QQuickItem *child = nullptr;
+ DelegateIncubator incubator{*this};
+
+ static void appendChoice(QQmlListProperty<DelegateChoice> *, DelegateChoice *);
+ static int choiceCount(QQmlListProperty<DelegateChoice> *);
+ static DelegateChoice *choice(QQmlListProperty<DelegateChoice> *, int index);
+ static void clearChoices(QQmlListProperty<DelegateChoice> *);
+};
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 <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-#include <functional>
-
-#include <QContextMenuEvent>
-#include <QDesktopServices>
-#include <QFontDatabase>
-#include <QMenu>
-#include <QTimer>
-#include <QtGlobal>
-
-#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<QString>(this);
- connect(colorGenerating_,
- &QFutureWatcher<QString>::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("<em>%1</em>").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<ImageItem>(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<FileItem>(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<AudioItem>(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<VideoItem>(video, userid, withSender);
-}
-
-TimelineItem::TimelineItem(ImageItem *image,
- const mtx::events::RoomEvent<mtx::events::msg::Image> &event,
- bool with_sender,
- const QString &room_id,
- QWidget *parent)
- : QWidget(parent)
- , message_type_(mtx::events::MessageType::Image)
- , room_id_{room_id}
-{
- setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, 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<mtx::events::Sticker, StickerItem>(image, event, with_sender);
-
- markOwnMessagesAsReceived(event.sender);
-
- addSaveImageAction(image);
-}
-
-TimelineItem::TimelineItem(FileItem *file,
- const mtx::events::RoomEvent<mtx::events::msg::File> &event,
- bool with_sender,
- const QString &room_id,
- QWidget *parent)
- : QWidget(parent)
- , message_type_(mtx::events::MessageType::File)
- , room_id_{room_id}
-{
- setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>(
- file, event, with_sender);
-
- markOwnMessagesAsReceived(event.sender);
-}
-
-TimelineItem::TimelineItem(AudioItem *audio,
- const mtx::events::RoomEvent<mtx::events::msg::Audio> &event,
- bool with_sender,
- const QString &room_id,
- QWidget *parent)
- : QWidget(parent)
- , message_type_(mtx::events::MessageType::Audio)
- , room_id_{room_id}
-{
- setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>(
- audio, event, with_sender);
-
- markOwnMessagesAsReceived(event.sender);
-}
-
-TimelineItem::TimelineItem(VideoItem *video,
- const mtx::events::RoomEvent<mtx::events::msg::Video> &event,
- bool with_sender,
- const QString &room_id,
- QWidget *parent)
- : QWidget(parent)
- , message_type_(mtx::events::MessageType::Video)
- , room_id_{room_id}
-{
- setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>(
- video, event, with_sender);
-
- markOwnMessagesAsReceived(event.sender);
-}
-
-/*
- * Used to display remote notice messages.
- */
-TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &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<mtx::events::msg::Emote> &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("<em>%1</em>").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<mtx::events::msg::Text> &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<QString()> 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("<span style=\"color: #999\"> %1 </span>").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<EventProxy>();
- 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 <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QApplication>
-#include <QDateTime>
-#include <QHBoxLayout>
-#include <QLabel>
-#include <QLayout>
-#include <QPainter>
-#include <QSettings>
-#include <QTimer>
-
-#include <QtConcurrent>
-
-#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<mtx::events::msg::Notice> &e,
- bool with_sender,
- const QString &room_id,
- QWidget *parent = 0);
- TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &e,
- bool with_sender,
- const QString &room_id,
- QWidget *parent = 0);
- TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &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<mtx::events::msg::Image> &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<mtx::events::msg::File> &e,
- bool with_sender,
- const QString &room_id,
- QWidget *parent);
- TimelineItem(AudioItem *audio,
- const mtx::events::RoomEvent<mtx::events::msg::Audio> &e,
- bool with_sender,
- const QString &room_id,
- QWidget *parent);
- TimelineItem(VideoItem *video,
- const mtx::events::RoomEvent<mtx::events::msg::Video> &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<class Widget>
- void setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender);
-
- template<class Event, class Widget>
- 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<QString> *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<class Widget>
-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<Widget>()),
- 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<class Event, class Widget>
-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<Widget>()),
- 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..e3d87ae6
--- /dev/null
+++ b/src/timeline/TimelineModel.cpp
@@ -0,0 +1,1541 @@
+#include "TimelineModel.h"
+
+#include <algorithm>
+#include <type_traits>
+
+#include <QFileDialog>
+#include <QMimeDatabase>
+#include <QRegularExpression>
+#include <QStandardPaths>
+
+#include "ChatPage.h"
+#include "Logging.h"
+#include "MainWindow.h"
+#include "MxcImageProvider.h"
+#include "Olm.h"
+#include "TimelineViewManager.h"
+#include "Utils.h"
+#include "dialogs/RawMessage.h"
+
+Q_DECLARE_METATYPE(QModelIndex)
+
+namespace {
+template<class T>
+QString
+eventId(const mtx::events::RoomEvent<T> &event)
+{
+ return QString::fromStdString(event.event_id);
+}
+template<class T>
+QString
+roomId(const mtx::events::Event<T> &event)
+{
+ return QString::fromStdString(event.room_id);
+}
+template<class T>
+QString
+senderId(const mtx::events::RoomEvent<T> &event)
+{
+ return QString::fromStdString(event.sender);
+}
+
+template<class T>
+QDateTime
+eventTimestamp(const mtx::events::RoomEvent<T> &event)
+{
+ return QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
+}
+
+template<class T>
+std::string
+eventMsgType(const mtx::events::Event<T> &)
+{
+ return "";
+}
+template<class T>
+auto
+eventMsgType(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.msgtype)
+{
+ return e.content.msgtype;
+}
+
+template<class T>
+QString
+eventBody(const mtx::events::Event<T> &)
+{
+ return QString("");
+}
+template<class T>
+auto
+eventBody(const mtx::events::RoomEvent<T> &e)
+ -> std::enable_if_t<std::is_same<decltype(e.content.body), std::string>::value, QString>
+{
+ return QString::fromStdString(e.content.body);
+}
+
+template<class T>
+QString
+eventFormattedBody(const mtx::events::Event<T> &)
+{
+ return QString("");
+}
+template<class T>
+auto
+eventFormattedBody(const mtx::events::RoomEvent<T> &e)
+ -> std::enable_if_t<std::is_same<decltype(e.content.formatted_body), std::string>::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", "<br>");
+ }
+}
+
+template<class T>
+boost::optional<mtx::crypto::EncryptedFile>
+eventEncryptionInfo(const mtx::events::Event<T> &)
+{
+ return boost::none;
+}
+
+template<class T>
+auto
+eventEncryptionInfo(const mtx::events::RoomEvent<T> &e) -> std::enable_if_t<
+ std::is_same<decltype(e.content.file), boost::optional<mtx::crypto::EncryptedFile>>::value,
+ boost::optional<mtx::crypto::EncryptedFile>>
+{
+ return e.content.file;
+}
+
+template<class T>
+QString
+eventUrl(const mtx::events::Event<T> &)
+{
+ return "";
+}
+
+QString
+eventUrl(const mtx::events::StateEvent<mtx::events::state::Avatar> &e)
+{
+ return QString::fromStdString(e.content.url);
+}
+
+template<class T>
+auto
+eventUrl(const mtx::events::RoomEvent<T> &e)
+ -> std::enable_if_t<std::is_same<decltype(e.content.url), std::string>::value, QString>
+{
+ if (e.content.file)
+ return QString::fromStdString(e.content.file->url);
+ return QString::fromStdString(e.content.url);
+}
+
+template<class T>
+QString
+eventThumbnailUrl(const mtx::events::Event<T> &)
+{
+ return "";
+}
+template<class T>
+auto
+eventThumbnailUrl(const mtx::events::RoomEvent<T> &e)
+ -> std::enable_if_t<std::is_same<decltype(e.content.info.thumbnail_url), std::string>::value,
+ QString>
+{
+ return QString::fromStdString(e.content.info.thumbnail_url);
+}
+
+template<class T>
+QString
+eventFilename(const mtx::events::Event<T> &)
+{
+ return "";
+}
+QString
+eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Audio> &e)
+{
+ // body may be the original filename
+ return QString::fromStdString(e.content.body);
+}
+QString
+eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Video> &e)
+{
+ // body may be the original filename
+ return QString::fromStdString(e.content.body);
+}
+QString
+eventFilename(const mtx::events::RoomEvent<mtx::events::msg::Image> &e)
+{
+ // body may be the original filename
+ return QString::fromStdString(e.content.body);
+}
+QString
+eventFilename(const mtx::events::RoomEvent<mtx::events::msg::File> &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<class T>
+auto
+eventFilesize(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.info.size)
+{
+ return e.content.info.size;
+}
+
+template<class T>
+int64_t
+eventFilesize(const mtx::events::Event<T> &)
+{
+ return 0;
+}
+
+template<class T>
+QString
+eventMimeType(const mtx::events::Event<T> &)
+{
+ return QString();
+}
+template<class T>
+auto
+eventMimeType(const mtx::events::RoomEvent<T> &e)
+ -> std::enable_if_t<std::is_same<decltype(e.content.info.mimetype), std::string>::value, QString>
+{
+ return QString::fromStdString(e.content.info.mimetype);
+}
+
+template<class T>
+QString
+eventRelatesTo(const mtx::events::Event<T> &)
+{
+ return QString();
+}
+template<class T>
+auto
+eventRelatesTo(const mtx::events::RoomEvent<T> &e) -> std::enable_if_t<
+ std::is_same<decltype(e.content.relates_to.in_reply_to.event_id), std::string>::value,
+ QString>
+{
+ return QString::fromStdString(e.content.relates_to.in_reply_to.event_id);
+}
+
+template<class T>
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<T> &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<mtx::events::msg::Audio> &)
+{
+ return qml_mtx_events::EventType::AudioMessage;
+}
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::Emote> &)
+{
+ return qml_mtx_events::EventType::EmoteMessage;
+}
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::File> &)
+{
+ return qml_mtx_events::EventType::FileMessage;
+}
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::Image> &)
+{
+ return qml_mtx_events::EventType::ImageMessage;
+}
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::Notice> &)
+{
+ return qml_mtx_events::EventType::NoticeMessage;
+}
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::Text> &)
+{
+ return qml_mtx_events::EventType::TextMessage;
+}
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::Video> &)
+{
+ return qml_mtx_events::EventType::VideoMessage;
+}
+
+qml_mtx_events::EventType
+toRoomEventType(const mtx::events::Event<mtx::events::msg::Redacted> &)
+{
+ return qml_mtx_events::EventType::Redacted;
+}
+// ::EventType::Type toRoomEventType(const Event<mtx::events::msg::Location> &e) { return
+// ::EventType::LocationMessage; }
+
+template<class T>
+uint64_t
+eventHeight(const mtx::events::Event<T> &)
+{
+ return -1;
+}
+template<class T>
+auto
+eventHeight(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.info.h)
+{
+ return e.content.info.h;
+}
+template<class T>
+uint64_t
+eventWidth(const mtx::events::Event<T> &)
+{
+ return -1;
+}
+template<class T>
+auto
+eventWidth(const mtx::events::RoomEvent<T> &e) -> decltype(e.content.info.w)
+{
+ return e.content.info.w;
+}
+
+template<class T>
+double
+eventPropHeight(const mtx::events::RoomEvent<T> &e)
+{
+ auto w = eventWidth(e);
+ if (w == 0)
+ w = 1;
+
+ double prop = eventHeight(e) / (double)w;
+
+ return prop > 0 ? prop : 1.;
+}
+}
+
+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.removeOne(txn_id);
+ failed.insert(txn_id);
+ int idx = idToIndex(txn_id);
+ if (idx < 0) {
+ nhlog::ui()->warn("Failed index out of range");
+ return;
+ }
+ isProcessingPending = false;
+ emit dataChanged(index(idx, 0), index(idx, 0));
+ });
+ connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) {
+ pending.removeOne(txn_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);
+
+ isProcessingPending = false;
+ emit dataChanged(index(idx, 0), index(idx, 0));
+
+ if (pending.size() > 0)
+ emit nextPendingMessage();
+ });
+ connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) {
+ emit ChatPage::instance()->showNotification(msg);
+ });
+
+ connect(
+ this, &TimelineModel::nextPendingMessage, this, &TimelineModel::processOnePendingMessage);
+ connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage);
+}
+
+QHash<int, QByteArray>
+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<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&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--) {
+ auto tempEv = events.value(eventOrder[r]);
+ QDateTime prevDate = boost::apply_visitor(
+ [](const auto &e) -> QDateTime { return eventTimestamp(e); }, tempEv);
+ 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); }, tempEv);
+ 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(
+ utils::linkifyMessage(boost::apply_visitor(
+ [](const auto &e) -> QString { return eventFormattedBody(e); }, event)))
+ .remove("<mx-reply>")
+ .remove("</mx-reply>"));
+ 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<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+ &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<QString> ids = internalAddEvents(timeline.events);
+
+ if (ids.empty())
+ return;
+
+ beginInsertRows(QModelIndex(),
+ static_cast<int>(this->eventOrder.size()),
+ static_cast<int>(this->eventOrder.size() + ids.size() - 1));
+ this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end());
+ endInsertRows();
+
+ updateLastMessage();
+}
+
+template<typename T>
+auto
+isMessage(const mtx::events::RoomEvent<T> &e)
+ -> std::enable_if_t<std::is_same<decltype(e.content.msgtype), std::string>::value, bool>
+{
+ return true;
+}
+
+template<typename T>
+auto
+isMessage(const mtx::events::Event<T> &)
+{
+ return false;
+}
+
+void
+TimelineModel::updateLastMessage()
+{
+ for (auto it = eventOrder.rbegin(); it != eventOrder.rend(); ++it) {
+ auto event = events.value(*it);
+ if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
+ &event)) {
+ event = decryptEvent(*e).event;
+ }
+
+ if (!boost::apply_visitor([](const auto &e) -> bool { return isMessage(e); },
+ event))
+ continue;
+
+ auto description = utils::getMessageDescription(
+ event, QString::fromStdString(http::client()->user_id().to_string()), room_id_);
+ emit manager_->updateRoomsLastMessage(room_id_, description);
+ return;
+ }
+}
+
+std::vector<QString>
+TimelineModel::internalAddEvents(
+ const std::vector<mtx::events::collections::TimelineEvents> &timeline)
+{
+ std::vector<QString> 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<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&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::msg::Redacted> {
+ mtx::events::RoomEvent<mtx::events::msg::Redacted>
+ 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
+ }
+
+ if (auto event =
+ boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&e)) {
+ auto temp = decryptEvent(*event).event;
+ auto encInfo = boost::apply_visitor(
+ [](const auto &ev) -> boost::optional<mtx::crypto::EncryptedFile> {
+ return eventEncryptionInfo(ev);
+ },
+ temp);
+
+ if (encInfo)
+ emit newEncryptedImage(encInfo.value());
+ }
+
+ 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) &&
+ ChatPage::instance()->isActiveWindow()) {
+ 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<QString> ids = internalAddEvents(msgs.chunk);
+
+ if (!ids.empty()) {
+ beginInsertRows(QModelIndex(), 0, static_cast<int>(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<mtx::events::msg::Encrypted> &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<mtx::events::msg::Notice> 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<mtx::events::collections::TimelineEvents> 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);
+ if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
+ event = decryptEvent(*e).event;
+ }
+
+ 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(
+ "<mx-reply>.*</mx-reply>", 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<QString> &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<msg::Encrypted, EventType::RoomEncrypted>(
+ 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<int>(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<StateKeeper>([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<msg::Encrypted, EventType::RoomEncrypted>(
+ 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<int>(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());
+ emit messageFailed(QString::fromStdString(txn_id));
+ }
+ });
+
+ mtx::requests::QueryKeys req;
+ for (const auto &member : members)
+ req.device_keys[member] = {};
+
+ http::client()->query_keys(
+ req,
+ [keeper = std::move(keeper), megolm_payload, txn_id, 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<int>(err->status_code));
+ // TODO: Mark the event as failed. Communicate with the UI.
+ emit messageFailed(QString::fromStdString(txn_id));
+ 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<std::string, std::string> room_key_msgs;
+ std::map<std::string, DevicePublicKeys> 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<std::string> 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());
+ emit messageFailed(QString::fromStdString(txn_id));
+ } catch (const mtx::crypto::olm_exception &e) {
+ nhlog::crypto()->critical(
+ "failed to open outbound megolm session ({}): {}", room_id, e.what());
+ emit messageFailed(QString::fromStdString(txn_id));
+ }
+}
+
+void
+TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
+ const std::map<std::string, std::string> &room_keys,
+ const std::map<std::string, DevicePublicKeys> &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<int>(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;
+ });
+}
+
+struct SendMessageVisitor
+{
+ SendMessageVisitor(const QString &txn_id, TimelineModel *model)
+ : txn_id_qstr_(txn_id)
+ , model_(model)
+ {}
+
+ template<typename T>
+ void operator()(const mtx::events::Event<T> &)
+ {}
+
+ template<typename T,
+ std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0>
+ void operator()(const mtx::events::RoomEvent<T> &msg)
+
+ {
+ if (cache::client()->isRoomEncrypted(model_->room_id_.toStdString())) {
+ model_->sendEncryptedMessage(txn_id_qstr_.toStdString(),
+ nlohmann::json(msg.content));
+ } else {
+ QString txn_id_qstr = txn_id_qstr_;
+ TimelineModel *model = model_;
+ http::client()->send_room_message<T, mtx::events::EventType::RoomMessage>(
+ model->room_id_.toStdString(),
+ txn_id_qstr.toStdString(),
+ msg.content,
+ [txn_id_qstr, model](const mtx::responses::EventId &res,
+ mtx::http::RequestErr err) {
+ if (err) {
+ const int status_code =
+ static_cast<int>(err->status_code);
+ nhlog::net()->warn("[{}] failed to send message: {} {}",
+ txn_id_qstr.toStdString(),
+ err->matrix_error.error,
+ status_code);
+ emit model->messageFailed(txn_id_qstr);
+ }
+ emit model->messageSent(
+ txn_id_qstr, QString::fromStdString(res.event_id.to_string()));
+ });
+ }
+ }
+
+ QString txn_id_qstr_;
+ TimelineModel *model_;
+};
+
+void
+TimelineModel::processOnePendingMessage()
+{
+ if (isProcessingPending || pending.isEmpty())
+ return;
+
+ isProcessingPending = true;
+
+ QString txn_id_qstr = pending.first();
+
+ auto event = events.value(txn_id_qstr);
+ boost::apply_visitor(SendMessageVisitor{txn_id_qstr, this}, event);
+}
+
+void
+TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
+{
+ internalAddEvents({event});
+
+ QString txn_id_qstr =
+ boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, event);
+ beginInsertRows(QModelIndex(),
+ static_cast<int>(this->eventOrder.size()),
+ static_cast<int>(this->eventOrder.size()));
+ pending.push_back(txn_id_qstr);
+ this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr);
+ endInsertRows();
+ updateLastMessage();
+
+ if (!isProcessingPending)
+ emit nextPendingMessage();
+}
+
+void
+TimelineModel::saveMedia(QString eventId) const
+{
+ mtx::events::collections::TimelineEvents event = events.value(eventId);
+
+ if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
+ event = decryptEvent(*e).event;
+ }
+
+ QString mxcUrl =
+ boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event);
+ QString originalFilename =
+ boost::apply_visitor([](const auto &e) -> QString { return eventFilename(e); }, event);
+ QString mimeType =
+ boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event);
+
+ using EncF = boost::optional<mtx::crypto::EncryptedFile>;
+ EncF encryptionInfo =
+ boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event);
+
+ qml_mtx_events::EventType eventType = boost::apply_visitor(
+ [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, event);
+
+ 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(
+ manager_->getWidget(), dialogTitle, originalFilename, filterString);
+
+ if (filename.isEmpty())
+ return;
+
+ const auto url = mxcUrl.toStdString();
+
+ http::client()->download(
+ url,
+ [filename, url, encryptionInfo](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<int>(err->status_code));
+ return;
+ }
+
+ try {
+ auto temp = data;
+ if (encryptionInfo)
+ temp = mtx::crypto::to_string(
+ mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
+
+ QFile file(filename);
+
+ if (!file.open(QIODevice::WriteOnly))
+ return;
+
+ file.write(QByteArray(temp.data(), (int)temp.size()));
+ file.close();
+ } catch (const std::exception &e) {
+ nhlog::ui()->warn("Error while saving file to: {}", e.what());
+ }
+ });
+}
+
+void
+TimelineModel::cacheMedia(QString eventId)
+{
+ mtx::events::collections::TimelineEvents event = events.value(eventId);
+
+ if (auto e = boost::get<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
+ event = decryptEvent(*e).event;
+ }
+
+ QString mxcUrl =
+ boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event);
+ QString mimeType =
+ boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event);
+
+ using EncF = boost::optional<mtx::crypto::EncryptedFile>;
+ EncF encryptionInfo =
+ boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event);
+
+ // 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, encryptionInfo](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<int>(err->status_code));
+ return;
+ }
+
+ try {
+ auto temp = data;
+ if (encryptionInfo)
+ temp = mtx::crypto::to_string(
+ mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
+
+ QFile file(filename.filePath());
+
+ if (!file.open(QIODevice::WriteOnly))
+ return;
+
+ file.write(QByteArray(temp.data(), temp.size()));
+ file.close();
+ } catch (const std::exception &e) {
+ nhlog::ui()->warn("Error while saving file to: {}", e.what());
+ }
+
+ emit mediaCached(mxcUrl, filename.filePath());
+ });
+}
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
new file mode 100644
index 00000000..06c64acf
--- /dev/null
+++ b/src/timeline/TimelineModel.h
@@ -0,0 +1,242 @@
+#pragma once
+
+#include <QAbstractListModel>
+#include <QColor>
+#include <QDate>
+#include <QHash>
+#include <QSet>
+
+#include <mtx/common.hpp>
+#include <mtx/responses.hpp>
+
+#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<void()> &&fn)
+ : fn_(std::move(fn))
+ {}
+
+ ~StateKeeper() { fn_(); }
+
+private:
+ std::function<void()> 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<int, QByteArray> 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;
+ Q_INVOKABLE void cacheMedia(QString eventId);
+ Q_INVOKABLE void saveMedia(QString eventId) const;
+
+ void addEvents(const mtx::responses::Timeline &events);
+ template<class T>
+ void sendMessage(const T &msg);
+
+public slots:
+ void fetchHistory();
+ void setCurrentIndex(int index);
+ int currentIndex() const { return idToIndex(currentId); }
+ void markEventsAsRead(const std::vector<QString> &event_ids);
+
+private slots:
+ // Add old events at the top of the timeline.
+ void addBackwardsEvents(const mtx::responses::Messages &msgs);
+ void processOnePendingMessage();
+ void addPendingMessage(mtx::events::collections::TimelineEvents event);
+
+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);
+ void nextPendingMessage();
+ void newMessageToSend(mtx::events::collections::TimelineEvents event);
+ void mediaCached(QString mxcUrl, QString cacheUrl);
+ void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo);
+
+private:
+ DecryptionResult decryptEvent(
+ const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const;
+ std::vector<QString> internalAddEvents(
+ const std::vector<mtx::events::collections::TimelineEvents> &timeline);
+ void sendEncryptedMessage(const std::string &txn_id, nlohmann::json content);
+ void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
+ const std::map<std::string, std::string> &room_key,
+ const std::map<std::string, DevicePublicKeys> &pks,
+ const std::string &user_id,
+ const mtx::responses::ClaimKeys &res,
+ mtx::http::RequestErr err);
+ void updateLastMessage();
+ void readEvent(const std::string &id);
+
+ QHash<QString, mtx::events::collections::TimelineEvents> events;
+ QSet<QString> failed, read;
+ QList<QString> pending;
+ std::vector<QString> eventOrder;
+
+ QString room_id_;
+ QString prev_batch_token_;
+
+ bool isInitialSync = true;
+ bool paginationInProgress = false;
+ bool isProcessingPending = false;
+
+ QHash<QString, QColor> userColors;
+ QString currentId;
+
+ TimelineViewManager *manager_;
+
+ friend struct SendMessageVisitor;
+};
+
+template<class T>
+void
+TimelineModel::sendMessage(const T &msg)
+{
+ auto txn_id = http::client()->generate_txn_id();
+ mtx::events::RoomEvent<T> 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();
+
+ emit newMessageToSend(msgCopy);
+}
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 <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-#include <boost/variant.hpp>
-
-#include <QApplication>
-#include <QFileInfo>
-#include <QTimer>
-#include <QtConcurrent>
-
-#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<TimelineItem *>(widget);
- if (item)
- return item->descriptionMessage().datetime;
-
- auto infoMsg = qobject_cast<InfoMessage *>(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<msg::Audio>;
- using EmoteEvent = RoomEvent<msg::Emote>;
- using FileEvent = RoomEvent<msg::File>;
- using ImageEvent = RoomEvent<msg::Image>;
- using NoticeEvent = RoomEvent<msg::Notice>;
- using TextEvent = RoomEvent<msg::Text>;
- using VideoEvent = RoomEvent<msg::Video>;
-
- if (boost::get<RedactionEvent<msg::Redaction>>(&event) != nullptr) {
- auto redaction_event = boost::get<RedactionEvent<msg::Redaction>>(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<StateEvent<state::Encryption>>(&event) != nullptr) {
- auto msg = boost::get<StateEvent<state::Encryption>>(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<RoomEvent<msg::Audio>>(&event) != nullptr) {
- auto audio = boost::get<RoomEvent<msg::Audio>>(event);
- return processMessageEvent<AudioEvent, AudioItem>(audio, direction);
- } else if (boost::get<RoomEvent<msg::Emote>>(&event) != nullptr) {
- auto emote = boost::get<RoomEvent<msg::Emote>>(event);
- return processMessageEvent<EmoteEvent>(emote, direction);
- } else if (boost::get<RoomEvent<msg::File>>(&event) != nullptr) {
- auto file = boost::get<RoomEvent<msg::File>>(event);
- return processMessageEvent<FileEvent, FileItem>(file, direction);
- } else if (boost::get<RoomEvent<msg::Image>>(&event) != nullptr) {
- auto image = boost::get<RoomEvent<msg::Image>>(event);
- return processMessageEvent<ImageEvent, ImageItem>(image, direction);
- } else if (boost::get<RoomEvent<msg::Notice>>(&event) != nullptr) {
- auto notice = boost::get<RoomEvent<msg::Notice>>(event);
- return processMessageEvent<NoticeEvent>(notice, direction);
- } else if (boost::get<RoomEvent<msg::Text>>(&event) != nullptr) {
- auto text = boost::get<RoomEvent<msg::Text>>(event);
- return processMessageEvent<TextEvent>(text, direction);
- } else if (boost::get<RoomEvent<msg::Video>>(&event) != nullptr) {
- auto video = boost::get<RoomEvent<msg::Video>>(event);
- return processMessageEvent<VideoEvent, VideoItem>(video, direction);
- } else if (boost::get<Sticker>(&event) != nullptr) {
- return processMessageEvent<Sticker, StickerItem>(boost::get<Sticker>(event),
- direction);
- } else if (boost::get<EncryptedEvent<msg::Encrypted>>(&event) != nullptr) {
- auto res = parseEncryptedEvent(boost::get<EncryptedEvent<msg::Encrypted>>(event));
- auto widget = parseMessageEvent(res.event, direction);
-
- if (widget == nullptr)
- return nullptr;
-
- auto item = qobject_cast<TimelineItem *>(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<mtx::events::msg::Encrypted> &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<mtx::events::msg::Notice> 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<TimelineEvent> 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<TimelineEvent> events)
-{
- QtConcurrent::run(
- [events = std::move(events), room_id = room_id_, local_user = local_user_, this]() {
- std::vector<QString> 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<TimelineEvent> &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<TimelineEvent> &events)
-{
- std::vector<QWidget *> 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<TimelineItem *>(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<QString> &event_ids) {
- for (const auto &event : event_ids) {
- if (eventIds_.contains(event)) {
- auto widget = eventIds_[event];
- if (!widget)
- return;
-
- auto item = qobject_cast<TimelineItem *>(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<msg::Audio, EventType::RoomMessage>(
- room_id_.toStdString(),
- m.txn_id,
- toRoomMessage<msg::Audio>(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<msg::Image, EventType::RoomMessage>(
- room_id_.toStdString(),
- m.txn_id,
- toRoomMessage<msg::Image>(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<msg::Video, EventType::RoomMessage>(
- room_id_.toStdString(),
- m.txn_id,
- toRoomMessage<msg::Video>(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<msg::File, EventType::RoomMessage>(
- room_id_.toStdString(),
- m.txn_id,
- toRoomMessage<msg::File>(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<msg::Text, EventType::RoomMessage>(
- room_id_.toStdString(),
- m.txn_id,
- toRoomMessage<msg::Text>(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<msg::Emote, EventType::RoomMessage>(
- room_id_.toStdString(),
- m.txn_id,
- toRoomMessage<msg::Emote>(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<TimelineItem *>(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<TimelineItem *>(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<TimelineItem *>(prevWidget);
- auto nextItem = qobject_cast<TimelineItem *>(nextWidget);
-
- // ... or a date separator
- auto prevLabel = qobject_cast<DateSeparator *>(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<TimelineEvent> &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<TimelineEvent> &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<int>(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<mtx::events::msg::Audio>(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<mtx::events::msg::Image>(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<mtx::events::msg::Video>(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<mtx::events::msg::Emote>(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<mtx::events::msg::File>(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<mtx::events::msg::Text>(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::Audio>(msg));
- break;
- }
- case MessageType::Emote: {
- content = json(toRoomMessage<msg::Emote>(msg));
- break;
- }
- case MessageType::File: {
- content = json(toRoomMessage<msg::File>(msg));
- break;
- }
- case MessageType::Image: {
- content = json(toRoomMessage<msg::Image>(msg));
- break;
- }
- case MessageType::Text: {
- content = json(toRoomMessage<msg::Text>(msg));
- break;
- }
- case MessageType::Video: {
- content = json(toRoomMessage<msg::Video>(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<msg::Encrypted, EventType::RoomEncrypted>(
- 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<StateKeeper>(
- [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<msg::Encrypted, EventType::RoomEncrypted>(
- 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<int>(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<std::string, std::string> room_key_msgs;
- std::map<std::string, DevicePublicKeys> 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<std::string> 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<StateKeeper> keeper,
- const std::map<std::string, std::string> &room_keys,
- const std::map<std::string, DevicePublicKeys> &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<int>(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 <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QApplication>
-#include <QLayout>
-#include <QList>
-#include <QQueue>
-#include <QScrollArea>
-#include <QScrollBar>
-#include <QStyle>
-#include <QStyleOption>
-#include <QTimer>
-
-#include <mtx/events.hpp>
-#include <mtx/responses/messages.hpp>
-
-#include "../Utils.h"
-#include "MatrixClient.h"
-#include "timeline/TimelineItem.h"
-
-class StateKeeper
-{
-public:
- StateKeeper(std::function<void()> &&fn)
- : fn_(std::move(fn))
- {}
-
- ~StateKeeper() { fn_(); }
-
-private:
- std::function<void()> 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<class MessageT>
-MessageT
-toRoomMessage(const PendingMessage &) = delete;
-
-template<>
-mtx::events::msg::Audio
-toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m);
-
-template<>
-mtx::events::msg::Emote
-toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m);
-
-template<>
-mtx::events::msg::File
-toRoomMessage<mtx::events::msg::File>(const PendingMessage &);
-
-template<>
-mtx::events::msg::Image
-toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m);
-
-template<>
-mtx::events::msg::Text
-toRoomMessage<mtx::events::msg::Text>(const PendingMessage &);
-
-template<>
-mtx::events::msg::Video
-toRoomMessage<mtx::events::msg::Video>(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<class Widget, mtx::events::MessageType MsgType>
- 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<QString> &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<TimelineEvent> 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<mtx::events::msg::Encrypted> &e);
-
- void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
- const std::map<std::string, std::string> &room_key,
- const std::map<std::string, DevicePublicKeys> &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<TimelineEvent> &events);
- TimelineEvent findLastViewableEvent(const std::vector<TimelineEvent> &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<class Event, class Widget>
- TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction);
-
- // TODO: Remove this eventually.
- template<class Event>
- TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction);
-
- // For events with custom display widgets.
- template<class Event, class Widget>
- TimelineItem *createTimelineItem(const Event &event, bool withSender);
-
- // For events without custom display widgets.
- // TODO: All events should have custom widgets.
- template<class Event>
- 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<TimelineEvent> bottomMessages_;
- //! Messages received by /messages not added to the timeline.
- std::vector<TimelineEvent> topMessages_;
-
- //! Render the given timeline events to the bottom of the timeline.
- void renderBottomEvents(const std::vector<TimelineEvent> &events);
- //! Render the given timeline events to the top of the timeline.
- void renderTopEvents(const std::vector<TimelineEvent> &events);
-
- // The events currently rendered. Used for duplicate detection.
- QMap<QString, QWidget *> eventIds_;
- QQueue<PendingMessage> pending_msgs_;
- QList<PendingMessage> pending_sent_msgs_;
-};
-
-template<class Widget, mtx::events::MessageType MsgType>
-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<class Event>
-TimelineItem *
-TimelineView::createTimelineItem(const Event &event, bool withSender)
-{
- TimelineItem *item = new TimelineItem(event, withSender, room_id_, scroll_widget_);
- return item;
-}
-
-template<class Event, class Widget>
-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<class Event>
-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>(event, with_sender);
-
- eventIds_[event_id] = item;
-
- return item;
-}
-
-template<class Event, class Widget>
-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, Widget>(event, with_sender);
-
- eventIds_[event_id] = item;
-
- return item;
-}
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 86505481..6e18d111 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -1,340 +1,292 @@
-/*
- * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
+#include "TimelineViewManager.h"
-#include <random>
+#include <QMetaType>
+#include <QPalette>
+#include <QQmlContext>
-#include <QApplication>
-#include <QFileInfo>
-#include <QSettings>
-
-#include "Cache.h"
+#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<QString> &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)
-{
- auto view = views_[room_id];
-
- if (view)
- view->removeEvent(event_id);
-}
-
-void
-TimelineViewManager::queueTextMessage(const QString &msg)
-{
- if (active_room_.isEmpty())
- return;
-
- auto room_id = active_room_;
- auto view = views_[room_id];
-
- view->addUserMessage(mtx::events::MessageType::Text, msg);
-}
-
-void
-TimelineViewManager::queueEmoteMessage(const QString &msg)
+TimelineViewManager::TimelineViewManager(QWidget *parent)
+ : imgProvider(new MxcImageProvider())
+ , colorImgProvider(new ColorImageProvider())
{
- if (active_room_.isEmpty())
- return;
-
- auto room_id = active_room_;
- auto view = views_[room_id];
+ qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
+ "im.nheko",
+ 1,
+ 0,
+ "MtxEvent",
+ "Can't instantiate enum!");
+ qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
+ qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
- view->addUserMessage(mtx::events::MessageType::Emote, msg);
-}
+#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);
-void
-TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related)
-{
- if (active_room_.isEmpty())
- return;
-
- auto room_id = active_room_;
- auto view = views_[room_id];
+ 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"));
- view->addUserMessage(mtx::events::MessageType::Text, reply, related);
+ connect(dynamic_cast<ChatPage *>(parent),
+ &ChatPage::themeChanged,
+ this,
+ &TimelineViewManager::updateColorPalette);
}
void
-TimelineViewManager::queueImageMessage(const QString &roomid,
- const QString &filename,
- const QString &url,
- const QString &mime,
- uint64_t size,
- const QSize &dimensions)
+TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
{
- if (!timelineViewExists(roomid)) {
- nhlog::ui()->warn("Cannot send m.image message to a non-managed view");
- return;
+ 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);
}
- auto view = views_[roomid];
-
- view->addUserMessage<ImageItem, mtx::events::MessageType::Image>(
- url, filename, mime, size, dimensions);
+ this->isInitialSync_ = false;
+ emit initialSyncChanged(false);
}
void
-TimelineViewManager::queueFileMessage(const QString &roomid,
- const QString &filename,
- const QString &url,
- const QString &mime,
- uint64_t size)
+TimelineViewManager::addRoom(const QString &room_id)
{
- if (!timelineViewExists(roomid)) {
- nhlog::ui()->warn("cannot send m.file message to a non-managed view");
- return;
+ if (!models.contains(room_id)) {
+ QSharedPointer<TimelineModel> newRoom(new TimelineModel(this, room_id));
+ connect(newRoom.data(),
+ &TimelineModel::newEncryptedImage,
+ imgProvider,
+ &MxcImageProvider::addEncryptionInfo);
+ models.insert(room_id, std::move(newRoom));
}
-
- auto view = views_[roomid];
-
- view->addUserMessage<FileItem, mtx::events::MessageType::File>(url, filename, mime, size);
}
void
-TimelineViewManager::queueAudioMessage(const QString &roomid,
- const QString &filename,
- const QString &url,
- const QString &mime,
- uint64_t size)
+TimelineViewManager::setHistoryView(const QString &room_id)
{
- if (!timelineViewExists(roomid)) {
- nhlog::ui()->warn("cannot send m.audio message to a non-managed view");
- return;
- }
-
- auto view = views_[roomid];
+ nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
- view->addUserMessage<AudioItem, mtx::events::MessageType::Audio>(url, filename, mime, size);
+ 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::queueVideoMessage(const QString &roomid,
- const QString &filename,
- const QString &url,
- const QString &mime,
- uint64_t size)
+TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const
{
- if (!timelineViewExists(roomid)) {
- nhlog::ui()->warn("cannot send m.video message to a non-managed view");
- return;
- }
-
- auto view = views_[roomid];
+ QQuickImageResponse *imgResponse =
+ imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize());
+ connect(imgResponse, &QQuickImageResponse::finished, this, [this, eventId, 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());
- view->addUserMessage<VideoItem, mtx::events::MessageType::Video>(url, filename, mime, size);
+ auto imgDialog = new dialogs::ImageOverlay(pixmap);
+ imgDialog->show();
+ connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId]() {
+ timeline_->saveMedia(eventId);
+ });
+ });
}
void
-TimelineViewManager::initialize(const mtx::responses::Rooms &rooms)
+TimelineViewManager::updateReadReceipts(const QString &room_id,
+ const std::vector<QString> &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<QString, mtx::responses::Timeline> &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<TimelineView>(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<std::string> &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;
+ mtx::events::msg::Text text = {};
- // Create a history view with the room events.
- TimelineView *view = new TimelineView(room.timeline, room_id);
- views_.emplace(room_id, QSharedPointer<TimelineView>(view));
+ 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<TimelineView>(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 boost::optional<mtx::crypto::EncryptedFile> &file,
+ 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();
+ image.file = file;
+ models.value(roomid)->sendMessage(image);
}
void
-TimelineViewManager::setHistoryView(const QString &room_id)
+TimelineViewManager::queueFileMessage(
+ const QString &roomid,
+ const QString &filename,
+ const boost::optional<mtx::crypto::EncryptedFile> &encryptedFile,
+ 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();
+ file.file = encryptedFile;
+ models.value(roomid)->sendMessage(file);
}
-QString
-TimelineViewManager::chooseRandomColor()
+void
+TimelineViewManager::queueAudioMessage(const QString &roomid,
+ const QString &filename,
+ const boost::optional<mtx::crypto::EncryptedFile> &file,
+ const QString &url,
+ const QString &mime,
+ uint64_t dsize)
{
- std::random_device random_device;
- std::mt19937 engine{random_device()};
- std::uniform_real_distribution<float> 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();
+ audio.file = file;
+ models.value(roomid)->sendMessage(audio);
}
-bool
-TimelineViewManager::hasLoaded() const
+void
+TimelineViewManager::queueVideoMessage(const QString &roomid,
+ const QString &filename,
+ const boost::optional<mtx::crypto::EncryptedFile> &file,
+ 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();
+ video.file = file;
+ models.value(roomid)->sendMessage(video);
}
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index b52136d9..9e8de616 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -1,98 +1,97 @@
-/*
- * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
#pragma once
+#include <QQuickView>
+#include <QQuickWidget>
#include <QSharedPointer>
-#include <QStackedWidget>
+#include <QWidget>
-#include <mtx.hpp>
+#include <mtx/common.hpp>
+#include <mtx/responses.hpp>
+#include "Cache.h"
+#include "Logging.h"
+#include "TimelineModel.h"
#include "Utils.h"
-class QFile;
-
-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);
+ Q_PROPERTY(
+ TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged)
+ Q_PROPERTY(
+ bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
- // Initialize with timeline events.
- void initialize(const mtx::responses::Rooms &rooms);
- // Empty initialization.
- void initialize(const std::vector<std::string> &rooms);
-
- 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(); }
+ void addRoom(const QString &room_id);
- // Check if all the timelines have been loaded.
- bool hasLoaded() const;
+ void clearAll() { models.clear(); }
- static QString chooseRandomColor();
+ Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
+ Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
+ Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const;
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 initialSyncChanged(bool isInitialSync);
public slots:
void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
- void removeTimelineEvent(const QString &room_id, const QString &event_id);
void initWithMessages(const std::map<QString, mtx::responses::Timeline> &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 boost::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
const QString &mime,
uint64_t dsize,
const QSize &dimensions);
void queueFileMessage(const QString &roomid,
const QString &filename,
+ const boost::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
const QString &mime,
uint64_t dsize);
void queueAudioMessage(const QString &roomid,
const QString &filename,
+ const boost::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
const QString &mime,
uint64_t dsize);
void queueVideoMessage(const QString &roomid,
const QString &filename,
+ const boost::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
const QString &mime,
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(); }
+#ifdef USE_QUICK_VIEW
+ QQuickView *view;
+#else
+ QQuickWidget *view;
+#endif
+ QWidget *container;
+
+ MxcImageProvider *imgProvider;
+ ColorImageProvider *colorImgProvider;
- QString active_room_;
- std::map<QString, QSharedPointer<TimelineView>> views_;
+ QHash<QString, QSharedPointer<TimelineModel>> models;
+ TimelineModel *timeline_ = nullptr;
+ bool isInitialSync_ = true;
};
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 <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-#include <QBrush>
-#include <QDesktopServices>
-#include <QFile>
-#include <QFileDialog>
-#include <QPainter>
-#include <QPixmap>
-#include <QtGlobal>
-
-#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<mtx::events::msg::Audio> &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<MediaProxy>();
- 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 <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QEvent>
-#include <QIcon>
-#include <QMediaPlayer>
-#include <QMouseEvent>
-#include <QSharedPointer>
-#include <QWidget>
-
-#include <mtx.hpp>
-
-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<mtx::events::msg::Audio> &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<mtx::events::msg::Audio> 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 <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-#include <QBrush>
-#include <QDesktopServices>
-#include <QFile>
-#include <QFileDialog>
-#include <QPainter>
-#include <QPixmap>
-#include <QtGlobal>
-
-#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<mtx::events::msg::File> &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<MediaProxy>();
- 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 <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QEvent>
-#include <QIcon>
-#include <QMouseEvent>
-#include <QSharedPointer>
-#include <QWidget>
-
-#include <mtx.hpp>
-
-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<mtx::events::msg::File> &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<mtx::events::msg::File> 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 <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-#include <QBrush>
-#include <QDesktopServices>
-#include <QFileDialog>
-#include <QFileInfo>
-#include <QPainter>
-#include <QPixmap>
-#include <QUuid>
-#include <QtGlobal>
-
-#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<MediaProxy>();
- 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<int>(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<mtx::events::msg::Image> &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<MediaProxy>();
- 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<int>(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 <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QEvent>
-#include <QMouseEvent>
-#include <QSharedPointer>
-#include <QWidget>
-
-#include <mtx.hpp>
-
-namespace dialogs {
-class ImageOverlay;
-}
-
-class ImageItem : public QWidget
-{
- Q_OBJECT
-public:
- ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &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<mtx::events::msg::Image> 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 <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-#include <QLabel>
-#include <QVBoxLayout>
-
-#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<mtx::events::msg::Video> &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("<a href=%1>%2</a>").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 <siderisk@auth.gr>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QEvent>
-#include <QLabel>
-#include <QSharedPointer>
-#include <QUrl>
-#include <QWidget>
-
-#include <mtx.hpp>
-
-class VideoItem : public QWidget
-{
- Q_OBJECT
-
-public:
- VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &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<mtx::events::msg::Video> event_;
-};
diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp
index 501a8968..e4a90f81 100644
--- a/src/ui/Avatar.cpp
+++ b/src/ui/Avatar.cpp
@@ -101,7 +101,7 @@ Avatar::setIcon(const QIcon &icon)
void
Avatar::paintEvent(QPaintEvent *)
{
- bool rounded = QSettings().value("user/avatar/circles", true).toBool();
+ bool rounded = QSettings().value("user/avatar_circles", true).toBool();
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
|