diff --git a/imports/Spectral/Panel/RoomListPanel.qml b/imports/Spectral/Panel/RoomListPanel.qml index 646229c1d..9284f3c07 100644 --- a/imports/Spectral/Panel/RoomListPanel.qml +++ b/imports/Spectral/Panel/RoomListPanel.qml @@ -259,7 +259,7 @@ Item { Label { visible: notificationCount > 0 && highlightCount == 0 - color: "white" + color: MPalette.background text: notificationCount leftPadding: 12 rightPadding: 12 diff --git a/imports/Spectral/Panel/RoomPanelInput.qml b/imports/Spectral/Panel/RoomPanelInput.qml index cae34ff9e..65139d88c 100644 --- a/imports/Spectral/Panel/RoomPanelInput.qml +++ b/imports/Spectral/Panel/RoomPanelInput.qml @@ -380,24 +380,9 @@ Control { var PREFIX_ME = '/me ' var PREFIX_NOTICE = '/notice ' var PREFIX_RAINBOW = '/rainbow ' - var PREFIX_HTML = '/html ' - var PREFIX_MARKDOWN = '/md ' - if (isReply) { - currentRoom.sendReply(replyUser.id, replyEventID, replyContent, text) - return - } + var messageEventType = RoomMessageEvent.Text - if (text.indexOf(PREFIX_ME) === 0) { - text = text.substr(PREFIX_ME.length) - currentRoom.postMessage(text, RoomMessageEvent.Emote) - return - } - if (text.indexOf(PREFIX_NOTICE) === 0) { - text = text.substr(PREFIX_NOTICE.length) - currentRoom.postMessage(text, RoomMessageEvent.Notice) - return - } if (text.indexOf(PREFIX_RAINBOW) === 0) { text = text.substr(PREFIX_RAINBOW.length) @@ -406,23 +391,38 @@ Control { for (var i = 0; i < text.length; i++) { parsedText = parsedText + "" + text.charAt(i) + "" } - currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text) - return - } - if (text.indexOf(PREFIX_HTML) === 0) { - text = text.substr(PREFIX_HTML.length) - var re = new RegExp("<.*?>") - var plainText = text.replace(re, "") - currentRoom.postHtmlMessage(plainText, text, RoomMessageEvent.Text) - return - } - if (text.indexOf(PREFIX_MARKDOWN) === 0) { - text = text.substr(PREFIX_MARKDOWN.length) - currentRoom.postMarkdownText(text) + currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text, replyEventID) return } - currentRoom.postPlainText(text) + if (text.indexOf(PREFIX_ME) === 0) { + text = text.substr(PREFIX_ME.length) + messageEventType = RoomMessageEvent.Emote + } else if (text.indexOf(PREFIX_NOTICE) === 0) { + text = text.substr(PREFIX_NOTICE.length) + messageEventType = RoomMessageEvent.Notice + } + + if (MSettings.markdownFormatting) { + currentRoom.postArbitaryMessage(text, messageEventType, replyEventID) + } else { + currentRoom.postPlainMessage(text, messageEventType, replyEventID) + } + } + } + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + + icon: "\ue165" + font.pixelSize: 16 + color: MPalette.foreground + opacity: MSettings.markdownFormatting ? 1 : 0.3 + + MouseArea { + anchors.fill: parent + + onClicked: MSettings.markdownFormatting = !MSettings.markdownFormatting } } diff --git a/imports/Spectral/Setting/Setting.qml b/imports/Spectral/Setting/Setting.qml index 3dd86287b..6aee66ad3 100644 --- a/imports/Spectral/Setting/Setting.qml +++ b/imports/Spectral/Setting/Setting.qml @@ -10,4 +10,6 @@ Settings { property bool darkTheme property string fontFamily: "Roboto,Noto Sans,Noto Color Emoji" + + property bool markdownFormatting: true } diff --git a/src/spectralroom.cpp b/src/spectralroom.cpp index 5aa154c09..7e4b0efa0 100644 --- a/src/spectralroom.cpp +++ b/src/spectralroom.cpp @@ -10,6 +10,7 @@ #include "csapi/rooms.h" #include "csapi/typing.h" #include "events/accountdataevents.h" +#include "events/roommessageevent.h" #include "events/typingevent.h" #include "jobs/downloadfilejob.h" @@ -18,6 +19,7 @@ #include #include #include +#include #include "html.h" @@ -169,24 +171,6 @@ void SpectralRoom::countChanged() { } } -void SpectralRoom::sendReply(QString userId, - QString eventId, - QString replyContent, - QString sendContent) { - QJsonObject json{ - {"msgtype", "m.text"}, - {"body", "> <" + userId + "> " + replyContent + "\n\n" + sendContent}, - {"format", "org.matrix.custom.html"}, - {"m.relates_to", - QJsonObject{{"m.in_reply_to", QJsonObject{{"event_id", eventId}}}}}, - {"formatted_body", - "
In reply to " + userId + "
" + replyContent + - "
" + sendContent}}; - postJson("m.room.message", json); -} - QDateTime SpectralRoom::lastActiveTime() { if (timelineSize() == 0) return QDateTime(); @@ -234,29 +218,6 @@ QVariantList SpectralRoom::getUsers(const QString& prefix) { return matchedList; } -QString SpectralRoom::postMarkdownText(const QString& markdown) { - unsigned char* sequence = - (unsigned char*)qstrdup(markdown.toUtf8().constData()); - qint64 length = strlen((char*)sequence); - - hoedown_renderer* renderer = - hoedown_html_renderer_new(HOEDOWN_HTML_USE_XHTML, 32); - hoedown_extensions extensions = (hoedown_extensions)( - (HOEDOWN_EXT_BLOCK | HOEDOWN_EXT_SPAN | HOEDOWN_EXT_MATH_EXPLICIT) & - ~HOEDOWN_EXT_QUOTE); - hoedown_document* document = hoedown_document_new(renderer, extensions, 32); - hoedown_buffer* html = hoedown_buffer_new(length); - hoedown_document_render(document, html, sequence, length); - QString result = QString::fromUtf8((char*)html->data, html->size); - - free(sequence); - hoedown_buffer_free(html); - hoedown_document_free(document); - hoedown_html_renderer_free(renderer); - - return postHtmlText(markdown, result); -} - QUrl SpectralRoom::urlToMxcUrl(QUrl mxcUrl) { return DownloadFileJob::makeRequestUrl(connection()->homeserver(), mxcUrl); } @@ -336,3 +297,131 @@ void SpectralRoom::removeLocalAlias(const QString& alias) { setLocalAliases(aliases); } + +QString SpectralRoom::markdownToHTML(const QString& markdown) { + unsigned char* sequence = + (unsigned char*)qstrdup(markdown.toUtf8().constData()); + qint64 length = strlen((char*)sequence); + + hoedown_renderer* renderer = + hoedown_html_renderer_new(HOEDOWN_HTML_USE_XHTML, 32); + hoedown_extensions extensions = (hoedown_extensions)( + (HOEDOWN_EXT_BLOCK | HOEDOWN_EXT_SPAN | HOEDOWN_EXT_MATH_EXPLICIT) & + ~HOEDOWN_EXT_QUOTE); + hoedown_document* document = hoedown_document_new(renderer, extensions, 32); + hoedown_buffer* html = hoedown_buffer_new(length); + hoedown_document_render(document, html, sequence, length); + QString result = QString::fromUtf8((char*)html->data, html->size); + + free(sequence); + hoedown_buffer_free(html); + hoedown_document_free(document); + hoedown_html_renderer_free(renderer); + + return result; +} + +void SpectralRoom::postArbitaryMessage(const QString& text, + MessageEventType type, + const QString& replyEventId) { + auto parsedHTML = markdownToHTML(text); + bool isRichText = Qt::mightBeRichText(parsedHTML); + + if (isRichText) { // Markdown + postHtmlMessage(text, parsedHTML, type, replyEventId); + } else { // Plain text + postPlainMessage(text, type, replyEventId); + } +} + +QString msgTypeToString(MessageEventType msgType) { + switch (msgType) { + case MessageEventType::Text: + return "m.text"; + case MessageEventType::File: + return "m.file"; + case MessageEventType::Audio: + return "m.audio"; + case MessageEventType::Emote: + return "m.emote"; + case MessageEventType::Image: + return "m.image"; + case MessageEventType::Video: + return "m.video"; + case MessageEventType::Notice: + return "m.notice"; + case MessageEventType::Location: + return "m.location"; + default: + return "m.text"; + } +} + +void SpectralRoom::postPlainMessage(const QString& text, + MessageEventType type, + const QString& replyEventId) { + bool isReply = !replyEventId.isEmpty(); + const auto replyIt = findInTimeline(replyEventId); + if (replyIt == timelineEdge()) + isReply = false; + + if (isReply) { + const auto& replyEvt = **replyIt; + + QJsonObject json{{"msgtype", msgTypeToString(type)}, + {"body", "> <" + replyEvt.senderId() + "> " + + eventToString(replyEvt) + "\n\n" + text}, + {"format", "org.matrix.custom.html"}, + {"m.relates_to", + QJsonObject{{"m.in_reply_to", + QJsonObject{{"event_id", replyEventId}}}}}, + {"formatted_body", + "
In reply to " + replyEvt.senderId() + + "
" + eventToString(replyEvt, Qt::RichText) + + "
" + text.toHtmlEscaped()}}; + postJson("m.room.message", + json); // TODO: Support other message event types? + + return; + } + + Room::postMessage(text, type); +} + +void SpectralRoom::postHtmlMessage(const QString& text, + const QString& html, + MessageEventType type, + const QString& replyEventId) { + bool isReply = !replyEventId.isEmpty(); + const auto replyIt = findInTimeline(replyEventId); + if (replyIt == timelineEdge()) + isReply = false; + + if (isReply) { + const auto& replyEvt = **replyIt; + + QJsonObject json{{"msgtype", msgTypeToString(type)}, + {"body", "> <" + replyEvt.senderId() + "> " + + eventToString(replyEvt) + "\n\n" + text}, + {"format", "org.matrix.custom.html"}, + {"m.relates_to", + QJsonObject{{"m.in_reply_to", + QJsonObject{{"event_id", replyEventId}}}}}, + {"formatted_body", + "
In reply to " + replyEvt.senderId() + + "
" + eventToString(replyEvt, Qt::RichText) + + "
" + html}}; + postJson("m.room.message", + json); // TODO: Support other message event types? + + return; + } + + Room::postHtmlMessage(text, html, type); +} diff --git a/src/spectralroom.h b/src/spectralroom.h index 729b7c1d6..e4885b8f5 100644 --- a/src/spectralroom.h +++ b/src/spectralroom.h @@ -8,13 +8,13 @@ #include #include +#include #include #include #include #include #include #include -#include using namespace QMatrixClient; @@ -90,8 +90,6 @@ class SpectralRoom : public Room { Q_INVOKABLE QVariantList getUsers(const QString& prefix); - Q_INVOKABLE QString postMarkdownText(const QString& markdown); - Q_INVOKABLE QUrl urlToMxcUrl(QUrl mxcUrl); QUrl avatarUrl() const { @@ -287,6 +285,8 @@ class SpectralRoom : public Room { void onAddNewTimelineEvents(timeline_iter_t from) override; void onAddHistoricalTimelineEvents(rev_iter_t from) override; + static QString markdownToHTML(const QString& plaintext); + private slots: void countChanged(); @@ -302,10 +302,16 @@ class SpectralRoom : public Room { void acceptInvitation(); void forget(); void sendTypingNotification(bool isTyping); - void sendReply(QString userId, - QString eventId, - QString replyContent, - QString sendContent); + void postArbitaryMessage(const QString& text, + MessageEventType type = MessageEventType::Text, + const QString& replyEventId = ""); + void postPlainMessage(const QString& text, + MessageEventType type = MessageEventType::Text, + const QString& replyEventId = ""); + void postHtmlMessage(const QString& text, + const QString& html, + MessageEventType type = MessageEventType::Text, + const QString& replyEventId = ""); void changeAvatar(QUrl localFile); void addLocalAlias(const QString& alias); void removeLocalAlias(const QString& alias);