diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
index 230c68cb31..e1dab55979 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
@@ -139,6 +139,7 @@ import im.vector.app.features.home.room.detail.composer.TextComposerViewEvents
 import im.vector.app.features.home.room.detail.composer.TextComposerViewModel
 import im.vector.app.features.home.room.detail.composer.TextComposerViewState
 import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
+import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
 import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
 import im.vector.app.features.home.room.detail.timeline.TimelineEventController
 import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction
@@ -694,15 +695,15 @@ class RoomDetailFragment @Inject constructor(
     private fun setupVoiceMessageView() {
         voiceMessagePlaybackTracker.track(VoiceMessagePlaybackTracker.RECORDING_ID, views.voiceMessageRecorderView)
         views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback {
-            override fun onVoiceRecordingStarted(): Boolean {
-                return if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
+
+            private var currentUiState: RecordingUiState = RecordingUiState.None
+
+            override fun onVoiceRecordingStarted() {
+                if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
                     roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage)
                     textComposerViewModel.handle(TextComposerAction.OnVoiceRecordingStateChanged(true))
                     vibrate(requireContext())
-                    true
-                } else {
-                    // Permission dialog is displayed
-                    false
+                    views.voiceMessageRecorderView.display(RecordingUiState.Started)
                 }
             }
 
@@ -718,6 +719,29 @@ class RoomDetailFragment @Inject constructor(
             override fun onVoicePlaybackButtonClicked() {
                 roomDetailViewModel.handle(RoomDetailAction.PlayOrPauseRecordingPlayback)
             }
+
+            override fun onRecordingStopped() {
+                if (currentUiState != RecordingUiState.Locked && currentUiState != RecordingUiState.None) {
+                    views.voiceMessageRecorderView.display(RecordingUiState.None)
+                }
+            }
+
+            override fun onUiStateChanged(state: RecordingUiState) {
+                currentUiState = state
+                views.voiceMessageRecorderView.display(state)
+            }
+
+            override fun sendVoiceMessage() {
+                views.voiceMessageRecorderView.display(RecordingUiState.None)
+            }
+
+            override fun deleteVoiceMessage() {
+                views.voiceMessageRecorderView.display(RecordingUiState.None)
+            }
+
+            override fun onRecordingLimitReached() {
+                views.voiceMessageRecorderView.display(RecordingUiState.Playback)
+            }
         }
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/DraggableStateProcessor.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/DraggableStateProcessor.kt
index 4cbb96a703..5825e60ecf 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/DraggableStateProcessor.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/DraggableStateProcessor.kt
@@ -21,7 +21,7 @@ import android.view.MotionEvent
 import im.vector.app.R
 import im.vector.app.core.utils.DimensionConverter
 import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.DraggingState
-import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingState
+import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
 import kotlin.math.abs
 
 class DraggableStateProcessor(
@@ -50,7 +50,7 @@ class DraggableStateProcessor(
         lastDistanceY = 0F
     }
 
-    fun process(event: MotionEvent, recordingState: RecordingState): RecordingState {
+    fun process(event: MotionEvent, recordingState: RecordingUiState): RecordingUiState {
         val currentX = event.rawX
         val currentY = event.rawY
         val distanceX = abs(firstX - currentX)
@@ -63,9 +63,9 @@ class DraggableStateProcessor(
         }
     }
 
-    private fun nextRecordingState(recordingState: RecordingState, currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): RecordingState {
+    private fun nextRecordingState(recordingState: RecordingUiState, currentX: Float, currentY: Float, distanceX: Float, distanceY: Float): RecordingUiState {
         return when (recordingState) {
-            RecordingState.Started      -> {
+            RecordingUiState.Started    -> {
                 // Determine if cancelling or locking for the first move action.
                 if (((currentX < firstX && rtlXMultiplier == 1) || (currentX > firstX && rtlXMultiplier == -1)) && distanceX > distanceY && distanceX > lastDistanceX) {
                     DraggingState.Cancelling(distanceX)
@@ -78,9 +78,9 @@ class DraggableStateProcessor(
             is DraggingState.Cancelling -> {
                 // Check if cancelling conditions met, also check if it should be initial state
                 if (distanceX < minimumMove && distanceX < lastDistanceX) {
-                    RecordingState.Started
+                    RecordingUiState.Started
                 } else if (shouldCancelRecording(distanceX)) {
-                    RecordingState.Cancelled
+                    RecordingUiState.Cancelled
                 } else {
                     DraggingState.Cancelling(distanceX)
                 }
@@ -88,9 +88,9 @@ class DraggableStateProcessor(
             is DraggingState.Locking    -> {
                 // Check if locking conditions met, also check if it should be initial state
                 if (distanceY < minimumMove && distanceY < lastDistanceY) {
-                    RecordingState.Started
+                    RecordingUiState.Started
                 } else if (shouldLockRecording(distanceY)) {
-                    RecordingState.Locked
+                    RecordingUiState.Locked
                 } else {
                     DraggingState.Locking(distanceY)
                 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
index 6bd55d4400..79898dad32 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
@@ -28,7 +28,6 @@ import im.vector.app.core.utils.CountUpTimer
 import im.vector.app.core.utils.DimensionConverter
 import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
 import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
-import org.matrix.android.sdk.api.extensions.orFalse
 import kotlin.math.floor
 
 /**
@@ -41,11 +40,15 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
 ) : ConstraintLayout(context, attrs, defStyleAttr), VoiceMessagePlaybackTracker.Listener {
 
     interface Callback {
-        // Return true if the recording is started
-        fun onVoiceRecordingStarted(): Boolean
+        fun onVoiceRecordingStarted()
         fun onVoiceRecordingEnded(isCancelled: Boolean)
         fun onVoiceRecordingPlaybackModeOn()
         fun onVoicePlaybackButtonClicked()
+        fun onRecordingStopped()
+        fun onUiStateChanged(state: RecordingUiState)
+        fun sendVoiceMessage()
+        fun deleteVoiceMessage()
+        fun onRecordingLimitReached()
     }
 
     // We need to define views as lateinit var to be able to check if initialized for the bug fix on api 21 and 22.
@@ -54,7 +57,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
 
     var callback: Callback? = null
 
-    private var recordingState: RecordingState = RecordingState.None
+    private var currentUiState: RecordingUiState = RecordingUiState.None
     private var recordingTicker: CountUpTimer? = null
 
     init {
@@ -78,7 +81,6 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
     }
 
     fun initVoiceRecordingViews() {
-        recordingState = RecordingState.None
         stopRecordingTicker()
         voiceMessageViews.initViews(onVoiceRecordingEnded = {})
     }
@@ -86,36 +88,39 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
     private fun initListeners() {
         voiceMessageViews.start(object : VoiceMessageViews.Actions {
             override fun onRequestRecording() {
-                if (callback?.onVoiceRecordingStarted().orFalse()) {
-                    display(RecordingState.Started)
-                }
+                callback?.onVoiceRecordingStarted()
             }
 
             override fun onRecordingStopped() {
-                if (recordingState != RecordingState.Locked && recordingState != RecordingState.None) {
-                    display(RecordingState.None)
-                }
+                callback?.onRecordingStopped()
             }
 
-            override fun isActive() = recordingState != RecordingState.Cancelled
+            override fun isActive() = currentUiState != RecordingUiState.Cancelled
 
-            override fun updateState(updater: (RecordingState) -> RecordingState) {
-                updater(recordingState).also {
-                    display(it)
+            override fun updateState(updater: (RecordingUiState) -> RecordingUiState) {
+                updater(currentUiState).also { newState ->
+                    when (newState) {
+                        is DraggingState -> display(newState)
+                        else             -> {
+                            if (newState != currentUiState) {
+                                callback?.onUiStateChanged(newState)
+                            }
+                        }
+                    }
                 }
             }
 
             override fun sendMessage() {
-                display(RecordingState.None)
+                callback?.sendVoiceMessage()
             }
 
             override fun delete() {
                 // this was previously marked as cancelled true
-                display(RecordingState.None)
+                callback?.deleteVoiceMessage()
             }
 
             override fun waveformClicked() {
-                display(RecordingState.Playback)
+                display(RecordingUiState.Playback)
             }
 
             override fun onVoicePlaybackButtonClicked() {
@@ -124,43 +129,41 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
         })
     }
 
-    fun display(recordingState: RecordingState) {
-        val previousState = this.recordingState
-        val stateHasChanged = recordingState != this.recordingState
-        this.recordingState = recordingState
+    fun display(recordingState: RecordingUiState) {
+        if (recordingState == this.currentUiState) return
 
-        if (stateHasChanged) {
-            when (recordingState) {
-                RecordingState.None      -> {
-                    val isCancelled = previousState == RecordingState.Cancelled
-                    voiceMessageViews.hideRecordingViews(recordingState, isCancelled = isCancelled) { callback?.onVoiceRecordingEnded(it) }
-                    stopRecordingTicker()
-                }
-                RecordingState.Started   -> {
-                    startRecordingTicker()
-                    voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast))
-                    voiceMessageViews.showRecordingViews()
-                }
-                RecordingState.Cancelled -> {
-                    voiceMessageViews.hideRecordingViews(recordingState, isCancelled = true) { callback?.onVoiceRecordingEnded(it) }
-                    vibrate(context)
-                }
-                RecordingState.Locked    -> {
-                    voiceMessageViews.renderLocked()
-                    postDelayed({
-                        voiceMessageViews.showRecordingLockedViews(recordingState) { callback?.onVoiceRecordingEnded(it) }
-                    }, 500)
-                }
-                RecordingState.Playback  -> {
-                    stopRecordingTicker()
-                    voiceMessageViews.showPlaybackViews()
-                    callback?.onVoiceRecordingPlaybackModeOn()
-                }
-                is DraggingState         -> when (recordingState) {
-                    is DraggingState.Cancelling -> voiceMessageViews.renderCancelling(recordingState.distanceX)
-                    is DraggingState.Locking    -> voiceMessageViews.renderLocking(recordingState.distanceY)
-                }.exhaustive
+        val previousState = this.currentUiState
+        this.currentUiState = recordingState
+        when (recordingState) {
+            RecordingUiState.None      -> {
+                val isCancelled = previousState == RecordingUiState.Cancelled
+                voiceMessageViews.hideRecordingViews(recordingState, isCancelled = isCancelled) { callback?.onVoiceRecordingEnded(it) }
+                stopRecordingTicker()
             }
+            RecordingUiState.Started   -> {
+                startRecordingTicker()
+                voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast))
+                voiceMessageViews.showRecordingViews()
+            }
+            RecordingUiState.Cancelled -> {
+                voiceMessageViews.hideRecordingViews(recordingState, isCancelled = true) { callback?.onVoiceRecordingEnded(it) }
+                vibrate(context)
+            }
+            RecordingUiState.Locked    -> {
+                voiceMessageViews.renderLocked()
+                postDelayed({
+                    voiceMessageViews.showRecordingLockedViews(recordingState) { callback?.onVoiceRecordingEnded(it) }
+                }, 500)
+            }
+            RecordingUiState.Playback  -> {
+                stopRecordingTicker()
+                voiceMessageViews.showPlaybackViews()
+                callback?.onVoiceRecordingPlaybackModeOn()
+            }
+            is DraggingState           -> when (recordingState) {
+                is DraggingState.Cancelling -> voiceMessageViews.renderCancelling(recordingState.distanceX)
+                is DraggingState.Locking    -> voiceMessageViews.renderLocking(recordingState.distanceY)
+            }.exhaustive
         }
     }
 
@@ -178,11 +181,11 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
     }
 
     private fun onRecordingTick(milliseconds: Long) {
-        voiceMessageViews.renderRecordingTimer(recordingState, milliseconds / 1_000)
+        voiceMessageViews.renderRecordingTimer(currentUiState, milliseconds / 1_000)
         val timeDiffToRecordingLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS - milliseconds
         if (timeDiffToRecordingLimit <= 0) {
             post {
-                display(RecordingState.Playback)
+                callback?.onRecordingLimitReached()
             }
         } else if (timeDiffToRecordingLimit in 10_000..10_999) {
             post {
@@ -200,7 +203,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
     /**
      * Returns true if the voice message is recording or is in playback mode
      */
-    fun isActive() = recordingState !in listOf(RecordingState.None, RecordingState.Cancelled)
+    fun isActive() = currentUiState !in listOf(RecordingUiState.None, RecordingUiState.Cancelled)
 
     override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) {
         when (state) {
@@ -217,17 +220,16 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
         }
     }
 
-    sealed interface RecordingState {
-        object None : RecordingState
-        object Started : RecordingState
-        object Cancelled : RecordingState
-        object Locked : RecordingState
-        object Playback : RecordingState
+    sealed interface RecordingUiState {
+        object None : RecordingUiState
+        object Started : RecordingUiState
+        object Cancelled : RecordingUiState
+        object Locked : RecordingUiState
+        object Playback : RecordingUiState
     }
 
-    sealed interface DraggingState : RecordingState {
+    sealed interface DraggingState : RecordingUiState {
         data class Cancelling(val distanceX: Float) : DraggingState
         data class Locking(val distanceY: Float) : DraggingState
     }
 }
-
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
index d9f5f9675b..ce4ec4b519 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
@@ -32,7 +32,7 @@ import im.vector.app.core.extensions.setAttributeTintedBackground
 import im.vector.app.core.extensions.setAttributeTintedImageResource
 import im.vector.app.core.utils.DimensionConverter
 import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
-import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingState
+import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
 import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
 import org.matrix.android.sdk.api.extensions.orFalse
 
@@ -155,9 +155,9 @@ class VoiceMessageViews(
         views.voiceMessageSendButton.isVisible = false
     }
 
-    fun hideRecordingViews(recordingState: RecordingState, isCancelled: Boolean?, onVoiceRecordingEnded: (Boolean) -> Unit) {
+    fun hideRecordingViews(recordingState: RecordingUiState, isCancelled: Boolean?, onVoiceRecordingEnded: (Boolean) -> Unit) {
         // We need to animate the lock image first
-        if (recordingState != RecordingState.Locked || isCancelled.orFalse()) {
+        if (recordingState != RecordingUiState.Locked || isCancelled.orFalse()) {
             views.voiceMessageLockImage.isVisible = false
             views.voiceMessageLockImage.animate().translationY(0f).start()
             views.voiceMessageLockBackground.isVisible = false
@@ -171,7 +171,7 @@ class VoiceMessageViews(
         views.voiceMessageSlideToCancel.animate().translationX(0f).translationY(0f).start()
         views.voiceMessagePlaybackLayout.isVisible = false
 
-        if (recordingState != RecordingState.Locked) {
+        if (recordingState != RecordingUiState.Locked) {
             views.voiceMessageMicButton
                     .animate()
                     .scaleX(1f)
@@ -203,7 +203,7 @@ class VoiceMessageViews(
         }
 
         // Hide toasts if user cancelled recording before the timeout of the toast.
-        if (recordingState == RecordingState.Cancelled || recordingState == RecordingState.None) {
+        if (recordingState == RecordingUiState.Cancelled || recordingState == RecordingUiState.None) {
             hideToast()
         }
     }
@@ -266,7 +266,7 @@ class VoiceMessageViews(
         views.voiceMessageToast.isVisible = false
     }
 
-    fun showRecordingLockedViews(recordingState: RecordingState, onVoiceRecordingEnded: (Boolean) -> Unit) {
+    fun showRecordingLockedViews(recordingState: RecordingUiState, onVoiceRecordingEnded: (Boolean) -> Unit) {
         hideRecordingViews(recordingState, null, onVoiceRecordingEnded)
         views.voiceMessagePlaybackLayout.isVisible = true
         views.voiceMessagePlaybackTimerIndicator.isVisible = true
@@ -283,7 +283,7 @@ class VoiceMessageViews(
     }
 
     fun initViews(onVoiceRecordingEnded: (Boolean) -> Unit) {
-        hideRecordingViews(RecordingState.None, null, onVoiceRecordingEnded)
+        hideRecordingViews(RecordingUiState.None, null, onVoiceRecordingEnded)
         views.voiceMessageMicButton.isVisible = true
         views.voiceMessageSendButton.isVisible = false
         views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() }
@@ -312,9 +312,9 @@ class VoiceMessageViews(
         views.voiceMessageToast.isVisible = false
     }
 
-    fun renderRecordingTimer(recordingState: RecordingState, recordingTimeMillis: Long) {
+    fun renderRecordingTimer(recordingState: RecordingUiState, recordingTimeMillis: Long) {
         val formattedTimerText = DateUtils.formatElapsedTime(recordingTimeMillis)
-        if (recordingState == RecordingState.Locked) {
+        if (recordingState == RecordingUiState.Locked) {
             views.voicePlaybackTime.apply {
                 post {
                     text = formattedTimerText
@@ -349,7 +349,7 @@ class VoiceMessageViews(
         fun onRequestRecording()
         fun onRecordingStopped()
         fun isActive(): Boolean
-        fun updateState(updater: (RecordingState) -> RecordingState)
+        fun updateState(updater: (RecordingUiState) -> RecordingUiState)
         fun sendMessage()
         fun delete()
         fun waveformClicked()