From 5482aad7baee423255a7f0c14414547c0d8fe21e Mon Sep 17 00:00:00 2001 From: James Graham Date: Sun, 12 Feb 2023 13:46:23 +0000 Subject: [PATCH] Inline Edits Edit text messages inline instead of in the chatbar --- src/actionshandler.cpp | 52 ++++-- src/actionshandler.h | 18 ++- src/chatdocumenthandler.cpp | 61 +++++-- src/chatdocumenthandler.h | 20 ++- src/neochatroom.cpp | 16 ++ src/neochatroom.h | 16 ++ src/qml/Component/ChatBox/ChatBar.qml | 30 +--- src/qml/Component/ChatBox/CompletionMenu.qml | 2 +- src/qml/Component/ChatBox/ReplyPane.qml | 3 +- .../Component/Timeline/MessageDelegate.qml | 6 + .../Timeline/MessageEditComponent.qml | 149 ++++++++++++++++++ src/qml/Page/RoomPage.qml | 1 - src/res.qrc | 1 + src/roommanager.cpp | 3 + 14 files changed, 327 insertions(+), 51 deletions(-) create mode 100644 src/qml/Component/Timeline/MessageEditComponent.qml diff --git a/src/actionshandler.cpp b/src/actionshandler.cpp index ee64c89e2..f735f5cc0 100644 --- a/src/actionshandler.cpp +++ b/src/actionshandler.cpp @@ -17,6 +17,7 @@ #include "models/actionsmodel.h" #include "models/customemojimodel.h" #include "neochatconfig.h" +#include "neochatroom.h" #include "neochatuser.h" #include "roommanager.h" @@ -58,9 +59,9 @@ void ActionsHandler::setRoom(NeoChatRoom *room) Q_EMIT roomChanged(); } -void ActionsHandler::handleMessage() +void ActionsHandler::handleNewMessage() { - checkEffects(); + checkEffects(m_room->chatBoxText()); if (!m_room->chatBoxAttachmentPath().isEmpty()) { QUrl url(m_room->chatBoxAttachmentPath()); auto path = url.isLocalFile() ? url.toLocalFile() : url.toString(); @@ -69,13 +70,39 @@ void ActionsHandler::handleMessage() m_room->setChatBoxText({}); return; } - QString handledText = m_room->chatBoxText(); - std::sort(m_room->mentions()->begin(), m_room->mentions()->end(), [](const auto &a, const auto &b) -> bool { + QString handledText = m_room->chatBoxText(); + handledText = handleMentions(handledText); + handleMessage(m_room->chatBoxText(), handledText); +} + +void ActionsHandler::handleEdit() +{ + checkEffects(m_room->editText()); + + QString handledText = m_room->editText(); + handledText = handleMentions(handledText, true); + handleMessage(m_room->editText(), handledText, true); +} + +QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit) +{ + if (!m_room) { + return QString(); + } + + QVector *mentions; + if (isEdit) { + mentions = m_room->editMentions(); + } else { + mentions = m_room->mentions(); + } + + std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool { return a.cursor.anchor() > b.cursor.anchor(); }); - for (const auto &mention : *m_room->mentions()) { + for (const auto &mention : *mentions) { if (mention.text.isEmpty() || mention.id.isEmpty()) { continue; } @@ -83,11 +110,16 @@ void ActionsHandler::handleMessage() mention.cursor.position() - mention.cursor.anchor(), QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text, mention.id)); } - m_room->mentions()->clear(); + mentions->clear(); + return handledText; +} + +void ActionsHandler::handleMessage(const QString &text, QString handledText, const bool &isEdit) +{ if (NeoChatConfig::allowQuickEdit()) { QRegularExpression sed("^s/([^/]*)/([^/]*)(/g)?$"); - auto match = sed.match(m_room->chatBoxText()); + auto match = sed.match(text); if (match.hasMatch()) { const QString regex = match.captured(1); const QString replacement = match.captured(2).toHtmlEscaped(); @@ -146,13 +178,13 @@ void ActionsHandler::handleMessage() if (handledText.length() == 0) { return; } - m_room->postMessage(m_room->chatBoxText(), handledText, messageType, m_room->chatBoxReplyId(), m_room->chatBoxEditId()); + + m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : ""); } -void ActionsHandler::checkEffects() +void ActionsHandler::checkEffects(const QString &text) { std::optional effect = std::nullopt; - const auto &text = m_room->chatBoxText(); if (text.contains("\u2744")) { effect = QLatin1String("snowflake"); } else if (text.contains("\u1F386")) { diff --git a/src/actionshandler.h b/src/actionshandler.h index 3435c0a75..f03071b1b 100644 --- a/src/actionshandler.h +++ b/src/actionshandler.h @@ -33,14 +33,22 @@ Q_SIGNALS: public Q_SLOTS: - /// \brief Post a message. - /// - /// This also interprets commands if any. - void handleMessage(); + /** + * @brief Pre-process text and send message. + */ + void handleNewMessage(); + + /** + * @brief Pre-process text and send edit. + */ + void handleEdit(); private: NeoChatRoom *m_room = nullptr; - void checkEffects(); + void checkEffects(const QString &text); + + QString handleMentions(QString handledText, const bool &isEdit = false); + void handleMessage(const QString &text, QString handledText, const bool &isEdit = false); }; QString markdownToHTML(const QString &markdown); diff --git a/src/chatdocumenthandler.cpp b/src/chatdocumenthandler.cpp index d08656a04..faa7b59e6 100644 --- a/src/chatdocumenthandler.cpp +++ b/src/chatdocumenthandler.cpp @@ -108,11 +108,16 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent) static QPointer previousRoom = nullptr; if (previousRoom) { disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr); + disconnect(previousRoom, &NeoChatRoom::editTextChanged, this, nullptr); } previousRoom = m_room; connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() { int start = completionStartIndex(); - m_completionModel->setText(m_room->chatBoxText().mid(start, cursorPosition() - start), m_room->chatBoxText().mid(start)); + m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start)); + }); + connect(m_room, &NeoChatRoom::editTextChanged, this, [this]() { + int start = completionStartIndex(); + m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start)); }); }); connect(this, &ChatDocumentHandler::documentChanged, this, [this]() { @@ -123,7 +128,7 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent) return; } int start = completionStartIndex(); - m_completionModel->setText(m_room->chatBoxText().mid(start, cursorPosition() - start), m_room->chatBoxText().mid(start)); + m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start)); }); } @@ -138,7 +143,7 @@ int ChatDocumentHandler::completionStartIndex() const #else const auto cursor = cursorPosition(); #endif - const auto &text = m_room->chatBoxText(); + const auto &text = getText(); auto start = std::min(cursor, text.size()) - 1; while (start > -1) { if (text.at(start) == QLatin1Char(' ')) { @@ -150,6 +155,20 @@ int ChatDocumentHandler::completionStartIndex() const return start; } +bool ChatDocumentHandler::isEdit() const +{ + return m_isEdit; +} + +void ChatDocumentHandler::setIsEdit(bool edit) +{ + if (edit == m_isEdit) { + return; + } + m_isEdit = edit; + Q_EMIT isEditChanged(); +} + QQuickTextDocument *ChatDocumentHandler::document() const { return m_document; @@ -204,7 +223,7 @@ void ChatDocumentHandler::complete(int index) if (m_completionModel->autoCompletionType() == CompletionModel::User) { auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Text).toString(); auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString(); - auto text = m_room->chatBoxText(); + auto text = getText(); auto at = text.lastIndexOf(QLatin1Char('@'), cursorPosition() - 1); QTextCursor cursor(document()->textDocument()); cursor.setPosition(at); @@ -213,11 +232,11 @@ void ChatDocumentHandler::complete(int index) cursor.setPosition(at); cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor); cursor.setKeepPositionOnInsert(true); - m_room->mentions()->push_back({cursor, name, 0, 0, id}); + pushMention({cursor, name, 0, 0, id}); m_highlighter->rehighlight(); } else if (m_completionModel->autoCompletionType() == CompletionModel::Command) { auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString(); - auto text = m_room->chatBoxText(); + auto text = getText(); auto at = text.lastIndexOf(QLatin1Char('/')); QTextCursor cursor(document()->textDocument()); cursor.setPosition(at); @@ -225,7 +244,7 @@ void ChatDocumentHandler::complete(int index) cursor.insertText(QStringLiteral("/%1 ").arg(command)); } else if (m_completionModel->autoCompletionType() == CompletionModel::Room) { auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString(); - auto text = m_room->chatBoxText(); + auto text = getText(); auto at = text.lastIndexOf(QLatin1Char('#'), cursorPosition() - 1); QTextCursor cursor(document()->textDocument()); cursor.setPosition(at); @@ -234,11 +253,11 @@ void ChatDocumentHandler::complete(int index) cursor.setPosition(at); cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor); cursor.setKeepPositionOnInsert(true); - m_room->mentions()->push_back({cursor, alias, 0, 0, alias}); + pushMention({cursor, alias, 0, 0, alias}); m_highlighter->rehighlight(); } else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) { auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString(); - auto text = m_room->chatBoxText(); + auto text = getText(); auto at = text.lastIndexOf(QLatin1Char(':')); QTextCursor cursor(document()->textDocument()); cursor.setPosition(at); @@ -281,3 +300,27 @@ void ChatDocumentHandler::setSelectionEnd(int position) m_selectionEnd = position; Q_EMIT selectionEndChanged(); } + +QString ChatDocumentHandler::getText() const +{ + if (!m_room) { + return QString(); + } + if (m_isEdit) { + return m_room->editText(); + } else { + return m_room->chatBoxText(); + } +} + +void ChatDocumentHandler::pushMention(const Mention mention) const +{ + if (!m_room) { + return; + } + if (m_isEdit) { + m_room->editMentions()->push_back(mention); + } else { + m_room->mentions()->push_back(mention); + } +} diff --git a/src/chatdocumenthandler.h b/src/chatdocumenthandler.h index 0e0f72b33..096f0f098 100644 --- a/src/chatdocumenthandler.h +++ b/src/chatdocumenthandler.h @@ -9,6 +9,7 @@ #include "models/completionmodel.h" #include "models/userlistmodel.h" +#include "neochatroom.h" class QTextDocument; class NeoChatRoom; @@ -17,6 +18,14 @@ class SyntaxHighlighter; class ChatDocumentHandler : public QObject { Q_OBJECT + + /** + * @brief Is the instance being used to handle an edit message. + * + * This is needed to ensure that the text and mentions are saved and retrieved + * from the correct parameters in the assigned room. + */ + Q_PROPERTY(bool isEdit READ isEdit WRITE setIsEdit NOTIFY isEditChanged) Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged) Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged) Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged) @@ -24,11 +33,14 @@ class ChatDocumentHandler : public QObject Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged) - Q_PROPERTY(NeoChatRoom *room READ room NOTIFY roomChanged) + Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged) public: explicit ChatDocumentHandler(QObject *parent = nullptr); + [[nodiscard]] bool isEdit() const; + void setIsEdit(bool edit); + [[nodiscard]] QQuickTextDocument *document() const; void setDocument(QQuickTextDocument *document); @@ -49,6 +61,7 @@ public: void updateCompletions(); CompletionModel *completionModel() const; Q_SIGNALS: + void isEditChanged(); void documentChanged(); void cursorPositionChanged(); void roomChanged(); @@ -59,6 +72,8 @@ Q_SIGNALS: private: int completionStartIndex() const; + bool m_isEdit; + QQuickTextDocument *m_document; NeoChatRoom *m_room = nullptr; @@ -68,6 +83,9 @@ private: int m_selectionStart; int m_selectionEnd; + QString getText() const; + void pushMention(const Mention mention) const; + SyntaxHighlighter *m_highlighter = nullptr; CompletionModel::AutoCompletionType m_completionType = CompletionModel::None; diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index 01b5300ac..c9828e515 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -1626,6 +1626,17 @@ void NeoChatRoom::setChatBoxText(const QString &text) Q_EMIT chatBoxTextChanged(); } +QString NeoChatRoom::editText() const +{ + return m_editText; +} + +void NeoChatRoom::setEditText(const QString &text) +{ + m_editText = text; + Q_EMIT editTextChanged(); +} + QString NeoChatRoom::chatBoxReplyId() const { return m_chatBoxReplyId; @@ -1702,6 +1713,11 @@ QVector *NeoChatRoom::mentions() return &m_mentions; } +QVector *NeoChatRoom::editMentions() +{ + return &m_editMentions; +} + QString NeoChatRoom::savedText() const { return m_savedText; diff --git a/src/neochatroom.h b/src/neochatroom.h index fcd29689a..c15cdf14c 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -79,6 +79,11 @@ class NeoChatRoom : public Quotient::Room // Due to problems with QTextDocument, unlike the other properties here, chatBoxText is *not* used to store the text when switching rooms Q_PROPERTY(QString chatBoxText READ chatBoxText WRITE setChatBoxText NOTIFY chatBoxTextChanged) + + /** + * @brief The text for any message currently being edited in the room. + */ + Q_PROPERTY(QString editText READ editText WRITE setEditText NOTIFY editTextChanged) Q_PROPERTY(QString chatBoxReplyId READ chatBoxReplyId WRITE setChatBoxReplyId NOTIFY chatBoxReplyIdChanged) Q_PROPERTY(QString chatBoxEditId READ chatBoxEditId WRITE setChatBoxEditId NOTIFY chatBoxEditIdChanged) Q_PROPERTY(NeoChatUser *chatBoxReplyUser READ chatBoxReplyUser NOTIFY chatBoxReplyIdChanged) @@ -271,6 +276,9 @@ public: QString chatBoxText() const; void setChatBoxText(const QString &text); + QString editText() const; + void setEditText(const QString &text); + QString chatBoxReplyId() const; void setChatBoxReplyId(const QString &replyId); @@ -288,6 +296,11 @@ public: QVector *mentions(); + /** + * @brief Vector of mentions in the current edit text. + */ + QVector *editMentions(); + QString savedText() const; void setSavedText(const QString &savedText); @@ -337,10 +350,12 @@ private: QCoro::Task doUploadFile(QUrl url, QString body = QString()); QString m_chatBoxText; + QString m_editText; QString m_chatBoxReplyId; QString m_chatBoxEditId; QString m_chatBoxAttachmentPath; QVector m_mentions; + QVector m_editMentions; QString m_savedText; #ifdef QUOTIENT_07 QCache m_polls; @@ -363,6 +378,7 @@ Q_SIGNALS: void pushNotificationStateChanged(PushNotificationState::State state); void showMessage(MessageType messageType, const QString &message); void chatBoxTextChanged(); + void editTextChanged(); void chatBoxReplyIdChanged(); void chatBoxEditIdChanged(); void chatBoxAttachmentPathChanged(); diff --git a/src/qml/Component/ChatBox/ChatBar.qml b/src/qml/Component/ChatBox/ChatBar.qml index 52b9c7e78..75c9fa4c2 100644 --- a/src/qml/Component/ChatBox/ChatBar.qml +++ b/src/qml/Component/ChatBox/ChatBar.qml @@ -14,9 +14,7 @@ QQC2.Control { property alias textField: textField property bool isReplying: currentRoom.chatBoxReplyId.length > 0 - property bool isEditing: currentRoom.chatBoxEditId.length > 0 - property bool replyPaneVisible: isReplying || isEditing - property NeoChatUser replyUser: currentRoom.chatBoxReplyUser ?? currentRoom.chatBoxEditUser + property NeoChatUser replyUser: currentRoom.chatBoxReplyUser property bool attachmentPaneVisible: currentRoom.chatBoxAttachmentPath.length > 0 signal messageSent() @@ -122,7 +120,7 @@ QQC2.Control { leftPadding: LayoutMirroring.enabled ? actionsRow.width : (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing) rightPadding: LayoutMirroring.enabled ? (root.width > chatBoxMaxWidth ? 0 : Kirigami.Units.largeSpacing) : actionsRow.width - placeholderText: readOnly ? i18n("This room is encrypted. Build libQuotient with encryption enabled to send encrypted messages.") : currentRoom.chatBoxEditId.length > 0 ? i18n("Edit Message") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : currentRoom.chatBoxAttachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…") + placeholderText: readOnly ? i18n("This room is encrypted. Build libQuotient with encryption enabled to send encrypted messages.") : currentRoom.usesEncryption ? i18n("Send an encrypted message…") : currentRoom.chatBoxAttachmentPath.length > 0 ? i18n("Set an attachment caption...") : i18n("Send a message…") verticalAlignment: TextEdit.AlignVCenter wrapMode: Text.Wrap readOnly: (currentRoom.usesEncryption && !Controller.encryptionSupported) @@ -198,8 +196,8 @@ QQC2.Control { anchors.rightMargin: root.width > chatBoxMaxWidth ? 0 : (chatBarScrollView.QQC2.ScrollBar.vertical.visible ? Kirigami.Units.largeSpacing * 3.5 : Kirigami.Units.largeSpacing) active: visible - visible: root.replyPaneVisible || root.attachmentPaneVisible - sourceComponent: root.replyPaneVisible ? replyPane : attachmentPane + visible: root.isReplying || root.attachmentPaneVisible + sourceComponent: root.isReplying ? replyPane : attachmentPane } Component { id: replyPane @@ -207,8 +205,7 @@ QQC2.Control { userName: root.replyUser ? root.replyUser.displayName : "" userColor: root.replyUser ? root.replyUser.color : "" userAvatar: root.replyUser ? "image://mxc/" + currentRoom.getUser(root.replyUser.id).avatarMediaId : "" - isReply: root.isReplying - text: isEditing ? currentRoom.chatBoxEditMessage : currentRoom.chatBoxReplyMessage + text: currentRoom.chatBoxReplyMessage } } Component { @@ -263,14 +260,13 @@ QQC2.Control { anchors.right: parent.right anchors.rightMargin: (root.width - chatBoxMaxWidth) / 2 + Kirigami.Units.largeSpacing + (chatBarScrollView.QQC2.ScrollBar.vertical.visible && !(root.width > chatBoxMaxWidth) ? Kirigami.Units.largeSpacing * 2.5 : 0) - visible: root.replyPaneVisible + visible: root.isReplying display: QQC2.AbstractButton.IconOnly action: Kirigami.Action { - text: root.isReplying ? i18nc("@action:button", "Cancel reply") : i18nc("@action:button", "Cancel edit") + text: i18nc("@action:button", "Cancel reply") icon.name: "dialog-close" onTriggered: { currentRoom.chatBoxReplyId = ""; - currentRoom.chatBoxEditId = ""; currentRoom.chatBoxAttachmentPath = ""; root.forceActiveFocus() } @@ -356,15 +352,6 @@ QQC2.Control { } } - Connections { - target: currentRoom - function onChatBoxEditIdChanged() { - if (currentRoom.chatBoxEditMessage.length > 0) { - textField.text = currentRoom.chatBoxEditMessage - } - } - } - ChatDocumentHandler { id: documentHandler document: textField.textDocument @@ -398,12 +385,11 @@ QQC2.Control { } function postMessage() { - actionsHandler.handleMessage(); + actionsHandler.handleNewMessage(); repeatTimer.stop() currentRoom.markAllMessagesAsRead(); textField.clear(); currentRoom.chatBoxReplyId = ""; - currentRoom.chatBoxEditId = ""; messageSent() } } diff --git a/src/qml/Component/ChatBox/CompletionMenu.qml b/src/qml/Component/ChatBox/CompletionMenu.qml index 202374d4e..22e8eb37e 100644 --- a/src/qml/Component/ChatBox/CompletionMenu.qml +++ b/src/qml/Component/ChatBox/CompletionMenu.qml @@ -22,7 +22,7 @@ QQC2.Popup { connection: Controller.activeConnection } - required property var chatDocumentHandler + property var chatDocumentHandler Component.onCompleted: { chatDocumentHandler.completionModel.roomListModel = roomListModel; } diff --git a/src/qml/Component/ChatBox/ReplyPane.qml b/src/qml/Component/ChatBox/ReplyPane.qml index 25d4e9726..1851eeec5 100644 --- a/src/qml/Component/ChatBox/ReplyPane.qml +++ b/src/qml/Component/ChatBox/ReplyPane.qml @@ -15,7 +15,6 @@ GridLayout { property string userName property color userColor: Kirigami.Theme.highlightColor property var userAvatar: "" - property bool isReply property var text rows: 3 @@ -30,7 +29,7 @@ GridLayout { Layout.columnSpan: 3 topPadding: Kirigami.Units.smallSpacing - text: isReply ? i18n("Replying to:") : i18n("Editing message:") + text: i18n("Replying to:") } Rectangle { id: verticalBorder diff --git a/src/qml/Component/Timeline/MessageDelegate.qml b/src/qml/Component/Timeline/MessageDelegate.qml index 9e2e7e986..cec3d8622 100644 --- a/src/qml/Component/Timeline/MessageDelegate.qml +++ b/src/qml/Component/Timeline/MessageDelegate.qml @@ -21,8 +21,14 @@ TimelineContainer { RichLabel { id: label Layout.fillWidth: true + visible: currentRoom.chatBoxEditId !== model.eventId isEmote: messageDelegate.isEmote } + MessageEditComponent { + Layout.fillWidth: true + messageId: model.eventId + visible: currentRoom.chatBoxEditId === model.eventId + } Loader { id: linkPreviewLoader Layout.fillWidth: true diff --git a/src/qml/Component/Timeline/MessageEditComponent.qml b/src/qml/Component/Timeline/MessageEditComponent.qml new file mode 100644 index 000000000..d583b6701 --- /dev/null +++ b/src/qml/Component/Timeline/MessageEditComponent.qml @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: 2023 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 + +import org.kde.kirigami 2.15 as Kirigami +import org.kde.neochat 1.0 + +QQC2.TextArea { + id: root + + property string messageId + + Layout.fillWidth: true + Layout.minimumHeight: editButtons.height + topPadding + bottomPadding + Layout.preferredWidth: editTextMetrics.advanceWidth + rightPadding + Kirigami.Units.smallSpacing + Kirigami.Units.gridUnit + rightPadding: editButtons.width + editButtons.anchors.rightMargin * 2 + + color: Kirigami.Theme.textColor + verticalAlignment: TextEdit.AlignVCenter + wrapMode: Text.Wrap + + onVisibleChanged: { + if (visible) { + forceActiveFocus(); + root.cursorPosition = root.length; + } + } + onTextChanged: { + currentRoom.editText = text + } + + Keys.onEnterPressed: { + if (completionMenu.visible) { + completionMenu.complete() + } else if (event.modifiers & Qt.ShiftModifier) { + root.insert(cursorPosition, "\n") + } else { + root.postEdit(); + } + } + Keys.onReturnPressed: { + if (completionMenu.visible) { + completionMenu.complete() + } else if (event.modifiers & Qt.ShiftModifier) { + root.insert(cursorPosition, "\n") + } else { + root.postEdit(); + } + } + Keys.onTabPressed: { + if (completionMenu.visible) { + completionMenu.complete() + } + } + Keys.onPressed: { + if (event.key === Qt.Key_Up && completionMenu.visible) { + completionMenu.decrementIndex() + } else if (event.key === Qt.Key_Down && completionMenu.visible) { + completionMenu.incrementIndex() + } + } + + /** + * This is anchored like this so that control expands properly as the edited + * text grows in length. + */ + RowLayout { + id: editButtons + anchors.verticalCenter: root.verticalCenter + anchors.right: root.right + anchors.rightMargin: Kirigami.Units.smallSpacing + spacing: 0 + QQC2.ToolButton { + display: QQC2.AbstractButton.IconOnly + action: Kirigami.Action { + text: i18nc("@action:button", "Confirm edit") + icon.name: "checkmark" + onTriggered: { + root.postEdit(); + } + } + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + } + QQC2.ToolButton { + display: QQC2.AbstractButton.IconOnly + action: Kirigami.Action { + text: i18nc("@action:button", "Cancel edit") + icon.name: "dialog-close" + onTriggered: { + currentRoom.chatBoxEditId = ""; + } + shortcut: "Escape" + } + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + } + } + + Connections { + target: currentRoom + function onChatBoxEditIdChanged() { + if (currentRoom.chatBoxEditId == messageId && currentRoom.chatBoxEditMessage.length > 0) { + root.text = currentRoom.chatBoxEditMessage + } + } + } + + CompletionMenu { + id: completionMenu + height: implicitHeight + y: -height - 5 + z: 10 + chatDocumentHandler: documentHandler + Behavior on height { + NumberAnimation { + property: "height" + duration: Kirigami.Units.shortDuration + easing.type: Easing.OutCubic + } + } + } + + ChatDocumentHandler { + id: documentHandler + isEdit: true + document: root.textDocument + cursorPosition: root.cursorPosition + selectionStart: root.selectionStart + selectionEnd: root.selectionEnd + room: currentRoom // We don't care about saving for edits so this is OK. + } + + TextMetrics { + id: editTextMetrics + text: root.text + } + + function postEdit() { + actionsHandler.handleEdit(); + root.clear(); + currentRoom.chatBoxEditId = ""; + } +} + + diff --git a/src/qml/Page/RoomPage.qml b/src/qml/Page/RoomPage.qml index 9936c33b2..9060ee55a 100644 --- a/src/qml/Page/RoomPage.qml +++ b/src/qml/Page/RoomPage.qml @@ -540,7 +540,6 @@ Kirigami.ScrollablePage { onClicked: { currentRoom.chatBoxEditId = hoverActions.event.eventId; currentRoom.chatBoxReplyId = ""; - chatBox.chatBar.forceActiveFocus(); } } QQC2.Button { diff --git a/src/res.qrc b/src/res.qrc index fdcc70989..fd28cae4a 100644 --- a/src/res.qrc +++ b/src/res.qrc @@ -47,6 +47,7 @@ qml/Component/Timeline/PollDelegate.qml qml/Component/Timeline/MimeComponent.qml qml/Component/Timeline/StateComponent.qml + qml/Component/Timeline/MessageEditComponent.qml qml/Component/Login/LoginStep.qml qml/Component/Login/Login.qml qml/Component/Login/Password.qml diff --git a/src/roommanager.cpp b/src/roommanager.cpp index 8b92d531b..35713f67b 100644 --- a/src/roommanager.cpp +++ b/src/roommanager.cpp @@ -125,6 +125,9 @@ void RoomManager::openRoomForActiveConnection() void RoomManager::enterRoom(NeoChatRoom *room) { + if (m_currentRoom && !m_currentRoom->chatBoxEditId().isEmpty()) { + m_currentRoom->setChatBoxEditId(""); + } if (m_currentRoom && m_chatDocumentHandler) { // We're doing these things here because it is critical that they are switched at the same time m_currentRoom->setSavedText(m_chatDocumentHandler->document()->textDocument()->toPlainText());