diff --git a/changelog.d/4067.bugfix b/changelog.d/4067.bugfix
new file mode 100644
index 0000000000..63d62df840
--- /dev/null
+++ b/changelog.d/4067.bugfix
@@ -0,0 +1 @@
+Allow voice messages to continue recording during device rotation
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index 69257f1f05..cac694e84e 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -42,6 +42,7 @@ import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel
 import im.vector.app.features.home.UnreadMessagesSharedViewModel
 import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel
 import im.vector.app.features.home.room.detail.RoomDetailViewModel
+import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
 import im.vector.app.features.home.room.detail.search.SearchViewModel
 import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel
 import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel
@@ -508,6 +509,11 @@ interface MavericksViewModelModule {
     @MavericksViewModelKey(RoomDetailViewModel::class)
     fun roomDetailViewModelFactory(factory: RoomDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
 
+    @Binds
+    @IntoMap
+    @MavericksViewModelKey(MessageComposerViewModel::class)
+    fun messageComposerViewModelFactory(factory: MessageComposerViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
     @Binds
     @IntoMap
     @MavericksViewModelKey(SetIdentityServerViewModel::class)
diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
index 429b88be69..350e1f6b7a 100644
--- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
@@ -29,6 +29,8 @@ import dagger.hilt.components.SingletonComponent
 import im.vector.app.core.dispatchers.CoroutineDispatchers
 import im.vector.app.core.error.DefaultErrorFormatter
 import im.vector.app.core.error.ErrorFormatter
+import im.vector.app.core.time.Clock
+import im.vector.app.core.time.DefaultClock
 import im.vector.app.features.invite.AutoAcceptInvites
 import im.vector.app.features.invite.CompileTimeAutoAcceptInvites
 import im.vector.app.features.navigation.DefaultNavigator
@@ -66,6 +68,9 @@ abstract class VectorBindModule {
 
     @Binds
     abstract fun bindAutoAcceptInvites(autoAcceptInvites: CompileTimeAutoAcceptInvites): AutoAcceptInvites
+
+    @Binds
+    abstract fun bindDefaultClock(clock: DefaultClock): Clock
 }
 
 @InstallIn(SingletonComponent::class)
diff --git a/vector/src/main/java/im/vector/app/core/time/Clock.kt b/vector/src/main/java/im/vector/app/core/time/Clock.kt
new file mode 100644
index 0000000000..b7b6e88f8d
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/time/Clock.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.time
+
+import javax.inject.Inject
+
+interface Clock {
+    fun epochMillis(): Long
+}
+
+class DefaultClock @Inject constructor() : Clock {
+
+    /**
+     * Provides a UTC epoch in milliseconds
+     *
+     * This value is not guaranteed to be correct with reality
+     * as a User can override the system time and date to any values.
+     */
+    override fun epochMillis(): Long {
+        return System.currentTimeMillis()
+    }
+}
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 d8f1846d62..08a2e6cd9c 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
@@ -87,6 +87,7 @@ import im.vector.app.core.platform.VectorBaseFragment
 import im.vector.app.core.platform.lifecycleAwareLazy
 import im.vector.app.core.platform.showOptimizedSnackbar
 import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.time.Clock
 import im.vector.app.core.ui.views.CurrentCallsView
 import im.vector.app.core.ui.views.CurrentCallsViewPresenter
 import im.vector.app.core.ui.views.FailedMessagesWarningView
@@ -240,7 +241,6 @@ class RoomDetailFragment @Inject constructor(
         autoCompleterFactory: AutoCompleter.Factory,
         private val permalinkHandler: PermalinkHandler,
         private val notificationDrawerManager: NotificationDrawerManager,
-        val messageComposerViewModelFactory: MessageComposerViewModel.Factory,
         private val eventHtmlRenderer: EventHtmlRenderer,
         private val vectorPreferences: VectorPreferences,
         private val colorProvider: ColorProvider,
@@ -251,7 +251,8 @@ class RoomDetailFragment @Inject constructor(
         private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
         private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
         private val callManager: WebRtcCallManager,
-        private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker
+        private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
+        private val clock: Clock
 ) :
         VectorBaseFragment<FragmentRoomDetailBinding>(),
         TimelineEventController.Callback,
@@ -393,8 +394,8 @@ class RoomDetailFragment @Inject constructor(
             when (mode) {
                 is SendMode.Regular -> renderRegularMode(mode.text)
                 is SendMode.Edit    -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
-                is SendMode.Quote -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
-                is SendMode.Reply -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
+                is SendMode.Quote   -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
+                is SendMode.Reply   -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
             }
         }
 
@@ -700,7 +701,7 @@ class RoomDetailFragment @Inject constructor(
                 if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
                     messageComposerViewModel.handle(MessageComposerAction.StartRecordingVoiceMessage)
                     vibrate(requireContext())
-                    updateRecordingUiState(RecordingUiState.Started)
+                    updateRecordingUiState(RecordingUiState.Started(clock.epochMillis()))
                 }
             }
 
@@ -714,7 +715,9 @@ class RoomDetailFragment @Inject constructor(
             }
 
             override fun onVoiceRecordingLocked() {
-                updateRecordingUiState(RecordingUiState.Locked)
+                val startedState = withState(messageComposerViewModel) { it.voiceRecordingUiState as? RecordingUiState.Started }
+                val startTime = startedState?.recordingStartTimestamp ?: clock.epochMillis()
+                updateRecordingUiState(RecordingUiState.Locked(startTime))
             }
 
             override fun onVoiceRecordingEnded() {
@@ -1130,14 +1133,17 @@ class RoomDetailFragment @Inject constructor(
 
     override fun onPause() {
         super.onPause()
-
         notificationDrawerManager.setCurrentRoom(null)
+        voiceMessagePlaybackTracker.unTrack(VoiceMessagePlaybackTracker.RECORDING_ID)
 
-        messageComposerViewModel.handle(MessageComposerAction.SaveDraft(views.composerLayout.text.toString()))
-
-        // We should improve the UX to support going into playback mode when paused and delete the media when the view is destroyed.
-        messageComposerViewModel.handle(MessageComposerAction.EndAllVoiceActions(deleteRecord = false))
-        views.voiceMessageRecorderView.render(RecordingUiState.None)
+        if (withState(messageComposerViewModel) { it.isVoiceRecording } && requireActivity().isChangingConfigurations) {
+            // we're rotating, maintain any active recordings
+        } else {
+            messageComposerViewModel.handle(MessageComposerAction.SaveDraft(views.composerLayout.text.toString()))
+            // We should improve the UX to support going into playback mode when paused and delete the media when the view is destroyed.
+            messageComposerViewModel.handle(MessageComposerAction.EndAllVoiceActions(deleteRecord = false))
+            views.voiceMessageRecorderView.render(RecordingUiState.None)
+        }
     }
 
     private val attachmentFileActivityResultLauncher = registerStartForActivityResult {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
index f4bfb32fce..16a2d16a50 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
@@ -16,13 +16,13 @@
 
 package im.vector.app.features.home.room.detail.composer
 
-import com.airbnb.mvrx.FragmentViewModelContext
 import com.airbnb.mvrx.MavericksViewModelFactory
-import com.airbnb.mvrx.ViewModelContext
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import im.vector.app.R
+import im.vector.app.core.di.MavericksAssistedViewModelFactory
+import im.vector.app.core.di.hiltMavericksViewModelFactory
 import im.vector.app.core.extensions.exhaustive
 import im.vector.app.core.platform.VectorViewModel
 import im.vector.app.core.resources.StringProvider
@@ -30,7 +30,6 @@ import im.vector.app.features.attachments.toContentAttachmentData
 import im.vector.app.features.command.CommandParser
 import im.vector.app.features.command.ParsedCommand
 import im.vector.app.features.home.room.detail.ChatEffect
-import im.vector.app.features.home.room.detail.RoomDetailFragment
 import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
 import im.vector.app.features.home.room.detail.toMessageType
 import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
@@ -764,23 +763,9 @@ class MessageComposerViewModel @AssistedInject constructor(
     }
 
     @AssistedFactory
-    interface Factory {
-        fun create(initialState: MessageComposerViewState): MessageComposerViewModel
+    interface Factory : MavericksAssistedViewModelFactory<MessageComposerViewModel, MessageComposerViewState> {
+        override fun create(initialState: MessageComposerViewState): MessageComposerViewModel
     }
 
-    /**
-     * We're unable to create this ViewModel with `by hiltMavericksViewModelFactory()` due to the
-     * VoiceMessagePlaybackTracker being ActivityScoped
-     *
-     * This factory allows us to provide the ViewModel instance from the Fragment directly
-     * bypassing the Singleton scope requirement
-     */
-    companion object : MavericksViewModelFactory<MessageComposerViewModel, MessageComposerViewState> {
-
-        @JvmStatic
-        override fun create(viewModelContext: ViewModelContext, state: MessageComposerViewState): MessageComposerViewModel {
-            val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment()
-            return fragment.messageComposerViewModelFactory.create(state)
-        }
-    }
+    companion object : MavericksViewModelFactory<MessageComposerViewModel, MessageComposerViewState> by hiltMavericksViewModelFactory()
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt
index 0df093c661..f9c32d3194 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt
@@ -54,8 +54,8 @@ data class MessageComposerViewState(
         VoiceMessageRecorderView.RecordingUiState.None,
         VoiceMessageRecorderView.RecordingUiState.Cancelled,
         VoiceMessageRecorderView.RecordingUiState.Playback -> false
-        VoiceMessageRecorderView.RecordingUiState.Locked,
-        VoiceMessageRecorderView.RecordingUiState.Started  -> true
+        is VoiceMessageRecorderView.RecordingUiState.Locked,
+        is VoiceMessageRecorderView.RecordingUiState.Started  -> true
     }
 
     val isVoiceMessageIdle = !isVoiceRecording
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 14d5a58279..0989337264 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
@@ -20,19 +20,23 @@ import android.content.Context
 import android.util.AttributeSet
 import android.view.View
 import androidx.constraintlayout.widget.ConstraintLayout
+import dagger.hilt.android.AndroidEntryPoint
 import im.vector.app.BuildConfig
 import im.vector.app.R
 import im.vector.app.core.extensions.exhaustive
 import im.vector.app.core.hardware.vibrate
+import im.vector.app.core.time.Clock
 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 javax.inject.Inject
 import kotlin.math.floor
 
 /**
  * Encapsulates the voice message recording view and animations.
  */
+@AndroidEntryPoint
 class VoiceMessageRecorderView @JvmOverloads constructor(
         context: Context,
         attrs: AttributeSet? = null,
@@ -51,6 +55,8 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
         fun onRecordingWaveformClicked()
     }
 
+    @Inject lateinit var clock: Clock
+
     // We need to define views as lateinit var to be able to check if initialized for the bug fix on api 21 and 22.
     @Suppress("UNNECESSARY_LATEINIT")
     private lateinit var voiceMessageViews: VoiceMessageViews
@@ -105,32 +111,35 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
 
     fun render(recordingState: RecordingUiState) {
         if (lastKnownState == recordingState) return
-        lastKnownState = recordingState
         when (recordingState) {
-            RecordingUiState.None      -> {
+            RecordingUiState.None       -> {
                 reset()
             }
-            RecordingUiState.Started   -> {
-                startRecordingTicker()
+            is RecordingUiState.Started -> {
+                startRecordingTicker(startFromLocked = false, startAt = recordingState.recordingStartTimestamp)
                 voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast))
                 voiceMessageViews.showRecordingViews()
                 dragState = DraggingState.Ready
             }
-            RecordingUiState.Cancelled -> {
+            RecordingUiState.Cancelled  -> {
                 reset()
                 vibrate(context)
             }
-            RecordingUiState.Locked    -> {
+            is RecordingUiState.Locked  -> {
+                if (lastKnownState == null) {
+                    startRecordingTicker(startFromLocked = true, startAt = recordingState.recordingStartTimestamp)
+                }
                 voiceMessageViews.renderLocked()
                 postDelayed({
                     voiceMessageViews.showRecordingLockedViews(recordingState)
                 }, 500)
             }
-            RecordingUiState.Playback  -> {
+            RecordingUiState.Playback   -> {
                 stopRecordingTicker()
                 voiceMessageViews.showPlaybackViews()
             }
         }
+        lastKnownState = recordingState
     }
 
     private fun reset() {
@@ -140,6 +149,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
     }
 
     private fun onDrag(currentDragState: DraggingState, newDragState: DraggingState) {
+        if (currentDragState == newDragState) return
         when (newDragState) {
             is DraggingState.Cancelling -> voiceMessageViews.renderCancelling(newDragState.distanceX)
             is DraggingState.Locking    -> {
@@ -158,22 +168,23 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
         dragState = newDragState
     }
 
-    private fun startRecordingTicker() {
+    private fun startRecordingTicker(startFromLocked: Boolean, startAt: Long) {
+        val startMs = ((clock.epochMillis() - startAt)).coerceAtLeast(0)
         recordingTicker?.stop()
         recordingTicker = CountUpTimer().apply {
             tickListener = object : CountUpTimer.TickListener {
                 override fun onTick(milliseconds: Long) {
-                    onRecordingTick(milliseconds)
+                    val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked
+                    onRecordingTick(isLocked, milliseconds + startMs)
                 }
             }
             resume()
         }
-        onRecordingTick(0L)
+        onRecordingTick(startFromLocked, milliseconds = startMs)
     }
 
-    private fun onRecordingTick(milliseconds: Long) {
-        val currentState = lastKnownState ?: return
-        voiceMessageViews.renderRecordingTimer(currentState, milliseconds / 1_000)
+    private fun onRecordingTick(isLocked: Boolean, milliseconds: Long) {
+        voiceMessageViews.renderRecordingTimer(isLocked, milliseconds / 1_000)
         val timeDiffToRecordingLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS - milliseconds
         if (timeDiffToRecordingLimit <= 0) {
             post {
@@ -210,9 +221,9 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
 
     sealed interface RecordingUiState {
         object None : RecordingUiState
-        object Started : RecordingUiState
+        data class Started(val recordingStartTimestamp: Long) : RecordingUiState
         object Cancelled : RecordingUiState
-        object Locked : RecordingUiState
+        data class Locked(val recordingStartTimestamp: Long) : RecordingUiState
         object Playback : RecordingUiState
     }
 
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 32f21a3177..e138e14261 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
@@ -154,7 +154,7 @@ class VoiceMessageViews(
 
     fun hideRecordingViews(recordingState: RecordingUiState) {
         // We need to animate the lock image first
-        if (recordingState != RecordingUiState.Locked) {
+        if (recordingState !is RecordingUiState.Locked) {
             views.voiceMessageLockImage.isVisible = false
             views.voiceMessageLockImage.animate().translationY(0f).start()
             views.voiceMessageLockBackground.isVisible = false
@@ -171,7 +171,7 @@ class VoiceMessageViews(
         views.voiceMessageTimerIndicator.isVisible = false
         views.voiceMessageTimer.isVisible = false
 
-        if (recordingState != RecordingUiState.Locked) {
+        if (recordingState !is RecordingUiState.Locked) {
             views.voiceMessageMicButton
                     .animate()
                     .scaleX(1f)
@@ -304,9 +304,9 @@ class VoiceMessageViews(
         views.voiceMessageToast.isVisible = false
     }
 
-    fun renderRecordingTimer(recordingState: RecordingUiState, recordingTimeMillis: Long) {
+    fun renderRecordingTimer(isLocked: Boolean, recordingTimeMillis: Long) {
         val formattedTimerText = DateUtils.formatElapsedTime(recordingTimeMillis)
-        if (recordingState == RecordingUiState.Locked) {
+        if (isLocked) {
             views.voicePlaybackTime.apply {
                 post {
                     text = formattedTimerText
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt
index 2e8f6d9336..86cc792e7b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/VoiceMessagePlaybackTracker.kt
@@ -18,10 +18,10 @@ package im.vector.app.features.home.room.detail.timeline.helper
 
 import android.os.Handler
 import android.os.Looper
-import dagger.hilt.android.scopes.ActivityScoped
 import javax.inject.Inject
+import javax.inject.Singleton
 
-@ActivityScoped
+@Singleton
 class VoiceMessagePlaybackTracker @Inject constructor() {
 
     private val mainHandler = Handler(Looper.getMainLooper())