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.
pull/76/head
Joseph Donofry 6 years ago
parent 9f310fed09
commit 129beb57c9
No known key found for this signature in database
GPG Key ID: E8A1D78EF044B0CB
  1. 4
      src/ChatPage.cpp
  2. 5
      src/ChatPage.h
  3. 30
      src/TextInputWidget.cpp
  4. 17
      src/TextInputWidget.h
  5. 14
      src/Utils.cpp
  6. 13
      src/Utils.h
  7. 10
      src/popups/PopupItem.cpp
  8. 1
      src/popups/PopupItem.h
  9. 88
      src/popups/ReplyPopup.cpp
  10. 16
      src/popups/ReplyPopup.h
  11. 8
      src/timeline/TimelineItem.cpp
  12. 31
      src/timeline/TimelineView.cpp
  13. 5
      src/timeline/TimelineView.h
  14. 5
      src/timeline/TimelineViewManager.cpp
  15. 4
      src/timeline/TimelineViewManager.h

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save