mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-09-03 13:43:34 +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:
parent
9457c7ccbb
commit
47cd60fbe4
21 changed files with 448 additions and 18 deletions
97
src/app/commoncomponents/EditedPopup.qml
Normal file
97
src/app/commoncomponents/EditedPopup.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
99
src/app/mainview/components/EditContainer.qml
Normal file
99
src/app/mainview/components/EditContainer.qml
Normal 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 = ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"];
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue