From 7c630b13630ca3c34a2e13616b579d9168905dbc Mon Sep 17 00:00:00 2001 From: redsky17 Date: Mon, 27 May 2019 16:04:35 -0400 Subject: Add reply and menu buttons to TimelineItem --- src/timeline/TimelineItem.cpp | 49 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) (limited to 'src/timeline/TimelineItem.cpp') diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index d23dbf49..24685641 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -126,11 +126,17 @@ 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_); } @@ -139,11 +145,17 @@ 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_); } @@ -155,6 +167,7 @@ TimelineItem::init() timestamp_ = nullptr; userName_ = nullptr; body_ = nullptr; + auto buttonSize_ = 32; contextMenu_ = new QMenu(this); showReadReceipts_ = new QAction("Read receipts", this); @@ -166,6 +179,7 @@ TimelineItem::init() contextMenu_->addAction(markAsRead_); contextMenu_->addAction(redactMsg_); + connect(showReadReceipts_, &QAction::triggered, this, [this]() { if (!event_id_.isEmpty()) MainWindow::instance()->openReadReceiptsDialog(event_id_); @@ -207,9 +221,13 @@ TimelineItem::init() 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); @@ -218,6 +236,28 @@ TimelineItem::init() 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); @@ -825,15 +865,18 @@ TimelineItem::addReplyAction() auto replyAction = new QAction("Reply", this); contextMenu_->addAction(replyAction); - connect(replyAction, &QAction::triggered, this, [this]() { + connect(replyAction, &QAction::triggered, this, &TimelineItem::replyAction); + } +} + +void +TimelineItem::replyAction() { if (!body_) return; emit ChatPage::instance()->messageReply( Cache::displayName(room_id_, descriptionMsg_.userid), body_->toPlainText()); - }); - } } void -- cgit 1.5.1 From 9671b1c0d6b1dc70a8f5a7b23ea8166dcbd07a16 Mon Sep 17 00:00:00 2001 From: redsky17 Date: Mon, 27 May 2019 16:06:28 -0400 Subject: Fix linting issues --- src/timeline/TimelineItem.cpp | 31 +++++++++++++++---------------- src/timeline/TimelineItem.h | 3 +-- 2 files changed, 16 insertions(+), 18 deletions(-) (limited to 'src/timeline/TimelineItem.cpp') diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index 24685641..faae1da3 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -126,8 +126,8 @@ void TimelineItem::adjustMessageLayoutForWidget() { messageLayout_->addLayout(widgetLayout_, 1); - actionLayout_->addWidget(replyBtn_); - actionLayout_->addWidget(contextBtn_); + actionLayout_->addWidget(replyBtn_); + actionLayout_->addWidget(contextBtn_); messageLayout_->addLayout(actionLayout_); messageLayout_->addWidget(statusIndicator_); messageLayout_->addWidget(timestamp_); @@ -145,8 +145,8 @@ void TimelineItem::adjustMessageLayout() { messageLayout_->addWidget(body_, 1); - actionLayout_->addWidget(replyBtn_); - actionLayout_->addWidget(contextBtn_); + actionLayout_->addWidget(replyBtn_); + actionLayout_->addWidget(contextBtn_); messageLayout_->addLayout(actionLayout_); messageLayout_->addWidget(statusIndicator_); messageLayout_->addWidget(timestamp_); @@ -163,10 +163,10 @@ TimelineItem::adjustMessageLayout() void TimelineItem::init() { - userAvatar_ = nullptr; - timestamp_ = nullptr; - userName_ = nullptr; - body_ = nullptr; + userAvatar_ = nullptr; + timestamp_ = nullptr; + userName_ = nullptr; + body_ = nullptr; auto buttonSize_ = 32; contextMenu_ = new QMenu(this); @@ -179,7 +179,6 @@ TimelineItem::init() contextMenu_->addAction(markAsRead_); contextMenu_->addAction(redactMsg_); - connect(showReadReceipts_, &QAction::triggered, this, [this]() { if (!event_id_.isEmpty()) MainWindow::instance()->openReadReceiptsDialog(event_id_); @@ -221,7 +220,7 @@ TimelineItem::init() topLayout_ = new QHBoxLayout(this); mainLayout_ = new QVBoxLayout; messageLayout_ = new QHBoxLayout; - actionLayout_ = new QHBoxLayout; + actionLayout_ = new QHBoxLayout; messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0); messageLayout_->setSpacing(MSG_PADDING); @@ -870,13 +869,13 @@ TimelineItem::addReplyAction() } void -TimelineItem::replyAction() { - if (!body_) - return; +TimelineItem::replyAction() +{ + if (!body_) + return; - emit ChatPage::instance()->messageReply( - Cache::displayName(room_id_, descriptionMsg_.userid), - body_->toPlainText()); + emit ChatPage::instance()->messageReply( + Cache::displayName(room_id_, descriptionMsg_.userid), body_->toPlainText()); } void diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h index b0fabdcf..6fe4a6f2 100644 --- a/src/timeline/TimelineItem.h +++ b/src/timeline/TimelineItem.h @@ -289,7 +289,7 @@ private: QHBoxLayout *topLayout_ = nullptr; QHBoxLayout *messageLayout_ = nullptr; - QHBoxLayout *actionLayout_ = nullptr; + QHBoxLayout *actionLayout_ = nullptr; QVBoxLayout *mainLayout_ = nullptr; QHBoxLayout *widgetLayout_ = nullptr; @@ -307,7 +307,6 @@ private: FlatButton *replyBtn_; FlatButton *contextBtn_; - }; template -- cgit 1.5.1 From b9dde957a83c7198e9c5941c657e785577d11ed5 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Sun, 9 Jun 2019 19:03:18 -0400 Subject: Add initial support for rich replies to nheko --- deps/CMakeLists.txt | 4 ++-- src/ChatPage.cpp | 5 +++++ src/ChatPage.h | 2 +- src/TextInputWidget.cpp | 18 +++++++++++++++--- src/TextInputWidget.h | 11 ++++++++++- src/timeline/TimelineItem.cpp | 2 +- src/timeline/TimelineView.cpp | 15 ++++++++++++++- src/timeline/TimelineView.h | 2 ++ src/timeline/TimelineViewManager.cpp | 13 +++++++++++++ src/timeline/TimelineViewManager.h | 2 ++ 10 files changed, 65 insertions(+), 9 deletions(-) (limited to 'src/timeline/TimelineItem.cpp') diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 9eb1d840..1a45112e 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -46,10 +46,10 @@ set(BOOST_SHA256 set( MTXCLIENT_URL - https://github.com/Nheko-Reborn/mtxclient/archive/1c31a8072f09a454b9239e11740473c8a7dbee39.tar.gz + https://github.com/Nheko-Reborn/mtxclient/archive/8c6e9ba8fc18ed9dd69d014eebd1ebff08701d6d.tar.gz ) set(MTXCLIENT_HASH - 5cdcd7d6feaefa8df8966bd053ea2d1181eb7e6c0f3b548b2f98f00400e2760e) + b31ec18b9d7d74db1a17b930bfa570fa1cede56cc49b43948b7d86c396f2f3d3) set( TWEENY_URL https://github.com/mobius3/tweeny/archive/b94ce07cfb02a0eb8ac8aaf66137dabdaea857cf.tar.gz diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index dd23fb80..5b838259 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -264,6 +264,11 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) view_manager_, SLOT(queueTextMessage(const QString &))); + connect(text_input_, + SIGNAL(sendReplyMessage(const QString &, const QString &)), + view_manager_, + SLOT(queueReplyMessage(const QString &, const QString &))); + connect(text_input_, SIGNAL(sendEmoteMessage(const QString &)), view_manager_, diff --git a/src/ChatPage.h b/src/ChatPage.h index 7d3b3273..09e7a2c6 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -83,7 +83,7 @@ signals: void connectionLost(); void connectionRestored(); - void messageReply(const QString &username, const QString &msg); + void messageReply(const QString &username, const QString &msg, const QString &related_event); void notificationsRetrieved(const mtx::responses::Notifications &); diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 934f2b2c..340c6f7c 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -398,14 +398,24 @@ FilteredTextEdit::submit() auto name = text.mid(1, command_end - 1); auto args = text.mid(command_end + 1); if (name.isEmpty() || name == "/") { - message(args); + if (!related_event_.isEmpty()) { + reply(args, related_event_); + } else { + message(args); + } } else { command(name, args); } } else { - message(std::move(text)); + if (!related_event_.isEmpty()) { + reply(std::move(text), std::move(related_event_)); + } else { + message(std::move(text)); + } } + related_event_ = ""; + clear(); } @@ -536,6 +546,7 @@ TextInputWidget::TextInputWidget(QWidget *parent) connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit); connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); 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); @@ -653,7 +664,7 @@ TextInputWidget::paintEvent(QPaintEvent *) } void -TextInputWidget::addReply(const QString &username, const QString &msg) +TextInputWidget::addReply(const QString &username, const QString &msg, const QString &replied_event) { input_->setText(QString("> %1: %2\n\n").arg(username).arg(msg)); input_->setFocus(); @@ -661,4 +672,5 @@ TextInputWidget::addReply(const QString &username, const QString &msg) auto cursor = input_->textCursor(); cursor.movePosition(QTextCursor::End); input_->setTextCursor(cursor); + input_->setRelatedEvent(replied_event); } diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index 8f634f6b..a12183d8 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -54,6 +54,7 @@ public: QSize minimumSizeHint() const override; void submit(); + void setRelatedEvent(const QString &event) { related_event_ = event; } signals: void heightChanged(int height); @@ -61,6 +62,7 @@ signals: void stoppedTyping(); void startedUpload(); void message(QString); + void reply(QString, QString); void command(QString name, QString args); void image(QSharedPointer data, const QString &filename); void audio(QSharedPointer data, const QString &filename); @@ -94,6 +96,9 @@ private: SuggestionsPopup popup_; + // Used for replies + QString related_event_; + enum class AnchorType { Tab = 0, @@ -158,13 +163,14 @@ public slots: void openFileSelection(); void hideUploadSpinner(); void focusLineEdit() { input_->setFocus(); } - void addReply(const QString &username, const QString &msg); + void addReply(const QString &username, const QString &msg, const QString &related_event); private slots: void addSelectedEmoji(const QString &emoji); signals: void sendTextMessage(QString msg); + void sendReplyMessage(QString msg, QString event_id); void sendEmoteMessage(QString msg); void heightChanged(int height); @@ -189,6 +195,9 @@ private: QHBoxLayout *topLayout_; FilteredTextEdit *input_; + // Used for replies + QString related_event_; + LoadingIndicator *spinner_; FlatButton *sendFileBtn_; diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index faae1da3..dd09ec78 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -875,7 +875,7 @@ TimelineItem::replyAction() return; emit ChatPage::instance()->messageReply( - Cache::displayName(room_id_, descriptionMsg_.userid), body_->toPlainText()); + Cache::displayName(room_id_, descriptionMsg_.userid), body_->toPlainText(), eventId()); } void diff --git a/src/timeline/TimelineView.cpp b/src/timeline/TimelineView.cpp index 2b4f979b..ee7021d8 100644 --- a/src/timeline/TimelineView.cpp +++ b/src/timeline/TimelineView.cpp @@ -690,7 +690,7 @@ TimelineView::updatePendingMessage(const std::string &txn_id, const QString &eve } void -TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) +TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body, const QString &related_event) { auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); @@ -702,6 +702,9 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) message.txn_id = http::client()->generate_txn_id(); message.body = body; message.widget = view_item; + if (!related_event.isEmpty()) { + message.related_event = related_event.toStdString(); + } try { message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString()); @@ -722,6 +725,12 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) handleNewUserMessage(message); } +void +TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) +{ + addUserMessage(ty, body, ""); +} + void TimelineView::handleNewUserMessage(PendingMessage msg) { @@ -1267,6 +1276,10 @@ toRoomMessage(const PendingMessage &m) if (html != m.body.trimmed().toHtmlEscaped()) text.formatted_body = html.toStdString(); + if (!m.related_event.empty()) { + text.relates_to.in_reply_to.event_id = m.related_event; + } + return text; } diff --git a/src/timeline/TimelineView.h b/src/timeline/TimelineView.h index b0909b44..450b5dfa 100644 --- a/src/timeline/TimelineView.h +++ b/src/timeline/TimelineView.h @@ -63,6 +63,7 @@ struct PendingMessage { mtx::events::MessageType ty; std::string txn_id; + std::string related_event; QString body; QString filename; QString mime; @@ -120,6 +121,7 @@ public: // 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 QString &related_event); void addUserMessage(mtx::events::MessageType ty, const QString &msg); template diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index feab46a3..10c2d747 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -78,6 +78,19 @@ TimelineViewManager::queueEmoteMessage(const QString &msg) view->addUserMessage(mtx::events::MessageType::Emote, msg); } +void +TimelineViewManager::queueReplyMessage(const QString &reply, + const QString &related_event) +{ + if (active_room_.isEmpty()) + return; + + auto room_id = active_room_; + auto view = views_[room_id]; + + view->addUserMessage(mtx::events::MessageType::Text, reply, related_event); +} + void TimelineViewManager::queueImageMessage(const QString &roomid, const QString &filename, diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index d23345d3..cc981d9e 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -63,6 +63,8 @@ public slots: void setHistoryView(const QString &room_id); void queueTextMessage(const QString &msg); + void queueReplyMessage(const QString &reply, + const QString &related_event); void queueEmoteMessage(const QString &msg); void queueImageMessage(const QString &roomid, const QString &filename, -- cgit 1.5.1 From 129beb57c9525439d04fc2bf74f4ccaed30369c9 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Thu, 13 Jun 2019 22:33:04 -0400 Subject: Further Improve Reply Functionality Quoted replies now include matrix.to links for the event and the user. UI Rendering has been (slightly) improved... still very WIP. Restructured the reply structure in the code for future usability improvements. --- src/ChatPage.cpp | 4 +- src/ChatPage.h | 5 +- src/TextInputWidget.cpp | 30 ++++++------ src/TextInputWidget.h | 17 ++++--- src/Utils.cpp | 14 ++++++ src/Utils.h | 13 ++++++ src/popups/PopupItem.cpp | 10 ++++ src/popups/PopupItem.h | 1 + src/popups/ReplyPopup.cpp | 88 ++++++++++++++++++++++++++---------- src/popups/ReplyPopup.h | 16 ++++++- src/timeline/TimelineItem.cpp | 8 +++- src/timeline/TimelineView.cpp | 31 +++++++------ src/timeline/TimelineView.h | 5 +- src/timeline/TimelineViewManager.cpp | 5 +- src/timeline/TimelineViewManager.h | 4 +- 15 files changed, 177 insertions(+), 74 deletions(-) (limited to 'src/timeline/TimelineItem.cpp') diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 5b838259..f7dbf7ca 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -265,9 +265,9 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) SLOT(queueTextMessage(const QString &))); connect(text_input_, - SIGNAL(sendReplyMessage(const QString &, const QString &)), + SIGNAL(sendReplyMessage(const QString &, const RelatedInfo &)), view_manager_, - SLOT(queueReplyMessage(const QString &, const QString &))); + SLOT(queueReplyMessage(const QString &, const RelatedInfo &))); connect(text_input_, SIGNAL(sendEmoteMessage(const QString &)), diff --git a/src/ChatPage.h b/src/ChatPage.h index f70f5bdd..6e6f5aed 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -30,6 +30,7 @@ #include "Cache.h" #include "CommunitiesList.h" #include "MatrixClient.h" +#include "Utils.h" #include "notifications/Manager.h" class OverlayModal; @@ -83,9 +84,7 @@ signals: void connectionLost(); void connectionRestored(); - void messageReply(const QString &username, - const QString &msg, - const QString &related_event); + void messageReply(const RelatedInfo &related); void notificationsRetrieved(const mtx::responses::Notifications &); diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index dc41b4d3..8becf5ce 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -93,6 +93,8 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) cursor.insertText(text); }); + connect(&replyPopup_, &ReplyPopup::cancel, this, [this]() { closeReply(); }); + // For cycling through the suggestions by hitting tab. connect(this, &FilteredTextEdit::selectNextSuggestion, @@ -219,6 +221,7 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) if (!(event->modifiers() & Qt::ShiftModifier)) { stopTyping(); submit(); + closeReply(); } else { QTextEdit::keyPressEvent(event); } @@ -415,8 +418,8 @@ FilteredTextEdit::submit() auto name = text.mid(1, command_end - 1); auto args = text.mid(command_end + 1); if (name.isEmpty() || name == "/") { - if (!related_event_.isEmpty()) { - reply(args, related_event_); + if (!related_.related_event.empty()) { + reply(args, related_); } else { message(args); } @@ -424,14 +427,14 @@ FilteredTextEdit::submit() command(name, args); } } else { - if (!related_event_.isEmpty()) { - reply(std::move(text), std::move(related_event_)); + if (!related_.related_event.empty()) { + reply(std::move(text), std::move(related_)); } else { message(std::move(text)); } } - related_event_ = ""; + related_ = {}; clear(); } @@ -439,16 +442,8 @@ FilteredTextEdit::submit() void FilteredTextEdit::showReplyPopup(const QString &user, const QString &msg, const QString &event_id) { - QPoint pos; + QPoint pos = viewport()->mapToGlobal(this->pos()); - if (isAnchorValid()) { - auto cursor = textCursor(); - cursor.setPosition(atTriggerPosition_); - pos = viewport()->mapToGlobal(cursorRect(cursor).topLeft()); - } else { - auto rect = cursorRect(); - pos = viewport()->mapToGlobal(rect.topLeft()); - } replyPopup_.setReplyContent(user, msg, event_id); replyPopup_.move(pos.x(), pos.y() - replyPopup_.height() - 10); replyPopup_.show(); @@ -699,14 +694,15 @@ TextInputWidget::paintEvent(QPaintEvent *) } void -TextInputWidget::addReply(const QString &username, const QString &msg, const QString &replied_event) +TextInputWidget::addReply(const RelatedInfo &related) { // input_->setText(QString("> %1: %2\n\n").arg(username).arg(msg)); input_->setFocus(); - input_->showReplyPopup(username, msg, replied_event); + input_->showReplyPopup( + related.quoted_user, related.quoted_body, QString::fromStdString(related.related_event)); auto cursor = input_->textCursor(); cursor.movePosition(QTextCursor::End); input_->setTextCursor(cursor); - input_->setRelatedEvent(replied_event); + input_->setRelated(related); } diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index c462de05..f68560e9 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -28,6 +28,7 @@ #include #include +#include "Utils.h" #include "dialogs/PreviewUploadOverlay.h" #include "emoji/PickButton.h" #include "popups/ReplyPopup.h" @@ -55,7 +56,7 @@ public: QSize minimumSizeHint() const override; void submit(); - void setRelatedEvent(const QString &event) { related_event_ = event; } + void setRelated(const RelatedInfo &related) { related_ = related; } void showReplyPopup(const QString &user, const QString &msg, const QString &event_id); signals: @@ -64,7 +65,7 @@ signals: void stoppedTyping(); void startedUpload(); void message(QString); - void reply(QString, QString); + void reply(QString, const RelatedInfo &); void command(QString name, QString args); void image(QSharedPointer data, const QString &filename); void audio(QSharedPointer data, const QString &filename); @@ -100,7 +101,7 @@ private: ReplyPopup replyPopup_; // Used for replies - QString related_event_; + RelatedInfo related_; enum class AnchorType { @@ -113,7 +114,11 @@ private: int anchorWidth(AnchorType anchor) { return static_cast(anchor); } void closeSuggestions() { suggestionsPopup_.hide(); } - void closeReply() { replyPopup_.hide(); } + void closeReply() + { + replyPopup_.hide(); + related_ = {}; + } void resetAnchor() { atTriggerPosition_ = -1; } bool isAnchorValid() { return atTriggerPosition_ != -1; } bool hasAnchor(int pos, AnchorType anchor) @@ -167,14 +172,14 @@ public slots: void openFileSelection(); void hideUploadSpinner(); void focusLineEdit() { input_->setFocus(); } - void addReply(const QString &username, const QString &msg, const QString &related_event); + void addReply(const RelatedInfo &related); private slots: void addSelectedEmoji(const QString &emoji); signals: void sendTextMessage(QString msg); - void sendReplyMessage(QString msg, QString event_id); + void sendReplyMessage(QString msg, const RelatedInfo &related); void sendEmoteMessage(QString msg); void heightChanged(int height); diff --git a/src/Utils.cpp b/src/Utils.cpp index f8fdfaf9..690a9a9a 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -314,6 +314,20 @@ utils::markdownToHtml(const QString &text) return result; } +QString +utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html) +{ + return QString("
In reply " + "to%3
%4
") + .arg(QString::fromStdString(related.related_event), + related.quoted_user, + related.quoted_user, + related.quoted_body) + + html; +} + QString utils::linkColor() { diff --git a/src/Utils.h b/src/Utils.h index 8672e7d4..bf941c4c 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -18,6 +18,15 @@ class QComboBox; +// Contains information about related events for +// outgoing messages +struct RelatedInfo +{ + QString quoted_body; + std::string related_event; + QString quoted_user; +}; + namespace utils { using TimelineEvent = mtx::events::collections::TimelineEvents; @@ -225,6 +234,10 @@ linkifyMessage(const QString &body); QString markdownToHtml(const QString &text); +//! Generate a Rich Reply quote message +QString +getFormattedQuoteBody(const RelatedInfo &related, const QString &html); + //! Retrieve the color of the links based on the current theme. QString linkColor(); diff --git a/src/popups/PopupItem.cpp b/src/popups/PopupItem.cpp index 94de3a92..f905983a 100644 --- a/src/popups/PopupItem.cpp +++ b/src/popups/PopupItem.cpp @@ -36,6 +36,16 @@ PopupItem::paintEvent(QPaintEvent *) p.fillRect(rect(), hoverColor_); } +UserItem::UserItem(QWidget *parent) + : PopupItem(parent) +{ + userName_ = new QLabel("Placeholder", this); + avatar_->setSize(conf::popup::avatar); + avatar_->setLetter("P"); + topLayout_->addWidget(avatar_); + topLayout_->addWidget(userName_, 1); +} + UserItem::UserItem(QWidget *parent, const QString &user_id) : PopupItem(parent) , userId_{user_id} diff --git a/src/popups/PopupItem.h b/src/popups/PopupItem.h index 1fc54bf7..cab73a9d 100644 --- a/src/popups/PopupItem.h +++ b/src/popups/PopupItem.h @@ -50,6 +50,7 @@ class UserItem : public PopupItem Q_OBJECT public: + UserItem(QWidget *parent); UserItem(QWidget *parent, const QString &user_id); QString selectedText() const { return userId_; } void updateItem(const QString &user_id); diff --git a/src/popups/ReplyPopup.cpp b/src/popups/ReplyPopup.cpp index 04c3cea5..3c7c482a 100644 --- a/src/popups/ReplyPopup.cpp +++ b/src/popups/ReplyPopup.cpp @@ -11,41 +11,69 @@ ReplyPopup::ReplyPopup(QWidget *parent) : QWidget(parent) + , userItem_{0} + , msgLabel_{0} + , eventLabel_{0} { setAttribute(Qt::WA_ShowWithoutActivating, true); setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint); - layout_ = new QVBoxLayout(this); - layout_->setMargin(0); - layout_->setSpacing(0); + mainLayout_ = new QVBoxLayout(this); + mainLayout_->setMargin(0); + mainLayout_->setSpacing(0); + + topLayout_ = new QHBoxLayout(); + topLayout_->setSpacing(0); + topLayout_->setContentsMargins(13, 1, 13, 0); + + userItem_ = new UserItem(this); + connect(userItem_, &UserItem::clicked, this, &ReplyPopup::userSelected); + topLayout_->addWidget(userItem_); + + buttonLayout_ = new QHBoxLayout(); + buttonLayout_->setSpacing(0); + buttonLayout_->setMargin(0); + + topLayout_->addLayout(buttonLayout_); + QFont f; + f.setPointSizeF(f.pointSizeF()); + const int fontHeight = QFontMetrics(f).height(); + buttonSize_ = std::min(fontHeight, 20); + + closeBtn_ = new FlatButton(this); + closeBtn_->setToolTip(tr("Logout")); + closeBtn_->setCornerRadius(buttonSize_ / 2); + closeBtn_->setText("X"); + + QIcon icon; + icon.addFile(":/icons/icons/ui/remove-symbol.png"); + + closeBtn_->setIcon(icon); + closeBtn_->setIconSize(QSize(buttonSize_, buttonSize_)); + connect(closeBtn_, &FlatButton::clicked, this, [this]() { emit cancel(); }); + + buttonLayout_->addWidget(closeBtn_); + + topLayout_->addLayout(buttonLayout_); + + mainLayout_->addLayout(topLayout_); + msgLabel_ = new QLabel(this); + mainLayout_->addWidget(msgLabel_); + eventLabel_ = new QLabel(this); + mainLayout_->addWidget(eventLabel_); + + setLayout(mainLayout_); } void ReplyPopup::setReplyContent(const QString &user, const QString &msg, const QString &srcEvent) { - QLayoutItem *child; - while ((child = layout_->takeAt(0)) != 0) { - delete child->widget(); - delete child; - } - // Create a new widget if there isn't already one in that - // layout position. - // if (!item) { - auto userItem = new UserItem(this, user); - auto *text = new QLabel(this); - text->setText(msg); - auto *event = new QLabel(this); - event->setText(srcEvent); - connect(userItem, &UserItem::clicked, this, &ReplyPopup::userSelected); - layout_->addWidget(userItem); - layout_->addWidget(text); - layout_->addWidget(event); - // } else { // Update the current widget with the new data. - // auto userWidget = qobject_cast(item->widget()); - // if (userWidget) - // userWidget->updateItem(users.at(i).user_id); - // } + userItem_->updateItem(user); + + msgLabel_->setText(msg); + + eventLabel_->setText(srcEvent); adjustSize(); } @@ -58,3 +86,13 @@ ReplyPopup::paintEvent(QPaintEvent *) QPainter p(this); style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } + +void +ReplyPopup::mousePressEvent(QMouseEvent *event) +{ + if (event->buttons() != Qt::RightButton) { + emit clicked(eventLabel_->text()); + } + + QWidget::mousePressEvent(event); +} diff --git a/src/popups/ReplyPopup.h b/src/popups/ReplyPopup.h index 57c2bc8a..829f707b 100644 --- a/src/popups/ReplyPopup.h +++ b/src/popups/ReplyPopup.h @@ -3,11 +3,13 @@ #include #include #include +#include #include #include "../AvatarProvider.h" #include "../Cache.h" #include "../ChatPage.h" +#include "../ui/FlatButton.h" #include "PopupItem.h" class ReplyPopup : public QWidget @@ -22,10 +24,22 @@ public slots: protected: void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; signals: void userSelected(const QString &user); + void clicked(const QString &text); + void cancel(); private: - QVBoxLayout *layout_; + QHBoxLayout *topLayout_; + QVBoxLayout *mainLayout_; + QHBoxLayout *buttonLayout_; + + UserItem *userItem_; + FlatButton *closeBtn_; + QLabel *msgLabel_; + QLabel *eventLabel_; + + int buttonSize_; }; diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index dd09ec78..bf5b1b53 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -874,8 +874,12 @@ TimelineItem::replyAction() if (!body_) return; - emit ChatPage::instance()->messageReply( - Cache::displayName(room_id_, descriptionMsg_.userid), body_->toPlainText(), eventId()); + RelatedInfo related; + related.quoted_body = body_->toPlainText(); + related.quoted_user = descriptionMsg_.userid; + related.related_event = eventId().toStdString(); + + emit ChatPage::instance()->messageReply(related); } void diff --git a/src/timeline/TimelineView.cpp b/src/timeline/TimelineView.cpp index 6d947c15..fc89fd38 100644 --- a/src/timeline/TimelineView.cpp +++ b/src/timeline/TimelineView.cpp @@ -692,7 +692,7 @@ TimelineView::updatePendingMessage(const std::string &txn_id, const QString &eve void TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body, - const QString &related_event) + const RelatedInfo &related = RelatedInfo()) { auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); @@ -700,13 +700,11 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, new TimelineItem(ty, local_user_, body, with_sender, room_id_, scroll_widget_); PendingMessage message; - message.ty = ty; - message.txn_id = http::client()->generate_txn_id(); - message.body = body; - message.widget = view_item; - if (!related_event.isEmpty()) { - message.related_event = related_event.toStdString(); - } + message.ty = ty; + message.txn_id = http::client()->generate_txn_id(); + message.body = body; + message.widget = view_item; + message.related = related; try { message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString()); @@ -730,7 +728,7 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, void TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) { - addUserMessage(ty, body, ""); + addUserMessage(ty, body, RelatedInfo()); } void @@ -1273,13 +1271,20 @@ toRoomMessage(const PendingMessage &m) auto html = utils::markdownToHtml(m.body); mtx::events::msg::Text text; + text.body = m.body.trimmed().toStdString(); - if (html != m.body.trimmed().toHtmlEscaped()) - text.formatted_body = html.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_event.empty()) { - text.relates_to.in_reply_to.event_id = m.related_event; + if (!m.related.related_event.empty()) { + text.relates_to.in_reply_to.event_id = m.related.related_event; } return text; diff --git a/src/timeline/TimelineView.h b/src/timeline/TimelineView.h index db6087eb..35796efd 100644 --- a/src/timeline/TimelineView.h +++ b/src/timeline/TimelineView.h @@ -30,6 +30,7 @@ #include #include +#include "../Utils.h" #include "MatrixClient.h" #include "timeline/TimelineItem.h" @@ -63,7 +64,7 @@ struct PendingMessage { mtx::events::MessageType ty; std::string txn_id; - std::string related_event; + RelatedInfo related; QString body; QString filename; QString mime; @@ -123,7 +124,7 @@ public: void addEvents(const mtx::responses::Timeline &timeline); void addUserMessage(mtx::events::MessageType ty, const QString &body, - const QString &related_event); + const RelatedInfo &related); void addUserMessage(mtx::events::MessageType ty, const QString &msg); template diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 1ce3794f..86505481 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -23,6 +23,7 @@ #include "Cache.h" #include "Logging.h" +#include "Utils.h" #include "timeline/TimelineView.h" #include "timeline/TimelineViewManager.h" #include "timeline/widgets/AudioItem.h" @@ -79,7 +80,7 @@ TimelineViewManager::queueEmoteMessage(const QString &msg) } void -TimelineViewManager::queueReplyMessage(const QString &reply, const QString &related_event) +TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) { if (active_room_.isEmpty()) return; @@ -87,7 +88,7 @@ TimelineViewManager::queueReplyMessage(const QString &reply, const QString &rela auto room_id = active_room_; auto view = views_[room_id]; - view->addUserMessage(mtx::events::MessageType::Text, reply, related_event); + view->addUserMessage(mtx::events::MessageType::Text, reply, related); } void diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 0ac6d67e..b52136d9 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -22,6 +22,8 @@ #include +#include "Utils.h" + class QFile; class RoomInfoListItem; @@ -63,7 +65,7 @@ public slots: void setHistoryView(const QString &room_id); void queueTextMessage(const QString &msg); - void queueReplyMessage(const QString &reply, const QString &related_event); + void queueReplyMessage(const QString &reply, const RelatedInfo &related); void queueEmoteMessage(const QString &msg); void queueImageMessage(const QString &roomid, const QString &filename, -- cgit 1.5.1 From cfd6c5703a7ca4a22fe1b1e78713b33f32f1a085 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Fri, 14 Jun 2019 20:45:37 -0400 Subject: Further UI Updates to Rich Replies --- src/TextInputWidget.cpp | 8 ++++---- src/TextInputWidget.h | 2 +- src/popups/ReplyPopup.cpp | 16 ++++++++++------ src/popups/ReplyPopup.h | 6 ++++-- src/timeline/TimelineItem.cpp | 2 ++ src/timeline/TimelineView.cpp | 10 ++++++++-- 6 files changed, 29 insertions(+), 15 deletions(-) (limited to 'src/timeline/TimelineItem.cpp') diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index 8becf5ce..1ae26c2d 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -440,12 +440,13 @@ FilteredTextEdit::submit() } void -FilteredTextEdit::showReplyPopup(const QString &user, const QString &msg, const QString &event_id) +FilteredTextEdit::showReplyPopup(const RelatedInfo &related) { QPoint pos = viewport()->mapToGlobal(this->pos()); - replyPopup_.setReplyContent(user, msg, event_id); + replyPopup_.setReplyContent(related); replyPopup_.move(pos.x(), pos.y() - replyPopup_.height() - 10); + replyPopup_.setFixedWidth(this->parentWidget()->width()); replyPopup_.show(); } @@ -699,8 +700,7 @@ TextInputWidget::addReply(const RelatedInfo &related) // input_->setText(QString("> %1: %2\n\n").arg(username).arg(msg)); input_->setFocus(); - input_->showReplyPopup( - related.quoted_user, related.quoted_body, QString::fromStdString(related.related_event)); + input_->showReplyPopup(related); auto cursor = input_->textCursor(); cursor.movePosition(QTextCursor::End); input_->setTextCursor(cursor); diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index f68560e9..4a726364 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -57,7 +57,7 @@ public: void submit(); void setRelated(const RelatedInfo &related) { related_ = related; } - void showReplyPopup(const QString &user, const QString &msg, const QString &event_id); + void showReplyPopup(const RelatedInfo &related); signals: void heightChanged(int height); diff --git a/src/popups/ReplyPopup.cpp b/src/popups/ReplyPopup.cpp index 3c7c482a..0ebf7c88 100644 --- a/src/popups/ReplyPopup.cpp +++ b/src/popups/ReplyPopup.cpp @@ -7,6 +7,7 @@ #include "../Utils.h" #include "../ui/Avatar.h" #include "../ui/DropShadow.h" +#include "../ui/TextLabel.h" #include "ReplyPopup.h" ReplyPopup::ReplyPopup(QWidget *parent) @@ -42,7 +43,7 @@ ReplyPopup::ReplyPopup(QWidget *parent) closeBtn_ = new FlatButton(this); closeBtn_->setToolTip(tr("Logout")); - closeBtn_->setCornerRadius(buttonSize_ / 2); + closeBtn_->setCornerRadius(buttonSize_ / 4); closeBtn_->setText("X"); QIcon icon; @@ -57,7 +58,8 @@ ReplyPopup::ReplyPopup(QWidget *parent) topLayout_->addLayout(buttonLayout_); mainLayout_->addLayout(topLayout_); - msgLabel_ = new QLabel(this); + msgLabel_ = new TextLabel(this); + msgLabel_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); mainLayout_->addWidget(msgLabel_); eventLabel_ = new QLabel(this); mainLayout_->addWidget(eventLabel_); @@ -66,14 +68,16 @@ ReplyPopup::ReplyPopup(QWidget *parent) } void -ReplyPopup::setReplyContent(const QString &user, const QString &msg, const QString &srcEvent) +ReplyPopup::setReplyContent(const RelatedInfo &related) { // Update the current widget with the new data. - userItem_->updateItem(user); + userItem_->updateItem(related.quoted_user); - msgLabel_->setText(msg); + msgLabel_->setText(utils::getFormattedQuoteBody(related, "") + .replace("", "") + .replace("", "")); - eventLabel_->setText(srcEvent); + // eventLabel_->setText(srcEvent); adjustSize(); } diff --git a/src/popups/ReplyPopup.h b/src/popups/ReplyPopup.h index 829f707b..b28cd0cf 100644 --- a/src/popups/ReplyPopup.h +++ b/src/popups/ReplyPopup.h @@ -9,7 +9,9 @@ #include "../AvatarProvider.h" #include "../Cache.h" #include "../ChatPage.h" +#include "../Utils.h" #include "../ui/FlatButton.h" +#include "../ui/TextLabel.h" #include "PopupItem.h" class ReplyPopup : public QWidget @@ -20,7 +22,7 @@ public: explicit ReplyPopup(QWidget *parent = nullptr); public slots: - void setReplyContent(const QString &user, const QString &msg, const QString &srcEvent); + void setReplyContent(const RelatedInfo &related); protected: void paintEvent(QPaintEvent *event) override; @@ -38,7 +40,7 @@ private: UserItem *userItem_; FlatButton *closeBtn_; - QLabel *msgLabel_; + TextLabel *msgLabel_; QLabel *eventLabel_; int buttonSize_; diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index bf5b1b53..1094bde5 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -316,6 +316,8 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty, } formatted_body = utils::linkifyMessage(formatted_body); + formatted_body.replace("mx-reply", "div"); + nhlog::ui()->info("formatted_body: {}", formatted_body.toStdString()); generateTimestamp(timestamp); diff --git a/src/timeline/TimelineView.cpp b/src/timeline/TimelineView.cpp index fc89fd38..18b73eb0 100644 --- a/src/timeline/TimelineView.cpp +++ b/src/timeline/TimelineView.cpp @@ -696,15 +696,21 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, { 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_, body, with_sender, room_id_, scroll_widget_); + 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.widget = view_item; message.related = related; + message.widget = view_item; try { message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString()); -- cgit 1.5.1 From c0a010acbb7f0045aa1ce33f82b0d537a715a886 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Thu, 4 Jul 2019 21:20:19 -0400 Subject: Fix deprecated function call issues with Qt 5.13 Update to mtxclient 0.3.0 --- CMakeLists.txt | 2 +- deps/CMakeLists.txt | 5 ++-- nheko-backtrace.dump | Bin 0 -> 288 bytes src/RoomInfoListItem.cpp | 4 ++-- src/TypingDisplay.cpp | 2 +- src/Utils.cpp | 47 +++++++++++++++++++++++++++++++------ src/Utils.h | 7 ++++++ src/dialogs/ImageOverlay.cpp | 5 +++- src/main.cpp | 5 +++- src/notifications/ManagerLinux.cpp | 2 +- src/timeline/TimelineItem.cpp | 6 ++++- src/timeline/TimelineItem.h | 8 +++++++ src/timeline/widgets/AudioItem.cpp | 2 +- src/timeline/widgets/FileItem.cpp | 2 +- src/timeline/widgets/ImageItem.cpp | 2 +- src/ui/DropShadow.h | 22 ++++++++++------- src/ui/InfoMessage.cpp | 4 ++-- src/ui/Painter.h | 6 +++-- 18 files changed, 97 insertions(+), 34 deletions(-) create mode 100644 nheko-backtrace.dump (limited to 'src/timeline/TimelineItem.cpp') diff --git a/CMakeLists.txt b/CMakeLists.txt index 66d262f0..4a0bf50f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -270,7 +270,7 @@ find_package(Boost 1.66 REQUIRED thread) find_package(ZLIB REQUIRED) find_package(OpenSSL REQUIRED) -find_package(MatrixClient 0.1.0 REQUIRED) +find_package(MatrixClient 0.3.0 REQUIRED) find_package(Olm 2 REQUIRED) find_package(spdlog 1.0.0 CONFIG REQUIRED) find_package(cmark REQUIRED) diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 9a6fd433..1df09cc9 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -46,17 +46,16 @@ set(BOOST_SHA256 set( MTXCLIENT_URL - https://github.com/Nheko-Reborn/mtxclient/archive/32065798a2efa205052fcd2f470c52326a46d0b9.tar.gz + https://github.com/Nheko-Reborn/mtxclient/archive/35b596a98d516e044a6a25803ba6b93b6c0a538b.tar.gz ) set(MTXCLIENT_HASH - 3ddc6a482b5f388533bbaa69c44f1621d65a4e38fcb6cafaff83330975ea7e2b) + ea770f52afaad45706b8050aa3d860fa98780c60f2d3f061f2c89dfd3b3bf9be) set( TWEENY_URL https://github.com/mobius3/tweeny/archive/b94ce07cfb02a0eb8ac8aaf66137dabdaea857cf.tar.gz ) set(TWEENY_HASH 9a632b9da84823fae002ad5d9ba02c8d77c0a3810479974c6b637c5504165475) - set( LMDBXX_HEADER_URL https://raw.githubusercontent.com/bendiken/lmdbxx/0b43ca87d8cfabba392dfe884eb1edb83874de02/lmdb%2B%2B.h diff --git a/nheko-backtrace.dump b/nheko-backtrace.dump new file mode 100644 index 00000000..11aeb9d2 Binary files /dev/null and b/nheko-backtrace.dump differ diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp index f17b383c..d6a4f78f 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp @@ -182,7 +182,7 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) QFont tsFont; tsFont.setPointSizeF(tsFont.pointSizeF() * 0.9); - const int msgStampWidth = QFontMetrics(tsFont).width(lastMsgInfo_.timestamp) + 4; + const int msgStampWidth = QFontMetrics(tsFont).horizontalAdvance(lastMsgInfo_.timestamp) + 4; // We use the full width of the widget if there is no unread msg bubble. const int bottomLineWidthLimit = (unreadMsgCount_ > 0) ? msgStampWidth : 0; @@ -211,7 +211,7 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) p.setFont(QFont{}); p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), userName); - int nameWidth = QFontMetrics(QFont{}).width(userName); + int nameWidth = QFontMetrics(QFont{}).horizontalAdvance(userName); p.setFont(QFont{}); diff --git a/src/TypingDisplay.cpp b/src/TypingDisplay.cpp index 11313adc..97aeb268 100644 --- a/src/TypingDisplay.cpp +++ b/src/TypingDisplay.cpp @@ -69,7 +69,7 @@ TypingDisplay::paintEvent(QPaintEvent *) text_ = fm.elidedText(text_, Qt::ElideRight, (double)(width() * 0.75)); QPainterPath path; - path.addRoundedRect(QRectF(0, 0, fm.width(text_) + 2 * LEFT_PADDING, height()), 3, 3); + path.addRoundedRect(QRectF(0, 0, fm.horizontalAdvance(text_) + 2 * LEFT_PADDING, height()), 3, 3); p.fillPath(path, backgroundColor()); p.drawText(region, Qt::AlignVCenter, text_); diff --git a/src/Utils.cpp b/src/Utils.cpp index 3d304e7d..863de79e 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -229,8 +231,10 @@ utils::scaleImageToPixmap(const QImage &img, int size) if (img.isNull()) return QPixmap(); + // Deprecated in 5.13: const double sz = + // std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size); const double sz = - std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size); + std::ceil(QGuiApplication::primaryScreen()->devicePixelRatio() * (double)size); return QPixmap::fromImage( img.scaled(sz, sz, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); } @@ -318,16 +322,44 @@ QString utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html) { return QString("
In reply " - "to%3
%4
") - .arg(QString::fromStdString(related.related_event), + "href=\"https://matrix.to/#/%1/%2\">In reply " + "to* %4
%5") + .arg(related.room, + QString::fromStdString(related.related_event), related.quoted_user, related.quoted_user, - related.quoted_body) + + getQuoteBody(related)) + html; } +QString +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("sent a file."); + } + case MsgType::Image: { + return QString("sent an image."); + } + case MsgType::Audio: { + return QString("sent an audio file."); + } + case MsgType::Video: { + return QString("sent a video"); + } + default: { + return related.quoted_body; + } + } +} + QString utils::linkColor() { @@ -475,7 +507,8 @@ utils::centerWidget(QWidget *widget, QWidget *parent) return; } - widget->move(findCenter(QApplication::desktop()->screenGeometry())); + // Deprecated in 5.13: widget->move(findCenter(QApplication::desktop()->screenGeometry())); + widget->move(findCenter(QGuiApplication::primaryScreen()->geometry())); } void diff --git a/src/Utils.h b/src/Utils.h index 0f022770..a840a187 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -22,6 +22,9 @@ class QComboBox; // outgoing messages struct RelatedInfo { + using MsgType = mtx::events::MessageType; + MsgType type; + QString room; QString quoted_body; std::string related_event; QString quoted_user; @@ -238,6 +241,10 @@ markdownToHtml(const QString &text); QString getFormattedQuoteBody(const RelatedInfo &related, const QString &html); +//! Get the body for the quote, depending on the event type. +QString +getQuoteBody(const RelatedInfo &related); + //! Retrieve the color of the links based on the current theme. QString linkColor(); diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp index dbf5bbe4..dd9cd03a 100644 --- a/src/dialogs/ImageOverlay.cpp +++ b/src/dialogs/ImageOverlay.cpp @@ -17,7 +17,9 @@ #include #include +#include #include +#include #include "dialogs/ImageOverlay.h" @@ -39,7 +41,8 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent) setAttribute(Qt::WA_DeleteOnClose, true); setWindowState(Qt::WindowFullScreen); - screen_ = QApplication::desktop()->availableGeometry(); + // Deprecated in 5.13: screen_ = QApplication::desktop()->availableGeometry(); + screen_ = QGuiApplication::primaryScreen()->availableGeometry(); move(QApplication::desktop()->mapToGlobal(screen_.topLeft())); resize(screen_.size()); diff --git a/src/main.cpp b/src/main.cpp index 0c196a33..bd7560da 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,10 +21,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -72,7 +74,8 @@ registerSignalHandlers() QPoint screenCenter(int width, int height) { - QRect screenGeometry = QApplication::desktop()->screenGeometry(); + // Deprecated in 5.13: QRect screenGeometry = QApplication::desktop()->screenGeometry(); + QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); int x = (screenGeometry.width() - width) / 2; int y = (screenGeometry.height() - height) / 2; diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp index d3901c52..1d9f97d9 100644 --- a/src/notifications/ManagerLinux.cpp +++ b/src/notifications/ManagerLinux.cpp @@ -142,7 +142,7 @@ operator<<(QDBusArgument &arg, const QImage &image) int channels = i.isGrayscale() ? 1 : (i.hasAlphaChannel() ? 4 : 3); arg << i.depth() / channels; arg << channels; - arg << QByteArray(reinterpret_cast(i.bits()), i.byteCount()); + arg << QByteArray(reinterpret_cast(i.bits()), i.sizeInBytes()); arg.endStructure(); return arg; } diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index 1094bde5..80153026 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -731,7 +731,9 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam userName_->setToolTipDuration(1500); userName_->setAttribute(Qt::WA_Hover); userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop); - userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text())); + // width deprecated in 5.13: + // userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text())); + userName_->setFixedWidth(QFontMetrics(userName_->font()).horizontalAdvance(userName_->text())); // Set the user color asynchronously if it hasn't been generated yet, // otherwise this will just set it. @@ -877,9 +879,11 @@ TimelineItem::replyAction() 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); } diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h index 6fe4a6f2..4db36c07 100644 --- a/src/timeline/TimelineItem.h +++ b/src/timeline/TimelineItem.h @@ -28,6 +28,8 @@ #include +#include "mtx/events.hpp" + #include "AvatarProvider.h" #include "RoomInfoListItem.h" #include "Utils.h" @@ -276,6 +278,7 @@ private: QString replaceEmoji(const QString &body); QString event_id_; + mtx::events::MessageType message_type_; QString room_id_; DescInfo descriptionMsg_; @@ -349,6 +352,11 @@ TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSen { 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); diff --git a/src/timeline/widgets/AudioItem.cpp b/src/timeline/widgets/AudioItem.cpp index 72332174..8cc2ba8f 100644 --- a/src/timeline/widgets/AudioItem.cpp +++ b/src/timeline/widgets/AudioItem.cpp @@ -163,7 +163,7 @@ AudioItem::resizeEvent(QResizeEvent *event) QFontMetrics fm(font); const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); + fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); resize(computedWidth, Height); diff --git a/src/timeline/widgets/FileItem.cpp b/src/timeline/widgets/FileItem.cpp index e97554e2..903fc4b2 100644 --- a/src/timeline/widgets/FileItem.cpp +++ b/src/timeline/widgets/FileItem.cpp @@ -154,7 +154,7 @@ FileItem::resizeEvent(QResizeEvent *event) QFontMetrics fm(font); const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); + fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); resize(computedWidth, Height); diff --git a/src/timeline/widgets/ImageItem.cpp b/src/timeline/widgets/ImageItem.cpp index 4ee9e42a..79a66c4d 100644 --- a/src/timeline/widgets/ImageItem.cpp +++ b/src/timeline/widgets/ImageItem.cpp @@ -192,7 +192,7 @@ ImageItem::paintEvent(QPaintEvent *event) if (image_.isNull()) { QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10); - setFixedSize(metrics.width(elidedText), fontHeight); + setFixedSize(metrics.horizontalAdvance(elidedText), fontHeight); painter.setFont(font); painter.setPen(QPen(QColor(66, 133, 244))); diff --git a/src/ui/DropShadow.h b/src/ui/DropShadow.h index b7ba1985..d322fb42 100644 --- a/src/ui/DropShadow.h +++ b/src/ui/DropShadow.h @@ -30,7 +30,11 @@ public: gradient.setStart(right0); gradient.setFinalStop(right1); painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( + // Deprecated in 5.13: painter.drawRoundRect( + // QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), + // 0.0, + // 0.0); + painter.drawRoundedRect( QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), 0.0, 0.0); @@ -41,7 +45,7 @@ public: gradient.setStart(left0); gradient.setFinalStop(left1); painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( + painter.drawRoundedRect( QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0); // Top @@ -50,7 +54,7 @@ public: gradient.setStart(top0); gradient.setFinalStop(top1); painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( + painter.drawRoundedRect( QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0); // Bottom @@ -59,7 +63,7 @@ public: gradient.setStart(bottom0); gradient.setFinalStop(bottom1); painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( + painter.drawRoundedRect( QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), 0.0, 0.0); @@ -71,7 +75,7 @@ public: gradient.setFinalStop(bottomright1); gradient.setColorAt(endPosition1, end); painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(bottomright0, bottomright1), 0.0, 0.0); + painter.drawRoundedRect(QRectF(bottomright0, bottomright1), 0.0, 0.0); // BottomLeft QPointF bottomleft0(margin, height - margin); @@ -80,7 +84,7 @@ public: gradient.setFinalStop(bottomleft1); gradient.setColorAt(endPosition1, end); painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0); + painter.drawRoundedRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0); // TopLeft QPointF topleft0(margin, margin); @@ -89,7 +93,7 @@ public: gradient.setFinalStop(topleft1); gradient.setColorAt(endPosition1, end); painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(topleft0, topleft1), 0.0, 0.0); + painter.drawRoundedRect(QRectF(topleft0, topleft1), 0.0, 0.0); // TopRight QPointF topright0(width - margin, margin); @@ -98,12 +102,12 @@ public: gradient.setFinalStop(topright1); gradient.setColorAt(endPosition1, end); painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(topright0, topright1), 0.0, 0.0); + painter.drawRoundedRect(QRectF(topright0, topright1), 0.0, 0.0); // Widget painter.setBrush(QBrush("#FFFFFF")); painter.setRenderHint(QPainter::Antialiasing); - painter.drawRoundRect( + painter.drawRoundedRect( QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)), radius, radius); diff --git a/src/ui/InfoMessage.cpp b/src/ui/InfoMessage.cpp index e9de20cc..b18a80c4 100644 --- a/src/ui/InfoMessage.cpp +++ b/src/ui/InfoMessage.cpp @@ -22,7 +22,7 @@ InfoMessage::InfoMessage(QString msg, QWidget *parent) initFont(); QFontMetrics fm{font()}; - width_ = fm.width(msg_) + HPadding * 2; + width_ = fm.horizontalAdvance(msg_) + HPadding * 2; height_ = fm.ascent() + 2 * VPadding; setFixedHeight(height_ + 2 * HMargin); @@ -64,7 +64,7 @@ DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent) msg_ = datetime.toString(fmt); QFontMetrics fm{font()}; - width_ = fm.width(msg_) + HPadding * 2; + width_ = fm.horizontalAdvance(msg_) + HPadding * 2; height_ = fm.ascent() + 2 * VPadding; setFixedHeight(height_ + 2 * HMargin); diff --git a/src/ui/Painter.h b/src/ui/Painter.h index 8de39651..8feed17b 100644 --- a/src/ui/Painter.h +++ b/src/ui/Painter.h @@ -20,8 +20,10 @@ public: void drawTextRight(int x, int y, int outerw, const QString &text, int textWidth = -1) { QFontMetrics m(fontMetrics()); - if (textWidth < 0) - textWidth = m.width(text); + if (textWidth < 0) { + // deprecated in 5.13: textWidth = m.width(text); + textWidth = m.horizontalAdvance(text); + } drawText((outerw - x - textWidth), y + m.ascent(), text); } -- cgit 1.5.1 From 2484e0c11874cac06a18ab64e9e02e375d93a6dd Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Thu, 4 Jul 2019 21:31:28 -0400 Subject: Fix formatting issues --- src/RoomInfoListItem.cpp | 3 ++- src/TypingDisplay.cpp | 3 ++- src/timeline/TimelineItem.cpp | 3 ++- src/timeline/TimelineItem.h | 4 ++-- src/timeline/widgets/AudioItem.cpp | 5 +++-- src/timeline/widgets/FileItem.cpp | 5 +++-- src/ui/DropShadow.h | 5 ++--- 7 files changed, 16 insertions(+), 12 deletions(-) (limited to 'src/timeline/TimelineItem.cpp') diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp index d6a4f78f..c8924bed 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp @@ -182,7 +182,8 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) QFont tsFont; tsFont.setPointSizeF(tsFont.pointSizeF() * 0.9); - const int msgStampWidth = QFontMetrics(tsFont).horizontalAdvance(lastMsgInfo_.timestamp) + 4; + const int msgStampWidth = + QFontMetrics(tsFont).horizontalAdvance(lastMsgInfo_.timestamp) + 4; // We use the full width of the widget if there is no unread msg bubble. const int bottomLineWidthLimit = (unreadMsgCount_ > 0) ? msgStampWidth : 0; diff --git a/src/TypingDisplay.cpp b/src/TypingDisplay.cpp index 97aeb268..fc5e2df4 100644 --- a/src/TypingDisplay.cpp +++ b/src/TypingDisplay.cpp @@ -69,7 +69,8 @@ TypingDisplay::paintEvent(QPaintEvent *) text_ = fm.elidedText(text_, Qt::ElideRight, (double)(width() * 0.75)); QPainterPath path; - path.addRoundedRect(QRectF(0, 0, fm.horizontalAdvance(text_) + 2 * LEFT_PADDING, height()), 3, 3); + path.addRoundedRect( + QRectF(0, 0, fm.horizontalAdvance(text_) + 2 * LEFT_PADDING, height()), 3, 3); p.fillPath(path, backgroundColor()); p.drawText(region, Qt::AlignVCenter, text_); diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index 80153026..2ec6bd81 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -733,7 +733,8 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop); // width deprecated in 5.13: // userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text())); - userName_->setFixedWidth(QFontMetrics(userName_->font()).horizontalAdvance(userName_->text())); + userName_->setFixedWidth( + QFontMetrics(userName_->font()).horizontalAdvance(userName_->text())); // Set the user color asynchronously if it hasn't been generated yet, // otherwise this will just set it. diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h index 4db36c07..e2b1cda4 100644 --- a/src/timeline/TimelineItem.h +++ b/src/timeline/TimelineItem.h @@ -352,11 +352,11 @@ TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSen { init(); - //if (event.type == mtx::events::EventType::RoomMessage) { + // if (event.type == mtx::events::EventType::RoomMessage) { // message_type_ = mtx::events::getMessageType(event.content.msgtype); //} // TODO: Fix this. - message_type_ = mtx::events::MessageType::Unknown; + message_type_ = mtx::events::MessageType::Unknown; event_id_ = QString::fromStdString(event.event_id); const auto sender = QString::fromStdString(event.sender); diff --git a/src/timeline/widgets/AudioItem.cpp b/src/timeline/widgets/AudioItem.cpp index 8cc2ba8f..7ce9bf9a 100644 --- a/src/timeline/widgets/AudioItem.cpp +++ b/src/timeline/widgets/AudioItem.cpp @@ -162,8 +162,9 @@ AudioItem::resizeEvent(QResizeEvent *event) font.setWeight(QFont::Medium); QFontMetrics fm(font); - const int computedWidth = std::min( - fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); + const int computedWidth = + std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, + (double)MaxWidth); resize(computedWidth, Height); diff --git a/src/timeline/widgets/FileItem.cpp b/src/timeline/widgets/FileItem.cpp index 903fc4b2..3ae76913 100644 --- a/src/timeline/widgets/FileItem.cpp +++ b/src/timeline/widgets/FileItem.cpp @@ -153,8 +153,9 @@ FileItem::resizeEvent(QResizeEvent *event) font.setWeight(QFont::Medium); QFontMetrics fm(font); - const int computedWidth = std::min( - fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); + const int computedWidth = + std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, + (double)MaxWidth); resize(computedWidth, Height); diff --git a/src/ui/DropShadow.h b/src/ui/DropShadow.h index d322fb42..60fc697b 100644 --- a/src/ui/DropShadow.h +++ b/src/ui/DropShadow.h @@ -31,9 +31,8 @@ public: gradient.setFinalStop(right1); painter.setBrush(QBrush(gradient)); // Deprecated in 5.13: painter.drawRoundRect( - // QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), - // 0.0, - // 0.0); + // QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - + // margin)), 0.0, 0.0); painter.drawRoundedRect( QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), 0.0, -- cgit 1.5.1 From 4c0d4f35fecbd53f9bea12abe50a14fb74820435 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Thu, 4 Jul 2019 22:58:06 -0400 Subject: Fix support for Qt versions < 5.11 --- src/RoomInfoListItem.cpp | 11 +++++++++-- src/TypingDisplay.cpp | 6 +++++- src/timeline/TimelineItem.cpp | 7 +++++-- src/timeline/widgets/AudioItem.cpp | 7 ++++++- src/timeline/widgets/FileItem.cpp | 7 ++++++- src/timeline/widgets/ImageItem.cpp | 7 +++++-- src/ui/InfoMessage.cpp | 16 ++++++++++++++-- src/ui/Painter.h | 7 ++++++- 8 files changed, 56 insertions(+), 12 deletions(-) (limited to 'src/timeline/TimelineItem.cpp') diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp index c8924bed..9bcce134 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include "Cache.h" #include "Config.h" @@ -182,9 +183,12 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) QFont tsFont; tsFont.setPointSizeF(tsFont.pointSizeF() * 0.9); +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + const int msgStampWidth = QFontMetrics(tsFont).width(lastMsgInfo_.timestamp) + 4; +#else const int msgStampWidth = QFontMetrics(tsFont).horizontalAdvance(lastMsgInfo_.timestamp) + 4; - +#endif // We use the full width of the widget if there is no unread msg bubble. const int bottomLineWidthLimit = (unreadMsgCount_ > 0) ? msgStampWidth : 0; @@ -212,8 +216,11 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) 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 diff --git a/src/TypingDisplay.cpp b/src/TypingDisplay.cpp index fc5e2df4..6059601d 100644 --- a/src/TypingDisplay.cpp +++ b/src/TypingDisplay.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "Config.h" #include "TypingDisplay.h" @@ -69,9 +70,12 @@ TypingDisplay::paintEvent(QPaintEvent *) text_ = fm.elidedText(text_, Qt::ElideRight, (double)(width() * 0.75)); QPainterPath path; +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + path.addRoundedRect(QRectF(0, 0, fm.width(text_) + 2 * LEFT_PADDING, height()), 3, 3); +#else path.addRoundedRect( QRectF(0, 0, fm.horizontalAdvance(text_) + 2 * LEFT_PADDING, height()), 3, 3); - +#endif p.fillPath(path, backgroundColor()); p.drawText(region, Qt::AlignVCenter, text_); } diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index 2ec6bd81..22bdaaa2 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "ChatPage.h" #include "Config.h" @@ -731,11 +732,13 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam 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())); + 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(); diff --git a/src/timeline/widgets/AudioItem.cpp b/src/timeline/widgets/AudioItem.cpp index 7ce9bf9a..5d6431ee 100644 --- a/src/timeline/widgets/AudioItem.cpp +++ b/src/timeline/widgets/AudioItem.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "Logging.h" #include "MatrixClient.h" @@ -162,10 +163,14 @@ AudioItem::resizeEvent(QResizeEvent *event) 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(); diff --git a/src/timeline/widgets/FileItem.cpp b/src/timeline/widgets/FileItem.cpp index 3ae76913..1a555d1c 100644 --- a/src/timeline/widgets/FileItem.cpp +++ b/src/timeline/widgets/FileItem.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "Logging.h" #include "MatrixClient.h" @@ -153,10 +154,14 @@ FileItem::resizeEvent(QResizeEvent *event) 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(); diff --git a/src/timeline/widgets/ImageItem.cpp b/src/timeline/widgets/ImageItem.cpp index 79a66c4d..26c569d7 100644 --- a/src/timeline/widgets/ImageItem.cpp +++ b/src/timeline/widgets/ImageItem.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "Config.h" #include "ImageItem.h" @@ -191,9 +192,11 @@ ImageItem::paintEvent(QPaintEvent *event) 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); diff --git a/src/ui/InfoMessage.cpp b/src/ui/InfoMessage.cpp index b18a80c4..fa575d76 100644 --- a/src/ui/InfoMessage.cpp +++ b/src/ui/InfoMessage.cpp @@ -4,6 +4,7 @@ #include #include #include +#include constexpr int VPadding = 6; constexpr int HPadding = 12; @@ -22,7 +23,13 @@ InfoMessage::InfoMessage(QString msg, QWidget *parent) initFont(); QFontMetrics fm{font()}; - width_ = fm.horizontalAdvance(msg_) + HPadding * 2; +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + // width deprecated in 5.13 + width_ = fm.width(msg_) + HPadding * 2; +#else + width_ = fm.horizontalAdvance(msg_) + HPadding * 2; +#endif + height_ = fm.ascent() + 2 * VPadding; setFixedHeight(height_ + 2 * HMargin); @@ -64,7 +71,12 @@ DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent) msg_ = datetime.toString(fmt); QFontMetrics fm{font()}; - width_ = fm.horizontalAdvance(msg_) + HPadding * 2; +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + // width deprecated in 5.13 + width_ = fm.width(msg_) + HPadding * 2; +#else + width_ = fm.horizontalAdvance(msg_) + HPadding * 2; +#endif height_ = fm.ascent() + 2 * VPadding; setFixedHeight(height_ + 2 * HMargin); diff --git a/src/ui/Painter.h b/src/ui/Painter.h index 8feed17b..1c1b17b0 100644 --- a/src/ui/Painter.h +++ b/src/ui/Painter.h @@ -3,6 +3,7 @@ #include #include #include +#include class Painter : public QPainter { @@ -21,8 +22,12 @@ public: { QFontMetrics m(fontMetrics()); if (textWidth < 0) { - // deprecated in 5.13: textWidth = m.width(text); +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + // deprecated in 5.13: + textWidth = m.width(text); +#else textWidth = m.horizontalAdvance(text); +#endif } drawText((outerw - x - textWidth), y + m.ascent(), text); } -- cgit 1.5.1 From 778921be8ac0a995704838ecc680c45b1ca4cab3 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Fri, 26 Jul 2019 17:31:59 -0400 Subject: Add emoji text selection option for non-mac --- src/UserSettingsPage.cpp | 39 +++++++++++++++++++++++++++++++++++++-- src/UserSettingsPage.h | 6 ++++++ src/emoji/ItemDelegate.cpp | 13 +++++++++++-- src/timeline/TimelineItem.cpp | 10 +++++++--- 4 files changed, 61 insertions(+), 7 deletions(-) (limited to 'src/timeline/TimelineItem.cpp') diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 6bd49058..1ac3f738 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -51,6 +51,7 @@ UserSettings::load() isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool(); theme_ = settings.value("user/theme", "light").toString(); font_ = settings.value("user/font_family", "default").toString(); + emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); applyTheme(); @@ -70,6 +71,13 @@ UserSettings::setFontFamily(QString family) save(); } +void +UserSettings::setEmojiFontFamily(QString family) +{ + emojiFont_ = family; + save(); +} + void UserSettings::setTheme(QString theme) { @@ -115,6 +123,8 @@ UserSettings::save() settings.setValue("desktop_notifications", hasDesktopNotifications_); settings.setValue("theme", theme()); settings.setValue("font_family", font_); + settings.setValue("emoji_font_family", emojiFont_); + settings.endGroup(); } @@ -230,22 +240,41 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge fontSizeOptionLayout->addWidget(fontSizeCombo_, 0, Qt::AlignRight); auto fontFamilyOptionLayout = new QHBoxLayout; + auto emojiFontFamilyOptionLayout = new QHBoxLayout; fontFamilyOptionLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto fontFamilyLabel = new QLabel(tr("Font Family"), this); + emojiFontFamilyOptionLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); + auto fontFamilyLabel = new QLabel(tr("Font Family"), this); + auto emojiFamilyLabel = new QLabel(tr("Emoji Font Famly"), this); fontFamilyLabel->setFont(font); - fontSelectionCombo_ = new QComboBox(this); + emojiFamilyLabel->setFont(font); + fontSelectionCombo_ = new QComboBox(this); + emojiFontSelectionCombo_ = new QComboBox(this); QFontDatabase fontDb; auto fontFamilies = fontDb.families(); + // TODO: Is there a way to limit to just emojis, rather than + // all emoji fonts? + auto emojiFamilies = fontDb.families(QFontDatabase::Symbol); + for (const auto &family : fontFamilies) { fontSelectionCombo_->addItem(family); } + for (const auto &family : emojiFamilies) { + emojiFontSelectionCombo_->addItem(family); + } + int fontIndex = fontSelectionCombo_->findText(settings_->font()); fontSelectionCombo_->setCurrentIndex(fontIndex); + fontIndex = emojiFontSelectionCombo_->findText(settings_->emojiFont()); + emojiFontSelectionCombo_->setCurrentIndex(fontIndex); + fontFamilyOptionLayout->addWidget(fontFamilyLabel); fontFamilyOptionLayout->addWidget(fontSelectionCombo_, 0, Qt::AlignRight); + emojiFontFamilyOptionLayout->addWidget(emojiFamilyLabel); + emojiFontFamilyOptionLayout->addWidget(emojiFontSelectionCombo_, 0, Qt::AlignRight); + auto themeOptionLayout_ = new QHBoxLayout; themeOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto themeLabel_ = new QLabel(tr("Theme"), this); @@ -346,11 +375,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge #if defined(Q_OS_MAC) scaleFactorLabel->hide(); scaleFactorCombo_->hide(); + emojiFamilyLabel->hide(); + emojiFontSelectionCombo_->hide(); #endif mainLayout_->addLayout(scaleFactorOptionLayout); mainLayout_->addLayout(fontSizeOptionLayout); mainLayout_->addLayout(fontFamilyOptionLayout); + mainLayout_->addLayout(emojiFontFamilyOptionLayout); mainLayout_->addWidget(new HorizontalLine(this)); mainLayout_->addLayout(themeOptionLayout_); mainLayout_->addWidget(new HorizontalLine(this)); @@ -393,6 +425,9 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge connect(fontSelectionCombo_, static_cast(&QComboBox::activated), [this](const QString &family) { settings_->setFontFamily(family.trimmed()); }); + connect(emojiFontSelectionCombo_, + static_cast(&QComboBox::activated), + [this](const QString &family) { settings_->setEmojiFontFamily(family.trimmed()); }); connect(trayToggle_, &Toggle::toggled, this, [this](bool isDisabled) { settings_->setTray(!isDisabled); if (isDisabled) { diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index 900f57e4..8b782213 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -56,6 +56,7 @@ public: void setFontSize(double size); void setFontFamily(QString family); + void setEmojiFontFamily(QString family); void setGroupView(bool state) { @@ -93,6 +94,8 @@ public: bool hasDesktopNotifications() const { return hasDesktopNotifications_; } double fontSize() const { return baseFontSize_; } QString font() const { return font_; } + QString emojiFont() const { return emojiFont_; } + signals: void groupViewStateChanged(bool state); @@ -107,6 +110,8 @@ private: bool hasDesktopNotifications_; double baseFontSize_; QString font_; + QString emojiFont_; + }; class HorizontalLine : public QFrame @@ -160,6 +165,7 @@ private: QComboBox *scaleFactorCombo_; QComboBox *fontSizeCombo_; QComboBox *fontSelectionCombo_; + QComboBox *emojiFontSelectionCombo_; int sideMargin_ = 0; }; diff --git a/src/emoji/ItemDelegate.cpp b/src/emoji/ItemDelegate.cpp index 304ab023..890e334a 100644 --- a/src/emoji/ItemDelegate.cpp +++ b/src/emoji/ItemDelegate.cpp @@ -17,6 +17,7 @@ #include #include +#include #include "emoji/ItemDelegate.h" @@ -43,9 +44,17 @@ ItemDelegate::paint(QPainter *painter, auto emoji = index.data(Qt::UserRole).toString(); + QSettings settings; + QFont font; - font.setFamily("emoji"); - font.setPixelSize(48); + QString userFontFamily = settings.value("user/emoji_font_family", "emoji").toString(); + if (!userFontFamily.isEmpty()) { + font.setFamily(userFontFamily); + } else { + font.setFamily("emoji"); + } + + font.setPixelSize(36); painter->setFont(font); if (option.state & QStyle::State_MouseOver) { painter->setBackgroundMode(Qt::OpaqueMode); diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index 22bdaaa2..cf5d68c6 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -727,7 +727,7 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam userName_ = new QLabel(this); userName_->setFont(usernameFont); - userName_->setText(fm.elidedText(sender, Qt::ElideRight, 500)); + userName_->setText(replaceEmoji(fm.elidedText(sender, Qt::ElideRight, 500))); userName_->setToolTip(user_id); userName_->setToolTipDuration(1500); userName_->setAttribute(Qt::WA_Hover); @@ -780,11 +780,15 @@ TimelineItem::replaceEmoji(const QString &body) QVector utf32_string = body.toUcs4(); + QSettings settings; + QString userFontFamily = settings.value("user/emoji_font_family", "emoji").toString(); + for (auto &code : utf32_string) { // TODO: Be more precise here. if (code > 9000) - fmtBody += QString("") + - QString::fromUcs4(&code, 1) + ""; + fmtBody += + QString("") + + QString::fromUcs4(&code, 1) + ""; else fmtBody += QString::fromUcs4(&code, 1); } -- cgit 1.5.1 From 86888ee71349cb416e79940e3b369e047cf7492a Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Fri, 26 Jul 2019 17:44:44 -0400 Subject: Fix bug with emoji font setting and clean linting --- src/ChatPage.cpp | 22 +++++++++++----------- src/ChatPage.h | 3 ++- src/UserSettingsPage.cpp | 2 +- src/UserSettingsPage.h | 2 -- src/Utils.cpp | 23 +++++++++++++++++++++++ src/Utils.h | 3 +++ src/timeline/TimelineItem.cpp | 27 ++------------------------- src/timeline/TimelineItem.h | 1 - 8 files changed, 42 insertions(+), 41 deletions(-) (limited to 'src/timeline/TimelineItem.cpp') diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index deefec14..18188429 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -90,13 +90,13 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) connect(sidebarActions_, &SideBarActions::joinRoom, this, &ChatPage::joinRoom); connect(sidebarActions_, &SideBarActions::createRoom, this, &ChatPage::createRoom); - user_info_widget_ = new UserInfoWidget(sideBar_); - //user_mentions_widget_ = new UserMentionsWidget(sideBar_); - room_list_ = new RoomList(sideBar_); + user_info_widget_ = new UserInfoWidget(sideBar_); + // user_mentions_widget_ = new UserMentionsWidget(sideBar_); + room_list_ = new RoomList(sideBar_); connect(room_list_, &RoomList::joinRoom, this, &ChatPage::joinRoom); sideBarLayout_->addWidget(user_info_widget_); - //sideBarLayout_->addWidget(user_mentions_widget_); + // sideBarLayout_->addWidget(user_mentions_widget_); sideBarLayout_->addWidget(room_list_); sideBarLayout_->addWidget(sidebarActions_); @@ -159,7 +159,8 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) 1000, "", "highlight", - [this, mentionsPos](const mtx::responses::Notifications &res, mtx::http::RequestErr err) { + [this, mentionsPos](const mtx::responses::Notifications &res, + mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to retrieve notifications: {} ({})", err->matrix_error.error, @@ -218,8 +219,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) } }); - - connect(room_list_, &RoomList::roomChanged, this, [this](const QString &roomid) { QStringList users; @@ -1007,10 +1006,11 @@ ChatPage::showNotificationsDialog(const mtx::responses::Notifications &res, cons nhlog::db()->warn("error while sending desktop notification: {}", e.what()); } } - notifDialog->setGeometry(widgetPos.x() - (width() / 10), widgetPos.y() + 25, width() / 5, height() / 2); - //notifDialog->move(widgetPos.x(), widgetPos.y()); - //notifDialog->setFixedWidth(width() / 10); - //notifDialog->setFixedHeight(height() / 2); + notifDialog->setGeometry( + widgetPos.x() - (width() / 10), widgetPos.y() + 25, width() / 5, height() / 2); + // notifDialog->move(widgetPos.x(), widgetPos.y()); + // notifDialog->setFixedWidth(width() / 10); + // notifDialog->setFixedHeight(height() / 2); notifDialog->raise(); notifDialog->show(); } diff --git a/src/ChatPage.h b/src/ChatPage.h index b2f04b02..3c97ba25 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -89,7 +89,8 @@ signals: void messageReply(const RelatedInfo &related); void notificationsRetrieved(const mtx::responses::Notifications &); - void highlightedNotifsRetrieved(const mtx::responses::Notifications &, const QPoint widgetPos); + void highlightedNotifsRetrieved(const mtx::responses::Notifications &, + const QPoint widgetPos); void uploadFailed(const QString &msg); void imageUploaded(const QString &roomid, diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 1ac3f738..49cb2c1f 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -239,7 +239,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge fontSizeOptionLayout->addWidget(fontSizeLabel); fontSizeOptionLayout->addWidget(fontSizeCombo_, 0, Qt::AlignRight); - auto fontFamilyOptionLayout = new QHBoxLayout; + auto fontFamilyOptionLayout = new QHBoxLayout; auto emojiFontFamilyOptionLayout = new QHBoxLayout; fontFamilyOptionLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); emojiFontFamilyOptionLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index 8b782213..ffff1e20 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -96,7 +96,6 @@ public: QString font() const { return font_; } QString emojiFont() const { return emojiFont_; } - signals: void groupViewStateChanged(bool state); @@ -111,7 +110,6 @@ private: double baseFontSize_; QString font_; QString emojiFont_; - }; class HorizontalLine : public QFrame diff --git a/src/Utils.cpp b/src/Utils.cpp index 863de79e..d6b092b1 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -26,6 +26,29 @@ utils::localUser() return settings.value("auth/user_id").toString(); } +QString +utils::replaceEmoji(const QString &body) +{ + QString fmtBody = ""; + + QVector utf32_string = body.toUcs4(); + + QSettings settings; + QString userFontFamily = settings.value("user/emoji_font_family", "emoji").toString(); + + for (auto &code : utf32_string) { + // TODO: Be more precise here. + if (code > 9000) + fmtBody += + QString("") + + QString::fromUcs4(&code, 1) + ""; + else + fmtBody += QString::fromUcs4(&code, 1); + } + + return fmtBody; +} + void utils::setScaleFactor(float factor) { diff --git a/src/Utils.h b/src/Utils.h index a840a187..756dc1e3 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -34,6 +34,9 @@ namespace utils { using TimelineEvent = mtx::events::collections::TimelineEvents; +QString +replaceEmoji(const QString &body); + QString localUser(); diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index cf5d68c6..54011279 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -650,7 +650,7 @@ TimelineItem::markReceived(bool isEncrypted) void TimelineItem::generateBody(const QString &body) { - body_ = new TextLabel(replaceEmoji(body), this); + body_ = new TextLabel(utils::replaceEmoji(body), this); body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) { @@ -727,7 +727,7 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam userName_ = new QLabel(this); userName_->setFont(usernameFont); - userName_->setText(replaceEmoji(fm.elidedText(sender, Qt::ElideRight, 500))); + userName_->setText(utils::replaceEmoji(fm.elidedText(sender, Qt::ElideRight, 500))); userName_->setToolTip(user_id); userName_->setToolTipDuration(1500); userName_->setAttribute(Qt::WA_Hover); @@ -773,29 +773,6 @@ TimelineItem::generateTimestamp(const QDateTime &time) QString(" %1 ").arg(time.toString("HH:mm"))); } -QString -TimelineItem::replaceEmoji(const QString &body) -{ - QString fmtBody = ""; - - QVector utf32_string = body.toUcs4(); - - QSettings settings; - QString userFontFamily = settings.value("user/emoji_font_family", "emoji").toString(); - - for (auto &code : utf32_string) { - // TODO: Be more precise here. - if (code > 9000) - fmtBody += - QString("") + - QString::fromUcs4(&code, 1) + ""; - else - fmtBody += QString::fromUcs4(&code, 1); - } - - return fmtBody; -} - void TimelineItem::setupAvatarLayout(const QString &userName) { diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h index e2b1cda4..c0dab6b8 100644 --- a/src/timeline/TimelineItem.h +++ b/src/timeline/TimelineItem.h @@ -276,7 +276,6 @@ private: QFutureWatcher *colorGenerating_; - QString replaceEmoji(const QString &body); QString event_id_; mtx::events::MessageType message_type_; QString room_id_; -- cgit 1.5.1 From 9fc079a4a9d6d9574aa2be5a9d9d2d69ee138863 Mon Sep 17 00:00:00 2001 From: Joseph Donofry Date: Fri, 26 Jul 2019 17:47:34 -0400 Subject: Remove uneeded log message --- src/timeline/TimelineItem.cpp | 1 - 1 file changed, 1 deletion(-) (limited to 'src/timeline/TimelineItem.cpp') diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index 54011279..e52dce7b 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -318,7 +318,6 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty, formatted_body = utils::linkifyMessage(formatted_body); formatted_body.replace("mx-reply", "div"); - nhlog::ui()->info("formatted_body: {}", formatted_body.toStdString()); generateTimestamp(timestamp); -- cgit 1.5.1 From 1c9cc33902d8242ec25b4416a1662ee6c9902583 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 28 Jul 2019 12:50:10 +0200 Subject: Try to localise timestamps I'm not sure, if that is the right way, but Qt doesn't really have a way to format custom localised dates, so I tried to find the closest approximations to what we currently have. Relates to #69 --- resources/langs/nheko_de.ts | 101 +++++++++++++++++++++++++++++++--------- resources/langs/nheko_el.ts | 101 +++++++++++++++++++++++++++++++--------- resources/langs/nheko_en.ts | 101 +++++++++++++++++++++++++++++++--------- resources/langs/nheko_fr.ts | 101 +++++++++++++++++++++++++++++++--------- resources/langs/nheko_nl.ts | 101 +++++++++++++++++++++++++++++++--------- resources/langs/nheko_pl.ts | 101 +++++++++++++++++++++++++++++++--------- resources/langs/nheko_ru.ts | 103 ++++++++++++++++++++++++++++++++--------- resources/langs/nheko_zh_CN.ts | 101 +++++++++++++++++++++++++++++++--------- src/Utils.cpp | 20 ++++---- src/dialogs/MemberList.cpp | 4 +- src/dialogs/ReadReceipts.cpp | 12 +++-- src/timeline/TimelineItem.cpp | 2 +- src/ui/InfoMessage.cpp | 13 +++--- 13 files changed, 668 insertions(+), 193 deletions(-) (limited to 'src/timeline/TimelineItem.cpp') diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts index f592a4d8..8c92c084 100644 --- a/resources/langs/nheko_de.ts +++ b/resources/langs/nheko_de.ts @@ -4,7 +4,7 @@ AudioItem - + Save File In Datei speichern @@ -12,7 +12,7 @@ ChatPage - + Failed to upload image. Please try again. Hochladen der Bilddatei fehlgeschlagen. Bitte versuche es erneut. @@ -32,7 +32,7 @@ Hochladen der Videodatei fehlgeschlagen. Bitte versuche es erneut. - + Failed to restore OLM account. Please login again. Wiederherstellung des OLM Accounts fehlgeschlagen. Bitte logge dich erneut ein. @@ -42,7 +42,7 @@ Nachrichten konnten nicht aus dem Cache geladen werden. Bitte melde dich erneut an. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Erstellung des Schlüsselmaterials fehlgeschlagen. Antwort des Servers: %1 %2. Bitte versuche es später erneut. @@ -118,7 +118,7 @@ FileItem - + Save File Datei speichern @@ -126,7 +126,7 @@ ImageItem - + Save image Bild speichern @@ -205,8 +205,8 @@ Teilnehmerliste - - ESC + + OK @@ -277,7 +277,7 @@ RoomInfo - + no version stored @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room Raum verlassen - + Accept Akzeptieren @@ -331,7 +331,7 @@ StatusIndicator - + Encrypted @@ -354,13 +354,13 @@ TextInputWidget - + Send a file - + Write a message... Schreibe eine Nachricht... @@ -424,7 +424,12 @@ - + + Mentions + + + + Invite users Benutzer einladen @@ -460,7 +465,7 @@ TypingDisplay - + is typing tippt @@ -481,7 +486,7 @@ UserSettingsPage - + Minimize to tray Ins Benachrichtigungsfeld minimieren @@ -521,12 +526,17 @@ - + Font Family - + + Emoji Font Famly + + + + Theme Erscheinungsbild @@ -566,7 +576,7 @@ ALLGEMEINES - + Open Sessions File @@ -635,6 +645,14 @@ ANMELDEN + + descriptiveTime + + + Yesterday + + + dialogs::CreateRoom @@ -778,7 +796,7 @@ Medien-Größe: %2 dialogs::ReadReceipts - + Read receipts Lesebestätigungen @@ -793,6 +811,19 @@ Medien-Größe: %2 + + dialogs::ReceiptItem + + + Today %1 + + + + + Yesterday %1 + + + dialogs::RoomSettings @@ -1003,4 +1034,32 @@ Medien-Größe: %2 Flaggen + + utils + + + You + + + + + sent a file. + + + + + sent an image. + + + + + sent an audio file. + + + + + sent a video + + + diff --git a/resources/langs/nheko_el.ts b/resources/langs/nheko_el.ts index 5a6be4cb..74b14266 100644 --- a/resources/langs/nheko_el.ts +++ b/resources/langs/nheko_el.ts @@ -4,7 +4,7 @@ AudioItem - + Save File Αποθήκευση @@ -12,7 +12,7 @@ ChatPage - + Failed to upload image. Please try again. @@ -32,7 +32,7 @@ - + Failed to restore OLM account. Please login again. @@ -42,7 +42,7 @@ - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. @@ -118,7 +118,7 @@ FileItem - + Save File Αποθήκευση @@ -126,7 +126,7 @@ ImageItem - + Save image Αποθήκευση Εικόνας @@ -205,8 +205,8 @@ Μέλη - - ESC + + OK @@ -277,7 +277,7 @@ RoomInfo - + no version stored @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room Βγές - + Accept Αποδοχή @@ -331,7 +331,7 @@ StatusIndicator - + Encrypted @@ -354,13 +354,13 @@ TextInputWidget - + Send a file - + Write a message... Γράψε ένα μήνυμα... @@ -424,7 +424,12 @@ - + + Mentions + + + + Invite users Προσκάλεσε χρήστες @@ -460,7 +465,7 @@ TypingDisplay - + is typing πληκτρολογεί @@ -481,7 +486,7 @@ UserSettingsPage - + Minimize to tray Ελαχιστοποίηση @@ -521,12 +526,17 @@ - + Font Family - + + Emoji Font Famly + + + + Theme Φόντο @@ -566,7 +576,7 @@ ΓΕΝΙΚΑ - + Open Sessions File @@ -635,6 +645,14 @@ ΕΙΣΟΔΟΣ + + descriptiveTime + + + Yesterday + + + dialogs::CreateRoom @@ -776,7 +794,7 @@ Media size: %2 dialogs::ReadReceipts - + Read receipts @@ -791,6 +809,19 @@ Media size: %2 + + dialogs::ReceiptItem + + + Today %1 + + + + + Yesterday %1 + + + dialogs::RoomSettings @@ -1001,4 +1032,32 @@ Media size: %2 Σημαίες + + utils + + + You + + + + + sent a file. + + + + + sent an image. + + + + + sent an audio file. + + + + + sent a video + + + diff --git a/resources/langs/nheko_en.ts b/resources/langs/nheko_en.ts index d8259dc5..5c4bad0e 100644 --- a/resources/langs/nheko_en.ts +++ b/resources/langs/nheko_en.ts @@ -4,7 +4,7 @@ AudioItem - + Save File Save File @@ -12,7 +12,7 @@ ChatPage - + Failed to upload image. Please try again. Failed to upload image. Please try again. @@ -32,7 +32,7 @@ Failed to upload video. Please try again. - + Failed to restore OLM account. Please login again. Failed to restore OLM account. Please login again. @@ -42,7 +42,7 @@ Failed to restore save data. Please login again. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Failed to setup encryption keys. Server response: %1 %2. Please try again later. @@ -118,7 +118,7 @@ FileItem - + Save File Save File @@ -126,7 +126,7 @@ ImageItem - + Save image Save image @@ -205,8 +205,8 @@ Room members - - ESC + + OK @@ -277,7 +277,7 @@ RoomInfo - + no version stored no version stored @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room Leave room - + Accept Accept @@ -331,7 +331,7 @@ StatusIndicator - + Encrypted @@ -354,13 +354,13 @@ TextInputWidget - + Send a file - + Write a message... @@ -424,7 +424,12 @@ - + + Mentions + + + + Invite users @@ -460,7 +465,7 @@ TypingDisplay - + is typing @@ -481,7 +486,7 @@ UserSettingsPage - + Minimize to tray @@ -521,12 +526,17 @@ - + Font Family - + + Emoji Font Famly + + + + Theme @@ -566,7 +576,7 @@ - + Open Sessions File @@ -635,6 +645,14 @@ + + descriptiveTime + + + Yesterday + + + dialogs::CreateRoom @@ -776,7 +794,7 @@ Media size: %2 dialogs::ReadReceipts - + Read receipts @@ -791,6 +809,19 @@ Media size: %2 + + dialogs::ReceiptItem + + + Today %1 + + + + + Yesterday %1 + + + dialogs::RoomSettings @@ -1001,4 +1032,32 @@ Media size: %2 Flags + + utils + + + You + + + + + sent a file. + + + + + sent an image. + + + + + sent an audio file. + + + + + sent a video + + + diff --git a/resources/langs/nheko_fr.ts b/resources/langs/nheko_fr.ts index beab8752..f8425e26 100644 --- a/resources/langs/nheko_fr.ts +++ b/resources/langs/nheko_fr.ts @@ -4,7 +4,7 @@ AudioItem - + Save File Enregistrer le fichier @@ -12,7 +12,7 @@ ChatPage - + Failed to upload image. Please try again. @@ -32,7 +32,7 @@ - + Failed to restore OLM account. Please login again. @@ -42,7 +42,7 @@ - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. @@ -118,7 +118,7 @@ FileItem - + Save File Enregistrer le fichier @@ -126,7 +126,7 @@ ImageItem - + Save image Enregistrer l'image @@ -205,8 +205,8 @@ Membres du salon - - ESC + + OK @@ -278,7 +278,7 @@ RoomInfo - + no version stored @@ -286,12 +286,12 @@ RoomInfoListItem - + Leave room Quitter le salon - + Accept Accepter @@ -332,7 +332,7 @@ StatusIndicator - + Encrypted @@ -355,13 +355,13 @@ TextInputWidget - + Send a file - + Write a message... Écrivez un message... @@ -425,7 +425,12 @@ - + + Mentions + + + + Invite users Inviter des utilisateurs @@ -461,7 +466,7 @@ TypingDisplay - + is typing est en train d'écrire @@ -482,7 +487,7 @@ UserSettingsPage - + Minimize to tray Réduire à la barre des tâches @@ -522,12 +527,17 @@ - + Font Family - + + Emoji Font Famly + + + + Theme Thème @@ -567,7 +577,7 @@ GÉNÉRAL - + Open Sessions File @@ -636,6 +646,14 @@ CONNEXION + + descriptiveTime + + + Yesterday + + + dialogs::CreateRoom @@ -779,7 +797,7 @@ Taille du média : %2 dialogs::ReadReceipts - + Read receipts Accusés de lecture @@ -794,6 +812,19 @@ Taille du média : %2 + + dialogs::ReceiptItem + + + Today %1 + + + + + Yesterday %1 + + + dialogs::RoomSettings @@ -1004,4 +1035,32 @@ Taille du média : %2 Drapeaux + + utils + + + You + + + + + sent a file. + + + + + sent an image. + + + + + sent an audio file. + + + + + sent a video + + + diff --git a/resources/langs/nheko_nl.ts b/resources/langs/nheko_nl.ts index 4c81ec76..51ec18fe 100644 --- a/resources/langs/nheko_nl.ts +++ b/resources/langs/nheko_nl.ts @@ -4,7 +4,7 @@ AudioItem - + Save File Bestand opslaan @@ -12,7 +12,7 @@ ChatPage - + Failed to upload image. Please try again. @@ -32,7 +32,7 @@ - + Failed to restore OLM account. Please login again. @@ -42,7 +42,7 @@ - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. @@ -118,7 +118,7 @@ FileItem - + Save File Bestand opslaan @@ -126,7 +126,7 @@ ImageItem - + Save image Afbeelding opslaan @@ -205,8 +205,8 @@ Kamerleden - - ESC + + OK @@ -277,7 +277,7 @@ RoomInfo - + no version stored @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room Kamer verlaten - + Accept Accepteren @@ -331,7 +331,7 @@ StatusIndicator - + Encrypted @@ -354,13 +354,13 @@ TextInputWidget - + Send a file - + Write a message... Typ een bericht... @@ -424,7 +424,12 @@ - + + Mentions + + + + Invite users Gebruikers uitnodigen @@ -460,7 +465,7 @@ TypingDisplay - + is typing is aan het typen @@ -481,7 +486,7 @@ UserSettingsPage - + Minimize to tray Minimaliseren naar systeemvak @@ -521,12 +526,17 @@ - + Font Family - + + Emoji Font Famly + + + + Theme Thema @@ -566,7 +576,7 @@ ALGEMEEN - + Open Sessions File @@ -635,6 +645,14 @@ INLOGGEN + + descriptiveTime + + + Yesterday + + + dialogs::CreateRoom @@ -778,7 +796,7 @@ Mediagrootte: %2 dialogs::ReadReceipts - + Read receipts Leesbevestigingen @@ -793,6 +811,19 @@ Mediagrootte: %2 + + dialogs::ReceiptItem + + + Today %1 + + + + + Yesterday %1 + + + dialogs::RoomSettings @@ -1003,4 +1034,32 @@ Mediagrootte: %2 Vlaggen + + utils + + + You + + + + + sent a file. + + + + + sent an image. + + + + + sent an audio file. + + + + + sent a video + + + diff --git a/resources/langs/nheko_pl.ts b/resources/langs/nheko_pl.ts index edea85b9..ca021554 100644 --- a/resources/langs/nheko_pl.ts +++ b/resources/langs/nheko_pl.ts @@ -4,7 +4,7 @@ AudioItem - + Save File Zapisz plik @@ -12,7 +12,7 @@ ChatPage - + Failed to upload image. Please try again. Nie udało się wysłać obrazu. Spróbuj ponownie. @@ -32,7 +32,7 @@ Nie udało się wysłać filmu. Spróbuj ponownie. - + Failed to restore OLM account. Please login again. Nie udało się przywrócić konta OLM. Spróbuj zalogować się ponownie. @@ -42,7 +42,7 @@ Nie udało się przywrócić zapisanych danych. Spróbuj zalogować się ponownie. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. @@ -118,7 +118,7 @@ FileItem - + Save File Zapisz plik @@ -126,7 +126,7 @@ ImageItem - + Save image Zapisz obraz @@ -205,8 +205,8 @@ Członkowie pokoju - - ESC + + OK @@ -277,7 +277,7 @@ RoomInfo - + no version stored @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room Opuść pokój - + Accept Akceptuj @@ -331,7 +331,7 @@ StatusIndicator - + Encrypted Szyfrowana @@ -354,13 +354,13 @@ TextInputWidget - + Send a file Wyślij plik - + Write a message... Napisz wiadomość… @@ -424,7 +424,12 @@ Ustawienia pokoju - + + Mentions + + + + Invite users Zaproś użytkowników @@ -460,7 +465,7 @@ TypingDisplay - + is typing pisze @@ -481,7 +486,7 @@ UserSettingsPage - + Minimize to tray Zminimalizuj do paska zadań @@ -521,12 +526,17 @@ - + Font Family - + + Emoji Font Famly + + + + Theme Motyw @@ -566,7 +576,7 @@ OGÓLNE - + Open Sessions File @@ -635,6 +645,14 @@ ZALOGUJ SIĘ + + descriptiveTime + + + Yesterday + + + dialogs::CreateRoom @@ -778,7 +796,7 @@ Rozmiar multimediów: %2 dialogs::ReadReceipts - + Read receipts Potwierdzenia przeczytania @@ -793,6 +811,19 @@ Rozmiar multimediów: %2 + + dialogs::ReceiptItem + + + Today %1 + + + + + Yesterday %1 + + + dialogs::RoomSettings @@ -1006,4 +1037,32 @@ Rozmiar multimediów: %2 Flagi + + utils + + + You + + + + + sent a file. + + + + + sent an image. + + + + + sent an audio file. + + + + + sent a video + + + diff --git a/resources/langs/nheko_ru.ts b/resources/langs/nheko_ru.ts index 4c157884..39cf7b72 100644 --- a/resources/langs/nheko_ru.ts +++ b/resources/langs/nheko_ru.ts @@ -4,7 +4,7 @@ AudioItem - + Save File Сохранить файл @@ -12,7 +12,7 @@ ChatPage - + Failed to upload image. Please try again. Не удалось загрузить изображение. Пожалуйста, попробуйте еще раз. @@ -32,7 +32,7 @@ Не удалось загрузить видео. Пожалуйста, попробуйте еще раз. - + Failed to restore OLM account. Please login again. Не удалось восстановить учетную запись OLM. Пожалуйста, войдите снова. @@ -42,7 +42,7 @@ Не удалось восстановить сохраненные данные. Пожалуйста, войдите снова. - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. Не удалось настроить ключи шифрования. Ответ сервера:%1 %2. Пожалуйста, попробуйте позже. @@ -118,7 +118,7 @@ FileItem - + Save File Сохранить файл @@ -126,7 +126,7 @@ ImageItem - + Save image Сохранить изображение @@ -205,9 +205,9 @@ Участники комнаты - - ESC - + + OK + @@ -277,7 +277,7 @@ RoomInfo - + no version stored @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room Покинуть комнату - + Accept Принять @@ -331,7 +331,7 @@ StatusIndicator - + Encrypted Зашифровано @@ -354,13 +354,13 @@ TextInputWidget - + Send a file Отправить файл - + Write a message... Написать сообщение... @@ -424,7 +424,12 @@ Настройки комнаты - + + Mentions + + + + Invite users Пригласить пользователей @@ -460,7 +465,7 @@ TypingDisplay - + is typing печатает @@ -481,7 +486,7 @@ UserSettingsPage - + Minimize to tray Сворачивать в системную панель @@ -521,12 +526,17 @@ Размер шрифта - + Font Family - + + Emoji Font Famly + + + + Theme Тема @@ -566,7 +576,7 @@ ГЛАВНОЕ - + Open Sessions File Открыть файл сеансов @@ -636,6 +646,14 @@ ВХОД + + descriptiveTime + + + Yesterday + + + dialogs::CreateRoom @@ -779,7 +797,7 @@ Media size: %2 dialogs::ReadReceipts - + Read receipts Подтверждать прочтение @@ -794,6 +812,19 @@ Media size: %2 + + dialogs::ReceiptItem + + + Today %1 + + + + + Yesterday %1 + + + dialogs::RoomSettings @@ -1005,4 +1036,32 @@ Media size: %2 + + utils + + + You + + + + + sent a file. + + + + + sent an image. + + + + + sent an audio file. + + + + + sent a video + + + diff --git a/resources/langs/nheko_zh_CN.ts b/resources/langs/nheko_zh_CN.ts index ca7c6e22..08463cd7 100644 --- a/resources/langs/nheko_zh_CN.ts +++ b/resources/langs/nheko_zh_CN.ts @@ -4,7 +4,7 @@ AudioItem - + Save File 保存文件 @@ -12,7 +12,7 @@ ChatPage - + Failed to upload image. Please try again. 上传图像失败。请重试。 @@ -32,7 +32,7 @@ 上传视频失败。请重试。 - + Failed to restore OLM account. Please login again. 恢复 OLM 账户失败。请重新登录。 @@ -42,7 +42,7 @@ 恢复保存的数据失败。请重新登录。 - + Failed to setup encryption keys. Server response: %1 %2. Please try again later. @@ -118,7 +118,7 @@ FileItem - + Save File 保存文件 @@ -126,7 +126,7 @@ ImageItem - + Save image 保存图像 @@ -205,8 +205,8 @@ 聊天室成员 - - ESC + + OK @@ -277,7 +277,7 @@ RoomInfo - + no version stored @@ -285,12 +285,12 @@ RoomInfoListItem - + Leave room 离开聊天室 - + Accept 接受 @@ -331,7 +331,7 @@ StatusIndicator - + Encrypted 加密的 @@ -354,13 +354,13 @@ TextInputWidget - + Send a file 发送一个文件 - + Write a message... 写一条消息... @@ -424,7 +424,12 @@ 聊天室选项 - + + Mentions + + + + Invite users 邀请用户 @@ -460,7 +465,7 @@ TypingDisplay - + is typing 正在打字 @@ -481,7 +486,7 @@ UserSettingsPage - + Minimize to tray 最小化至托盘 @@ -521,12 +526,17 @@ - + Font Family - + + Emoji Font Famly + + + + Theme 主题 @@ -566,7 +576,7 @@ 通用 - + Open Sessions File 打开会话文件 @@ -635,6 +645,14 @@ 登录 + + descriptiveTime + + + Yesterday + + + dialogs::CreateRoom @@ -778,7 +796,7 @@ Media size: %2 dialogs::ReadReceipts - + Read receipts 阅读回执 @@ -793,6 +811,19 @@ Media size: %2 + + dialogs::ReceiptItem + + + Today %1 + + + + + Yesterday %1 + + + dialogs::RoomSettings @@ -1012,4 +1043,32 @@ Media size: %2 Flags + + utils + + + You + + + + + sent a file. + + + + + sent an image. + + + + + sent an audio file. + + + + + sent a video + + + diff --git a/src/Utils.cpp b/src/Utils.cpp index d6b092b1..a3c15c96 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -99,13 +99,13 @@ utils::descriptiveTime(const QDateTime &then) const auto days = then.daysTo(now); if (days == 0) - return then.toString("HH:mm"); + return then.time().toString(Qt::DefaultLocaleShortDate); else if (days < 2) - return QString("Yesterday"); - else if (days < 365) - return then.toString("dd/MM"); + return QString(QCoreApplication::translate("descriptiveTime", "Yesterday")); + else if (days < 7) + return then.toString("dddd"); - return then.toString("dd/MM/yy"); + return then.date().toString(Qt::DefaultLocaleShortDate); } DescInfo @@ -147,7 +147,7 @@ utils::getMessageDescription(const TimelineEvent &event, DescInfo info; if (sender == localUser) - info.username = "You"; + info.username = QCoreApplication::translate("utils", "You"); else info.username = username; @@ -366,16 +366,16 @@ utils::getQuoteBody(const RelatedInfo &related) return markdownToHtml(related.quoted_body); } case MsgType::File: { - return QString("sent a file."); + return QString(QCoreApplication::translate("utils", "sent a file.")); } case MsgType::Image: { - return QString("sent an image."); + return QString(QCoreApplication::translate("utils", "sent an image.")); } case MsgType::Audio: { - return QString("sent an audio file."); + return QString(QCoreApplication::translate("utils", "sent an audio file.")); } case MsgType::Video: { - return QString("sent a video"); + return QString(QCoreApplication::translate("utils", "sent a video")); } default: { return related.quoted_body; diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp index f4167143..3b957c15 100644 --- a/src/dialogs/MemberList.cpp +++ b/src/dialogs/MemberList.cpp @@ -97,7 +97,7 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) topLabel_->setAlignment(Qt::AlignCenter); topLabel_->setFont(font); - auto okBtn = new QPushButton("OK", this); + auto okBtn = new QPushButton(tr("OK"), this); auto buttonLayout = new QHBoxLayout(); buttonLayout->setSpacing(15); @@ -126,7 +126,7 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) qCritical() << e.what(); } - auto closeShortcut = new QShortcut(QKeySequence(tr("ESC")), this); + auto closeShortcut = new QShortcut(QKeySequence("ESC"), this); connect(closeShortcut, &QShortcut::activated, this, &MemberList::close); connect(okBtn, &QPushButton::clicked, this, &MemberList::close); } diff --git a/src/dialogs/ReadReceipts.cpp b/src/dialogs/ReadReceipts.cpp index dc4145db..03ce3068 100644 --- a/src/dialogs/ReadReceipts.cpp +++ b/src/dialogs/ReadReceipts.cpp @@ -78,13 +78,15 @@ ReceiptItem::dateFormat(const QDateTime &then) const auto days = then.daysTo(now); if (days == 0) - return QString("Today %1").arg(then.toString("HH:mm")); + return tr("Today %1").arg(then.time().toString(Qt::DefaultLocaleShortDate)); else if (days < 2) - return QString("Yesterday %1").arg(then.toString("HH:mm")); - else if (days < 365) - return then.toString("dd/MM HH:mm"); + return tr("Yesterday %1").arg(then.time().toString(Qt::DefaultLocaleShortDate)); + else if (days < 7) + return QString("%1 %2") + .arg(then.toString("dddd")) + .arg(then.time().toString(Qt::DefaultLocaleShortDate)); - return then.toString("dd/MM/yy"); + return then.toString(Qt::DefaultLocaleShortDate); } ReadReceipts::ReadReceipts(QWidget *parent) diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index e52dce7b..c0d7f97f 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -951,4 +951,4 @@ TimelineItem::openRawMessageViewer() const "failed to serialize event ({}, {})", room_id, event_id); } }); -} \ No newline at end of file +} diff --git a/src/ui/InfoMessage.cpp b/src/ui/InfoMessage.cpp index fa575d76..27bc0a5f 100644 --- a/src/ui/InfoMessage.cpp +++ b/src/ui/InfoMessage.cpp @@ -2,6 +2,7 @@ #include "Config.h" #include +#include #include #include #include @@ -61,14 +62,14 @@ DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent) { auto now = QDateTime::currentDateTime(); - QString fmt; + QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); - if (now.date().year() != datetime.date().year()) - fmt = QString("ddd d MMMM yy"); - else - fmt = QString("ddd d MMMM"); + if (now.date().year() == datetime.date().year()) { + QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); + fmt = fmt.remove(rx); + } - msg_ = datetime.toString(fmt); + msg_ = datetime.date().toString(fmt); QFontMetrics fm{font()}; #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) -- cgit 1.5.1 From b28115eb37e44d7c6cc3c6bf7292feea0f6a35c6 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 28 Jul 2019 16:50:04 +0200 Subject: Fix message_type not being initialized correctly Fixes "sent an audio file" replies, that were actually replies to text messages. --- src/timeline/TimelineItem.cpp | 12 ++++++++++++ src/timeline/TimelineItem.h | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) (limited to 'src/timeline/TimelineItem.cpp') diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index c0d7f97f..dd3b48c3 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -282,6 +282,7 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty, const QString &room_id, QWidget *parent) : QWidget(parent) + , message_type_(ty) , room_id_{room_id} { init(); @@ -341,6 +342,7 @@ TimelineItem::TimelineItem(ImageItem *image, const QString &room_id, QWidget *parent) : QWidget{parent} + , message_type_(mtx::events::MessageType::Image) , room_id_{room_id} { init(); @@ -356,6 +358,7 @@ TimelineItem::TimelineItem(FileItem *file, const QString &room_id, QWidget *parent) : QWidget{parent} + , message_type_(mtx::events::MessageType::File) , room_id_{room_id} { init(); @@ -369,6 +372,7 @@ TimelineItem::TimelineItem(AudioItem *audio, const QString &room_id, QWidget *parent) : QWidget{parent} + , message_type_(mtx::events::MessageType::Audio) , room_id_{room_id} { init(); @@ -382,6 +386,7 @@ TimelineItem::TimelineItem(VideoItem *video, const QString &room_id, QWidget *parent) : QWidget{parent} + , message_type_(mtx::events::MessageType::Video) , room_id_{room_id} { init(); @@ -395,6 +400,7 @@ TimelineItem::TimelineItem(ImageItem *image, const QString &room_id, QWidget *parent) : QWidget(parent) + , message_type_(mtx::events::MessageType::Image) , room_id_{room_id} { setupWidgetLayout, ImageItem>( @@ -426,6 +432,7 @@ TimelineItem::TimelineItem(FileItem *file, const QString &room_id, QWidget *parent) : QWidget(parent) + , message_type_(mtx::events::MessageType::File) , room_id_{room_id} { setupWidgetLayout, FileItem>( @@ -440,6 +447,7 @@ TimelineItem::TimelineItem(AudioItem *audio, const QString &room_id, QWidget *parent) : QWidget(parent) + , message_type_(mtx::events::MessageType::Audio) , room_id_{room_id} { setupWidgetLayout, AudioItem>( @@ -454,6 +462,7 @@ TimelineItem::TimelineItem(VideoItem *video, const QString &room_id, QWidget *parent) : QWidget(parent) + , message_type_(mtx::events::MessageType::Video) , room_id_{room_id} { setupWidgetLayout, VideoItem>( @@ -470,6 +479,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent const QString &room_id, QWidget *parent) : QWidget(parent) + , message_type_(mtx::events::MessageType::Emote) , room_id_{room_id} { init(); @@ -565,6 +576,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent const QString &room_id, QWidget *parent) : QWidget(parent) + , message_type_(mtx::events::MessageType::Text) , room_id_{room_id} { init(); diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h index c0dab6b8..a3294d3f 100644 --- a/src/timeline/TimelineItem.h +++ b/src/timeline/TimelineItem.h @@ -277,7 +277,7 @@ private: QFutureWatcher *colorGenerating_; QString event_id_; - mtx::events::MessageType message_type_; + mtx::events::MessageType message_type_ = mtx::events::MessageType::Unknown; QString room_id_; DescInfo descriptionMsg_; -- cgit 1.5.1 From 52056a79fa94117ce0fc8f388458ceb48cc3be54 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 26 Aug 2019 01:24:56 +0200 Subject: Try to reduce memory usage by reusing avatar pixmaps --- src/AvatarProvider.cpp | 55 +++++++++++++++++++++++++++++++------ src/AvatarProvider.h | 12 ++++++-- src/Cache.cpp | 5 +--- src/Cache.h | 1 - src/ChatPage.cpp | 38 +++---------------------- src/ChatPage.h | 4 +-- src/Logging.cpp | 2 ++ src/Logging.h | 2 ++ src/RoomInfoListItem.cpp | 9 ++++-- src/RoomInfoListItem.h | 2 +- src/RoomList.cpp | 39 ++------------------------ src/RoomList.h | 6 ++-- src/TopRoomBar.cpp | 5 ++-- src/TopRoomBar.h | 2 +- src/UserInfoWidget.cpp | 19 ++++++------- src/UserInfoWidget.h | 2 +- src/dialogs/MemberList.cpp | 12 ++------ src/dialogs/ReadReceipts.cpp | 8 ++---- src/dialogs/RoomSettings.cpp | 17 ++++++------ src/dialogs/RoomSettings.h | 4 +-- src/dialogs/UserProfile.cpp | 6 ++-- src/popups/PopupItem.cpp | 26 ++++-------------- src/timeline/.TimelineItem.cpp.swp | Bin 0 -> 114688 bytes src/timeline/TimelineItem.cpp | 22 ++++++--------- src/timeline/TimelineItem.h | 8 ++---- src/ui/Avatar.cpp | 39 ++++++++++++-------------- src/ui/Avatar.h | 10 ++++--- 27 files changed, 148 insertions(+), 207 deletions(-) create mode 100644 src/timeline/.TimelineItem.cpp.swp (limited to 'src/timeline/TimelineItem.cpp') diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp index 57b61c75..277a4030 100644 --- a/src/AvatarProvider.cpp +++ b/src/AvatarProvider.cpp @@ -16,30 +16,44 @@ */ #include +#include #include +#include #include "AvatarProvider.h" #include "Cache.h" #include "Logging.h" #include "MatrixClient.h" -namespace AvatarProvider { +static QPixmapCache avatar_cache; +namespace AvatarProvider { void -resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback callback) +resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback callback) { - const auto key = QString("%1 %2").arg(room_id).arg(user_id); - const auto avatarUrl = Cache::avatarUrl(room_id, user_id); + avatar_cache.setCacheLimit(1024 * 1024); - if (!Cache::AvatarUrls.contains(key) || !cache::client()) + const auto cacheKey = avatarUrl + "_size_" + size; + + if (!cache::client()) return; if (avatarUrl.isEmpty()) return; + QPixmap pixmap; + if (avatar_cache.find(cacheKey, pixmap)) { + nhlog::net()->info("cached pixmap {}", avatarUrl.toStdString()); + callback(pixmap); + return; + } + auto data = cache::client()->image(avatarUrl); if (!data.isNull()) { - callback(QImage::fromData(data)); + pixmap.loadFromData(data); + avatar_cache.insert(cacheKey, pixmap); + nhlog::net()->info("loaded pixmap from disk cache {}", avatarUrl.toStdString()); + callback(pixmap); return; } @@ -47,7 +61,12 @@ resolve(const QString &room_id, const QString &user_id, QObject *receiver, Avata QObject::connect(proxy.get(), &AvatarProxy::avatarDownloaded, receiver, - [callback](const QByteArray &data) { callback(QImage::fromData(data)); }); + [callback, cacheKey](const QByteArray &data) { + QPixmap pm; + pm.loadFromData(data); + avatar_cache.insert(cacheKey, pm); + callback(pm); + }); mtx::http::ThumbOpts opts; opts.width = 256; @@ -67,8 +86,26 @@ resolve(const QString &room_id, const QString &user_id, QObject *receiver, Avata cache::client()->saveImage(opts.mxc_url, res); - auto data = QByteArray(res.data(), res.size()); - emit proxy->avatarDownloaded(data); + nhlog::net()->info("downloaded pixmap {}", opts.mxc_url); + + emit proxy->avatarDownloaded(QByteArray(res.data(), res.size())); }); } + +void +resolve(const QString &room_id, + const QString &user_id, + int size, + QObject *receiver, + AvatarCallback callback) +{ + const auto key = QString("%1 %2").arg(room_id).arg(user_id); + const auto avatarUrl = Cache::avatarUrl(room_id, user_id); + const auto cacheKey = avatarUrl + "_size_" + size; + + if (!Cache::AvatarUrls.contains(key) || !cache::client()) + return; + + resolve(avatarUrl, size, receiver, callback); +} } diff --git a/src/AvatarProvider.h b/src/AvatarProvider.h index 4b4e15e9..47ed028e 100644 --- a/src/AvatarProvider.h +++ b/src/AvatarProvider.h @@ -17,7 +17,7 @@ #pragma once -#include +#include #include class AvatarProxy : public QObject @@ -28,9 +28,15 @@ signals: void avatarDownloaded(const QByteArray &data); }; -using AvatarCallback = std::function; +using AvatarCallback = std::function; namespace AvatarProvider { void -resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback cb); +resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback cb); +void +resolve(const QString &room_id, + const QString &user_id, + int size, + QObject *receiver, + AvatarCallback cb); } diff --git a/src/Cache.cpp b/src/Cache.cpp index ef0f951e..083dbe89 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -1830,10 +1830,7 @@ Cache::searchRooms(const std::string &query, std::uint8_t max_items) std::vector results; for (auto it = items.begin(); it != end; it++) { - results.push_back( - RoomSearchResult{it->second.first, - it->second.second, - QImage::fromData(image(txn, it->second.second.avatar_url))}); + results.push_back(RoomSearchResult{it->second.first, it->second.second}); } txn.commit(); diff --git a/src/Cache.h b/src/Cache.h index 302bb65b..0da49793 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -153,7 +153,6 @@ struct RoomSearchResult { std::string room_id; RoomInfo info; - QImage img; }; Q_DECLARE_METATYPE(RoomSearchResult) diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 2ed64b6b..21ded4b3 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -774,12 +774,12 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) } void -ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img) +ChatPage::updateTopBarAvatar(const QString &roomid, const QString &img) { if (current_room_ != roomid) return; - top_bar_->updateRoomAvatar(img.toImage()); + top_bar_->updateRoomAvatar(img); } void @@ -807,7 +807,7 @@ ChatPage::changeTopRoomInfo(const QString &room_id) if (img.isNull()) top_bar_->updateRoomAvatarFromName(name); else - top_bar_->updateRoomAvatar(img); + top_bar_->updateRoomAvatar(avatar_url); } catch (const lmdb::error &e) { nhlog::ui()->error("failed to change top bar room info: {}", e.what()); @@ -1337,37 +1337,7 @@ ChatPage::getProfileInfo() emit setUserDisplayName(QString::fromStdString(res.display_name)); - if (cache::client()) { - auto data = cache::client()->image(res.avatar_url); - if (!data.isNull()) { - emit setUserAvatar(QImage::fromData(data)); - return; - } - } - - if (res.avatar_url.empty()) - return; - - http::client()->download( - res.avatar_url, - [this, res](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to download user avatar: {} - {}", - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - if (cache::client()) - cache::client()->saveImage(res.avatar_url, data); - - emit setUserAvatar( - QImage::fromData(QByteArray(data.data(), data.size()))); - }); + emit setUserAvatar(QString::fromStdString(res.avatar_url)); }); http::client()->joined_groups( diff --git a/src/ChatPage.h b/src/ChatPage.h index 189af387..e41ae1ae 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -129,7 +129,7 @@ signals: void ownProfileOk(); void setUserDisplayName(const QString &name); - void setUserAvatar(const QImage &avatar); + void setUserAvatar(const QString &avatar); void loggedOut(); void trySyncCb(); @@ -159,7 +159,7 @@ signals: private slots: void showUnreadMessageNotification(int count); - void updateTopBarAvatar(const QString &roomid, const QPixmap &img); + void updateTopBarAvatar(const QString &roomid, const QString &img); void changeTopRoomInfo(const QString &room_id); void logout(); void removeRoom(const QString &room_id); diff --git a/src/Logging.cpp b/src/Logging.cpp index 686274d8..32287582 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -16,6 +16,8 @@ constexpr auto MAX_LOG_FILES = 3; } namespace nhlog { +bool enable_debug_log_from_commandline = false; + void init(const std::string &file_path) { diff --git a/src/Logging.h b/src/Logging.h index 2feae60d..e54f3c3f 100644 --- a/src/Logging.h +++ b/src/Logging.h @@ -18,4 +18,6 @@ db(); std::shared_ptr crypto(); + +extern bool enable_debug_log_from_commandline; } diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp index 9bcce134..dbcd6806 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp @@ -21,6 +21,7 @@ #include #include +#include "AvatarProvider.h" #include "Cache.h" #include "Config.h" #include "RoomInfoListItem.h" @@ -434,10 +435,12 @@ RoomInfoListItem::mousePressEvent(QMouseEvent *event) } void -RoomInfoListItem::setAvatar(const QImage &img) +RoomInfoListItem::setAvatar(const QString &avatar_url) { - roomAvatar_ = utils::scaleImageToPixmap(img, IconSize); - update(); + AvatarProvider::resolve(avatar_url, IconSize, this, [this](const QPixmap &img) { + roomAvatar_ = img; + update(); + }); } void diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h index 40c938c1..54e02a76 100644 --- a/src/RoomInfoListItem.h +++ b/src/RoomInfoListItem.h @@ -73,7 +73,7 @@ public: bool isPressed() const { return isPressed_; } int unreadMessageCount() const { return unreadMsgCount_; } - void setAvatar(const QImage &avatar_image); + void setAvatar(const QString &avatar_url); void setDescriptionMessage(const DescInfo &info); DescInfo lastMessageInfo() const { return lastMsgInfo_; } diff --git a/src/RoomList.cpp b/src/RoomList.cpp index 1abf3533..c5e05621 100644 --- a/src/RoomList.cpp +++ b/src/RoomList.cpp @@ -89,40 +89,7 @@ RoomList::updateAvatar(const QString &room_id, const QString &url) if (url.isEmpty()) return; - QByteArray savedImgData; - - if (cache::client()) - savedImgData = cache::client()->image(url); - - if (savedImgData.isEmpty()) { - mtx::http::ThumbOpts opts; - opts.mxc_url = url.toStdString(); - http::client()->get_thumbnail( - opts, [room_id, opts, this](const std::string &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to download room avatar: {} {} {}", - opts.mxc_url, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - if (cache::client()) - cache::client()->saveImage(opts.mxc_url, res); - - auto data = QByteArray(res.data(), res.size()); - QPixmap pixmap; - pixmap.loadFromData(data); - - emit updateRoomAvatarCb(room_id, pixmap); - }); - } else { - QPixmap img; - img.loadFromData(savedImgData); - - updateRoomAvatar(room_id, img); - } + emit updateRoomAvatarCb(room_id, url); } void @@ -252,7 +219,7 @@ RoomList::highlightSelectedRoom(const QString &room_id) } void -RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img) +RoomList::updateRoomAvatar(const QString &roomid, const QString &img) { if (!roomExists(roomid)) { nhlog::ui()->warn("avatar update on non-existent room_id: {}", @@ -260,7 +227,7 @@ RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img) return; } - rooms_[roomid]->setAvatar(img.toImage()); + rooms_[roomid]->setAvatar(img); // Used to inform other widgets for the new image data. emit roomAvatarChanged(roomid, img); diff --git a/src/RoomList.h b/src/RoomList.h index 155a969c..95fc0d9b 100644 --- a/src/RoomList.h +++ b/src/RoomList.h @@ -61,12 +61,12 @@ signals: void totalUnreadMessageCountUpdated(int count); void acceptInvite(const QString &room_id); void declineInvite(const QString &room_id); - void roomAvatarChanged(const QString &room_id, const QPixmap &img); + void roomAvatarChanged(const QString &room_id, const QString &img); void joinRoom(const QString &room_id); - void updateRoomAvatarCb(const QString &room_id, const QPixmap &img); + void updateRoomAvatarCb(const QString &room_id, const QString &img); public slots: - void updateRoomAvatar(const QString &roomid, const QPixmap &img); + void updateRoomAvatar(const QString &roomid, const QString &img); void highlightSelectedRoom(const QString &room_id); void updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount); void updateRoomDescription(const QString &roomid, const DescInfo &info); diff --git a/src/TopRoomBar.cpp b/src/TopRoomBar.cpp index a8049e3a..712fe9aa 100644 --- a/src/TopRoomBar.cpp +++ b/src/TopRoomBar.cpp @@ -46,9 +46,8 @@ TopRoomBar::TopRoomBar(QWidget *parent) topLayout_->setContentsMargins( 2 * widgetMargin, widgetMargin, 2 * widgetMargin, widgetMargin); - avatar_ = new Avatar(this); + avatar_ = new Avatar(this, fontHeight * 2); avatar_->setLetter(""); - avatar_->setSize(fontHeight * 2); textLayout_ = new QVBoxLayout(); textLayout_->setSpacing(0); @@ -183,7 +182,7 @@ TopRoomBar::reset() } void -TopRoomBar::updateRoomAvatar(const QImage &avatar_image) +TopRoomBar::updateRoomAvatar(const QString &avatar_image) { avatar_->setImage(avatar_image); update(); diff --git a/src/TopRoomBar.h b/src/TopRoomBar.h index 5f2c936e..3243064e 100644 --- a/src/TopRoomBar.h +++ b/src/TopRoomBar.h @@ -44,7 +44,7 @@ class TopRoomBar : public QWidget public: TopRoomBar(QWidget *parent = 0); - void updateRoomAvatar(const QImage &avatar_image); + void updateRoomAvatar(const QString &avatar_image); void updateRoomAvatar(const QIcon &icon); void updateRoomName(const QString &name); void updateRoomTopic(QString topic); diff --git a/src/UserInfoWidget.cpp b/src/UserInfoWidget.cpp index 5345fb2a..19d7478e 100644 --- a/src/UserInfoWidget.cpp +++ b/src/UserInfoWidget.cpp @@ -52,10 +52,9 @@ UserInfoWidget::UserInfoWidget(QWidget *parent) textLayout_->setSpacing(widgetMargin / 2); textLayout_->setContentsMargins(widgetMargin * 2, widgetMargin, widgetMargin, widgetMargin); - userAvatar_ = new Avatar(this); + userAvatar_ = new Avatar(this, fontHeight * 2.5); userAvatar_->setObjectName("userAvatar"); userAvatar_->setLetter(QChar('?')); - userAvatar_->setSize(fontHeight * 2.5); QFont nameFont; nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); @@ -134,14 +133,6 @@ UserInfoWidget::reset() userAvatar_->setLetter(QChar('?')); } -void -UserInfoWidget::setAvatar(const QImage &img) -{ - avatar_image_ = img; - userAvatar_->setImage(img); - update(); -} - void UserInfoWidget::setDisplayName(const QString &name) { @@ -160,6 +151,14 @@ UserInfoWidget::setUserId(const QString &userid) { user_id_ = userid; userIdLabel_->setText(userid); + update(); +} + +void +UserInfoWidget::setAvatar(const QString &url) +{ + userAvatar_->setImage(url); + update(); } void diff --git a/src/UserInfoWidget.h b/src/UserInfoWidget.h index 65de7be9..263dd0c2 100644 --- a/src/UserInfoWidget.h +++ b/src/UserInfoWidget.h @@ -33,9 +33,9 @@ class UserInfoWidget : public QWidget public: UserInfoWidget(QWidget *parent = 0); - void setAvatar(const QImage &img); void setDisplayName(const QString &name); void setUserId(const QString &userid); + void setAvatar(const QString &url); void reset(); diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp index 88a95403..9e973efa 100644 --- a/src/dialogs/MemberList.cpp +++ b/src/dialogs/MemberList.cpp @@ -9,7 +9,6 @@ #include "dialogs/MemberList.h" -#include "AvatarProvider.h" #include "Cache.h" #include "ChatPage.h" #include "Config.h" @@ -28,17 +27,10 @@ MemberItem::MemberItem(const RoomMember &member, QWidget *parent) textLayout_->setMargin(0); textLayout_->setSpacing(0); - avatar_ = new Avatar(this); - avatar_->setSize(44); + avatar_ = new Avatar(this, 44); avatar_->setLetter(utils::firstChar(member.display_name)); - if (!member.avatar.isNull()) - avatar_->setImage(member.avatar); - else - AvatarProvider::resolve(ChatPage::instance()->currentRoom(), - member.user_id, - this, - [this](const QImage &img) { avatar_->setImage(img); }); + avatar_->setImage(ChatPage::instance()->currentRoom(), member.user_id); QFont nameFont; nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); diff --git a/src/dialogs/ReadReceipts.cpp b/src/dialogs/ReadReceipts.cpp index 5a0d98c7..58ad59c3 100644 --- a/src/dialogs/ReadReceipts.cpp +++ b/src/dialogs/ReadReceipts.cpp @@ -37,8 +37,7 @@ ReceiptItem::ReceiptItem(QWidget *parent, auto displayName = Cache::displayName(room_id, user_id); - avatar_ = new Avatar(this); - avatar_->setSize(44); + avatar_ = new Avatar(this, 44); avatar_->setLetter(utils::firstChar(displayName)); // If it's a matrix id we use the second letter. @@ -56,10 +55,7 @@ ReceiptItem::ReceiptItem(QWidget *parent, topLayout_->addWidget(avatar_); topLayout_->addLayout(textLayout_, 1); - AvatarProvider::resolve(ChatPage::instance()->currentRoom(), - user_id, - this, - [this](const QImage &img) { avatar_->setImage(img); }); + avatar_->setImage(ChatPage::instance()->currentRoom(), user_id); } void diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index 1fe5904b..00b034cc 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -350,12 +350,12 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) keyRequestsToggle_->hide(); } - avatar_ = new Avatar(this); - avatar_->setSize(128); + avatar_ = new Avatar(this, 128); if (avatarImg_.isNull()) avatar_->setLetter(utils::firstChar(QString::fromStdString(info_.name))); else - avatar_->setImage(avatarImg_); + avatar_->setImage(room_id_, + QString::fromStdString(http::client()->user_id().to_string())); if (canChangeAvatar(room_id_.toStdString(), utils::localUser().toStdString())) { auto filter = new ClickableFilter(this); @@ -487,7 +487,7 @@ RoomSettings::retrieveRoomInfo() try { usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString()); info_ = cache::client()->singleRoomInfo(room_id_.toStdString()); - setAvatar(QImage::fromData(cache::client()->image(info_.avatar_url))); + setAvatar(); } catch (const lmdb::error &e) { nhlog::db()->warn("failed to retrieve room info from cache: {}", room_id_.toStdString()); @@ -633,14 +633,13 @@ RoomSettings::displayErrorMessage(const QString &msg) } void -RoomSettings::setAvatar(const QImage &img) +RoomSettings::setAvatar() { stopLoadingSpinner(); - avatarImg_ = img; - if (avatar_) - avatar_->setImage(img); + avatar_->setImage(room_id_, + QString::fromStdString(http::client()->user_id().to_string())); } void @@ -733,7 +732,7 @@ RoomSettings::updateAvatar() return; } - emit proxy->avatarChanged(QImage::fromData(content)); + emit proxy->avatarChanged(); }); }); } diff --git a/src/dialogs/RoomSettings.h b/src/dialogs/RoomSettings.h index 6667b68b..e1807ba1 100644 --- a/src/dialogs/RoomSettings.h +++ b/src/dialogs/RoomSettings.h @@ -52,7 +52,7 @@ class ThreadProxy : public QObject signals: void error(const QString &msg); - void avatarChanged(const QImage &img); + void avatarChanged(); void nameEventSent(const QString &); void topicEventSent(); }; @@ -140,7 +140,7 @@ private: void resetErrorLabel(); void displayErrorMessage(const QString &msg); - void setAvatar(const QImage &img); + void setAvatar(); void setupEditButton(); //! Retrieve the current room information from cache. void retrieveRoomInfo(); diff --git a/src/dialogs/UserProfile.cpp b/src/dialogs/UserProfile.cpp index 6aea96a8..5ad3afa2 100644 --- a/src/dialogs/UserProfile.cpp +++ b/src/dialogs/UserProfile.cpp @@ -114,9 +114,8 @@ UserProfile::UserProfile(QWidget *parent) btnLayout->setSpacing(8); btnLayout->setMargin(0); - avatar_ = new Avatar(this); + avatar_ = new Avatar(this, 128); avatar_->setLetter("X"); - avatar_->setSize(128); QFont font; font.setPointSizeF(font.pointSizeF() * 2); @@ -210,8 +209,7 @@ UserProfile::init(const QString &userId, const QString &roomId) displayNameLabel_->setText(displayName); avatar_->setLetter(utils::firstChar(displayName)); - AvatarProvider::resolve( - roomId, userId, this, [this](const QImage &img) { avatar_->setImage(img); }); + avatar_->setImage(roomId, userId); auto localUser = utils::localUser(); diff --git a/src/popups/PopupItem.cpp b/src/popups/PopupItem.cpp index f905983a..c4d4327f 100644 --- a/src/popups/PopupItem.cpp +++ b/src/popups/PopupItem.cpp @@ -11,7 +11,7 @@ constexpr int PopupItemMargin = 3; PopupItem::PopupItem(QWidget *parent) : QWidget(parent) - , avatar_{new Avatar(this)} + , avatar_{new Avatar(this, conf::popup::avatar)} , hovering_{false} { setMouseTracking(true); @@ -40,7 +40,6 @@ UserItem::UserItem(QWidget *parent) : PopupItem(parent) { userName_ = new QLabel("Placeholder", this); - avatar_->setSize(conf::popup::avatar); avatar_->setLetter("P"); topLayout_->addWidget(avatar_); topLayout_->addWidget(userName_, 1); @@ -52,7 +51,6 @@ UserItem::UserItem(QWidget *parent, const QString &user_id) { auto displayName = Cache::displayName(ChatPage::instance()->currentRoom(), userId_); - avatar_->setSize(conf::popup::avatar); avatar_->setLetter(utils::firstChar(displayName)); // If it's a matrix id we use the second letter. @@ -87,16 +85,7 @@ UserItem::updateItem(const QString &user_id) void UserItem::resolveAvatar(const QString &user_id) { - AvatarProvider::resolve( - ChatPage::instance()->currentRoom(), userId_, this, [this, user_id](const QImage &img) { - // The user on the widget when the avatar is resolved, - // might be different from the user that made the call. - if (user_id == userId_) - avatar_->setImage(img); - else - // We try to resolve the avatar again. - resolveAvatar(userId_); - }); + avatar_->setImage(ChatPage::instance()->currentRoom(), user_id); } void @@ -116,7 +105,6 @@ RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res) auto name = QFontMetrics(QFont()).elidedText( QString::fromStdString(res.info.name), Qt::ElideRight, parentWidget()->width() - 10); - avatar_->setSize(conf::popup::avatar + 6); avatar_->setLetter(utils::firstChar(name)); roomName_ = new QLabel(name, this); @@ -125,8 +113,7 @@ RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res) topLayout_->addWidget(avatar_); topLayout_->addWidget(roomName_, 1); - if (!res.img.isNull()) - avatar_->setImage(res.img); + avatar_->setImage(QString::fromStdString(res.info.avatar_url)); } void @@ -141,10 +128,7 @@ RoomItem::updateItem(const RoomSearchResult &result) roomName_->setText(name); - if (!result.img.isNull()) - avatar_->setImage(result.img); - else - avatar_->setLetter(utils::firstChar(name)); + avatar_->setImage(QString::fromStdString(result.info.avatar_url)); } void @@ -154,4 +138,4 @@ RoomItem::mousePressEvent(QMouseEvent *event) emit clicked(selectedText()); QWidget::mousePressEvent(event); -} \ No newline at end of file +} diff --git a/src/timeline/.TimelineItem.cpp.swp b/src/timeline/.TimelineItem.cpp.swp new file mode 100644 index 00000000..75e03aeb Binary files /dev/null and b/src/timeline/.TimelineItem.cpp.swp differ diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp index dd3b48c3..7916bd80 100644 --- a/src/timeline/TimelineItem.cpp +++ b/src/timeline/TimelineItem.cpp @@ -326,8 +326,7 @@ TimelineItem::TimelineItem(mtx::events::MessageType ty, generateBody(userid, displayName, formatted_body); setupAvatarLayout(displayName); - AvatarProvider::resolve( - room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); + setUserAvatar(userid); } else { generateBody(formatted_body); setupSimpleLayout(); @@ -509,8 +508,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent generateBody(sender, displayName, formatted_body); setupAvatarLayout(displayName); - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); + setUserAvatar(sender); } else { generateBody(formatted_body); setupSimpleLayout(); @@ -607,8 +604,7 @@ TimelineItem::TimelineItem(const mtx::events::RoomEvent generateBody(sender, displayName, formatted_body); setupAvatarLayout(displayName); - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); + setUserAvatar(sender); } else { generateBody(formatted_body); setupSimpleLayout(); @@ -793,9 +789,8 @@ TimelineItem::setupAvatarLayout(const QString &userName) QFont f; f.setPointSizeF(f.pointSizeF()); - userAvatar_ = new Avatar(this); + userAvatar_ = new Avatar(this, QFontMetrics(f).height() * 2); userAvatar_->setLetter(QChar(userName[0]).toUpper()); - userAvatar_->setSize(QFontMetrics(f).height() * 2); // TODO: The provided user name should be a UserId class if (userName[0] == '@' && userName.size() > 1) @@ -822,12 +817,12 @@ TimelineItem::setupSimpleLayout() } void -TimelineItem::setUserAvatar(const QImage &avatar) +TimelineItem::setUserAvatar(const QString &userid) { if (userAvatar_ == nullptr) return; - userAvatar_->setImage(avatar); + userAvatar_->setImage(room_id_, userid); } void @@ -911,8 +906,7 @@ TimelineItem::addAvatar() setupAvatarLayout(displayName); - AvatarProvider::resolve( - room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); + setUserAvatar(userid); } void diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h index fe354000..356976e5 100644 --- a/src/timeline/TimelineItem.h +++ b/src/timeline/TimelineItem.h @@ -215,7 +215,7 @@ public: void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } QColor backgroundColor() const { return backgroundColor_; } - void setUserAvatar(const QImage &pixmap); + void setUserAvatar(const QString &userid); DescInfo descriptionMessage() const { return descriptionMsg_; } QString eventId() const { return event_id_; } void setEventId(const QString &event_id) { event_id_ = event_id; } @@ -336,8 +336,7 @@ TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool generateBody(userid, displayName, ""); setupAvatarLayout(displayName); - AvatarProvider::resolve( - room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); + setUserAvatar(userid); } else { setupSimpleLayout(); } @@ -381,8 +380,7 @@ TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSen generateBody(sender, displayName, ""); setupAvatarLayout(displayName); - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); + setUserAvatar(sender); } else { setupSimpleLayout(); } diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp index 4b4cd272..98bf21c6 100644 --- a/src/ui/Avatar.cpp +++ b/src/ui/Avatar.cpp @@ -1,12 +1,13 @@ #include +#include "AvatarProvider.h" #include "Utils.h" #include "ui/Avatar.h" -Avatar::Avatar(QWidget *parent) +Avatar::Avatar(QWidget *parent, int size) : QWidget(parent) + , size_(size) { - size_ = ui::AvatarSize; type_ = ui::AvatarType::Letter; letter_ = "A"; @@ -61,35 +62,31 @@ Avatar::setBackgroundColor(const QColor &color) } void -Avatar::setSize(int size) +Avatar::setLetter(const QString &letter) { - size_ = size; - - if (!image_.isNull()) - pixmap_ = utils::scaleImageToPixmap(image_, size_); - - QFont _font(font()); - _font.setPointSizeF(size_ * (ui::FontSize) / 40); - - setFont(_font); + letter_ = letter; + type_ = ui::AvatarType::Letter; update(); } void -Avatar::setLetter(const QString &letter) +Avatar::setImage(const QString &avatar_url) { - letter_ = letter; - type_ = ui::AvatarType::Letter; - update(); + AvatarProvider::resolve(avatar_url, size_, this, [this](QPixmap pm) { + type_ = ui::AvatarType::Image; + pixmap_ = pm; + update(); + }); } void -Avatar::setImage(const QImage &image) +Avatar::setImage(const QString &room, const QString &user) { - image_ = image; - type_ = ui::AvatarType::Image; - pixmap_ = utils::scaleImageToPixmap(image_, size_); - update(); + AvatarProvider::resolve(room, user, size_, this, [this](QPixmap pm) { + type_ = ui::AvatarType::Image; + pixmap_ = pm; + update(); + }); } void diff --git a/src/ui/Avatar.h b/src/ui/Avatar.h index 41967af5..b9225dd1 100644 --- a/src/ui/Avatar.h +++ b/src/ui/Avatar.h @@ -15,13 +15,14 @@ class Avatar : public QWidget Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) public: - explicit Avatar(QWidget *parent = 0); + explicit Avatar(QWidget *parent = 0, int size = ui::AvatarSize); void setBackgroundColor(const QColor &color); void setIcon(const QIcon &icon); - void setImage(const QImage &image); + void setImage(const QString &avatar_url); + void setImage(const QString &room, const QString &user); void setLetter(const QString &letter); - void setSize(int size); + // void setSize(int size); void setTextColor(const QColor &color); QColor backgroundColor() const; @@ -41,7 +42,8 @@ private: QColor background_color_; QColor text_color_; QIcon icon_; - QImage image_; QPixmap pixmap_; + const std::string room; + const std::string user; int size_; }; -- cgit 1.5.1 From 91d1f19058a31cc35ca1212f042a9dd6f501a7b7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 9 Nov 2019 03:06:10 +0100 Subject: Remove old timeline --- CMakeLists.txt | 26 +- src/ChatPage.cpp | 2 +- src/Utils.h | 14 +- src/dialogs/MemberList.cpp | 1 + src/timeline/DelegateChooser.cpp | 138 +++ src/timeline/DelegateChooser.h | 82 ++ src/timeline/TimelineItem.cpp | 960 ------------------- src/timeline/TimelineItem.h | 389 -------- src/timeline/TimelineModel.cpp | 1220 ++++++++++++++++++++++++ src/timeline/TimelineModel.h | 258 ++++++ src/timeline/TimelineView.cpp | 1627 --------------------------------- src/timeline/TimelineView.h | 449 --------- src/timeline/TimelineViewManager.cpp | 564 +++++++----- src/timeline/TimelineViewManager.h | 111 ++- src/timeline/widgets/AudioItem.cpp | 236 ----- src/timeline/widgets/AudioItem.h | 104 --- src/timeline/widgets/FileItem.cpp | 221 ----- src/timeline/widgets/FileItem.h | 79 -- src/timeline/widgets/ImageItem.cpp | 267 ------ src/timeline/widgets/ImageItem.h | 104 --- src/timeline/widgets/VideoItem.cpp | 65 -- src/timeline/widgets/VideoItem.h | 51 -- src/timeline2/DelegateChooser.cpp | 138 --- src/timeline2/DelegateChooser.h | 82 -- src/timeline2/TimelineModel.cpp | 1220 ------------------------ src/timeline2/TimelineModel.h | 258 ------ src/timeline2/TimelineViewManager.cpp | 400 -------- src/timeline2/TimelineViewManager.h | 117 --- 28 files changed, 2088 insertions(+), 7095 deletions(-) create mode 100644 src/timeline/DelegateChooser.cpp create mode 100644 src/timeline/DelegateChooser.h delete mode 100644 src/timeline/TimelineItem.cpp delete mode 100644 src/timeline/TimelineItem.h create mode 100644 src/timeline/TimelineModel.cpp create mode 100644 src/timeline/TimelineModel.h delete mode 100644 src/timeline/TimelineView.cpp delete mode 100644 src/timeline/TimelineView.h delete mode 100644 src/timeline/widgets/AudioItem.cpp delete mode 100644 src/timeline/widgets/AudioItem.h delete mode 100644 src/timeline/widgets/FileItem.cpp delete mode 100644 src/timeline/widgets/FileItem.h delete mode 100644 src/timeline/widgets/ImageItem.cpp delete mode 100644 src/timeline/widgets/ImageItem.h delete mode 100644 src/timeline/widgets/VideoItem.cpp delete mode 100644 src/timeline/widgets/VideoItem.h delete mode 100644 src/timeline2/DelegateChooser.cpp delete mode 100644 src/timeline2/DelegateChooser.h delete mode 100644 src/timeline2/TimelineModel.cpp delete mode 100644 src/timeline2/TimelineModel.h delete mode 100644 src/timeline2/TimelineViewManager.cpp delete mode 100644 src/timeline2/TimelineViewManager.h (limited to 'src/timeline/TimelineItem.cpp') diff --git a/CMakeLists.txt b/CMakeLists.txt index a7cddc50..e07df88d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -192,16 +192,9 @@ set(SRC_FILES src/emoji/Provider.cpp # Timeline - src/timeline2/TimelineViewManager.cpp - src/timeline2/TimelineModel.cpp - src/timeline2/DelegateChooser.cpp - #src/timeline/TimelineViewManager.cpp - #src/timeline/TimelineItem.cpp - #src/timeline/TimelineView.cpp - #src/timeline/widgets/AudioItem.cpp - #src/timeline/widgets/FileItem.cpp - #src/timeline/widgets/ImageItem.cpp - #src/timeline/widgets/VideoItem.cpp + src/timeline/TimelineViewManager.cpp + src/timeline/TimelineModel.cpp + src/timeline/DelegateChooser.cpp # UI components src/ui/Avatar.cpp @@ -339,16 +332,9 @@ qt5_wrap_cpp(MOC_HEADERS src/emoji/PickButton.h # Timeline - src/timeline2/TimelineViewManager.h - src/timeline2/TimelineModel.h - src/timeline2/DelegateChooser.h - #src/timeline/TimelineItem.h - #src/timeline/TimelineView.h - #src/timeline/TimelineViewManager.h - #src/timeline/widgets/AudioItem.h - #src/timeline/widgets/FileItem.h - #src/timeline/widgets/ImageItem.h - #src/timeline/widgets/VideoItem.h + src/timeline/TimelineViewManager.h + src/timeline/TimelineModel.h + src/timeline/DelegateChooser.h # UI components src/ui/Avatar.h diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index b8f312ac..091a9fa0 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -44,7 +44,7 @@ #include "dialogs/ReadReceipts.h" #include "popups/UserMentions.h" -#include "timeline2/TimelineViewManager.h" +#include "timeline/TimelineViewManager.h" // TODO: Needs to be updated with an actual secret. static const std::string STORAGE_SECRET_KEY("secret"); diff --git a/src/Utils.h b/src/Utils.h index 007126c3..bdb51844 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -4,10 +4,6 @@ #include "Cache.h" #include "RoomInfoListItem.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" #include #include @@ -94,7 +90,7 @@ messageDescription(const QString &username = "", using Video = mtx::events::RoomEvent; using Encrypted = mtx::events::EncryptedEvent; - if (std::is_same::value || std::is_same::value) { + if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent an audio clip"); @@ -102,7 +98,7 @@ messageDescription(const QString &username = "", return QCoreApplication::translate("message-description sent:", "%1 sent an audio clip") .arg(username); - } else if (std::is_same::value || std::is_same::value) { + } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent an image"); @@ -110,7 +106,7 @@ messageDescription(const QString &username = "", return QCoreApplication::translate("message-description sent:", "%1 sent an image") .arg(username); - } else if (std::is_same::value || std::is_same::value) { + } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a file"); @@ -118,7 +114,7 @@ messageDescription(const QString &username = "", return QCoreApplication::translate("message-description sent:", "%1 sent a file") .arg(username); - } else if (std::is_same::value || std::is_same::value) { + } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a video"); @@ -126,7 +122,7 @@ messageDescription(const QString &username = "", return QCoreApplication::translate("message-description sent:", "%1 sent a video") .arg(username); - } else if (std::is_same::value || std::is_same::value) { + } else if (std::is_same::value) { if (isLocal) return QCoreApplication::translate("message-description sent:", "You sent a sticker"); diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp index 9e973efa..f62cf9fe 100644 --- a/src/dialogs/MemberList.cpp +++ b/src/dialogs/MemberList.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp new file mode 100644 index 00000000..632a2a64 --- /dev/null +++ b/src/timeline/DelegateChooser.cpp @@ -0,0 +1,138 @@ +#include "DelegateChooser.h" + +#include "Logging.h" + +// uses private API, which moved between versions +#include +#include + +QQmlComponent * +DelegateChoice::delegate() const +{ + return delegate_; +} + +void +DelegateChoice::setDelegate(QQmlComponent *delegate) +{ + if (delegate != delegate_) { + delegate_ = delegate; + emit delegateChanged(); + emit changed(); + } +} + +QVariant +DelegateChoice::roleValue() const +{ + return roleValue_; +} + +void +DelegateChoice::setRoleValue(const QVariant &value) +{ + if (value != roleValue_) { + roleValue_ = value; + emit roleValueChanged(); + emit changed(); + } +} + +QVariant +DelegateChooser::roleValue() const +{ + return roleValue_; +} + +void +DelegateChooser::setRoleValue(const QVariant &value) +{ + if (value != roleValue_) { + roleValue_ = value; + recalcChild(); + emit roleValueChanged(); + } +} + +QQmlListProperty +DelegateChooser::choices() +{ + return QQmlListProperty(this, + this, + &DelegateChooser::appendChoice, + &DelegateChooser::choiceCount, + &DelegateChooser::choice, + &DelegateChooser::clearChoices); +} + +void +DelegateChooser::appendChoice(QQmlListProperty *p, DelegateChoice *c) +{ + DelegateChooser *dc = static_cast(p->object); + dc->choices_.append(c); +} + +int +DelegateChooser::choiceCount(QQmlListProperty *p) +{ + return static_cast(p->object)->choices_.count(); +} +DelegateChoice * +DelegateChooser::choice(QQmlListProperty *p, int index) +{ + return static_cast(p->object)->choices_.at(index); +} +void +DelegateChooser::clearChoices(QQmlListProperty *p) +{ + static_cast(p->object)->choices_.clear(); +} + +void +DelegateChooser::recalcChild() +{ + for (const auto choice : choices_) { + auto choiceValue = choice->roleValue(); + if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) { + if (child) { + child->setParentItem(nullptr); + child = nullptr; + } + + choice->delegate()->create(incubator, QQmlEngine::contextForObject(this)); + return; + } + } +} + +void +DelegateChooser::componentComplete() +{ + QQuickItem::componentComplete(); + recalcChild(); +} + +void +DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) +{ + if (status == QQmlIncubator::Ready) { + chooser.child = dynamic_cast(object()); + if (chooser.child == nullptr) { + nhlog::ui()->error("Delegate has to be derived of Item!"); + return; + } + + chooser.child->setParentItem(&chooser); + connect(chooser.child, &QQuickItem::heightChanged, &chooser, [this]() { + chooser.setHeight(chooser.child->height()); + }); + chooser.setHeight(chooser.child->height()); + QQmlEngine::setObjectOwnership(chooser.child, + QQmlEngine::ObjectOwnership::JavaScriptOwnership); + + } else if (status == QQmlIncubator::Error) { + for (const auto &e : errors()) + nhlog::ui()->error("Error instantiating delegate: {}", + e.toString().toStdString()); + } +} diff --git a/src/timeline/DelegateChooser.h b/src/timeline/DelegateChooser.h new file mode 100644 index 00000000..68ebeb04 --- /dev/null +++ b/src/timeline/DelegateChooser.h @@ -0,0 +1,82 @@ +// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt +// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent + +#pragma once + +#include +#include +#include +#include +#include +#include + +class QQmlAdaptorModel; + +class DelegateChoice : public QObject +{ + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "delegate") + +public: + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) + + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); + + QVariant roleValue() const; + void setRoleValue(const QVariant &value); + +signals: + void delegateChanged(); + void roleValueChanged(); + void changed(); + +private: + QVariant roleValue_; + QQmlComponent *delegate_ = nullptr; +}; + +class DelegateChooser : public QQuickItem +{ + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "choices") + +public: + Q_PROPERTY(QQmlListProperty choices READ choices CONSTANT) + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + + QQmlListProperty choices(); + + QVariant roleValue() const; + void setRoleValue(const QVariant &value); + + void recalcChild(); + void componentComplete() override; + +signals: + void roleChanged(); + void roleValueChanged(); + +private: + struct DelegateIncubator : public QQmlIncubator + { + DelegateIncubator(DelegateChooser &parent) + : QQmlIncubator(QQmlIncubator::AsynchronousIfNested) + , chooser(parent) + {} + void statusChanged(QQmlIncubator::Status status) override; + + DelegateChooser &chooser; + }; + + QVariant roleValue_; + QList choices_; + QQuickItem *child = nullptr; + DelegateIncubator incubator{*this}; + + static void appendChoice(QQmlListProperty *, DelegateChoice *); + static int choiceCount(QQmlListProperty *); + static DelegateChoice *choice(QQmlListProperty *, int index); + static void clearChoices(QQmlListProperty *); +}; diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp deleted file mode 100644 index 7916bd80..00000000 --- a/src/timeline/TimelineItem.cpp +++ /dev/null @@ -1,960 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#include - -#include -#include -#include -#include -#include -#include - -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "MainWindow.h" -#include "Olm.h" -#include "ui/Avatar.h" -#include "ui/Painter.h" -#include "ui/TextLabel.h" - -#include "timeline/TimelineItem.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -#include "dialogs/RawMessage.h" -#include "mtx/identifiers.hpp" - -constexpr int MSG_RIGHT_MARGIN = 7; -constexpr int MSG_PADDING = 20; - -StatusIndicator::StatusIndicator(QWidget *parent) - : QWidget(parent) -{ - lockIcon_.addFile(":/icons/icons/ui/lock.png"); - clockIcon_.addFile(":/icons/icons/ui/clock.png"); - checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png"); - doubleCheckmarkIcon_.addFile(":/icons/icons/ui/double-tick-indicator.png"); -} - -void -StatusIndicator::paintIcon(QPainter &p, QIcon &icon) -{ - auto pixmap = icon.pixmap(width()); - - QPainter painter(&pixmap); - painter.setCompositionMode(QPainter::CompositionMode_SourceIn); - painter.fillRect(pixmap.rect(), p.pen().color()); - - QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal); -} - -void -StatusIndicator::paintEvent(QPaintEvent *) -{ - if (state_ == StatusIndicatorState::Empty) - return; - - Painter p(this); - PainterHighQualityEnabler hq(p); - - p.setPen(iconColor_); - - switch (state_) { - case StatusIndicatorState::Sent: { - paintIcon(p, clockIcon_); - break; - } - case StatusIndicatorState::Encrypted: - paintIcon(p, lockIcon_); - break; - case StatusIndicatorState::Received: { - paintIcon(p, checkmarkIcon_); - break; - } - case StatusIndicatorState::Read: { - paintIcon(p, doubleCheckmarkIcon_); - break; - } - case StatusIndicatorState::Empty: - break; - } -} - -void -StatusIndicator::setState(StatusIndicatorState state) -{ - state_ = state; - - switch (state) { - case StatusIndicatorState::Encrypted: - setToolTip(tr("Encrypted")); - break; - case StatusIndicatorState::Received: - setToolTip(tr("Delivered")); - break; - case StatusIndicatorState::Read: - setToolTip(tr("Seen")); - break; - case StatusIndicatorState::Sent: - setToolTip(tr("Sent")); - break; - case StatusIndicatorState::Empty: - setToolTip(""); - break; - } - - update(); -} - -void -TimelineItem::adjustMessageLayoutForWidget() -{ - messageLayout_->addLayout(widgetLayout_, 1); - actionLayout_->addWidget(replyBtn_); - actionLayout_->addWidget(contextBtn_); - messageLayout_->addLayout(actionLayout_); - messageLayout_->addWidget(statusIndicator_); - messageLayout_->addWidget(timestamp_); - - actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight); - actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight); - messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); - messageLayout_->setAlignment(timestamp_, Qt::AlignTop); - messageLayout_->setAlignment(actionLayout_, Qt::AlignTop); - - mainLayout_->addLayout(messageLayout_); -} - -void -TimelineItem::adjustMessageLayout() -{ - messageLayout_->addWidget(body_, 1); - actionLayout_->addWidget(replyBtn_); - actionLayout_->addWidget(contextBtn_); - messageLayout_->addLayout(actionLayout_); - messageLayout_->addWidget(statusIndicator_); - messageLayout_->addWidget(timestamp_); - - actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight); - actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight); - messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); - messageLayout_->setAlignment(timestamp_, Qt::AlignTop); - messageLayout_->setAlignment(actionLayout_, Qt::AlignTop); - - mainLayout_->addLayout(messageLayout_); -} - -void -TimelineItem::init() -{ - userAvatar_ = nullptr; - timestamp_ = nullptr; - userName_ = nullptr; - body_ = nullptr; - auto buttonSize_ = 32; - - contextMenu_ = new QMenu(this); - showReadReceipts_ = new QAction("Read receipts", this); - markAsRead_ = new QAction("Mark as read", this); - viewRawMessage_ = new QAction("View raw message", this); - redactMsg_ = new QAction("Redact message", this); - contextMenu_->addAction(showReadReceipts_); - contextMenu_->addAction(viewRawMessage_); - contextMenu_->addAction(markAsRead_); - contextMenu_->addAction(redactMsg_); - - connect(showReadReceipts_, &QAction::triggered, this, [this]() { - if (!event_id_.isEmpty()) - MainWindow::instance()->openReadReceiptsDialog(event_id_); - }); - - connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) { - emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id); - }); - connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) { - emit ChatPage::instance()->showNotification(msg); - }); - connect(redactMsg_, &QAction::triggered, this, [this]() { - if (!event_id_.isEmpty()) - http::client()->redact_event( - room_id_.toStdString(), - event_id_.toStdString(), - [this](const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - emit redactionFailed(tr("Message redaction failed: %1") - .arg(QString::fromStdString( - err->matrix_error.error))); - return; - } - - emit eventRedacted(event_id_); - }); - }); - connect( - ChatPage::instance(), &ChatPage::themeChanged, this, &TimelineItem::refreshAuthorColor); - connect(markAsRead_, &QAction::triggered, this, &TimelineItem::sendReadReceipt); - connect(viewRawMessage_, &QAction::triggered, this, &TimelineItem::openRawMessageViewer); - - colorGenerating_ = new QFutureWatcher(this); - connect(colorGenerating_, - &QFutureWatcher::finished, - this, - &TimelineItem::finishedGeneratingColor); - - topLayout_ = new QHBoxLayout(this); - mainLayout_ = new QVBoxLayout; - messageLayout_ = new QHBoxLayout; - actionLayout_ = new QHBoxLayout; - messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0); - messageLayout_->setSpacing(MSG_PADDING); - - actionLayout_->setContentsMargins(13, 1, 13, 0); - actionLayout_->setSpacing(0); - - topLayout_->setContentsMargins( - conf::timeline::msgLeftMargin, conf::timeline::msgTopMargin, 0, 0); - topLayout_->setSpacing(0); - topLayout_->addLayout(mainLayout_); - - mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0); - mainLayout_->setSpacing(0); - - replyBtn_ = new FlatButton(this); - replyBtn_->setToolTip(tr("Reply")); - replyBtn_->setFixedSize(buttonSize_, buttonSize_); - replyBtn_->setCornerRadius(buttonSize_ / 2); - - QIcon reply_icon; - reply_icon.addFile(":/icons/icons/ui/mail-reply.png"); - replyBtn_->setIcon(reply_icon); - replyBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); - connect(replyBtn_, &FlatButton::clicked, this, &TimelineItem::replyAction); - - contextBtn_ = new FlatButton(this); - contextBtn_->setToolTip(tr("Options")); - contextBtn_->setFixedSize(buttonSize_, buttonSize_); - contextBtn_->setCornerRadius(buttonSize_ / 2); - - QIcon context_icon; - context_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png"); - contextBtn_->setIcon(context_icon); - contextBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); - contextBtn_->setMenu(contextMenu_); - - timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9); - timestampFont_.setFamily("Monospace"); - timestampFont_.setStyleHint(QFont::Monospace); - - QFontMetrics tsFm(timestampFont_); - - statusIndicator_ = new StatusIndicator(this); - statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading()); - statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading()); - - parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); -} - -/* - * For messages created locally. - */ -TimelineItem::TimelineItem(mtx::events::MessageType ty, - const QString &userid, - QString body, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(ty) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - auto displayName = Cache::displayName(room_id_, userid); - auto timestamp = QDateTime::currentDateTime(); - - // Generate the html body to be rendered. - auto formatted_body = utils::markdownToHtml(body); - - // Escape html if the input is not formatted. - if (formatted_body == body.trimmed().toHtmlEscaped()) - formatted_body = body.toHtmlEscaped(); - - QString emptyEventId; - - if (ty == mtx::events::MessageType::Emote) { - formatted_body = QString("%1").arg(formatted_body); - descriptionMsg_ = {emptyEventId, - "", - userid, - QString("* %1 %2").arg(displayName).arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - } else { - descriptionMsg_ = {emptyEventId, - "You: ", - userid, - body, - utils::descriptiveTime(timestamp), - timestamp}; - } - - formatted_body = utils::linkifyMessage(formatted_body); - formatted_body.replace("mx-reply", "div"); - - generateTimestamp(timestamp); - - if (withSender) { - generateBody(userid, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(userid); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -TimelineItem::TimelineItem(ImageItem *image, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::Image) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout(image, userid, withSender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(FileItem *file, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::File) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout(file, userid, withSender); -} - -TimelineItem::TimelineItem(AudioItem *audio, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::Audio) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout(audio, userid, withSender); -} - -TimelineItem::TimelineItem(VideoItem *video, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , message_type_(mtx::events::MessageType::Video) - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout(video, userid, withSender); -} - -TimelineItem::TimelineItem(ImageItem *image, - const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Image) - , room_id_{room_id} -{ - setupWidgetLayout, ImageItem>( - image, event, with_sender); - - markOwnMessagesAsReceived(event.sender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(StickerItem *image, - const mtx::events::Sticker &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout(image, event, with_sender); - - markOwnMessagesAsReceived(event.sender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(FileItem *file, - const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::File) - , room_id_{room_id} -{ - setupWidgetLayout, FileItem>( - file, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -TimelineItem::TimelineItem(AudioItem *audio, - const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Audio) - , room_id_{room_id} -{ - setupWidgetLayout, AudioItem>( - audio, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -TimelineItem::TimelineItem(VideoItem *video, - const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Video) - , room_id_{room_id} -{ - setupWidgetLayout, VideoItem>( - video, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -/* - * Used to display remote notice messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Notice) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - descriptionMsg_ = {event_id_, - Cache::displayName(room_id_, sender), - sender, - " sent a notification", - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - auto displayName = Cache::displayName(room_id_, sender); - - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -/* - * Used to display remote emote messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Emote) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - formatted_body = QString("%1").arg(formatted_body); - - descriptionMsg_ = {event_id_, - "", - sender, - QString("* %1 %2").arg(displayName).arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -/* - * Used to display remote text messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , message_type_(mtx::events::MessageType::Text) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - - QSettings settings; - descriptionMsg_ = {event_id_, - sender == settings.value("auth/user_id") ? "You" : displayName, - sender, - QString(": %1").arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -TimelineItem::~TimelineItem() -{ - colorGenerating_->cancel(); - colorGenerating_->waitForFinished(); -} - -void -TimelineItem::markSent() -{ - statusIndicator_->setState(StatusIndicatorState::Sent); -} - -void -TimelineItem::markOwnMessagesAsReceived(const std::string &sender) -{ - QSettings settings; - if (sender == settings.value("auth/user_id").toString().toStdString()) - statusIndicator_->setState(StatusIndicatorState::Received); -} - -void -TimelineItem::markRead() -{ - if (statusIndicator_->state() != StatusIndicatorState::Encrypted) - statusIndicator_->setState(StatusIndicatorState::Read); -} - -void -TimelineItem::markReceived(bool isEncrypted) -{ - isReceived_ = true; - - if (isEncrypted) - statusIndicator_->setState(StatusIndicatorState::Encrypted); - else - statusIndicator_->setState(StatusIndicatorState::Received); - - sendReadReceipt(); -} - -// Only the body is displayed. -void -TimelineItem::generateBody(const QString &body) -{ - body_ = new TextLabel(utils::replaceEmoji(body), this); - body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - - connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) { - MainWindow::instance()->openUserProfile(user_id, - ChatPage::instance()->currentRoom()); - }); -} - -void -TimelineItem::refreshAuthorColor() -{ - // Cancel and wait if we are already generating the color. - if (colorGenerating_->isRunning()) { - colorGenerating_->cancel(); - colorGenerating_->waitForFinished(); - } - if (userName_) { - // generate user's unique color. - std::function generate = [this]() { - QString userColor = utils::generateContrastingHexColor( - userName_->toolTip(), backgroundColor().name()); - return userColor; - }; - - QString userColor = Cache::userColor(userName_->toolTip()); - - // If the color is empty, then generate it asynchronously - if (userColor.isEmpty()) { - colorGenerating_->setFuture(QtConcurrent::run(generate)); - } else { - userName_->setStyleSheet("QLabel { color : " + userColor + "; }"); - } - } -} - -void -TimelineItem::finishedGeneratingColor() -{ - nhlog::ui()->debug("finishedGeneratingColor for: {}", userName_->toolTip().toStdString()); - QString userColor = colorGenerating_->result(); - - if (!userColor.isEmpty()) { - // another TimelineItem might have inserted in the meantime. - if (Cache::userColor(userName_->toolTip()).isEmpty()) { - Cache::insertUserColor(userName_->toolTip(), userColor); - } - userName_->setStyleSheet("QLabel { color : " + userColor + "; }"); - } -} -// The username/timestamp is displayed along with the message body. -void -TimelineItem::generateBody(const QString &user_id, const QString &displayname, const QString &body) -{ - generateUserName(user_id, displayname); - generateBody(body); -} - -void -TimelineItem::generateUserName(const QString &user_id, const QString &displayname) -{ - auto sender = displayname; - - if (displayname.startsWith("@")) { - // TODO: Fix this by using a UserId type. - if (displayname.split(":")[0].split("@").size() > 1) - sender = displayname.split(":")[0].split("@")[1]; - } - - QFont usernameFont; - usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1); - usernameFont.setWeight(QFont::Medium); - - QFontMetrics fm(usernameFont); - - userName_ = new QLabel(this); - userName_->setFont(usernameFont); - userName_->setText(utils::replaceEmoji(fm.elidedText(sender, Qt::ElideRight, 500))); - userName_->setToolTip(user_id); - userName_->setToolTipDuration(1500); - userName_->setAttribute(Qt::WA_Hover); - userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - // width deprecated in 5.13: - userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text())); -#else - userName_->setFixedWidth( - QFontMetrics(userName_->font()).horizontalAdvance(userName_->text())); -#endif - // Set the user color asynchronously if it hasn't been generated yet, - // otherwise this will just set it. - refreshAuthorColor(); - - auto filter = new UserProfileFilter(user_id, userName_); - userName_->installEventFilter(filter); - userName_->setCursor(Qt::PointingHandCursor); - - connect(filter, &UserProfileFilter::hoverOn, this, [this]() { - QFont f = userName_->font(); - f.setUnderline(true); - userName_->setFont(f); - }); - - connect(filter, &UserProfileFilter::hoverOff, this, [this]() { - QFont f = userName_->font(); - f.setUnderline(false); - userName_->setFont(f); - }); - - connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() { - MainWindow::instance()->openUserProfile(user_id, room_id_); - }); -} - -void -TimelineItem::generateTimestamp(const QDateTime &time) -{ - timestamp_ = new QLabel(this); - timestamp_->setFont(timestampFont_); - timestamp_->setText( - QString(" %1 ").arg(time.toString("HH:mm"))); -} - -void -TimelineItem::setupAvatarLayout(const QString &userName) -{ - topLayout_->setContentsMargins( - conf::timeline::msgLeftMargin, conf::timeline::msgAvatarTopMargin, 0, 0); - - QFont f; - f.setPointSizeF(f.pointSizeF()); - - userAvatar_ = new Avatar(this, QFontMetrics(f).height() * 2); - userAvatar_->setLetter(QChar(userName[0]).toUpper()); - - // TODO: The provided user name should be a UserId class - if (userName[0] == '@' && userName.size() > 1) - userAvatar_->setLetter(QChar(userName[1]).toUpper()); - - topLayout_->insertWidget(0, userAvatar_); - topLayout_->setAlignment(userAvatar_, Qt::AlignTop | Qt::AlignLeft); - - if (userName_) - mainLayout_->insertWidget(0, userName_, Qt::AlignTop | Qt::AlignLeft); -} - -void -TimelineItem::setupSimpleLayout() -{ - QFont f; - f.setPointSizeF(f.pointSizeF()); - - topLayout_->setContentsMargins(conf::timeline::msgLeftMargin + - QFontMetrics(f).height() * 2 + 2, - conf::timeline::msgTopMargin, - 0, - 0); -} - -void -TimelineItem::setUserAvatar(const QString &userid) -{ - if (userAvatar_ == nullptr) - return; - - userAvatar_->setImage(room_id_, userid); -} - -void -TimelineItem::contextMenuEvent(QContextMenuEvent *event) -{ - if (contextMenu_) - contextMenu_->exec(event->globalPos()); -} - -void -TimelineItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -TimelineItem::addSaveImageAction(ImageItem *image) -{ - if (contextMenu_) { - auto saveImage = new QAction("Save image", this); - contextMenu_->addAction(saveImage); - - connect(saveImage, &QAction::triggered, image, &ImageItem::saveAs); - } -} - -void -TimelineItem::addReplyAction() -{ - if (contextMenu_) { - auto replyAction = new QAction("Reply", this); - contextMenu_->addAction(replyAction); - - connect(replyAction, &QAction::triggered, this, &TimelineItem::replyAction); - } -} - -void -TimelineItem::replyAction() -{ - if (!body_) - return; - - RelatedInfo related; - related.type = message_type_; - related.quoted_body = body_->toPlainText(); - related.quoted_user = descriptionMsg_.userid; - related.related_event = eventId().toStdString(); - related.room = room_id_; - - emit ChatPage::instance()->messageReply(related); -} - -void -TimelineItem::addKeyRequestAction() -{ - if (contextMenu_) { - auto requestKeys = new QAction("Request encryption keys", this); - contextMenu_->addAction(requestKeys); - - connect(requestKeys, &QAction::triggered, this, [this]() { - olm::request_keys(room_id_.toStdString(), event_id_.toStdString()); - }); - } -} - -void -TimelineItem::addAvatar() -{ - if (userAvatar_) - return; - - // TODO: should be replaced with the proper event struct. - auto userid = descriptionMsg_.userid; - auto displayName = Cache::displayName(room_id_, userid); - - generateUserName(userid, displayName); - - setupAvatarLayout(displayName); - - setUserAvatar(userid); -} - -void -TimelineItem::sendReadReceipt() const -{ - if (!event_id_.isEmpty()) - http::client()->read_event(room_id_.toStdString(), - event_id_.toStdString(), - [this](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read_event ({}, {})", - room_id_.toStdString(), - event_id_.toStdString()); - } - }); -} - -void -TimelineItem::openRawMessageViewer() const -{ - const auto event_id = event_id_.toStdString(); - const auto room_id = room_id_.toStdString(); - - auto proxy = std::make_shared(); - connect(proxy.get(), &EventProxy::eventRetrieved, this, [](const nlohmann::json &obj) { - auto dialog = new dialogs::RawMessage{QString::fromStdString(obj.dump(4))}; - Q_UNUSED(dialog); - }); - - http::client()->get_event( - room_id, - event_id, - [event_id, room_id, proxy = std::move(proxy)]( - const mtx::events::collections::TimelineEvents &res, mtx::http::RequestErr err) { - using namespace mtx::events; - - if (err) { - nhlog::net()->warn( - "failed to retrieve event {} from {}", event_id, room_id); - return; - } - - try { - emit proxy->eventRetrieved(utils::serialize_event(res)); - } catch (const nlohmann::json::exception &e) { - nhlog::net()->warn( - "failed to serialize event ({}, {})", room_id, event_id); - } - }); -} diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h deleted file mode 100644 index 356976e5..00000000 --- a/src/timeline/TimelineItem.h +++ /dev/null @@ -1,389 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "mtx/events.hpp" - -#include "AvatarProvider.h" -#include "RoomInfoListItem.h" -#include "Utils.h" - -#include "Cache.h" -#include "MatrixClient.h" - -#include "ui/FlatButton.h" - -class ImageItem; -class StickerItem; -class AudioItem; -class VideoItem; -class FileItem; -class Avatar; -class TextLabel; - -enum class StatusIndicatorState -{ - //! The encrypted message was received by the server. - Encrypted, - //! The plaintext message was received by the server. - Received, - //! At least one of the participants has read the message. - Read, - //! The client sent the message. Not yet received. - Sent, - //! When the message is loaded from cache or backfill. - Empty, -}; - -//! -//! Used to notify the user about the status of a message. -//! -class StatusIndicator : public QWidget -{ - Q_OBJECT - -public: - explicit StatusIndicator(QWidget *parent); - void setState(StatusIndicatorState state); - StatusIndicatorState state() const { return state_; } - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - void paintIcon(QPainter &p, QIcon &icon); - - QIcon lockIcon_; - QIcon clockIcon_; - QIcon checkmarkIcon_; - QIcon doubleCheckmarkIcon_; - - QColor iconColor_ = QColor("#999"); - - StatusIndicatorState state_ = StatusIndicatorState::Empty; - - static constexpr int MaxWidth = 24; -}; - -class EventProxy : public QObject -{ - Q_OBJECT - -signals: - void eventRetrieved(const nlohmann::json &); -}; - -class UserProfileFilter : public QObject -{ - Q_OBJECT - -public: - explicit UserProfileFilter(const QString &user_id, QLabel *parent) - : QObject(parent) - , user_id_{user_id} - {} - -signals: - void hoverOff(); - void hoverOn(); - void clicked(); - -protected: - bool eventFilter(QObject *obj, QEvent *event) - { - if (event->type() == QEvent::MouseButtonRelease) { - emit clicked(); - return true; - } else if (event->type() == QEvent::HoverLeave) { - emit hoverOff(); - return true; - } else if (event->type() == QEvent::HoverEnter) { - emit hoverOn(); - return true; - } - - return QObject::eventFilter(obj, event); - } - -private: - QString user_id_; -}; - -class TimelineItem : public QWidget -{ - Q_OBJECT - Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) - -public: - TimelineItem(const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - - // For local messages. - // m.text & m.emote - TimelineItem(mtx::events::MessageType ty, - const QString &userid, - QString body, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - // m.image - TimelineItem(ImageItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(FileItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(AudioItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(VideoItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - - TimelineItem(ImageItem *img, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(StickerItem *img, - const mtx::events::Sticker &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(FileItem *file, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(AudioItem *audio, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(VideoItem *video, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - - ~TimelineItem(); - - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - QColor backgroundColor() const { return backgroundColor_; } - - void setUserAvatar(const QString &userid); - DescInfo descriptionMessage() const { return descriptionMsg_; } - QString eventId() const { return event_id_; } - void setEventId(const QString &event_id) { event_id_ = event_id; } - void markReceived(bool isEncrypted); - void markRead(); - void markSent(); - bool isReceived() { return isReceived_; }; - void setRoomId(QString room_id) { room_id_ = room_id; } - void sendReadReceipt() const; - void openRawMessageViewer() const; - void replyAction(); - - //! Add a user avatar for this event. - void addAvatar(); - void addKeyRequestAction(); - -signals: - void eventRedacted(const QString &event_id); - void redactionFailed(const QString &msg); - -public slots: - void refreshAuthorColor(); - void finishedGeneratingColor(); - -protected: - void paintEvent(QPaintEvent *event) override; - void contextMenuEvent(QContextMenuEvent *event) override; - -private: - //! If we are the sender of the message the event wil be marked as received by the server. - void markOwnMessagesAsReceived(const std::string &sender); - void init(); - //! Add a context menu option to save the image of the timeline item. - void addSaveImageAction(ImageItem *image); - //! Add the reply action in the context menu for widgets that support it. - void addReplyAction(); - - template - void setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender); - - template - void setupWidgetLayout(Widget *widget, const Event &event, bool withSender); - - void generateBody(const QString &body); - void generateBody(const QString &user_id, const QString &displayname, const QString &body); - void generateTimestamp(const QDateTime &time); - void generateUserName(const QString &userid, const QString &displayname); - - void setupAvatarLayout(const QString &userName); - void setupSimpleLayout(); - - void adjustMessageLayout(); - void adjustMessageLayoutForWidget(); - - //! Whether or not the event associated with the widget - //! has been acknowledged by the server. - bool isReceived_ = false; - - QFutureWatcher *colorGenerating_; - - QString event_id_; - mtx::events::MessageType message_type_ = mtx::events::MessageType::Unknown; - QString room_id_; - - DescInfo descriptionMsg_; - - QMenu *contextMenu_; - QAction *showReadReceipts_; - QAction *markAsRead_; - QAction *redactMsg_; - QAction *viewRawMessage_; - QAction *replyMsg_; - - QHBoxLayout *topLayout_ = nullptr; - QHBoxLayout *messageLayout_ = nullptr; - QHBoxLayout *actionLayout_ = nullptr; - QVBoxLayout *mainLayout_ = nullptr; - QHBoxLayout *widgetLayout_ = nullptr; - - Avatar *userAvatar_; - - QFont timestampFont_; - - StatusIndicator *statusIndicator_; - - QLabel *timestamp_; - QLabel *userName_; - TextLabel *body_; - - QColor backgroundColor_; - - FlatButton *replyBtn_; - FlatButton *contextBtn_; -}; - -template -void -TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender) -{ - auto displayName = Cache::displayName(room_id_, userid); - auto timestamp = QDateTime::currentDateTime(); - - descriptionMsg_ = {"", // No event_id up until this point. - "You", - userid, - QString(" %1").arg(utils::messageDescription()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout; - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(userid, displayName, ""); - setupAvatarLayout(displayName); - - setUserAvatar(userid); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} - -template -void -TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSender) -{ - init(); - - // if (event.type == mtx::events::EventType::RoomMessage) { - // message_type_ = mtx::events::getMessageType(event.content.msgtype); - //} - // TODO: Fix this. - message_type_ = mtx::events::MessageType::Unknown; - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - - QSettings settings; - descriptionMsg_ = {event_id_, - sender == settings.value("auth/user_id") ? "You" : displayName, - sender, - QString(" %1").arg(utils::messageDescription()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout(); - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(sender, displayName, ""); - setupAvatarLayout(displayName); - - setUserAvatar(sender); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp new file mode 100644 index 00000000..ab7d3d47 --- /dev/null +++ b/src/timeline/TimelineModel.cpp @@ -0,0 +1,1220 @@ +#include "TimelineModel.h" + +#include +#include + +#include + +#include "ChatPage.h" +#include "Logging.h" +#include "MainWindow.h" +#include "Olm.h" +#include "TimelineViewManager.h" +#include "Utils.h" +#include "dialogs/RawMessage.h" + +Q_DECLARE_METATYPE(QModelIndex) + +namespace { +template +QString +eventId(const mtx::events::RoomEvent &event) +{ + return QString::fromStdString(event.event_id); +} +template +QString +roomId(const mtx::events::Event &event) +{ + return QString::fromStdString(event.room_id); +} +template +QString +senderId(const mtx::events::RoomEvent &event) +{ + return QString::fromStdString(event.sender); +} + +template +QDateTime +eventTimestamp(const mtx::events::RoomEvent &event) +{ + return QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); +} + +template +std::string +eventMsgType(const mtx::events::Event &) +{ + return ""; +} +template +auto +eventMsgType(const mtx::events::RoomEvent &e) -> decltype(e.content.msgtype) +{ + return e.content.msgtype; +} + +template +QString +eventBody(const mtx::events::Event &) +{ + return QString(""); +} +template +auto +eventBody(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + return QString::fromStdString(e.content.body); +} + +template +QString +eventFormattedBody(const mtx::events::Event &) +{ + return QString(""); +} +template +auto +eventFormattedBody(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + auto temp = e.content.formatted_body; + if (!temp.empty()) { + return QString::fromStdString(temp); + } else { + return QString::fromStdString(e.content.body).toHtmlEscaped().replace("\n", "
"); + } +} + +template +QString +eventUrl(const mtx::events::Event &) +{ + return ""; +} +template +auto +eventUrl(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + return QString::fromStdString(e.content.url); +} + +template +QString +eventThumbnailUrl(const mtx::events::Event &) +{ + return ""; +} +template +auto +eventThumbnailUrl(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, + QString> +{ + return QString::fromStdString(e.content.info.thumbnail_url); +} + +template +QString +eventFilename(const mtx::events::Event &) +{ + return ""; +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + if (!e.content.filename.empty()) + return QString::fromStdString(e.content.filename); + return QString::fromStdString(e.content.body); +} + +template +auto +eventFilesize(const mtx::events::RoomEvent &e) -> decltype(e.content.info.size) +{ + return e.content.info.size; +} + +template +int64_t +eventFilesize(const mtx::events::Event &) +{ + return 0; +} + +template +QString +eventMimeType(const mtx::events::Event &) +{ + return QString(); +} +template +auto +eventMimeType(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + return QString::fromStdString(e.content.info.mimetype); +} + +template +QString +eventRelatesTo(const mtx::events::Event &) +{ + return QString(); +} +template +auto +eventRelatesTo(const mtx::events::RoomEvent &e) -> std::enable_if_t< + std::is_same::value, + QString> +{ + return QString::fromStdString(e.content.relates_to.in_reply_to.event_id); +} + +template +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &e) +{ + using mtx::events::EventType; + switch (e.type) { + case EventType::RoomKeyRequest: + return qml_mtx_events::EventType::KeyRequest; + case EventType::RoomAliases: + return qml_mtx_events::EventType::Aliases; + case EventType::RoomAvatar: + return qml_mtx_events::EventType::Avatar; + case EventType::RoomCanonicalAlias: + return qml_mtx_events::EventType::CanonicalAlias; + case EventType::RoomCreate: + return qml_mtx_events::EventType::Create; + case EventType::RoomEncrypted: + return qml_mtx_events::EventType::Encrypted; + case EventType::RoomEncryption: + return qml_mtx_events::EventType::Encryption; + case EventType::RoomGuestAccess: + return qml_mtx_events::EventType::GuestAccess; + case EventType::RoomHistoryVisibility: + return qml_mtx_events::EventType::HistoryVisibility; + case EventType::RoomJoinRules: + return qml_mtx_events::EventType::JoinRules; + case EventType::RoomMember: + return qml_mtx_events::EventType::Member; + case EventType::RoomMessage: + return qml_mtx_events::EventType::UnknownMessage; + case EventType::RoomName: + return qml_mtx_events::EventType::Name; + case EventType::RoomPowerLevels: + return qml_mtx_events::EventType::PowerLevels; + case EventType::RoomTopic: + return qml_mtx_events::EventType::Topic; + case EventType::RoomTombstone: + return qml_mtx_events::EventType::Tombstone; + case EventType::RoomRedaction: + return qml_mtx_events::EventType::Redaction; + case EventType::RoomPinnedEvents: + return qml_mtx_events::EventType::PinnedEvents; + case EventType::Sticker: + return qml_mtx_events::EventType::Sticker; + case EventType::Tag: + return qml_mtx_events::EventType::Tag; + case EventType::Unsupported: + default: + return qml_mtx_events::EventType::Unsupported; + } +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::AudioMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::EmoteMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::FileMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::ImageMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::NoticeMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::TextMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::VideoMessage; +} + +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::Redacted; +} +// ::EventType::Type toRoomEventType(const Event &e) { return +// ::EventType::LocationMessage; } + +template +uint64_t +eventHeight(const mtx::events::Event &) +{ + return -1; +} +template +auto +eventHeight(const mtx::events::RoomEvent &e) -> decltype(e.content.info.h) +{ + return e.content.info.h; +} +template +uint64_t +eventWidth(const mtx::events::Event &) +{ + return -1; +} +template +auto +eventWidth(const mtx::events::RoomEvent &e) -> decltype(e.content.info.w) +{ + return e.content.info.w; +} + +template +double +eventPropHeight(const mtx::events::RoomEvent &e) +{ + auto w = eventWidth(e); + if (w == 0) + w = 1; + return eventHeight(e) / (double)w; +} +} + +TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent) + : QAbstractListModel(parent) + , room_id_(room_id) + , manager_(manager) +{ + connect( + this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); + connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) { + pending.remove(txn_id); + failed.insert(txn_id); + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Failed index out of range"); + return; + } + emit dataChanged(index(idx, 0), index(idx, 0)); + }); + connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) { + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Sent index out of range"); + return; + } + eventOrder[idx] = event_id; + auto ev = events.value(txn_id); + ev = boost::apply_visitor( + [event_id](const auto &e) -> mtx::events::collections::TimelineEvents { + auto eventCopy = e; + eventCopy.event_id = event_id.toStdString(); + return eventCopy; + }, + ev); + events.remove(txn_id); + events.insert(event_id, ev); + + // mark our messages as read + readEvent(event_id.toStdString()); + + // ask to be notified for read receipts + cache::client()->addPendingReceipt(room_id_, event_id); + + emit dataChanged(index(idx, 0), index(idx, 0)); + }); + connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { + emit ChatPage::instance()->showNotification(msg); + }); +} + +QHash +TimelineModel::roleNames() const +{ + return { + {Section, "section"}, + {Type, "type"}, + {Body, "body"}, + {FormattedBody, "formattedBody"}, + {UserId, "userId"}, + {UserName, "userName"}, + {Timestamp, "timestamp"}, + {Url, "url"}, + {ThumbnailUrl, "thumbnailUrl"}, + {Filename, "filename"}, + {Filesize, "filesize"}, + {MimeType, "mimetype"}, + {Height, "height"}, + {Width, "width"}, + {ProportionalHeight, "proportionalHeight"}, + {Id, "id"}, + {State, "state"}, + {IsEncrypted, "isEncrypted"}, + {ReplyTo, "replyTo"}, + }; +} +int +TimelineModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return (int)this->eventOrder.size(); +} + +QVariant +TimelineModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 && index.row() >= (int)eventOrder.size()) + return QVariant(); + + QString id = eventOrder[index.row()]; + + mtx::events::collections::TimelineEvents event = events.value(id); + + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + switch (role) { + case Section: { + QDateTime date = boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event); + date.setTime(QTime()); + + QString userId = + boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, event); + + for (int r = index.row() - 1; r > 0; r--) { + QDateTime prevDate = boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, + events.value(eventOrder[r])); + prevDate.setTime(QTime()); + if (prevDate != date) + return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId); + + QString prevUserId = + boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, + events.value(eventOrder[r])); + if (userId != prevUserId) + break; + } + + return QString("%1").arg(userId); + } + case UserId: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, event)); + case UserName: + return QVariant(displayName(boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, event))); + + case Timestamp: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event)); + case Type: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, + event)); + case Body: + return QVariant(utils::replaceEmoji(boost::apply_visitor( + [](const auto &e) -> QString { return eventBody(e); }, event))); + case FormattedBody: + return QVariant( + utils::replaceEmoji( + boost::apply_visitor( + [](const auto &e) -> QString { return eventFormattedBody(e); }, event)) + .remove("") + .remove("")); + case Url: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventUrl(e); }, event)); + case ThumbnailUrl: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventThumbnailUrl(e); }, event)); + case Filename: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventFilename(e); }, event)); + case Filesize: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { + return utils::humanReadableFileSize(eventFilesize(e)); + }, + event)); + case MimeType: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventMimeType(e); }, event)); + case Height: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qulonglong { return eventHeight(e); }, event)); + case Width: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qulonglong { return eventWidth(e); }, event)); + case ProportionalHeight: + return QVariant(boost::apply_visitor( + [](const auto &e) -> double { return eventPropHeight(e); }, event)); + case Id: + return id; + case State: + // only show read receipts for messages not from us + if (boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, + event) + .toStdString() != http::client()->user_id().to_string()) + return qml_mtx_events::Empty; + else if (failed.contains(id)) + return qml_mtx_events::Failed; + else if (pending.contains(id)) + return qml_mtx_events::Sent; + else if (read.contains(id) || + cache::client()->readReceipts(id, room_id_).size() > 1) + return qml_mtx_events::Read; + else + return qml_mtx_events::Received; + case IsEncrypted: { + auto tempEvent = events[id]; + return boost::get>( + &tempEvent) != nullptr; + } + case ReplyTo: { + QString evId = boost::apply_visitor( + [](const auto &e) -> QString { return eventRelatesTo(e); }, event); + return QVariant(evId); + } + default: + return QVariant(); + } +} + +void +TimelineModel::addEvents(const mtx::responses::Timeline &timeline) +{ + if (isInitialSync) { + prev_batch_token_ = QString::fromStdString(timeline.prev_batch); + isInitialSync = false; + } + + if (timeline.events.empty()) + return; + + std::vector ids = internalAddEvents(timeline.events); + + if (ids.empty()) + return; + + beginInsertRows(QModelIndex(), + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size() + ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); + endInsertRows(); + + updateLastMessage(); +} + +void +TimelineModel::updateLastMessage() +{ + auto event = events.value(eventOrder.back()); + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + auto description = utils::getMessageDescription( + event, QString::fromStdString(http::client()->user_id().to_string()), room_id_); + emit manager_->updateRoomsLastMessage(room_id_, description); +} + +std::vector +TimelineModel::internalAddEvents( + const std::vector &timeline) +{ + std::vector ids; + for (const auto &e : timeline) { + QString id = + boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); + + if (this->events.contains(id)) { + this->events.insert(id, e); + int idx = idToIndex(id); + emit dataChanged(index(idx, 0), index(idx, 0)); + continue; + } + + if (auto redaction = + boost::get>(&e)) { + QString redacts = QString::fromStdString(redaction->redacts); + auto redacted = std::find(eventOrder.begin(), eventOrder.end(), redacts); + + if (redacted != eventOrder.end()) { + auto redactedEvent = boost::apply_visitor( + [](const auto &ev) + -> mtx::events::RoomEvent { + mtx::events::RoomEvent + replacement = {}; + replacement.event_id = ev.event_id; + replacement.room_id = ev.room_id; + replacement.sender = ev.sender; + replacement.origin_server_ts = ev.origin_server_ts; + replacement.type = ev.type; + return replacement; + }, + e); + events.insert(redacts, redactedEvent); + + int row = (int)std::distance(eventOrder.begin(), redacted); + emit dataChanged(index(row, 0), index(row, 0)); + } + + continue; // don't insert redaction into timeline + } + + this->events.insert(id, e); + ids.push_back(id); + } + return ids; +} + +void +TimelineModel::fetchHistory() +{ + if (paginationInProgress) { + nhlog::ui()->warn("Already loading older messages"); + return; + } + + paginationInProgress = true; + mtx::http::MessagesOpts opts; + opts.room_id = room_id_.toStdString(); + opts.from = prev_batch_token_.toStdString(); + + nhlog::ui()->info("Paginationg room {}", opts.room_id); + + http::client()->messages( + opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("failed to call /messages ({}): {} - {}", + opts.room_id, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + paginationInProgress = false; + return; + } + + emit oldMessagesRetrieved(std::move(res)); + paginationInProgress = false; + }); +} + +void +TimelineModel::setCurrentIndex(int index) +{ + auto oldIndex = idToIndex(currentId); + currentId = indexToId(index); + emit currentIndexChanged(index); + + if (oldIndex < index && !pending.contains(currentId)) { + readEvent(currentId.toStdString()); + } +} + +void +TimelineModel::readEvent(const std::string &id) +{ + http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to read_event ({}, {})", + room_id_.toStdString(), + currentId.toStdString()); + } + }); +} + +void +TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) +{ + std::vector ids = internalAddEvents(msgs.chunk); + + if (!ids.empty()) { + beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); + endInsertRows(); + } + + prev_batch_token_ = QString::fromStdString(msgs.end); +} + +QColor +TimelineModel::userColor(QString id, QColor background) +{ + if (!userColors.contains(id)) + userColors.insert( + id, QColor(utils::generateContrastingHexColor(id, background.name()))); + return userColors.value(id); +} + +QString +TimelineModel::displayName(QString id) const +{ + return Cache::displayName(room_id_, id); +} + +QString +TimelineModel::avatarUrl(QString id) const +{ + return Cache::avatarUrl(room_id_, id); +} + +QString +TimelineModel::formatDateSeparator(QDate date) const +{ + auto now = QDateTime::currentDateTime(); + + QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); + + if (now.date().year() == date.year()) { + QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); + fmt = fmt.remove(rx); + } + + return date.toString(fmt); +} + +QString +TimelineModel::escapeEmoji(QString str) const +{ + return utils::replaceEmoji(str); +} + +void +TimelineModel::viewRawMessage(QString id) const +{ + std::string ev = utils::serialize_event(events.value(id)).dump(4); + auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); + Q_UNUSED(dialog); +} + +void + +TimelineModel::openUserProfile(QString userid) const +{ + MainWindow::instance()->openUserProfile(userid, room_id_); +} + +DecryptionResult +TimelineModel::decryptEvent(const mtx::events::EncryptedEvent &e) const +{ + MegolmSessionIndex index; + index.room_id = room_id_.toStdString(); + index.session_id = e.content.session_id; + index.sender_key = e.content.sender_key; + + mtx::events::RoomEvent dummy; + dummy.origin_server_ts = e.origin_server_ts; + dummy.event_id = e.event_id; + dummy.sender = e.sender; + dummy.content.body = + tr("-- Encrypted Event (No keys found for decryption) --", + "Placeholder, when the message was not decrypted yet or can't be decrypted") + .toStdString(); + + try { + if (!cache::client()->inboundMegolmSessionExists(index)) { + nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", + index.room_id, + index.session_id, + e.sender); + // TODO: request megolm session_id & session_key from the sender. + return {dummy, false}; + } + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); + dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --", + "Placeholder, when the message can't be decrypted, because " + "the DB access failed when trying to lookup the session.") + .toStdString(); + return {dummy, false}; + } + + std::string msg_str; + try { + auto session = cache::client()->getInboundMegolmSession(index); + auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); + msg_str = std::string((char *)res.data.data(), res.data.size()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (failed to retrieve megolm keys from db) --", + "Placeholder, when the message can't be decrypted, because the DB access " + "failed.") + .toStdString(); + return {dummy, false}; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (%1) --", + "Placeholder, when the message can't be decrypted. In this case, the Olm " + "decrytion returned an error, which is passed ad %1") + .arg(e.what()) + .toStdString(); + return {dummy, false}; + } + + // Add missing fields for the event. + json body = json::parse(msg_str); + body["event_id"] = e.event_id; + body["sender"] = e.sender; + body["origin_server_ts"] = e.origin_server_ts; + body["unsigned"] = e.unsigned_data; + + json event_array = json::array(); + event_array.push_back(body); + + std::vector temp_events; + mtx::responses::utils::parse_timeline_events(event_array, temp_events); + + if (temp_events.size() == 1) + return {temp_events.at(0), true}; + + dummy.content.body = + tr("-- Encrypted Event (Unknown event type) --", + "Placeholder, when the message was decrypted, but we couldn't parse it, because " + "Nheko/mtxclient don't support that event type yet") + .toStdString(); + return {dummy, false}; +} + +void +TimelineModel::replyAction(QString id) +{ + auto event = events.value(id); + RelatedInfo related = boost::apply_visitor( + [](const auto &ev) -> RelatedInfo { + RelatedInfo related_ = {}; + related_.quoted_user = QString::fromStdString(ev.sender); + related_.related_event = ev.event_id; + return related_; + }, + event); + related.type = mtx::events::getMessageType(boost::apply_visitor( + [](const auto &e) -> std::string { return eventMsgType(e); }, event)); + related.quoted_body = boost::apply_visitor( + [](const auto &e) -> QString { return eventFormattedBody(e); }, event); + related.quoted_body.remove(QRegularExpression( + ".*", QRegularExpression::DotMatchesEverythingOption)); + nhlog::ui()->debug("after replacement: {}", related.quoted_body.toStdString()); + related.room = room_id_; + + if (related.quoted_body.isEmpty()) + return; + + ChatPage::instance()->messageReply(related); +} + +void +TimelineModel::readReceiptsAction(QString id) const +{ + MainWindow::instance()->openReadReceiptsDialog(id); +} + +void +TimelineModel::redactEvent(QString id) +{ + if (!id.isEmpty()) + http::client()->redact_event( + room_id_.toStdString(), + id.toStdString(), + [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit redactionFailed( + tr("Message redaction failed: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit eventRedacted(id); + }); +} + +int +TimelineModel::idToIndex(QString id) const +{ + if (id.isEmpty()) + return -1; + for (int i = 0; i < (int)eventOrder.size(); i++) + if (id == eventOrder[i]) + return i; + return -1; +} + +QString +TimelineModel::indexToId(int index) const +{ + if (index < 0 || index >= (int)eventOrder.size()) + return ""; + return eventOrder[index]; +} + +// Note: this will only be called for our messages +void +TimelineModel::markEventsAsRead(const std::vector &event_ids) +{ + for (const auto &id : event_ids) { + read.insert(id); + int idx = idToIndex(id); + if (idx < 0) { + nhlog::ui()->warn("Read index out of range"); + return; + } + emit dataChanged(index(idx, 0), index(idx, 0)); + } +} + +void +TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json content) +{ + const auto room_id = room_id_.toStdString(); + + using namespace mtx::events; + using namespace mtx::identifiers; + + json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; + + try { + // Check if we have already an outbound megolm session then we can use. + if (cache::client()->outboundMegolmSessionExists(room_id)) { + auto data = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc.dump()); + + http::client()->send_room_message( + room_id, + txn_id, + data, + [this, txn_id](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed(QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + return; + } + + nhlog::ui()->debug("creating new outbound megolm session"); + + // Create a new outbound megolm session. + auto outbound_session = olm::client()->init_outbound_group_session(); + const auto session_id = mtx::crypto::session_id(outbound_session.get()); + const auto session_key = mtx::crypto::session_key(outbound_session.get()); + + // TODO: needs to be moved in the lib. + auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, + {"room_id", room_id}, + {"session_id", session_id}, + {"session_key", session_key}}; + + // Saving the new megolm session. + // TODO: Maybe it's too early to save. + OutboundGroupSessionData session_data; + session_data.session_id = session_id; + session_data.session_key = session_key; + session_data.message_index = 0; // TODO Update me + cache::client()->saveOutboundMegolmSession( + room_id, session_data, std::move(outbound_session)); + + const auto members = cache::client()->roomMembers(room_id); + nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); + + auto keeper = + std::make_shared([megolm_payload, room_id, doc, txn_id, this]() { + try { + auto data = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc.dump()); + + http::client() + ->send_room_message( + room_id, + txn_id, + data, + [this, txn_id](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast(err->status_code); + nhlog::net()->warn( + "[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed( + QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to save megolm outbound session: {}", e.what()); + } + }); + + mtx::requests::QueryKeys req; + for (const auto &member : members) + req.device_keys[member] = {}; + + http::client()->query_keys( + req, + [keeper = std::move(keeper), megolm_payload, this]( + const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {} {}", + err->matrix_error.error, + static_cast(err->status_code)); + // TODO: Mark the event as failed. Communicate with the UI. + return; + } + + for (const auto &user : res.device_keys) { + // Mapping from a device_id with valid identity keys to the + // generated room_key event used for sharing the megolm session. + std::map room_key_msgs; + std::map deviceKeys; + + room_key_msgs.clear(); + deviceKeys.clear(); + + for (const auto &dev : user.second) { + const auto user_id = ::UserId(dev.second.user_id); + const auto device_id = DeviceId(dev.second.device_id); + + const auto device_keys = dev.second.keys; + const auto curveKey = "curve25519:" + device_id.get(); + const auto edKey = "ed25519:" + device_id.get(); + + if ((device_keys.find(curveKey) == device_keys.end()) || + (device_keys.find(edKey) == device_keys.end())) { + nhlog::net()->debug( + "ignoring malformed keys for device {}", + device_id.get()); + continue; + } + + DevicePublicKeys pks; + pks.ed25519 = device_keys.at(edKey); + pks.curve25519 = device_keys.at(curveKey); + + try { + if (!mtx::crypto::verify_identity_signature( + json(dev.second), device_id, user_id)) { + nhlog::crypto()->warn( + "failed to verify identity keys: {}", + json(dev.second).dump(2)); + continue; + } + } catch (const json::exception &e) { + nhlog::crypto()->warn( + "failed to parse device key json: {}", + e.what()); + continue; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->warn( + "failed to verify device key json: {}", + e.what()); + continue; + } + + auto room_key = olm::client() + ->create_room_key_event( + user_id, pks.ed25519, megolm_payload) + .dump(); + + room_key_msgs.emplace(device_id, room_key); + deviceKeys.emplace(device_id, pks); + } + + std::vector valid_devices; + valid_devices.reserve(room_key_msgs.size()); + for (auto const &d : room_key_msgs) { + valid_devices.push_back(d.first); + + nhlog::net()->info("{}", d.first); + nhlog::net()->info(" curve25519 {}", + deviceKeys.at(d.first).curve25519); + nhlog::net()->info(" ed25519 {}", + deviceKeys.at(d.first).ed25519); + } + + nhlog::net()->info( + "sending claim request for user {} with {} devices", + user.first, + valid_devices.size()); + + http::client()->claim_keys( + user.first, + valid_devices, + std::bind(&TimelineModel::handleClaimedKeys, + this, + keeper, + room_key_msgs, + deviceKeys, + user.first, + std::placeholders::_1, + std::placeholders::_2)); + + // TODO: Wait before sending the next batch of requests. + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + }); + + // TODO: Let the user know about the errors. + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + } +} + +void +TimelineModel::handleClaimedKeys(std::shared_ptr keeper, + const std::map &room_keys, + const std::map &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err) +{ + if (err) { + nhlog::net()->warn("claim keys error: {} {} {}", + err->matrix_error.error, + err->parse_error, + static_cast(err->status_code)); + return; + } + + nhlog::net()->debug("claimed keys for {}", user_id); + + if (res.one_time_keys.size() == 0) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + auto retrieved_devices = res.one_time_keys.at(user_id); + + // Payload with all the to_device message to be sent. + json body; + body["messages"][user_id] = json::object(); + + for (const auto &rd : retrieved_devices) { + const auto device_id = rd.first; + nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); + + // TODO: Verify signatures + auto otk = rd.second.begin()->at("key"); + + if (pks.find(device_id) == pks.end()) { + nhlog::net()->critical("couldn't find public key for device: {}", + device_id); + continue; + } + + auto id_key = pks.at(device_id).curve25519; + auto s = olm::client()->create_outbound_session(id_key, otk); + + if (room_keys.find(device_id) == room_keys.end()) { + nhlog::net()->critical("couldn't find m.room_key for device: {}", + device_id); + continue; + } + + auto device_msg = olm::client()->create_olm_encrypted_content( + s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); + + try { + cache::client()->saveOlmSession(id_key, std::move(s)); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to pickle outbound olm session: {}", + e.what()); + } + + body["messages"][user_id][device_id] = device_msg; + } + + nhlog::net()->info("send_to_device: {}", user_id); + + http::client()->send_to_device( + "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send " + "send_to_device " + "message: {}", + err->matrix_error.error); + } + + (void)keeper; + }); +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h new file mode 100644 index 00000000..31e41315 --- /dev/null +++ b/src/timeline/TimelineModel.h @@ -0,0 +1,258 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" + +namespace qml_mtx_events { +Q_NAMESPACE + +enum EventType +{ + // Unsupported event + Unsupported, + /// m.room_key_request + KeyRequest, + /// m.room.aliases + Aliases, + /// m.room.avatar + Avatar, + /// m.room.canonical_alias + CanonicalAlias, + /// m.room.create + Create, + /// m.room.encrypted. + Encrypted, + /// m.room.encryption. + Encryption, + /// m.room.guest_access + GuestAccess, + /// m.room.history_visibility + HistoryVisibility, + /// m.room.join_rules + JoinRules, + /// m.room.member + Member, + /// m.room.name + Name, + /// m.room.power_levels + PowerLevels, + /// m.room.tombstone + Tombstone, + /// m.room.topic + Topic, + /// m.room.redaction + Redaction, + /// m.room.pinned_events + PinnedEvents, + // m.sticker + Sticker, + // m.tag + Tag, + /// m.room.message + AudioMessage, + EmoteMessage, + FileMessage, + ImageMessage, + LocationMessage, + NoticeMessage, + TextMessage, + VideoMessage, + Redacted, + UnknownMessage, +}; +Q_ENUM_NS(EventType) + +enum EventState +{ + //! The plaintext message was received by the server. + Received, + //! At least one of the participants has read the message. + Read, + //! The client sent the message. Not yet received. + Sent, + //! When the message is loaded from cache or backfill. + Empty, + //! When the message failed to send + Failed, +}; +Q_ENUM_NS(EventState) +} + +class StateKeeper +{ +public: + StateKeeper(std::function &&fn) + : fn_(std::move(fn)) + {} + + ~StateKeeper() { fn_(); } + +private: + std::function fn_; +}; + +struct DecryptionResult +{ + //! The decrypted content as a normal plaintext event. + mtx::events::collections::TimelineEvents event; + //! Whether or not the decryption was successful. + bool isDecrypted = false; +}; + +class TimelineViewManager; + +class TimelineModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY( + int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + +public: + explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = 0); + + enum Roles + { + Section, + Type, + Body, + FormattedBody, + UserId, + UserName, + Timestamp, + Url, + ThumbnailUrl, + Filename, + Filesize, + MimeType, + Height, + Width, + ProportionalHeight, + Id, + State, + IsEncrypted, + ReplyTo, + }; + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + Q_INVOKABLE QColor userColor(QString id, QColor background); + Q_INVOKABLE QString displayName(QString id) const; + Q_INVOKABLE QString avatarUrl(QString id) const; + Q_INVOKABLE QString formatDateSeparator(QDate date) const; + + Q_INVOKABLE QString escapeEmoji(QString str) const; + Q_INVOKABLE void viewRawMessage(QString id) const; + Q_INVOKABLE void openUserProfile(QString userid) const; + Q_INVOKABLE void replyAction(QString id); + Q_INVOKABLE void readReceiptsAction(QString id) const; + Q_INVOKABLE void redactEvent(QString id); + Q_INVOKABLE int idToIndex(QString id) const; + Q_INVOKABLE QString indexToId(int index) const; + + void addEvents(const mtx::responses::Timeline &events); + template + void sendMessage(const T &msg); + +public slots: + void fetchHistory(); + void setCurrentIndex(int index); + int currentIndex() const { return idToIndex(currentId); } + void markEventsAsRead(const std::vector &event_ids); + +private slots: + // Add old events at the top of the timeline. + void addBackwardsEvents(const mtx::responses::Messages &msgs); + +signals: + void oldMessagesRetrieved(const mtx::responses::Messages &res); + void messageFailed(QString txn_id); + void messageSent(QString txn_id, QString event_id); + void currentIndexChanged(int index); + void redactionFailed(QString id); + void eventRedacted(QString id); + +private: + DecryptionResult decryptEvent( + const mtx::events::EncryptedEvent &e) const; + std::vector internalAddEvents( + const std::vector &timeline); + void sendEncryptedMessage(const std::string &txn_id, nlohmann::json content); + void handleClaimedKeys(std::shared_ptr keeper, + const std::map &room_key, + const std::map &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err); + void updateLastMessage(); + void readEvent(const std::string &id); + + QHash events; + QSet pending, failed, read; + std::vector eventOrder; + + QString room_id_; + QString prev_batch_token_; + + bool isInitialSync = true; + bool paginationInProgress = false; + + QHash userColors; + QString currentId; + + TimelineViewManager *manager_; +}; + +template +void +TimelineModel::sendMessage(const T &msg) +{ + auto txn_id = http::client()->generate_txn_id(); + mtx::events::RoomEvent msgCopy = {}; + msgCopy.content = msg; + msgCopy.type = mtx::events::EventType::RoomMessage; + msgCopy.event_id = txn_id; + msgCopy.sender = http::client()->user_id().to_string(); + msgCopy.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); + internalAddEvents({msgCopy}); + + QString txn_id_qstr = QString::fromStdString(txn_id); + beginInsertRows(QModelIndex(), + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size())); + pending.insert(txn_id_qstr); + this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); + endInsertRows(); + updateLastMessage(); + + if (cache::client()->isRoomEncrypted(room_id_.toStdString())) + sendEncryptedMessage(txn_id, nlohmann::json(msg)); + else + http::client()->send_room_message( + room_id_.toStdString(), + txn_id, + msg, + [this, txn_id, txn_id_qstr](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = static_cast(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed(txn_id_qstr); + } + emit messageSent(txn_id_qstr, + QString::fromStdString(res.event_id.to_string())); + }); +} diff --git a/src/timeline/TimelineView.cpp b/src/timeline/TimelineView.cpp deleted file mode 100644 index ed783e90..00000000 --- a/src/timeline/TimelineView.cpp +++ /dev/null @@ -1,1627 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include - -#include -#include -#include -#include - -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "Olm.h" -#include "UserSettingsPage.h" -#include "Utils.h" -#include "ui/FloatingButton.h" -#include "ui/InfoMessage.h" - -#include "timeline/TimelineView.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -using TimelineEvent = mtx::events::collections::TimelineEvents; - -//! Maximum number of widgets to keep in the timeline layout. -constexpr int MAX_RETAINED_WIDGETS = 100; -constexpr int MIN_SCROLLBAR_HANDLE = 60; - -//! Retrieve the timestamp of the event represented by the given widget. -QDateTime -getDate(QWidget *widget) -{ - auto item = qobject_cast(widget); - if (item) - return item->descriptionMessage().datetime; - - auto infoMsg = qobject_cast(widget); - if (infoMsg) - return infoMsg->datetime(); - - return QDateTime(); -} - -TimelineView::TimelineView(const mtx::responses::Timeline &timeline, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addEvents(timeline); -} - -TimelineView::TimelineView(const QString &room_id, QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - getMessages(); -} - -void -TimelineView::sliderRangeChanged(int min, int max) -{ - Q_UNUSED(min); - - if (!scroll_area_->verticalScrollBar()->isVisible()) { - scroll_area_->verticalScrollBar()->setValue(max); - return; - } - - // If the scrollbar is close to the bottom and a new message - // is added we move the scrollbar. - if (max - scroll_area_->verticalScrollBar()->value() < SCROLL_BAR_GAP) { - scroll_area_->verticalScrollBar()->setValue(max); - return; - } - - int currentHeight = scroll_widget_->size().height(); - int diff = currentHeight - oldHeight_; - int newPosition = oldPosition_ + diff; - - // Keep the scroll bar to the bottom if it hasn't been activated yet. - if (oldPosition_ == 0 && !scroll_area_->verticalScrollBar()->isVisible()) - newPosition = max; - - if (lastMessageDirection_ == TimelineDirection::Top) - scroll_area_->verticalScrollBar()->setValue(newPosition); -} - -void -TimelineView::fetchHistory() -{ - if (!isScrollbarActivated() && !isTimelineFinished) { - if (!isVisible()) - return; - - isPaginationInProgress_ = true; - getMessages(); - paginationTimer_->start(2000); - - return; - } - - paginationTimer_->stop(); -} - -void -TimelineView::scrollDown() -{ - int current = scroll_area_->verticalScrollBar()->value(); - int max = scroll_area_->verticalScrollBar()->maximum(); - - // The first time we enter the room move the scroll bar to the bottom. - if (!isInitialized) { - scroll_area_->verticalScrollBar()->setValue(max); - isInitialized = true; - return; - } - - // If the gap is small enough move the scroll bar down. e.g when a new - // message appears. - if (max - current < SCROLL_BAR_GAP) - scroll_area_->verticalScrollBar()->setValue(max); -} - -void -TimelineView::sliderMoved(int position) -{ - if (!scroll_area_->verticalScrollBar()->isVisible()) - return; - - toggleScrollDownButton(); - - // The scrollbar is high enough so we can start retrieving old events. - if (position < SCROLL_BAR_GAP) { - if (isTimelineFinished) - return; - - // Prevent user from moving up when there is pagination in - // progress. - if (isPaginationInProgress_) - return; - - isPaginationInProgress_ = true; - - getMessages(); - } -} - -bool -TimelineView::isStartOfTimeline(const mtx::responses::Messages &msgs) -{ - return (msgs.chunk.size() == 0 && (msgs.end.empty() || msgs.end == msgs.start)); -} - -void -TimelineView::addBackwardsEvents(const mtx::responses::Messages &msgs) -{ - // We've reached the start of the timline and there're no more messages. - if (isStartOfTimeline(msgs)) { - nhlog::ui()->info("[{}] start of timeline reached, no more messages to fetch", - room_id_.toStdString()); - isTimelineFinished = true; - return; - } - - isTimelineFinished = false; - - // Queue incoming messages to be rendered later. - topMessages_.insert(topMessages_.end(), - std::make_move_iterator(msgs.chunk.begin()), - std::make_move_iterator(msgs.chunk.end())); - - // The RoomList message preview will be updated only if this - // is the first batch of messages received through /messages - // i.e there are no other messages currently present. - if (!topMessages_.empty() && scroll_layout_->count() == 0) - notifyForLastEvent(findFirstViewableEvent(topMessages_)); - - if (isVisible()) { - renderTopEvents(topMessages_); - - // Free up space for new messages. - topMessages_.clear(); - - // Send a read receipt for the last event. - if (isActiveWindow()) - readLastEvent(); - } - - prev_batch_token_ = QString::fromStdString(msgs.end); - isPaginationInProgress_ = false; -} - -QWidget * -TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event, - TimelineDirection direction) -{ - using namespace mtx::events; - - using AudioEvent = RoomEvent; - using EmoteEvent = RoomEvent; - using FileEvent = RoomEvent; - using ImageEvent = RoomEvent; - using NoticeEvent = RoomEvent; - using TextEvent = RoomEvent; - using VideoEvent = RoomEvent; - - if (boost::get>(&event) != nullptr) { - auto redaction_event = boost::get>(event); - const auto event_id = QString::fromStdString(redaction_event.redacts); - - QTimer::singleShot(0, this, [event_id, this]() { - if (eventIds_.contains(event_id)) - removeEvent(event_id); - }); - - return nullptr; - } else if (boost::get>(&event) != nullptr) { - auto msg = boost::get>(event); - auto event_id = QString::fromStdString(msg.event_id); - - if (eventIds_.contains(event_id)) - return nullptr; - - auto item = new InfoMessage(tr("Encryption is enabled"), this); - item->saveDatetime(QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts)); - eventIds_[event_id] = item; - - // Force the next message to have avatar by not providing the current username. - saveMessageInfo("", msg.origin_server_ts, direction); - - return item; - } else if (boost::get>(&event) != nullptr) { - auto audio = boost::get>(event); - return processMessageEvent(audio, direction); - } else if (boost::get>(&event) != nullptr) { - auto emote = boost::get>(event); - return processMessageEvent(emote, direction); - } else if (boost::get>(&event) != nullptr) { - auto file = boost::get>(event); - return processMessageEvent(file, direction); - } else if (boost::get>(&event) != nullptr) { - auto image = boost::get>(event); - return processMessageEvent(image, direction); - } else if (boost::get>(&event) != nullptr) { - auto notice = boost::get>(event); - return processMessageEvent(notice, direction); - } else if (boost::get>(&event) != nullptr) { - auto text = boost::get>(event); - return processMessageEvent(text, direction); - } else if (boost::get>(&event) != nullptr) { - auto video = boost::get>(event); - return processMessageEvent(video, direction); - } else if (boost::get(&event) != nullptr) { - return processMessageEvent(boost::get(event), - direction); - } else if (boost::get>(&event) != nullptr) { - auto res = parseEncryptedEvent(boost::get>(event)); - auto widget = parseMessageEvent(res.event, direction); - - if (widget == nullptr) - return nullptr; - - auto item = qobject_cast(widget); - - if (item && res.isDecrypted) - item->markReceived(true); - else if (item && !res.isDecrypted) - item->addKeyRequestAction(); - - return widget; - } - - return nullptr; -} - -DecryptionResult -TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent &e) -{ - MegolmSessionIndex index; - index.room_id = room_id_.toStdString(); - index.session_id = e.content.session_id; - index.sender_key = e.content.sender_key; - - mtx::events::RoomEvent dummy; - dummy.origin_server_ts = e.origin_server_ts; - dummy.event_id = e.event_id; - dummy.sender = e.sender; - dummy.content.body = - tr("-- Encrypted Event (No keys found for decryption) --", - "Placeholder, when the message was not decrypted yet or can't be decrypted") - .toStdString(); - - try { - if (!cache::client()->inboundMegolmSessionExists(index)) { - nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", - index.room_id, - index.session_id, - e.sender); - // TODO: request megolm session_id & session_key from the sender. - return {dummy, false}; - } - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); - dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --", - "Placeholder, when the message can't be decrypted, because " - "the DB access failed when trying to lookup the session.") - .toStdString(); - return {dummy, false}; - } - - std::string msg_str; - try { - auto session = cache::client()->getInboundMegolmSession(index); - auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (failed to retrieve megolm keys from db) --", - "Placeholder, when the message can't be decrypted, because the DB access " - "failed.") - .toStdString(); - return {dummy, false}; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (%1) --", - "Placeholder, when the message can't be decrypted. In this case, the Olm " - "decrytion returned an error, which is passed ad %1") - .arg(e.what()) - .toStdString(); - return {dummy, false}; - } - - // Add missing fields for the event. - json body = json::parse(msg_str); - body["event_id"] = e.event_id; - body["sender"] = e.sender; - body["origin_server_ts"] = e.origin_server_ts; - body["unsigned"] = e.unsigned_data; - - nhlog::crypto()->debug("decrypted event: {}", e.event_id); - - json event_array = json::array(); - event_array.push_back(body); - - std::vector events; - mtx::responses::utils::parse_timeline_events(event_array, events); - - if (events.size() == 1) - return {events.at(0), true}; - - dummy.content.body = - tr("-- Encrypted Event (Unknown event type) --", - "Placeholder, when the message was decrypted, but we couldn't parse it, because " - "Nheko/mtxclient don't support that event type yet") - .toStdString(); - return {dummy, false}; -} - -void -TimelineView::displayReadReceipts(std::vector events) -{ - QtConcurrent::run( - [events = std::move(events), room_id = room_id_, local_user = local_user_, this]() { - std::vector event_ids; - - for (const auto &e : events) { - if (utils::event_sender(e) == local_user) - event_ids.emplace_back( - QString::fromStdString(utils::event_id(e))); - } - - auto readEvents = - cache::client()->filterReadEvents(room_id, event_ids, local_user.toStdString()); - - if (!readEvents.empty()) - emit markReadEvents(readEvents); - }); -} - -void -TimelineView::renderBottomEvents(const std::vector &events) -{ - int counter = 0; - - for (const auto &event : events) { - QWidget *item = parseMessageEvent(event, TimelineDirection::Bottom); - - if (item != nullptr) { - addTimelineItem(item, TimelineDirection::Bottom); - counter++; - - // Prevent blocking of the event-loop - // by calling processEvents every 10 items we render. - if (counter % 4 == 0) - QApplication::processEvents(); - } - } - - lastMessageDirection_ = TimelineDirection::Bottom; - - displayReadReceipts(events); - - QApplication::processEvents(); -} - -void -TimelineView::renderTopEvents(const std::vector &events) -{ - std::vector items; - - // Reset the sender of the first message in the timeline - // cause we're about to insert a new one. - firstSender_.clear(); - firstMsgTimestamp_ = QDateTime(); - - // Parse in reverse order to determine where we should not show sender's name. - for (auto it = events.rbegin(); it != events.rend(); ++it) { - auto item = parseMessageEvent(*it, TimelineDirection::Top); - - if (item != nullptr) - items.push_back(item); - } - - // Reverse again to render them. - std::reverse(items.begin(), items.end()); - - oldPosition_ = scroll_area_->verticalScrollBar()->value(); - oldHeight_ = scroll_widget_->size().height(); - - for (const auto &item : items) - addTimelineItem(item, TimelineDirection::Top); - - lastMessageDirection_ = TimelineDirection::Top; - - QApplication::processEvents(); - - displayReadReceipts(events); - - // If this batch is the first being rendered (i.e the first and the last - // events originate from this batch), set the last sender. - if (lastSender_.isEmpty() && !items.empty()) { - for (const auto &w : items) { - auto timelineItem = qobject_cast(w); - if (timelineItem) { - saveLastMessageInfo(timelineItem->descriptionMessage().userid, - timelineItem->descriptionMessage().datetime); - break; - } - } - } -} - -void -TimelineView::addEvents(const mtx::responses::Timeline &timeline) -{ - if (isInitialSync) { - prev_batch_token_ = QString::fromStdString(timeline.prev_batch); - isInitialSync = false; - } - - bottomMessages_.insert(bottomMessages_.end(), - std::make_move_iterator(timeline.events.begin()), - std::make_move_iterator(timeline.events.end())); - - if (!bottomMessages_.empty()) - notifyForLastEvent(findLastViewableEvent(bottomMessages_)); - - // If the current timeline is open and there are messages to be rendered. - if (isVisible() && !bottomMessages_.empty()) { - renderBottomEvents(bottomMessages_); - - // Free up space for new messages. - bottomMessages_.clear(); - - // Send a read receipt for the last event. - if (isActiveWindow()) - readLastEvent(); - } -} - -void -TimelineView::init() -{ - local_user_ = utils::localUser(); - - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-arrow-down.png"); - scrollDownBtn_ = new FloatingButton(icon, this); - scrollDownBtn_->hide(); - - connect(scrollDownBtn_, &QPushButton::clicked, this, [this]() { - const int max = scroll_area_->verticalScrollBar()->maximum(); - scroll_area_->verticalScrollBar()->setValue(max); - }); - top_layout_ = new QVBoxLayout(this); - top_layout_->setSpacing(0); - top_layout_->setMargin(0); - - scroll_area_ = new QScrollArea(this); - scroll_area_->setWidgetResizable(true); - scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scroll_widget_ = new QWidget(this); - scroll_widget_->setObjectName("scroll_widget"); - - // Height of the typing display. - QFont f; - f.setPointSizeF(f.pointSizeF() * 0.9); - const int bottomMargin = QFontMetrics(f).height() + 6; - - scroll_layout_ = new QVBoxLayout(scroll_widget_); - scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); - scroll_layout_->setSpacing(0); - scroll_layout_->setObjectName("timelinescrollarea"); - - scroll_area_->setWidget(scroll_widget_); - scroll_area_->setAlignment(Qt::AlignBottom); - - top_layout_->addWidget(scroll_area_); - - setLayout(top_layout_); - - paginationTimer_ = new QTimer(this); - connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory); - - connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents); - - connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage); - connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage); - - connect( - this, &TimelineView::markReadEvents, this, [this](const std::vector &event_ids) { - for (const auto &event : event_ids) { - if (eventIds_.contains(event)) { - auto widget = eventIds_[event]; - if (!widget) - return; - - auto item = qobject_cast(widget); - if (!item) - return; - - item->markRead(); - } - } - }); - - connect(scroll_area_->verticalScrollBar(), - SIGNAL(valueChanged(int)), - this, - SLOT(sliderMoved(int))); - connect(scroll_area_->verticalScrollBar(), - SIGNAL(rangeChanged(int, int)), - this, - SLOT(sliderRangeChanged(int, int))); -} - -void -TimelineView::getMessages() -{ - mtx::http::MessagesOpts opts; - opts.room_id = room_id_.toStdString(); - opts.from = prev_batch_token_.toStdString(); - - http::client()->messages( - opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error("failed to call /messages ({}): {} - {}", - opts.room_id, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - emit messagesRetrieved(std::move(res)); - }); -} - -void -TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction) -{ - if (direction == TimelineDirection::Bottom) - lastSender_ = user_id; - else - firstSender_ = user_id; -} - -bool -TimelineView::isSenderRendered(const QString &user_id, - uint64_t origin_server_ts, - TimelineDirection direction) -{ - if (direction == TimelineDirection::Bottom) { - return (lastSender_ != user_id) || - isDateDifference(lastMsgTimestamp_, - QDateTime::fromMSecsSinceEpoch(origin_server_ts)); - } else { - return (firstSender_ != user_id) || - isDateDifference(firstMsgTimestamp_, - QDateTime::fromMSecsSinceEpoch(origin_server_ts)); - } -} - -void -TimelineView::addTimelineItem(QWidget *item, TimelineDirection direction) -{ - const auto newDate = getDate(item); - - if (direction == TimelineDirection::Bottom) { - QWidget *lastItem = nullptr; - int lastItemPosition = 0; - - if (scroll_layout_->count() > 0) { - lastItemPosition = scroll_layout_->count() - 1; - lastItem = scroll_layout_->itemAt(lastItemPosition)->widget(); - } - - if (lastItem) { - const auto oldDate = getDate(lastItem); - - if (oldDate.daysTo(newDate) != 0) { - auto separator = new DateSeparator(newDate, this); - - if (separator) - pushTimelineItem(separator, direction); - } - } - - pushTimelineItem(item, direction); - } else { - if (scroll_layout_->count() > 0) { - const auto firstItem = scroll_layout_->itemAt(0)->widget(); - - if (firstItem) { - const auto oldDate = getDate(firstItem); - - if (newDate.daysTo(oldDate) != 0) { - auto separator = new DateSeparator(oldDate); - - if (separator) - pushTimelineItem(separator, direction); - } - } - } - - pushTimelineItem(item, direction); - } -} - -void -TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id) -{ - nhlog::ui()->debug("[{}] message was received by the server", txn_id); - if (!pending_msgs_.isEmpty() && - pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet - auto msg = pending_msgs_.dequeue(); - msg.event_id = event_id; - - if (msg.widget) { - msg.widget->setEventId(event_id); - eventIds_[event_id] = msg.widget; - - // If the response comes after we have received the event from sync - // we've already marked the widget as received. - if (!msg.widget->isReceived()) { - msg.widget->markReceived(msg.is_encrypted); - cache::client()->addPendingReceipt(room_id_, event_id); - pending_sent_msgs_.append(msg); - } - } else { - nhlog::ui()->warn("[{}] received message response for invalid widget", - txn_id); - } - } - - sendNextPendingMessage(); -} - -void -TimelineView::addUserMessage(mtx::events::MessageType ty, - const QString &body, - const RelatedInfo &related = RelatedInfo()) -{ - auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); - - QString full_body; - if (related.related_event.empty()) { - full_body = body; - } else { - full_body = utils::getFormattedQuoteBody(related, body); - } - TimelineItem *view_item = - new TimelineItem(ty, local_user_, full_body, with_sender, room_id_, scroll_widget_); - - PendingMessage message; - message.ty = ty; - message.txn_id = http::client()->generate_txn_id(); - message.body = body; - message.related = related; - message.widget = view_item; - - try { - message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check encryption status of room {}", e.what()); - view_item->deleteLater(); - - // TODO: Send a notification to the user. - - return; - } - - addTimelineItem(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - handleNewUserMessage(message); -} - -void -TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) -{ - addUserMessage(ty, body, RelatedInfo()); -} - -void -TimelineView::handleNewUserMessage(PendingMessage msg) -{ - pending_msgs_.enqueue(msg); - if (pending_msgs_.size() == 1 && pending_sent_msgs_.isEmpty()) - sendNextPendingMessage(); -} - -void -TimelineView::sendNextPendingMessage() -{ - if (pending_msgs_.size() == 0) - return; - - using namespace mtx::events; - - PendingMessage &m = pending_msgs_.head(); - - nhlog::ui()->debug("[{}] sending next queued message", m.txn_id); - - if (m.widget) - m.widget->markSent(); - - if (m.is_encrypted) { - nhlog::ui()->debug("[{}] sending encrypted event", m.txn_id); - prepareEncryptedMessage(std::move(m)); - return; - } - - switch (m.ty) { - case mtx::events::MessageType::Audio: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Image: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Video: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::File: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Text: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Emote: { - http::client()->send_room_message( - room_id_.toStdString(), - m.txn_id, - toRoomMessage(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - break; - } - default: - nhlog::ui()->warn("cannot send unknown message type: {}", m.body.toStdString()); - break; - } -} - -void -TimelineView::notifyForLastEvent() -{ - if (scroll_layout_->count() == 0) { - nhlog::ui()->error("notifyForLastEvent called with empty timeline"); - return; - } - - auto lastItem = scroll_layout_->itemAt(scroll_layout_->count() - 1); - - if (!lastItem) - return; - - auto *lastTimelineItem = qobject_cast(lastItem->widget()); - - if (lastTimelineItem) - emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage()); - else - nhlog::ui()->warn("cast to TimelineItem failed: {}", room_id_.toStdString()); -} - -void -TimelineView::notifyForLastEvent(const TimelineEvent &event) -{ - auto descInfo = utils::getMessageDescription(event, local_user_, room_id_); - - if (!descInfo.timestamp.isEmpty()) - emit updateLastTimelineMessage(room_id_, descInfo); -} - -bool -TimelineView::isPendingMessage(const std::string &txn_id, - const QString &sender, - const QString &local_userid) -{ - if (sender != local_userid) - return false; - - auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; }; - - return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) || - std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid); -} - -void -TimelineView::removePendingMessage(const std::string &txn_id) -{ - if (txn_id.empty()) - return; - - for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) { - if (it->txn_id == txn_id) { - int index = std::distance(pending_sent_msgs_.begin(), it); - pending_sent_msgs_.removeAt(index); - - if (pending_sent_msgs_.isEmpty()) - sendNextPendingMessage(); - - nhlog::ui()->debug("[{}] removed message with sync", txn_id); - } - } - for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) { - if (it->txn_id == txn_id) { - if (it->widget) { - it->widget->markReceived(it->is_encrypted); - - // TODO: update when a solution for encrypted messages is available. - if (!it->is_encrypted) - cache::client()->addPendingReceipt(room_id_, it->event_id); - } - - nhlog::ui()->debug("[{}] received sync before message response", txn_id); - return; - } - } -} - -void -TimelineView::handleFailedMessage(const std::string &txn_id) -{ - Q_UNUSED(txn_id); - // Note: We do this even if the message has already been echoed. - QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage())); -} - -void -TimelineView::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -TimelineView::readLastEvent() const -{ - if (!ChatPage::instance()->userSettings()->isReadReceiptsEnabled()) - return; - - const auto eventId = getLastEventId(); - - if (!eventId.isEmpty()) - http::client()->read_event(room_id_.toStdString(), - eventId.toStdString(), - [this, eventId](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read event ({}, {})", - room_id_.toStdString(), - eventId.toStdString()); - } - }); -} - -QString -TimelineView::getLastEventId() const -{ - auto index = scroll_layout_->count(); - - // Search backwards for the first event that has a valid event id. - while (index > 0) { - --index; - - auto lastItem = scroll_layout_->itemAt(index); - auto *lastTimelineItem = qobject_cast(lastItem->widget()); - - if (lastTimelineItem && !lastTimelineItem->eventId().isEmpty()) - return lastTimelineItem->eventId(); - } - - return QString(""); -} - -void -TimelineView::showEvent(QShowEvent *event) -{ - if (!topMessages_.empty()) { - renderTopEvents(topMessages_); - topMessages_.clear(); - } - - if (!bottomMessages_.empty()) { - renderBottomEvents(bottomMessages_); - bottomMessages_.clear(); - scrollDown(); - } - - toggleScrollDownButton(); - - readLastEvent(); - - QWidget::showEvent(event); -} - -void -TimelineView::hideEvent(QHideEvent *event) -{ - const auto handleHeight = scroll_area_->verticalScrollBar()->sizeHint().height(); - const auto widgetsNum = scroll_layout_->count(); - - // Remove widgets from the timeline to reduce the memory footprint. - if (handleHeight < MIN_SCROLLBAR_HANDLE && widgetsNum > MAX_RETAINED_WIDGETS) - clearTimeline(); - - QWidget::hideEvent(event); -} - -bool -TimelineView::event(QEvent *event) -{ - if (event->type() == QEvent::WindowActivate) - readLastEvent(); - - return QWidget::event(event); -} - -void -TimelineView::clearTimeline() -{ - // Delete all widgets. - QLayoutItem *item; - while ((item = scroll_layout_->takeAt(0)) != nullptr) { - delete item->widget(); - delete item; - } - - // The next call to /messages will be without a prev token. - prev_batch_token_.clear(); - eventIds_.clear(); - - // Clear queues with pending messages to be rendered. - bottomMessages_.clear(); - topMessages_.clear(); - - firstSender_.clear(); - lastSender_.clear(); -} - -void -TimelineView::toggleScrollDownButton() -{ - const int maxScroll = scroll_area_->verticalScrollBar()->maximum(); - const int currentScroll = scroll_area_->verticalScrollBar()->value(); - - if (maxScroll - currentScroll > SCROLL_BAR_GAP) { - scrollDownBtn_->show(); - scrollDownBtn_->raise(); - } else { - scrollDownBtn_->hide(); - } -} - -void -TimelineView::removeEvent(const QString &event_id) -{ - if (!eventIds_.contains(event_id)) { - nhlog::ui()->warn("cannot remove widget with unknown event_id: {}", - event_id.toStdString()); - return; - } - - auto removedItem = eventIds_[event_id]; - - // Find the next and the previous widgets in the timeline - auto prevWidget = relativeWidget(removedItem, -1); - auto nextWidget = relativeWidget(removedItem, 1); - - // See if they are timeline items - auto prevItem = qobject_cast(prevWidget); - auto nextItem = qobject_cast(nextWidget); - - // ... or a date separator - auto prevLabel = qobject_cast(prevWidget); - - // If it's a TimelineItem add an avatar. - if (prevItem) { - prevItem->addAvatar(); - } - - if (nextItem) { - nextItem->addAvatar(); - } else if (prevLabel) { - // If there's no chat message after this, and we have a label before us, delete the - // label. - prevLabel->deleteLater(); - } - - // If we deleted the last item in the timeline... - if (!nextItem && prevItem) - saveLastMessageInfo(prevItem->descriptionMessage().userid, - prevItem->descriptionMessage().datetime); - - // If we deleted the first item in the timeline... - if (!prevItem && nextItem) - saveFirstMessageInfo(nextItem->descriptionMessage().userid, - nextItem->descriptionMessage().datetime); - - // If we deleted the only item in the timeline... - if (!prevItem && !nextItem) { - firstSender_.clear(); - firstMsgTimestamp_ = QDateTime(); - lastSender_.clear(); - lastMsgTimestamp_ = QDateTime(); - } - - // Finally remove the event. - removedItem->deleteLater(); - eventIds_.remove(event_id); - - // Update the room list with a view of the last message after - // all events have been processed. - QTimer::singleShot(0, this, [this]() { notifyForLastEvent(); }); -} - -QWidget * -TimelineView::relativeWidget(QWidget *item, int dt) const -{ - int pos = scroll_layout_->indexOf(item); - - if (pos == -1) - return nullptr; - - pos = pos + dt; - - bool isOutOfBounds = (pos < 0 || pos > scroll_layout_->count() - 1); - - return isOutOfBounds ? nullptr : scroll_layout_->itemAt(pos)->widget(); -} - -TimelineEvent -TimelineView::findFirstViewableEvent(const std::vector &events) -{ - auto it = std::find_if(events.begin(), events.end(), [](const auto &event) { - return mtx::events::EventType::RoomMessage == utils::event_type(event); - }); - - return (it == std::end(events)) ? events.front() : *it; -} - -TimelineEvent -TimelineView::findLastViewableEvent(const std::vector &events) -{ - auto it = std::find_if(events.rbegin(), events.rend(), [](const auto &event) { - return (mtx::events::EventType::RoomMessage == utils::event_type(event)) || - (mtx::events::EventType::RoomEncrypted == utils::event_type(event)); - }); - - return (it == std::rend(events)) ? events.back() : *it; -} - -void -TimelineView::saveMessageInfo(const QString &sender, - uint64_t origin_server_ts, - TimelineDirection direction) -{ - updateLastSender(sender, direction); - - if (direction == TimelineDirection::Bottom) - lastMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts); - else - firstMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts); -} - -bool -TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second) const -{ - // Check if the dates are in a different day. - if (std::abs(first.daysTo(second)) != 0) - return true; - - const uint64_t diffInSeconds = std::abs(first.msecsTo(second)) / 1000; - constexpr uint64_t fifteenMins = 15 * 60; - - return diffInSeconds > fifteenMins; -} - -void -TimelineView::sendRoomMessageHandler(const std::string &txn_id, - const mtx::responses::EventId &res, - mtx::http::RequestErr err) -{ - if (err) { - const int status_code = static_cast(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(txn_id); - return; - } - - emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string())); -} - -template<> -mtx::events::msg::Audio -toRoomMessage(const PendingMessage &m) -{ - mtx::events::msg::Audio audio; - audio.info.mimetype = m.mime.toStdString(); - audio.info.size = m.media_size; - audio.body = m.filename.toStdString(); - audio.url = m.body.toStdString(); - return audio; -} - -template<> -mtx::events::msg::Image -toRoomMessage(const PendingMessage &m) -{ - mtx::events::msg::Image image; - image.info.mimetype = m.mime.toStdString(); - image.info.size = m.media_size; - image.body = m.filename.toStdString(); - image.url = m.body.toStdString(); - image.info.h = m.dimensions.height(); - image.info.w = m.dimensions.width(); - return image; -} - -template<> -mtx::events::msg::Video -toRoomMessage(const PendingMessage &m) -{ - mtx::events::msg::Video video; - video.info.mimetype = m.mime.toStdString(); - video.info.size = m.media_size; - video.body = m.filename.toStdString(); - video.url = m.body.toStdString(); - return video; -} - -template<> -mtx::events::msg::Emote -toRoomMessage(const PendingMessage &m) -{ - auto html = utils::markdownToHtml(m.body); - - mtx::events::msg::Emote emote; - emote.body = m.body.trimmed().toStdString(); - - if (html != m.body.trimmed().toHtmlEscaped()) - emote.formatted_body = html.toStdString(); - - return emote; -} - -template<> -mtx::events::msg::File -toRoomMessage(const PendingMessage &m) -{ - mtx::events::msg::File file; - file.info.mimetype = m.mime.toStdString(); - file.info.size = m.media_size; - file.body = m.filename.toStdString(); - file.url = m.body.toStdString(); - return file; -} - -template<> -mtx::events::msg::Text -toRoomMessage(const PendingMessage &m) -{ - auto html = utils::markdownToHtml(m.body); - - mtx::events::msg::Text text; - - text.body = m.body.trimmed().toStdString(); - - if (html != m.body.trimmed().toHtmlEscaped()) { - if (!m.related.quoted_body.isEmpty()) { - text.formatted_body = - utils::getFormattedQuoteBody(m.related, html).toStdString(); - } else { - text.formatted_body = html.toStdString(); - } - } - - if (!m.related.related_event.empty()) { - text.relates_to.in_reply_to.event_id = m.related.related_event; - } - - return text; -} - -void -TimelineView::prepareEncryptedMessage(const PendingMessage &msg) -{ - const auto room_id = room_id_.toStdString(); - - using namespace mtx::events; - using namespace mtx::identifiers; - - json content; - - // Serialize the message to the plaintext that will be encrypted. - switch (msg.ty) { - case MessageType::Audio: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::Emote: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::File: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::Image: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::Text: { - content = json(toRoomMessage(msg)); - break; - } - case MessageType::Video: { - content = json(toRoomMessage(msg)); - break; - } - default: - break; - } - - json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; - - try { - // Check if we have already an outbound megolm session then we can use. - if (cache::client()->outboundMegolmSessionExists(room_id)) { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client()->send_room_message( - room_id, - msg.txn_id, - data, - std::bind(&TimelineView::sendRoomMessageHandler, - this, - msg.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - return; - } - - nhlog::ui()->debug("creating new outbound megolm session"); - - // Create a new outbound megolm session. - auto outbound_session = olm::client()->init_outbound_group_session(); - const auto session_id = mtx::crypto::session_id(outbound_session.get()); - const auto session_key = mtx::crypto::session_key(outbound_session.get()); - - // TODO: needs to be moved in the lib. - auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, - {"room_id", room_id}, - {"session_id", session_id}, - {"session_key", session_key}}; - - // Saving the new megolm session. - // TODO: Maybe it's too early to save. - OutboundGroupSessionData session_data; - session_data.session_id = session_id; - session_data.session_key = session_key; - session_data.message_index = 0; // TODO Update me - cache::client()->saveOutboundMegolmSession( - room_id, session_data, std::move(outbound_session)); - - const auto members = cache::client()->roomMembers(room_id); - nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); - - auto keeper = std::make_shared( - [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() { - try { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client() - ->send_room_message( - room_id, - txn_id, - data, - std::bind(&TimelineView::sendRoomMessageHandler, - this, - txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to save megolm outbound session: {}", e.what()); - } - }); - - mtx::requests::QueryKeys req; - for (const auto &member : members) - req.device_keys[member] = {}; - - http::client()->query_keys( - req, - [keeper = std::move(keeper), megolm_payload, this]( - const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to query device keys: {} {}", - err->matrix_error.error, - static_cast(err->status_code)); - // TODO: Mark the event as failed. Communicate with the UI. - return; - } - - for (const auto &user : res.device_keys) { - // Mapping from a device_id with valid identity keys to the - // generated room_key event used for sharing the megolm session. - std::map room_key_msgs; - std::map deviceKeys; - - room_key_msgs.clear(); - deviceKeys.clear(); - - for (const auto &dev : user.second) { - const auto user_id = UserId(dev.second.user_id); - const auto device_id = DeviceId(dev.second.device_id); - - const auto device_keys = dev.second.keys; - const auto curveKey = "curve25519:" + device_id.get(); - const auto edKey = "ed25519:" + device_id.get(); - - if ((device_keys.find(curveKey) == device_keys.end()) || - (device_keys.find(edKey) == device_keys.end())) { - nhlog::net()->debug( - "ignoring malformed keys for device {}", - device_id.get()); - continue; - } - - DevicePublicKeys pks; - pks.ed25519 = device_keys.at(edKey); - pks.curve25519 = device_keys.at(curveKey); - - try { - if (!mtx::crypto::verify_identity_signature( - json(dev.second), device_id, user_id)) { - nhlog::crypto()->warn( - "failed to verify identity keys: {}", - json(dev.second).dump(2)); - continue; - } - } catch (const json::exception &e) { - nhlog::crypto()->warn( - "failed to parse device key json: {}", - e.what()); - continue; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->warn( - "failed to verify device key json: {}", - e.what()); - continue; - } - - auto room_key = olm::client() - ->create_room_key_event( - user_id, pks.ed25519, megolm_payload) - .dump(); - - room_key_msgs.emplace(device_id, room_key); - deviceKeys.emplace(device_id, pks); - } - - std::vector valid_devices; - valid_devices.reserve(room_key_msgs.size()); - for (auto const &d : room_key_msgs) { - valid_devices.push_back(d.first); - - nhlog::net()->info("{}", d.first); - nhlog::net()->info(" curve25519 {}", - deviceKeys.at(d.first).curve25519); - nhlog::net()->info(" ed25519 {}", - deviceKeys.at(d.first).ed25519); - } - - nhlog::net()->info( - "sending claim request for user {} with {} devices", - user.first, - valid_devices.size()); - - http::client()->claim_keys( - user.first, - valid_devices, - std::bind(&TimelineView::handleClaimedKeys, - this, - keeper, - room_key_msgs, - deviceKeys, - user.first, - std::placeholders::_1, - std::placeholders::_2)); - - // TODO: Wait before sending the next batch of requests. - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - }); - - // TODO: Let the user know about the errors. - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } -} - -void -TimelineView::handleClaimedKeys(std::shared_ptr keeper, - const std::map &room_keys, - const std::map &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err) -{ - if (err) { - nhlog::net()->warn("claim keys error: {} {} {}", - err->matrix_error.error, - err->parse_error, - static_cast(err->status_code)); - return; - } - - nhlog::net()->debug("claimed keys for {}", user_id); - - if (res.one_time_keys.size() == 0) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - auto retrieved_devices = res.one_time_keys.at(user_id); - - // Payload with all the to_device message to be sent. - json body; - body["messages"][user_id] = json::object(); - - for (const auto &rd : retrieved_devices) { - const auto device_id = rd.first; - nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); - - // TODO: Verify signatures - auto otk = rd.second.begin()->at("key"); - - if (pks.find(device_id) == pks.end()) { - nhlog::net()->critical("couldn't find public key for device: {}", - device_id); - continue; - } - - auto id_key = pks.at(device_id).curve25519; - auto s = olm::client()->create_outbound_session(id_key, otk); - - if (room_keys.find(device_id) == room_keys.end()) { - nhlog::net()->critical("couldn't find m.room_key for device: {}", - device_id); - continue; - } - - auto device_msg = olm::client()->create_olm_encrypted_content( - s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); - - try { - cache::client()->saveOlmSession(id_key, std::move(s)); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to pickle outbound olm session: {}", - e.what()); - } - - body["messages"][user_id][device_id] = device_msg; - } - - nhlog::net()->info("send_to_device: {}", user_id); - - http::client()->send_to_device( - "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - - (void)keeper; - }); -} diff --git a/src/timeline/TimelineView.h b/src/timeline/TimelineView.h deleted file mode 100644 index 35796efd..00000000 --- a/src/timeline/TimelineView.h +++ /dev/null @@ -1,449 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "../Utils.h" -#include "MatrixClient.h" -#include "timeline/TimelineItem.h" - -class StateKeeper -{ -public: - StateKeeper(std::function &&fn) - : fn_(std::move(fn)) - {} - - ~StateKeeper() { fn_(); } - -private: - std::function fn_; -}; - -struct DecryptionResult -{ - //! The decrypted content as a normal plaintext event. - utils::TimelineEvent event; - //! Whether or not the decryption was successful. - bool isDecrypted = false; -}; - -class FloatingButton; -struct DescInfo; - -// Contains info about a message shown in the history view -// but not yet confirmed by the homeserver through sync. -struct PendingMessage -{ - mtx::events::MessageType ty; - std::string txn_id; - RelatedInfo related; - QString body; - QString filename; - QString mime; - uint64_t media_size; - QString event_id; - TimelineItem *widget; - QSize dimensions; - bool is_encrypted = false; -}; - -template -MessageT -toRoomMessage(const PendingMessage &) = delete; - -template<> -mtx::events::msg::Audio -toRoomMessage(const PendingMessage &m); - -template<> -mtx::events::msg::Emote -toRoomMessage(const PendingMessage &m); - -template<> -mtx::events::msg::File -toRoomMessage(const PendingMessage &); - -template<> -mtx::events::msg::Image -toRoomMessage(const PendingMessage &m); - -template<> -mtx::events::msg::Text -toRoomMessage(const PendingMessage &); - -template<> -mtx::events::msg::Video -toRoomMessage(const PendingMessage &m); - -// In which place new TimelineItems should be inserted. -enum class TimelineDirection -{ - Top, - Bottom, -}; - -class TimelineView : public QWidget -{ - Q_OBJECT - -public: - TimelineView(const mtx::responses::Timeline &timeline, - const QString &room_id, - QWidget *parent = 0); - TimelineView(const QString &room_id, QWidget *parent = 0); - - // Add new events at the end of the timeline. - void addEvents(const mtx::responses::Timeline &timeline); - void addUserMessage(mtx::events::MessageType ty, - const QString &body, - const RelatedInfo &related); - void addUserMessage(mtx::events::MessageType ty, const QString &msg); - - template - void addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions = QSize()); - void updatePendingMessage(const std::string &txn_id, const QString &event_id); - void scrollDown(); - - //! Remove an item from the timeline with the given Event ID. - void removeEvent(const QString &event_id); - void setPrevBatchToken(const QString &token) { prev_batch_token_ = token; } - -public slots: - void sliderRangeChanged(int min, int max); - void sliderMoved(int position); - void fetchHistory(); - - // Add old events at the top of the timeline. - void addBackwardsEvents(const mtx::responses::Messages &msgs); - - // Whether or not the initial batch has been loaded. - bool hasLoaded() { return scroll_layout_->count() > 0 || isTimelineFinished; } - - void handleFailedMessage(const std::string &txn_id); - -private slots: - void sendNextPendingMessage(); - -signals: - void updateLastTimelineMessage(const QString &user, const DescInfo &info); - void messagesRetrieved(const mtx::responses::Messages &res); - void messageFailed(const std::string &txn_id); - void messageSent(const std::string &txn_id, const QString &event_id); - void markReadEvents(const std::vector &event_ids); - -protected: - void paintEvent(QPaintEvent *event) override; - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - bool event(QEvent *event) override; - -private: - using TimelineEvent = mtx::events::collections::TimelineEvents; - - //! Mark our own widgets as read if they have more than one receipt. - void displayReadReceipts(std::vector events); - //! Determine if the start of the timeline is reached from the response of /messages. - bool isStartOfTimeline(const mtx::responses::Messages &msgs); - - QWidget *relativeWidget(QWidget *item, int dt) const; - - DecryptionResult parseEncryptedEvent( - const mtx::events::EncryptedEvent &e); - - void handleClaimedKeys(std::shared_ptr keeper, - const std::map &room_key, - const std::map &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err); - - //! Callback for all message sending. - void sendRoomMessageHandler(const std::string &txn_id, - const mtx::responses::EventId &res, - mtx::http::RequestErr err); - void prepareEncryptedMessage(const PendingMessage &msg); - - //! Call the /messages endpoint to fill the timeline. - void getMessages(); - //! HACK: Fixing layout flickering when adding to the bottom - //! of the timeline. - void pushTimelineItem(QWidget *item, TimelineDirection dir) - { - setUpdatesEnabled(false); - item->hide(); - - if (dir == TimelineDirection::Top) - scroll_layout_->insertWidget(0, item); - else - scroll_layout_->addWidget(item); - - QTimer::singleShot(0, this, [item, this]() { - item->show(); - item->adjustSize(); - setUpdatesEnabled(true); - }); - } - - //! Decides whether or not to show or hide the scroll down button. - void toggleScrollDownButton(); - void init(); - void addTimelineItem(QWidget *item, - TimelineDirection direction = TimelineDirection::Bottom); - void updateLastSender(const QString &user_id, TimelineDirection direction); - void notifyForLastEvent(); - void notifyForLastEvent(const TimelineEvent &event); - //! Keep track of the sender and the timestamp of the current message. - void saveLastMessageInfo(const QString &sender, const QDateTime &datetime) - { - lastSender_ = sender; - lastMsgTimestamp_ = datetime; - } - void saveFirstMessageInfo(const QString &sender, const QDateTime &datetime) - { - firstSender_ = sender; - firstMsgTimestamp_ = datetime; - } - //! Keep track of the sender and the timestamp of the current message. - void saveMessageInfo(const QString &sender, - uint64_t origin_server_ts, - TimelineDirection direction); - - TimelineEvent findFirstViewableEvent(const std::vector &events); - TimelineEvent findLastViewableEvent(const std::vector &events); - - //! Mark the last event as read. - void readLastEvent() const; - //! Whether or not the scrollbar is visible (non-zero height). - bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; } - //! Retrieve the event id of the last item. - QString getLastEventId() const; - - template - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // TODO: Remove this eventually. - template - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // For events with custom display widgets. - template - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // For events without custom display widgets. - // TODO: All events should have custom widgets. - template - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // Used to determine whether or not we should prefix a message with the - // sender's name. - bool isSenderRendered(const QString &user_id, - uint64_t origin_server_ts, - TimelineDirection direction); - - bool isPendingMessage(const std::string &txn_id, - const QString &sender, - const QString &userid); - void removePendingMessage(const std::string &txn_id); - - bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); } - - void handleNewUserMessage(PendingMessage msg); - bool isDateDifference(const QDateTime &first, - const QDateTime &second = QDateTime::currentDateTime()) const; - - // Return nullptr if the event couldn't be parsed. - QWidget *parseMessageEvent(const mtx::events::collections::TimelineEvents &event, - TimelineDirection direction); - - //! Store the event id associated with the given widget. - void saveEventId(QWidget *widget); - //! Remove all widgets from the timeline layout. - void clearTimeline(); - - QVBoxLayout *top_layout_; - QVBoxLayout *scroll_layout_; - - QScrollArea *scroll_area_; - QWidget *scroll_widget_; - - QString firstSender_; - QDateTime firstMsgTimestamp_; - QString lastSender_; - QDateTime lastMsgTimestamp_; - - QString room_id_; - QString prev_batch_token_; - QString local_user_; - - bool isPaginationInProgress_ = false; - - // Keeps track whether or not the user has visited the view. - bool isInitialized = false; - bool isTimelineFinished = false; - bool isInitialSync = true; - - const int SCROLL_BAR_GAP = 200; - - QTimer *paginationTimer_; - - int scroll_height_ = 0; - int previous_max_height_ = 0; - - int oldPosition_; - int oldHeight_; - - FloatingButton *scrollDownBtn_; - - TimelineDirection lastMessageDirection_; - - //! Messages received by sync not added to the timeline. - std::vector bottomMessages_; - //! Messages received by /messages not added to the timeline. - std::vector topMessages_; - - //! Render the given timeline events to the bottom of the timeline. - void renderBottomEvents(const std::vector &events); - //! Render the given timeline events to the top of the timeline. - void renderTopEvents(const std::vector &events); - - // The events currently rendered. Used for duplicate detection. - QMap eventIds_; - QQueue pending_msgs_; - QList pending_sent_msgs_; -}; - -template -void -TimelineView::addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions) -{ - auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); - auto trimmed = QFileInfo{filename}.fileName(); // Trim file path. - - auto widget = new Widget(url, trimmed, size, this); - - TimelineItem *view_item = - new TimelineItem(widget, local_user_, with_sender, room_id_, scroll_widget_); - - addTimelineItem(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - // Keep track of the sender and the timestamp of the current message. - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - - PendingMessage message; - message.ty = MsgType; - message.txn_id = http::client()->generate_txn_id(); - message.body = url; - message.filename = trimmed; - message.mime = mime; - message.media_size = size; - message.widget = view_item; - message.dimensions = dimensions; - - handleNewUserMessage(message); -} - -template -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - TimelineItem *item = new TimelineItem(event, withSender, room_id_, scroll_widget_); - return item; -} - -template -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - auto eventWidget = new Widget(event); - auto item = new TimelineItem(eventWidget, event, withSender, room_id_, scroll_widget_); - - return item; -} - -template -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} - -template -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 86505481..d733ad90 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -1,340 +1,400 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include - -#include -#include -#include - -#include "Cache.h" +#include "TimelineViewManager.h" + +#include +#include +#include +#include +#include +#include + +#include "ChatPage.h" +#include "ColorImageProvider.h" +#include "DelegateChooser.h" #include "Logging.h" -#include "Utils.h" -#include "timeline/TimelineView.h" -#include "timeline/TimelineViewManager.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -TimelineViewManager::TimelineViewManager(QWidget *parent) - : QStackedWidget(parent) -{} +#include "MxcImageProvider.h" +#include "UserSettingsPage.h" +#include "dialogs/ImageOverlay.h" void -TimelineViewManager::updateReadReceipts(const QString &room_id, - const std::vector &event_ids) +TimelineViewManager::updateColorPalette() { - if (timelineViewExists(room_id)) { - auto view = views_[room_id]; - if (view) - emit view->markReadEvents(event_ids); + UserSettings settings; + if (settings.theme() == "light") { + QPalette lightActive(/*windowText*/ QColor("#333"), + /*button*/ QColor("#333"), + /*light*/ QColor(), + /*dark*/ QColor(220, 220, 220, 120), + /*mid*/ QColor(), + /*text*/ QColor("#333"), + /*bright_text*/ QColor(), + /*base*/ QColor("white"), + /*window*/ QColor("white")); + view->rootContext()->setContextProperty("currentActivePalette", lightActive); + view->rootContext()->setContextProperty("currentInactivePalette", lightActive); + } else if (settings.theme() == "dark") { + QPalette darkActive(/*windowText*/ QColor("#caccd1"), + /*button*/ QColor("#caccd1"), + /*light*/ QColor(), + /*dark*/ QColor(45, 49, 57, 120), + /*mid*/ QColor(), + /*text*/ QColor("#caccd1"), + /*bright_text*/ QColor(), + /*base*/ QColor("#202228"), + /*window*/ QColor("#202228")); + darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9")); + view->rootContext()->setContextProperty("currentActivePalette", darkActive); + view->rootContext()->setContextProperty("currentInactivePalette", darkActive); + } else { + view->rootContext()->setContextProperty("currentActivePalette", QPalette()); + view->rootContext()->setContextProperty("currentInactivePalette", nullptr); } } -void -TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id) +TimelineViewManager::TimelineViewManager(QWidget *parent) + : imgProvider(new MxcImageProvider()) + , colorImgProvider(new ColorImageProvider()) { - auto view = views_[room_id]; - - if (view) - view->removeEvent(event_id); + qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, + "com.github.nheko", + 1, + 0, + "MtxEvent", + "Can't instantiate enum!"); + qmlRegisterType("com.github.nheko", 1, 0, "DelegateChoice"); + qmlRegisterType("com.github.nheko", 1, 0, "DelegateChooser"); + +#ifdef USE_QUICK_VIEW + view = new QQuickView(); + container = QWidget::createWindowContainer(view, parent); +#else + view = new QQuickWidget(parent); + container = view; + view->setResizeMode(QQuickWidget::SizeRootObjectToView); + container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { + nhlog::ui()->debug("Status changed to {}", status); + }); +#endif + container->setMinimumSize(200, 200); + view->rootContext()->setContextProperty("timelineManager", this); + updateColorPalette(); + view->engine()->addImageProvider("MxcImage", imgProvider); + view->engine()->addImageProvider("colorimage", colorImgProvider); + view->setSource(QUrl("qrc:///qml/TimelineView.qml")); + + connect(dynamic_cast(parent), + &ChatPage::themeChanged, + this, + &TimelineViewManager::updateColorPalette); } void -TimelineViewManager::queueTextMessage(const QString &msg) +TimelineViewManager::sync(const mtx::responses::Rooms &rooms) { - if (active_room_.isEmpty()) - return; - - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Text, msg); + for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { + // addRoom will only add the room, if it doesn't exist + addRoom(QString::fromStdString(it->first)); + models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline); + } } void -TimelineViewManager::queueEmoteMessage(const QString &msg) +TimelineViewManager::addRoom(const QString &room_id) { - if (active_room_.isEmpty()) - return; - - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Emote, msg); + if (!models.contains(room_id)) + models.insert(room_id, + QSharedPointer(new TimelineModel(this, room_id))); } void -TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) +TimelineViewManager::setHistoryView(const QString &room_id) { - if (active_room_.isEmpty()) - return; + nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); - auto room_id = active_room_; - auto view = views_[room_id]; - - view->addUserMessage(mtx::events::MessageType::Text, reply, related); + auto room = models.find(room_id); + if (room != models.end()) { + timeline_ = room.value().data(); + emit activeTimelineChanged(timeline_); + nhlog::ui()->info("Activated room {}", room_id.toStdString()); + } } void -TimelineViewManager::queueImageMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size, - const QSize &dimensions) +TimelineViewManager::openImageOverlay(QString mxcUrl, + QString originalFilename, + QString mimeType, + qml_mtx_events::EventType eventType) const { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("Cannot send m.image message to a non-managed view"); - return; - } - - auto view = views_[roomid]; - - view->addUserMessage( - url, filename, mime, size, dimensions); + QQuickImageResponse *imgResponse = + imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize()); + connect(imgResponse, + &QQuickImageResponse::finished, + this, + [this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() { + if (!imgResponse->errorString().isEmpty()) { + nhlog::ui()->error("Error when retrieving image for overlay: {}", + imgResponse->errorString().toStdString()); + return; + } + auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); + + auto imgDialog = new dialogs::ImageOverlay(pixmap); + imgDialog->show(); + connect(imgDialog, + &dialogs::ImageOverlay::saving, + this, + [this, mxcUrl, originalFilename, mimeType, eventType]() { + saveMedia(mxcUrl, originalFilename, mimeType, eventType); + }); + }); } void -TimelineViewManager::queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::saveMedia(QString mxcUrl, + QString originalFilename, + QString mimeType, + qml_mtx_events::EventType eventType) const { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.file message to a non-managed view"); - return; + QString dialogTitle; + if (eventType == qml_mtx_events::EventType::ImageMessage) { + dialogTitle = tr("Save image"); + } else if (eventType == qml_mtx_events::EventType::VideoMessage) { + dialogTitle = tr("Save video"); + } else if (eventType == qml_mtx_events::EventType::AudioMessage) { + dialogTitle = tr("Save audio"); + } else { + dialogTitle = tr("Save file"); } - auto view = views_[roomid]; + QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); + + auto filename = + QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString); + + if (filename.isEmpty()) + return; - view->addUserMessage(url, filename, mime, size); + const auto url = mxcUrl.toStdString(); + + http::client()->download( + url, + [filename, url](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(data.data(), data.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); } void -TimelineViewManager::queueAudioMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.audio message to a non-managed view"); + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + emit mediaCached(mxcUrl, mxcUrl); return; } - auto view = views_[roomid]; - - view->addUserMessage(url, filename, mime, size); -} + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); -void -TimelineViewManager::queueVideoMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) -{ - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.video message to a non-managed view"); + const auto url = mxcUrl.toStdString(); + QFileInfo filename(QString("%1/media_cache/%2.%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString(mxcUrl).remove("mxc://")) + .arg(suffix)); + if (QDir::cleanPath(filename.path()) != filename.path()) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); return; } - auto view = views_[roomid]; + QDir().mkpath(filename.path()); - view->addUserMessage(url, filename, mime, size); + if (filename.isReadable()) { + emit mediaCached(mxcUrl, filename.filePath()); + return; + } + + http::client()->download( + url, + [this, mxcUrl, filename, url](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(data.data(), data.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + + emit mediaCached(mxcUrl, filename.filePath()); + }); } void -TimelineViewManager::initialize(const mtx::responses::Rooms &rooms) +TimelineViewManager::updateReadReceipts(const QString &room_id, + const std::vector &event_ids) { - for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { - addRoom(it->second, QString::fromStdString(it->first)); + auto room = models.find(room_id); + if (room != models.end()) { + room.value()->markEventsAsRead(event_ids); } - - sync(rooms); } void TimelineViewManager::initWithMessages(const std::map &msgs) { - for (auto it = msgs.cbegin(); it != msgs.cend(); ++it) { - if (timelineViewExists(it->first)) - return; - - // Create a history view with the room events. - TimelineView *view = new TimelineView(it->second, it->first); - views_.emplace(it->first, QSharedPointer(view)); + for (const auto &e : msgs) { + addRoom(e.first); - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); - - // Add the view in the widget stack. - addWidget(view); + models.value(e.first)->addEvents(e.second); } } void -TimelineViewManager::initialize(const std::vector &rooms) +TimelineViewManager::queueTextMessage(const QString &msg) { - for (const auto &roomid : rooms) - addRoom(QString::fromStdString(roomid)); + mtx::events::msg::Text text = {}; + text.body = msg.trimmed().toStdString(); + text.format = "org.matrix.custom.html"; + text.formatted_body = utils::markdownToHtml(msg).toStdString(); + + if (timeline_) + timeline_->sendMessage(text); } void -TimelineViewManager::addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id) +TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) { - if (timelineViewExists(room_id)) - return; - - // Create a history view with the room events. - TimelineView *view = new TimelineView(room.timeline, room_id); - views_.emplace(room_id, QSharedPointer(view)); + mtx::events::msg::Text text = {}; + + QString body; + bool firstLine = true; + for (const auto &line : related.quoted_body.split("\n")) { + if (firstLine) { + firstLine = false; + body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line); + } else { + body = QString("%1\n> %2\n").arg(body).arg(line); + } + } - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); + text.body = QString("%1\n%2").arg(body).arg(reply).toStdString(); + text.format = "org.matrix.custom.html"; + text.formatted_body = + utils::getFormattedQuoteBody(related, utils::markdownToHtml(reply)).toStdString(); + text.relates_to.in_reply_to.event_id = related.related_event; - // Add the view in the widget stack. - addWidget(view); + if (timeline_) + timeline_->sendMessage(text); } void -TimelineViewManager::addRoom(const QString &room_id) +TimelineViewManager::queueEmoteMessage(const QString &msg) { - if (timelineViewExists(room_id)) - return; + auto html = utils::markdownToHtml(msg); - // Create a history view without any events. - TimelineView *view = new TimelineView(room_id); - views_.emplace(room_id, QSharedPointer(view)); + mtx::events::msg::Emote emote; + emote.body = msg.trimmed().toStdString(); - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); + if (html != msg.trimmed().toHtmlEscaped()) + emote.formatted_body = html.toStdString(); - // Add the view in the widget stack. - addWidget(view); + if (timeline_) + timeline_->sendMessage(emote); } void -TimelineViewManager::sync(const mtx::responses::Rooms &rooms) +TimelineViewManager::queueImageMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize, + const QSize &dimensions) { - for (const auto &room : rooms.join) { - auto roomid = QString::fromStdString(room.first); - - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("ignoring event from unknown room: {}", - roomid.toStdString()); - continue; - } - - auto view = views_.at(roomid); - - view->addEvents(room.second.timeline); - } + mtx::events::msg::Image image; + image.info.mimetype = mime.toStdString(); + image.info.size = dsize; + image.body = filename.toStdString(); + image.url = url.toStdString(); + image.info.h = dimensions.height(); + image.info.w = dimensions.width(); + models.value(roomid)->sendMessage(image); } void -TimelineViewManager::setHistoryView(const QString &room_id) +TimelineViewManager::queueFileMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) { - if (!timelineViewExists(room_id)) { - nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}", - room_id.toStdString()); - return; - } - - active_room_ = room_id; - auto view = views_.at(room_id); - - setCurrentWidget(view.data()); - - view->fetchHistory(); - view->scrollDown(); + mtx::events::msg::File file; + file.info.mimetype = mime.toStdString(); + file.info.size = dsize; + file.body = filename.toStdString(); + file.url = url.toStdString(); + models.value(roomid)->sendMessage(file); } -QString -TimelineViewManager::chooseRandomColor() +void +TimelineViewManager::queueAudioMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) { - std::random_device random_device; - std::mt19937 engine{random_device()}; - std::uniform_real_distribution dist(0, 1); - - float hue = dist(engine); - float saturation = 0.9; - float value = 0.7; - - int hue_i = hue * 6; - - float f = hue * 6 - hue_i; - - float p = value * (1 - saturation); - float q = value * (1 - f * saturation); - float t = value * (1 - (1 - f) * saturation); - - float r = 0; - float g = 0; - float b = 0; - - if (hue_i == 0) { - r = value; - g = t; - b = p; - } else if (hue_i == 1) { - r = q; - g = value; - b = p; - } else if (hue_i == 2) { - r = p; - g = value; - b = t; - } else if (hue_i == 3) { - r = p; - g = q; - b = value; - } else if (hue_i == 4) { - r = t; - g = p; - b = value; - } else if (hue_i == 5) { - r = value; - g = p; - b = q; - } - - int ri = r * 256; - int gi = g * 256; - int bi = b * 256; - - QColor color(ri, gi, bi); - - return color.name(); + mtx::events::msg::Audio audio; + audio.info.mimetype = mime.toStdString(); + audio.info.size = dsize; + audio.body = filename.toStdString(); + audio.url = url.toStdString(); + models.value(roomid)->sendMessage(audio); } -bool -TimelineViewManager::hasLoaded() const +void +TimelineViewManager::queueVideoMessage(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + uint64_t dsize) { - return std::all_of(views_.cbegin(), views_.cend(), [](const auto &view) { - return view.second->hasLoaded(); - }); + mtx::events::msg::Video video; + video.info.mimetype = mime.toStdString(); + video.info.size = dsize; + video.body = filename.toStdString(); + video.url = url.toStdString(); + models.value(roomid)->sendMessage(video); } diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index b52136d9..691c8ddb 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -1,69 +1,80 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - #pragma once +#include +#include #include -#include +#include -#include +#include +#include "Cache.h" +#include "Logging.h" +#include "TimelineModel.h" #include "Utils.h" -class QFile; +// temporary for stubs +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" -class RoomInfoListItem; -class TimelineView; -struct DescInfo; -struct SavedMessages; +class MxcImageProvider; +class ColorImageProvider; -class TimelineViewManager : public QStackedWidget +class TimelineViewManager : public QObject { Q_OBJECT -public: - TimelineViewManager(QWidget *parent); - - // Initialize with timeline events. - void initialize(const mtx::responses::Rooms &rooms); - // Empty initialization. - void initialize(const std::vector &rooms); + Q_PROPERTY( + TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) - void addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id); - void addRoom(const QString &room_id); +public: + TimelineViewManager(QWidget *parent = 0); + QWidget *getWidget() const { return container; } void sync(const mtx::responses::Rooms &rooms); - void clearAll() { views_.clear(); } - - // Check if all the timelines have been loaded. - bool hasLoaded() const; + void addRoom(const QString &room_id); - static QString chooseRandomColor(); + void clearAll() { models.clear(); } + + Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } + void openImageOverlay(QString mxcUrl, + QString originalFilename, + QString mimeType, + qml_mtx_events::EventType eventType) const; + void saveMedia(QString mxcUrl, + QString originalFilename, + QString mimeType, + qml_mtx_events::EventType eventType) const; + Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType); + // Qml can only pass enum as int + Q_INVOKABLE void openImageOverlay(QString mxcUrl, + QString originalFilename, + QString mimeType, + int eventType) const + { + openImageOverlay( + mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); + } + Q_INVOKABLE void saveMedia(QString mxcUrl, + QString originalFilename, + QString mimeType, + int eventType) const + { + saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); + } signals: void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(const QString &user, const DescInfo &info); + void updateRoomsLastMessage(QString roomid, const DescInfo &info); + void activeTimelineChanged(TimelineModel *timeline); + void mediaCached(QString mxcUrl, QString cacheUrl); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); - void removeTimelineEvent(const QString &room_id, const QString &event_id); void initWithMessages(const std::map &msgs); void setHistoryView(const QString &room_id); + void updateColorPalette(); + void queueTextMessage(const QString &msg); void queueReplyMessage(const QString &reply, const RelatedInfo &related); void queueEmoteMessage(const QString &msg); @@ -90,9 +101,17 @@ public slots: uint64_t dsize); private: - //! Check if the given room id is managed by a TimelineView. - bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); } - - QString active_room_; - std::map> views_; +#ifdef USE_QUICK_VIEW + QQuickView *view; +#else + QQuickWidget *view; +#endif + QWidget *container; + TimelineModel *timeline_ = nullptr; + MxcImageProvider *imgProvider; + ColorImageProvider *colorImgProvider; + + QHash> models; }; + +#pragma GCC diagnostic pop diff --git a/src/timeline/widgets/AudioItem.cpp b/src/timeline/widgets/AudioItem.cpp deleted file mode 100644 index 5d6431ee..00000000 --- a/src/timeline/widgets/AudioItem.cpp +++ /dev/null @@ -1,236 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include - -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" - -#include "timeline/widgets/AudioItem.h" - -constexpr int MaxWidth = 400; -constexpr int Height = 70; -constexpr int IconRadius = 22; -constexpr int IconDiameter = IconRadius * 2; -constexpr int HorizontalPadding = 12; -constexpr int TextPadding = 15; -constexpr int ActionIconRadius = IconRadius - 4; - -constexpr double VerticalPadding = Height - 2 * IconRadius; -constexpr double IconYCenter = Height / 2; -constexpr double IconXCenter = HorizontalPadding + IconRadius; - -void -AudioItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - playIcon_.addFile(":/icons/icons/ui/play-sign.png"); - pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png"); - - player_ = new QMediaPlayer; - player_->setMedia(QUrl(url_)); - player_->setVolume(100); - player_->setNotifyInterval(1000); - - connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) { - if (state == QMediaPlayer::StoppedState) { - state_ = AudioState::Play; - player_->setMedia(QUrl(url_)); - update(); - } - }); - - setFixedHeight(Height); -} - -AudioItem::AudioItem(const mtx::events::RoomEvent &event, QWidget *parent) - : QWidget(parent) - , url_{QUrl(QString::fromStdString(event.content.url))} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); -} - -AudioItem::AudioItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} - -QSize -AudioItem::sizeHint() const -{ - return QSize(MaxWidth, Height); -} - -void -AudioItem::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton) - return; - - auto point = event->pos(); - - // Click on the download icon. - if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter) - .contains(point)) { - if (state_ == AudioState::Play) { - state_ = AudioState::Pause; - player_->play(); - } else { - state_ = AudioState::Play; - player_->pause(); - } - - update(); - } else { - filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_); - - if (filenameToSave_.isEmpty()) - return; - - auto proxy = std::make_shared(); - connect(proxy.get(), &MediaProxy::fileDownloaded, this, &AudioItem::fileDownloaded); - - http::client()->download( - url_.toString().toStdString(), - [proxy = std::move(proxy), url = url_](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->info("failed to retrieve m.audio content: {}", - url.toString().toStdString()); - return; - } - - emit proxy->fileDownloaded(QByteArray(data.data(), data.size())); - }); - } -} - -void -AudioItem::fileDownloaded(const QByteArray &data) -{ - try { - QFile file(filenameToSave_); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("error while saving file: {}", e.what()); - } -} - -void -AudioItem::resizeEvent(QResizeEvent *event) -{ - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); -#else - const int computedWidth = - std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, - (double)MaxWidth); -#endif - resize(computedWidth, Height); - - event->accept(); -} - -void -AudioItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, backgroundColor_); - painter.drawPath(path); - - QPainterPath circle; - circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius); - - painter.setPen(Qt::NoPen); - painter.fillPath(circle, iconColor_); - painter.drawPath(circle); - - QIcon icon_; - if (state_ == AudioState::Play) - icon_ = playIcon_; - else - icon_ = pauseIcon_; - - icon_.paint(&painter, - QRect(IconXCenter - ActionIconRadius / 2, - IconYCenter - ActionIconRadius / 2, - ActionIconRadius, - ActionIconRadius), - Qt::AlignCenter, - QIcon::Normal); - - const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding; - const int textStartY = VerticalPadding + fm.ascent() / 2; - - // Draw the filename. - QString elidedText = fm.elidedText( - text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius); - - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY), elidedText); - - // Draw the filesize. - font.setWeight(QFont::Normal); - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_); -} diff --git a/src/timeline/widgets/AudioItem.h b/src/timeline/widgets/AudioItem.h deleted file mode 100644 index c32b7731..00000000 --- a/src/timeline/widgets/AudioItem.h +++ /dev/null @@ -1,104 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include - -class AudioItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - - Q_PROPERTY(QColor durationBackgroundColor WRITE setDurationBackgroundColor READ - durationBackgroundColor) - Q_PROPERTY(QColor durationForegroundColor WRITE setDurationForegroundColor READ - durationForegroundColor) - -public: - AudioItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - AudioItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - void setDurationBackgroundColor(const QColor &color) { durationBgColor_ = color; } - void setDurationForegroundColor(const QColor &color) { durationFgColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - - QColor durationBackgroundColor() const { return durationBgColor_; } - QColor durationForegroundColor() const { return durationFgColor_; } - -protected: - void paintEvent(QPaintEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void init(); - - enum class AudioState - { - Play, - Pause, - }; - - AudioState state_ = AudioState::Play; - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent event_; - - QMediaPlayer *player_; - - QIcon playIcon_; - QIcon pauseIcon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); - - QColor durationBgColor_ = QColor("black"); - QColor durationFgColor_ = QColor("blue"); -}; diff --git a/src/timeline/widgets/FileItem.cpp b/src/timeline/widgets/FileItem.cpp deleted file mode 100644 index 1a555d1c..00000000 --- a/src/timeline/widgets/FileItem.cpp +++ /dev/null @@ -1,221 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include - -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" - -#include "timeline/widgets/FileItem.h" - -constexpr int MaxWidth = 400; -constexpr int Height = 70; -constexpr int IconRadius = 22; -constexpr int IconDiameter = IconRadius * 2; -constexpr int HorizontalPadding = 12; -constexpr int TextPadding = 15; -constexpr int DownloadIconRadius = IconRadius - 4; - -constexpr double VerticalPadding = Height - 2 * IconRadius; -constexpr double IconYCenter = Height / 2; -constexpr double IconXCenter = HorizontalPadding + IconRadius; - -void -FileItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png"); - - setFixedHeight(Height); -} - -FileItem::FileItem(const mtx::events::RoomEvent &event, QWidget *parent) - : QWidget(parent) - , url_{QString::fromStdString(event.content.url)} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); -} - -FileItem::FileItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} - -void -FileItem::openUrl() -{ - if (url_.toString().isEmpty()) - return; - - auto urlToOpen = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); - - if (!QDesktopServices::openUrl(urlToOpen)) - nhlog::ui()->warn("Could not open url: {}", urlToOpen.toStdString()); -} - -QSize -FileItem::sizeHint() const -{ - return QSize(MaxWidth, Height); -} - -void -FileItem::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton) - return; - - auto point = event->pos(); - - // Click on the download icon. - if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter) - .contains(point)) { - filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_); - - if (filenameToSave_.isEmpty()) - return; - - auto proxy = std::make_shared(); - connect(proxy.get(), &MediaProxy::fileDownloaded, this, &FileItem::fileDownloaded); - - http::client()->download( - url_.toString().toStdString(), - [proxy = std::move(proxy), url = url_](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->warn("failed to retrieve m.file content: {}", - url.toString().toStdString()); - return; - } - - emit proxy->fileDownloaded(QByteArray(data.data(), data.size())); - }); - } else { - openUrl(); - } -} - -void -FileItem::fileDownloaded(const QByteArray &data) -{ - try { - QFile file(filenameToSave_); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } -} - -void -FileItem::resizeEvent(QResizeEvent *event) -{ - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); -#else - const int computedWidth = - std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, - (double)MaxWidth); -#endif - resize(computedWidth, Height); - - event->accept(); -} - -void -FileItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, backgroundColor_); - painter.drawPath(path); - - QPainterPath circle; - circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius); - - painter.setPen(Qt::NoPen); - painter.fillPath(circle, iconColor_); - painter.drawPath(circle); - - icon_.paint(&painter, - QRect(IconXCenter - DownloadIconRadius / 2, - IconYCenter - DownloadIconRadius / 2, - DownloadIconRadius, - DownloadIconRadius), - Qt::AlignCenter, - QIcon::Normal); - - const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding; - const int textStartY = VerticalPadding + fm.ascent() / 2; - - // Draw the filename. - QString elidedText = fm.elidedText( - text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius); - - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY), elidedText); - - // Draw the filesize. - font.setWeight(QFont::Normal); - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_); -} diff --git a/src/timeline/widgets/FileItem.h b/src/timeline/widgets/FileItem.h deleted file mode 100644 index d63cce88..00000000 --- a/src/timeline/widgets/FileItem.h +++ /dev/null @@ -1,79 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include - -class FileItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - FileItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - FileItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void openUrl(); - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent event_; - - QIcon icon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); -}; diff --git a/src/timeline/widgets/ImageItem.cpp b/src/timeline/widgets/ImageItem.cpp deleted file mode 100644 index 26c569d7..00000000 --- a/src/timeline/widgets/ImageItem.cpp +++ /dev/null @@ -1,267 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Config.h" -#include "ImageItem.h" -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" -#include "dialogs/ImageOverlay.h" - -void -ImageItem::downloadMedia(const QUrl &url) -{ - auto proxy = std::make_shared(); - connect(proxy.get(), &MediaProxy::imageDownloaded, this, &ImageItem::setImage); - - http::client()->download(url.toString().toStdString(), - [proxy = std::move(proxy), url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to retrieve image {}: {} {}", - url.toString().toStdString(), - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - QPixmap img; - img.loadFromData(QByteArray(data.data(), data.size())); - - emit proxy->imageDownloaded(img); - }); -} - -void -ImageItem::saveImage(const QString &filename, const QByteArray &data) -{ - try { - QFile file(filename); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } -} - -void -ImageItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - downloadMedia(url_); -} - -ImageItem::ImageItem(const mtx::events::RoomEvent &event, QWidget *parent) - : QWidget(parent) - , event_{event} -{ - url_ = QString::fromStdString(event.content.url); - text_ = QString::fromStdString(event.content.body); - - init(); -} - -ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - Q_UNUSED(size); - init(); -} - -void -ImageItem::openUrl() -{ - if (url_.toString().isEmpty()) - return; - - auto urlToOpen = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); - - if (!QDesktopServices::openUrl(urlToOpen)) - nhlog::ui()->warn("could not open url: {}", urlToOpen.toStdString()); -} - -QSize -ImageItem::sizeHint() const -{ - if (image_.isNull()) - return QSize(max_width_, bottom_height_); - - return QSize(width_, height_); -} - -void -ImageItem::setImage(const QPixmap &image) -{ - image_ = image; - scaled_image_ = utils::scaleDown(max_width_, max_height_, image_); - - width_ = scaled_image_.width(); - height_ = scaled_image_.height(); - - setFixedSize(width_, height_); - update(); -} - -void -ImageItem::mousePressEvent(QMouseEvent *event) -{ - if (!isInteractive_) { - event->accept(); - return; - } - - if (event->button() != Qt::LeftButton) - return; - - if (image_.isNull()) { - openUrl(); - return; - } - - if (textRegion_.contains(event->pos())) { - openUrl(); - } else { - auto imgDialog = new dialogs::ImageOverlay(image_); - imgDialog->show(); - connect(imgDialog, &dialogs::ImageOverlay::saving, this, &ImageItem::saveAs); - } -} - -void -ImageItem::resizeEvent(QResizeEvent *event) -{ - if (!image_) - return QWidget::resizeEvent(event); - - scaled_image_ = utils::scaleDown(max_width_, max_height_, image_); - - width_ = scaled_image_.width(); - height_ = scaled_image_.height(); - - setFixedSize(width_, height_); -} - -void -ImageItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - - QFontMetrics metrics(font); - const int fontHeight = metrics.height() + metrics.ascent(); - - if (image_.isNull()) { - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - setFixedSize(metrics.width(elidedText), fontHeight); -#else - setFixedSize(metrics.horizontalAdvance(elidedText), fontHeight); -#endif - painter.setFont(font); - painter.setPen(QPen(QColor(66, 133, 244))); - painter.drawText(QPoint(0, fontHeight / 2), elidedText); - - return; - } - - imageRegion_ = QRectF(0, 0, width_, height_); - - QPainterPath path; - path.addRoundedRect(imageRegion_, 5, 5); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, scaled_image_); - painter.drawPath(path); - - // Bottom text section - if (isInteractive_ && underMouse()) { - const int textBoxHeight = fontHeight / 2 + 6; - - textRegion_ = QRectF(0, height_ - textBoxHeight, width_, textBoxHeight); - - QPainterPath textPath; - textPath.addRoundedRect(textRegion_, 0, 0); - - painter.fillPath(textPath, QColor(40, 40, 40, 140)); - - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, width_ - 10); - - font.setWeight(QFont::Medium); - painter.setFont(font); - painter.setPen(QPen(QColor(Qt::white))); - - textRegion_.adjust(5, 0, 5, 0); - painter.drawText(textRegion_, Qt::AlignVCenter, elidedText); - } -} - -void -ImageItem::saveAs() -{ - auto filename = QFileDialog::getSaveFileName(this, tr("Save image"), text_); - - if (filename.isEmpty()) - return; - - const auto url = url_.toString().toStdString(); - - auto proxy = std::make_shared(); - connect(proxy.get(), &MediaProxy::imageSaved, this, &ImageItem::saveImage); - - http::client()->download( - url, - [proxy = std::move(proxy), filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - emit proxy->imageSaved(filename, QByteArray(data.data(), data.size())); - }); -} diff --git a/src/timeline/widgets/ImageItem.h b/src/timeline/widgets/ImageItem.h deleted file mode 100644 index 65bd962d..00000000 --- a/src/timeline/widgets/ImageItem.h +++ /dev/null @@ -1,104 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include - -#include - -namespace dialogs { -class ImageOverlay; -} - -class ImageItem : public QWidget -{ - Q_OBJECT -public: - ImageItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - ImageItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - -public slots: - //! Show a save as dialog for the image. - void saveAs(); - void setImage(const QPixmap &image); - void saveImage(const QString &filename, const QByteArray &data); - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - - //! Whether the user can interact with the displayed image. - bool isInteractive_ = true; - -private: - void init(); - void openUrl(); - void downloadMedia(const QUrl &url); - - int max_width_ = 500; - int max_height_ = 300; - - int width_; - int height_; - - QPixmap scaled_image_; - QPixmap image_; - - QUrl url_; - QString text_; - - int bottom_height_ = 30; - - QRectF textRegion_; - QRectF imageRegion_; - - mtx::events::RoomEvent event_; -}; - -class StickerItem : public ImageItem -{ - Q_OBJECT - -public: - StickerItem(const mtx::events::Sticker &event, QWidget *parent = nullptr) - : ImageItem{QString::fromStdString(event.content.url), - QString::fromStdString(event.content.body), - event.content.info.size, - parent} - , event_{event} - { - isInteractive_ = false; - setCursor(Qt::ArrowCursor); - setMouseTracking(false); - setAttribute(Qt::WA_Hover, false); - } - -private: - mtx::events::Sticker event_; -}; diff --git a/src/timeline/widgets/VideoItem.cpp b/src/timeline/widgets/VideoItem.cpp deleted file mode 100644 index 4b5dc022..00000000 --- a/src/timeline/widgets/VideoItem.cpp +++ /dev/null @@ -1,65 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include - -#include "Config.h" -#include "MatrixClient.h" -#include "Utils.h" -#include "timeline/widgets/VideoItem.h" - -void -VideoItem::init() -{ - url_ = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); -} - -VideoItem::VideoItem(const mtx::events::RoomEvent &event, QWidget *parent) - : QWidget(parent) - , url_{QString::fromStdString(event.content.url)} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); - - auto layout = new QVBoxLayout(this); - layout->setMargin(0); - layout->setSpacing(0); - - QString link = QString("%2").arg(url_.toString()).arg(text_); - - label_ = new QLabel(link, this); - label_->setMargin(0); - label_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - label_->setOpenExternalLinks(true); - - layout->addWidget(label_); -} - -VideoItem::VideoItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} diff --git a/src/timeline/widgets/VideoItem.h b/src/timeline/widgets/VideoItem.h deleted file mode 100644 index 26fa1c35..00000000 --- a/src/timeline/widgets/VideoItem.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include - -class VideoItem : public QWidget -{ - Q_OBJECT - -public: - VideoItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - VideoItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - -private: - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - - QLabel *label_; - - mtx::events::RoomEvent event_; -}; diff --git a/src/timeline2/DelegateChooser.cpp b/src/timeline2/DelegateChooser.cpp deleted file mode 100644 index 632a2a64..00000000 --- a/src/timeline2/DelegateChooser.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include "DelegateChooser.h" - -#include "Logging.h" - -// uses private API, which moved between versions -#include -#include - -QQmlComponent * -DelegateChoice::delegate() const -{ - return delegate_; -} - -void -DelegateChoice::setDelegate(QQmlComponent *delegate) -{ - if (delegate != delegate_) { - delegate_ = delegate; - emit delegateChanged(); - emit changed(); - } -} - -QVariant -DelegateChoice::roleValue() const -{ - return roleValue_; -} - -void -DelegateChoice::setRoleValue(const QVariant &value) -{ - if (value != roleValue_) { - roleValue_ = value; - emit roleValueChanged(); - emit changed(); - } -} - -QVariant -DelegateChooser::roleValue() const -{ - return roleValue_; -} - -void -DelegateChooser::setRoleValue(const QVariant &value) -{ - if (value != roleValue_) { - roleValue_ = value; - recalcChild(); - emit roleValueChanged(); - } -} - -QQmlListProperty -DelegateChooser::choices() -{ - return QQmlListProperty(this, - this, - &DelegateChooser::appendChoice, - &DelegateChooser::choiceCount, - &DelegateChooser::choice, - &DelegateChooser::clearChoices); -} - -void -DelegateChooser::appendChoice(QQmlListProperty *p, DelegateChoice *c) -{ - DelegateChooser *dc = static_cast(p->object); - dc->choices_.append(c); -} - -int -DelegateChooser::choiceCount(QQmlListProperty *p) -{ - return static_cast(p->object)->choices_.count(); -} -DelegateChoice * -DelegateChooser::choice(QQmlListProperty *p, int index) -{ - return static_cast(p->object)->choices_.at(index); -} -void -DelegateChooser::clearChoices(QQmlListProperty *p) -{ - static_cast(p->object)->choices_.clear(); -} - -void -DelegateChooser::recalcChild() -{ - for (const auto choice : choices_) { - auto choiceValue = choice->roleValue(); - if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) { - if (child) { - child->setParentItem(nullptr); - child = nullptr; - } - - choice->delegate()->create(incubator, QQmlEngine::contextForObject(this)); - return; - } - } -} - -void -DelegateChooser::componentComplete() -{ - QQuickItem::componentComplete(); - recalcChild(); -} - -void -DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) -{ - if (status == QQmlIncubator::Ready) { - chooser.child = dynamic_cast(object()); - if (chooser.child == nullptr) { - nhlog::ui()->error("Delegate has to be derived of Item!"); - return; - } - - chooser.child->setParentItem(&chooser); - connect(chooser.child, &QQuickItem::heightChanged, &chooser, [this]() { - chooser.setHeight(chooser.child->height()); - }); - chooser.setHeight(chooser.child->height()); - QQmlEngine::setObjectOwnership(chooser.child, - QQmlEngine::ObjectOwnership::JavaScriptOwnership); - - } else if (status == QQmlIncubator::Error) { - for (const auto &e : errors()) - nhlog::ui()->error("Error instantiating delegate: {}", - e.toString().toStdString()); - } -} diff --git a/src/timeline2/DelegateChooser.h b/src/timeline2/DelegateChooser.h deleted file mode 100644 index 68ebeb04..00000000 --- a/src/timeline2/DelegateChooser.h +++ /dev/null @@ -1,82 +0,0 @@ -// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt -// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent - -#pragma once - -#include -#include -#include -#include -#include -#include - -class QQmlAdaptorModel; - -class DelegateChoice : public QObject -{ - Q_OBJECT - Q_CLASSINFO("DefaultProperty", "delegate") - -public: - Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) - Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) - - QQmlComponent *delegate() const; - void setDelegate(QQmlComponent *delegate); - - QVariant roleValue() const; - void setRoleValue(const QVariant &value); - -signals: - void delegateChanged(); - void roleValueChanged(); - void changed(); - -private: - QVariant roleValue_; - QQmlComponent *delegate_ = nullptr; -}; - -class DelegateChooser : public QQuickItem -{ - Q_OBJECT - Q_CLASSINFO("DefaultProperty", "choices") - -public: - Q_PROPERTY(QQmlListProperty choices READ choices CONSTANT) - Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) - - QQmlListProperty choices(); - - QVariant roleValue() const; - void setRoleValue(const QVariant &value); - - void recalcChild(); - void componentComplete() override; - -signals: - void roleChanged(); - void roleValueChanged(); - -private: - struct DelegateIncubator : public QQmlIncubator - { - DelegateIncubator(DelegateChooser &parent) - : QQmlIncubator(QQmlIncubator::AsynchronousIfNested) - , chooser(parent) - {} - void statusChanged(QQmlIncubator::Status status) override; - - DelegateChooser &chooser; - }; - - QVariant roleValue_; - QList choices_; - QQuickItem *child = nullptr; - DelegateIncubator incubator{*this}; - - static void appendChoice(QQmlListProperty *, DelegateChoice *); - static int choiceCount(QQmlListProperty *); - static DelegateChoice *choice(QQmlListProperty *, int index); - static void clearChoices(QQmlListProperty *); -}; diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp deleted file mode 100644 index ab7d3d47..00000000 --- a/src/timeline2/TimelineModel.cpp +++ /dev/null @@ -1,1220 +0,0 @@ -#include "TimelineModel.h" - -#include -#include - -#include - -#include "ChatPage.h" -#include "Logging.h" -#include "MainWindow.h" -#include "Olm.h" -#include "TimelineViewManager.h" -#include "Utils.h" -#include "dialogs/RawMessage.h" - -Q_DECLARE_METATYPE(QModelIndex) - -namespace { -template -QString -eventId(const mtx::events::RoomEvent &event) -{ - return QString::fromStdString(event.event_id); -} -template -QString -roomId(const mtx::events::Event &event) -{ - return QString::fromStdString(event.room_id); -} -template -QString -senderId(const mtx::events::RoomEvent &event) -{ - return QString::fromStdString(event.sender); -} - -template -QDateTime -eventTimestamp(const mtx::events::RoomEvent &event) -{ - return QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); -} - -template -std::string -eventMsgType(const mtx::events::Event &) -{ - return ""; -} -template -auto -eventMsgType(const mtx::events::RoomEvent &e) -> decltype(e.content.msgtype) -{ - return e.content.msgtype; -} - -template -QString -eventBody(const mtx::events::Event &) -{ - return QString(""); -} -template -auto -eventBody(const mtx::events::RoomEvent &e) - -> std::enable_if_t::value, QString> -{ - return QString::fromStdString(e.content.body); -} - -template -QString -eventFormattedBody(const mtx::events::Event &) -{ - return QString(""); -} -template -auto -eventFormattedBody(const mtx::events::RoomEvent &e) - -> std::enable_if_t::value, QString> -{ - auto temp = e.content.formatted_body; - if (!temp.empty()) { - return QString::fromStdString(temp); - } else { - return QString::fromStdString(e.content.body).toHtmlEscaped().replace("\n", "
"); - } -} - -template -QString -eventUrl(const mtx::events::Event &) -{ - return ""; -} -template -auto -eventUrl(const mtx::events::RoomEvent &e) - -> std::enable_if_t::value, QString> -{ - return QString::fromStdString(e.content.url); -} - -template -QString -eventThumbnailUrl(const mtx::events::Event &) -{ - return ""; -} -template -auto -eventThumbnailUrl(const mtx::events::RoomEvent &e) - -> std::enable_if_t::value, - QString> -{ - return QString::fromStdString(e.content.info.thumbnail_url); -} - -template -QString -eventFilename(const mtx::events::Event &) -{ - return ""; -} -QString -eventFilename(const mtx::events::RoomEvent &e) -{ - // body may be the original filename - return QString::fromStdString(e.content.body); -} -QString -eventFilename(const mtx::events::RoomEvent &e) -{ - // body may be the original filename - return QString::fromStdString(e.content.body); -} -QString -eventFilename(const mtx::events::RoomEvent &e) -{ - // body may be the original filename - return QString::fromStdString(e.content.body); -} -QString -eventFilename(const mtx::events::RoomEvent &e) -{ - // body may be the original filename - if (!e.content.filename.empty()) - return QString::fromStdString(e.content.filename); - return QString::fromStdString(e.content.body); -} - -template -auto -eventFilesize(const mtx::events::RoomEvent &e) -> decltype(e.content.info.size) -{ - return e.content.info.size; -} - -template -int64_t -eventFilesize(const mtx::events::Event &) -{ - return 0; -} - -template -QString -eventMimeType(const mtx::events::Event &) -{ - return QString(); -} -template -auto -eventMimeType(const mtx::events::RoomEvent &e) - -> std::enable_if_t::value, QString> -{ - return QString::fromStdString(e.content.info.mimetype); -} - -template -QString -eventRelatesTo(const mtx::events::Event &) -{ - return QString(); -} -template -auto -eventRelatesTo(const mtx::events::RoomEvent &e) -> std::enable_if_t< - std::is_same::value, - QString> -{ - return QString::fromStdString(e.content.relates_to.in_reply_to.event_id); -} - -template -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &e) -{ - using mtx::events::EventType; - switch (e.type) { - case EventType::RoomKeyRequest: - return qml_mtx_events::EventType::KeyRequest; - case EventType::RoomAliases: - return qml_mtx_events::EventType::Aliases; - case EventType::RoomAvatar: - return qml_mtx_events::EventType::Avatar; - case EventType::RoomCanonicalAlias: - return qml_mtx_events::EventType::CanonicalAlias; - case EventType::RoomCreate: - return qml_mtx_events::EventType::Create; - case EventType::RoomEncrypted: - return qml_mtx_events::EventType::Encrypted; - case EventType::RoomEncryption: - return qml_mtx_events::EventType::Encryption; - case EventType::RoomGuestAccess: - return qml_mtx_events::EventType::GuestAccess; - case EventType::RoomHistoryVisibility: - return qml_mtx_events::EventType::HistoryVisibility; - case EventType::RoomJoinRules: - return qml_mtx_events::EventType::JoinRules; - case EventType::RoomMember: - return qml_mtx_events::EventType::Member; - case EventType::RoomMessage: - return qml_mtx_events::EventType::UnknownMessage; - case EventType::RoomName: - return qml_mtx_events::EventType::Name; - case EventType::RoomPowerLevels: - return qml_mtx_events::EventType::PowerLevels; - case EventType::RoomTopic: - return qml_mtx_events::EventType::Topic; - case EventType::RoomTombstone: - return qml_mtx_events::EventType::Tombstone; - case EventType::RoomRedaction: - return qml_mtx_events::EventType::Redaction; - case EventType::RoomPinnedEvents: - return qml_mtx_events::EventType::PinnedEvents; - case EventType::Sticker: - return qml_mtx_events::EventType::Sticker; - case EventType::Tag: - return qml_mtx_events::EventType::Tag; - case EventType::Unsupported: - default: - return qml_mtx_events::EventType::Unsupported; - } -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::AudioMessage; -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::EmoteMessage; -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::FileMessage; -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::ImageMessage; -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::NoticeMessage; -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::TextMessage; -} -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::VideoMessage; -} - -qml_mtx_events::EventType -toRoomEventType(const mtx::events::Event &) -{ - return qml_mtx_events::EventType::Redacted; -} -// ::EventType::Type toRoomEventType(const Event &e) { return -// ::EventType::LocationMessage; } - -template -uint64_t -eventHeight(const mtx::events::Event &) -{ - return -1; -} -template -auto -eventHeight(const mtx::events::RoomEvent &e) -> decltype(e.content.info.h) -{ - return e.content.info.h; -} -template -uint64_t -eventWidth(const mtx::events::Event &) -{ - return -1; -} -template -auto -eventWidth(const mtx::events::RoomEvent &e) -> decltype(e.content.info.w) -{ - return e.content.info.w; -} - -template -double -eventPropHeight(const mtx::events::RoomEvent &e) -{ - auto w = eventWidth(e); - if (w == 0) - w = 1; - return eventHeight(e) / (double)w; -} -} - -TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent) - : QAbstractListModel(parent) - , room_id_(room_id) - , manager_(manager) -{ - connect( - this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); - connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) { - pending.remove(txn_id); - failed.insert(txn_id); - int idx = idToIndex(txn_id); - if (idx < 0) { - nhlog::ui()->warn("Failed index out of range"); - return; - } - emit dataChanged(index(idx, 0), index(idx, 0)); - }); - connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) { - int idx = idToIndex(txn_id); - if (idx < 0) { - nhlog::ui()->warn("Sent index out of range"); - return; - } - eventOrder[idx] = event_id; - auto ev = events.value(txn_id); - ev = boost::apply_visitor( - [event_id](const auto &e) -> mtx::events::collections::TimelineEvents { - auto eventCopy = e; - eventCopy.event_id = event_id.toStdString(); - return eventCopy; - }, - ev); - events.remove(txn_id); - events.insert(event_id, ev); - - // mark our messages as read - readEvent(event_id.toStdString()); - - // ask to be notified for read receipts - cache::client()->addPendingReceipt(room_id_, event_id); - - emit dataChanged(index(idx, 0), index(idx, 0)); - }); - connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { - emit ChatPage::instance()->showNotification(msg); - }); -} - -QHash -TimelineModel::roleNames() const -{ - return { - {Section, "section"}, - {Type, "type"}, - {Body, "body"}, - {FormattedBody, "formattedBody"}, - {UserId, "userId"}, - {UserName, "userName"}, - {Timestamp, "timestamp"}, - {Url, "url"}, - {ThumbnailUrl, "thumbnailUrl"}, - {Filename, "filename"}, - {Filesize, "filesize"}, - {MimeType, "mimetype"}, - {Height, "height"}, - {Width, "width"}, - {ProportionalHeight, "proportionalHeight"}, - {Id, "id"}, - {State, "state"}, - {IsEncrypted, "isEncrypted"}, - {ReplyTo, "replyTo"}, - }; -} -int -TimelineModel::rowCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent); - return (int)this->eventOrder.size(); -} - -QVariant -TimelineModel::data(const QModelIndex &index, int role) const -{ - if (index.row() < 0 && index.row() >= (int)eventOrder.size()) - return QVariant(); - - QString id = eventOrder[index.row()]; - - mtx::events::collections::TimelineEvents event = events.value(id); - - if (auto e = boost::get>(&event)) { - event = decryptEvent(*e).event; - } - - switch (role) { - case Section: { - QDateTime date = boost::apply_visitor( - [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event); - date.setTime(QTime()); - - QString userId = - boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, event); - - for (int r = index.row() - 1; r > 0; r--) { - QDateTime prevDate = boost::apply_visitor( - [](const auto &e) -> QDateTime { return eventTimestamp(e); }, - events.value(eventOrder[r])); - prevDate.setTime(QTime()); - if (prevDate != date) - return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId); - - QString prevUserId = - boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, - events.value(eventOrder[r])); - if (userId != prevUserId) - break; - } - - return QString("%1").arg(userId); - } - case UserId: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { return senderId(e); }, event)); - case UserName: - return QVariant(displayName(boost::apply_visitor( - [](const auto &e) -> QString { return senderId(e); }, event))); - - case Timestamp: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event)); - case Type: - return QVariant(boost::apply_visitor( - [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, - event)); - case Body: - return QVariant(utils::replaceEmoji(boost::apply_visitor( - [](const auto &e) -> QString { return eventBody(e); }, event))); - case FormattedBody: - return QVariant( - utils::replaceEmoji( - boost::apply_visitor( - [](const auto &e) -> QString { return eventFormattedBody(e); }, event)) - .remove("") - .remove("")); - case Url: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { return eventUrl(e); }, event)); - case ThumbnailUrl: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { return eventThumbnailUrl(e); }, event)); - case Filename: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { return eventFilename(e); }, event)); - case Filesize: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { - return utils::humanReadableFileSize(eventFilesize(e)); - }, - event)); - case MimeType: - return QVariant(boost::apply_visitor( - [](const auto &e) -> QString { return eventMimeType(e); }, event)); - case Height: - return QVariant(boost::apply_visitor( - [](const auto &e) -> qulonglong { return eventHeight(e); }, event)); - case Width: - return QVariant(boost::apply_visitor( - [](const auto &e) -> qulonglong { return eventWidth(e); }, event)); - case ProportionalHeight: - return QVariant(boost::apply_visitor( - [](const auto &e) -> double { return eventPropHeight(e); }, event)); - case Id: - return id; - case State: - // only show read receipts for messages not from us - if (boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, - event) - .toStdString() != http::client()->user_id().to_string()) - return qml_mtx_events::Empty; - else if (failed.contains(id)) - return qml_mtx_events::Failed; - else if (pending.contains(id)) - return qml_mtx_events::Sent; - else if (read.contains(id) || - cache::client()->readReceipts(id, room_id_).size() > 1) - return qml_mtx_events::Read; - else - return qml_mtx_events::Received; - case IsEncrypted: { - auto tempEvent = events[id]; - return boost::get>( - &tempEvent) != nullptr; - } - case ReplyTo: { - QString evId = boost::apply_visitor( - [](const auto &e) -> QString { return eventRelatesTo(e); }, event); - return QVariant(evId); - } - default: - return QVariant(); - } -} - -void -TimelineModel::addEvents(const mtx::responses::Timeline &timeline) -{ - if (isInitialSync) { - prev_batch_token_ = QString::fromStdString(timeline.prev_batch); - isInitialSync = false; - } - - if (timeline.events.empty()) - return; - - std::vector ids = internalAddEvents(timeline.events); - - if (ids.empty()) - return; - - beginInsertRows(QModelIndex(), - static_cast(this->eventOrder.size()), - static_cast(this->eventOrder.size() + ids.size() - 1)); - this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); - endInsertRows(); - - updateLastMessage(); -} - -void -TimelineModel::updateLastMessage() -{ - auto event = events.value(eventOrder.back()); - if (auto e = boost::get>(&event)) { - event = decryptEvent(*e).event; - } - - auto description = utils::getMessageDescription( - event, QString::fromStdString(http::client()->user_id().to_string()), room_id_); - emit manager_->updateRoomsLastMessage(room_id_, description); -} - -std::vector -TimelineModel::internalAddEvents( - const std::vector &timeline) -{ - std::vector ids; - for (const auto &e : timeline) { - QString id = - boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); - - if (this->events.contains(id)) { - this->events.insert(id, e); - int idx = idToIndex(id); - emit dataChanged(index(idx, 0), index(idx, 0)); - continue; - } - - if (auto redaction = - boost::get>(&e)) { - QString redacts = QString::fromStdString(redaction->redacts); - auto redacted = std::find(eventOrder.begin(), eventOrder.end(), redacts); - - if (redacted != eventOrder.end()) { - auto redactedEvent = boost::apply_visitor( - [](const auto &ev) - -> mtx::events::RoomEvent { - mtx::events::RoomEvent - replacement = {}; - replacement.event_id = ev.event_id; - replacement.room_id = ev.room_id; - replacement.sender = ev.sender; - replacement.origin_server_ts = ev.origin_server_ts; - replacement.type = ev.type; - return replacement; - }, - e); - events.insert(redacts, redactedEvent); - - int row = (int)std::distance(eventOrder.begin(), redacted); - emit dataChanged(index(row, 0), index(row, 0)); - } - - continue; // don't insert redaction into timeline - } - - this->events.insert(id, e); - ids.push_back(id); - } - return ids; -} - -void -TimelineModel::fetchHistory() -{ - if (paginationInProgress) { - nhlog::ui()->warn("Already loading older messages"); - return; - } - - paginationInProgress = true; - mtx::http::MessagesOpts opts; - opts.room_id = room_id_.toStdString(); - opts.from = prev_batch_token_.toStdString(); - - nhlog::ui()->info("Paginationg room {}", opts.room_id); - - http::client()->messages( - opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error("failed to call /messages ({}): {} - {}", - opts.room_id, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - paginationInProgress = false; - return; - } - - emit oldMessagesRetrieved(std::move(res)); - paginationInProgress = false; - }); -} - -void -TimelineModel::setCurrentIndex(int index) -{ - auto oldIndex = idToIndex(currentId); - currentId = indexToId(index); - emit currentIndexChanged(index); - - if (oldIndex < index && !pending.contains(currentId)) { - readEvent(currentId.toStdString()); - } -} - -void -TimelineModel::readEvent(const std::string &id) -{ - http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to read_event ({}, {})", - room_id_.toStdString(), - currentId.toStdString()); - } - }); -} - -void -TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) -{ - std::vector ids = internalAddEvents(msgs.chunk); - - if (!ids.empty()) { - beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1)); - this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); - endInsertRows(); - } - - prev_batch_token_ = QString::fromStdString(msgs.end); -} - -QColor -TimelineModel::userColor(QString id, QColor background) -{ - if (!userColors.contains(id)) - userColors.insert( - id, QColor(utils::generateContrastingHexColor(id, background.name()))); - return userColors.value(id); -} - -QString -TimelineModel::displayName(QString id) const -{ - return Cache::displayName(room_id_, id); -} - -QString -TimelineModel::avatarUrl(QString id) const -{ - return Cache::avatarUrl(room_id_, id); -} - -QString -TimelineModel::formatDateSeparator(QDate date) const -{ - auto now = QDateTime::currentDateTime(); - - QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); - - if (now.date().year() == date.year()) { - QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); - fmt = fmt.remove(rx); - } - - return date.toString(fmt); -} - -QString -TimelineModel::escapeEmoji(QString str) const -{ - return utils::replaceEmoji(str); -} - -void -TimelineModel::viewRawMessage(QString id) const -{ - std::string ev = utils::serialize_event(events.value(id)).dump(4); - auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); - Q_UNUSED(dialog); -} - -void - -TimelineModel::openUserProfile(QString userid) const -{ - MainWindow::instance()->openUserProfile(userid, room_id_); -} - -DecryptionResult -TimelineModel::decryptEvent(const mtx::events::EncryptedEvent &e) const -{ - MegolmSessionIndex index; - index.room_id = room_id_.toStdString(); - index.session_id = e.content.session_id; - index.sender_key = e.content.sender_key; - - mtx::events::RoomEvent dummy; - dummy.origin_server_ts = e.origin_server_ts; - dummy.event_id = e.event_id; - dummy.sender = e.sender; - dummy.content.body = - tr("-- Encrypted Event (No keys found for decryption) --", - "Placeholder, when the message was not decrypted yet or can't be decrypted") - .toStdString(); - - try { - if (!cache::client()->inboundMegolmSessionExists(index)) { - nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", - index.room_id, - index.session_id, - e.sender); - // TODO: request megolm session_id & session_key from the sender. - return {dummy, false}; - } - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); - dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --", - "Placeholder, when the message can't be decrypted, because " - "the DB access failed when trying to lookup the session.") - .toStdString(); - return {dummy, false}; - } - - std::string msg_str; - try { - auto session = cache::client()->getInboundMegolmSession(index); - auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (failed to retrieve megolm keys from db) --", - "Placeholder, when the message can't be decrypted, because the DB access " - "failed.") - .toStdString(); - return {dummy, false}; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - tr("-- Decryption Error (%1) --", - "Placeholder, when the message can't be decrypted. In this case, the Olm " - "decrytion returned an error, which is passed ad %1") - .arg(e.what()) - .toStdString(); - return {dummy, false}; - } - - // Add missing fields for the event. - json body = json::parse(msg_str); - body["event_id"] = e.event_id; - body["sender"] = e.sender; - body["origin_server_ts"] = e.origin_server_ts; - body["unsigned"] = e.unsigned_data; - - json event_array = json::array(); - event_array.push_back(body); - - std::vector temp_events; - mtx::responses::utils::parse_timeline_events(event_array, temp_events); - - if (temp_events.size() == 1) - return {temp_events.at(0), true}; - - dummy.content.body = - tr("-- Encrypted Event (Unknown event type) --", - "Placeholder, when the message was decrypted, but we couldn't parse it, because " - "Nheko/mtxclient don't support that event type yet") - .toStdString(); - return {dummy, false}; -} - -void -TimelineModel::replyAction(QString id) -{ - auto event = events.value(id); - RelatedInfo related = boost::apply_visitor( - [](const auto &ev) -> RelatedInfo { - RelatedInfo related_ = {}; - related_.quoted_user = QString::fromStdString(ev.sender); - related_.related_event = ev.event_id; - return related_; - }, - event); - related.type = mtx::events::getMessageType(boost::apply_visitor( - [](const auto &e) -> std::string { return eventMsgType(e); }, event)); - related.quoted_body = boost::apply_visitor( - [](const auto &e) -> QString { return eventFormattedBody(e); }, event); - related.quoted_body.remove(QRegularExpression( - ".*", QRegularExpression::DotMatchesEverythingOption)); - nhlog::ui()->debug("after replacement: {}", related.quoted_body.toStdString()); - related.room = room_id_; - - if (related.quoted_body.isEmpty()) - return; - - ChatPage::instance()->messageReply(related); -} - -void -TimelineModel::readReceiptsAction(QString id) const -{ - MainWindow::instance()->openReadReceiptsDialog(id); -} - -void -TimelineModel::redactEvent(QString id) -{ - if (!id.isEmpty()) - http::client()->redact_event( - room_id_.toStdString(), - id.toStdString(), - [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - emit redactionFailed( - tr("Message redaction failed: %1") - .arg(QString::fromStdString(err->matrix_error.error))); - return; - } - - emit eventRedacted(id); - }); -} - -int -TimelineModel::idToIndex(QString id) const -{ - if (id.isEmpty()) - return -1; - for (int i = 0; i < (int)eventOrder.size(); i++) - if (id == eventOrder[i]) - return i; - return -1; -} - -QString -TimelineModel::indexToId(int index) const -{ - if (index < 0 || index >= (int)eventOrder.size()) - return ""; - return eventOrder[index]; -} - -// Note: this will only be called for our messages -void -TimelineModel::markEventsAsRead(const std::vector &event_ids) -{ - for (const auto &id : event_ids) { - read.insert(id); - int idx = idToIndex(id); - if (idx < 0) { - nhlog::ui()->warn("Read index out of range"); - return; - } - emit dataChanged(index(idx, 0), index(idx, 0)); - } -} - -void -TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json content) -{ - const auto room_id = room_id_.toStdString(); - - using namespace mtx::events; - using namespace mtx::identifiers; - - json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; - - try { - // Check if we have already an outbound megolm session then we can use. - if (cache::client()->outboundMegolmSessionExists(room_id)) { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client()->send_room_message( - room_id, - txn_id, - data, - [this, txn_id](const mtx::responses::EventId &res, - mtx::http::RequestErr err) { - if (err) { - const int status_code = - static_cast(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(QString::fromStdString(txn_id)); - } - emit messageSent( - QString::fromStdString(txn_id), - QString::fromStdString(res.event_id.to_string())); - }); - return; - } - - nhlog::ui()->debug("creating new outbound megolm session"); - - // Create a new outbound megolm session. - auto outbound_session = olm::client()->init_outbound_group_session(); - const auto session_id = mtx::crypto::session_id(outbound_session.get()); - const auto session_key = mtx::crypto::session_key(outbound_session.get()); - - // TODO: needs to be moved in the lib. - auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, - {"room_id", room_id}, - {"session_id", session_id}, - {"session_key", session_key}}; - - // Saving the new megolm session. - // TODO: Maybe it's too early to save. - OutboundGroupSessionData session_data; - session_data.session_id = session_id; - session_data.session_key = session_key; - session_data.message_index = 0; // TODO Update me - cache::client()->saveOutboundMegolmSession( - room_id, session_data, std::move(outbound_session)); - - const auto members = cache::client()->roomMembers(room_id); - nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); - - auto keeper = - std::make_shared([megolm_payload, room_id, doc, txn_id, this]() { - try { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client() - ->send_room_message( - room_id, - txn_id, - data, - [this, txn_id](const mtx::responses::EventId &res, - mtx::http::RequestErr err) { - if (err) { - const int status_code = - static_cast(err->status_code); - nhlog::net()->warn( - "[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed( - QString::fromStdString(txn_id)); - } - emit messageSent( - QString::fromStdString(txn_id), - QString::fromStdString(res.event_id.to_string())); - }); - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to save megolm outbound session: {}", e.what()); - } - }); - - mtx::requests::QueryKeys req; - for (const auto &member : members) - req.device_keys[member] = {}; - - http::client()->query_keys( - req, - [keeper = std::move(keeper), megolm_payload, this]( - const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to query device keys: {} {}", - err->matrix_error.error, - static_cast(err->status_code)); - // TODO: Mark the event as failed. Communicate with the UI. - return; - } - - for (const auto &user : res.device_keys) { - // Mapping from a device_id with valid identity keys to the - // generated room_key event used for sharing the megolm session. - std::map room_key_msgs; - std::map deviceKeys; - - room_key_msgs.clear(); - deviceKeys.clear(); - - for (const auto &dev : user.second) { - const auto user_id = ::UserId(dev.second.user_id); - const auto device_id = DeviceId(dev.second.device_id); - - const auto device_keys = dev.second.keys; - const auto curveKey = "curve25519:" + device_id.get(); - const auto edKey = "ed25519:" + device_id.get(); - - if ((device_keys.find(curveKey) == device_keys.end()) || - (device_keys.find(edKey) == device_keys.end())) { - nhlog::net()->debug( - "ignoring malformed keys for device {}", - device_id.get()); - continue; - } - - DevicePublicKeys pks; - pks.ed25519 = device_keys.at(edKey); - pks.curve25519 = device_keys.at(curveKey); - - try { - if (!mtx::crypto::verify_identity_signature( - json(dev.second), device_id, user_id)) { - nhlog::crypto()->warn( - "failed to verify identity keys: {}", - json(dev.second).dump(2)); - continue; - } - } catch (const json::exception &e) { - nhlog::crypto()->warn( - "failed to parse device key json: {}", - e.what()); - continue; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->warn( - "failed to verify device key json: {}", - e.what()); - continue; - } - - auto room_key = olm::client() - ->create_room_key_event( - user_id, pks.ed25519, megolm_payload) - .dump(); - - room_key_msgs.emplace(device_id, room_key); - deviceKeys.emplace(device_id, pks); - } - - std::vector valid_devices; - valid_devices.reserve(room_key_msgs.size()); - for (auto const &d : room_key_msgs) { - valid_devices.push_back(d.first); - - nhlog::net()->info("{}", d.first); - nhlog::net()->info(" curve25519 {}", - deviceKeys.at(d.first).curve25519); - nhlog::net()->info(" ed25519 {}", - deviceKeys.at(d.first).ed25519); - } - - nhlog::net()->info( - "sending claim request for user {} with {} devices", - user.first, - valid_devices.size()); - - http::client()->claim_keys( - user.first, - valid_devices, - std::bind(&TimelineModel::handleClaimedKeys, - this, - keeper, - room_key_msgs, - deviceKeys, - user.first, - std::placeholders::_1, - std::placeholders::_2)); - - // TODO: Wait before sending the next batch of requests. - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - }); - - // TODO: Let the user know about the errors. - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } -} - -void -TimelineModel::handleClaimedKeys(std::shared_ptr keeper, - const std::map &room_keys, - const std::map &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err) -{ - if (err) { - nhlog::net()->warn("claim keys error: {} {} {}", - err->matrix_error.error, - err->parse_error, - static_cast(err->status_code)); - return; - } - - nhlog::net()->debug("claimed keys for {}", user_id); - - if (res.one_time_keys.size() == 0) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - auto retrieved_devices = res.one_time_keys.at(user_id); - - // Payload with all the to_device message to be sent. - json body; - body["messages"][user_id] = json::object(); - - for (const auto &rd : retrieved_devices) { - const auto device_id = rd.first; - nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); - - // TODO: Verify signatures - auto otk = rd.second.begin()->at("key"); - - if (pks.find(device_id) == pks.end()) { - nhlog::net()->critical("couldn't find public key for device: {}", - device_id); - continue; - } - - auto id_key = pks.at(device_id).curve25519; - auto s = olm::client()->create_outbound_session(id_key, otk); - - if (room_keys.find(device_id) == room_keys.end()) { - nhlog::net()->critical("couldn't find m.room_key for device: {}", - device_id); - continue; - } - - auto device_msg = olm::client()->create_olm_encrypted_content( - s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); - - try { - cache::client()->saveOlmSession(id_key, std::move(s)); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to pickle outbound olm session: {}", - e.what()); - } - - body["messages"][user_id][device_id] = device_msg; - } - - nhlog::net()->info("send_to_device: {}", user_id); - - http::client()->send_to_device( - "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - - (void)keeper; - }); -} diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h deleted file mode 100644 index 31e41315..00000000 --- a/src/timeline2/TimelineModel.h +++ /dev/null @@ -1,258 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include - -#include "Cache.h" -#include "Logging.h" -#include "MatrixClient.h" - -namespace qml_mtx_events { -Q_NAMESPACE - -enum EventType -{ - // Unsupported event - Unsupported, - /// m.room_key_request - KeyRequest, - /// m.room.aliases - Aliases, - /// m.room.avatar - Avatar, - /// m.room.canonical_alias - CanonicalAlias, - /// m.room.create - Create, - /// m.room.encrypted. - Encrypted, - /// m.room.encryption. - Encryption, - /// m.room.guest_access - GuestAccess, - /// m.room.history_visibility - HistoryVisibility, - /// m.room.join_rules - JoinRules, - /// m.room.member - Member, - /// m.room.name - Name, - /// m.room.power_levels - PowerLevels, - /// m.room.tombstone - Tombstone, - /// m.room.topic - Topic, - /// m.room.redaction - Redaction, - /// m.room.pinned_events - PinnedEvents, - // m.sticker - Sticker, - // m.tag - Tag, - /// m.room.message - AudioMessage, - EmoteMessage, - FileMessage, - ImageMessage, - LocationMessage, - NoticeMessage, - TextMessage, - VideoMessage, - Redacted, - UnknownMessage, -}; -Q_ENUM_NS(EventType) - -enum EventState -{ - //! The plaintext message was received by the server. - Received, - //! At least one of the participants has read the message. - Read, - //! The client sent the message. Not yet received. - Sent, - //! When the message is loaded from cache or backfill. - Empty, - //! When the message failed to send - Failed, -}; -Q_ENUM_NS(EventState) -} - -class StateKeeper -{ -public: - StateKeeper(std::function &&fn) - : fn_(std::move(fn)) - {} - - ~StateKeeper() { fn_(); } - -private: - std::function fn_; -}; - -struct DecryptionResult -{ - //! The decrypted content as a normal plaintext event. - mtx::events::collections::TimelineEvents event; - //! Whether or not the decryption was successful. - bool isDecrypted = false; -}; - -class TimelineViewManager; - -class TimelineModel : public QAbstractListModel -{ - Q_OBJECT - Q_PROPERTY( - int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) - -public: - explicit TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent = 0); - - enum Roles - { - Section, - Type, - Body, - FormattedBody, - UserId, - UserName, - Timestamp, - Url, - ThumbnailUrl, - Filename, - Filesize, - MimeType, - Height, - Width, - ProportionalHeight, - Id, - State, - IsEncrypted, - ReplyTo, - }; - - QHash roleNames() const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - Q_INVOKABLE QColor userColor(QString id, QColor background); - Q_INVOKABLE QString displayName(QString id) const; - Q_INVOKABLE QString avatarUrl(QString id) const; - Q_INVOKABLE QString formatDateSeparator(QDate date) const; - - Q_INVOKABLE QString escapeEmoji(QString str) const; - Q_INVOKABLE void viewRawMessage(QString id) const; - Q_INVOKABLE void openUserProfile(QString userid) const; - Q_INVOKABLE void replyAction(QString id); - Q_INVOKABLE void readReceiptsAction(QString id) const; - Q_INVOKABLE void redactEvent(QString id); - Q_INVOKABLE int idToIndex(QString id) const; - Q_INVOKABLE QString indexToId(int index) const; - - void addEvents(const mtx::responses::Timeline &events); - template - void sendMessage(const T &msg); - -public slots: - void fetchHistory(); - void setCurrentIndex(int index); - int currentIndex() const { return idToIndex(currentId); } - void markEventsAsRead(const std::vector &event_ids); - -private slots: - // Add old events at the top of the timeline. - void addBackwardsEvents(const mtx::responses::Messages &msgs); - -signals: - void oldMessagesRetrieved(const mtx::responses::Messages &res); - void messageFailed(QString txn_id); - void messageSent(QString txn_id, QString event_id); - void currentIndexChanged(int index); - void redactionFailed(QString id); - void eventRedacted(QString id); - -private: - DecryptionResult decryptEvent( - const mtx::events::EncryptedEvent &e) const; - std::vector internalAddEvents( - const std::vector &timeline); - void sendEncryptedMessage(const std::string &txn_id, nlohmann::json content); - void handleClaimedKeys(std::shared_ptr keeper, - const std::map &room_key, - const std::map &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err); - void updateLastMessage(); - void readEvent(const std::string &id); - - QHash events; - QSet pending, failed, read; - std::vector eventOrder; - - QString room_id_; - QString prev_batch_token_; - - bool isInitialSync = true; - bool paginationInProgress = false; - - QHash userColors; - QString currentId; - - TimelineViewManager *manager_; -}; - -template -void -TimelineModel::sendMessage(const T &msg) -{ - auto txn_id = http::client()->generate_txn_id(); - mtx::events::RoomEvent msgCopy = {}; - msgCopy.content = msg; - msgCopy.type = mtx::events::EventType::RoomMessage; - msgCopy.event_id = txn_id; - msgCopy.sender = http::client()->user_id().to_string(); - msgCopy.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - internalAddEvents({msgCopy}); - - QString txn_id_qstr = QString::fromStdString(txn_id); - beginInsertRows(QModelIndex(), - static_cast(this->eventOrder.size()), - static_cast(this->eventOrder.size())); - pending.insert(txn_id_qstr); - this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); - endInsertRows(); - updateLastMessage(); - - if (cache::client()->isRoomEncrypted(room_id_.toStdString())) - sendEncryptedMessage(txn_id, nlohmann::json(msg)); - else - http::client()->send_room_message( - room_id_.toStdString(), - txn_id, - msg, - [this, txn_id, txn_id_qstr](const mtx::responses::EventId &res, - mtx::http::RequestErr err) { - if (err) { - const int status_code = static_cast(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(txn_id_qstr); - } - emit messageSent(txn_id_qstr, - QString::fromStdString(res.event_id.to_string())); - }); -} diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp deleted file mode 100644 index d733ad90..00000000 --- a/src/timeline2/TimelineViewManager.cpp +++ /dev/null @@ -1,400 +0,0 @@ -#include "TimelineViewManager.h" - -#include -#include -#include -#include -#include -#include - -#include "ChatPage.h" -#include "ColorImageProvider.h" -#include "DelegateChooser.h" -#include "Logging.h" -#include "MxcImageProvider.h" -#include "UserSettingsPage.h" -#include "dialogs/ImageOverlay.h" - -void -TimelineViewManager::updateColorPalette() -{ - UserSettings settings; - if (settings.theme() == "light") { - QPalette lightActive(/*windowText*/ QColor("#333"), - /*button*/ QColor("#333"), - /*light*/ QColor(), - /*dark*/ QColor(220, 220, 220, 120), - /*mid*/ QColor(), - /*text*/ QColor("#333"), - /*bright_text*/ QColor(), - /*base*/ QColor("white"), - /*window*/ QColor("white")); - view->rootContext()->setContextProperty("currentActivePalette", lightActive); - view->rootContext()->setContextProperty("currentInactivePalette", lightActive); - } else if (settings.theme() == "dark") { - QPalette darkActive(/*windowText*/ QColor("#caccd1"), - /*button*/ QColor("#caccd1"), - /*light*/ QColor(), - /*dark*/ QColor(45, 49, 57, 120), - /*mid*/ QColor(), - /*text*/ QColor("#caccd1"), - /*bright_text*/ QColor(), - /*base*/ QColor("#202228"), - /*window*/ QColor("#202228")); - darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9")); - view->rootContext()->setContextProperty("currentActivePalette", darkActive); - view->rootContext()->setContextProperty("currentInactivePalette", darkActive); - } else { - view->rootContext()->setContextProperty("currentActivePalette", QPalette()); - view->rootContext()->setContextProperty("currentInactivePalette", nullptr); - } -} - -TimelineViewManager::TimelineViewManager(QWidget *parent) - : imgProvider(new MxcImageProvider()) - , colorImgProvider(new ColorImageProvider()) -{ - qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, - "com.github.nheko", - 1, - 0, - "MtxEvent", - "Can't instantiate enum!"); - qmlRegisterType("com.github.nheko", 1, 0, "DelegateChoice"); - qmlRegisterType("com.github.nheko", 1, 0, "DelegateChooser"); - -#ifdef USE_QUICK_VIEW - view = new QQuickView(); - container = QWidget::createWindowContainer(view, parent); -#else - view = new QQuickWidget(parent); - container = view; - view->setResizeMode(QQuickWidget::SizeRootObjectToView); - container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - - connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { - nhlog::ui()->debug("Status changed to {}", status); - }); -#endif - container->setMinimumSize(200, 200); - view->rootContext()->setContextProperty("timelineManager", this); - updateColorPalette(); - view->engine()->addImageProvider("MxcImage", imgProvider); - view->engine()->addImageProvider("colorimage", colorImgProvider); - view->setSource(QUrl("qrc:///qml/TimelineView.qml")); - - connect(dynamic_cast(parent), - &ChatPage::themeChanged, - this, - &TimelineViewManager::updateColorPalette); -} - -void -TimelineViewManager::sync(const mtx::responses::Rooms &rooms) -{ - for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { - // addRoom will only add the room, if it doesn't exist - addRoom(QString::fromStdString(it->first)); - models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline); - } -} - -void -TimelineViewManager::addRoom(const QString &room_id) -{ - if (!models.contains(room_id)) - models.insert(room_id, - QSharedPointer(new TimelineModel(this, room_id))); -} - -void -TimelineViewManager::setHistoryView(const QString &room_id) -{ - nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); - - auto room = models.find(room_id); - if (room != models.end()) { - timeline_ = room.value().data(); - emit activeTimelineChanged(timeline_); - nhlog::ui()->info("Activated room {}", room_id.toStdString()); - } -} - -void -TimelineViewManager::openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const -{ - QQuickImageResponse *imgResponse = - imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize()); - connect(imgResponse, - &QQuickImageResponse::finished, - this, - [this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() { - if (!imgResponse->errorString().isEmpty()) { - nhlog::ui()->error("Error when retrieving image for overlay: {}", - imgResponse->errorString().toStdString()); - return; - } - auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); - - auto imgDialog = new dialogs::ImageOverlay(pixmap); - imgDialog->show(); - connect(imgDialog, - &dialogs::ImageOverlay::saving, - this, - [this, mxcUrl, originalFilename, mimeType, eventType]() { - saveMedia(mxcUrl, originalFilename, mimeType, eventType); - }); - }); -} - -void -TimelineViewManager::saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const -{ - QString dialogTitle; - if (eventType == qml_mtx_events::EventType::ImageMessage) { - dialogTitle = tr("Save image"); - } else if (eventType == qml_mtx_events::EventType::VideoMessage) { - dialogTitle = tr("Save video"); - } else if (eventType == qml_mtx_events::EventType::AudioMessage) { - dialogTitle = tr("Save audio"); - } else { - dialogTitle = tr("Save file"); - } - - QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); - - auto filename = - QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString); - - if (filename.isEmpty()) - return; - - const auto url = mxcUrl.toStdString(); - - http::client()->download( - url, - [filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - try { - QFile file(filename); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(QByteArray(data.data(), data.size())); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - }); -} - -void -TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType) -{ - // If the message is a link to a non mxcUrl, don't download it - if (!mxcUrl.startsWith("mxc://")) { - emit mediaCached(mxcUrl, mxcUrl); - return; - } - - QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); - - const auto url = mxcUrl.toStdString(); - QFileInfo filename(QString("%1/media_cache/%2.%3") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString(mxcUrl).remove("mxc://")) - .arg(suffix)); - if (QDir::cleanPath(filename.path()) != filename.path()) { - nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); - return; - } - - QDir().mkpath(filename.path()); - - if (filename.isReadable()) { - emit mediaCached(mxcUrl, filename.filePath()); - return; - } - - http::client()->download( - url, - [this, mxcUrl, filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - try { - QFile file(filename.filePath()); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(QByteArray(data.data(), data.size())); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - - emit mediaCached(mxcUrl, filename.filePath()); - }); -} - -void -TimelineViewManager::updateReadReceipts(const QString &room_id, - const std::vector &event_ids) -{ - auto room = models.find(room_id); - if (room != models.end()) { - room.value()->markEventsAsRead(event_ids); - } -} - -void -TimelineViewManager::initWithMessages(const std::map &msgs) -{ - for (const auto &e : msgs) { - addRoom(e.first); - - models.value(e.first)->addEvents(e.second); - } -} - -void -TimelineViewManager::queueTextMessage(const QString &msg) -{ - mtx::events::msg::Text text = {}; - text.body = msg.trimmed().toStdString(); - text.format = "org.matrix.custom.html"; - text.formatted_body = utils::markdownToHtml(msg).toStdString(); - - if (timeline_) - timeline_->sendMessage(text); -} - -void -TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) -{ - mtx::events::msg::Text text = {}; - - QString body; - bool firstLine = true; - for (const auto &line : related.quoted_body.split("\n")) { - if (firstLine) { - firstLine = false; - body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line); - } else { - body = QString("%1\n> %2\n").arg(body).arg(line); - } - } - - text.body = QString("%1\n%2").arg(body).arg(reply).toStdString(); - text.format = "org.matrix.custom.html"; - text.formatted_body = - utils::getFormattedQuoteBody(related, utils::markdownToHtml(reply)).toStdString(); - text.relates_to.in_reply_to.event_id = related.related_event; - - if (timeline_) - timeline_->sendMessage(text); -} - -void -TimelineViewManager::queueEmoteMessage(const QString &msg) -{ - auto html = utils::markdownToHtml(msg); - - mtx::events::msg::Emote emote; - emote.body = msg.trimmed().toStdString(); - - if (html != msg.trimmed().toHtmlEscaped()) - emote.formatted_body = html.toStdString(); - - if (timeline_) - timeline_->sendMessage(emote); -} - -void -TimelineViewManager::queueImageMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize, - const QSize &dimensions) -{ - mtx::events::msg::Image image; - image.info.mimetype = mime.toStdString(); - image.info.size = dsize; - image.body = filename.toStdString(); - image.url = url.toStdString(); - image.info.h = dimensions.height(); - image.info.w = dimensions.width(); - models.value(roomid)->sendMessage(image); -} - -void -TimelineViewManager::queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::File file; - file.info.mimetype = mime.toStdString(); - file.info.size = dsize; - file.body = filename.toStdString(); - file.url = url.toStdString(); - models.value(roomid)->sendMessage(file); -} - -void -TimelineViewManager::queueAudioMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::Audio audio; - audio.info.mimetype = mime.toStdString(); - audio.info.size = dsize; - audio.body = filename.toStdString(); - audio.url = url.toStdString(); - models.value(roomid)->sendMessage(audio); -} - -void -TimelineViewManager::queueVideoMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize) -{ - mtx::events::msg::Video video; - video.info.mimetype = mime.toStdString(); - video.info.size = dsize; - video.body = filename.toStdString(); - video.url = url.toStdString(); - models.value(roomid)->sendMessage(video); -} diff --git a/src/timeline2/TimelineViewManager.h b/src/timeline2/TimelineViewManager.h deleted file mode 100644 index 691c8ddb..00000000 --- a/src/timeline2/TimelineViewManager.h +++ /dev/null @@ -1,117 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#include "Cache.h" -#include "Logging.h" -#include "TimelineModel.h" -#include "Utils.h" - -// temporary for stubs -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - -class MxcImageProvider; -class ColorImageProvider; - -class TimelineViewManager : public QObject -{ - Q_OBJECT - - Q_PROPERTY( - TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) - -public: - TimelineViewManager(QWidget *parent = 0); - QWidget *getWidget() const { return container; } - - void sync(const mtx::responses::Rooms &rooms); - void addRoom(const QString &room_id); - - void clearAll() { models.clear(); } - - Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } - void openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const; - void saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const; - Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType); - // Qml can only pass enum as int - Q_INVOKABLE void openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - int eventType) const - { - openImageOverlay( - mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); - } - Q_INVOKABLE void saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - int eventType) const - { - saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); - } - -signals: - void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(QString roomid, const DescInfo &info); - void activeTimelineChanged(TimelineModel *timeline); - void mediaCached(QString mxcUrl, QString cacheUrl); - -public slots: - void updateReadReceipts(const QString &room_id, const std::vector &event_ids); - void initWithMessages(const std::map &msgs); - - void setHistoryView(const QString &room_id); - void updateColorPalette(); - - void queueTextMessage(const QString &msg); - void queueReplyMessage(const QString &reply, const RelatedInfo &related); - void queueEmoteMessage(const QString &msg); - void queueImageMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize, - const QSize &dimensions); - void queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize); - void queueAudioMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize); - void queueVideoMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize); - -private: -#ifdef USE_QUICK_VIEW - QQuickView *view; -#else - QQuickWidget *view; -#endif - QWidget *container; - TimelineModel *timeline_ = nullptr; - MxcImageProvider *imgProvider; - ColorImageProvider *colorImgProvider; - - QHash> models; -}; - -#pragma GCC diagnostic pop -- cgit 1.5.1