diff --git a/imports/NeoChat/Component/ChatTextInput.qml b/imports/NeoChat/Component/ChatTextInput.qml index 2275c5356..621f4ddf5 100644 --- a/imports/NeoChat/Component/ChatTextInput.qml +++ b/imports/NeoChat/Component/ChatTextInput.qml @@ -22,8 +22,10 @@ ToolBar { property alias isReply: replyItem.visible property bool isReaction: false property var replyUser - property string replyEventID - property string replyContent + property string replyEventID: "" + property string replyContent: "" + + property string editEventId property alias isAutoCompleting: autoCompleteListView.visible property var autoCompleteModel @@ -262,6 +264,19 @@ ToolBar { } } + RowLayout { + visible: editEventId.length > 0 + ToolButton { + icon.name: "dialog-cancel" + onClicked: clearEditReply(); + } + + Label { + Layout.alignment: Qt.AlignVCenter + text: i18n("Edit Message") + } + } + Kirigami.Separator { Layout.fillWidth: true Layout.preferredHeight: 1 @@ -280,7 +295,7 @@ ToolBar { icon.name: "dialog-cancel" - onClicked: clearReply() + onClicked: clearEditReply() } @@ -359,13 +374,13 @@ ToolBar { } else { postMessage() text = "" - clearReply() + clearEditReply() closeAll() } } Keys.onEscapePressed: { - clearReply(); + clearEditReply(); closeAll(); } @@ -450,7 +465,7 @@ ToolBar { // lines. const updatedText = inputField.text.trim() .replace(/@([^: ]*):([^ ]*\.[^ ]*)/, "[@$1:$2](https://matrix.to/#/@$1:$2)"); - documentHandler.postMessage(updatedText, attachmentPath, replyEventID); + documentHandler.postMessage(updatedText, attachmentPath, replyEventID, editEventId); clearAttachment(); currentRoom.markAllMessagesAsRead(); clear(); @@ -521,7 +536,7 @@ ToolBar { onClicked: { inputField.postMessage() inputField.text = "" - root.clearReply() + root.clearEditReply() root.closeAll() } @@ -553,17 +568,23 @@ ToolBar { inputField.clear() } - function clearReply() { - isReply = false + function clearEditReply() { + isReply = false; replyUser = null; - replyContent = ""; - replyEventID = "" + root.replyContent = ""; + root.replyEventID = ""; + root.editEventId = ""; } function focus() { inputField.forceActiveFocus() } + function edit(editContent, editEventId) { + inputField.text = editContent; + root.editEventId = editEventId + } + function closeAll() { replyItem.visible = false autoCompleteListView.visible = false diff --git a/imports/NeoChat/Component/Timeline/MessageDelegate.qml b/imports/NeoChat/Component/Timeline/MessageDelegate.qml index 6c90cffcf..49f620b07 100644 --- a/imports/NeoChat/Component/Timeline/MessageDelegate.qml +++ b/imports/NeoChat/Component/Timeline/MessageDelegate.qml @@ -134,6 +134,13 @@ RowLayout { onReact: currentRoom.toggleReaction(eventId, emoji) } } + QQC2.Button { + QQC2.ToolTip.text: i18n("Edit") + QQC2.ToolTip.visible: hovered + visible: controlContainer.hovered && author.id === Controller.activeConnection.localUserId && (model.eventType === "emote" || model.eventType === "message") + icon.name: "document-edit" + onClicked: chatTextInput.edit(message, eventId) + } QQC2.Button { QQC2.ToolTip.text: i18n("Reply") QQC2.ToolTip.visible: hovered diff --git a/imports/NeoChat/Page/RoomPage.qml b/imports/NeoChat/Page/RoomPage.qml index 9e7d4797b..ff0332eff 100644 --- a/imports/NeoChat/Page/RoomPage.qml +++ b/imports/NeoChat/Page/RoomPage.qml @@ -198,12 +198,14 @@ Kirigami.ScrollablePage { filterRowCallback: Config.showLeaveJoinEvent ? dontFilterLeaveJoin : filterLeaveJoin function dontFilterLeaveJoin(row, parent) { - return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10 + return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.SpecialMarksRole) !== EventStatus.Hidden + && messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10 && messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other"; } function filterLeaveJoin(row, parent) { - return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10 + return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.SpecialMarksRole) !== EventStatus.Hidden + && messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10 && messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other" && messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "state"; } diff --git a/src/chatdocumenthandler.cpp b/src/chatdocumenthandler.cpp index 35927a4aa..74d50c07f 100644 --- a/src/chatdocumenthandler.cpp +++ b/src/chatdocumenthandler.cpp @@ -167,7 +167,7 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo() }; } -void ChatDocumentHandler::postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId) const +void ChatDocumentHandler::postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId, const QString &editEventId) const { if (!m_room || !m_document) { return; @@ -200,7 +200,7 @@ void ChatDocumentHandler::postMessage(const QString &text, const QString &attach for (int i = 0; i < cleanedText.length(); i++) { rainbowText = rainbowText % QStringLiteral("" % cleanedText.at(i) % ""; } - m_room->postHtmlMessage(cleanedText, rainbowText, messageEventType, replyEventId); + m_room->postHtmlMessage(cleanedText, rainbowText, messageEventType, replyEventId, editEventId); return; } @@ -211,7 +211,7 @@ void ChatDocumentHandler::postMessage(const QString &text, const QString &attach cleanedText = cleanedText.remove(0, noticePrefix.length()); messageEventType = RoomMessageEvent::MsgType::Notice; } - m_room->postArbitaryMessage(cleanedText, messageEventType, replyEventId); + m_room->postMessage(cleanedText, messageEventType, replyEventId, editEventId); } void ChatDocumentHandler::replaceAutoComplete(const QString &word) diff --git a/src/chatdocumenthandler.h b/src/chatdocumenthandler.h index 5e7c413e6..dcfd2a538 100644 --- a/src/chatdocumenthandler.h +++ b/src/chatdocumenthandler.h @@ -51,7 +51,7 @@ public: [[nodiscard]] NeoChatRoom *room() const; void setRoom(NeoChatRoom *room); - Q_INVOKABLE void postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId) const; + Q_INVOKABLE void postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId, const QString &editEventId) const; /// This function will look at the current QTextCursor and determine if there /// is the posibility to autocomplete it. diff --git a/src/messageeventmodel.cpp b/src/messageeventmodel.cpp index 3753d6d9a..8c21575f7 100644 --- a/src/messageeventmodel.cpp +++ b/src/messageeventmodel.cpp @@ -415,6 +415,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const } } + // isReplacement? + if (auto e = eventCast(&evt)) + if (!e->replacedEvent().isEmpty()) + return EventStatus::Hidden; + if (is(evt) || is(evt)) { return EventStatus::Hidden; } diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index 0e2ffa2fb..7f30a59c4 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -479,18 +479,6 @@ QString NeoChatRoom::markdownToHTML(const QString &markdown) return result; } -void NeoChatRoom::postArbitaryMessage(const QString &text, Quotient::RoomMessageEvent::MsgType type, const QString &replyEventId) -{ - const auto parsedHTML = markdownToHTML(text); - const 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) { @@ -515,59 +503,48 @@ QString msgTypeToString(MessageEventType msgType) } } -void NeoChatRoom::postPlainMessage(const QString &text, MessageEventType type, const QString &replyEventId) +void NeoChatRoom::postMessage(const QString &text, MessageEventType type, const QString &replyEventId, const QString &relateToEventId) { - bool isReply = !replyEventId.isEmpty(); - const auto replyIt = findInTimeline(replyEventId); - if (replyIt == timelineEdge()) { - isReply = false; - } - - if (isReply) { - const auto &replyEvt = **replyIt; - - // clang-format off - 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 - } - }; - // clang-format on - - postJson("m.room.message", json); - - return; - } - - Room::postMessage(text, type); + const auto html = markdownToHTML(text); + postHtmlMessage(text, html, type, replyEventId, relateToEventId); } -void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, MessageEventType type, const QString &replyEventId) +void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, MessageEventType type, const QString &replyEventId, const QString &relateToEventId) { + QString htmlWithLinks = html; + htmlWithLinks = htmlWithLinks.replace(QRegularExpression("@([^: ]*):([^ ]*\\.[^ ]*)"), "@$1:$2"); + bool isRichText = Qt::mightBeRichText(htmlWithLinks); bool isReply = !replyEventId.isEmpty(); + bool isEdit = !relateToEventId.isEmpty(); const auto replyIt = findInTimeline(replyEventId); if (replyIt == timelineEdge()) { isReply = false; } + + if (isEdit) { + QJsonObject json { + {"type", "m.room.message"}, + {"msgtype", msgTypeToString(type)}, + {"body", "* " + (isRichText ? text : htmlWithLinks)}, + {"m.new_content", + QJsonObject { + {"body", (isRichText ? text : htmlWithLinks)}, + {"msgtype", msgTypeToString(type)} + } + }, + {"m.relates_to", + QJsonObject { + {"rel_type", "m.replace"}, + {"event_id", relateToEventId} + } + } + }; + + postJson("m.room.message", json); + return; + } + if (isReply) { const auto &replyEvt = **replyIt; @@ -592,7 +569,7 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess "\">In reply to " + replyEvt.senderId() + "
" + eventToString(replyEvt, Qt::RichText) + - "" + html + "" + (isRichText ? htmlWithLinks : text) } }; // clang-format on @@ -602,7 +579,11 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess return; } - Room::postHtmlMessage(text, html, type); + if (isRichText) { + Room::postHtmlMessage(text, html, type); + } else { + Room::postMessage(text, type); + } } void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction) diff --git a/src/neochatroom.h b/src/neochatroom.h index 847a750b8..827d8f1b5 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -130,13 +130,12 @@ Q_SIGNALS: void lastActiveTimeChanged(); public Q_SLOTS: - void uploadFile(const QUrl &url, const QString &body = ""); + void uploadFile(const QUrl &url, const QString &body = QString()); void acceptInvitation(); void forget(); void sendTypingNotification(bool isTyping); - void postArbitaryMessage(const QString &text, Quotient::RoomMessageEvent::MsgType type, const QString &replyEventId); - void postPlainMessage(const QString &text, Quotient::RoomMessageEvent::MsgType type = Quotient::MessageEventType::Text, const QString &replyEventId = ""); - void postHtmlMessage(const QString &text, const QString &html, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = ""); + void postMessage(const QString &text, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = QString(), const QString &relateToEventId = QString()); + void postHtmlMessage(const QString &text, const QString &html, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = QString(), const QString &relateToEventId = QString()); void changeAvatar(const QUrl &localFile); void addLocalAlias(const QString &alias); void removeLocalAlias(const QString &alias);