Skip to content

Commit 71734cc

Browse files
committed
Add rich replying and editing support
Cc #596 Closes #447
1 parent 0315b39 commit 71734cc

File tree

7 files changed

+284
-26
lines changed

7 files changed

+284
-26
lines changed

client/chatroomwidget.cpp

Lines changed: 143 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
#include "quaternionroom.h"
5353
#include "chatedit.h"
5454
#include "htmlfilter.h"
55+
#include "models/messageeventmodel.h"
5556

5657
static auto DefaultPlaceholderText()
5758
{
@@ -84,6 +85,13 @@ ChatRoomWidget::ChatRoomWidget(MainWindow* parent)
8485
m_hudCaption->setFont(f);
8586
m_hudCaption->setTextFormat(Qt::RichText);
8687

88+
m_modeIndicator = new QToolButton();
89+
m_modeIndicator->setAutoRaise(true);
90+
m_modeIndicator->hide();
91+
connect(m_modeIndicator, &QToolButton::clicked, this, [this] {
92+
setDefaultMode();
93+
});
94+
8795
auto attachButton = new QToolButton();
8896
attachButton->setAutoRaise(true);
8997
m_attachAction = new QAction(QIcon::fromTheme("mail-attachment"),
@@ -207,6 +215,7 @@ ChatRoomWidget::ChatRoomWidget(MainWindow* parent)
207215
layout->addWidget(m_hudCaption);
208216
{
209217
auto inputLayout = new QHBoxLayout;
218+
inputLayout->addWidget(m_modeIndicator);
210219
inputLayout->addWidget(attachButton);
211220
inputLayout->addWidget(m_chatEdit);
212221
layout->addLayout(inputLayout);
@@ -262,6 +271,7 @@ void ChatRoomWidget::setRoom(QuaternionRoom* newRoom)
262271
}
263272
typingChanged();
264273
encryptionChanged();
274+
setDefaultMode();
265275
}
266276

267277
void ChatRoomWidget::typingChanged()
@@ -416,38 +426,79 @@ void ChatRoomWidget::sendFile()
416426
m_chatEdit->setPlaceholderText(DefaultPlaceholderText());
417427
}
418428

419-
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
420-
void sendMarkdown(QuaternionRoom* room, const QTextDocumentFragment& text)
421-
{
422-
room->postHtmlText(text.toPlainText(),
423-
HtmlFilter::toMatrixHtml(text.toHtml(), room,
424-
HtmlFilter::ConvertMarkdown));
425-
}
426-
#endif
427-
428429
void ChatRoomWidget::sendMessage()
429430
{
430431
if (m_chatEdit->toPlainText().startsWith("//"))
431432
QTextCursor(m_chatEdit->document()).deleteChar();
432433

434+
QTextCursor c(m_chatEdit->document());
435+
c.select(QTextCursor::Document);
436+
sendMessageFromFragment(c.selection());
437+
}
438+
439+
void ChatRoomWidget::sendMessageFromFragment(const QTextDocumentFragment& text,
440+
enum TextFormat textFormat)
441+
{
442+
const auto& plainText = text.toPlainText();
443+
const auto& htmlText =
444+
HtmlFilter::toMatrixHtml(text.toHtml(), currentRoom(),
433445
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
434-
if (m_uiSettings.get("auto_markdown", false)) {
435-
sendMarkdown(currentRoom(),
436-
QTextDocumentFragment(m_chatEdit->document()));
437-
return;
438-
}
446+
((textFormat != Plaintext
447+
&& m_uiSettings.get("auto_markdown", false))
448+
|| textFormat == Markdown)
449+
? HtmlFilter::ConvertMarkdown
450+
:
439451
#endif
440-
const auto& plainText = m_chatEdit->toPlainText();
441-
const auto& htmlText =
442-
HtmlFilter::toMatrixHtml(m_chatEdit->toHtml(), currentRoom());
452+
HtmlFilter::Default);
443453
Q_ASSERT(!plainText.isEmpty() && !htmlText.isEmpty());
444454
// Send plain text if htmlText has no markup or just <br/> elements
445455
// (those are easily represented as line breaks in plain text)
446456
static const QRegularExpression MarkupRE { "<(?![Bb][Rr])" };
447-
if (htmlText.contains(MarkupRE))
448-
currentRoom()->postHtmlText(plainText, htmlText);
449-
else
450-
currentRoom()->postPlainText(plainText);
457+
458+
using namespace Quotient;
459+
switch (mode) {
460+
case Editing:
461+
// Any quotation is ignored intentionally, see
462+
// https://spec.matrix.org/latest/client-server-api/#edits-of-replies
463+
{
464+
auto eventRelation = EventRelation::replace(
465+
referencedEventIndex().data(MessageEventModel::EventIdRole).toString()
466+
);
467+
EventContent::TextContent* textContent;
468+
if (htmlText.contains(MarkupRE)) {
469+
textContent = new EventContent::TextContent(htmlText,
470+
QStringLiteral("text/html"), eventRelation);
471+
} else {
472+
textContent = new EventContent::TextContent("",
473+
QStringLiteral("text/plain"), eventRelation);
474+
}
475+
auto roomMessageEvent = new RoomMessageEvent(plainText,
476+
MessageEventType::Text, textContent);
477+
currentRoom()->postEvent(roomMessageEvent);
478+
}
479+
break;
480+
case Replying:
481+
{
482+
QString htmlQuotation, plainTextQuotation;
483+
auto reference = referencedEventIndex();
484+
htmlQuotation = reference.data(MessageEventModel::HtmlQuotationRole).toString();
485+
plainTextQuotation = reference.data(MessageEventModel::QuotationRole).toString();
486+
auto textContent = new EventContent::TextContent(htmlQuotation + htmlText,
487+
QStringLiteral("text/html"),
488+
EventRelation::replyTo(
489+
reference.data(MessageEventModel::EventIdRole).toString()
490+
));
491+
auto roomMessageEvent = new RoomMessageEvent(plainTextQuotation + plainText,
492+
MessageEventType::Text, textContent);
493+
currentRoom()->postEvent(roomMessageEvent);
494+
}
495+
break;
496+
default:
497+
if (htmlText.contains(MarkupRE))
498+
currentRoom()->postHtmlText(plainText, htmlText);
499+
else
500+
currentRoom()->postPlainText(plainText);
501+
}
451502
}
452503

453504
static auto NothingToSendMsg()
@@ -651,7 +702,8 @@ QString ChatRoomWidget::sendCommand(QStringView command,
651702
const auto& plainMsg = m_chatEdit->toPlainText().mid(CmdLen);
652703
if (plainMsg.isEmpty())
653704
return NothingToSendMsg();
654-
currentRoom()->postPlainText(plainMsg);
705+
const auto& fragment = QTextDocumentFragment::fromPlainText(plainMsg);
706+
sendMessageFromFragment(fragment, Plaintext);
655707
return {};
656708
}
657709
if (command == u"html")
@@ -670,9 +722,7 @@ QString ChatRoomWidget::sendCommand(QStringView command,
670722
.arg(errorPos).arg(errorString);
671723

672724
const auto& fragment = QTextDocumentFragment::fromHtml(cleanQtHtml);
673-
currentRoom()->postHtmlText(fragment.toPlainText(),
674-
HtmlFilter::toMatrixHtml(fragment.toHtml(),
675-
currentRoom()));
725+
sendMessageFromFragment(fragment);
676726
return {};
677727
}
678728
if (command == u"md") {
@@ -682,7 +732,7 @@ QString ChatRoomWidget::sendCommand(QStringView command,
682732
QTextCursor c(m_chatEdit->document());
683733
c.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 4);
684734
c.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
685-
sendMarkdown(currentRoom(), c.selection());
735+
sendMessageFromFragment(c.selection(), Markdown);
686736
return {};
687737
#else
688738
return tr("Your build of Quaternion doesn't support Markdown");
@@ -731,6 +781,43 @@ void ChatRoomWidget::sendInput()
731781
}
732782

733783
m_chatEdit->saveInput();
784+
setDefaultMode();
785+
}
786+
787+
void ChatRoomWidget::setDefaultMode()
788+
{
789+
mode = Default;
790+
emit m_timelineWidget->setCurrentIndex(-1);
791+
referencedEventId = "";
792+
m_modeIndicator->hide();
793+
}
794+
795+
bool ChatRoomWidget::setReferringMode(const int newMode, const QString& eventId,
796+
const char* icon_name)
797+
{
798+
Q_ASSERT( newMode == Replying || newMode == Editing );
799+
// Actually, we could let the user to refer to pending events too but in
800+
// this case we would need a universal pointer instead of event id. Now the
801+
// user cannot start to edit a pending message which might be annoying if
802+
// transactions are acknowledged slowly.
803+
if (!m_timelineWidget->indexOf(eventId).isValid())
804+
return false;
805+
mode = newMode;
806+
referencedEventId = eventId;
807+
emit m_timelineWidget->setCurrentIndex(referencedEventIndex().row());
808+
809+
m_modeIndicator->setIcon(QIcon::fromTheme(icon_name));
810+
811+
m_modeIndicator->show();
812+
return true;
813+
}
814+
815+
QModelIndex ChatRoomWidget::referencedEventIndex()
816+
{
817+
Q_ASSERT(!referencedEventId.isEmpty());
818+
auto idx = m_timelineWidget->indexOf(referencedEventId);
819+
Q_ASSERT(idx.isValid());
820+
return idx;
734821
}
735822

736823
ChatRoomWidget::completions_t
@@ -789,6 +876,36 @@ void ChatRoomWidget::quote(const QString& htmlText)
789876
m_chatEdit->insertPlainText(sendString);
790877
}
791878

879+
void ChatRoomWidget::reply(const QString& eventId)
880+
{
881+
if (!setReferringMode(Replying, eventId, "mail-reply-sender")) {
882+
setHudHtml(tr("Referenced message not suitable for replying (yet)"));
883+
return;
884+
}
885+
setHudHtml(tr("Reply message"));
886+
}
887+
888+
void ChatRoomWidget::edit(const QString& eventId)
889+
{
890+
if (!setReferringMode(Editing, eventId, "edit-entry")) {
891+
setHudHtml(tr("Referenced message not suitable for editing (yet)"));
892+
return;
893+
}
894+
895+
auto htmlText = referencedEventIndex()
896+
.data(MessageEventModel::NudeRichBodyRole)
897+
.toString();
898+
m_chatEdit->clear();
899+
// We can never be sure which input format was used to build this message.
900+
// It can be markdown, matrixhtml (`/html`), rich text paste or a mixture of
901+
// these. Perhaps the best solution is to introduce a generic format
902+
// converter into ChatEdit's contextmenu which can be used any time by the
903+
// user. By using it, the user could convert this rich text to the desired
904+
// format.
905+
m_chatEdit->insertHtml(htmlText);
906+
setHudHtml(tr("Edit message"));
907+
}
908+
792909
void ChatRoomWidget::resizeEvent(QResizeEvent*)
793910
{
794911
m_chatEdit->setMaximumHeight(maximumChatEditHeight());

client/chatroomwidget.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
#include <settings.h>
2525

2626
#include <QtWidgets/QWidget>
27+
#include <QtWidgets/QToolButton>
28+
#include <QtCore/QModelIndex>
2729

2830
class TimelineWidget;
2931
class QuaternionRoom;
@@ -41,6 +43,17 @@ class User;
4143

4244
class ChatRoomWidget : public QWidget
4345
{
46+
enum Modes {
47+
Default,
48+
Replying,
49+
Editing,
50+
};
51+
enum TextFormat {
52+
Unspecified,
53+
Markdown,
54+
Plaintext,
55+
};
56+
4457
Q_OBJECT
4558
public:
4659
using completions_t = ChatEdit::completions_t;
@@ -63,6 +76,8 @@ class ChatRoomWidget : public QWidget
6376

6477
void typingChanged();
6578
void quote(const QString& htmlText);
79+
void edit(const QString& eventId);
80+
void reply(const QString& eventId);
6681
void fileDrop(const QString& url);
6782
void htmlDrop(const QString& html);
6883
void textDrop(const QString& text);
@@ -74,20 +89,31 @@ class ChatRoomWidget : public QWidget
7489
private:
7590
TimelineWidget* m_timelineWidget;
7691
QLabel* m_hudCaption; //< For typing and completion notifications
92+
QToolButton* m_modeIndicator;
7793
QAction* m_attachAction;
7894
ChatEdit* m_chatEdit;
7995

96+
int mode;
97+
QString referencedEventId;
98+
8099
QString attachedFileName;
81100
QTemporaryFile* m_fileToAttach;
82101
Quotient::SettingsGroup m_uiSettings;
83102

84103
MainWindow* mainWindow() const;
85104
QuaternionRoom* currentRoom() const;
86105

106+
void setDefaultMode();
107+
bool setReferringMode(const int newMode, const QString& eventId,
108+
const char* icon_name);
109+
QModelIndex referencedEventIndex();
110+
87111
void sendFile();
88112
void sendMessage();
89113
[[nodiscard]] QString sendCommand(QStringView command,
90114
const QString& argString);
115+
void sendMessageFromFragment(const QTextDocumentFragment& text,
116+
enum TextFormat textFormat = Unspecified);
91117

92118
void resizeEvent(QResizeEvent*) override;
93119
void keyPressEvent(QKeyEvent* event) override;

0 commit comments

Comments
 (0)