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 &)));
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 &)),

View file

@ -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 &);

View file

@ -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);
}

View file

@ -28,6 +28,7 @@
#include <QTextEdit>
#include <QWidget>
#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<QIODevice> data, const QString &filename);
void audio(QSharedPointer<QIODevice> 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<int>(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);

View file

@ -314,6 +314,20 @@ utils::markdownToHtml(const QString &text)
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
utils::linkColor()
{

View file

@ -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();

View file

@ -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}

View file

@ -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);

View file

@ -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<UserItem *>(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);
}

View file

@ -3,11 +3,13 @@
#include <QHBoxLayout>
#include <QLabel>
#include <QPoint>
#include <QVBoxLayout>
#include <QWidget>
#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_;
};

View file

@ -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

View file

@ -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<mtx::events::msg::Text>(const PendingMessage &m)
auto html = utils::markdownToHtml(m.body);
mtx::events::msg::Text text;
text.body = m.body.trimmed().toStdString();
if (html != m.body.trimmed().toHtmlEscaped())
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;

View file

@ -30,6 +30,7 @@
#include <mtx/events.hpp>
#include <mtx/responses/messages.hpp>
#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<class Widget, mtx::events::MessageType MsgType>

View file

@ -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

View file

@ -22,6 +22,8 @@
#include <mtx.hpp>
#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,