Skip to content

Commit b59f66e

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

File tree

7 files changed

+282
-26
lines changed

7 files changed

+282
-26
lines changed

client/chatroomwidget.cpp

Lines changed: 144 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 == Unspecified
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, Html);
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,44 @@ 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+
auto idx = m_timelineWidget->indexOf(eventId);
804+
if (!idx.isValid())
805+
return false;
806+
mode = newMode;
807+
referencedEventId = eventId;
808+
emit m_timelineWidget->setCurrentIndex(idx.row());
809+
810+
m_modeIndicator->setIcon(QIcon::fromTheme(icon_name));
811+
812+
m_modeIndicator->show();
813+
return true;
814+
}
815+
816+
QModelIndex ChatRoomWidget::referencedEventIndex()
817+
{
818+
Q_ASSERT(!referencedEventId.isEmpty());
819+
auto idx = m_timelineWidget->indexOf(referencedEventId);
820+
Q_ASSERT(idx.isValid());
821+
return idx;
734822
}
735823

736824
ChatRoomWidget::completions_t
@@ -789,6 +877,36 @@ void ChatRoomWidget::quote(const QString& htmlText)
789877
m_chatEdit->insertPlainText(sendString);
790878
}
791879

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

client/chatroomwidget.h

Lines changed: 27 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,18 @@ 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+
Html,
56+
};
57+
4458
Q_OBJECT
4559
public:
4660
using completions_t = ChatEdit::completions_t;
@@ -63,6 +77,8 @@ class ChatRoomWidget : public QWidget
6377

6478
void typingChanged();
6579
void quote(const QString& htmlText);
80+
void edit(const QString& eventId);
81+
void reply(const QString& eventId);
6682
void fileDrop(const QString& url);
6783
void htmlDrop(const QString& html);
6884
void textDrop(const QString& text);
@@ -74,20 +90,31 @@ class ChatRoomWidget : public QWidget
7490
private:
7591
TimelineWidget* m_timelineWidget;
7692
QLabel* m_hudCaption; //< For typing and completion notifications
93+
QToolButton* m_modeIndicator;
7794
QAction* m_attachAction;
7895
ChatEdit* m_chatEdit;
7996

97+
int mode;
98+
QString referencedEventId;
99+
80100
QString attachedFileName;
81101
QTemporaryFile* m_fileToAttach;
82102
Quotient::SettingsGroup m_uiSettings;
83103

84104
MainWindow* mainWindow() const;
85105
QuaternionRoom* currentRoom() const;
86106

107+
void setDefaultMode();
108+
bool setReferringMode(const int newMode, const QString& eventId,
109+
const char* icon_name);
110+
QModelIndex referencedEventIndex();
111+
87112
void sendFile();
88113
void sendMessage();
89114
[[nodiscard]] QString sendCommand(QStringView command,
90115
const QString& argString);
116+
void sendMessageFromFragment(const QTextDocumentFragment& text,
117+
enum TextFormat textFormat = Unspecified);
91118

92119
void resizeEvent(QResizeEvent*) override;
93120
void keyPressEvent(QKeyEvent* event) override;

0 commit comments

Comments
 (0)