diff --git a/jami-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.kt b/jami-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.kt
index 6c0a7040f32a684f44dfb2750b24f6177b5f84d0..538294d60262656016d8e229faabc293fcd92f53 100644
--- a/jami-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.kt
+++ b/jami-android/app/src/main/java/cx/ring/adapters/ConversationAdapter.kt
@@ -318,26 +318,62 @@ class ConversationAdapter(
         conversationViewHolder: ConversationViewHolder,
         interaction: Interaction
     ) {
+        val statusIcon = conversationViewHolder.mStatusIcon ?: return
+        val messageToAttach = conversationViewHolder.mLayoutStatusIconId?.id ?: return
+
         val conversation = interaction.conversation
         if (conversation == null || conversation !is Conversation) {
-            conversationViewHolder.mStatusIcon?.isVisible = false
+            statusIcon.isVisible = false
             return
         }
-        conversationViewHolder.compositeDisposable.add(presenter.conversationFacade
-            .getLoadedContact(
-                interaction.account!!,
-                conversation,
-                interaction.displayedContacts
-            )
-            .observeOn(DeviceUtils.uiScheduler)
-            .subscribe { contacts ->
-                conversationViewHolder.mStatusIcon?.isVisible = contacts.isNotEmpty()
-                conversationViewHolder.mStatusIcon?.update(
-                    contacts,
-                    interaction.status,
-                    conversationViewHolder.mLayoutStatusIconId?.id ?: View.NO_ID
-                )
-            })
+
+        // Attach the statusIcon to the message layout.
+        statusIcon.attachToMessage(messageToAttach)
+
+        // Remove user from statusMap.
+        val modifiedStatusMap = interaction.statusMap
+            .filter { !conversation.findContact(net.jami.model.Uri.fromId(it.key))!!.isUser }
+
+        val isDisplayed = modifiedStatusMap.any { it.value == Interaction.MessageStates.DISPLAYED }
+        val isSending = modifiedStatusMap.isEmpty() or
+                modifiedStatusMap.any { it.value == Interaction.MessageStates.SENDING }
+        val isReceived = modifiedStatusMap.any { it.value == Interaction.MessageStates.SUCCESS }
+        val lastDisplayedIdx = conversation.lastDisplayedMessages
+            .map { conversation.getMessage(it.value) }
+            .maxOfOrNull { mInteractions.indexOf(it) }
+        val currentIdx = mInteractions.indexOf(interaction)
+
+        // Case 1: Message is sending
+        if(!isDisplayed && isSending){
+            statusIcon.let {
+                it.visibility = View.VISIBLE
+                it.updateSending()
+            }
+        }
+        // Case 2: Message is received by at least one contact
+        else if(!isDisplayed && isReceived){
+            statusIcon.let {
+                it.visibility = View.VISIBLE
+                it.updateSuccess()
+            }
+        }
+        // Case 3: Message is displayed
+        else if (isDisplayed && currentIdx == lastDisplayedIdx) {
+            conversationViewHolder.compositeDisposable.add(
+                presenter.conversationFacade
+                    .getLoadedContact(
+                        accountId = interaction.account!!,
+                        conversation = conversation,
+                        contactIds = modifiedStatusMap.map { it.key }
+                    )
+                    .observeOn(DeviceUtils.uiScheduler)
+                    .subscribe { seenBy ->
+                        statusIcon.visibility = View.VISIBLE
+                        statusIcon.updateDisplayed(seenBy)
+                    })
+        }
+        // Case 4: Message is displayed but not the last displayed message
+        else statusIcon.visibility = View.GONE
     }
 
     /**
diff --git a/jami-android/app/src/main/java/cx/ring/views/MessageStatusView.kt b/jami-android/app/src/main/java/cx/ring/views/MessageStatusView.kt
index 05f886b68b5450d0f55b501663d73c8a9bfbb804..a7f464181c3b64d101412baf50efd0341800b2aa 100644
--- a/jami-android/app/src/main/java/cx/ring/views/MessageStatusView.kt
+++ b/jami-android/app/src/main/java/cx/ring/views/MessageStatusView.kt
@@ -32,95 +32,117 @@ import net.jami.model.ContactViewModel
 import net.jami.model.Interaction
 import net.jami.utils.Log
 
+/**
+ * MessageStatusView display the status of a message (sending, sent, displayed).
+ * Sending and sent Status are displayed as icons, while Displayed status is displayed as avatars.
+ */
 class MessageStatusView @JvmOverloads constructor(
     context: Context,
     attrs: AttributeSet? = null,
     defStyle: Int = 0,
 ) : LinearLayout(context, attrs, defStyle) {
 
+    @IdRes
+    private var attachedMessage: Int = View.NO_ID
     private val iconSize = resources.getDimensionPixelSize(R.dimen.conversation_status_icon_size)
     private val iconTint: ColorStateList =
         ColorStateList.valueOf(ContextCompat.getColor(context, R.color.grey_500))
 
+    // Add or remove views to match the given count.
+    // "Sending" or "Sent" need 1 view, "Displayed" needs as many views as there are contacts.
     private fun resize(count: Int) {
-        if (count == 0) {
-            removeAllViews()
-        } else if (childCount > count) {
-            while (childCount > count) {
-                removeViewAt(childCount - 1)
-            }
-        } else if (childCount < count) {
-            var i = childCount
-            while (childCount < count) {
-                addView(ImageView(context).apply {
-                    layoutParams = LayoutParams(iconSize, iconSize).apply {
-                        marginStart = if (i != 0) -iconSize/3  else 0
-                    }
-                })
-                i++
-            }
-        }
-    }
+        if (count == childCount) return
 
-    fun update(contacts: Collection<ContactViewModel>, status: Interaction.InteractionStatus, @IdRes resId: Int = View.NO_ID) {
-        val showStatus = contacts.isEmpty() && (status == Interaction.InteractionStatus.SUCCESS || status == Interaction.InteractionStatus.SENDING)
-        if (showStatus) {
-            resize(1)
-            (getChildAt(0) as ImageView).apply {
-                setImageResource(when (status) {
-                    Interaction.InteractionStatus.FAILURE -> R.drawable.round_highlight_off_24
-                    Interaction.InteractionStatus.SENDING -> R.drawable.baseline_check_circle_outline_24
-                    Interaction.InteractionStatus.SUCCESS -> R.drawable.baseline_check_circle_24
-                    else -> -1 //R.drawable.baseline_check_circle_24
-                })
-                ImageViewCompat.setImageTintList(this, iconTint)
-            }
-        } else {
-            resize(contacts.size)
-            contacts.forEachIndexed { index, contact ->
-                (getChildAt(index) as ImageView).apply {
-                    imageTintList = null
-                    setImageDrawable(AvatarDrawable.Builder()
-                        .withCircleCrop(true)
-                        .withContact(contact)
-                        .withPresence(false)
-                        .build(context))
+        // Update layout only if there is a change in the mode (empty, single or multiple).
+        if (childCount == 0 || (count == 1 && childCount > 1) || (count > 1 && childCount == 1))
+            layout(count)
+
+        if (count == 0) removeAllViews()
+        else if (childCount > count) while (childCount > count) removeViewAt(childCount - 1)
+        else if (childCount < count) repeat(count - childCount) {
+            addView(ImageView(context).apply {
+                layoutParams = LayoutParams(iconSize, iconSize).apply {
+                    marginStart = if (it != 0) -iconSize / 3 else 0
                 }
-            }
+            })
         }
+    }
+
+    // Layout the views depending on the count and the layout type.
+    // If one view is displayed, it is put on the right of the message.
+    // If multiple views are displayed, they are put below the message.
+    private fun layout(count: Int) {
+        val fitRight = count < 2
         when (layoutParams) {
             is RelativeLayout.LayoutParams -> {
                 val params = layoutParams as RelativeLayout.LayoutParams? ?: return
-                val fitRight = showStatus || contacts.size < 2
                 if (fitRight) {
-                    // Put the avatar on the right of the message if there is only one contact
+                    // Put the view on the right of the message.
                     params.removeRule(RelativeLayout.BELOW)
-                    params.addRule(RelativeLayout.ALIGN_BOTTOM, resId)
+                    params.addRule(RelativeLayout.ALIGN_BOTTOM, attachedMessage)
                 } else {
-                    // Put the avatars below the message if there are multiple contacts
+                    // Put the view below the message.
                     params.removeRule(RelativeLayout.ALIGN_BOTTOM)
-                    params.addRule(RelativeLayout.BELOW, resId)
+                    params.addRule(RelativeLayout.BELOW, attachedMessage)
                 }
                 layoutParams = params
             }
+
             is ConstraintLayout.LayoutParams -> {
                 val params = layoutParams as ConstraintLayout.LayoutParams? ?: return
-                val fitRight = showStatus || contacts.size < 2
                 if (fitRight) {
-                    // Put the avatar on the right of the message if there is only one contact
+                    // Put the view on the right of the message.
                     params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
                     params.topToBottom = ConstraintLayout.LayoutParams.UNSET
                 } else {
-                    // Put the avatars below the message if there are multiple contacts
+                    // Put the view below the message.
                     params.bottomToBottom = ConstraintLayout.LayoutParams.UNSET
-                    params.topToBottom = resId
+                    params.topToBottom = attachedMessage
                 }
                 layoutParams = params
             }
+
             else -> Log.w(TAG, "Error layout params.")
         }
     }
 
+    fun attachToMessage(@IdRes resId: Int) {
+        attachedMessage = resId
+        layout(childCount)
+    }
+
+    fun updateSending() {
+        resize(1)
+        (getChildAt(0) as ImageView).apply {
+            setImageResource(R.drawable.sent)
+            ImageViewCompat.setImageTintList(this, iconTint)
+        }
+    }
+
+    fun updateSuccess() {
+        resize(1)
+        (getChildAt(0) as ImageView).apply {
+            setImageResource(R.drawable.receive)
+            ImageViewCompat.setImageTintList(this, iconTint)
+        }
+    }
+
+    fun updateDisplayed(seenBy: Collection<ContactViewModel>) {
+        resize(seenBy.size)
+        seenBy.forEachIndexed { index, contact ->
+            (getChildAt(index) as ImageView).apply {
+                imageTintList = null
+                setImageDrawable(
+                    AvatarDrawable.Builder()
+                        .withCircleCrop(true)
+                        .withContact(contact)
+                        .withPresence(false)
+                        .build(context)
+                )
+            }
+        }
+    }
+
     companion object {
         val TAG = MessageStatusView::class.simpleName!!
     }
diff --git a/jami-android/app/src/main/res/drawable/receive.xml b/jami-android/app/src/main/res/drawable/receive.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7dd4db924fd7a288df3408bf2399943c864d1d33
--- /dev/null
+++ b/jami-android/app/src/main/res/drawable/receive.xml
@@ -0,0 +1,13 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="12dp"
+    android:height="12dp"
+    android:viewportWidth="12"
+    android:viewportHeight="12">
+  <group>
+    <clip-path
+        android:pathData="M0,0h12v12h-12z"/>
+    <path
+        android:pathData="M6.43,8.784 L3.007,5.362 4.06,4.309l2.37,2.37 4.314,-4.314A5.966,5.966 0,0 0,6 0c-0.032,0 -0.061,0.008 -0.094,0.01A5.98,5.98 0,0 0,0.094 5.074,5.911 5.911,0 0,0 0,6a5.911,5.911 0,0 0,0.094 0.926A5.98,5.98 0,0 0,5.906 11.99c0.032,0 0.061,0.01 0.094,0.01a6,6 0,0 0,5.533 -8.32Z"
+        android:fillColor="#60c880"/>
+  </group>
+</vector>
diff --git a/jami-android/app/src/main/res/drawable/sent.xml b/jami-android/app/src/main/res/drawable/sent.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f407a6a04a5151a871c56b654b77f15bd0235ad8
--- /dev/null
+++ b/jami-android/app/src/main/res/drawable/sent.xml
@@ -0,0 +1,13 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="12dp"
+    android:height="12dp"
+    android:viewportWidth="12"
+    android:viewportHeight="12">
+  <path
+      android:pathData="M6,6m-5.25,0a5.25,5.25 0,1 1,10.5 0a5.25,5.25 0,1 1,-10.5 0"
+      android:strokeAlpha="0.5"
+      android:strokeWidth="1.5"
+      android:fillColor="#00000000"
+      android:strokeColor="#fff"
+      android:fillAlpha="0.5"/>
+</vector>
diff --git a/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Conversation.kt b/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Conversation.kt
index f08fbb2226ed8a6801be674839d2a4719a75079e..04b3c070f39b3484efaa4071949a6ded7188943c 100644
--- a/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Conversation.kt
+++ b/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Conversation.kt
@@ -35,7 +35,7 @@ class Conversation : ConversationHistory {
     private val currentCalls = ArrayList<Conference>()
     val aggregateHistory = ArrayList<Interaction>(32)
 
-    private val lastDisplayedMessages: MutableMap<String, String> = HashMap()
+    val lastDisplayedMessages: MutableMap<String, String> = HashMap()
     private val updatedElementSubject: Subject<Pair<Interaction, ElementStatus>> = PublishSubject.create()
     private val clearedSubject: Subject<List<Interaction>> = PublishSubject.create()
     private val callsSubject: Subject<List<Conference>> = BehaviorSubject.createDefault(emptyList())
@@ -358,8 +358,20 @@ class Conversation : ConversationHistory {
                 updatedElementSubject.onNext(Pair(e, ElementStatus.UPDATE))
             }
         }
-        // Add contact to new displayed interaction
-        lastDisplayedMessages[contactId] = messageId
+
+        // Check if the new message is after the last displayed message (could be not the case).
+        val currentLastMessageDisplayed: Interaction? =
+            lastDisplayedMessages[contactId]?.let { getMessage(it) }
+        val newPotentialMessageDisplayed = getMessage(messageId)
+        val isAfter =
+            if (currentLastMessageDisplayed != null && newPotentialMessageDisplayed != null) {
+                isAfter(currentLastMessageDisplayed, newPotentialMessageDisplayed)
+            } else false
+
+        // Update the last displayed message
+        if (isAfter or (currentLastMessageDisplayed == null))
+            lastDisplayedMessages[contactId] = messageId
+
         mMessages[messageId]?.let { e ->
             e.displayedContacts.add(contactId)
             updatedElementSubject.onNext(Pair(e, ElementStatus.UPDATE))
@@ -367,18 +379,23 @@ class Conversation : ConversationHistory {
     }
 
     @Synchronized
-    fun updateSwarmInteraction(messageId: String, contactUri: Uri, newStatus: Interaction.InteractionStatus) {
-        val e = mMessages[messageId] ?: return
-        if (newStatus == Interaction.InteractionStatus.DISPLAYED) {
-            Log.w(TAG, "updateSwarmInteraction DISPLAYED")
+    fun updateSwarmInteraction(
+        messageId: String,
+        contactUri: Uri,
+        newStatus: Interaction.MessageStates,
+    ) {
+        val interaction = mMessages[messageId] ?: return
+        if (newStatus == Interaction.MessageStates.DISPLAYED) {
             findContact(contactUri)?.let { contact ->
                 if (!contact.isUser)
                     setLastMessageDisplayed(contactUri.host, messageId)
             }
-        } else if (newStatus != Interaction.InteractionStatus.SENDING) {
-            e.status = newStatus
-            updatedElementSubject.onNext(Pair(e, ElementStatus.UPDATE))
+        } else if (newStatus != Interaction.MessageStates.SENDING) {
+            interaction.status = Interaction.InteractionStatus.SENDING
         }
+
+        interaction.statusMap = interaction.statusMap.plus(Pair(contactUri.host, newStatus))
+        updatedElementSubject.onNext(Pair(interaction, ElementStatus.UPDATE))
     }
 
     @Synchronized
diff --git a/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Interaction.kt b/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Interaction.kt
index f94253735f673a1e603c598e1ecb6d80c40722e7..789b5ae85fe576beeb9bc8702307ec5ecaffc89b 100644
--- a/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Interaction.kt
+++ b/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Interaction.kt
@@ -36,6 +36,7 @@ open class Interaction {
     var reactToId: String? = null
     var reactions: MutableList<Interaction> = ArrayList()
     var history: MutableList<Interaction> = ArrayList<Interaction>(1).apply { add(this@Interaction) }
+    var statusMap: Map<String, MessageStates> = emptyMap()
 
     private var historySubject: Subject<List<Interaction>> = BehaviorSubject.createDefault(history)
 
diff --git a/jami-android/libjamiclient/src/main/kotlin/net/jami/services/AccountService.kt b/jami-android/libjamiclient/src/main/kotlin/net/jami/services/AccountService.kt
index 84dec240669bd5dbe61ec79d629f7705adaab317..8f81638135d436a7e3cab15b57828feb1f44dbbb 100644
--- a/jami-android/libjamiclient/src/main/kotlin/net/jami/services/AccountService.kt
+++ b/jami-android/libjamiclient/src/main/kotlin/net/jami/services/AccountService.kt
@@ -1016,18 +1016,27 @@ class AccountService(
         incomingMessageSubject.onNext(Message(accountId, messageId, callId, from, messages))
     }
 
-    fun accountMessageStatusChanged(accountId: String, conversationId: String, messageId: String, contactId: String, status: Int) {
-        val newStatus = InteractionStatus.fromIntTextMessage(status)
-        Log.d(TAG, "accountMessageStatusChanged: $accountId, $conversationId, $messageId, $contactId, $newStatus")
+    fun accountMessageStatusChanged(
+        accountId: String,
+        conversationId: String,
+        messageId: String,
+        contactId: String,
+        status: Int,
+    ) {
         val account = getAccount(accountId) ?: return
+        val interactionStatus = InteractionStatus.fromIntTextMessage(status)
+        val messageState = Interaction.MessageStates.fromInt(status)
+
         if (conversationId.isEmpty() && !account.isJami) {
             mHistoryService
-                .accountMessageStatusChanged(accountId, messageId, contactId, newStatus)
-                .subscribe({ t: TextMessage -> messageSubject.onNext(t) }) { e: Throwable ->
-                    Log.e(TAG, "Error updating message: " + e.localizedMessage) }
+                .accountMessageStatusChanged(
+                    accountId, messageId, contactId, interactionStatus, messageState
+                )
+                .blockingSubscribe({ t: TextMessage -> messageSubject.onNext(t) })
+                { e: Throwable -> Log.e(TAG, "Error updating message: " + e.localizedMessage) }
         } else {
             account.getSwarm(conversationId)
-                ?.updateSwarmInteraction(messageId, Uri.fromId(contactId), newStatus)
+                ?.updateSwarmInteraction(messageId, Uri.fromId(contactId), messageState)
         }
     }
 
@@ -1280,8 +1289,11 @@ class AccountService(
         val interaction = getInteraction(account, conversation, body)
         val edits = message.editions.map { getInteraction(account, conversation, it.toNative()) }
         val reactions = message.reactions.map { getInteraction(account, conversation, it.toNative()) }
+        val statusMap = message.status.mapValues { Interaction.MessageStates.fromInt(it.value) }
+
         interaction.addEdits(edits)
         interaction.addReactions(reactions)
+        interaction.statusMap = statusMap
 
         return interaction
     }
diff --git a/jami-android/libjamiclient/src/main/kotlin/net/jami/services/HistoryService.kt b/jami-android/libjamiclient/src/main/kotlin/net/jami/services/HistoryService.kt
index c0db7d2af9bc5647b11ca5dba92ffe386923cb74..cb6ae53289fb30cb4aceb33273f3063bff1db819 100644
--- a/jami-android/libjamiclient/src/main/kotlin/net/jami/services/HistoryService.kt
+++ b/jami-android/libjamiclient/src/main/kotlin/net/jami/services/HistoryService.kt
@@ -179,7 +179,13 @@ abstract class HistoryService {
         txt
     }.subscribeOn(scheduler)
 
-    fun accountMessageStatusChanged(accountId: String, daemonId: String, peer: String, status: InteractionStatus): Single<TextMessage> = Single.fromCallable {
+    fun accountMessageStatusChanged(
+        accountId: String,
+        daemonId: String,
+        peer: String,
+        interactionStatus: InteractionStatus,
+        messageState: Interaction.MessageStates,
+    ): Single<TextMessage> = Single.fromCallable {
         val textList = getInteractionDataDao(accountId).queryForEq(Interaction.COLUMN_DAEMON_ID, daemonId)
         if (textList == null || textList.isEmpty()) {
             throw RuntimeException("accountMessageStatusChanged: not able to find message with id $daemonId in database")
@@ -190,7 +196,8 @@ abstract class HistoryService {
             throw RuntimeException("accountMessageStatusChanged: received an invalid text message")
         }
         val msg = TextMessage(text)
-        msg.status = status
+        msg.status = interactionStatus
+        msg.statusMap = msg.statusMap.plus(accountId to messageState)
         getInteractionDataDao(accountId).update(msg)
         msg.account = accountId
         msg