From 47cd60fbe406e155bef3b6cb5c3abf42709ca0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= <sebastien.blin@savoirfairelinux.com> Date: Tue, 11 Oct 2022 16:24:04 -0400 Subject: [PATCH] messagelistmodel: support message edition Handle application/edited-message type to support message edition. Previous bodies are saved in the interaction to be able to get the original post to avoid unwanted editions. While loading a conversation, we store the editions in a temporary map that we link once the edited message is detected. This work because we can't edit a message before this message exists. PreviousBodies in interaction.h contains every previous body detected and the client can show previous version of the message in a popup. Deleting a message works the same way, just that any message with an empty body is not shown. https://git.jami.net/savoirfairelinux/jami-daemon/-/issues/316 Change-Id: Ib158abd16ad4b629532de11694e88d86a12d72a8 --- src/app/commoncomponents/EditedPopup.qml | 97 ++++++++++++++++++ src/app/commoncomponents/SBSContextMenu.qml | 22 +++++ src/app/commoncomponents/SBSMessageBase.qml | 5 +- .../commoncomponents/TextMessageDelegate.qml | 48 +++++++++ .../contextmenu/GeneralMenuItem.qml | 3 +- src/app/constant/JamiStrings.qml | 2 + src/app/constant/JamiTheme.qml | 1 + .../mainview/components/ChatViewFooter.qml | 29 +++++- src/app/mainview/components/EditContainer.qml | 99 +++++++++++++++++++ .../mainview/components/MessageListView.qml | 3 +- .../components/ParticipantOverlay.qml | 2 +- src/app/messagesadapter.cpp | 18 ++++ src/app/messagesadapter.h | 4 + src/app/qml.qrc | 2 + src/libclient/api/conversation.h | 2 +- src/libclient/api/conversationmodel.h | 7 ++ src/libclient/api/interaction.h | 22 ++++- src/libclient/conversationmodel.cpp | 33 ++++++- src/libclient/messagelistmodel.cpp | 56 ++++++++++- src/libclient/messagelistmodel.h | 4 + .../qtwrapper/configurationmanager_wrap.h | 7 +- 21 files changed, 448 insertions(+), 18 deletions(-) create mode 100644 src/app/commoncomponents/EditedPopup.qml create mode 100644 src/app/mainview/components/EditContainer.qml diff --git a/src/app/commoncomponents/EditedPopup.qml b/src/app/commoncomponents/EditedPopup.qml new file mode 100644 index 000000000..2df12aef0 --- /dev/null +++ b/src/app/commoncomponents/EditedPopup.qml @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects + +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 + +BaseModalDialog { + id: root + + width: 488 + height: 256 + + property var previousBodies: undefined + + popupContent: Item { + id: rect + + width: root.width + + JamiListView { + anchors.fill: parent + anchors.margins: JamiTheme.preferredMarginSize + + model: root.previousBodies + + delegate: Rectangle { + width: root.width - 2 * JamiTheme.preferredMarginSize + height: Math.max(JamiTheme.menuItemsPreferredHeight, rowBody.implicitHeight) + color: index % 2 === 0 ? JamiTheme.backgroundColor : JamiTheme.secondaryBackgroundColor + + RowLayout { + id: rowBody + spacing: JamiTheme.preferredMarginSize + width: parent.width + anchors.centerIn: parent + + Text { + Layout.preferredWidth: root.width / 4 + Layout.leftMargin: JamiTheme.settingsMarginSize + + text: MessagesAdapter.getFormattedDay(modelData.timestamp.toString()) + + " - " + MessagesAdapter.getFormattedTime(modelData.timestamp.toString()) + color: JamiTheme.textColor + opacity: 0.5 + } + + Text { + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + + TextMetrics { + id: metrics + elide: Text.ElideRight + elideWidth: 3 * rowBody.width / 4 - 2 * JamiTheme.preferredMarginSize + text: modelData.body + } + + text: metrics.elidedText + color: JamiTheme.textColor + } + } + } + } + + PushButton { + id: btnCancel + imageColor: "grey" + normalColor: "transparent" + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 10 + anchors.rightMargin: 10 + source: JamiResources.round_close_24dp_svg + onClicked: close() + } + } +} \ No newline at end of file diff --git a/src/app/commoncomponents/SBSContextMenu.qml b/src/app/commoncomponents/SBSContextMenu.qml index 8c6f0f094..261b0048b 100644 --- a/src/app/commoncomponents/SBSContextMenu.qml +++ b/src/app/commoncomponents/SBSContextMenu.qml @@ -29,6 +29,7 @@ ContextMenuAutoLoader { id: root property string location + property bool isOutgoing property string msgId property string transferName property string transferId @@ -57,8 +58,29 @@ ContextMenuAutoLoader { itemName: JamiStrings.reply onClicked: { + MessagesAdapter.editId = "" MessagesAdapter.replyToId = root.msgId } + }, + GeneralMenuItem { + id: edit + + canTrigger: transferId === "" && isOutgoing + itemName: JamiStrings.edit + onClicked: { + MessagesAdapter.replyToId = "" + MessagesAdapter.editId = root.msgId + } + }, + GeneralMenuItem { + id: deleteMsg + dangerous: true + + canTrigger: transferId === "" && isOutgoing + itemName: JamiStrings.optionDelete + onClicked: { + MessagesAdapter.editMessage(CurrentConversation.id, "", root.msgId) + } } ] diff --git a/src/app/commoncomponents/SBSMessageBase.qml b/src/app/commoncomponents/SBSMessageBase.qml index 343b4da43..13ccfd497 100644 --- a/src/app/commoncomponents/SBSMessageBase.qml +++ b/src/app/commoncomponents/SBSMessageBase.qml @@ -153,8 +153,8 @@ Control { var baseColor = isOutgoing ? JamiTheme.messageOutBgColor : CurrentConversation.isCoreDialog ? JamiTheme.messageInBgColor : Qt.lighter(CurrentConversation.color, 1.5) - if (Id === MessagesAdapter.replyToId) { - // If we are replying to + if (Id === MessagesAdapter.replyToId || Id === MessagesAdapter.editId) { + // If we are replying to or editing the message return Qt.darker(baseColor, 1.5) } return baseColor @@ -289,6 +289,7 @@ Control { msgId: Id location: root.location + isOutgoing: root.isOutgoing transferId: root.transferId transferName: root.transferName } diff --git a/src/app/commoncomponents/TextMessageDelegate.qml b/src/app/commoncomponents/TextMessageDelegate.qml index fdd352f19..6fb61fd46 100644 --- a/src/app/commoncomponents/TextMessageDelegate.qml +++ b/src/app/commoncomponents/TextMessageDelegate.qml @@ -42,6 +42,12 @@ SBSMessageBase { formattedDay: MessagesAdapter.getFormattedDay(Timestamp) extraHeight: extraContent.active && !isRemoteImage ? msgRadius : -isRemoteImage + EditedPopup { + id: editedPopup + + previousBodies: PreviousBodies + } + innerContent.children: [ TextEdit { id: textEditId @@ -106,6 +112,48 @@ SBSMessageBase { selectOnly: parent.readOnly } }, + RowLayout { + id: editedRow + anchors.right: isOutgoing ? parent.right : undefined + visible: PreviousBodies.length !== 0 + + ResponsiveImage { + id: editedImage + + Layout.leftMargin: JamiTheme.preferredMarginSize + Layout.bottomMargin: JamiTheme.preferredMarginSize + + source: JamiResources.round_edit_24dp_svg + width: JamiTheme.editedFontSize + height: JamiTheme.editedFontSize + layer { + enabled: true + effect: ColorOverlay { + color: editedLabel.color + } + } + } + + Text { + id: editedLabel + + Layout.rightMargin: JamiTheme.preferredMarginSize + Layout.bottomMargin: JamiTheme.preferredMarginSize + + text: JamiStrings.edited + color: UtilsAdapter.luma(bubble.color) ? + JamiTheme.chatviewTextColorLight : + JamiTheme.chatviewTextColorDark + font.pointSize: JamiTheme.editedFontSize + + TapHandler { + acceptedButtons: Qt.LeftButton + onTapped: { + editedPopup.open() + } + } + } + }, Loader { id: extraContent anchors.right: isOutgoing ? parent.right : undefined diff --git a/src/app/commoncomponents/contextmenu/GeneralMenuItem.qml b/src/app/commoncomponents/contextmenu/GeneralMenuItem.qml index 82a0b9fb1..d42b5cd5e 100644 --- a/src/app/commoncomponents/contextmenu/GeneralMenuItem.qml +++ b/src/app/commoncomponents/contextmenu/GeneralMenuItem.qml @@ -37,6 +37,7 @@ MenuItem { property bool canTrigger: true property bool addMenuSeparatorAfter: false property bool autoTextSizeAdjustment: true + property bool dangerous: false property BaseContextMenu parentMenu property int itemPreferredWidth: JamiTheme.menuItemsPreferredWidth @@ -94,7 +95,7 @@ MenuItem { Layout.fillWidth: true text: itemName - color: JamiTheme.textColor + color: dangerous ? JamiTheme.redColor : JamiTheme.textColor font.pointSize: JamiTheme.textFontSize horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml index 3e83a6c47..9624c0df4 100644 --- a/src/app/constant/JamiStrings.qml +++ b/src/app/constant/JamiStrings.qml @@ -726,6 +726,8 @@ Item { property string inReplyTo: qsTr("In reply to") property string reply: qsTr("Reply") property string writeTo: qsTr("Write to %1") + property string edit: qsTr("Edit") + property string edited: qsTr("Edited") // Invitation View property string invitationViewSentRequest: qsTr("%1 has sent you a request for a conversation.") diff --git a/src/app/constant/JamiTheme.qml b/src/app/constant/JamiTheme.qml index 67447c788..7712b000f 100644 --- a/src/app/constant/JamiTheme.qml +++ b/src/app/constant/JamiTheme.qml @@ -267,6 +267,7 @@ Item { property real smartlistItemInfoFontSize: calcSize(9 + fontSizeOffsetSmall) property real filterItemFontSize: calcSize(smartlistItemFontSize) property real filterBadgeFontSize: calcSize(8.25) + property real editedFontSize: calcSize(8) property real accountListItemHeight: 64 property real accountListAvatarSize: 40 property real smartListItemHeight: 64 diff --git a/src/app/mainview/components/ChatViewFooter.qml b/src/app/mainview/components/ChatViewFooter.qml index c51070764..9ab6cd287 100644 --- a/src/app/mainview/components/ChatViewFooter.qml +++ b/src/app/mainview/components/ChatViewFooter.qml @@ -81,6 +81,16 @@ Rectangle { function onNewTextPasted() { messageBar.textAreaObj.pasteText() } + + function onEditIdChanged() { + if (MessagesAdapter.editId.length > 0) + messageBar.textAreaObj.forceActiveFocus() + } + + function onReplyToIdChanged() { + if (MessagesAdapter.replyToId.length > 0) + messageBar.forceActiveFocus() + } } RecordBox { @@ -131,6 +141,16 @@ Rectangle { visible: MessagesAdapter.replyToId !== "" } + EditContainer { + id: editContainer + + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: footerColumnLayout.width + Layout.maximumWidth: JamiTheme.chatViewMaximumWidth + Layout.preferredHeight: 36 + visible: MessagesAdapter.editId !== "" + } + MessageBar { id: messageBar @@ -162,8 +182,13 @@ Rectangle { onSendFileButtonClicked: jamiFileDialog.open() onSendMessageButtonClicked: { // Send text message - if (messageBar.text) - MessagesAdapter.sendMessage(messageBar.text) + if (messageBar.text) { + if (MessagesAdapter.editId !== "") { + MessagesAdapter.editMessage(CurrentConversation.id, messageBar.text) + } else { + MessagesAdapter.sendMessage(messageBar.text) + } + } messageBar.textAreaObj.clearText() // Send file messages diff --git a/src/app/mainview/components/EditContainer.qml b/src/app/mainview/components/EditContainer.qml new file mode 100644 index 000000000..5f1360987 --- /dev/null +++ b/src/app/mainview/components/EditContainer.qml @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 +import net.jami.Models 1.1 + +import "../../commoncomponents" + +Rectangle { + id: root + + color: JamiTheme.messageOutBgColor + + property var body: { + if (MessagesAdapter.editId === "") + return "" + + return MessagesAdapter.dataForInteraction(MessagesAdapter.editId, MessageList.Body) + } + + RowLayout { + anchors.fill: parent + spacing: 12 + + RowLayout { + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + + Label { + id: editLbl + + text: JamiStrings.edit + + color: UtilsAdapter.luma(root.color) ? + JamiTheme.chatviewTextColorLight : + JamiTheme.chatviewTextColorDark + font.pointSize: JamiTheme.textFontSize + font.kerning: true + font.bold: true + Layout.leftMargin: JamiTheme.preferredMarginSize + } + + Label { + id: bodyLbl + + TextMetrics { + id: metrics + elide: Text.ElideRight + elideWidth: root.width - 100 + text: root.body + } + + text: metrics.elidedText + color: UtilsAdapter.luma(root.color) ? + JamiTheme.chatviewTextColorLight : + JamiTheme.chatviewTextColorDark + font.pointSize: JamiTheme.textFontSize + font.kerning: true + font.bold: true + } + } + + + PushButton { + id: closeReply + + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.rightMargin: JamiTheme.preferredMarginSize + + preferredSize: 24 + + source: JamiResources.round_close_24dp_svg + + normalColor: JamiTheme.chatviewBgColor + imageColor: JamiTheme.chatviewButtonColor + + onClicked: MessagesAdapter.editId = "" + } + } +} diff --git a/src/app/mainview/components/MessageListView.qml b/src/app/mainview/components/MessageListView.qml index ffc2c0125..01cff87db 100644 --- a/src/app/mainview/components/MessageListView.qml +++ b/src/app/mainview/components/MessageListView.qml @@ -193,7 +193,8 @@ JamiListView { onMessageListModelChanged: sourceModel = messageListModel filters: ExpressionFilter { readonly property int mergeType: Interaction.Type.MERGE - expression: Body !== "" && Type !== mergeType + readonly property int editedType: Interaction.Type.EDITED + expression: Body !== "" && Type !== mergeType && Type !== editedType } sorters: ExpressionSorter { expression: modelLeft.index > modelRight.index diff --git a/src/app/mainview/components/ParticipantOverlay.qml b/src/app/mainview/components/ParticipantOverlay.qml index e66bdb54b..3de0ad19a 100644 --- a/src/app/mainview/components/ParticipantOverlay.qml +++ b/src/app/mainview/components/ParticipantOverlay.qml @@ -291,7 +291,7 @@ Item { visible: (!root.isMe && !root.meModerator) ? root.participantIsMuted : root.isLocalMuted source: JamiResources.micro_off_black_24dp_svg - color: "red" + color: JamiTheme.redColor HoverHandler { id: hoverMicrophone } MaterialToolTip { diff --git a/src/app/messagesadapter.cpp b/src/app/messagesadapter.cpp index b3ae25b31..3d9f70ba6 100644 --- a/src/app/messagesadapter.cpp +++ b/src/app/messagesadapter.cpp @@ -52,6 +52,7 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager, { connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, [this]() { set_replyToId(""); + set_editId(""); const QString& convId = lrcInstance_->get_selectedConvUid(); const auto& conversation = lrcInstance_->getConversationFromConvUid(convId); set_messageListModel(QVariant::fromValue(conversation.interactions.get())); @@ -155,6 +156,23 @@ MessagesAdapter::sendMessage(const QString& message) } } +void +MessagesAdapter::editMessage(const QString& convId, const QString& newBody, const QString& messageId) +{ + try { + const auto convUid = lrcInstance_->get_selectedConvUid(); + auto editId = !messageId.isEmpty() ? messageId : editId_; + if (editId.isEmpty()) { + qWarning("No message to edit"); + return; + } + lrcInstance_->getCurrentConversationModel()->editMessage(convId, newBody, editId); + set_editId(""); + } catch (...) { + qDebug() << "Exception during message edition:" << messageId; + } +} + void MessagesAdapter::sendFile(const QString& message) { diff --git a/src/app/messagesadapter.h b/src/app/messagesadapter.h index 210979347..b059fc8e6 100644 --- a/src/app/messagesadapter.h +++ b/src/app/messagesadapter.h @@ -32,6 +32,7 @@ class MessagesAdapter final : public QmlAdapterBase Q_OBJECT QML_RO_PROPERTY(QVariant, messageListModel) QML_PROPERTY(QString, replyToId) + QML_PROPERTY(QString, editId) QML_RO_PROPERTY(QList<QString>, currentConvComposingList) public: @@ -67,6 +68,9 @@ protected: Q_INVOKABLE void unbanContact(int index); Q_INVOKABLE void unbanConversation(const QString& convUid); Q_INVOKABLE void sendMessage(const QString& message); + Q_INVOKABLE void editMessage(const QString& convId, + const QString& newBody, + const QString& messageId = ""); Q_INVOKABLE void sendFile(const QString& message); Q_INVOKABLE void acceptFile(const QString& arg); Q_INVOKABLE void cancelFile(const QString& arg); diff --git a/src/app/qml.qrc b/src/app/qml.qrc index 854594f81..3560ee0bb 100644 --- a/src/app/qml.qrc +++ b/src/app/qml.qrc @@ -142,6 +142,7 @@ <file>mainview/components/CallButtonDelegate.qml</file> <file>mainview/components/CallActionBar.qml</file> <file>commoncomponents/HalfPill.qml</file> + <file>commoncomponents/EditedPopup.qml</file> <file>commoncomponents/MaterialToolTip.qml</file> <file>mainview/components/ParticipantCallInStatusDelegate.qml</file> <file>mainview/components/ParticipantCallInStatusView.qml</file> @@ -163,6 +164,7 @@ <file>mainview/components/MessageBar.qml</file> <file>mainview/components/FilesToSendContainer.qml</file> <file>mainview/components/ReplyingContainer.qml</file> + <file>mainview/components/EditContainer.qml</file> <file>commoncomponents/Avatar.qml</file> <file>mainview/components/ConversationAvatar.qml</file> <file>mainview/components/InvitationView.qml</file> diff --git a/src/libclient/api/conversation.h b/src/libclient/api/conversation.h index 51911788e..5cb87ecc0 100644 --- a/src/libclient/api/conversation.h +++ b/src/libclient/api/conversation.h @@ -74,7 +74,7 @@ struct Info QString callId; QString confId; std::unique_ptr<MessageListModel> interactions; - QString lastMessageUid = 0; + QString lastMessageUid; QHash<QString, QString> parentsId; // pair messageid/parentid for messages without parent loaded unsigned int unreadMessages = 0; QVector<QPair<int, QString>> errors; diff --git a/src/libclient/api/conversationmodel.h b/src/libclient/api/conversationmodel.h index 29907bb8d..d0871caac 100644 --- a/src/libclient/api/conversationmodel.h +++ b/src/libclient/api/conversationmodel.h @@ -217,6 +217,13 @@ public: * @param parentId id of parent message. Default is "" - last message in conversation. */ void sendMessage(const QString& uid, const QString& body, const QString& parentId = ""); + /** + * Edit a message (empty body = delete message) + * @param convId The conversation with the message to edit + * @param newBody The new body + * @param messageId The id of the message (MUST be by the same author & plain/text) + */ + void editMessage(const QString& convId, const QString& newBody, const QString& messageId); /** * Modify the current filter (will change the result of getFilteredConversations) * @param filter the new filter diff --git a/src/libclient/api/interaction.h b/src/libclient/api/interaction.h index 3b32df017..5993fc79e 100644 --- a/src/libclient/api/interaction.h +++ b/src/libclient/api/interaction.h @@ -32,7 +32,7 @@ namespace interaction { Q_NAMESPACE Q_CLASSINFO("RegisterEnumClassesUnscoped", "false") -enum class Type { INVALID, INITIAL, TEXT, CALL, CONTACT, DATA_TRANSFER, MERGE, COUNT__ }; +enum class Type { INVALID, INITIAL, TEXT, CALL, CONTACT, DATA_TRANSFER, MERGE, EDITED, COUNT__ }; Q_ENUM_NS(Type) static inline const QString @@ -51,6 +51,8 @@ to_string(const Type& type) return "DATA_TRANSFER"; case Type::MERGE: return "MERGE"; + case Type::EDITED: + return "EDITED"; case Type::INVALID: case Type::COUNT__: default: @@ -73,6 +75,8 @@ to_type(const QString& type) return interaction::Type::DATA_TRANSFER; else if (type == "merge") return interaction::Type::MERGE; + else if (type == "application/edited-message") + return interaction::Type::EDITED; else return interaction::Type::INVALID; } @@ -238,6 +242,19 @@ getContactInteractionString(const QString& authorUri, const ContactAction& actio return {}; } +struct Body +{ + Q_GADGET + + Q_PROPERTY(QString commitId MEMBER commitId) + Q_PROPERTY(QString body MEMBER body) + Q_PROPERTY(int timestamp MEMBER timestamp) +public: + QString commitId; + QString body; + std::time_t timestamp; +}; + /** * @var authorUri * @var body @@ -263,6 +280,7 @@ struct Info MapStringString commit; QVariantMap linkPreviewInfo = {}; bool linkified = false; + QVector<Body> previousBodies; Info() {} @@ -286,7 +304,7 @@ struct Info Info(const MapStringString& message, const QString& accountURI) { type = to_type(message["type"]); - if (type == Type::TEXT) { + if (type == Type::TEXT || type == Type::EDITED) { body = message["body"]; } authorUri = accountURI == message["author"] ? "" : message["author"]; diff --git a/src/libclient/conversationmodel.cpp b/src/libclient/conversationmodel.cpp index 0a4184ffb..c904ad1df 100644 --- a/src/libclient/conversationmodel.cpp +++ b/src/libclient/conversationmodel.cpp @@ -220,7 +220,7 @@ public: const VectorString peersForConversation(const conversation::Info& conversation) const; // insert swarm interactions. Return false if interaction already exists. bool insertSwarmInteraction(const QString& interactionId, - const interaction::Info& interaction, + interaction::Info& interaction, conversation::Info& conversation, bool insertAtBegin); void invalidateModel(); @@ -1154,7 +1154,7 @@ ConversationModel::sendMessage(const QString& uid, const QString& body, const QS try { auto& conversation = pimpl_->getConversationForUid(uid, true).get(); if (!conversation.isLegacy()) { - ConfigurationManager::instance().sendMessage(owner.id, uid, body, parentId); + ConfigurationManager::instance().sendMessage(owner.id, uid, body, parentId, 0); return; } @@ -1181,7 +1181,8 @@ ConversationModel::sendMessage(const QString& uid, const QString& body, const QS ConfigurationManager::instance().sendMessage(owner.id, conversationId, body, - parentId); + parentId, + 0); return; } auto& peers = pimpl_->peersForConversation(newConv); @@ -1271,6 +1272,18 @@ ConversationModel::sendMessage(const QString& uid, const QString& body, const QS } } +void +ConversationModel::editMessage(const QString& convId, + const QString& newBody, + const QString& messageId) +{ + auto conversationOpt = getConversationForUid(convId); + if (!conversationOpt.has_value()) { + return; + } + ConfigurationManager::instance().sendMessage(owner.id, convId, newBody, messageId, 1); +} + void ConversationModel::refreshFilter() { @@ -2323,6 +2336,7 @@ ConversationModelPimpl::slotConversationLoaded(uint32_t requestId, } auto msgId = message["id"]; auto msg = interaction::Info(message, linked.owner.profileInfo.uri); + conversation.interactions->editMessage(msgId, msg); auto downloadFile = false; if (msg.type == interaction::Type::INITIAL) { allLoaded = true; @@ -2356,6 +2370,8 @@ ConversationModelPimpl::slotConversationLoaded(uint32_t requestId, msg.body = interaction::getContactInteractionString(bestName, interaction::to_action( message["action"])); + } else if (msg.type == interaction::Type::EDITED) { + conversation.interactions->addEdition(msgId, msg, false); } insertSwarmInteraction(msgId, msg, conversation, true); if (downloadFile) { @@ -2380,7 +2396,6 @@ ConversationModelPimpl::slotConversationLoaded(uint32_t requestId, return; } } - // In this case, we only have loaded merge commits. Load more messages ConfigurationManager::instance().loadConversationMessages(linked.owner.id, conversationId, @@ -2427,6 +2442,7 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId, } auto msgId = message["id"]; auto msg = interaction::Info(message, linked.owner.profileInfo.uri); + conversation.interactions->editMessage(msgId, msg); api::datatransfer::Info info; QString fileId; @@ -2464,6 +2480,8 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId, } else if (msg.type == interaction::Type::TEXT && msg.authorUri != linked.owner.profileInfo.uri) { conversation.unreadMessages++; + } else if (msg.type == interaction::Type::EDITED) { + conversation.interactions->addEdition(msgId, msg, true); } if (!insertSwarmInteraction(msgId, msg, conversation, false)) { // message already exists @@ -2510,7 +2528,7 @@ ConversationModelPimpl::slotConversationProfileUpdated(const QString& accountId, bool ConversationModelPimpl::insertSwarmInteraction(const QString& interactionId, - const interaction::Info& interaction, + interaction::Info& interaction, conversation::Info& conversation, bool insertAtBegin) { @@ -2518,6 +2536,11 @@ ConversationModelPimpl::insertSwarmInteraction(const QString& interactionId, auto itExists = conversation.interactions->find(interactionId); if (itExists != conversation.interactions->end()) { // Erase interaction if exists, as it will be updated via a re-insertion + if (itExists->second.previousBodies.size() != 0) { + // If the message was edited, we should keep this state + interaction.body = itExists->second.body; + interaction.previousBodies = itExists->second.previousBodies; + } itExists = conversation.interactions->erase(itExists); if (itExists != conversation.interactions->end()) { // next interaction doesn't have parent anymore. diff --git a/src/libclient/messagelistmodel.cpp b/src/libclient/messagelistmodel.cpp index 339ec2895..a2c5cb43e 100644 --- a/src/libclient/messagelistmodel.cpp +++ b/src/libclient/messagelistmodel.cpp @@ -176,6 +176,7 @@ MessageListModel::clear() Q_EMIT beginResetModel(); interactions_.clear(); replyTo_.clear(); + editedBodies_.clear(); Q_EMIT endResetModel(); } @@ -410,6 +411,13 @@ MessageListModel::dataForItem(item_t item, int, int role) const return QVariant(item.second.commit["uri"]); case Role::ContactAction: return QVariant(item.second.commit["action"]); + case Role::PreviousBodies: { + QVariantList variantList; + for (int i = 0; i < item.second.previousBodies.size(); i++) { + variantList.append(QVariant::fromValue(item.second.previousBodies[i])); + } + return variantList; + } case Role::ReplyTo: return QVariant(replyId); case Role::ReplyToAuthor: @@ -541,12 +549,58 @@ MessageListModel::emitDataChanged(const QString& msgId, VectorInt roles) Q_EMIT dataChanged(modelIndex, modelIndex, roles); } +void +MessageListModel::addEdition(const QString& msgId, const interaction::Info& info, bool end) +{ + auto editedId = info.commit["edit"]; + if (editedId.isEmpty()) + return; + auto& edited = editedBodies_[editedId]; + auto value = interaction::Body {msgId, info.body, info.timestamp}; + if (end) + edited.push_back(value); + else + edited.push_front(value); + auto editedIt = find(editedId); + if (editedIt != interactions_.end()) { + // If already there, we can update the content + editMessage(editedId, editedIt->second); + } +} + +void +MessageListModel::editMessage(const QString& msgId, interaction::Info& info) +{ + auto it = editedBodies_.find(msgId); + if (it != editedBodies_.end()) { + if (info.previousBodies.isEmpty()) { + info.previousBodies.push_back(interaction::Body {msgId, info.body, info.timestamp}); + } + // Find if already added (because MessageReceived can be triggered + // multiple times for same message) + for (const auto& editedBody : *it) { + auto itCommit = std::find_if(info.previousBodies.begin(), + info.previousBodies.end(), + [&](const auto& element) { + return element.commitId == editedBody.commitId; + }); + if (itCommit == info.previousBodies.end()) { + info.previousBodies.push_back(editedBody); + } + } + info.body = it->rbegin()->body; + editedBodies_.erase(it); + emitDataChanged(msgId, {MessageList::Role::Body, MessageList::Role::PreviousBodies}); + } +} + QString MessageListModel::lastMessageUid() const { for (auto it = interactions_.rbegin(); it != interactions_.rend(); ++it) { auto lastType = it->second.type; - if (lastType != interaction::Type::MERGE and !it->second.body.isEmpty()) { + if (lastType != interaction::Type::MERGE and lastType != interaction::Type::EDITED + and !it->second.body.isEmpty()) { return it->first; } } diff --git a/src/libclient/messagelistmodel.h b/src/libclient/messagelistmodel.h index b3d494ac7..241e59bd7 100644 --- a/src/libclient/messagelistmodel.h +++ b/src/libclient/messagelistmodel.h @@ -45,6 +45,7 @@ struct Info; X(ActionUri) \ X(LinkPreviewInfo) \ X(Linkified) \ + X(PreviousBodies) \ X(ReplyTo) \ X(ReplyToBody) \ X(ReplyToAuthor) \ @@ -130,6 +131,8 @@ public: Q_SIGNAL void timestampUpdate(); + void addEdition(const QString& msgId, const interaction::Info& info, bool end); + void editMessage(const QString& msgId, interaction::Info& info); QString lastMessageUid() const; protected: @@ -145,6 +148,7 @@ private: QMap<QString, QString> lastDisplayedMessageUid_; QMap<QString, QStringList> messageToReaders_; QMap<QString, QStringList> replyTo_; + QMap<QString, QVector<interaction::Body>> editedBodies_; void moveMessage(const QString& msgId, const QString& parentId); void insertMessage(int index, item_t& message); diff --git a/src/libclient/qtwrapper/configurationmanager_wrap.h b/src/libclient/qtwrapper/configurationmanager_wrap.h index 4946e86a5..ad7328bf0 100644 --- a/src/libclient/qtwrapper/configurationmanager_wrap.h +++ b/src/libclient/qtwrapper/configurationmanager_wrap.h @@ -1047,13 +1047,16 @@ public Q_SLOTS: // METHODS void sendMessage(const QString& accountId, const QString& conversationId, const QString& message, - const QString& parent) + const QString& parent, + int flags = 0) { DRing::sendMessage(accountId.toStdString(), conversationId.toStdString(), message.toStdString(), - parent.toStdString()); + parent.toStdString(), + flags); } + uint32_t loadConversationMessages(const QString& accountId, const QString& conversationId, const QString& fromId, -- GitLab