Skip to content

Commit

Permalink
Add rich replying and editing support
Browse files Browse the repository at this point in the history
Cc #596
Closes #447
  • Loading branch information
rpallai committed Feb 21, 2023
1 parent 0315b39 commit b59f66e
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 26 deletions.
170 changes: 144 additions & 26 deletions client/chatroomwidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
#include "quaternionroom.h"
#include "chatedit.h"
#include "htmlfilter.h"
#include "models/messageeventmodel.h"

static auto DefaultPlaceholderText()
{
Expand Down Expand Up @@ -84,6 +85,13 @@ ChatRoomWidget::ChatRoomWidget(MainWindow* parent)
m_hudCaption->setFont(f);
m_hudCaption->setTextFormat(Qt::RichText);

m_modeIndicator = new QToolButton();
m_modeIndicator->setAutoRaise(true);
m_modeIndicator->hide();
connect(m_modeIndicator, &QToolButton::clicked, this, [this] {
setDefaultMode();
});

auto attachButton = new QToolButton();
attachButton->setAutoRaise(true);
m_attachAction = new QAction(QIcon::fromTheme("mail-attachment"),
Expand Down Expand Up @@ -207,6 +215,7 @@ ChatRoomWidget::ChatRoomWidget(MainWindow* parent)
layout->addWidget(m_hudCaption);
{
auto inputLayout = new QHBoxLayout;
inputLayout->addWidget(m_modeIndicator);
inputLayout->addWidget(attachButton);
inputLayout->addWidget(m_chatEdit);
layout->addLayout(inputLayout);
Expand Down Expand Up @@ -262,6 +271,7 @@ void ChatRoomWidget::setRoom(QuaternionRoom* newRoom)
}
typingChanged();
encryptionChanged();
setDefaultMode();
}

void ChatRoomWidget::typingChanged()
Expand Down Expand Up @@ -416,38 +426,79 @@ void ChatRoomWidget::sendFile()
m_chatEdit->setPlaceholderText(DefaultPlaceholderText());
}

#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
void sendMarkdown(QuaternionRoom* room, const QTextDocumentFragment& text)
{
room->postHtmlText(text.toPlainText(),
HtmlFilter::toMatrixHtml(text.toHtml(), room,
HtmlFilter::ConvertMarkdown));
}
#endif

void ChatRoomWidget::sendMessage()
{
if (m_chatEdit->toPlainText().startsWith("//"))
QTextCursor(m_chatEdit->document()).deleteChar();

QTextCursor c(m_chatEdit->document());
c.select(QTextCursor::Document);
sendMessageFromFragment(c.selection());
}

void ChatRoomWidget::sendMessageFromFragment(const QTextDocumentFragment& text,
enum TextFormat textFormat)
{
const auto& plainText = text.toPlainText();
const auto& htmlText =
HtmlFilter::toMatrixHtml(text.toHtml(), currentRoom(),
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
if (m_uiSettings.get("auto_markdown", false)) {
sendMarkdown(currentRoom(),
QTextDocumentFragment(m_chatEdit->document()));
return;
}
((textFormat == Unspecified
&& m_uiSettings.get("auto_markdown", false))
|| textFormat == Markdown)
? HtmlFilter::ConvertMarkdown
:
#endif
const auto& plainText = m_chatEdit->toPlainText();
const auto& htmlText =
HtmlFilter::toMatrixHtml(m_chatEdit->toHtml(), currentRoom());
HtmlFilter::Default);
Q_ASSERT(!plainText.isEmpty() && !htmlText.isEmpty());
// Send plain text if htmlText has no markup or just <br/> elements
// (those are easily represented as line breaks in plain text)
static const QRegularExpression MarkupRE { "<(?![Bb][Rr])" };
if (htmlText.contains(MarkupRE))
currentRoom()->postHtmlText(plainText, htmlText);
else
currentRoom()->postPlainText(plainText);

using namespace Quotient;
switch (mode) {
case Editing:
// Any quotation is ignored intentionally, see
// https://spec.matrix.org/latest/client-server-api/#edits-of-replies
{
auto eventRelation = EventRelation::replace(
referencedEventIndex().data(MessageEventModel::EventIdRole).toString()
);
EventContent::TextContent* textContent;
if (htmlText.contains(MarkupRE)) {
textContent = new EventContent::TextContent(htmlText,
QStringLiteral("text/html"), eventRelation);
} else {
textContent = new EventContent::TextContent("",
QStringLiteral("text/plain"), eventRelation);
}
auto roomMessageEvent = new RoomMessageEvent(plainText,
MessageEventType::Text, textContent);
currentRoom()->postEvent(roomMessageEvent);
}
break;
case Replying:
{
QString htmlQuotation, plainTextQuotation;
auto reference = referencedEventIndex();
htmlQuotation = reference.data(MessageEventModel::HtmlQuotationRole).toString();
plainTextQuotation = reference.data(MessageEventModel::QuotationRole).toString();
auto textContent = new EventContent::TextContent(htmlQuotation + htmlText,
QStringLiteral("text/html"),
EventRelation::replyTo(
reference.data(MessageEventModel::EventIdRole).toString()
));
auto roomMessageEvent = new RoomMessageEvent(plainTextQuotation + plainText,
MessageEventType::Text, textContent);
currentRoom()->postEvent(roomMessageEvent);
}
break;
default:
if (htmlText.contains(MarkupRE))
currentRoom()->postHtmlText(plainText, htmlText);
else
currentRoom()->postPlainText(plainText);
}
}

static auto NothingToSendMsg()
Expand Down Expand Up @@ -651,7 +702,8 @@ QString ChatRoomWidget::sendCommand(QStringView command,
const auto& plainMsg = m_chatEdit->toPlainText().mid(CmdLen);
if (plainMsg.isEmpty())
return NothingToSendMsg();
currentRoom()->postPlainText(plainMsg);
const auto& fragment = QTextDocumentFragment::fromPlainText(plainMsg);
sendMessageFromFragment(fragment, Plaintext);
return {};
}
if (command == u"html")
Expand All @@ -670,9 +722,7 @@ QString ChatRoomWidget::sendCommand(QStringView command,
.arg(errorPos).arg(errorString);

const auto& fragment = QTextDocumentFragment::fromHtml(cleanQtHtml);
currentRoom()->postHtmlText(fragment.toPlainText(),
HtmlFilter::toMatrixHtml(fragment.toHtml(),
currentRoom()));
sendMessageFromFragment(fragment, Html);
return {};
}
if (command == u"md") {
Expand All @@ -682,7 +732,7 @@ QString ChatRoomWidget::sendCommand(QStringView command,
QTextCursor c(m_chatEdit->document());
c.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 4);
c.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
sendMarkdown(currentRoom(), c.selection());
sendMessageFromFragment(c.selection(), Markdown);
return {};
#else
return tr("Your build of Quaternion doesn't support Markdown");
Expand Down Expand Up @@ -731,6 +781,44 @@ void ChatRoomWidget::sendInput()
}

m_chatEdit->saveInput();
setDefaultMode();
}

void ChatRoomWidget::setDefaultMode()
{
mode = Default;
emit m_timelineWidget->setCurrentIndex(-1);
referencedEventId = "";
m_modeIndicator->hide();
}

bool ChatRoomWidget::setReferringMode(const int newMode, const QString& eventId,
const char* icon_name)
{
Q_ASSERT( newMode == Replying || newMode == Editing );
// Actually, we could let the user to refer to pending events too but in
// this case we would need a universal pointer instead of event id. Now the
// user cannot start to edit a pending message which might be annoying if
// transactions are acknowledged slowly.
auto idx = m_timelineWidget->indexOf(eventId);
if (!idx.isValid())
return false;
mode = newMode;
referencedEventId = eventId;
emit m_timelineWidget->setCurrentIndex(idx.row());

m_modeIndicator->setIcon(QIcon::fromTheme(icon_name));

m_modeIndicator->show();
return true;
}

QModelIndex ChatRoomWidget::referencedEventIndex()
{
Q_ASSERT(!referencedEventId.isEmpty());
auto idx = m_timelineWidget->indexOf(referencedEventId);
Q_ASSERT(idx.isValid());
return idx;
}

ChatRoomWidget::completions_t
Expand Down Expand Up @@ -789,6 +877,36 @@ void ChatRoomWidget::quote(const QString& htmlText)
m_chatEdit->insertPlainText(sendString);
}

void ChatRoomWidget::reply(const QString& eventId)
{
if (!setReferringMode(Replying, eventId, "mail-reply-sender")) {
setHudHtml(tr("Referenced message not suitable for replying (yet)"));
return;
}
setHudHtml(tr("Reply message"));
}

void ChatRoomWidget::edit(const QString& eventId)
{
if (!setReferringMode(Editing, eventId, "edit-entry")) {
setHudHtml(tr("Referenced message not suitable for editing (yet)"));
return;
}

auto htmlText = referencedEventIndex()
.data(MessageEventModel::NudeRichBodyRole)
.toString();
m_chatEdit->clear();
// We can never be sure which input format was used to build this message.
// It can be markdown, matrixhtml (`/html`), rich text paste or a mixture of
// these. Perhaps the best solution is to introduce a generic format
// converter into ChatEdit's contextmenu which can be used any time by the
// user. By using it, the user could convert this rich text to the desired
// format.
m_chatEdit->insertHtml(htmlText);
setHudHtml(tr("Edit message"));
}

void ChatRoomWidget::resizeEvent(QResizeEvent*)
{
m_chatEdit->setMaximumHeight(maximumChatEditHeight());
Expand Down
27 changes: 27 additions & 0 deletions client/chatroomwidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
#include <settings.h>

#include <QtWidgets/QWidget>
#include <QtWidgets/QToolButton>
#include <QtCore/QModelIndex>

class TimelineWidget;
class QuaternionRoom;
Expand All @@ -41,6 +43,18 @@ class User;

class ChatRoomWidget : public QWidget
{
enum Modes {
Default,
Replying,
Editing,
};
enum TextFormat {
Unspecified,
Markdown,
Plaintext,
Html,
};

Q_OBJECT
public:
using completions_t = ChatEdit::completions_t;
Expand All @@ -63,6 +77,8 @@ class ChatRoomWidget : public QWidget

void typingChanged();
void quote(const QString& htmlText);
void edit(const QString& eventId);
void reply(const QString& eventId);
void fileDrop(const QString& url);
void htmlDrop(const QString& html);
void textDrop(const QString& text);
Expand All @@ -74,20 +90,31 @@ class ChatRoomWidget : public QWidget
private:
TimelineWidget* m_timelineWidget;
QLabel* m_hudCaption; //< For typing and completion notifications
QToolButton* m_modeIndicator;
QAction* m_attachAction;
ChatEdit* m_chatEdit;

int mode;
QString referencedEventId;

QString attachedFileName;
QTemporaryFile* m_fileToAttach;
Quotient::SettingsGroup m_uiSettings;

MainWindow* mainWindow() const;
QuaternionRoom* currentRoom() const;

void setDefaultMode();
bool setReferringMode(const int newMode, const QString& eventId,
const char* icon_name);
QModelIndex referencedEventIndex();

void sendFile();
void sendMessage();
[[nodiscard]] QString sendCommand(QStringView command,
const QString& argString);
void sendMessageFromFragment(const QTextDocumentFragment& text,
enum TextFormat textFormat = Unspecified);

void resizeEvent(QResizeEvent*) override;
void keyPressEvent(QKeyEvent* event) override;
Expand Down
Loading

0 comments on commit b59f66e

Please sign in to comment.