1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-09-05 06:33:25 +02:00

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
This commit is contained in:
Sébastien Blin 2022-10-11 16:24:04 -04:00
parent 9457c7ccbb
commit 47cd60fbe4
21 changed files with 448 additions and 18 deletions

View file

@ -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()
}
}
}

View file

@ -29,6 +29,7 @@ ContextMenuAutoLoader {
id: root id: root
property string location property string location
property bool isOutgoing
property string msgId property string msgId
property string transferName property string transferName
property string transferId property string transferId
@ -57,8 +58,29 @@ ContextMenuAutoLoader {
itemName: JamiStrings.reply itemName: JamiStrings.reply
onClicked: { onClicked: {
MessagesAdapter.editId = ""
MessagesAdapter.replyToId = root.msgId 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)
}
} }
] ]

View file

@ -153,8 +153,8 @@ Control {
var baseColor = isOutgoing ? JamiTheme.messageOutBgColor var baseColor = isOutgoing ? JamiTheme.messageOutBgColor
: CurrentConversation.isCoreDialog ? : CurrentConversation.isCoreDialog ?
JamiTheme.messageInBgColor : Qt.lighter(CurrentConversation.color, 1.5) JamiTheme.messageInBgColor : Qt.lighter(CurrentConversation.color, 1.5)
if (Id === MessagesAdapter.replyToId) { if (Id === MessagesAdapter.replyToId || Id === MessagesAdapter.editId) {
// If we are replying to // If we are replying to or editing the message
return Qt.darker(baseColor, 1.5) return Qt.darker(baseColor, 1.5)
} }
return baseColor return baseColor
@ -289,6 +289,7 @@ Control {
msgId: Id msgId: Id
location: root.location location: root.location
isOutgoing: root.isOutgoing
transferId: root.transferId transferId: root.transferId
transferName: root.transferName transferName: root.transferName
} }

View file

@ -42,6 +42,12 @@ SBSMessageBase {
formattedDay: MessagesAdapter.getFormattedDay(Timestamp) formattedDay: MessagesAdapter.getFormattedDay(Timestamp)
extraHeight: extraContent.active && !isRemoteImage ? msgRadius : -isRemoteImage extraHeight: extraContent.active && !isRemoteImage ? msgRadius : -isRemoteImage
EditedPopup {
id: editedPopup
previousBodies: PreviousBodies
}
innerContent.children: [ innerContent.children: [
TextEdit { TextEdit {
id: textEditId id: textEditId
@ -106,6 +112,48 @@ SBSMessageBase {
selectOnly: parent.readOnly 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 { Loader {
id: extraContent id: extraContent
anchors.right: isOutgoing ? parent.right : undefined anchors.right: isOutgoing ? parent.right : undefined

View file

@ -37,6 +37,7 @@ MenuItem {
property bool canTrigger: true property bool canTrigger: true
property bool addMenuSeparatorAfter: false property bool addMenuSeparatorAfter: false
property bool autoTextSizeAdjustment: true property bool autoTextSizeAdjustment: true
property bool dangerous: false
property BaseContextMenu parentMenu property BaseContextMenu parentMenu
property int itemPreferredWidth: JamiTheme.menuItemsPreferredWidth property int itemPreferredWidth: JamiTheme.menuItemsPreferredWidth
@ -94,7 +95,7 @@ MenuItem {
Layout.fillWidth: true Layout.fillWidth: true
text: itemName text: itemName
color: JamiTheme.textColor color: dangerous ? JamiTheme.redColor : JamiTheme.textColor
font.pointSize: JamiTheme.textFontSize font.pointSize: JamiTheme.textFontSize
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter

View file

@ -726,6 +726,8 @@ Item {
property string inReplyTo: qsTr("In reply to") property string inReplyTo: qsTr("In reply to")
property string reply: qsTr("Reply") property string reply: qsTr("Reply")
property string writeTo: qsTr("Write to %1") property string writeTo: qsTr("Write to %1")
property string edit: qsTr("Edit")
property string edited: qsTr("Edited")
// Invitation View // Invitation View
property string invitationViewSentRequest: qsTr("%1 has sent you a request for a conversation.") property string invitationViewSentRequest: qsTr("%1 has sent you a request for a conversation.")

View file

@ -267,6 +267,7 @@ Item {
property real smartlistItemInfoFontSize: calcSize(9 + fontSizeOffsetSmall) property real smartlistItemInfoFontSize: calcSize(9 + fontSizeOffsetSmall)
property real filterItemFontSize: calcSize(smartlistItemFontSize) property real filterItemFontSize: calcSize(smartlistItemFontSize)
property real filterBadgeFontSize: calcSize(8.25) property real filterBadgeFontSize: calcSize(8.25)
property real editedFontSize: calcSize(8)
property real accountListItemHeight: 64 property real accountListItemHeight: 64
property real accountListAvatarSize: 40 property real accountListAvatarSize: 40
property real smartListItemHeight: 64 property real smartListItemHeight: 64

View file

@ -81,6 +81,16 @@ Rectangle {
function onNewTextPasted() { function onNewTextPasted() {
messageBar.textAreaObj.pasteText() messageBar.textAreaObj.pasteText()
} }
function onEditIdChanged() {
if (MessagesAdapter.editId.length > 0)
messageBar.textAreaObj.forceActiveFocus()
}
function onReplyToIdChanged() {
if (MessagesAdapter.replyToId.length > 0)
messageBar.forceActiveFocus()
}
} }
RecordBox { RecordBox {
@ -131,6 +141,16 @@ Rectangle {
visible: MessagesAdapter.replyToId !== "" 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 { MessageBar {
id: messageBar id: messageBar
@ -162,8 +182,13 @@ Rectangle {
onSendFileButtonClicked: jamiFileDialog.open() onSendFileButtonClicked: jamiFileDialog.open()
onSendMessageButtonClicked: { onSendMessageButtonClicked: {
// Send text message // Send text message
if (messageBar.text) if (messageBar.text) {
MessagesAdapter.sendMessage(messageBar.text) if (MessagesAdapter.editId !== "") {
MessagesAdapter.editMessage(CurrentConversation.id, messageBar.text)
} else {
MessagesAdapter.sendMessage(messageBar.text)
}
}
messageBar.textAreaObj.clearText() messageBar.textAreaObj.clearText()
// Send file messages // Send file messages

View file

@ -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 = ""
}
}
}

View file

@ -193,7 +193,8 @@ JamiListView {
onMessageListModelChanged: sourceModel = messageListModel onMessageListModelChanged: sourceModel = messageListModel
filters: ExpressionFilter { filters: ExpressionFilter {
readonly property int mergeType: Interaction.Type.MERGE 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 { sorters: ExpressionSorter {
expression: modelLeft.index > modelRight.index expression: modelLeft.index > modelRight.index

View file

@ -291,7 +291,7 @@ Item {
visible: (!root.isMe && !root.meModerator) ? root.participantIsMuted : root.isLocalMuted visible: (!root.isMe && !root.meModerator) ? root.participantIsMuted : root.isLocalMuted
source: JamiResources.micro_off_black_24dp_svg source: JamiResources.micro_off_black_24dp_svg
color: "red" color: JamiTheme.redColor
HoverHandler { id: hoverMicrophone } HoverHandler { id: hoverMicrophone }
MaterialToolTip { MaterialToolTip {

View file

@ -52,6 +52,7 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
{ {
connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, [this]() { connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, [this]() {
set_replyToId(""); set_replyToId("");
set_editId("");
const QString& convId = lrcInstance_->get_selectedConvUid(); const QString& convId = lrcInstance_->get_selectedConvUid();
const auto& conversation = lrcInstance_->getConversationFromConvUid(convId); const auto& conversation = lrcInstance_->getConversationFromConvUid(convId);
set_messageListModel(QVariant::fromValue(conversation.interactions.get())); 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 void
MessagesAdapter::sendFile(const QString& message) MessagesAdapter::sendFile(const QString& message)
{ {

View file

@ -32,6 +32,7 @@ class MessagesAdapter final : public QmlAdapterBase
Q_OBJECT Q_OBJECT
QML_RO_PROPERTY(QVariant, messageListModel) QML_RO_PROPERTY(QVariant, messageListModel)
QML_PROPERTY(QString, replyToId) QML_PROPERTY(QString, replyToId)
QML_PROPERTY(QString, editId)
QML_RO_PROPERTY(QList<QString>, currentConvComposingList) QML_RO_PROPERTY(QList<QString>, currentConvComposingList)
public: public:
@ -67,6 +68,9 @@ protected:
Q_INVOKABLE void unbanContact(int index); Q_INVOKABLE void unbanContact(int index);
Q_INVOKABLE void unbanConversation(const QString& convUid); Q_INVOKABLE void unbanConversation(const QString& convUid);
Q_INVOKABLE void sendMessage(const QString& message); 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 sendFile(const QString& message);
Q_INVOKABLE void acceptFile(const QString& arg); Q_INVOKABLE void acceptFile(const QString& arg);
Q_INVOKABLE void cancelFile(const QString& arg); Q_INVOKABLE void cancelFile(const QString& arg);

View file

@ -142,6 +142,7 @@
<file>mainview/components/CallButtonDelegate.qml</file> <file>mainview/components/CallButtonDelegate.qml</file>
<file>mainview/components/CallActionBar.qml</file> <file>mainview/components/CallActionBar.qml</file>
<file>commoncomponents/HalfPill.qml</file> <file>commoncomponents/HalfPill.qml</file>
<file>commoncomponents/EditedPopup.qml</file>
<file>commoncomponents/MaterialToolTip.qml</file> <file>commoncomponents/MaterialToolTip.qml</file>
<file>mainview/components/ParticipantCallInStatusDelegate.qml</file> <file>mainview/components/ParticipantCallInStatusDelegate.qml</file>
<file>mainview/components/ParticipantCallInStatusView.qml</file> <file>mainview/components/ParticipantCallInStatusView.qml</file>
@ -163,6 +164,7 @@
<file>mainview/components/MessageBar.qml</file> <file>mainview/components/MessageBar.qml</file>
<file>mainview/components/FilesToSendContainer.qml</file> <file>mainview/components/FilesToSendContainer.qml</file>
<file>mainview/components/ReplyingContainer.qml</file> <file>mainview/components/ReplyingContainer.qml</file>
<file>mainview/components/EditContainer.qml</file>
<file>commoncomponents/Avatar.qml</file> <file>commoncomponents/Avatar.qml</file>
<file>mainview/components/ConversationAvatar.qml</file> <file>mainview/components/ConversationAvatar.qml</file>
<file>mainview/components/InvitationView.qml</file> <file>mainview/components/InvitationView.qml</file>

View file

@ -74,7 +74,7 @@ struct Info
QString callId; QString callId;
QString confId; QString confId;
std::unique_ptr<MessageListModel> interactions; std::unique_ptr<MessageListModel> interactions;
QString lastMessageUid = 0; QString lastMessageUid;
QHash<QString, QString> parentsId; // pair messageid/parentid for messages without parent loaded QHash<QString, QString> parentsId; // pair messageid/parentid for messages without parent loaded
unsigned int unreadMessages = 0; unsigned int unreadMessages = 0;
QVector<QPair<int, QString>> errors; QVector<QPair<int, QString>> errors;

View file

@ -217,6 +217,13 @@ public:
* @param parentId id of parent message. Default is "" - last message in conversation. * @param parentId id of parent message. Default is "" - last message in conversation.
*/ */
void sendMessage(const QString& uid, const QString& body, const QString& parentId = ""); 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) * Modify the current filter (will change the result of getFilteredConversations)
* @param filter the new filter * @param filter the new filter

View file

@ -32,7 +32,7 @@ namespace interaction {
Q_NAMESPACE Q_NAMESPACE
Q_CLASSINFO("RegisterEnumClassesUnscoped", "false") 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) Q_ENUM_NS(Type)
static inline const QString static inline const QString
@ -51,6 +51,8 @@ to_string(const Type& type)
return "DATA_TRANSFER"; return "DATA_TRANSFER";
case Type::MERGE: case Type::MERGE:
return "MERGE"; return "MERGE";
case Type::EDITED:
return "EDITED";
case Type::INVALID: case Type::INVALID:
case Type::COUNT__: case Type::COUNT__:
default: default:
@ -73,6 +75,8 @@ to_type(const QString& type)
return interaction::Type::DATA_TRANSFER; return interaction::Type::DATA_TRANSFER;
else if (type == "merge") else if (type == "merge")
return interaction::Type::MERGE; return interaction::Type::MERGE;
else if (type == "application/edited-message")
return interaction::Type::EDITED;
else else
return interaction::Type::INVALID; return interaction::Type::INVALID;
} }
@ -238,6 +242,19 @@ getContactInteractionString(const QString& authorUri, const ContactAction& actio
return {}; 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 authorUri
* @var body * @var body
@ -263,6 +280,7 @@ struct Info
MapStringString commit; MapStringString commit;
QVariantMap linkPreviewInfo = {}; QVariantMap linkPreviewInfo = {};
bool linkified = false; bool linkified = false;
QVector<Body> previousBodies;
Info() {} Info() {}
@ -286,7 +304,7 @@ struct Info
Info(const MapStringString& message, const QString& accountURI) Info(const MapStringString& message, const QString& accountURI)
{ {
type = to_type(message["type"]); type = to_type(message["type"]);
if (type == Type::TEXT) { if (type == Type::TEXT || type == Type::EDITED) {
body = message["body"]; body = message["body"];
} }
authorUri = accountURI == message["author"] ? "" : message["author"]; authorUri = accountURI == message["author"] ? "" : message["author"];

View file

@ -220,7 +220,7 @@ public:
const VectorString peersForConversation(const conversation::Info& conversation) const; const VectorString peersForConversation(const conversation::Info& conversation) const;
// insert swarm interactions. Return false if interaction already exists. // insert swarm interactions. Return false if interaction already exists.
bool insertSwarmInteraction(const QString& interactionId, bool insertSwarmInteraction(const QString& interactionId,
const interaction::Info& interaction, interaction::Info& interaction,
conversation::Info& conversation, conversation::Info& conversation,
bool insertAtBegin); bool insertAtBegin);
void invalidateModel(); void invalidateModel();
@ -1154,7 +1154,7 @@ ConversationModel::sendMessage(const QString& uid, const QString& body, const QS
try { try {
auto& conversation = pimpl_->getConversationForUid(uid, true).get(); auto& conversation = pimpl_->getConversationForUid(uid, true).get();
if (!conversation.isLegacy()) { if (!conversation.isLegacy()) {
ConfigurationManager::instance().sendMessage(owner.id, uid, body, parentId); ConfigurationManager::instance().sendMessage(owner.id, uid, body, parentId, 0);
return; return;
} }
@ -1181,7 +1181,8 @@ ConversationModel::sendMessage(const QString& uid, const QString& body, const QS
ConfigurationManager::instance().sendMessage(owner.id, ConfigurationManager::instance().sendMessage(owner.id,
conversationId, conversationId,
body, body,
parentId); parentId,
0);
return; return;
} }
auto& peers = pimpl_->peersForConversation(newConv); 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 void
ConversationModel::refreshFilter() ConversationModel::refreshFilter()
{ {
@ -2323,6 +2336,7 @@ ConversationModelPimpl::slotConversationLoaded(uint32_t requestId,
} }
auto msgId = message["id"]; auto msgId = message["id"];
auto msg = interaction::Info(message, linked.owner.profileInfo.uri); auto msg = interaction::Info(message, linked.owner.profileInfo.uri);
conversation.interactions->editMessage(msgId, msg);
auto downloadFile = false; auto downloadFile = false;
if (msg.type == interaction::Type::INITIAL) { if (msg.type == interaction::Type::INITIAL) {
allLoaded = true; allLoaded = true;
@ -2356,6 +2370,8 @@ ConversationModelPimpl::slotConversationLoaded(uint32_t requestId,
msg.body = interaction::getContactInteractionString(bestName, msg.body = interaction::getContactInteractionString(bestName,
interaction::to_action( interaction::to_action(
message["action"])); message["action"]));
} else if (msg.type == interaction::Type::EDITED) {
conversation.interactions->addEdition(msgId, msg, false);
} }
insertSwarmInteraction(msgId, msg, conversation, true); insertSwarmInteraction(msgId, msg, conversation, true);
if (downloadFile) { if (downloadFile) {
@ -2380,7 +2396,6 @@ ConversationModelPimpl::slotConversationLoaded(uint32_t requestId,
return; return;
} }
} }
// In this case, we only have loaded merge commits. Load more messages // In this case, we only have loaded merge commits. Load more messages
ConfigurationManager::instance().loadConversationMessages(linked.owner.id, ConfigurationManager::instance().loadConversationMessages(linked.owner.id,
conversationId, conversationId,
@ -2427,6 +2442,7 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId,
} }
auto msgId = message["id"]; auto msgId = message["id"];
auto msg = interaction::Info(message, linked.owner.profileInfo.uri); auto msg = interaction::Info(message, linked.owner.profileInfo.uri);
conversation.interactions->editMessage(msgId, msg);
api::datatransfer::Info info; api::datatransfer::Info info;
QString fileId; QString fileId;
@ -2464,6 +2480,8 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId,
} else if (msg.type == interaction::Type::TEXT } else if (msg.type == interaction::Type::TEXT
&& msg.authorUri != linked.owner.profileInfo.uri) { && msg.authorUri != linked.owner.profileInfo.uri) {
conversation.unreadMessages++; conversation.unreadMessages++;
} else if (msg.type == interaction::Type::EDITED) {
conversation.interactions->addEdition(msgId, msg, true);
} }
if (!insertSwarmInteraction(msgId, msg, conversation, false)) { if (!insertSwarmInteraction(msgId, msg, conversation, false)) {
// message already exists // message already exists
@ -2510,7 +2528,7 @@ ConversationModelPimpl::slotConversationProfileUpdated(const QString& accountId,
bool bool
ConversationModelPimpl::insertSwarmInteraction(const QString& interactionId, ConversationModelPimpl::insertSwarmInteraction(const QString& interactionId,
const interaction::Info& interaction, interaction::Info& interaction,
conversation::Info& conversation, conversation::Info& conversation,
bool insertAtBegin) bool insertAtBegin)
{ {
@ -2518,6 +2536,11 @@ ConversationModelPimpl::insertSwarmInteraction(const QString& interactionId,
auto itExists = conversation.interactions->find(interactionId); auto itExists = conversation.interactions->find(interactionId);
if (itExists != conversation.interactions->end()) { if (itExists != conversation.interactions->end()) {
// Erase interaction if exists, as it will be updated via a re-insertion // 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); itExists = conversation.interactions->erase(itExists);
if (itExists != conversation.interactions->end()) { if (itExists != conversation.interactions->end()) {
// next interaction doesn't have parent anymore. // next interaction doesn't have parent anymore.

View file

@ -176,6 +176,7 @@ MessageListModel::clear()
Q_EMIT beginResetModel(); Q_EMIT beginResetModel();
interactions_.clear(); interactions_.clear();
replyTo_.clear(); replyTo_.clear();
editedBodies_.clear();
Q_EMIT endResetModel(); Q_EMIT endResetModel();
} }
@ -410,6 +411,13 @@ MessageListModel::dataForItem(item_t item, int, int role) const
return QVariant(item.second.commit["uri"]); return QVariant(item.second.commit["uri"]);
case Role::ContactAction: case Role::ContactAction:
return QVariant(item.second.commit["action"]); 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: case Role::ReplyTo:
return QVariant(replyId); return QVariant(replyId);
case Role::ReplyToAuthor: case Role::ReplyToAuthor:
@ -541,12 +549,58 @@ MessageListModel::emitDataChanged(const QString& msgId, VectorInt roles)
Q_EMIT dataChanged(modelIndex, modelIndex, 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 QString
MessageListModel::lastMessageUid() const MessageListModel::lastMessageUid() const
{ {
for (auto it = interactions_.rbegin(); it != interactions_.rend(); ++it) { for (auto it = interactions_.rbegin(); it != interactions_.rend(); ++it) {
auto lastType = it->second.type; 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; return it->first;
} }
} }

View file

@ -45,6 +45,7 @@ struct Info;
X(ActionUri) \ X(ActionUri) \
X(LinkPreviewInfo) \ X(LinkPreviewInfo) \
X(Linkified) \ X(Linkified) \
X(PreviousBodies) \
X(ReplyTo) \ X(ReplyTo) \
X(ReplyToBody) \ X(ReplyToBody) \
X(ReplyToAuthor) \ X(ReplyToAuthor) \
@ -130,6 +131,8 @@ public:
Q_SIGNAL void timestampUpdate(); 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; QString lastMessageUid() const;
protected: protected:
@ -145,6 +148,7 @@ private:
QMap<QString, QString> lastDisplayedMessageUid_; QMap<QString, QString> lastDisplayedMessageUid_;
QMap<QString, QStringList> messageToReaders_; QMap<QString, QStringList> messageToReaders_;
QMap<QString, QStringList> replyTo_; QMap<QString, QStringList> replyTo_;
QMap<QString, QVector<interaction::Body>> editedBodies_;
void moveMessage(const QString& msgId, const QString& parentId); void moveMessage(const QString& msgId, const QString& parentId);
void insertMessage(int index, item_t& message); void insertMessage(int index, item_t& message);

View file

@ -1047,13 +1047,16 @@ public Q_SLOTS: // METHODS
void sendMessage(const QString& accountId, void sendMessage(const QString& accountId,
const QString& conversationId, const QString& conversationId,
const QString& message, const QString& message,
const QString& parent) const QString& parent,
int flags = 0)
{ {
DRing::sendMessage(accountId.toStdString(), DRing::sendMessage(accountId.toStdString(),
conversationId.toStdString(), conversationId.toStdString(),
message.toStdString(), message.toStdString(),
parent.toStdString()); parent.toStdString(),
flags);
} }
uint32_t loadConversationMessages(const QString& accountId, uint32_t loadConversationMessages(const QString& accountId,
const QString& conversationId, const QString& conversationId,
const QString& fromId, const QString& fromId,