From c69cdef765643a68c8b845c054a8fd5a463a0909 Mon Sep 17 00:00:00 2001
From: Sylvain Berfini <sylvain.berfini@belledonne-communications.com>
Date: Thu, 31 Aug 2023 17:27:25 +0200
Subject: [PATCH] Handling empty reaction as removal

---
 coreapi/linphonecore.c                        |  8 ++++
 coreapi/private_functions.h                   |  5 +++
 coreapi/vtables.c                             |  8 ++++
 include/linphone/api/c-callbacks.h            |  7 +++
 include/linphone/api/c-chat-message-cbs.h     | 16 +++++++
 include/linphone/callbacks.h                  | 13 ++++++
 include/linphone/core.h                       | 15 +++++++
 src/c-wrapper/api/c-chat-message-cbs.cpp      | 11 +++++
 src/c-wrapper/api/c-chat-message.cpp          |  4 ++
 .../chat-message/chat-message-reaction.cpp    | 23 ++++++++--
 src/chat/chat-message/chat-message.cpp        | 23 +++++++++-
 src/core/core.h                               |  2 +
 src/db/main-db.cpp                            | 21 +++++++++
 src/db/main-db.h                              |  2 +
 tester/group_chat_secure_tester.c             | 43 +++++++++++++++++++
 tester/liblinphone_tester.h                   |  5 +++
 tester/message_tester.c                       | 30 ++++++-------
 tester/tester.c                               | 12 ++++++
 18 files changed, 226 insertions(+), 22 deletions(-)

diff --git a/coreapi/linphonecore.c b/coreapi/linphonecore.c
index 6f45ee3fcd..d3992af132 100644
--- a/coreapi/linphonecore.c
+++ b/coreapi/linphonecore.c
@@ -376,6 +376,14 @@ void linphone_core_cbs_set_new_message_reaction(LinphoneCoreCbs *cbs, LinphoneCo
 	cbs->vtable->new_message_reaction = cb;
 }
 
+LinphoneCoreCbsReactionRemovedCb linphone_core_cbs_get_reaction_removed(LinphoneCoreCbs *cbs) {
+	return cbs->vtable->reaction_removed;
+}
+
+void linphone_core_cbs_set_reaction_removed(LinphoneCoreCbs *cbs, LinphoneCoreCbsReactionRemovedCb cb) {
+	cbs->vtable->reaction_removed = cb;
+}
+
 LinphoneCoreCbsMessagesReceivedCb linphone_core_cbs_get_messages_received(LinphoneCoreCbs *cbs) {
 	return cbs->vtable->messages_received;
 }
diff --git a/coreapi/private_functions.h b/coreapi/private_functions.h
index 042072d362..4020cacddc 100644
--- a/coreapi/private_functions.h
+++ b/coreapi/private_functions.h
@@ -533,6 +533,7 @@ void _linphone_chat_room_clear_callbacks(LinphoneChatRoom *cr);
 void _linphone_chat_message_notify_msg_state_changed(LinphoneChatMessage *msg, LinphoneChatMessageState state);
 void _linphone_chat_message_notify_new_message_reaction(LinphoneChatMessage *msg,
                                                         const LinphoneChatMessageReaction *reaction);
+void _linphone_chat_message_notify_reaction_removed(LinphoneChatMessage *msg, const LinphoneAddress *address);
 void _linphone_chat_message_notify_participant_imdn_state_changed(LinphoneChatMessage *msg,
                                                                   const LinphoneParticipantImdnState *state);
 void _linphone_chat_message_notify_file_transfer_recv(LinphoneChatMessage *msg,
@@ -853,6 +854,10 @@ void linphone_core_notify_new_message_reaction(LinphoneCore *lc,
                                                LinphoneChatRoom *room,
                                                LinphoneChatMessage *message,
                                                const LinphoneChatMessageReaction *reaction);
+void linphone_core_notify_message_reaction_removed(LinphoneCore *lc,
+                                                   LinphoneChatRoom *room,
+                                                   LinphoneChatMessage *message,
+                                                   const LinphoneAddress *address);
 void linphone_core_notify_messages_received(LinphoneCore *lc, LinphoneChatRoom *room, const bctbx_list_t *messages);
 void linphone_core_notify_message_sent(LinphoneCore *lc, LinphoneChatRoom *room, LinphoneChatMessage *message);
 void linphone_core_notify_message_received_unable_decrypt(LinphoneCore *lc,
diff --git a/coreapi/vtables.c b/coreapi/vtables.c
index e2423499c8..41fa7b8dce 100644
--- a/coreapi/vtables.c
+++ b/coreapi/vtables.c
@@ -252,6 +252,14 @@ void linphone_core_notify_new_message_reaction(LinphoneCore *lc,
 	cleanup_dead_vtable_refs(lc);
 }
 
+void linphone_core_notify_message_reaction_removed(LinphoneCore *lc,
+                                                   LinphoneChatRoom *room,
+                                                   LinphoneChatMessage *message,
+                                                   const LinphoneAddress *address) {
+	NOTIFY_IF_EXIST(reaction_removed, lc, room, message, address);
+	cleanup_dead_vtable_refs(lc);
+}
+
 void linphone_core_notify_messages_received(LinphoneCore *lc, LinphoneChatRoom *room, const bctbx_list_t *messages) {
 	NOTIFY_IF_EXIST(messages_received, lc, room, messages);
 	cleanup_dead_vtable_refs(lc);
diff --git a/include/linphone/api/c-callbacks.h b/include/linphone/api/c-callbacks.h
index 33d6db150f..f6b51fe812 100644
--- a/include/linphone/api/c-callbacks.h
+++ b/include/linphone/api/c-callbacks.h
@@ -230,6 +230,13 @@ typedef void (*LinphoneChatMessageCbsMsgStateChangedCb)(LinphoneChatMessage *mes
 typedef void (*LinphoneChatMessageCbsNewMessageReactionCb)(LinphoneChatMessage *message,
                                                            const LinphoneChatMessageReaction *reaction);
 
+/**
+ * Callback used to notify a reaction has been removed from a given message
+ * @param message #LinphoneChatMessage object @notnil
+ * @param address the #LinphoneAddress of the person that removed it's reaction @notnil
+ */
+typedef void (*LinphoneChatMessageCbsReactionRemovedCb)(LinphoneChatMessage *message, const LinphoneAddress *address);
+
 /**
  * Call back used to notify participant IMDN state
  * @param message #LinphoneChatMessage object @notnil
diff --git a/include/linphone/api/c-chat-message-cbs.h b/include/linphone/api/c-chat-message-cbs.h
index c00a746770..476de5866a 100644
--- a/include/linphone/api/c-chat-message-cbs.h
+++ b/include/linphone/api/c-chat-message-cbs.h
@@ -96,6 +96,22 @@ linphone_chat_message_cbs_get_new_message_reaction(const LinphoneChatMessageCbs
 LINPHONE_PUBLIC void linphone_chat_message_cbs_set_new_message_reaction(LinphoneChatMessageCbs *cbs,
                                                                         LinphoneChatMessageCbsNewMessageReactionCb cb);
 
+/**
+ * Get the removed reaction callback.
+ * @param cbs #LinphoneChatMessageCbs object. @notnil
+ * @return The current new reaction callback.
+ */
+LINPHONE_PUBLIC LinphoneChatMessageCbsReactionRemovedCb
+linphone_chat_message_cbs_get_reaction_removed(const LinphoneChatMessageCbs *cbs);
+
+/**
+ * Set the removed reaction callback.
+ * @param cbs LinphoneChatMessageCbs object. @notnil
+ * @param cb The new reaction callback to be used.
+ */
+LINPHONE_PUBLIC void linphone_chat_message_cbs_set_reaction_removed(LinphoneChatMessageCbs *cbs,
+                                                                    LinphoneChatMessageCbsReactionRemovedCb cb);
+
 /**
  * Get the file transfer receive callback.
  * @param cbs LinphoneChatMessageCbs object. @notnil
diff --git a/include/linphone/callbacks.h b/include/linphone/callbacks.h
index a3a399744e..e9bddaa609 100644
--- a/include/linphone/callbacks.h
+++ b/include/linphone/callbacks.h
@@ -253,6 +253,19 @@ typedef void (*LinphoneCoreCbsNewMessageReactionCb)(LinphoneCore *core,
                                                     LinphoneChatMessage *message,
                                                     const LinphoneChatMessageReaction *reaction);
 
+/**
+ * Chat message removed reaction callback prototype.
+ * @param core #LinphoneCore object @notnil
+ * @param chat_room #LinphoneChatRoom involved in this conversation. Can be created by the framework in case the
+ * From-URI is not present in any chat room. @notnil
+ * @param message the #LinphoneChatMessage to which a reaction has been removed from @notnil
+ * @param address the #LinphoneAddress of the person that removed it's reaction @notnil
+ */
+typedef void (*LinphoneCoreCbsReactionRemovedCb)(LinphoneCore *core,
+                                                 LinphoneChatRoom *chat_room,
+                                                 LinphoneChatMessage *message,
+                                                 const LinphoneAddress *address);
+
 /**
  * Chat messages callback prototype.
  * Only called when aggregation is enabled (aka [sip] chat_messages_aggregation == 1 or using
diff --git a/include/linphone/core.h b/include/linphone/core.h
index 1e7bafcd68..f659edbc44 100644
--- a/include/linphone/core.h
+++ b/include/linphone/core.h
@@ -302,6 +302,7 @@ typedef struct _LinphoneCoreVTable {
 	LinphoneCoreCbsOnAlertCb on_alert;
 	LinphoneCoreCbsPreviewDisplayErrorOccurredCb preview_display_error_occurred;
 	LinphoneCoreCbsNewMessageReactionCb new_message_reaction;
+	LinphoneCoreCbsReactionRemovedCb reaction_removed;
 	void *user_data; /**<User data associated with the above callbacks */
 } LinphoneCoreVTable;
 
@@ -600,6 +601,20 @@ LINPHONE_PUBLIC void linphone_core_cbs_set_new_message_reaction(LinphoneCoreCbs
  */
 LINPHONE_PUBLIC LinphoneCoreCbsNewMessageReactionCb linphone_core_cbs_get_new_message_reaction(LinphoneCoreCbs *cbs);
 
+/**
+ * Set the #LinphoneCoreCbsReactionRemovedCb callback.
+ * @param cbs A #LinphoneCoreCbs. @notnil
+ * @param cb The callback.
+ */
+LINPHONE_PUBLIC void linphone_core_cbs_set_reaction_removed(LinphoneCoreCbs *cbs, LinphoneCoreCbsReactionRemovedCb cb);
+
+/**
+ * Get the #LinphoneCoreCbsReactionRemovedCb callback.
+ * @param cbs A #LinphoneCoreCbs. @notnil
+ * @return The callback.
+ */
+LINPHONE_PUBLIC LinphoneCoreCbsReactionRemovedCb linphone_core_cbs_get_reaction_removed(LinphoneCoreCbs *cbs);
+
 /**
  * Set the #LinphoneCoreCbsMessagesReceivedCb callback.
  * @param cbs A #LinphoneCoreCbs. @notnil
diff --git a/src/c-wrapper/api/c-chat-message-cbs.cpp b/src/c-wrapper/api/c-chat-message-cbs.cpp
index 420a4e7a4b..a0fc399410 100644
--- a/src/c-wrapper/api/c-chat-message-cbs.cpp
+++ b/src/c-wrapper/api/c-chat-message-cbs.cpp
@@ -36,6 +36,7 @@ struct _LinphoneChatMessageCbs {
 	LinphoneChatMessageCbsEphemeralMessageDeletedCb ephemeral_message_deleted;
 	LinphoneChatMessageCbsFileTransferSendChunkCb file_transfer_send_chunk;
 	LinphoneChatMessageCbsNewMessageReactionCb new_message_reaction;
+	LinphoneChatMessageCbsReactionRemovedCb reaction_removed;
 };
 
 BELLE_SIP_DECLARE_VPTR_NO_EXPORT(LinphoneChatMessageCbs);
@@ -160,4 +161,14 @@ linphone_chat_message_cbs_get_new_message_reaction(const LinphoneChatMessageCbs
 void linphone_chat_message_cbs_set_new_message_reaction(LinphoneChatMessageCbs *cbs,
                                                         LinphoneChatMessageCbsNewMessageReactionCb cb) {
 	cbs->new_message_reaction = cb;
+}
+
+LinphoneChatMessageCbsReactionRemovedCb
+linphone_chat_message_cbs_get_reaction_removed(const LinphoneChatMessageCbs *cbs) {
+	return cbs->reaction_removed;
+}
+
+void linphone_chat_message_cbs_set_reaction_removed(LinphoneChatMessageCbs *cbs,
+                                                    LinphoneChatMessageCbsReactionRemovedCb cb) {
+	cbs->reaction_removed = cb;
 }
\ No newline at end of file
diff --git a/src/c-wrapper/api/c-chat-message.cpp b/src/c-wrapper/api/c-chat-message.cpp
index b248469365..6e2700d4a2 100644
--- a/src/c-wrapper/api/c-chat-message.cpp
+++ b/src/c-wrapper/api/c-chat-message.cpp
@@ -152,6 +152,10 @@ void _linphone_chat_message_notify_new_message_reaction(LinphoneChatMessage *msg
 	NOTIFY_IF_EXIST(NewMessageReaction, new_message_reaction, msg, reaction);
 }
 
+void _linphone_chat_message_notify_reaction_removed(LinphoneChatMessage *msg, const LinphoneAddress *address) {
+	NOTIFY_IF_EXIST(ReactionRemoved, reaction_removed, msg, address);
+}
+
 void _linphone_chat_message_notify_participant_imdn_state_changed(LinphoneChatMessage *msg,
                                                                   const LinphoneParticipantImdnState *state) {
 	NOTIFY_IF_EXIST(ParticipantImdnStateChanged, participant_imdn_state_changed, msg, state);
diff --git a/src/chat/chat-message/chat-message-reaction.cpp b/src/chat/chat-message/chat-message-reaction.cpp
index 9d97d71e4f..9322fb72bd 100644
--- a/src/chat/chat-message/chat-message-reaction.cpp
+++ b/src/chat/chat-message/chat-message-reaction.cpp
@@ -21,6 +21,8 @@
 #include "chat-message-reaction.h"
 #include "chat-message-p.h"
 #include "chat/chat-room/abstract-chat-room.h"
+#include "core/core-p.h"
+#include "db/main-db-p.h"
 #include "linphone/utils/utils.h"
 #include "logger/logger.h"
 
@@ -71,11 +73,24 @@ void ChatMessageReaction::onChatMessageStateChanged(const shared_ptr<ChatMessage
 		}
 
 		LinphoneChatMessage *msg = L_GET_C_BACK_PTR(originalMessage);
-		LinphoneChatMessageReaction *reaction = getSharedFromThis()->toC();
-		_linphone_chat_message_notify_new_message_reaction(msg, reaction);
-
+		const string &messageId = originalMessage->getImdnMessageId();
 		LinphoneChatRoom *cr = L_GET_C_BACK_PTR(message->getChatRoom());
-		linphone_core_notify_new_message_reaction(message->getCore()->getCCore(), cr, msg, reaction);
+
+		if (reaction.empty()) {
+			lInfo() << "[Chat Message Reaction] Sending empty reaction to chat message ID [" << messageId
+			        << "] to remove any previously existing reaction";
+			const LinphoneAddress *address = fromAddress->toC();
+			unique_ptr<MainDb> &mainDb = message->getChatRoom()->getCore()->getPrivate()->mainDb;
+			mainDb->removeConferenceChatMessageReactionEvent(messageId, fromAddress);
+
+			_linphone_chat_message_notify_reaction_removed(msg, address);
+			linphone_core_notify_message_reaction_removed(message->getCore()->getCCore(), cr, msg, address);
+		} else {
+			LinphoneChatMessageReaction *reaction = getSharedFromThis()->toC();
+			_linphone_chat_message_notify_new_message_reaction(msg, reaction);
+
+			linphone_core_notify_new_message_reaction(message->getCore()->getCCore(), cr, msg, reaction);
+		}
 
 		message->removeListener(getSharedFromThis());
 	} else if (state == ChatMessage::State::NotDelivered) {
diff --git a/src/chat/chat-message/chat-message.cpp b/src/chat/chat-message/chat-message.cpp
index e74ba2724d..0f6450d93a 100644
--- a/src/chat/chat-message/chat-message.cpp
+++ b/src/chat/chat-message/chat-message.cpp
@@ -886,7 +886,6 @@ LinphoneReason ChatMessagePrivate::receive() {
 
 	if (q->isReaction()) {
 		markAsRead();
-		storeInDb();
 
 		auto messageId = q->getReactionToMessageId();
 		auto originalMessage = q->getReactionToMessage();
@@ -896,12 +895,32 @@ LinphoneReason ChatMessagePrivate::receive() {
 			return reason;
 		}
 
+		shared_ptr<AbstractChatRoom> chatRoom = q->getChatRoom();
+		LinphoneChatRoom *cr = L_GET_C_BACK_PTR(chatRoom);
+
 		LinphoneChatMessage *msg = L_GET_C_BACK_PTR(originalMessage);
+
+		const string &reactionBody = getUtf8Text();
+		if (reactionBody.empty()) {
+			auto fromAddress = q->getFromAddress();
+			lInfo() << "Reaction body for message ID [" << messageId << "] is empty, removing existing reaction from ["
+			        << fromAddress->asStringUriOnly() << "] if any";
+			unique_ptr<MainDb> &mainDb = chatRoom->getCore()->getPrivate()->mainDb;
+			mainDb->removeConferenceChatMessageReactionEvent(messageId, fromAddress);
+
+			const LinphoneAddress *address = q->getFromAddress()->toC();
+			_linphone_chat_message_notify_reaction_removed(msg, address);
+			linphone_core_notify_message_reaction_removed(q->getCore()->getCCore(), cr, msg, address);
+
+			return reason;
+		}
+
+		storeInDb();
+
 		LinphoneChatMessageReaction *reaction =
 		    ChatMessageReaction::createCObject(messageId, getUtf8Text(), q->getFromAddress());
 		_linphone_chat_message_notify_new_message_reaction(msg, reaction);
 
-		LinphoneChatRoom *cr = L_GET_C_BACK_PTR(q->getChatRoom());
 		linphone_core_notify_new_message_reaction(q->getCore()->getCCore(), cr, msg, reaction);
 
 		linphone_chat_message_reaction_unref(reaction);
diff --git a/src/core/core.h b/src/core/core.h
index 01f560ed76..c6e707a54a 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -61,6 +61,7 @@ class ConferenceParams;
 class CorePrivate;
 class EncryptionEngine;
 class ChatMessage;
+class ChatMessageReaction;
 class ChatRoom;
 class Ldap;
 class PushNotificationMessage;
@@ -83,6 +84,7 @@ class LINPHONE_PUBLIC Core : public Object {
 	friend class CallSession;
 	friend class ChatMessage;
 	friend class ChatMessagePrivate;
+	friend class ChatMessageReaction;
 	friend class ChatRoom;
 	friend class ChatRoomPrivate;
 	friend class Conference;
diff --git a/src/db/main-db.cpp b/src/db/main-db.cpp
index 895dbc4955..6b1dbb471d 100644
--- a/src/db/main-db.cpp
+++ b/src/db/main-db.cpp
@@ -4575,6 +4575,10 @@ list<shared_ptr<ChatMessageReaction>> MainDb::getChatMessageReactions(const shar
 		soci::rowset<soci::row> rows = (session->prepare << query, soci::use(messageId));
 		for (const auto &row : rows) {
 			string body = row.get<string>(0);
+			if (body.empty()) {
+				lDebug() << "Found empty reaction for message [" << chatMessage << "], skipping";
+				continue;
+			}
 			shared_ptr<Address> fromAddress = make_shared<Address>(row.get<string>(1));
 			shared_ptr<ChatMessageReaction> reaction = ChatMessageReaction::create(messageId, body, fromAddress);
 			reactions.push_back(reaction);
@@ -4584,6 +4588,23 @@ list<shared_ptr<ChatMessageReaction>> MainDb::getChatMessageReactions(const shar
 	return reactions;
 }
 
+void MainDb::removeConferenceChatMessageReactionEvent(const string &messageId,
+                                                      const std::shared_ptr<const Address> &from) {
+#ifdef HAVE_DB_STORAGE
+	L_DB_TRANSACTION {
+		L_D();
+
+		const long long &fromSipAddressId = d->selectSipAddressId(from->toStringUriOnlyOrdered());
+
+		*d->dbSession.getBackendSession()
+		    << "DELETE FROM conference_chat_message_reaction_event WHERE"
+		       " from_sip_address_id = :from_sip_address_id AND reaction_to_message_id = :messageId",
+		    soci::use(fromSipAddressId), soci::use(messageId);
+		tr.commit();
+	};
+#endif
+}
+
 // -----------------------------------------------------------------------------
 
 void MainDb::disableDeliveryNotificationRequired(const std::shared_ptr<const EventLog> &eventLog) {
diff --git a/src/db/main-db.h b/src/db/main-db.h
index f133cf0df4..3e8f233e50 100644
--- a/src/db/main-db.h
+++ b/src/db/main-db.h
@@ -159,6 +159,8 @@ public:
 	void loadChatMessageContents(const std::shared_ptr<ChatMessage> &chatMessage);
 	std::list<std::shared_ptr<ChatMessageReaction>>
 	getChatMessageReactions(const std::shared_ptr<ChatMessage> &chatMessage);
+	void removeConferenceChatMessageReactionEvent(const std::string &messageId,
+	                                              const std::shared_ptr<const Address> &from);
 
 	void disableDeliveryNotificationRequired(const std::shared_ptr<const EventLog> &eventLog);
 	void disableDisplayNotificationRequired(const std::shared_ptr<const EventLog> &eventLog);
diff --git a/tester/group_chat_secure_tester.c b/tester/group_chat_secure_tester.c
index 6ae3b82b03..862e33d9b9 100644
--- a/tester/group_chat_secure_tester.c
+++ b/tester/group_chat_secure_tester.c
@@ -1621,6 +1621,49 @@ static void group_chat_lime_x3dh_chat_room_reaction_message_base(const int curve
 			check_reactions(marieSentMessage, 3, expected_reactions, expected_reactions_from);
 			check_reactions(paulineReceivedMessage, 3, expected_reactions, expected_reactions_from);
 		}
+	} else {
+		// Marie is sending an empty reaction to remove it's previous reaction
+		LinphoneChatMessageReaction *marieEmptyReaction = linphone_chat_message_create_reaction(marieSentMessage, "");
+		linphone_chat_message_reaction_send(marieEmptyReaction);
+
+		BC_ASSERT_TRUE(wait_for_list(coresList, &marie->stat.number_of_LinphoneReactionRemoved,
+		                             initialMarieStats.number_of_LinphoneReactionRemoved + 1, 5000));
+		linphone_chat_message_reaction_unref(marieEmptyReaction);
+
+		expected_reactions = bctbx_list_remove(expected_reactions, bctbx_list_last_elem(expected_reactions));
+		expected_reactions_from =
+		    bctbx_list_remove(expected_reactions_from, bctbx_list_last_elem(expected_reactions_from));
+		check_reactions(marieSentMessage, 2, expected_reactions, expected_reactions_from);
+
+		BC_ASSERT_TRUE(wait_for_list(coresList, &laure->stat.number_of_LinphoneReactionRemoved,
+		                             initialLaureStats.number_of_LinphoneReactionRemoved + 1, 5000));
+		BC_ASSERT_TRUE(wait_for_list(coresList, &pauline->stat.number_of_LinphoneReactionRemoved,
+		                             initialPaulineStats.number_of_LinphoneReactionRemoved + 1, 5000));
+
+		check_reactions(laureReceivedMessage, 2, expected_reactions, expected_reactions_from);
+		check_reactions(paulineReceivedMessage, 2, expected_reactions, expected_reactions_from);
+
+		// Laure is sending an empty reaction to remove it's previous reaction
+		LinphoneChatMessageReaction *laureEmptyReaction =
+		    linphone_chat_message_create_reaction(laureReceivedMessage, "");
+		linphone_chat_message_reaction_send(laureEmptyReaction);
+
+		BC_ASSERT_TRUE(wait_for_list(coresList, &laure->stat.number_of_LinphoneReactionRemoved,
+		                             initialMarieStats.number_of_LinphoneReactionRemoved + 2, 5000));
+		linphone_chat_message_reaction_unref(laureEmptyReaction);
+
+		expected_reactions = bctbx_list_remove(expected_reactions, bctbx_list_last_elem(expected_reactions));
+		expected_reactions_from =
+		    bctbx_list_remove(expected_reactions_from, bctbx_list_last_elem(expected_reactions_from));
+		check_reactions(laureReceivedMessage, 1, expected_reactions, expected_reactions_from);
+
+		BC_ASSERT_TRUE(wait_for_list(coresList, &marie->stat.number_of_LinphoneReactionRemoved,
+		                             initialLaureStats.number_of_LinphoneReactionRemoved + 2, 5000));
+		BC_ASSERT_TRUE(wait_for_list(coresList, &pauline->stat.number_of_LinphoneReactionRemoved,
+		                             initialPaulineStats.number_of_LinphoneReactionRemoved + 2, 5000));
+
+		check_reactions(marieSentMessage, 1, expected_reactions, expected_reactions_from);
+		check_reactions(paulineReceivedMessage, 1, expected_reactions, expected_reactions_from);
 	}
 
 end:
diff --git a/tester/liblinphone_tester.h b/tester/liblinphone_tester.h
index 99e15a7e97..5a7d9aab9f 100644
--- a/tester/liblinphone_tester.h
+++ b/tester/liblinphone_tester.h
@@ -341,6 +341,7 @@ typedef struct _stats {
 	int progress_of_LinphoneFileTransfer;
 	int number_of_LinphoneFileTransfer;
 	int number_of_LinphoneReactionSentOrReceived;
+	int number_of_LinphoneReactionRemoved;
 
 	int number_of_LinphoneChatRoomConferenceJoined;
 	int number_of_LinphoneChatRoomEphemeralLifetimeChanged;
@@ -636,6 +637,10 @@ void reaction_received(LinphoneCore *lc,
                        LinphoneChatRoom *room,
                        LinphoneChatMessage *msg,
                        const LinphoneChatMessageReaction *reaction);
+void reaction_removed(LinphoneCore *lc,
+                      LinphoneChatRoom *room,
+                      LinphoneChatMessage *msg,
+                      const LinphoneAddress *address);
 void file_transfer_received(LinphoneChatMessage *message, LinphoneContent *content, const LinphoneBuffer *buffer);
 LinphoneBuffer *
 tester_file_transfer_send(LinphoneChatMessage *message, LinphoneContent *content, size_t offset, size_t size);
diff --git a/tester/message_tester.c b/tester/message_tester.c
index d5960286f9..a9daf2d94d 100644
--- a/tester/message_tester.c
+++ b/tester/message_tester.c
@@ -422,25 +422,24 @@ void check_reactions(LinphoneChatMessage *message,
 	BC_ASSERT_PTR_NOT_NULL(reactions);
 
 	if (reactions_it) {
-		BC_ASSERT_EQUAL(bctbx_list_size(reactions), expected_reactions_count, size_t, "%zu");
-		if (bctbx_list_size(reactions) == expected_reactions_count) {
-			for (size_t i = 0; i < expected_reactions_count; i++) {
-				const LinphoneChatMessageReaction *reaction =
-				    (const LinphoneChatMessageReaction *)bctbx_list_get_data(reactions_it);
-				reactions_it = bctbx_list_next(reactions_it);
+		size_t count = bctbx_list_size(reactions);
+		BC_ASSERT_EQUAL(count, expected_reactions_count, size_t, "%zu");
+		for (size_t i = 0; i < count; i++) {
+			const LinphoneChatMessageReaction *reaction =
+			    (const LinphoneChatMessageReaction *)bctbx_list_get_data(reactions_it);
+			reactions_it = bctbx_list_next(reactions_it);
 
-				const char *expected_reaction = (const char *)bctbx_list_get_data(expected_reactions_it);
-				expected_reactions_it = bctbx_list_next(expected_reactions_it);
+			const char *expected_reaction = (const char *)bctbx_list_get_data(expected_reactions_it);
+			expected_reactions_it = bctbx_list_next(expected_reactions_it);
 
-				const char *expected_reaction_from = (const char *)bctbx_list_get_data(expected_reactions_from_it);
-				expected_reactions_from_it = bctbx_list_next(expected_reactions_from_it);
+			const char *expected_reaction_from = (const char *)bctbx_list_get_data(expected_reactions_from_it);
+			expected_reactions_from_it = bctbx_list_next(expected_reactions_from_it);
 
-				const char *reaction_body = linphone_chat_message_reaction_get_body(reaction);
-				BC_ASSERT_STRING_EQUAL(reaction_body, expected_reaction);
+			const char *reaction_body = linphone_chat_message_reaction_get_body(reaction);
+			BC_ASSERT_STRING_EQUAL(reaction_body, expected_reaction);
 
-				const LinphoneAddress *from = linphone_chat_message_reaction_get_from_address(reaction);
-				BC_ASSERT_STRING_EQUAL(linphone_address_as_string_uri_only(from), expected_reaction_from);
-			}
+			const LinphoneAddress *from = linphone_chat_message_reaction_get_from_address(reaction);
+			BC_ASSERT_STRING_EQUAL(linphone_address_as_string_uri_only(from), expected_reaction_from);
 		}
 	}
 	bctbx_list_free_with_data(reactions, (bctbx_list_free_func)linphone_chat_message_reaction_unref);
@@ -4101,4 +4100,3 @@ test_suite_t rtt_message_test_suite = {"RTT Message",
                                        sizeof(rtt_message_tests) / sizeof(rtt_message_tests[0]),
                                        rtt_message_tests,
                                        0};
-
diff --git a/tester/tester.c b/tester/tester.c
index 185d7fe089..e6d166d9db 100644
--- a/tester/tester.c
+++ b/tester/tester.c
@@ -2477,6 +2477,7 @@ void linphone_core_manager_init2(LinphoneCoreManager *mgr, BCTBX_UNUSED(const ch
 	linphone_core_cbs_set_call_state_changed(mgr->cbs, call_state_changed);
 	linphone_core_cbs_set_message_received(mgr->cbs, message_received);
 	linphone_core_cbs_set_new_message_reaction(mgr->cbs, reaction_received);
+	linphone_core_cbs_set_reaction_removed(mgr->cbs, reaction_removed);
 	linphone_core_cbs_set_messages_received(mgr->cbs, messages_received);
 	linphone_core_cbs_set_is_composing_received(mgr->cbs, is_composing_received);
 	linphone_core_cbs_set_new_subscription_requested(mgr->cbs, new_subscription_requested);
@@ -3218,6 +3219,17 @@ void reaction_received(LinphoneCore *lc,
 	counters->number_of_LinphoneReactionSentOrReceived++;
 }
 
+void reaction_removed(LinphoneCore *lc,
+                      BCTBX_UNUSED(LinphoneChatRoom *room),
+                      LinphoneChatMessage *msg,
+                      const LinphoneAddress *address) {
+	char *from = linphone_address_as_string(address);
+	const char *text = linphone_chat_message_get_text(msg);
+	ms_message("Reaction sent by [%s] for message [%s] has been removed", from, text);
+	stats *counters = get_stats(lc);
+	counters->number_of_LinphoneReactionRemoved++;
+}
+
 void is_composing_received(LinphoneCore *lc, LinphoneChatRoom *room) {
 	stats *counters = get_stats(lc);
 	if (linphone_chat_room_is_remote_composing(room)) {
-- 
GitLab