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,