Chat message retraction

This commit is contained in:
Christophe Deschamps 2025-12-10 10:22:39 +01:00
parent 13ec790648
commit d40045d5bb
13 changed files with 247 additions and 6 deletions

View file

@ -394,6 +394,7 @@ void ChatCore::setSelf(QSharedPointer<ChatCore> me) {
[this](std::shared_ptr<linphone::Friend> f) { updateInfo(f); });
mCoreModelConnection->makeConnectToModel(&CoreModel::friendRemoved,
[this](std::shared_ptr<linphone::Friend> f) { updateInfo(f, true); });
}
QDateTime ChatCore::getLastUpdatedTime() const {

View file

@ -115,11 +115,13 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &c
if (chatmessage) {
mChatMessageModel = Utils::makeQObject_ptr<ChatMessageModel>(chatmessage);
mChatMessageModel->setSelf(mChatMessageModel);
mText = ToolModel::getMessageFromContent(chatmessage->getContents());
mText = ToolModel::getMessageFromMessage(chatmessage);
mUtf8Text = mChatMessageModel->getUtf8Text();
mHasTextContent = mChatMessageModel->getHasTextContent();
mTimestamp = QDateTime::fromSecsSinceEpoch(chatmessage->getTime());
mIsOutgoing = chatmessage->isOutgoing();
mIsRetractable = chatmessage->isRetractable();
mIsRetracted = chatmessage->isRetracted();
mIsRemoteMessage = !chatmessage->isOutgoing();
mPeerAddress = Utils::coreStringToAppString(chatmessage->getPeerAddress()->asStringUriOnly());
mPeerName = ToolModel::getDisplayName(chatmessage->getPeerAddress());
@ -191,7 +193,7 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &c
if (mIsReply) {
auto replymessage = chatmessage->getReplyMessage();
if (replymessage) {
mReplyText = ToolModel::getMessageFromContent(replymessage->getContents());
mReplyText = ToolModel::getMessageFromMessage(replymessage);
if (mIsFromChatGroup) mRepliedToName = ToolModel::getDisplayName(replymessage->getFromAddress());
}
}
@ -207,6 +209,9 @@ void ChatMessageCore::setSelf(QSharedPointer<ChatMessageCore> me) {
mChatMessageModelConnection->makeConnectToCore(&ChatMessageCore::lDelete, [this] {
mChatMessageModelConnection->invokeToModel([this] { mChatMessageModel->deleteMessageFromChatRoom(true); });
});
mChatMessageModelConnection->makeConnectToCore(&ChatMessageCore::lRetract, [this] {
mChatMessageModelConnection->invokeToModel([this] { mChatMessageModel->retractMessageFromChatRoom(); });
});
mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::messageDeleted, [this](bool deletedByUser) {
mChatMessageModelConnection->invokeToCore([this, deletedByUser] {
//: Deleted
@ -350,6 +355,15 @@ void ChatMessageCore::setSelf(QSharedPointer<ChatMessageCore> me) {
int duration = now.secsTo(QDateTime::fromSecsSinceEpoch(expireTime));
mChatMessageModelConnection->invokeToCore([this, duration] { setEphemeralDuration(duration); });
});
mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::retracted,
[this](const std::shared_ptr<linphone::ChatMessage> &message) {
QString text = ToolModel::getMessageFromMessage(message);
mChatMessageModelConnection->invokeToCore([this, text] {
setText(text);
setRetracted();
});
});
}
QList<ImdnStatus> ChatMessageCore::computeDeliveryStatus(const std::shared_ptr<linphone::ChatMessage> &message) {
@ -472,6 +486,18 @@ void ChatMessageCore::setIsRead(bool read) {
}
}
void ChatMessageCore::setRetracted() {
if (!mIsRetracted) {
mIsRetracted = true;
emit isRetractedChanged();
emit messageStateChanged();
}
}
bool ChatMessageCore::isRetracted() const {
return mIsRetracted;
}
QString ChatMessageCore::getOwnReaction() const {
return mOwnReaction;
}
@ -650,4 +676,4 @@ std::shared_ptr<ChatMessageModel> ChatMessageCore::getModel() const {
ChatMessageContentGui *ChatMessageCore::getVoiceRecordingContent() const {
return new ChatMessageContentGui(mVoiceRecordingContent);
}
}

View file

@ -111,6 +111,9 @@ class ChatMessageCore : public QObject, public AbstractObject {
Q_PROPERTY(bool hasFileContent MEMBER mHasFileContent CONSTANT)
Q_PROPERTY(bool isVoiceRecording MEMBER mIsVoiceRecording CONSTANT)
Q_PROPERTY(bool isCalendarInvite MEMBER mIsCalendarInvite CONSTANT)
Q_PROPERTY(bool isOutgoing MEMBER mIsOutgoing CONSTANT)
Q_PROPERTY(bool isRetractable MEMBER mIsRetractable CONSTANT)
Q_PROPERTY(bool isRetracted READ isRetracted NOTIFY isRetractedChanged)
public:
static QSharedPointer<ChatMessageCore> create(const std::shared_ptr<linphone::ChatMessage> &chatmessage);
@ -143,6 +146,9 @@ public:
bool isRead() const;
void setIsRead(bool read);
bool isRetracted() const;
void setRetracted();
QString getOwnReaction() const;
void setOwnReaction(const QString &reaction);
QString getTotalReactionsLabel() const;
@ -176,9 +182,11 @@ signals:
void messageReactionChanged();
void singletonReactionMapChanged();
void ephemeralDurationChanged(int duration);
void isRetractedChanged();
void lDelete();
void deleted();
void lRetract();
void lMarkAsRead();
void readChanged();
void lSendReaction(const QString &reaction);
@ -214,6 +222,8 @@ private:
bool mIsVoiceRecording = false;
bool mIsEphemeral = false;
int mEphemeralDuration = 0;
bool mIsRetractable = false;
bool mIsRetracted = false;
bool mIsOutgoing = false;
QString mTotalReactionsLabel;

View file

@ -2486,6 +2486,42 @@ Only your correspondent can decrypt them.</translation>
<extracomment>%1 is writing</extracomment>
<translation>%1 is writing</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessagesListView.qml" line="470"/>
<source>conversation_dialog_delete_chat_message_title</source>
<extracomment>&quot;Delete this message?&quot;</extracomment>
<translation>Delete this message?</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessagesListView.qml" line="471"/>
<source>conversation_dialog_delete_locally_label</source>
<extracomment>&quot;For me&quot;</extracomment>
<translation>For me</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessagesListView.qml" line="472"/>
<source>conversation_dialog_delete_for_everyone_label</source>
<extracomment>&quot;For everyone&quot;</extracomment>
<translation>For everyone</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessagesListView.qml" line="472"/>
<source>dialog_cancel</source>
<extracomment>&quot;Cancel&quot;</extracomment>
<translation>Cancel</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessagesListView.qml" line="472"/>
<source>info_toast_deleted_title</source>
<extracomment>Deleted</extracomment>
<translation>Deleted</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessagesListView.qml" line="472"/>
<source>info_toast_deleted_message</source>
<extracomment>The message has been deleted</extracomment>
<translation>The message has been deleted</translation>
</message>
</context>
<context>
<name>ChatPage</name>
@ -5947,6 +5983,18 @@ To enable them in a commercial project, please contact us.</translation>
<source>conference_invitation_updated</source>
<translation>Meeting modification</translation>
</message>
<message>
<location filename="../../model/tool/ToolModel.cpp" line="530"/>
<source>conversation_message_content_deleted_label</source>
<extracomment>&quot;&lt;i&gt;This message has been deleted&lt;/i&gt;&quot;</extracomment>
<translation>&lt;i&gt;This message has been deleted&lt;/i&gt;</translation>
</message>
<message>
<location filename="../../model/tool/ToolModel.cpp" line="530"/>
<source>conversation_message_content_deleted_by_us_label</source>
<extracomment>&quot;&lt;i&gt;You have deleted this message&lt;/i&gt;&quot;</extracomment>
<translation>&lt;i&gt;You have deleted this message&lt;/i&gt;</translation>
</message>
</context>
<context>
<name>Utils</name>

View file

@ -2486,6 +2486,42 @@ en bout. Seul votre correspondant peut les déchiffrer.</translation>
<extracomment>%1 is writing</extracomment>
<translation>%1 est en train d&apos;écrire</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessagesListView.qml" line="470"/>
<source>conversation_dialog_delete_chat_message_title</source>
<extracomment>&quot;Delete this message?&quot;</extracomment>
<translation>Supprimer le message ?</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessagesListView.qml" line="471"/>
<source>conversation_dialog_delete_locally_label</source>
<extracomment>&quot;For me&quot;</extracomment>
<translation>Pour moi</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessagesListView.qml" line="472"/>
<source>conversation_dialog_delete_for_everyone_label</source>
<extracomment>&quot;For everyone&quot;</extracomment>
<translation>Pour tout le monde</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessagesListView.qml" line="472"/>
<source>dialog_cancel</source>
<extracomment>&quot;Cancel&quot;</extracomment>
<translation>Annuler</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessagesListView.qml" line="472"/>
<source>info_toast_deleted_title</source>
<extracomment>Deleted</extracomment>
<translation>Supprimé</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessagesListView.qml" line="472"/>
<source>info_toast_deleted_message</source>
<extracomment>The message has been deleted</extracomment>
<translation>Le message a é supprimé</translation>
</message>
</context>
<context>
<name>ChatPage</name>
@ -5947,6 +5983,18 @@ Pour les activer dans un projet commercial, merci de nous contacter.</translatio
<source>conference_invitation_updated</source>
<translation>Modification d&apos;une réunion</translation>
</message>
<message>
<location filename="../../model/tool/ToolModel.cpp" line="530"/>
<source>conversation_message_content_deleted_label</source>
<extracomment>&quot;&lt;i&gt;This message has been deleted&lt;/i&gt;&quot;</extracomment>
<translation>&lt;i&gt;Le message a é supprimé&lt;/i&gt;</translation>
</message>
<message>
<location filename="../../model/tool/ToolModel.cpp" line="530"/>
<source>conversation_message_content_deleted_by_us_label</source>
<extracomment>&quot;&lt;i&gt;You have deleted this message&lt;/i&gt;&quot;</extracomment>
<translation>&lt;i&gt;Vous avez supprimé le message&lt;/i&gt;</translation>
</message>
</context>
<context>
<name>Utils</name>

View file

@ -51,7 +51,7 @@ ChatMessageModel::~ChatMessageModel() {
}
QString ChatMessageModel::getText() const {
return ToolModel::getMessageFromContent(mMonitor->getContents());
return ToolModel::getMessageFromMessage(mMonitor);
}
QString ChatMessageModel::getUtf8Text() const {
@ -103,6 +103,13 @@ void ChatMessageModel::deleteMessageFromChatRoom(bool deletedByUser) {
}
}
void ChatMessageModel::retractMessageFromChatRoom() {
auto chatRoom = mMonitor->getChatRoom();
if (chatRoom) {
chatRoom->retractMessage(mMonitor);
}
}
void ChatMessageModel::sendReaction(const QString &reaction) {
auto linReaction = mMonitor->createReaction(Utils::appStringToCoreString(reaction));
linReaction->send();
@ -188,3 +195,7 @@ void ChatMessageModel::onEphemeralMessageTimerStarted(const std::shared_ptr<linp
void ChatMessageModel::onEphemeralMessageDeleted(const std::shared_ptr<linphone::ChatMessage> &message) {
emit ephemeralMessageDeleted(message);
}
void ChatMessageModel::onRetracted(const std::shared_ptr<linphone::ChatMessage> &message) {
emit retracted(message);
}

View file

@ -52,6 +52,7 @@ public:
void markAsRead();
void deleteMessageFromChatRoom(bool deletedByUser);
void retractMessageFromChatRoom();
void sendReaction(const QString &reaction);
@ -95,6 +96,7 @@ signals:
void ephemeralMessageTimerStarted(const std::shared_ptr<linphone::ChatMessage> &message);
void ephemeralMessageDeleted(const std::shared_ptr<linphone::ChatMessage> &message);
void ephemeralMessageTimeUpdated(const std::shared_ptr<linphone::ChatMessage> &message, int expireTime);
void retracted(const std::shared_ptr<linphone::ChatMessage> &message);
private:
linphone::ChatMessage::State mMessageState;
@ -130,6 +132,7 @@ private:
const std::shared_ptr<const linphone::ParticipantImdnState> &state) override;
void onEphemeralMessageTimerStarted(const std::shared_ptr<linphone::ChatMessage> &message) override;
void onEphemeralMessageDeleted(const std::shared_ptr<linphone::ChatMessage> &message) override;
void onRetracted(const std::shared_ptr<linphone::ChatMessage> &message) override;
};
#endif
#endif

View file

@ -537,6 +537,15 @@ QString ToolModel::getMessageFromContent(std::list<std::shared_ptr<linphone::Con
return res;
}
QString ToolModel::getMessageFromMessage(std::shared_ptr<linphone::ChatMessage> message) {
if (message->isRetracted()) {
return message->isOutgoing() ? tr("conversation_message_content_deleted_by_us_label")
: tr("conversation_message_content_deleted_label");
} else {
return getMessageFromContent(message->getContents());
}
}
// Load downloaded codecs like OpenH264 (needs to be after core is created and has loaded its plugins, as
// reloadMsPlugins modifies plugin path for the factory)
void ToolModel::loadDownloadedCodecs() {

View file

@ -77,6 +77,7 @@ public:
const std::shared_ptr<linphone::Friend> &f);
static QString getMessageFromContent(std::list<std::shared_ptr<linphone::Content>> contents);
static QString getMessageFromMessage(std::shared_ptr<linphone::ChatMessage> message);
static void loadDownloadedCodecs();
static void updateCodecs();

View file

@ -247,11 +247,21 @@ Control.Control {
}
contentItem: ColumnLayout {
spacing: Utils.getSizeWithScreenRatio(5)
Text {
id: retractedId
visible: mainItem.chatMessage.core.isRetracted
font: Typography.p1i
color: DefaultStyle.info_800_main
Layout.fillWidth: true
Layout.fillHeight: true
text: mainItem.chatMessage.core.text
}
ChatMessageContent {
id: chatBubbleContent
Layout.fillWidth: true
Layout.fillHeight: true
chatGui: mainItem.chat
visible: !mainItem.chatMessage.core.isRetracted
searchedTextPart: mainItem.searchedTextPart
chatMessageGui: mainItem.chatMessage
maxWidth: mainItem.maxWidth
@ -415,6 +425,7 @@ Control.Control {
}
IconLabelButton {
inverseLayout: true
visible: !mainItem.chatMessage.core.isRetracted
//: Reply
text: qsTr("chat_message_reply")
icon.source: AppIcons.reply
@ -427,6 +438,7 @@ Control.Control {
}
IconLabelButton {
inverseLayout: true
visible: !mainItem.chatMessage.core.isRetracted
text: chatBubbleContent.selectedText != ""
//: "Copy selection"
? qsTr("chat_message_copy_selection")
@ -447,6 +459,7 @@ Control.Control {
}
IconLabelButton {
inverseLayout: true
visible: !mainItem.chatMessage.core.isRetracted
//: Forward
text: qsTr("chat_message_forward")
icon.source: AppIcons.forward

View file

@ -225,6 +225,51 @@ ListView {
indicatorColor: DefaultStyle.main1_500_main
}
Dialog {
id: messageDeletionDialog
width: Utils.getSizeWithScreenRatio(637)
//: "Supprimer le message ?"
title: qsTr("conversation_dialog_delete_chat_message_title")
property var chatMessage
buttons: RowLayout {
id: buttonsLayout
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
spacing: Utils.getSizeWithScreenRatio(20)
MediumButton {
id: firstButtonId
text: qsTr("conversation_dialog_delete_locally_label")
style: ButtonStyle.main
onClicked: {
messageDeletionDialog.chatMessage.core.lDelete()
messageDeletionDialog.close()
}
KeyNavigation.left: thirdButtonId
KeyNavigation.right: secondButtonId
}
MediumButton {
id: secondButtonId
text: qsTr("conversation_dialog_delete_for_everyone_label")
style: ButtonStyle.main
onClicked: {
messageDeletionDialog.chatMessage.core.lRetract()
messageDeletionDialog.close()
}
KeyNavigation.left: firstButtonId
KeyNavigation.right: thirdButtonId
}
MediumButton {
id: thirdButtonId
text: qsTr("dialog_cancel")
style: ButtonStyle.secondary
onClicked: {
messageDeletionDialog.close()
}
KeyNavigation.left: secondButtonId
KeyNavigation.right: firstButtonId
}
}
}
delegate: DelegateChooser {
role: "eventType"
DelegateChoice {
@ -262,7 +307,14 @@ ListView {
? parent.right
: undefined
onMessageDeletionRequested: chatMessage.core.lDelete()
onMessageDeletionRequested: {
if (chatMessage.core.isOutgoing && chatMessage.core.isRetractable && !chatMessage.core.isRetracted) {
messageDeletionDialog.chatMessage = chatMessage
messageDeletionDialog.open()
} else {
chatMessage.core.lDelete()
}
}
onShowReactionsForMessageRequested: mainItem.showReactionsForMessageRequested(chatMessage)
onShowImdnStatusForMessageRequested: mainItem.showImdnStatusForMessageRequested(chatMessage)
onReplyToMessageRequested: mainItem.replyToMessageRequested(chatMessage)
@ -283,6 +335,16 @@ ListView {
}
}
}
Connections {
target: chatMessage.core
onIsRetractedChanged: {
if (chatMessage.core.isRetracted && chatMessage.core.isOutgoing) {
UtilsCpp.showInformationPopup(qsTr("info_toast_deleted_title"),
//: The message has been deleted
qsTr("info_toast_deleted_message"), true)
}
}
}
}
}

View file

@ -47,6 +47,7 @@ QtObject {
property var success_700: "#377d71"
property var success_900: "#1E4C53"
property var info_500_main: "#4AA8FF"
property var info_800_main: "#02528D"
property var vue_meter_light_green: "#6FF88D"
property var vue_meter_dark_green: "#00D916"

View file

@ -81,6 +81,14 @@ QtObject {
weight: Font.Normal
})
// Text/P1i - Paragraph text Italic
property font p1i: Qt.font( {
family: DefaultStyle.defaultFont,
pixelSize: Utils.getSizeWithScreenRatio(14),
weight: Font.Normal,
italic: true
})
// Text/P1s - Paragraph text
property font p1s: Qt.font( {
family: DefaultStyle.defaultFont,