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.
This commit is contained in:
Joseph Donofry 2019-06-13 22:33:04 -04:00
parent 9f310fed09
commit 129beb57c9
No known key found for this signature in database
GPG Key ID: E8A1D78EF044B0CB
15 changed files with 177 additions and 74 deletions

View File

@ -265,9 +265,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
SLOT(queueTextMessage(const QString &))); SLOT(queueTextMessage(const QString &)));
connect(text_input_, connect(text_input_,
SIGNAL(sendReplyMessage(const QString &, const QString &)), SIGNAL(sendReplyMessage(const QString &, const RelatedInfo &)),
view_manager_, view_manager_,
SLOT(queueReplyMessage(const QString &, const QString &))); SLOT(queueReplyMessage(const QString &, const RelatedInfo &)));
connect(text_input_, connect(text_input_,
SIGNAL(sendEmoteMessage(const QString &)), SIGNAL(sendEmoteMessage(const QString &)),

View File

@ -30,6 +30,7 @@
#include "Cache.h" #include "Cache.h"
#include "CommunitiesList.h" #include "CommunitiesList.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "Utils.h"
#include "notifications/Manager.h" #include "notifications/Manager.h"
class OverlayModal; class OverlayModal;
@ -83,9 +84,7 @@ signals:
void connectionLost(); void connectionLost();
void connectionRestored(); void connectionRestored();
void messageReply(const QString &username, void messageReply(const RelatedInfo &related);
const QString &msg,
const QString &related_event);
void notificationsRetrieved(const mtx::responses::Notifications &); void notificationsRetrieved(const mtx::responses::Notifications &);

View File

@ -93,6 +93,8 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent)
cursor.insertText(text); cursor.insertText(text);
}); });
connect(&replyPopup_, &ReplyPopup::cancel, this, [this]() { closeReply(); });
// For cycling through the suggestions by hitting tab. // For cycling through the suggestions by hitting tab.
connect(this, connect(this,
&FilteredTextEdit::selectNextSuggestion, &FilteredTextEdit::selectNextSuggestion,
@ -219,6 +221,7 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
if (!(event->modifiers() & Qt::ShiftModifier)) { if (!(event->modifiers() & Qt::ShiftModifier)) {
stopTyping(); stopTyping();
submit(); submit();
closeReply();
} else { } else {
QTextEdit::keyPressEvent(event); QTextEdit::keyPressEvent(event);
} }
@ -415,8 +418,8 @@ FilteredTextEdit::submit()
auto name = text.mid(1, command_end - 1); auto name = text.mid(1, command_end - 1);
auto args = text.mid(command_end + 1); auto args = text.mid(command_end + 1);
if (name.isEmpty() || name == "/") { if (name.isEmpty() || name == "/") {
if (!related_event_.isEmpty()) { if (!related_.related_event.empty()) {
reply(args, related_event_); reply(args, related_);
} else { } else {
message(args); message(args);
} }
@ -424,14 +427,14 @@ FilteredTextEdit::submit()
command(name, args); command(name, args);
} }
} else { } else {
if (!related_event_.isEmpty()) { if (!related_.related_event.empty()) {
reply(std::move(text), std::move(related_event_)); reply(std::move(text), std::move(related_));
} else { } else {
message(std::move(text)); message(std::move(text));
} }
} }
related_event_ = ""; related_ = {};
clear(); clear();
} }
@ -439,16 +442,8 @@ FilteredTextEdit::submit()
void void
FilteredTextEdit::showReplyPopup(const QString &user, const QString &msg, const QString &event_id) 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_.setReplyContent(user, msg, event_id);
replyPopup_.move(pos.x(), pos.y() - replyPopup_.height() - 10); replyPopup_.move(pos.x(), pos.y() - replyPopup_.height() - 10);
replyPopup_.show(); replyPopup_.show();
@ -699,14 +694,15 @@ TextInputWidget::paintEvent(QPaintEvent *)
} }
void 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_->setText(QString("> %1: %2\n\n").arg(username).arg(msg));
input_->setFocus(); 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(); auto cursor = input_->textCursor();
cursor.movePosition(QTextCursor::End); cursor.movePosition(QTextCursor::End);
input_->setTextCursor(cursor); input_->setTextCursor(cursor);
input_->setRelatedEvent(replied_event); input_->setRelated(related);
} }

View File

@ -28,6 +28,7 @@
#include <QTextEdit> #include <QTextEdit>
#include <QWidget> #include <QWidget>
#include "Utils.h"
#include "dialogs/PreviewUploadOverlay.h" #include "dialogs/PreviewUploadOverlay.h"
#include "emoji/PickButton.h" #include "emoji/PickButton.h"
#include "popups/ReplyPopup.h" #include "popups/ReplyPopup.h"
@ -55,7 +56,7 @@ public:
QSize minimumSizeHint() const override; QSize minimumSizeHint() const override;
void submit(); 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); void showReplyPopup(const QString &user, const QString &msg, const QString &event_id);
signals: signals:
@ -64,7 +65,7 @@ signals:
void stoppedTyping(); void stoppedTyping();
void startedUpload(); void startedUpload();
void message(QString); void message(QString);
void reply(QString, QString); void reply(QString, const RelatedInfo &);
void command(QString name, QString args); void command(QString name, QString args);
void image(QSharedPointer<QIODevice> data, const QString &filename); void image(QSharedPointer<QIODevice> data, const QString &filename);
void audio(QSharedPointer<QIODevice> data, const QString &filename); void audio(QSharedPointer<QIODevice> data, const QString &filename);
@ -100,7 +101,7 @@ private:
ReplyPopup replyPopup_; ReplyPopup replyPopup_;
// Used for replies // Used for replies
QString related_event_; RelatedInfo related_;
enum class AnchorType enum class AnchorType
{ {
@ -113,7 +114,11 @@ private:
int anchorWidth(AnchorType anchor) { return static_cast<int>(anchor); } int anchorWidth(AnchorType anchor) { return static_cast<int>(anchor); }
void closeSuggestions() { suggestionsPopup_.hide(); } void closeSuggestions() { suggestionsPopup_.hide(); }
void closeReply() { replyPopup_.hide(); } void closeReply()
{
replyPopup_.hide();
related_ = {};
}
void resetAnchor() { atTriggerPosition_ = -1; } void resetAnchor() { atTriggerPosition_ = -1; }
bool isAnchorValid() { return atTriggerPosition_ != -1; } bool isAnchorValid() { return atTriggerPosition_ != -1; }
bool hasAnchor(int pos, AnchorType anchor) bool hasAnchor(int pos, AnchorType anchor)
@ -167,14 +172,14 @@ public slots:
void openFileSelection(); void openFileSelection();
void hideUploadSpinner(); void hideUploadSpinner();
void focusLineEdit() { input_->setFocus(); } void focusLineEdit() { input_->setFocus(); }
void addReply(const QString &username, const QString &msg, const QString &related_event); void addReply(const RelatedInfo &related);
private slots: private slots:
void addSelectedEmoji(const QString &emoji); void addSelectedEmoji(const QString &emoji);
signals: signals:
void sendTextMessage(QString msg); void sendTextMessage(QString msg);
void sendReplyMessage(QString msg, QString event_id); void sendReplyMessage(QString msg, const RelatedInfo &related);
void sendEmoteMessage(QString msg); void sendEmoteMessage(QString msg);
void heightChanged(int height); void heightChanged(int height);

View File

@ -314,6 +314,20 @@ utils::markdownToHtml(const QString &text)
return result; return result;
} }
QString
utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html)
{
return QString("<mx-reply><blockquote><a "
"href=\"https://matrix.to/#/!%1\">In reply "
"to</a><a href=\"https://matrix.to/#/%2\">%3</a><br "
"/>%4</blockquote></mx-reply>")
.arg(QString::fromStdString(related.related_event),
related.quoted_user,
related.quoted_user,
related.quoted_body) +
html;
}
QString QString
utils::linkColor() utils::linkColor()
{ {

View File

@ -18,6 +18,15 @@
class QComboBox; class QComboBox;
// Contains information about related events for
// outgoing messages
struct RelatedInfo
{
QString quoted_body;
std::string related_event;
QString quoted_user;
};
namespace utils { namespace utils {
using TimelineEvent = mtx::events::collections::TimelineEvents; using TimelineEvent = mtx::events::collections::TimelineEvents;
@ -225,6 +234,10 @@ linkifyMessage(const QString &body);
QString QString
markdownToHtml(const QString &text); 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. //! Retrieve the color of the links based on the current theme.
QString QString
linkColor(); linkColor();

View File

@ -36,6 +36,16 @@ PopupItem::paintEvent(QPaintEvent *)
p.fillRect(rect(), hoverColor_); 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) UserItem::UserItem(QWidget *parent, const QString &user_id)
: PopupItem(parent) : PopupItem(parent)
, userId_{user_id} , userId_{user_id}

View File

@ -50,6 +50,7 @@ class UserItem : public PopupItem
Q_OBJECT Q_OBJECT
public: public:
UserItem(QWidget *parent);
UserItem(QWidget *parent, const QString &user_id); UserItem(QWidget *parent, const QString &user_id);
QString selectedText() const { return userId_; } QString selectedText() const { return userId_; }
void updateItem(const QString &user_id); void updateItem(const QString &user_id);

View File

@ -11,41 +11,69 @@
ReplyPopup::ReplyPopup(QWidget *parent) ReplyPopup::ReplyPopup(QWidget *parent)
: QWidget(parent) : QWidget(parent)
, userItem_{0}
, msgLabel_{0}
, eventLabel_{0}
{ {
setAttribute(Qt::WA_ShowWithoutActivating, true); setAttribute(Qt::WA_ShowWithoutActivating, true);
setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint); setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint);
layout_ = new QVBoxLayout(this); mainLayout_ = new QVBoxLayout(this);
layout_->setMargin(0); mainLayout_->setMargin(0);
layout_->setSpacing(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 void
ReplyPopup::setReplyContent(const QString &user, const QString &msg, const QString &srcEvent) 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. // Update the current widget with the new data.
// auto userWidget = qobject_cast<UserItem *>(item->widget()); userItem_->updateItem(user);
// if (userWidget)
// userWidget->updateItem(users.at(i).user_id); msgLabel_->setText(msg);
// }
eventLabel_->setText(srcEvent);
adjustSize(); adjustSize();
} }
@ -58,3 +86,13 @@ ReplyPopup::paintEvent(QPaintEvent *)
QPainter p(this); QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &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);
}

View File

@ -3,11 +3,13 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QPoint> #include <QPoint>
#include <QVBoxLayout>
#include <QWidget> #include <QWidget>
#include "../AvatarProvider.h" #include "../AvatarProvider.h"
#include "../Cache.h" #include "../Cache.h"
#include "../ChatPage.h" #include "../ChatPage.h"
#include "../ui/FlatButton.h"
#include "PopupItem.h" #include "PopupItem.h"
class ReplyPopup : public QWidget class ReplyPopup : public QWidget
@ -22,10 +24,22 @@ public slots:
protected: protected:
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
signals: signals:
void userSelected(const QString &user); void userSelected(const QString &user);
void clicked(const QString &text);
void cancel();
private: private:
QVBoxLayout *layout_; QHBoxLayout *topLayout_;
QVBoxLayout *mainLayout_;
QHBoxLayout *buttonLayout_;
UserItem *userItem_;
FlatButton *closeBtn_;
QLabel *msgLabel_;
QLabel *eventLabel_;
int buttonSize_;
}; };

View File

@ -874,8 +874,12 @@ TimelineItem::replyAction()
if (!body_) if (!body_)
return; return;
emit ChatPage::instance()->messageReply( RelatedInfo related;
Cache::displayName(room_id_, descriptionMsg_.userid), body_->toPlainText(), eventId()); related.quoted_body = body_->toPlainText();
related.quoted_user = descriptionMsg_.userid;
related.related_event = eventId().toStdString();
emit ChatPage::instance()->messageReply(related);
} }
void void

View File

@ -692,7 +692,7 @@ TimelineView::updatePendingMessage(const std::string &txn_id, const QString &eve
void void
TimelineView::addUserMessage(mtx::events::MessageType ty, TimelineView::addUserMessage(mtx::events::MessageType ty,
const QString &body, const QString &body,
const QString &related_event) const RelatedInfo &related = RelatedInfo())
{ {
auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); 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_); new TimelineItem(ty, local_user_, body, with_sender, room_id_, scroll_widget_);
PendingMessage message; PendingMessage message;
message.ty = ty; message.ty = ty;
message.txn_id = http::client()->generate_txn_id(); message.txn_id = http::client()->generate_txn_id();
message.body = body; message.body = body;
message.widget = view_item; message.widget = view_item;
if (!related_event.isEmpty()) { message.related = related;
message.related_event = related_event.toStdString();
}
try { try {
message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString()); message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString());
@ -730,7 +728,7 @@ TimelineView::addUserMessage(mtx::events::MessageType ty,
void void
TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body)
{ {
addUserMessage(ty, body, ""); addUserMessage(ty, body, RelatedInfo());
} }
void void
@ -1273,13 +1271,20 @@ toRoomMessage<mtx::events::msg::Text>(const PendingMessage &m)
auto html = utils::markdownToHtml(m.body); auto html = utils::markdownToHtml(m.body);
mtx::events::msg::Text text; mtx::events::msg::Text text;
text.body = m.body.trimmed().toStdString(); text.body = m.body.trimmed().toStdString();
if (html != m.body.trimmed().toHtmlEscaped()) if (html != m.body.trimmed().toHtmlEscaped()) {
text.formatted_body = html.toStdString(); 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()) { if (!m.related.related_event.empty()) {
text.relates_to.in_reply_to.event_id = m.related_event; text.relates_to.in_reply_to.event_id = m.related.related_event;
} }
return text; return text;

View File

@ -30,6 +30,7 @@
#include <mtx/events.hpp> #include <mtx/events.hpp>
#include <mtx/responses/messages.hpp> #include <mtx/responses/messages.hpp>
#include "../Utils.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "timeline/TimelineItem.h" #include "timeline/TimelineItem.h"
@ -63,7 +64,7 @@ struct PendingMessage
{ {
mtx::events::MessageType ty; mtx::events::MessageType ty;
std::string txn_id; std::string txn_id;
std::string related_event; RelatedInfo related;
QString body; QString body;
QString filename; QString filename;
QString mime; QString mime;
@ -123,7 +124,7 @@ public:
void addEvents(const mtx::responses::Timeline &timeline); void addEvents(const mtx::responses::Timeline &timeline);
void addUserMessage(mtx::events::MessageType ty, void addUserMessage(mtx::events::MessageType ty,
const QString &body, const QString &body,
const QString &related_event); const RelatedInfo &related);
void addUserMessage(mtx::events::MessageType ty, const QString &msg); void addUserMessage(mtx::events::MessageType ty, const QString &msg);
template<class Widget, mtx::events::MessageType MsgType> template<class Widget, mtx::events::MessageType MsgType>

View File

@ -23,6 +23,7 @@
#include "Cache.h" #include "Cache.h"
#include "Logging.h" #include "Logging.h"
#include "Utils.h"
#include "timeline/TimelineView.h" #include "timeline/TimelineView.h"
#include "timeline/TimelineViewManager.h" #include "timeline/TimelineViewManager.h"
#include "timeline/widgets/AudioItem.h" #include "timeline/widgets/AudioItem.h"
@ -79,7 +80,7 @@ TimelineViewManager::queueEmoteMessage(const QString &msg)
} }
void void
TimelineViewManager::queueReplyMessage(const QString &reply, const QString &related_event) TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related)
{ {
if (active_room_.isEmpty()) if (active_room_.isEmpty())
return; return;
@ -87,7 +88,7 @@ TimelineViewManager::queueReplyMessage(const QString &reply, const QString &rela
auto room_id = active_room_; auto room_id = active_room_;
auto view = views_[room_id]; auto view = views_[room_id];
view->addUserMessage(mtx::events::MessageType::Text, reply, related_event); view->addUserMessage(mtx::events::MessageType::Text, reply, related);
} }
void void

View File

@ -22,6 +22,8 @@
#include <mtx.hpp> #include <mtx.hpp>
#include "Utils.h"
class QFile; class QFile;
class RoomInfoListItem; class RoomInfoListItem;
@ -63,7 +65,7 @@ public slots:
void setHistoryView(const QString &room_id); void setHistoryView(const QString &room_id);
void queueTextMessage(const QString &msg); 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 queueEmoteMessage(const QString &msg);
void queueImageMessage(const QString &roomid, void queueImageMessage(const QString &roomid,
const QString &filename, const QString &filename,