diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt
index 7d9f24c5f..95a6ce254 100644
--- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt
+++ b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt
@@ -37,6 +37,7 @@ import com.nextcloud.talk.components.filebrowser.webdav.DavUtils
 import com.nextcloud.talk.dagger.modules.BusModule
 import com.nextcloud.talk.dagger.modules.ContextModule
 import com.nextcloud.talk.dagger.modules.DatabaseModule
+import com.nextcloud.talk.dagger.modules.ManagerModule
 import com.nextcloud.talk.dagger.modules.RepositoryModule
 import com.nextcloud.talk.dagger.modules.RestModule
 import com.nextcloud.talk.dagger.modules.UtilsModule
@@ -77,7 +78,8 @@ import javax.inject.Singleton
         ViewModelModule::class,
         RepositoryModule::class,
         UtilsModule::class,
-        ThemeModule::class
+        ThemeModule::class,
+        ManagerModule::class
     ]
 )
 @Singleton
diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
index 4fdb36f59..0236e0e29 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
@@ -14,79 +14,50 @@
 package com.nextcloud.talk.chat
 
 import android.Manifest
-import android.animation.ObjectAnimator
 import android.annotation.SuppressLint
 import android.app.Activity
-import android.content.BroadcastReceiver
 import android.content.ClipData
 import android.content.ClipboardManager
 import android.content.Context
 import android.content.Intent
-import android.content.IntentFilter
 import android.content.pm.PackageManager
 import android.content.res.AssetFileDescriptor
-import android.content.res.Resources
 import android.database.Cursor
 import android.graphics.drawable.BitmapDrawable
 import android.graphics.drawable.ColorDrawable
 import android.graphics.drawable.Drawable
-import android.media.AudioFocusRequest
-import android.media.AudioFormat
-import android.media.AudioManager
-import android.media.AudioRecord
 import android.media.MediaPlayer
-import android.media.MediaRecorder
 import android.net.Uri
 import android.os.Build
 import android.os.Bundle
-import android.os.CountDownTimer
 import android.os.Handler
-import android.os.SystemClock
 import android.provider.ContactsContract
 import android.provider.MediaStore
-import android.text.Editable
-import android.text.InputFilter
 import android.text.SpannableStringBuilder
 import android.text.TextUtils
-import android.text.TextWatcher
 import android.util.Log
-import android.util.TypedValue
 import android.view.Gravity
 import android.view.Menu
 import android.view.MenuItem
-import android.view.MotionEvent
 import android.view.View
-import android.view.View.OnTouchListener
 import android.view.animation.AccelerateDecelerateInterpolator
-import android.view.animation.AccelerateInterpolator
-import android.view.animation.AlphaAnimation
-import android.view.animation.Animation
-import android.view.animation.LinearInterpolator
 import android.widget.AbsListView
 import android.widget.FrameLayout
-import android.widget.ImageButton
 import android.widget.ImageView
-import android.widget.LinearLayout
 import android.widget.PopupMenu
-import android.widget.RelativeLayout
-import android.widget.RelativeLayout.BELOW
-import android.widget.RelativeLayout.LayoutParams
-import android.widget.SeekBar
 import android.widget.TextView
 import androidx.activity.OnBackPressedCallback
 import androidx.activity.result.ActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.appcompat.view.ContextThemeWrapper
-import androidx.core.content.ContextCompat
 import androidx.core.content.FileProvider
 import androidx.core.content.PermissionChecker
 import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
 import androidx.core.graphics.drawable.toBitmap
 import androidx.core.text.bold
-import androidx.core.widget.doAfterTextChanged
 import androidx.emoji2.text.EmojiCompat
-import androidx.emoji2.widget.EmojiTextView
 import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.commit
 import androidx.lifecycle.ViewModelProvider
 import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.LinearLayoutManager
@@ -97,13 +68,10 @@ import androidx.work.WorkInfo
 import androidx.work.WorkManager
 import autodagger.AutoInjector
 import coil.imageLoader
-import coil.load
 import coil.request.CachePolicy
 import coil.request.ImageRequest
 import coil.target.Target
 import coil.transform.CircleCropTransformation
-import com.google.android.flexbox.FlexboxLayout
-import com.google.android.material.button.MaterialButton
 import com.google.android.material.snackbar.Snackbar
 import com.nextcloud.android.common.ui.theme.utils.ColorRole
 import com.nextcloud.talk.BuildConfig
@@ -136,8 +104,8 @@ import com.nextcloud.talk.adapters.messages.UnreadNoticeMessageViewHolder
 import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
 import com.nextcloud.talk.api.NcApi
 import com.nextcloud.talk.application.NextcloudTalkApplication
-import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
 import com.nextcloud.talk.chat.viewmodels.ChatViewModel
+import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
 import com.nextcloud.talk.conversationinfo.ConversationInfoActivity
 import com.nextcloud.talk.conversationlist.ConversationsListActivity
 import com.nextcloud.talk.data.user.model.User
@@ -159,19 +127,14 @@ import com.nextcloud.talk.models.json.capabilities.SpreedCapability
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.chat.ChatOverall
 import com.nextcloud.talk.models.json.chat.ReadStatus
-import com.nextcloud.talk.models.json.mention.Mention
-import com.nextcloud.talk.models.json.signaling.NCSignalingMessage
 import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
-import com.nextcloud.talk.presenters.MentionAutocompletePresenter
 import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
 import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
 import com.nextcloud.talk.signaling.SignalingMessageReceiver
 import com.nextcloud.talk.signaling.SignalingMessageSender
 import com.nextcloud.talk.translate.ui.TranslateActivity
-import com.nextcloud.talk.ui.MicInputCloud
 import com.nextcloud.talk.ui.StatusDrawable
 import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
-import com.nextcloud.talk.ui.dialog.AttachmentDialog
 import com.nextcloud.talk.ui.dialog.DateTimePickerFragment
 import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
 import com.nextcloud.talk.ui.dialog.MessageActionsDialog
@@ -182,7 +145,6 @@ import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.AudioUtils
 import com.nextcloud.talk.utils.CapabilitiesUtil
-import com.nextcloud.talk.utils.CharPolicy
 import com.nextcloud.talk.utils.ContactUtils
 import com.nextcloud.talk.utils.ConversationUtils
 import com.nextcloud.talk.utils.DateConstants
@@ -190,7 +152,6 @@ import com.nextcloud.talk.utils.DateUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.FileUtils
 import com.nextcloud.talk.utils.FileViewerUtils
-import com.nextcloud.talk.utils.ImageEmojiEditText
 import com.nextcloud.talk.utils.Mimetype
 import com.nextcloud.talk.utils.NotificationUtils
 import com.nextcloud.talk.utils.ParticipantPermissions
@@ -211,7 +172,6 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM
 import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
 import com.nextcloud.talk.utils.rx.DisposableSet
 import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
-import com.nextcloud.talk.utils.text.Spans
 import com.nextcloud.talk.webrtc.WebSocketConnectionHelper
 import com.nextcloud.talk.webrtc.WebSocketInstance
 import com.otaliastudios.autocomplete.Autocomplete
@@ -221,8 +181,6 @@ import com.stfalcon.chatkit.messages.MessageHolders
 import com.stfalcon.chatkit.messages.MessageHolders.ContentChecker
 import com.stfalcon.chatkit.messages.MessagesListAdapter
 import com.stfalcon.chatkit.utils.DateFormatter
-import com.vanniktech.emoji.EmojiPopup
-import io.reactivex.subjects.BehaviorSubject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -237,12 +195,9 @@ import java.net.HttpURLConnection
 import java.text.SimpleDateFormat
 import java.util.Date
 import java.util.Locale
-import java.util.Objects
 import java.util.concurrent.ExecutionException
 import javax.inject.Inject
 import kotlin.collections.set
-import kotlin.math.abs
-import kotlin.math.log10
 import kotlin.math.roundToInt
 
 @AutoInjector(NextcloudTalkApplication::class)
@@ -275,8 +230,7 @@ class ChatActivity :
     lateinit var viewModelFactory: ViewModelProvider.Factory
 
     lateinit var chatViewModel: ChatViewModel
-
-    private lateinit var editMessage: ChatMessage
+    lateinit var messageInputViewModel: MessageInputViewModel
 
     private val startSelectContactForResult = registerForActivityResult(
         ActivityResultContracts
@@ -334,7 +288,7 @@ class ChatActivity :
     private var globalLastKnownFutureMessageId = -1
     private var globalLastKnownPastMessageId = -1
     var adapter: TalkMessagesListAdapter<ChatMessage>? = null
-    private var mentionAutocomplete: Autocomplete<*>? = null
+    var mentionAutocomplete: Autocomplete<*>? = null
     var layoutManager: LinearLayoutManager? = null
     var pullChatMessagesPending = false
     var newMessagesCount = 0
@@ -343,86 +297,32 @@ class ChatActivity :
     lateinit var roomId: String
     var voiceOnly: Boolean = true
     var isFirstMessagesProcessing = true
-    private var emojiPopup: EmojiPopup? = null
     private lateinit var path: String
 
     var myFirstMessage: CharSequence? = null
     var checkingLobbyStatus: Boolean = false
 
-    private var conversationInfoMenuItem: MenuItem? = null
     private var conversationVoiceCallMenuItem: MenuItem? = null
     private var conversationVideoMenuItem: MenuItem? = null
-    private var conversationSharedItemsItem: MenuItem? = null
 
-    private var webSocketInstance: WebSocketInstance? = null
-    private var signalingMessageSender: SignalingMessageSender? = null
+    var webSocketInstance: WebSocketInstance? = null
+    var signalingMessageSender: SignalingMessageSender? = null
 
     var getRoomInfoTimerHandler: Handler? = null
-    var pastPreconditionFailed = false
-    var futurePreconditionFailed = false
 
     private val filesToUpload: MutableList<String> = ArrayList()
-    private lateinit var sharedText: String
-    var currentVoiceRecordFile: String = ""
-    var isVoiceRecordingLocked: Boolean = false
-    private var isVoicePreviewPlaying: Boolean = false
-
-    private var recorder: MediaRecorder? = null
-
-    private enum class MediaRecorderState {
-        INITIAL,
-        INITIALIZED,
-        CONFIGURED,
-        PREPARED,
-        RECORDING,
-        RELEASED,
-        ERROR
-    }
-
-    private val editableBehaviorSubject = BehaviorSubject.createDefault(false)
-    private val editedTextBehaviorSubject = BehaviorSubject.createDefault("")
-
-    private var mediaRecorderState: MediaRecorderState = MediaRecorderState.INITIAL
-
-    private var voicePreviewMediaPlayer: MediaPlayer? = null
-    private var voicePreviewObjectAnimator: ObjectAnimator? = null
+    lateinit var sharedText: String
 
     var mediaPlayer: MediaPlayer? = null
     var mediaPlayerHandler: Handler? = null
 
     private var currentlyPlayedVoiceMessage: ChatMessage? = null
 
-    private lateinit var micInputAudioRecorder: AudioRecord
-    private var micInputAudioRecordThread: Thread? = null
-    private var isMicInputAudioThreadRunning: Boolean = false
-    private val bufferSize = AudioRecord.getMinBufferSize(
-        SAMPLE_RATE,
-        AudioFormat.CHANNEL_IN_MONO,
-        AudioFormat.ENCODING_PCM_16BIT
-    )
-
-    private var voiceRecordDuration = 0L
-    private var voiceRecordPauseTime = 0L
-
     // messy workaround for a mediaPlayer bug, don't delete
     private var lastRecordMediaPosition: Int = 0
     private var lastRecordedSeeked: Boolean = false
 
-    private val audioFocusChangeListener = getAudioFocusChangeListener()
-
-    private val noisyAudioStreamReceiver = object : BroadcastReceiver() {
-        override fun onReceive(context: Context?, intent: Intent?) {
-            chatViewModel.isPausedDueToBecomingNoisy = true
-            if (isVoicePreviewPlaying) {
-                pausePreviewVoicePlaying()
-            }
-            if (currentlyPlayedVoiceMessage != null) {
-                pausePlayback(currentlyPlayedVoiceMessage!!)
-            }
-        }
-    }
-
-    private lateinit var participantPermissions: ParticipantPermissions
+    lateinit var participantPermissions: ParticipantPermissions
 
     private var videoURI: Uri? = null
 
@@ -437,8 +337,6 @@ class ChatActivity :
         }
     }
 
-    var typingTimer: CountDownTimer? = null
-    var typedWhileTypingTimerIsRunning: Boolean = false
     val typingParticipants = HashMap<String, TypingParticipant>()
 
     var callStarted = false
@@ -505,13 +403,12 @@ class ChatActivity :
         setContentView(binding.root)
         setupSystemColors()
 
-        binding.messageInputView.messageSendButton.visibility = View.GONE
-
         conversationUser = currentUserProvider.currentUser.blockingGet()
 
         handleIntent(intent)
 
         chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java]
+        messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java]
 
         binding.progressBar.visibility = View.VISIBLE
 
@@ -593,14 +490,8 @@ class ChatActivity :
     override fun onStart() {
         super.onStart()
         active = true
-        context.getSharedPreferences(localClassName, MODE_PRIVATE).apply {
-            val text = getString(roomToken, "")
-            val cursor = getInt(roomToken + CURSOR_KEY, 0)
-            binding.messageInputView.messageInput.setText(text)
-            binding.messageInputView.messageInput.setSelection(cursor)
-        }
         this.lifecycle.addObserver(AudioUtils)
-        this.lifecycle.addObserver(ChatViewModel.LifeCycleObserver)
+        this.lifecycle.addObserver(chatViewModel)
     }
 
     override fun onSaveInstanceState(outState: Bundle) {
@@ -621,25 +512,8 @@ class ChatActivity :
     override fun onStop() {
         super.onStop()
         active = false
-        stopPreviewVoicePlaying()
-        if (isMicInputAudioThreadRunning) {
-            stopMicInputRecordingAnimation()
-        }
-        if (mediaRecorderState == MediaRecorderState.RECORDING) {
-            stopAudioRecording()
-        }
-        val text = binding.messageInputView.messageInput.text.toString()
-        val cursor = binding.messageInputView.messageInput.selectionStart
-        val previous = context.getSharedPreferences(localClassName, MODE_PRIVATE).getString(roomToken, "null")
-        if (text != previous) {
-            context.getSharedPreferences(localClassName, MODE_PRIVATE).edit().apply {
-                putString(roomToken, text)
-                putInt(roomToken + CURSOR_KEY, cursor)
-                apply()
-            }
-        }
         this.lifecycle.removeObserver(AudioUtils)
-        this.lifecycle.removeObserver(ChatViewModel.LifeCycleObserver)
+        this.lifecycle.removeObserver(chatViewModel)
     }
 
     @SuppressLint("NotifyDataSetChanged")
@@ -664,57 +538,46 @@ class ChatActivity :
 
         chatViewModel.getCapabilitiesViewState.observe(this) { state ->
             when (state) {
-                is ChatViewModel.GetCapabilitiesSuccessState -> {
+                is ChatViewModel.GetCapabilitiesUpdateState -> {
                     spreedCapabilities = state.spreedCapabilities
                     chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
+                    participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!)
 
                     invalidateOptionsMenu()
-                    initMessageInputView()
+                    checkShowCallButtons()
+                    checkShowMessageInputView()
+                    checkLobbyState()
+                    updateRoomTimerHandler()
+                }
+
+                is ChatViewModel.GetCapabilitiesInitialLoadState -> {
+                    spreedCapabilities = state.spreedCapabilities
+                    chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
+                    participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!)
+
+                    supportFragmentManager.commit {
+                        setReorderingAllowed(true) // optimizes out redundant replace operations
+                        replace(R.id.fragment_container_activity_chat, MessageInputFragment())
+                    }
+
+                    joinRoomWithPassword()
 
                     if (conversationUser?.userId != "?" &&
                         CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)
                     ) {
-                        binding.chatToolbar.setOnClickListener { v -> showConversationInfoScreen() }
+                        binding.chatToolbar.setOnClickListener { _ -> showConversationInfoScreen() }
                     }
 
                     if (adapter == null) {
                         initAdapter()
                         binding.messagesListView.setAdapter(adapter)
+                        layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager?
                     }
 
-                    layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager?
-
                     loadAvatarForStatusBar()
-                    setActionBarTitle()
-                    participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!)
-
                     setupSwipeToReply()
-                    setupMentionAutocomplete()
-                    checkShowCallButtons()
-                    checkShowMessageInputView()
-                    checkLobbyState()
-
-                    if (!validSessionId()) {
-                        joinRoomWithPassword()
-                    } else {
-                        Log.d(TAG, "already inConversation. joinRoomWithPassword is skipped")
-                    }
-
-                    val delayForRecursiveCall = if (shouldShowLobby()) {
-                        GET_ROOM_INFO_DELAY_LOBBY
-                    } else {
-                        GET_ROOM_INFO_DELAY_NORMAL
-                    }
-
-                    if (getRoomInfoTimerHandler == null) {
-                        getRoomInfoTimerHandler = Handler()
-                    }
-                    getRoomInfoTimerHandler?.postDelayed(
-                        {
-                            chatViewModel.getRoom(conversationUser!!, roomToken)
-                        },
-                        delayForRecursiveCall
-                    )
+                    setActionBarTitle()
+                    updateRoomTimerHandler()
 
                     chatViewModel.refreshChatParams(
                         setupFieldsForPullChatMessages(
@@ -776,8 +639,6 @@ class ChatActivity :
                 is ChatViewModel.LeaveRoomSuccessState -> {
                     logConversationInfos("leaveRoom#onNext")
 
-                    sendStopTypingMessage()
-
                     checkingLobbyStatus = false
 
                     if (getRoomInfoTimerHandler != null) {
@@ -803,9 +664,9 @@ class ChatActivity :
             }
         }
 
-        chatViewModel.sendChatMessageViewState.observe(this) { state ->
+        messageInputViewModel.sendChatMessageViewState.observe(this) { state ->
             when (state) {
-                is ChatViewModel.SendChatMessageSuccessState -> {
+                is MessageInputViewModel.SendChatMessageSuccessState -> {
                     myFirstMessage = state.message
 
                     if (binding.popupBubbleView.isShown == true) {
@@ -814,7 +675,7 @@ class ChatActivity :
                     binding.messagesListView.smoothScrollToPosition(0)
                 }
 
-                is ChatViewModel.SendChatMessageErrorState -> {
+                is MessageInputViewModel.SendChatMessageErrorState -> {
                     if (state.e is HttpException) {
                         val code = state.e.code()
                         if (code.toString().startsWith("2")) {
@@ -895,15 +756,19 @@ class ChatActivity :
                             val chatOverall = state.response.body() as ChatOverall?
                             var chatMessageList = chatOverall?.ocs!!.data!!
 
+                            val newXChatLastCommonRead = state.response.headers()["X-Chat-Last-Common-Read"]?.let {
+                                Integer.parseInt(it)
+                            }
+
                             processHeaderChatLastGiven(state.response, state.lookIntoFuture)
 
                             chatMessageList = handleSystemMessages(chatMessageList)
 
-                            if (chatMessageList.size == 0) {
+                            if (chatMessageList.isEmpty()) {
                                 chatViewModel.refreshChatParams(
                                     setupFieldsForPullChatMessages(
                                         true,
-                                        globalLastKnownFutureMessageId,
+                                        newXChatLastCommonRead,
                                         true
                                     )
                                 )
@@ -935,10 +800,6 @@ class ChatActivity :
                                 collapseSystemMessages()
                             }
 
-                            val newXChatLastCommonRead = state.response.headers()["X-Chat-Last-Common-Read"]?.let {
-                                Integer.parseInt(it)
-                            }
-
                             updateReadStatusOfAllMessages(newXChatLastCommonRead)
 
                             processCallStartedMessages(chatMessageList)
@@ -958,7 +819,7 @@ class ChatActivity :
                             chatViewModel.refreshChatParams(
                                 setupFieldsForPullChatMessages(
                                     true,
-                                    globalLastKnownFutureMessageId,
+                                    globalLastKnownPastMessageId,
                                     true
                                 )
                             )
@@ -968,7 +829,7 @@ class ChatActivity :
                             chatViewModel.refreshChatParams(
                                 setupFieldsForPullChatMessages(
                                     true,
-                                    globalLastKnownFutureMessageId,
+                                    globalLastKnownPastMessageId,
                                     true
                                 )
                             )
@@ -1026,9 +887,9 @@ class ChatActivity :
             }
         }
 
-        chatViewModel.editMessageViewState.observe(this) { state ->
+        messageInputViewModel.editMessageViewState.observe(this) { state ->
             when (state) {
-                is ChatViewModel.EditMessageSuccessState -> {
+                is MessageInputViewModel.EditMessageSuccessState -> {
                     when (state.messageEdited.ocs?.meta?.statusCode) {
                         HTTP_BAD_REQUEST -> {
                             Snackbar.make(
@@ -1054,16 +915,46 @@ class ChatActivity :
                             ).show()
                         }
                     }
-                    clearEditUI()
                 }
 
-                is ChatViewModel.EditMessageErrorState -> {
+                is MessageInputViewModel.EditMessageErrorState -> {
                     Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
                 }
 
                 else -> {}
             }
         }
+
+        chatViewModel.getVoiceRecordingLocked.observe(this) { showContiniousVoiceRecording ->
+            if (showContiniousVoiceRecording) {
+                binding.voiceRecordingLock.visibility = View.GONE
+                supportFragmentManager.commit {
+                    setReorderingAllowed(true) // apparently used for optimizations
+                    replace(R.id.fragment_container_activity_chat, MessageInputVoiceRecordingFragment())
+                }
+            } else {
+                supportFragmentManager.commit {
+                    setReorderingAllowed(true)
+                    replace(R.id.fragment_container_activity_chat, MessageInputFragment())
+                }
+            }
+        }
+
+        chatViewModel.getVoiceRecordingInProgress.observe(this) { voiceRecordingInProgress ->
+            VibrationUtils.vibrateShort(context)
+            binding.voiceRecordingLock.visibility = if (
+                voiceRecordingInProgress &&
+                chatViewModel.getVoiceRecordingLocked.value != true
+            ) {
+                View.VISIBLE
+            } else {
+                View.GONE
+            }
+        }
+
+        chatViewModel.recordTouchObserver.observe(this) { y ->
+            binding.voiceRecordingLock.y -= y
+        }
     }
 
     @Suppress("Detekt.TooGenericExceptionCaught")
@@ -1078,10 +969,6 @@ class ChatActivity :
         webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener)
         webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener)
 
-        initSmileyKeyboardToggler()
-
-        themeMessageInputView()
-
         cancelNotificationsForCurrentConversation()
 
         chatViewModel.getRoom(conversationUser!!, roomToken)
@@ -1119,8 +1006,6 @@ class ChatActivity :
 
         binding.let { viewThemeUtils.material.colorMaterialButtonPrimaryFilled(it.popupBubbleView) }
 
-        binding.messageInputView.setPadding(0, 0, 0, 0)
-
         binding.messagesListView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
             override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                 super.onScrollStateChanged(recyclerView, newState)
@@ -1150,116 +1035,6 @@ class ChatActivity :
         viewThemeUtils.material.colorToolbarOverflowIcon(binding.chatToolbar)
     }
 
-    private fun initMessageInputView() {
-        if (binding.messageInputView.inputEditText?.filters?.isEmpty() == true) {
-            val filters = arrayOfNulls<InputFilter>(1)
-            val lengthFilter = CapabilitiesUtil.getMessageMaxLength(spreedCapabilities)
-
-            filters[0] = InputFilter.LengthFilter(lengthFilter)
-            binding.messageInputView.inputEditText?.filters = filters
-
-            binding.messageInputView.inputEditText?.addTextChangedListener(object : TextWatcher {
-
-                override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
-                    // unused atm
-                }
-
-                @Suppress("Detekt.TooGenericExceptionCaught")
-                override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
-                    updateOwnTypingStatus(s)
-
-                    if (s.length >= lengthFilter) {
-                        binding.messageInputView.inputEditText?.error = String.format(
-                            Objects.requireNonNull<Resources>(resources).getString(R.string.nc_limit_hit),
-                            lengthFilter.toString()
-                        )
-                    } else {
-                        binding.messageInputView.inputEditText?.error = null
-                    }
-
-                    val editable = binding.messageInputView.inputEditText?.editableText
-                    editedTextBehaviorSubject.onNext(editable.toString().trim())
-
-                    if (editable != null && binding.messageInputView.inputEditText != null) {
-                        val mentionSpans = editable.getSpans(
-                            0,
-                            binding.messageInputView.inputEditText!!.length(),
-                            Spans.MentionChipSpan::class.java
-                        )
-                        var mentionSpan: Spans.MentionChipSpan
-                        for (i in mentionSpans.indices) {
-                            mentionSpan = mentionSpans[i]
-                            if (start >= editable.getSpanStart(mentionSpan) &&
-                                start < editable.getSpanEnd(mentionSpan)
-                            ) {
-                                if (editable.subSequence(
-                                        editable.getSpanStart(mentionSpan),
-                                        editable.getSpanEnd(mentionSpan)
-                                    ).toString().trim { it <= ' ' } != mentionSpan.label
-                                ) {
-                                    editable.removeSpan(mentionSpan)
-                                }
-                            }
-                        }
-                    }
-                }
-
-                override fun afterTextChanged(s: Editable) {
-                    // unused atm
-                }
-            })
-
-            // Image keyboard support
-            // See: https://developer.android.com/guide/topics/text/image-keyboard
-
-            (binding.messageInputView.inputEditText as ImageEmojiEditText).onCommitContentListener = {
-                uploadFile(it.toString(), false)
-            }
-            initVoiceRecordButton()
-
-            if (sharedText.isNotEmpty()) {
-                binding.messageInputView.inputEditText?.setText(sharedText)
-            }
-
-            binding.messageInputView.setAttachmentsListener {
-                AttachmentDialog(this, this).show()
-            }
-
-            binding.messageInputView.button?.setOnClickListener {
-                submitMessage(false)
-            }
-
-            if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_SEND)) {
-                binding.messageInputView.button?.setOnLongClickListener {
-                    showSendButtonMenu()
-                    true
-                }
-            }
-
-            binding.messageInputView.button?.contentDescription =
-                resources?.getString(R.string.nc_description_send_message_button)
-        }
-    }
-
-    private fun editMessageAPI(message: ChatMessage, editedMessageText: String) {
-        var apiVersion = 1
-        // FIXME Fix API checking with guests?
-        if (conversationUser != null) {
-            apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
-        }
-
-        chatViewModel.editChatMessage(
-            credentials!!,
-            ApiUtils.getUrlForChatMessage(
-                apiVersion,
-                conversationUser?.baseUrl!!,
-                roomToken,
-                message.id
-            ),
-            editedMessageText
-        )
-    }
-
     private fun getLastAdapterId(): Int {
         var lastId = 0
         if (adapter?.items?.size != 0) {
@@ -1273,68 +1048,6 @@ class ChatActivity :
         return lastId
     }
 
-    private fun setEditUI() {
-        binding.messageInputView.messageSendButton.visibility = View.GONE
-        binding.messageInputView.recordAudioButton.visibility = View.GONE
-        binding.messageInputView.editMessageButton.visibility = View.VISIBLE
-        binding.editView.editMessageView.visibility = View.VISIBLE
-        binding.messageInputView.attachmentButton.visibility = View.GONE
-    }
-
-    private fun clearEditUI() {
-        binding.messageInputView.editMessageButton.visibility = View.GONE
-        editableBehaviorSubject.onNext(false)
-        binding.messageInputView.inputEditText.setText("")
-        binding.editView.editMessageView.visibility = View.GONE
-        binding.messageInputView.attachmentButton.visibility = View.VISIBLE
-    }
-
-    private fun themeMessageInputView() {
-        binding.messageInputView.button?.let { viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) }
-
-        binding.messageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.setOnClickListener {
-            cancelReply()
-        }
-
-        binding.messageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.let {
-            viewThemeUtils.platform
-                .themeImageButton(it)
-        }
-
-        binding.messageInputView.findViewById<MaterialButton>(R.id.playPauseBtn)?.let {
-            viewThemeUtils.material.colorMaterialButtonText(it)
-        }
-
-        binding.messageInputView.findViewById<SeekBar>(R.id.seekbar)?.let {
-            viewThemeUtils.platform.themeHorizontalSeekBar(it)
-        }
-
-        binding.messageInputView.findViewById<ImageView>(R.id.deleteVoiceRecording)?.let {
-            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
-        }
-        binding.messageInputView.findViewById<ImageView>(R.id.sendVoiceRecording)?.let {
-            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
-        }
-
-        binding.messageInputView.findViewById<ImageView>(R.id.microphoneEnabledInfo)?.let {
-            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
-        }
-
-        binding.messageInputView.findViewById<LinearLayout>(R.id.voice_preview_container)?.let {
-            viewThemeUtils.talk.themeOutgoingMessageBubble(it, true, false)
-        }
-
-        binding.messageInputView.findViewById<MicInputCloud>(R.id.micInputCloud)?.let {
-            viewThemeUtils.talk.themeMicInputCloud(it)
-        }
-        binding.messageInputView.findViewById<ImageView>(R.id.editMessageButton)?.let {
-            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
-        }
-        binding.editView.clearEdit.let {
-            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
-        }
-    }
-
     private fun setupActionBar() {
         setSupportActionBar(binding.chatToolbar)
         binding.chatToolbar.setNavigationOnClickListener {
@@ -1517,445 +1230,6 @@ class ChatActivity :
         return messageHolders
     }
 
-    @SuppressLint("ClickableViewAccessibility")
-    private fun initVoiceRecordButton() {
-        if (!isVoiceRecordingLocked) {
-            if (!editableBehaviorSubject.value!!) {
-                if (binding.messageInputView.messageInput.text!!.isNotEmpty()) {
-                    showMicrophoneButton(false)
-                } else {
-                    showMicrophoneButton(true)
-                }
-            }
-        } else if (mediaRecorderState == MediaRecorderState.RECORDING) {
-            binding.messageInputView.playPauseBtn.visibility = View.GONE
-            binding.messageInputView.seekBar.visibility = View.GONE
-        } else {
-            showVoiceRecordingLockedInterface(true)
-            showPreviewVoiceRecording(true)
-            stopMicInputRecordingAnimation()
-            binding.messageInputView.micInputCloud.setState(MicInputCloud.ViewState.PAUSED_STATE)
-        }
-
-        isVoicePreviewPlaying = false
-        binding.messageInputView.messageInput.doAfterTextChanged {
-            if (!editableBehaviorSubject.value!!) {
-                if (binding.messageInputView.messageInput.text?.isEmpty() == true) {
-                    showMicrophoneButton(true)
-                } else {
-                    showMicrophoneButton(false)
-                }
-            }
-        }
-
-        var sliderInitX = 0F
-        var downX = 0f
-        var originY = 0f
-        var deltaX: Float
-        var deltaY: Float
-
-        var voiceRecordStartTime = 0L
-        var voiceRecordEndTime = 0L
-
-        // this is so that the seekbar is no longer draggable
-        binding.messageInputView.seekBar.setOnTouchListener(OnTouchListener { _, _ -> true })
-
-        binding.messageInputView.micInputCloud.setOnClickListener {
-            if (mediaRecorderState == MediaRecorderState.RECORDING) {
-                audioFocusRequest(false) {
-                    recorder?.stop()
-                }
-                mediaRecorderState = MediaRecorderState.INITIAL
-                stopMicInputRecordingAnimation()
-                showPreviewVoiceRecording(true)
-            } else {
-                stopPreviewVoicePlaying()
-                initMediaRecorder(currentVoiceRecordFile)
-                startMicInputRecordingAnimation()
-                showPreviewVoiceRecording(false)
-            }
-        }
-
-        binding.messageInputView.deleteVoiceRecording.setOnClickListener {
-            stopAndDiscardAudioRecording()
-            endVoiceRecordingUI()
-            stopMicInputRecordingAnimation()
-            binding.messageInputView.slideToCancelDescription.x = sliderInitX
-        }
-
-        binding.messageInputView.sendVoiceRecording.setOnClickListener {
-            stopAndSendAudioRecording()
-            endVoiceRecordingUI()
-            stopMicInputRecordingAnimation()
-            binding.messageInputView.slideToCancelDescription.x = sliderInitX
-        }
-
-        binding.messageInputView.playPauseBtn.setOnClickListener {
-            Log.d(TAG, "is voice preview playing $isVoicePreviewPlaying")
-            if (isVoicePreviewPlaying) {
-                Log.d(TAG, "Paused")
-                pausePreviewVoicePlaying()
-                binding.messageInputView.playPauseBtn.icon = ContextCompat.getDrawable(
-                    context,
-                    R.drawable
-                        .ic_baseline_play_arrow_voice_message_24
-                )
-                isVoicePreviewPlaying = false
-            } else {
-                Log.d(TAG, "Started")
-                startPreviewVoicePlaying()
-                binding.messageInputView.playPauseBtn.icon = ContextCompat.getDrawable(
-                    context,
-                    R.drawable
-                        .ic_baseline_pause_voice_message_24
-                )
-                isVoicePreviewPlaying = true
-            }
-        }
-
-        binding.messageInputView.recordAudioButton.setOnTouchListener(object : OnTouchListener {
-            override fun onTouch(v: View?, event: MotionEvent?): Boolean {
-                v?.performClick() // ?????????
-                when (event?.action) {
-                    MotionEvent.ACTION_DOWN -> {
-                        if (!isRecordAudioPermissionGranted()) {
-                            requestRecordAudioPermissions()
-                            return true
-                        }
-                        if (!permissionUtil.isFilesPermissionGranted()) {
-                            UploadAndShareFilesWorker.requestStoragePermission(this@ChatActivity)
-                            return true
-                        }
-
-                        voiceRecordStartTime = System.currentTimeMillis()
-
-                        setVoiceRecordFileName()
-                        startAudioRecording(currentVoiceRecordFile)
-                        downX = event.x
-                        originY = event.y
-                        showRecordAudioUi(true)
-                    }
-
-                    MotionEvent.ACTION_CANCEL -> {
-                        Log.d(TAG, "ACTION_CANCEL. same as for UP")
-                        if (mediaRecorderState != MediaRecorderState.RECORDING || !isRecordAudioPermissionGranted()) {
-                            return true
-                        }
-
-                        stopAndDiscardAudioRecording()
-                        endVoiceRecordingUI()
-                        binding.messageInputView.slideToCancelDescription.x = sliderInitX
-                    }
-
-                    MotionEvent.ACTION_UP -> {
-                        Log.d(TAG, "ACTION_UP. stop recording??")
-                        if (mediaRecorderState != MediaRecorderState.RECORDING ||
-                            !isRecordAudioPermissionGranted() ||
-                            isVoiceRecordingLocked
-                        ) {
-                            return true
-                        }
-                        showRecordAudioUi(false)
-
-                        voiceRecordEndTime = System.currentTimeMillis()
-                        voiceRecordDuration = voiceRecordEndTime - voiceRecordStartTime
-                        if (voiceRecordDuration < MINIMUM_VOICE_RECORD_DURATION) {
-                            Log.d(TAG, "voiceRecordDuration: $voiceRecordDuration")
-                            Snackbar.make(
-                                binding.root,
-                                context.getString(R.string.nc_voice_message_hold_to_record_info),
-                                Snackbar.LENGTH_SHORT
-                            ).show()
-                            stopAndDiscardAudioRecording()
-                            return true
-                        } else {
-                            voiceRecordStartTime = 0L
-                            voiceRecordEndTime = 0L
-                            stopAndSendAudioRecording()
-                        }
-
-                        binding.messageInputView.slideToCancelDescription.x = sliderInitX
-                    }
-
-                    MotionEvent.ACTION_MOVE -> {
-                        Log.d(TAG, "ACTION_MOVE.")
-
-                        if (mediaRecorderState != MediaRecorderState.RECORDING || !isRecordAudioPermissionGranted()) {
-                            return true
-                        }
-
-                        showRecordAudioUi(true)
-
-                        val movedX: Float = event.x
-                        val movedY: Float = event.y
-                        deltaX = movedX - downX
-                        deltaY = movedY - originY
-
-                        binding.voiceRecordingLock.translationY.let {
-                            if (it < VOICE_RECORD_LOCK_BUTTON_Y) {
-                                Log.d(TAG, "Voice Recording Locked")
-                                isVoiceRecordingLocked = true
-                                showVoiceRecordingLocked(true)
-                                showVoiceRecordingLockedInterface(true)
-                                startMicInputRecordingAnimation()
-                            } else if (deltaY < 0f) {
-                                binding.voiceRecordingLock.translationY = deltaY
-                            }
-                        }
-
-                        // only allow slide to left
-                        binding.messageInputView.slideToCancelDescription.x.let {
-                            if (sliderInitX == 0.0F) {
-                                sliderInitX = it
-                            }
-
-                            if (it > sliderInitX) {
-                                binding.messageInputView.slideToCancelDescription.x = sliderInitX
-                            }
-                        }
-
-                        binding.messageInputView.slideToCancelDescription.x.let {
-                            if (it < VOICE_RECORD_CANCEL_SLIDER_X) {
-                                Log.d(TAG, "stopping recording because slider was moved to left")
-                                stopAndDiscardAudioRecording()
-                                endVoiceRecordingUI()
-                                binding.messageInputView.slideToCancelDescription.x = sliderInitX
-                                return true
-                            } else {
-                                binding.messageInputView.slideToCancelDescription.x = it + deltaX
-                                downX = movedX
-                            }
-                        }
-                    }
-                }
-
-                return v?.onTouchEvent(event) ?: true
-            }
-        })
-    }
-
-    private fun showPreviewVoiceRecording(value: Boolean) {
-        val micInputCloudLayoutParams: LayoutParams = binding.messageInputView.micInputCloud
-            .layoutParams as LayoutParams
-
-        val deleteVoiceRecordingLayoutParams: LayoutParams = binding.messageInputView.deleteVoiceRecording
-            .layoutParams as LayoutParams
-
-        val sendVoiceRecordingLayoutParams: LayoutParams = binding.messageInputView.sendVoiceRecording
-            .layoutParams as LayoutParams
-
-        if (value) {
-            voiceRecordPauseTime = binding.messageInputView.audioRecordDuration.base - SystemClock.elapsedRealtime()
-            binding.messageInputView.audioRecordDuration.stop()
-            binding.messageInputView.audioRecordDuration.visibility = View.GONE
-            binding.messageInputView.playPauseBtn.visibility = View.VISIBLE
-            binding.messageInputView.playPauseBtn.icon = ContextCompat.getDrawable(
-                context,
-                R.drawable.ic_baseline_play_arrow_voice_message_24
-            )
-            binding.messageInputView.seekBar.visibility = View.VISIBLE
-            binding.messageInputView.seekBar.progress = 0
-            binding.messageInputView.seekBar.max = 0
-            micInputCloudLayoutParams.removeRule(BELOW)
-            micInputCloudLayoutParams.addRule(BELOW, R.id.voice_preview_container)
-            deleteVoiceRecordingLayoutParams.removeRule(BELOW)
-            deleteVoiceRecordingLayoutParams.addRule(BELOW, R.id.voice_preview_container)
-            sendVoiceRecordingLayoutParams.removeRule(BELOW)
-            sendVoiceRecordingLayoutParams.addRule(BELOW, R.id.voice_preview_container)
-        } else {
-            binding.messageInputView.audioRecordDuration.base = SystemClock.elapsedRealtime()
-            binding.messageInputView.audioRecordDuration.start()
-            binding.messageInputView.playPauseBtn.visibility = View.GONE
-            binding.messageInputView.seekBar.visibility = View.GONE
-            binding.messageInputView.audioRecordDuration.visibility = View.VISIBLE
-            micInputCloudLayoutParams.removeRule(BELOW)
-            micInputCloudLayoutParams.addRule(BELOW, R.id.audioRecordDuration)
-            deleteVoiceRecordingLayoutParams.removeRule(BELOW)
-            deleteVoiceRecordingLayoutParams.addRule(BELOW, R.id.audioRecordDuration)
-            sendVoiceRecordingLayoutParams.removeRule(BELOW)
-            sendVoiceRecordingLayoutParams.addRule(BELOW, R.id.audioRecordDuration)
-        }
-    }
-
-    private fun initPreviewVoiceRecording() {
-        voicePreviewMediaPlayer = MediaPlayer().apply {
-            setDataSource(currentVoiceRecordFile)
-            prepare()
-            setOnPreparedListener {
-                binding.messageInputView.seekBar.progress = 0
-                binding.messageInputView.seekBar.max = it.duration
-                voicePreviewObjectAnimator = ObjectAnimator.ofInt(
-                    binding.messageInputView.seekBar,
-                    "progress",
-                    0,
-                    it.duration
-                ).apply {
-                    duration = it.duration.toLong()
-                    interpolator = LinearInterpolator()
-                }
-                audioFocusRequest(true) {
-                    voicePreviewMediaPlayer!!.start()
-                    voicePreviewObjectAnimator!!.start()
-                    handleBecomingNoisyBroadcast(register = true)
-                }
-            }
-
-            setOnCompletionListener {
-                stopPreviewVoicePlaying()
-            }
-        }
-    }
-
-    private fun startPreviewVoicePlaying() {
-        Log.d(TAG, "started preview voice recording")
-        if (voicePreviewMediaPlayer == null) {
-            initPreviewVoiceRecording()
-        } else {
-            audioFocusRequest(true) {
-                voicePreviewMediaPlayer!!.start()
-                voicePreviewObjectAnimator!!.resume()
-                handleBecomingNoisyBroadcast(register = true)
-            }
-        }
-    }
-
-    private fun pausePreviewVoicePlaying() {
-        Log.d(TAG, "paused preview voice recording")
-        audioFocusRequest(false) {
-            voicePreviewMediaPlayer!!.pause()
-            voicePreviewObjectAnimator!!.pause()
-            handleBecomingNoisyBroadcast(register = false)
-        }
-    }
-
-    private fun stopPreviewVoicePlaying() {
-        if (voicePreviewMediaPlayer != null) {
-            isVoicePreviewPlaying = false
-            binding.messageInputView.playPauseBtn.icon = ContextCompat.getDrawable(context, R.drawable.ic_refresh)
-            voicePreviewObjectAnimator!!.end()
-            voicePreviewObjectAnimator = null
-            binding.messageInputView.seekBar.clearAnimation()
-            audioFocusRequest(false) {
-                voicePreviewMediaPlayer!!.stop()
-                voicePreviewMediaPlayer!!.release()
-                voicePreviewMediaPlayer = null
-                handleBecomingNoisyBroadcast(register = false)
-            }
-        }
-    }
-
-    private fun endVoiceRecordingUI() {
-        stopPreviewVoicePlaying()
-        showRecordAudioUi(false)
-        binding.voiceRecordingLock.translationY = 0f
-        isVoiceRecordingLocked = false
-        showVoiceRecordingLocked(false)
-        showVoiceRecordingLockedInterface(false)
-        stopMicInputRecordingAnimation()
-    }
-
-    private fun showVoiceRecordingLocked(value: Boolean) {
-        if (value) {
-            binding.voiceRecordingLock.setImageDrawable(
-                ContextCompat.getDrawable(context, R.drawable.ic_lock_grey600_24px)
-            )
-
-            binding.voiceRecordingLock.alpha = 1f
-            binding.voiceRecordingLock.animate().alpha(0f).setDuration(VOICE_RECORDING_LOCK_ANIMATION_DURATION.toLong())
-                .setInterpolator(AccelerateInterpolator()).start()
-        } else {
-            binding.voiceRecordingLock.setImageDrawable(
-                ContextCompat.getDrawable(context, R.drawable.ic_lock_open_grey600_24dp)
-            )
-            binding.voiceRecordingLock.alpha = 1f
-        }
-    }
-
-    private fun showVoiceRecordingLockedInterface(value: Boolean) {
-        val audioDurationLayoutParams: LayoutParams = binding.messageInputView.audioRecordDuration
-            .layoutParams as LayoutParams
-
-        val micInputCloudLayoutParams: LayoutParams = binding.messageInputView.micInputCloud
-            .layoutParams as LayoutParams
-
-        val deleteVoiceRecordingLayoutParams: LayoutParams = binding.messageInputView.deleteVoiceRecording
-            .layoutParams as LayoutParams
-
-        val sendVoiceRecordingLayoutParams: LayoutParams = binding.messageInputView.sendVoiceRecording
-            .layoutParams as LayoutParams
-
-        val standardQuarterMargin = TypedValue.applyDimension(
-            TypedValue.COMPLEX_UNIT_DIP,
-            resources.getDimension(R.dimen.standard_quarter_margin),
-            resources
-                .displayMetrics
-        ).toInt()
-
-        binding.messageInputView.button.isEnabled = true
-        if (value) {
-            binding.messageInputView.slideToCancelDescription.visibility = View.GONE
-            binding.messageInputView.deleteVoiceRecording.visibility = View.VISIBLE
-            binding.messageInputView.sendVoiceRecording.visibility = View.VISIBLE
-            binding.messageInputView.micInputCloud.visibility = View.VISIBLE
-            binding.messageInputView.recordAudioButton.visibility = View.GONE
-            binding.messageInputView.microphoneEnabledInfo.clearAnimation()
-            binding.messageInputView.microphoneEnabledInfo.visibility = View.GONE
-            binding.messageInputView.microphoneEnabledInfoBackground.visibility = View.GONE
-            binding.messageInputView.recordAudioButton.visibility = View.GONE
-            micInputCloudLayoutParams.removeRule(BELOW)
-            micInputCloudLayoutParams.addRule(BELOW, R.id.audioRecordDuration)
-            deleteVoiceRecordingLayoutParams.removeRule(BELOW)
-            deleteVoiceRecordingLayoutParams.addRule(BELOW, R.id.audioRecordDuration)
-            sendVoiceRecordingLayoutParams.removeRule(BELOW)
-            sendVoiceRecordingLayoutParams.addRule(BELOW, R.id.audioRecordDuration)
-            audioDurationLayoutParams.removeRule(RelativeLayout.CENTER_VERTICAL)
-            audioDurationLayoutParams.removeRule(RelativeLayout.END_OF)
-            audioDurationLayoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL, R.bool.value_true)
-            audioDurationLayoutParams.setMargins(0, standardQuarterMargin, 0, 0)
-        } else {
-            binding.messageInputView.deleteVoiceRecording.visibility = View.GONE
-            binding.messageInputView.micInputCloud.visibility = View.GONE
-            binding.messageInputView.recordAudioButton.visibility = View.VISIBLE
-            binding.messageInputView.sendVoiceRecording.visibility = View.GONE
-            binding.messageInputView.playPauseBtn.visibility = View.GONE
-            binding.messageInputView.seekBar.visibility = View.GONE
-            audioDurationLayoutParams.addRule(RelativeLayout.CENTER_VERTICAL, R.bool.value_true)
-            audioDurationLayoutParams.addRule(RelativeLayout.END_OF, R.id.microphoneEnabledInfo)
-            audioDurationLayoutParams.removeRule(RelativeLayout.CENTER_HORIZONTAL)
-            audioDurationLayoutParams.setMargins(0, 0, 0, 0)
-        }
-    }
-
-    private fun initSmileyKeyboardToggler() {
-        val smileyButton = binding.messageInputView.findViewById<ImageButton>(R.id.smileyButton)
-
-        emojiPopup = binding.messageInputView.inputEditText?.let {
-            EmojiPopup(
-                rootView = binding.root,
-                editText = it,
-                onEmojiPopupShownListener = {
-                    if (resources != null) {
-                        smileyButton?.setImageDrawable(
-                            ContextCompat.getDrawable(context, R.drawable.ic_baseline_keyboard_24)
-                        )
-                    }
-                },
-                onEmojiPopupDismissListener = {
-                    smileyButton?.setImageDrawable(
-                        ContextCompat.getDrawable(context, R.drawable.ic_insert_emoticon_black_24dp)
-                    )
-                },
-                onEmojiClickListener = {
-                    binding.messageInputView.inputEditText?.editableText?.append(" ")
-                }
-            )
-        }
-
-        smileyButton?.setOnClickListener {
-            emojiPopup?.toggle()
-        }
-    }
-
     @Suppress("MagicNumber", "LongMethod")
     private fun updateTypingIndicator() {
         fun ellipsize(text: String): String {
@@ -2023,18 +1297,21 @@ class ChatActivity :
 
             if (participantNames.size > 0) {
                 binding.typingIndicatorWrapper.animate()
-                    .translationY(binding.messageInputView.y - DisplayUtils.convertDpToPixel(18f, context))
+                    .translationY(binding.fragmentContainerActivityChat.y - DisplayUtils.convertDpToPixel(18f, context))
                     .setInterpolator(AccelerateDecelerateInterpolator())
                     .duration = TYPING_INDICATOR_ANIMATION_DURATION
             } else {
                 if (binding.typingIndicator.lineCount == 1) {
                     binding.typingIndicatorWrapper.animate()
-                        .translationY(binding.messageInputView.y)
+                        .translationY(binding.fragmentContainerActivityChat.y)
                         .setInterpolator(AccelerateDecelerateInterpolator())
                         .duration = TYPING_INDICATOR_ANIMATION_DURATION
                 } else if (binding.typingIndicator.lineCount == 2) {
                     binding.typingIndicatorWrapper.animate()
-                        .translationY(binding.messageInputView.y + DisplayUtils.convertDpToPixel(15f, context))
+                        .translationY(
+                            binding.fragmentContainerActivityChat.y +
+                                DisplayUtils.convertDpToPixel(15f, context)
+                        )
                         .setInterpolator(AccelerateDecelerateInterpolator())
                         .duration = TYPING_INDICATOR_ANIMATION_DURATION
                 }
@@ -2042,62 +1319,6 @@ class ChatActivity :
         }
     }
 
-    fun updateOwnTypingStatus(typedText: CharSequence) {
-        fun sendStartTypingSignalingMessage() {
-            for ((sessionId, _) in webSocketInstance?.getUserMap()!!) {
-                val ncSignalingMessage = NCSignalingMessage()
-                ncSignalingMessage.to = sessionId
-                ncSignalingMessage.type = TYPING_STARTED_SIGNALING_MESSAGE_TYPE
-                signalingMessageSender!!.send(ncSignalingMessage)
-            }
-        }
-
-        if (isTypingStatusEnabled()) {
-            if (typedText.isEmpty()) {
-                sendStopTypingMessage()
-            } else if (typingTimer == null) {
-                sendStartTypingSignalingMessage()
-
-                typingTimer = object : CountDownTimer(
-                    TYPING_DURATION_TO_SEND_NEXT_TYPING_MESSAGE,
-                    TYPING_INTERVAL_TO_SEND_NEXT_TYPING_MESSAGE
-                ) {
-                    override fun onTick(millisUntilFinished: Long) {
-                        // unused
-                    }
-
-                    override fun onFinish() {
-                        if (typedWhileTypingTimerIsRunning) {
-                            sendStartTypingSignalingMessage()
-                            cancel()
-                            start()
-                            typedWhileTypingTimerIsRunning = false
-                        } else {
-                            sendStopTypingMessage()
-                        }
-                    }
-                }.start()
-            } else {
-                typedWhileTypingTimerIsRunning = true
-            }
-        }
-    }
-
-    private fun sendStopTypingMessage() {
-        if (isTypingStatusEnabled()) {
-            typingTimer = null
-            typedWhileTypingTimerIsRunning = false
-
-            val concurrentSafeHashMap = webSocketInstance?.getUserMap()!!
-            for ((sessionId, _) in concurrentSafeHashMap) {
-                val ncSignalingMessage = NCSignalingMessage()
-                ncSignalingMessage.to = sessionId
-                ncSignalingMessage.type = TYPING_STOPPED_SIGNALING_MESSAGE_TYPE
-                signalingMessageSender!!.send(ncSignalingMessage)
-            }
-        }
-    }
-
     private fun isTypingStatusEnabled(): Boolean {
         return webSocketInstance != null &&
             !CapabilitiesUtil.isTypingStatusPrivate(conversationUser!!)
@@ -2114,7 +1335,7 @@ class ChatActivity :
                     override fun showReplyUI(position: Int) {
                         val chatMessage = adapter?.items?.getOrNull(position)?.item as ChatMessage?
                         if (chatMessage != null) {
-                            replyToMessage(chatMessage)
+                            messageInputViewModel.reply(chatMessage)
                         }
                     }
                 }
@@ -2211,6 +1432,24 @@ class ChatActivity :
         currentConversation != null && currentConversation?.type != null &&
             currentConversation?.type == ConversationType.ROOM_PUBLIC_CALL
 
+    private fun updateRoomTimerHandler() {
+        val delayForRecursiveCall = if (shouldShowLobby()) {
+            GET_ROOM_INFO_DELAY_LOBBY
+        } else {
+            GET_ROOM_INFO_DELAY_NORMAL
+        }
+
+        if (getRoomInfoTimerHandler == null) {
+            getRoomInfoTimerHandler = Handler()
+        }
+        getRoomInfoTimerHandler?.postDelayed(
+            {
+                chatViewModel.getRoom(conversationUser!!, roomToken)
+            },
+            delayForRecursiveCall
+        )
+    }
+
     private fun switchToRoom(token: String, startCallAfterRoomSwitch: Boolean, isVoiceOnlyCall: Boolean) {
         if (conversationUser != null) {
             runOnUiThread {
@@ -2246,27 +1485,6 @@ class ChatActivity :
         }
     }
 
-    private fun showSendButtonMenu() {
-        val popupMenu = PopupMenu(
-            ContextThemeWrapper(this, R.style.ChatSendButtonMenu),
-            binding.messageInputView.button,
-            Gravity.END
-        )
-        popupMenu.inflate(R.menu.chat_send_menu)
-
-        popupMenu.setOnMenuItemClickListener { item: MenuItem ->
-            when (item.itemId) {
-                R.id.send_without_notification -> submitMessage(true)
-            }
-            true
-        }
-
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-            popupMenu.setForceShowIcon(true)
-        }
-        popupMenu.show()
-    }
-
     private fun showCallButtonMenu(isVoiceOnlyCall: Boolean) {
         val anchor: View? = if (isVoiceOnlyCall) {
             findViewById(R.id.conversation_voice_call)
@@ -2296,73 +1514,6 @@ class ChatActivity :
         }
     }
 
-    private fun getAudioFocusChangeListener(): AudioManager.OnAudioFocusChangeListener {
-        return AudioManager.OnAudioFocusChangeListener { flag ->
-            when (flag) {
-                AudioManager.AUDIOFOCUS_LOSS -> {
-                    chatViewModel.isPausedDueToBecomingNoisy = false
-                    if (isVoicePreviewPlaying) {
-                        stopPreviewVoicePlaying()
-                    }
-                    if (currentlyPlayedVoiceMessage != null) {
-                        stopMediaPlayer(currentlyPlayedVoiceMessage!!)
-                    }
-                }
-
-                AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
-                    chatViewModel.isPausedDueToBecomingNoisy = false
-                    if (isVoicePreviewPlaying) {
-                        pausePreviewVoicePlaying()
-                    }
-                    if (currentlyPlayedVoiceMessage != null) {
-                        pausePlayback(currentlyPlayedVoiceMessage!!)
-                    }
-                }
-            }
-        }
-    }
-
-    private fun audioFocusRequest(shouldRequestFocus: Boolean, onGranted: () -> Unit) {
-        if (chatViewModel.isPausedDueToBecomingNoisy) {
-            onGranted()
-            return
-        }
-        val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
-        val duration = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
-
-        val isGranted: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            val focusRequest = AudioFocusRequest.Builder(duration)
-                .setOnAudioFocusChangeListener(audioFocusChangeListener)
-                .build()
-            if (shouldRequestFocus) {
-                audioManager.requestAudioFocus(focusRequest)
-            } else {
-                audioManager.abandonAudioFocusRequest(focusRequest)
-            }
-        } else {
-            @Deprecated("This method was deprecated in API level 26.")
-            if (shouldRequestFocus) {
-                audioManager.requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, duration)
-            } else {
-                audioManager.abandonAudioFocus(audioFocusChangeListener)
-            }
-        }
-        if (isGranted == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
-            onGranted()
-        }
-    }
-
-    private fun handleBecomingNoisyBroadcast(register: Boolean) {
-        if (register && !chatViewModel.receiverRegistered) {
-            registerReceiver(noisyAudioStreamReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY))
-            chatViewModel.receiverRegistered = true
-        } else if (!chatViewModel.receiverUnregistered) {
-            unregisterReceiver(noisyAudioStreamReceiver)
-            chatViewModel.receiverUnregistered = true
-            chatViewModel.receiverRegistered = false
-        }
-    }
-
     private fun startPlayback(message: ChatMessage, doPlay: Boolean = true) {
         if (!active) {
             // don't begin to play voice message if screen is not visible anymore.
@@ -2376,9 +1527,8 @@ class ChatActivity :
 
         mediaPlayer?.let {
             if (!it.isPlaying && doPlay) {
-                audioFocusRequest(true) {
+                chatViewModel.audioRequest(true) {
                     it.start()
-                    handleBecomingNoisyBroadcast(register = true)
                 }
             }
 
@@ -2418,9 +1568,8 @@ class ChatActivity :
 
     private fun pausePlayback(message: ChatMessage) {
         if (mediaPlayer!!.isPlaying) {
-            audioFocusRequest(false) {
+            chatViewModel.audioRequest(false) {
                 mediaPlayer!!.pause()
-                handleBecomingNoisyBroadcast(register = false)
             }
         }
 
@@ -2482,9 +1631,8 @@ class ChatActivity :
             mediaPlayer?.let {
                 if (it.isPlaying) {
                     Log.d(TAG, "media player is stopped")
-                    audioFocusRequest(false) {
+                    chatViewModel.audioRequest(false) {
                         it.stop()
-                        handleBecomingNoisyBroadcast(register = false)
                     }
                 }
             }
@@ -2609,223 +1757,14 @@ class ChatActivity :
             }
     }
 
-    @SuppressLint("SimpleDateFormat")
-    private fun setVoiceRecordFileName() {
-        val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN)
-        val date: String = simpleDateFormat.format(Date())
-
-        val fileNameWithoutSuffix = String.format(
-            context.resources.getString(R.string.nc_voice_message_filename),
-            date,
-            currentConversation!!.displayName
-        )
-        val fileName = fileNameWithoutSuffix + VOICE_MESSAGE_FILE_SUFFIX
-
-        currentVoiceRecordFile = "${context.cacheDir.absolutePath}/$fileName"
-    }
-
-    private fun showRecordAudioUi(show: Boolean) {
-        if (show) {
-            binding.messageInputView.microphoneEnabledInfo.visibility = View.VISIBLE
-            binding.messageInputView.microphoneEnabledInfoBackground.visibility = View.VISIBLE
-            binding.messageInputView.audioRecordDuration.visibility = View.VISIBLE
-            binding.messageInputView.slideToCancelDescription.visibility = View.VISIBLE
-            binding.messageInputView.attachmentButton.visibility = View.GONE
-            binding.messageInputView.smileyButton.visibility = View.GONE
-            binding.messageInputView.messageInput.visibility = View.GONE
-            binding.messageInputView.messageInput.hint = ""
-            binding.voiceRecordingLock.visibility = View.VISIBLE
-        } else {
-            binding.messageInputView.microphoneEnabledInfo.visibility = View.GONE
-            binding.messageInputView.microphoneEnabledInfoBackground.visibility = View.GONE
-            binding.messageInputView.audioRecordDuration.visibility = View.GONE
-            binding.messageInputView.slideToCancelDescription.visibility = View.GONE
-            binding.messageInputView.attachmentButton.visibility = View.VISIBLE
-            binding.messageInputView.smileyButton.visibility = View.VISIBLE
-            binding.messageInputView.messageInput.visibility = View.VISIBLE
-            binding.messageInputView.messageInput.hint =
-                context.resources?.getString(R.string.nc_hint_enter_a_message)
-            binding.voiceRecordingLock.visibility = View.GONE
-        }
-    }
-
-    private fun startMicInputRecordingAnimation() {
-        val permissionCheck = ContextCompat.checkSelfPermission(
-            context,
-            Manifest.permission.RECORD_AUDIO
-        )
-
-        if (micInputAudioRecordThread == null && permissionCheck == PERMISSION_GRANTED) {
-            Log.d(TAG, "Mic Animation Started")
-            micInputAudioRecorder = AudioRecord(
-                MediaRecorder.AudioSource.MIC,
-                SAMPLE_RATE,
-                AudioFormat.CHANNEL_IN_MONO,
-                AudioFormat.ENCODING_PCM_16BIT,
-                bufferSize
-            )
-            isMicInputAudioThreadRunning = true
-            micInputAudioRecorder.startRecording()
-            initMicInputAudioRecordThread()
-            micInputAudioRecordThread!!.start()
-            binding.messageInputView.micInputCloud.startAnimators()
-        }
-    }
-
-    private fun initMicInputAudioRecordThread() {
-        micInputAudioRecordThread = Thread(
-            Runnable {
-                while (isMicInputAudioThreadRunning) {
-                    val byteArr = ByteArray(bufferSize / 2)
-                    micInputAudioRecorder.read(byteArr, 0, byteArr.size)
-                    val d = abs(byteArr[0].toDouble())
-                    if (d > AUDIO_VALUE_MAX) {
-                        binding.messageInputView.micInputCloud.setRotationSpeed(
-                            log10(d).toFloat(),
-                            MicInputCloud.MAXIMUM_RADIUS
-                        )
-                    } else if (d > AUDIO_VALUE_MIN) {
-                        binding.messageInputView.micInputCloud.setRotationSpeed(
-                            log10(d).toFloat(),
-                            MicInputCloud.EXTENDED_RADIUS
-                        )
-                    } else {
-                        binding.messageInputView.micInputCloud.setRotationSpeed(
-                            1f,
-                            MicInputCloud.DEFAULT_RADIUS
-                        )
-                    }
-                    Thread.sleep(AUDIO_VALUE_SLEEP)
-                }
-            }
-        )
-    }
-
-    private fun stopMicInputRecordingAnimation() {
-        if (micInputAudioRecordThread != null) {
-            Log.d(TAG, "Mic Animation Ended")
-            audioFocusRequest(false) {
-                micInputAudioRecorder.stop()
-                micInputAudioRecorder.release()
-            }
-            isMicInputAudioThreadRunning = false
-            micInputAudioRecordThread = null
-        }
-    }
-
-    private fun isRecordAudioPermissionGranted(): Boolean {
+    fun isRecordAudioPermissionGranted(): Boolean {
         return PermissionChecker.checkSelfPermission(
             context,
             Manifest.permission.RECORD_AUDIO
         ) == PERMISSION_GRANTED
     }
 
-    private fun startAudioRecording(file: String) {
-        binding.messageInputView.audioRecordDuration.base = SystemClock.elapsedRealtime()
-        binding.messageInputView.audioRecordDuration.start()
-
-        val animation: Animation = AlphaAnimation(1.0f, 0.0f)
-        animation.duration = ANIMATION_DURATION
-        animation.interpolator = LinearInterpolator()
-        animation.repeatCount = Animation.INFINITE
-        animation.repeatMode = Animation.REVERSE
-        binding.messageInputView.microphoneEnabledInfo.startAnimation(animation)
-
-        initMediaRecorder(file)
-        VibrationUtils.vibrateShort(context)
-    }
-
-    private fun initMediaRecorder(file: String) {
-        recorder = MediaRecorder().apply {
-            setAudioSource(MediaRecorder.AudioSource.MIC)
-            mediaRecorderState = MediaRecorderState.INITIALIZED
-
-            setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
-            mediaRecorderState = MediaRecorderState.CONFIGURED
-
-            setOutputFile(file)
-            setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
-            setAudioSamplingRate(VOICE_MESSAGE_SAMPLING_RATE)
-            setAudioEncodingBitRate(VOICE_MESSAGE_ENCODING_BIT_RATE)
-            setAudioChannels(VOICE_MESSAGE_CHANNELS)
-
-            try {
-                prepare()
-                mediaRecorderState = MediaRecorderState.PREPARED
-            } catch (e: IOException) {
-                mediaRecorderState = MediaRecorderState.ERROR
-                Log.e(TAG, "prepare for audio recording failed")
-            }
-
-            try {
-                audioFocusRequest(true) {
-                    start()
-                }
-                mediaRecorderState = MediaRecorderState.RECORDING
-                Log.d(TAG, "recording started")
-            } catch (e: IllegalStateException) {
-                mediaRecorderState = MediaRecorderState.ERROR
-                Log.e(TAG, "start for audio recording failed")
-            }
-        }
-    }
-
-    private fun stopAndSendAudioRecording() {
-        stopAudioRecording()
-        Log.d(TAG, "stopped and sent audio recording")
-
-        if (mediaRecorderState != MediaRecorderState.ERROR) {
-            val uri = Uri.fromFile(File(currentVoiceRecordFile))
-            uploadFile(uri.toString(), true)
-        } else {
-            mediaRecorderState = MediaRecorderState.INITIAL
-        }
-    }
-
-    private fun stopAndDiscardAudioRecording() {
-        stopAudioRecording()
-        Log.d(TAG, "stopped and discarded audio recording")
-        val cachedFile = File(currentVoiceRecordFile)
-        cachedFile.delete()
-
-        if (mediaRecorderState == MediaRecorderState.ERROR) {
-            mediaRecorderState = MediaRecorderState.INITIAL
-        }
-    }
-
-    @Suppress("Detekt.TooGenericExceptionCaught")
-    private fun stopAudioRecording() {
-        binding.messageInputView.audioRecordDuration.stop()
-        binding.messageInputView.microphoneEnabledInfo.clearAnimation()
-
-        recorder?.apply {
-            try {
-                if (mediaRecorderState == MediaRecorderState.RECORDING) {
-                    audioFocusRequest(false) {
-                        stop()
-                        reset()
-                    }
-                    mediaRecorderState = MediaRecorderState.INITIAL
-                    Log.d(TAG, "stopped recorder")
-                }
-                release()
-                mediaRecorderState = MediaRecorderState.RELEASED
-            } catch (e: Exception) {
-                when (e) {
-                    is java.lang.IllegalStateException,
-                    is java.lang.RuntimeException -> {
-                        mediaRecorderState = MediaRecorderState.ERROR
-                        Log.e(TAG, "error while stopping recorder! with state $mediaRecorderState $e")
-                    }
-                }
-            }
-
-            VibrationUtils.vibrateShort(context)
-        }
-        recorder = null
-    }
-
-    private fun requestRecordAudioPermissions() {
+    fun requestRecordAudioPermissions() {
         requestPermissions(
             arrayOf(
                 Manifest.permission.RECORD_AUDIO
@@ -2888,9 +1827,9 @@ class ChatActivity :
             shouldShowLobby() ||
             !participantPermissions.hasChatPermission()
         ) {
-            binding.messageInputView.visibility = View.GONE
+            binding.fragmentContainerActivityChat.visibility = View.GONE
         } else {
-            binding.messageInputView.visibility = View.VISIBLE
+            binding.fragmentContainerActivityChat.visibility = View.VISIBLE
         }
     }
 
@@ -2943,7 +1882,7 @@ class ChatActivity :
             if (shouldShowLobby()) {
                 binding.lobby.lobbyView.visibility = View.VISIBLE
                 binding.messagesListView.visibility = View.GONE
-                binding.messageInputView.visibility = View.GONE
+                binding.fragmentContainerActivityChat.visibility = View.GONE
                 binding.progressBar.visibility = View.GONE
 
                 val sb = StringBuilder()
@@ -2969,14 +1908,12 @@ class ChatActivity :
             } else {
                 binding.lobby.lobbyView.visibility = View.GONE
                 binding.messagesListView.visibility = View.VISIBLE
-                binding.messageInputView.inputEditText?.visibility = View.VISIBLE
+                binding.fragmentContainerActivityChat.visibility = View.VISIBLE
             }
         } else {
             binding.lobby.lobbyView.visibility = View.GONE
             binding.messagesListView.visibility = View.VISIBLE
-            if (!isVoiceRecordingLocked) {
-                binding.messageInputView.inputEditText?.visibility = View.VISIBLE
-            }
+            binding.fragmentContainerActivityChat.visibility = View.VISIBLE
         }
     }
 
@@ -3262,21 +2199,7 @@ class ChatActivity :
 
         if (token == "") room = roomToken else room = token
 
-        try {
-            require(fileUri.isNotEmpty())
-            UploadAndShareFilesWorker.upload(
-                fileUri,
-                room,
-                currentConversation?.displayName!!,
-                metaData
-            )
-        } catch (e: IllegalArgumentException) {
-            context.resources?.getString(R.string.nc_upload_failed)?.let {
-                Snackbar.make(binding.root, it, Snackbar.LENGTH_LONG)
-                    .show()
-            }
-            Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
-        }
+        chatViewModel.uploadFile(fileUri, room, currentConversation?.displayName!!, metaData)
     }
 
     private fun showLocalFilePicker() {
@@ -3332,41 +2255,12 @@ class ChatActivity :
         startActivity(intent)
     }
 
-    private fun setupMentionAutocomplete() {
-        val elevation = MENTION_AUTO_COMPLETE_ELEVATION
-        resources?.let {
-            val backgroundDrawable = ColorDrawable(it.getColor(R.color.bg_default, null))
-            val presenter = MentionAutocompletePresenter(this, roomToken, chatApiVersion)
-            val callback = MentionAutocompleteCallback(
-                this,
-                conversationUser!!,
-                binding.messageInputView.inputEditText,
-                viewThemeUtils
-            )
-
-            if (mentionAutocomplete == null && binding.messageInputView.inputEditText != null) {
-                mentionAutocomplete = Autocomplete.on<Mention>(binding.messageInputView.inputEditText)
-                    .with(elevation)
-                    .with(backgroundDrawable)
-                    .with(CharPolicy('@'))
-                    .with(presenter)
-                    .with(callback)
-                    .build()
-            }
-        }
-    }
-
     private fun validSessionId(): Boolean {
         return currentConversation != null &&
             sessionIdAfterRoomJoined?.isNotEmpty() == true &&
             sessionIdAfterRoomJoined != "0"
     }
 
-    private fun cancelReply() {
-        binding.messageInputView.findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.visibility = View.GONE
-        binding.messageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility = View.VISIBLE
-    }
-
     @Suppress("Detekt.TooGenericExceptionCaught")
     private fun cancelNotificationsForCurrentConversation() {
         if (conversationUser != null) {
@@ -3542,59 +2436,6 @@ class ChatActivity :
         )
     }
 
-    private fun submitMessage(sendWithoutNotification: Boolean) {
-        if (binding.messageInputView.inputEditText != null) {
-            val editable = binding.messageInputView.inputEditText!!.editableText
-            val mentionSpans = editable.getSpans(
-                0,
-                editable.length,
-                Spans.MentionChipSpan::class.java
-            )
-            var mentionSpan: Spans.MentionChipSpan
-            for (i in mentionSpans.indices) {
-                mentionSpan = mentionSpans[i]
-                var mentionId = mentionSpan.id
-                val needsQuotes = mentionId.contains(" ") ||
-                    mentionId.contains("@") ||
-                    mentionId.startsWith("guest/") ||
-                    mentionId.startsWith("group/")
-
-                if (needsQuotes) {
-                    mentionId = "\"" + mentionId + "\""
-                }
-                editable.replace(editable.getSpanStart(mentionSpan), editable.getSpanEnd(mentionSpan), "@$mentionId")
-            }
-
-            binding.messageInputView.inputEditText?.setText("")
-            sendStopTypingMessage()
-            val replyMessageId: Int? = findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.tag as Int?
-            sendMessage(
-                editable,
-                if (findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.visibility == View.VISIBLE) {
-                    replyMessageId
-                } else {
-                    null
-                },
-                sendWithoutNotification
-            )
-            cancelReply()
-        }
-    }
-
-    private fun sendMessage(message: CharSequence, replyTo: Int?, sendWithoutNotification: Boolean) {
-        if (conversationUser != null) {
-            chatViewModel.sendChatMessage(
-                credentials!!,
-                ApiUtils.getUrlForChat(chatApiVersion, conversationUser!!.baseUrl!!, roomToken),
-                message,
-                conversationUser!!.displayName ?: "",
-                replyTo ?: 0,
-                sendWithoutNotification
-            )
-        }
-        showMicrophoneButton(true)
-    }
-
     private fun setupWebsocket() {
         if (conversationUser == null) {
             return
@@ -3966,7 +2807,7 @@ class ChatActivity :
         super.onCreateOptionsMenu(menu)
         menuInflater.inflate(R.menu.menu_conversation, menu)
 
-        binding.messageInputView.context?.let {
+        context.let {
             viewThemeUtils.platform.colorToolbarMenuIcon(
                 it,
                 menu.findItem(R.id.conversation_voice_call)
@@ -4527,68 +3368,6 @@ class ChatActivity :
             BuildConfig.DEBUG
     }
 
-    fun replyToMessage(message: IMessage?) {
-        val chatMessage = message as ChatMessage?
-        chatMessage?.let {
-            binding.messageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility =
-                View.GONE
-            binding.messageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.visibility =
-                View.VISIBLE
-
-            val quotedMessage = binding.messageInputView.findViewById<EmojiTextView>(R.id.quotedMessage)
-
-            quotedMessage?.maxLines = 2
-            quotedMessage?.ellipsize = TextUtils.TruncateAt.END
-            quotedMessage?.text = it.text
-            binding.messageInputView.findViewById<EmojiTextView>(R.id.quotedMessageAuthor)?.text =
-                it.actorDisplayName ?: context.getText(R.string.nc_nick_guest)
-
-            conversationUser?.let {
-                val quotedMessageImage = binding.messageInputView.findViewById<ImageView>(R.id.quotedMessageImage)
-                chatMessage.imageUrl?.let { previewImageUrl ->
-                    quotedMessageImage?.visibility = View.VISIBLE
-
-                    val px = TypedValue.applyDimension(
-                        TypedValue.COMPLEX_UNIT_DIP,
-                        QUOTED_MESSAGE_IMAGE_MAX_HEIGHT,
-                        resources?.displayMetrics
-                    )
-
-                    quotedMessageImage?.maxHeight = px.toInt()
-                    val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams
-                    layoutParams.flexGrow = 0f
-                    quotedMessageImage.layoutParams = layoutParams
-                    quotedMessageImage.load(previewImageUrl) {
-                        addHeader("Authorization", credentials!!)
-                    }
-                } ?: run {
-                    binding.messageInputView.findViewById<ImageView>(R.id.quotedMessageImage)?.visibility = View.GONE
-                }
-            }
-
-            val quotedChatMessageView =
-                binding.messageInputView.findViewById<RelativeLayout>(R.id.quotedChatMessageView)
-            quotedChatMessageView?.tag = message?.jsonMessageId
-            quotedChatMessageView?.visibility = View.VISIBLE
-        }
-    }
-
-    private fun showMicrophoneButton(show: Boolean) {
-        if (show && CapabilitiesUtil.hasSpreedFeatureCapability(
-                spreedCapabilities,
-                SpreedFeatures.VOICE_MESSAGE_SHARING
-            )
-        ) {
-            Log.d(TAG, "Microphone shown")
-            binding.messageInputView.messageSendButton.visibility = View.GONE
-            binding.messageInputView.recordAudioButton.visibility = View.VISIBLE
-        } else {
-            Log.d(TAG, "Microphone hidden")
-            binding.messageInputView.messageSendButton.visibility = View.VISIBLE
-            binding.messageInputView.recordAudioButton.visibility = View.GONE
-        }
-    }
-
     private fun setMessageAsDeleted(message: IMessage?) {
         val messageTemp = message as ChatMessage
         messageTemp.isDeleted = true
@@ -4864,30 +3643,6 @@ class ChatActivity :
         startActivity(shareIntent)
     }
 
-    fun editMessage(message: ChatMessage) {
-        editableBehaviorSubject.onNext(true)
-        editMessage = message
-        initMessageInputView()
-
-        setEditUI()
-
-        val editableText = Editable.Factory.getInstance().newEditable(editMessage.message)
-        binding.messageInputView.inputEditText.text = editableText
-        binding.messageInputView.inputEditText.setSelection(editableText.length)
-        binding.editView.editMessage.text = editMessage.message
-
-        binding.messageInputView.editMessageButton.setOnClickListener {
-            if (editMessage.message == editedTextBehaviorSubject.value!!) {
-                clearEditUI()
-                return@setOnClickListener
-            }
-            editMessageAPI(editMessage, editedMessageText = editedTextBehaviorSubject.value!!)
-        }
-        binding.editView.clearEdit.setOnClickListener {
-            clearEditUI()
-        }
-    }
-
     companion object {
         val TAG = ChatActivity::class.simpleName
         private const val CONTENT_TYPE_CALL_STARTED: Byte = 1
diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
new file mode 100644
index 000000000..ad21845f2
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
@@ -0,0 +1,821 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Julius Linus juliuslinus1@gmail.com
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.chat
+
+import android.content.res.Resources
+import android.graphics.drawable.ColorDrawable
+import android.os.Build
+import android.os.Bundle
+import android.os.CountDownTimer
+import android.os.SystemClock
+import android.text.Editable
+import android.text.InputFilter
+import android.text.TextUtils
+import android.text.TextWatcher
+import android.util.Log
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.animation.AlphaAnimation
+import android.view.animation.Animation
+import android.view.animation.LinearInterpolator
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.PopupMenu
+import android.widget.RelativeLayout
+import android.widget.SeekBar
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.view.ContextThemeWrapper
+import androidx.core.content.ContextCompat
+import androidx.core.widget.doAfterTextChanged
+import androidx.emoji2.widget.EmojiTextView
+import androidx.fragment.app.Fragment
+import autodagger.AutoInjector
+import coil.load
+import com.google.android.flexbox.FlexboxLayout
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.snackbar.Snackbar
+import com.nextcloud.android.common.ui.theme.utils.ColorRole
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
+import com.nextcloud.talk.chat.viewmodels.ChatViewModel
+import com.nextcloud.talk.databinding.FragmentMessageInputBinding
+import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
+import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.models.json.mention.Mention
+import com.nextcloud.talk.models.json.signaling.NCSignalingMessage
+import com.nextcloud.talk.presenters.MentionAutocompletePresenter
+import com.nextcloud.talk.ui.MicInputCloud
+import com.nextcloud.talk.ui.dialog.AttachmentDialog
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.users.UserManager
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.CapabilitiesUtil
+import com.nextcloud.talk.utils.CharPolicy
+import com.nextcloud.talk.utils.ImageEmojiEditText
+import com.nextcloud.talk.utils.SpreedFeatures
+import com.nextcloud.talk.utils.text.Spans
+import com.otaliastudios.autocomplete.Autocomplete
+import com.stfalcon.chatkit.commons.models.IMessage
+import com.vanniktech.emoji.EmojiPopup
+import java.util.Objects
+import javax.inject.Inject
+
+@Suppress("LongParameterList", "TooManyFunctions")
+@AutoInjector(NextcloudTalkApplication::class)
+class MessageInputFragment : Fragment() {
+
+    companion object {
+        fun newInstance() = MessageInputFragment()
+        private val TAG: String = MessageInputFragment::class.java.simpleName
+        private const val TYPING_DURATION_TO_SEND_NEXT_TYPING_MESSAGE = 10000L
+        private const val TYPING_INTERVAL_TO_SEND_NEXT_TYPING_MESSAGE = 1000L
+        private const val TYPING_STARTED_SIGNALING_MESSAGE_TYPE = "startedTyping"
+        private const val TYPING_STOPPED_SIGNALING_MESSAGE_TYPE = "stoppedTyping"
+        const val VOICE_MESSAGE_META_DATA = "{\"messageType\":\"voice-message\"}"
+        private const val QUOTED_MESSAGE_IMAGE_MAX_HEIGHT = 96f
+        private const val MENTION_AUTO_COMPLETE_ELEVATION = 6f
+        private const val MINIMUM_VOICE_RECORD_DURATION: Int = 1000
+        private const val ANIMATION_DURATION: Long = 750
+        private const val VOICE_RECORD_CANCEL_SLIDER_X: Int = -150
+        private const val VOICE_RECORD_LOCK_THRESHOLD: Float = 100f
+        private const val INCREMENT = 8f
+        private const val CURSOR_KEY = "_cursor"
+    }
+
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var userManager: UserManager
+
+    lateinit var binding: FragmentMessageInputBinding
+    private var typedWhileTypingTimerIsRunning: Boolean = false
+    private var typingTimer: CountDownTimer? = null
+    private lateinit var chatActivity: ChatActivity
+    private var emojiPopup: EmojiPopup? = null
+    private var mentionAutocomplete: Autocomplete<*>? = null
+    private var xcounter = 0f
+    private var ycounter = 0f
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        sharedApplication!!.componentApplication.inject(this)
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        binding = FragmentMessageInputBinding.inflate(inflater)
+        chatActivity = requireActivity() as ChatActivity
+        themeMessageInputView()
+        initMessageInputView()
+        initSmileyKeyboardToggler()
+        setupMentionAutocomplete()
+        initVoiceRecordButton()
+        restoreState()
+        return binding.root
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        saveState()
+        if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) {
+            mentionAutocomplete?.dismissPopup()
+        }
+        clearEditUI()
+        cancelReply()
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        initObservers()
+    }
+
+    private fun initObservers() {
+        Log.d(TAG, "LifeCyclerOwner is: ${viewLifecycleOwner.lifecycle}")
+        chatActivity.messageInputViewModel.getReplyChatMessage.observe(viewLifecycleOwner) { message ->
+            message?.let { replyToMessage(message) }
+        }
+
+        chatActivity.messageInputViewModel.getEditChatMessage.observe(viewLifecycleOwner) { message ->
+            message?.let { setEditUI(it as ChatMessage) }
+        }
+
+        chatActivity.chatViewModel.leaveRoomViewState.observe(viewLifecycleOwner) { state ->
+            when (state) {
+                is ChatViewModel.LeaveRoomSuccessState -> sendStopTypingMessage()
+                else -> {}
+            }
+        }
+    }
+
+    private fun restoreState() {
+        requireContext().getSharedPreferences(chatActivity.localClassName, AppCompatActivity.MODE_PRIVATE).apply {
+            val text = getString(chatActivity.roomToken, "")
+            val cursor = getInt(chatActivity.roomToken + CURSOR_KEY, 0)
+            binding.fragmentMessageInputView.messageInput.setText(text)
+            binding.fragmentMessageInputView.messageInput.setSelection(cursor)
+        }
+    }
+
+    private fun saveState() {
+        val text = binding.fragmentMessageInputView.messageInput.text.toString()
+        val cursor = binding.fragmentMessageInputView.messageInput.selectionStart
+        val previous = requireContext().getSharedPreferences(
+            chatActivity.localClassName,
+            AppCompatActivity
+                .MODE_PRIVATE
+        ).getString(chatActivity.roomToken, "null")
+
+        if (text != previous) {
+            requireContext().getSharedPreferences(
+                chatActivity.localClassName,
+                AppCompatActivity.MODE_PRIVATE
+            ).edit().apply {
+                putString(chatActivity.roomToken, text)
+                putInt(chatActivity.roomToken + CURSOR_KEY, cursor)
+                apply()
+            }
+        }
+    }
+
+    private fun initMessageInputView() {
+        if (!chatActivity.active) return
+
+        val filters = arrayOfNulls<InputFilter>(1)
+        val lengthFilter = CapabilitiesUtil.getMessageMaxLength(chatActivity.spreedCapabilities)
+
+        binding.fragmentEditView.editMessageView.visibility = View.GONE
+        binding.fragmentMessageInputView.setPadding(0, 0, 0, 0)
+
+        filters[0] = InputFilter.LengthFilter(lengthFilter)
+        binding.fragmentMessageInputView.inputEditText?.filters = filters
+
+        binding.fragmentMessageInputView.inputEditText?.addTextChangedListener(object : TextWatcher {
+
+            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+                // unused atm
+            }
+
+            @Suppress("Detekt.TooGenericExceptionCaught")
+            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+                updateOwnTypingStatus(s)
+
+                if (s.length >= lengthFilter) {
+                    binding.fragmentMessageInputView.inputEditText?.error = String.format(
+                        Objects.requireNonNull<Resources>(resources).getString(R.string.nc_limit_hit),
+                        lengthFilter.toString()
+                    )
+                } else {
+                    binding.fragmentMessageInputView.inputEditText?.error = null
+                }
+
+                val editable = binding.fragmentMessageInputView.inputEditText?.editableText
+
+                if (editable != null && binding.fragmentMessageInputView.inputEditText != null) {
+                    val mentionSpans = editable.getSpans(
+                        0,
+                        binding.fragmentMessageInputView.inputEditText!!.length(),
+                        Spans.MentionChipSpan::class.java
+                    )
+                    var mentionSpan: Spans.MentionChipSpan
+                    for (i in mentionSpans.indices) {
+                        mentionSpan = mentionSpans[i]
+                        if (start >= editable.getSpanStart(mentionSpan) &&
+                            start < editable.getSpanEnd(mentionSpan)
+                        ) {
+                            if (editable.subSequence(
+                                    editable.getSpanStart(mentionSpan),
+                                    editable.getSpanEnd(mentionSpan)
+                                ).toString().trim { it <= ' ' } != mentionSpan.label
+                            ) {
+                                editable.removeSpan(mentionSpan)
+                            }
+                        }
+                    }
+                }
+            }
+
+            override fun afterTextChanged(s: Editable) {
+                // unused atm
+            }
+        })
+
+        // Image keyboard support
+        // See: https://developer.android.com/guide/topics/text/image-keyboard
+
+        (binding.fragmentMessageInputView.inputEditText as ImageEmojiEditText).onCommitContentListener = {
+            uploadFile(it.toString(), false)
+        }
+
+        if (chatActivity.sharedText.isNotEmpty()) {
+            binding.fragmentMessageInputView.inputEditText?.setText(chatActivity.sharedText)
+        }
+
+        binding.fragmentMessageInputView.setAttachmentsListener {
+            AttachmentDialog(requireActivity(), requireActivity() as ChatActivity).show()
+        }
+
+        binding.fragmentMessageInputView.button?.setOnClickListener {
+            submitMessage(false)
+        }
+
+        binding.fragmentMessageInputView.editMessageButton.setOnClickListener {
+            val text = binding.fragmentMessageInputView.inputEditText.text.toString()
+            val message = chatActivity.messageInputViewModel.getEditChatMessage.value as ChatMessage
+            if (message.message!!.trim() != text.trim()) {
+                editMessageAPI(message, text)
+            }
+            clearEditUI()
+        }
+        binding.fragmentEditView.clearEdit.setOnClickListener {
+            clearEditUI()
+        }
+
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(chatActivity.spreedCapabilities, SpreedFeatures.SILENT_SEND)) {
+            binding.fragmentMessageInputView.button?.setOnLongClickListener {
+                showSendButtonMenu()
+                true
+            }
+        }
+
+        binding.fragmentMessageInputView.button?.contentDescription =
+            resources.getString(R.string.nc_description_send_message_button)
+    }
+
+    @Suppress("ClickableViewAccessibility", "CyclomaticComplexMethod", "LongMethod")
+    private fun initVoiceRecordButton() {
+        binding.fragmentMessageInputView.messageSendButton.visibility = View.GONE
+        binding.fragmentMessageInputView.inputEditText.doAfterTextChanged {
+            binding.fragmentMessageInputView.recordAudioButton.visibility =
+                if (binding.fragmentMessageInputView.inputEditText.text.isEmpty()) View.VISIBLE else View.GONE
+
+            binding.fragmentMessageInputView.messageSendButton.visibility =
+                if (binding.fragmentMessageInputView.inputEditText.text.isEmpty() ||
+                    binding.fragmentEditView.editMessageView.visibility == View.VISIBLE
+                ) {
+                    View.GONE
+                } else {
+                    View.VISIBLE
+                }
+        }
+
+        var prevDx = 0f
+        var voiceRecordStartTime = 0L
+        var voiceRecordEndTime: Long
+        binding.fragmentMessageInputView.recordAudioButton.setOnTouchListener { v, event ->
+            v?.performClick()
+            when (event?.action) {
+                MotionEvent.ACTION_DOWN -> {
+                    if (!chatActivity.isRecordAudioPermissionGranted()) {
+                        chatActivity.requestRecordAudioPermissions()
+                        return@setOnTouchListener true
+                    }
+                    if (!chatActivity.permissionUtil.isFilesPermissionGranted()) {
+                        UploadAndShareFilesWorker.requestStoragePermission(chatActivity)
+                        return@setOnTouchListener true
+                    }
+
+                    val base = SystemClock.elapsedRealtime()
+                    voiceRecordStartTime = System.currentTimeMillis()
+                    binding.fragmentMessageInputView.audioRecordDuration.base = base
+                    chatActivity.messageInputViewModel.setRecordingTime(base)
+                    binding.fragmentMessageInputView.audioRecordDuration.start()
+                    chatActivity.chatViewModel.startAudioRecording(requireContext(), chatActivity.currentConversation!!)
+                    showRecordAudioUi(true)
+                }
+
+                MotionEvent.ACTION_CANCEL -> {
+                    Log.d(TAG, "ACTION_CANCEL")
+                    if (chatActivity.chatViewModel.getVoiceRecordingInProgress.value == false ||
+                        !chatActivity.isRecordAudioPermissionGranted()
+                    ) {
+                        return@setOnTouchListener true
+                    }
+
+                    showRecordAudioUi(false)
+                    if (chatActivity.chatViewModel.getVoiceRecordingLocked.value != true) { // can also be null
+                        chatActivity.chatViewModel.stopAndDiscardAudioRecording()
+                    }
+                }
+
+                MotionEvent.ACTION_UP -> {
+                    Log.d(TAG, "ACTION_UP")
+                    if (chatActivity.chatViewModel.getVoiceRecordingInProgress.value == false ||
+                        chatActivity.chatViewModel.getVoiceRecordingLocked.value == true ||
+                        !chatActivity.isRecordAudioPermissionGranted()
+                    ) {
+                        return@setOnTouchListener false
+                    }
+                    showRecordAudioUi(false)
+
+                    voiceRecordEndTime = System.currentTimeMillis()
+                    val voiceRecordDuration = voiceRecordEndTime - voiceRecordStartTime
+                    if (voiceRecordDuration < MINIMUM_VOICE_RECORD_DURATION) {
+                        Snackbar.make(
+                            binding.root,
+                            requireContext().getString(R.string.nc_voice_message_hold_to_record_info),
+                            Snackbar.LENGTH_SHORT
+                        ).show()
+                        chatActivity.chatViewModel.stopAndDiscardAudioRecording()
+                        return@setOnTouchListener false
+                    } else {
+                        chatActivity.chatViewModel.stopAndSendAudioRecording(
+                            chatActivity.roomToken,
+                            chatActivity.currentConversation!!.displayName!!,
+                            VOICE_MESSAGE_META_DATA
+                        )
+                    }
+                    resetSlider()
+                }
+
+                MotionEvent.ACTION_MOVE -> {
+                    if (chatActivity.chatViewModel.getVoiceRecordingInProgress.value == false ||
+                        !chatActivity.isRecordAudioPermissionGranted()
+                    ) {
+                        return@setOnTouchListener false
+                    }
+
+                    if (event.x < VOICE_RECORD_CANCEL_SLIDER_X) {
+                        chatActivity.chatViewModel.stopAndDiscardAudioRecording()
+                        showRecordAudioUi(false)
+                        resetSlider()
+                        return@setOnTouchListener true
+                    }
+                    if (event.x < 0f) {
+                        val dX = event.x
+                        if (dX < prevDx) { // left
+                            binding.fragmentMessageInputView.slideToCancelDescription.x -= INCREMENT
+                            xcounter += INCREMENT
+                        } else { // right
+                            binding.fragmentMessageInputView.slideToCancelDescription.x += INCREMENT
+                            xcounter -= INCREMENT
+                        }
+
+                        prevDx = dX
+                    }
+
+                    if (event.y < 0f) {
+                        chatActivity.chatViewModel.postToRecordTouchObserver(INCREMENT)
+                        ycounter += INCREMENT
+                    }
+
+                    if (ycounter >= VOICE_RECORD_LOCK_THRESHOLD) {
+                        resetSlider()
+                        binding.fragmentMessageInputView.recordAudioButton.isEnabled = false
+                        chatActivity.chatViewModel.setVoiceRecordingLocked(true)
+                        binding.fragmentMessageInputView.recordAudioButton.isEnabled = true
+                    }
+                }
+            }
+            v?.onTouchEvent(event) ?: true
+        }
+    }
+
+    private fun resetSlider() {
+        binding.fragmentMessageInputView.audioRecordDuration.stop()
+        binding.fragmentMessageInputView.audioRecordDuration.clearAnimation()
+        binding.fragmentMessageInputView.slideToCancelDescription.x += xcounter
+        chatActivity.chatViewModel.postToRecordTouchObserver(-ycounter)
+        xcounter = 0f
+        ycounter = 0f
+    }
+
+    private fun setupMentionAutocomplete() {
+        val elevation = MENTION_AUTO_COMPLETE_ELEVATION
+        resources.let {
+            val backgroundDrawable = ColorDrawable(it.getColor(R.color.bg_default, null))
+            val presenter = MentionAutocompletePresenter(
+                requireContext(),
+                chatActivity.roomToken,
+                chatActivity.chatApiVersion
+            )
+            val callback = MentionAutocompleteCallback(
+                requireContext(),
+                chatActivity.conversationUser!!,
+                binding.fragmentMessageInputView.inputEditText,
+                viewThemeUtils
+            )
+
+            if (mentionAutocomplete == null && binding.fragmentMessageInputView.inputEditText != null) {
+                mentionAutocomplete =
+                    Autocomplete.on<Mention>(binding.fragmentMessageInputView.inputEditText)
+                        .with(elevation)
+                        .with(backgroundDrawable)
+                        .with(CharPolicy('@'))
+                        .with(presenter)
+                        .with(callback)
+                        .build()
+            }
+        }
+    }
+
+    private fun showRecordAudioUi(show: Boolean) {
+        if (show) {
+            val animation: Animation = AlphaAnimation(1.0f, 0.0f)
+            animation.duration = ANIMATION_DURATION
+            animation.interpolator = LinearInterpolator()
+            animation.repeatCount = Animation.INFINITE
+            animation.repeatMode = Animation.REVERSE
+            binding.fragmentMessageInputView.microphoneEnabledInfo.startAnimation(animation)
+
+            binding.fragmentMessageInputView.microphoneEnabledInfo.visibility = View.VISIBLE
+            binding.fragmentMessageInputView.microphoneEnabledInfoBackground.visibility = View.VISIBLE
+            binding.fragmentMessageInputView.audioRecordDuration.visibility = View.VISIBLE
+            binding.fragmentMessageInputView.slideToCancelDescription.visibility = View.VISIBLE
+            binding.fragmentMessageInputView.attachmentButton.visibility = View.GONE
+            binding.fragmentMessageInputView.smileyButton.visibility = View.GONE
+            binding.fragmentMessageInputView.messageInput.visibility = View.GONE
+            binding.fragmentMessageInputView.messageInput.hint = ""
+        } else {
+            binding.fragmentMessageInputView.microphoneEnabledInfo.clearAnimation()
+
+            binding.fragmentMessageInputView.microphoneEnabledInfo.visibility = View.GONE
+            binding.fragmentMessageInputView.microphoneEnabledInfoBackground.visibility = View.GONE
+            binding.fragmentMessageInputView.audioRecordDuration.visibility = View.GONE
+            binding.fragmentMessageInputView.slideToCancelDescription.visibility = View.GONE
+            binding.fragmentMessageInputView.attachmentButton.visibility = View.VISIBLE
+            binding.fragmentMessageInputView.smileyButton.visibility = View.VISIBLE
+            binding.fragmentMessageInputView.messageInput.visibility = View.VISIBLE
+            binding.fragmentMessageInputView.messageInput.hint =
+                requireContext().resources?.getString(R.string.nc_hint_enter_a_message)
+        }
+    }
+
+    private fun initSmileyKeyboardToggler() {
+        val smileyButton = binding.fragmentMessageInputView.findViewById<ImageButton>(R.id.smileyButton)
+
+        emojiPopup = binding.fragmentMessageInputView.inputEditText?.let {
+            EmojiPopup(
+                rootView = binding.root,
+                editText = it,
+                onEmojiPopupShownListener = {
+                    smileyButton?.setImageDrawable(
+                        ContextCompat.getDrawable(requireContext(), R.drawable.ic_baseline_keyboard_24)
+                    )
+                },
+                onEmojiPopupDismissListener = {
+                    smileyButton?.setImageDrawable(
+                        ContextCompat.getDrawable(requireContext(), R.drawable.ic_insert_emoticon_black_24dp)
+                    )
+                },
+                onEmojiClickListener = {
+                    binding.fragmentMessageInputView.inputEditText?.editableText?.append(" ")
+                }
+            )
+        }
+
+        smileyButton?.setOnClickListener {
+            emojiPopup?.toggle()
+        }
+    }
+
+    private fun replyToMessage(message: IMessage?) {
+        Log.d(TAG, "Reply")
+        val chatMessage = message as ChatMessage?
+        chatMessage?.let {
+            val view = binding.fragmentMessageInputView
+            view.findViewById<ImageButton>(R.id.attachmentButton)?.visibility =
+                View.GONE
+            view.findViewById<ImageButton>(R.id.cancelReplyButton)?.visibility =
+                View.VISIBLE
+
+            val quotedMessage = view.findViewById<EmojiTextView>(R.id.quotedMessage)
+
+            quotedMessage?.maxLines = 2
+            quotedMessage?.ellipsize = TextUtils.TruncateAt.END
+            quotedMessage?.text = it.text
+            view.findViewById<EmojiTextView>(R.id.quotedMessageAuthor)?.text =
+                it.actorDisplayName ?: requireContext().getText(R.string.nc_nick_guest)
+
+            chatActivity.conversationUser?.let {
+                val quotedMessageImage = view.findViewById<ImageView>(R.id.quotedMessageImage)
+                chatMessage.imageUrl?.let { previewImageUrl ->
+                    quotedMessageImage?.visibility = View.VISIBLE
+
+                    val px = TypedValue.applyDimension(
+                        TypedValue.COMPLEX_UNIT_DIP,
+                        QUOTED_MESSAGE_IMAGE_MAX_HEIGHT,
+                        resources.displayMetrics
+                    )
+
+                    quotedMessageImage?.maxHeight = px.toInt()
+                    val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams
+                    layoutParams.flexGrow = 0f
+                    quotedMessageImage.layoutParams = layoutParams
+                    quotedMessageImage.load(previewImageUrl) {
+                        addHeader("Authorization", chatActivity.credentials!!)
+                    }
+                } ?: run {
+                    view.findViewById<ImageView>(R.id.quotedMessageImage)?.visibility = View.GONE
+                }
+            }
+
+            val quotedChatMessageView =
+                view.findViewById<RelativeLayout>(R.id.quotedChatMessageView)
+            quotedChatMessageView?.tag = message?.jsonMessageId
+            quotedChatMessageView?.visibility = View.VISIBLE
+        }
+    }
+
+    fun updateOwnTypingStatus(typedText: CharSequence) {
+        fun sendStartTypingSignalingMessage() {
+            val concurrentSafeHashMap = chatActivity.webSocketInstance?.getUserMap()
+            if (concurrentSafeHashMap != null) {
+                for ((sessionId, _) in concurrentSafeHashMap) {
+                    val ncSignalingMessage = NCSignalingMessage()
+                    ncSignalingMessage.to = sessionId
+                    ncSignalingMessage.type = TYPING_STARTED_SIGNALING_MESSAGE_TYPE
+                    chatActivity.signalingMessageSender!!.send(ncSignalingMessage)
+                }
+            }
+        }
+
+        if (isTypingStatusEnabled()) {
+            if (typedText.isEmpty()) {
+                sendStopTypingMessage()
+            } else if (typingTimer == null) {
+                sendStartTypingSignalingMessage()
+
+                typingTimer = object : CountDownTimer(
+                    TYPING_DURATION_TO_SEND_NEXT_TYPING_MESSAGE,
+                    TYPING_INTERVAL_TO_SEND_NEXT_TYPING_MESSAGE
+                ) {
+                    override fun onTick(millisUntilFinished: Long) {
+                        // unused
+                    }
+
+                    override fun onFinish() {
+                        if (typedWhileTypingTimerIsRunning) {
+                            sendStartTypingSignalingMessage()
+                            cancel()
+                            start()
+                            typedWhileTypingTimerIsRunning = false
+                        } else {
+                            sendStopTypingMessage()
+                        }
+                    }
+                }.start()
+            } else {
+                typedWhileTypingTimerIsRunning = true
+            }
+        }
+    }
+
+    private fun sendStopTypingMessage() {
+        if (isTypingStatusEnabled()) {
+            typingTimer = null
+            typedWhileTypingTimerIsRunning = false
+
+            val concurrentSafeHashMap = chatActivity.webSocketInstance?.getUserMap()
+            if (concurrentSafeHashMap != null) {
+                for ((sessionId, _) in concurrentSafeHashMap) {
+                    val ncSignalingMessage = NCSignalingMessage()
+                    ncSignalingMessage.to = sessionId
+                    ncSignalingMessage.type = TYPING_STOPPED_SIGNALING_MESSAGE_TYPE
+                    chatActivity.signalingMessageSender?.send(ncSignalingMessage)
+                }
+            }
+        }
+    }
+
+    private fun isTypingStatusEnabled(): Boolean {
+        return !CapabilitiesUtil.isTypingStatusPrivate(chatActivity.conversationUser!!)
+    }
+
+    private fun uploadFile(fileUri: String, isVoiceMessage: Boolean, caption: String = "", token: String = "") {
+        var metaData = ""
+        val room: String
+
+        if (!chatActivity.participantPermissions.hasChatPermission()) {
+            Log.w(ChatActivity.TAG, "uploading file(s) is forbidden because of missing attendee permissions")
+            return
+        }
+
+        if (isVoiceMessage) {
+            metaData = VOICE_MESSAGE_META_DATA
+        }
+
+        if (caption != "") {
+            metaData = "{\"caption\":\"$caption\"}"
+        }
+
+        if (token == "") room = chatActivity.roomToken else room = token
+
+        chatActivity.chatViewModel.uploadFile(fileUri, room, chatActivity.currentConversation!!.displayName!!, metaData)
+    }
+
+    private fun submitMessage(sendWithoutNotification: Boolean) {
+        if (binding.fragmentMessageInputView.inputEditText != null) {
+            val editable = binding.fragmentMessageInputView.inputEditText!!.editableText
+            val mentionSpans = editable.getSpans(
+                0,
+                editable.length,
+                Spans.MentionChipSpan::class.java
+            )
+            var mentionSpan: Spans.MentionChipSpan
+            for (i in mentionSpans.indices) {
+                mentionSpan = mentionSpans[i]
+                var mentionId = mentionSpan.id
+                val shouldQuote = mentionId.contains(" ") ||
+                    mentionId.contains("@") ||
+                    mentionId.startsWith("guest/") ||
+                    mentionId.startsWith("group/")
+                if (shouldQuote) {
+                    mentionId = "\"" + mentionId + "\""
+                }
+                editable.replace(editable.getSpanStart(mentionSpan), editable.getSpanEnd(mentionSpan), "@$mentionId")
+            }
+
+            binding.fragmentMessageInputView.inputEditText?.setText("")
+            sendStopTypingMessage()
+            val replyMessageId = binding.fragmentMessageInputView
+                .findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.tag as Int? ?: 0
+
+            sendMessage(
+                editable,
+                replyMessageId,
+                sendWithoutNotification
+            )
+            cancelReply()
+        }
+    }
+
+    private fun sendMessage(message: CharSequence, replyTo: Int?, sendWithoutNotification: Boolean) {
+        chatActivity.messageInputViewModel.sendChatMessage(
+            chatActivity.conversationUser!!.getCredentials(),
+            ApiUtils.getUrlForChat(
+                chatActivity.chatApiVersion,
+                chatActivity.conversationUser!!.baseUrl!!,
+                chatActivity.roomToken
+            ),
+            message,
+            chatActivity.conversationUser!!.displayName ?: "",
+            replyTo ?: 0,
+            sendWithoutNotification
+        )
+    }
+
+    private fun showSendButtonMenu() {
+        val popupMenu = PopupMenu(
+            ContextThemeWrapper(requireContext(), R.style.ChatSendButtonMenu),
+            binding.fragmentMessageInputView.button,
+            Gravity.END
+        )
+        popupMenu.inflate(R.menu.chat_send_menu)
+
+        popupMenu.setOnMenuItemClickListener { item: MenuItem ->
+            when (item.itemId) {
+                R.id.send_without_notification -> submitMessage(true)
+            }
+            true
+        }
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            popupMenu.setForceShowIcon(true)
+        }
+        popupMenu.show()
+    }
+
+    private fun editMessageAPI(message: ChatMessage, editedMessageText: String) {
+        // FIXME Fix API checking with guests?
+        val apiVersion: Int = ApiUtils.getChatApiVersion(chatActivity.spreedCapabilities, intArrayOf(1))
+
+        chatActivity.messageInputViewModel.editChatMessage(
+            chatActivity.credentials!!,
+            ApiUtils.getUrlForChatMessage(
+                apiVersion,
+                chatActivity.conversationUser!!.baseUrl!!,
+                chatActivity.roomToken,
+                message.id
+            ),
+            editedMessageText
+        )
+    }
+
+    private fun setEditUI(message: ChatMessage) {
+        binding.fragmentEditView.editMessage.text = message.message
+        binding.fragmentMessageInputView.inputEditText.setText(message.message)
+        val end = binding.fragmentMessageInputView.inputEditText.text.length
+        binding.fragmentMessageInputView.inputEditText.setSelection(end)
+        binding.fragmentMessageInputView.messageSendButton.visibility = View.GONE
+        binding.fragmentMessageInputView.recordAudioButton.visibility = View.GONE
+        binding.fragmentMessageInputView.editMessageButton.visibility = View.VISIBLE
+        binding.fragmentEditView.editMessageView.visibility = View.VISIBLE
+        binding.fragmentMessageInputView.attachmentButton.visibility = View.GONE
+    }
+
+    private fun clearEditUI() {
+        binding.fragmentMessageInputView.editMessageButton.visibility = View.GONE
+        binding.fragmentMessageInputView.inputEditText.setText("")
+        binding.fragmentEditView.editMessageView.visibility = View.GONE
+        binding.fragmentMessageInputView.attachmentButton.visibility = View.VISIBLE
+        chatActivity.messageInputViewModel.edit(null)
+    }
+
+    private fun themeMessageInputView() {
+        binding.fragmentMessageInputView.button?.let { viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) }
+
+        binding.fragmentMessageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.setOnClickListener {
+            cancelReply()
+        }
+
+        binding.fragmentMessageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.let {
+            viewThemeUtils.platform
+                .themeImageButton(it)
+        }
+
+        binding.fragmentMessageInputView.findViewById<MaterialButton>(R.id.playPauseBtn)?.let {
+            viewThemeUtils.material.colorMaterialButtonText(it)
+        }
+
+        binding.fragmentMessageInputView.findViewById<SeekBar>(R.id.seekbar)?.let {
+            viewThemeUtils.platform.themeHorizontalSeekBar(it)
+        }
+
+        binding.fragmentMessageInputView.findViewById<ImageView>(R.id.deleteVoiceRecording)?.let {
+            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
+        }
+        binding.fragmentMessageInputView.findViewById<ImageView>(R.id.sendVoiceRecording)?.let {
+            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
+        }
+
+        binding.fragmentMessageInputView.findViewById<ImageView>(R.id.microphoneEnabledInfo)?.let {
+            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
+        }
+
+        binding.fragmentMessageInputView.findViewById<LinearLayout>(R.id.voice_preview_container)?.let {
+            viewThemeUtils.talk.themeOutgoingMessageBubble(it, true, false)
+        }
+
+        binding.fragmentMessageInputView.findViewById<MicInputCloud>(R.id.micInputCloud)?.let {
+            viewThemeUtils.talk.themeMicInputCloud(it)
+        }
+        binding.fragmentMessageInputView.findViewById<ImageView>(R.id.editMessageButton)?.let {
+            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
+        }
+        binding.fragmentEditView.clearEdit.let {
+            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
+        }
+    }
+
+    private fun cancelReply() {
+        val quote = binding.fragmentMessageInputView
+            .findViewById<RelativeLayout>(R.id.quotedChatMessageView)
+        quote.visibility = View.GONE
+        quote.tag = null
+        binding.fragmentMessageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility = View.VISIBLE
+        chatActivity.messageInputViewModel.reply(null)
+    }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt
new file mode 100644
index 000000000..a5dc768c6
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt
@@ -0,0 +1,220 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Julius Linus juliuslinus1@gmail.com
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.chat
+
+import android.os.Bundle
+import android.os.SystemClock
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.SeekBar
+import android.widget.SeekBar.OnSeekBarChangeListener
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import autodagger.AutoInjector
+import com.nextcloud.android.common.ui.theme.utils.ColorRole
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager
+import com.nextcloud.talk.databinding.FragmentMessageInputVoiceRecordingBinding
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class MessageInputVoiceRecordingFragment : Fragment() {
+    companion object {
+        val TAG: String = MessageInputVoiceRecordingFragment::class.java.simpleName
+        private const val SEEK_LIMIT = 98
+
+        @JvmStatic
+        fun newInstance() = MessageInputVoiceRecordingFragment()
+    }
+
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    lateinit var binding: FragmentMessageInputVoiceRecordingBinding
+    private lateinit var chatActivity: ChatActivity
+    private var pause = false
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        sharedApplication!!.componentApplication.inject(this)
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        binding = FragmentMessageInputVoiceRecordingBinding.inflate(inflater)
+        chatActivity = (requireActivity() as ChatActivity)
+        themeVoiceRecordingView()
+        initVoiceRecordingView()
+        initObservers()
+        this.lifecycle.addObserver(chatActivity.messageInputViewModel)
+        return binding.root
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        this.lifecycle.removeObserver(chatActivity.messageInputViewModel)
+    }
+
+    private fun initObservers() {
+        chatActivity.messageInputViewModel.startMicInput(requireContext())
+        chatActivity.messageInputViewModel.micInputAudioObserver.observe(viewLifecycleOwner) {
+            binding.micInputCloud.setRotationSpeed(it.first, it.second)
+        }
+        chatActivity.messageInputViewModel.mediaPlayerSeekbarObserver.observe(viewLifecycleOwner) { progress ->
+            if (progress >= SEEK_LIMIT) {
+                togglePausePlay()
+                binding.seekbar.progress = 0
+            } else if (!pause) {
+                binding.seekbar.progress = progress
+            }
+        }
+
+        chatActivity.messageInputViewModel.getAudioFocusChange.observe(viewLifecycleOwner) { state ->
+            when (state) {
+                AudioFocusRequestManager.ManagerState.AUDIO_FOCUS_CHANGE_LOSS -> {
+                    if (chatActivity.messageInputViewModel.isVoicePreviewPlaying.value == true) {
+                        chatActivity.messageInputViewModel.stopMediaPlayer()
+                    }
+                }
+                AudioFocusRequestManager.ManagerState.AUDIO_FOCUS_CHANGE_LOSS_TRANSIENT -> {
+                    if (chatActivity.messageInputViewModel.isVoicePreviewPlaying.value == true) {
+                        chatActivity.messageInputViewModel.pauseMediaPlayer()
+                    }
+                }
+                AudioFocusRequestManager.ManagerState.BROADCAST_RECEIVED -> {
+                    if (chatActivity.messageInputViewModel.isVoicePreviewPlaying.value == true) {
+                        chatActivity.messageInputViewModel.pauseMediaPlayer()
+                    }
+                }
+            }
+        }
+    }
+
+    private fun initVoiceRecordingView() {
+        binding.deleteVoiceRecording.setOnClickListener {
+            chatActivity.chatViewModel.stopAndDiscardAudioRecording()
+            clear()
+        }
+
+        binding.sendVoiceRecording.setOnClickListener {
+            chatActivity.chatViewModel.stopAndSendAudioRecording(
+                chatActivity.roomToken,
+                chatActivity.currentConversation!!.displayName!!,
+                MessageInputFragment.VOICE_MESSAGE_META_DATA
+            )
+            clear()
+        }
+
+        binding.micInputCloud.setOnClickListener {
+            togglePreviewVisibility()
+        }
+
+        binding.playPauseBtn.setOnClickListener {
+            togglePausePlay()
+        }
+
+        binding.audioRecordDuration.base = chatActivity.messageInputViewModel.getRecordingTime.value ?: 0L
+        binding.audioRecordDuration.start()
+
+        binding.seekbar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
+            override fun onProgressChanged(seekbar: SeekBar, progress: Int, fromUser: Boolean) {
+                if (fromUser) {
+                    chatActivity.messageInputViewModel.seekMediaPlayerTo(progress)
+                }
+            }
+
+            override fun onStartTrackingTouch(p0: SeekBar) {
+                pause = true
+            }
+
+            override fun onStopTrackingTouch(p0: SeekBar) {
+                pause = false
+            }
+        })
+    }
+
+    private fun clear() {
+        chatActivity.chatViewModel.setVoiceRecordingLocked(false)
+        chatActivity.messageInputViewModel.stopMicInput()
+        chatActivity.chatViewModel.stopAudioRecording()
+        chatActivity.messageInputViewModel.stopMediaPlayer()
+        binding.audioRecordDuration.stop()
+        binding.audioRecordDuration.clearAnimation()
+    }
+
+    private fun togglePreviewVisibility() {
+        val visibility = binding.voicePreviewContainer.visibility
+        binding.voicePreviewContainer.visibility = if (visibility == View.VISIBLE) {
+            chatActivity.messageInputViewModel.stopMediaPlayer()
+            binding.playPauseBtn.icon = ContextCompat.getDrawable(
+                requireContext(),
+                R.drawable.ic_baseline_play_arrow_voice_message_24
+            )
+            pause = true
+            chatActivity.messageInputViewModel.startMicInput(requireContext())
+            chatActivity.chatViewModel.startAudioRecording(requireContext(), chatActivity.currentConversation!!)
+            binding.audioRecordDuration.visibility = View.VISIBLE
+            binding.audioRecordDuration.base = SystemClock.elapsedRealtime()
+            binding.audioRecordDuration.start()
+            View.GONE
+        } else {
+            pause = false
+            binding.seekbar.progress = 0
+            chatActivity.messageInputViewModel.stopMicInput()
+            chatActivity.chatViewModel.stopAudioRecording()
+            binding.audioRecordDuration.visibility = View.GONE
+            binding.audioRecordDuration.stop()
+            View.VISIBLE
+        }
+    }
+
+    private fun togglePausePlay() {
+        val path = chatActivity.chatViewModel.getCurrentVoiceRecordFile()
+        if (chatActivity.messageInputViewModel.isVoicePreviewPlaying.value == true) {
+            binding.playPauseBtn.icon = ContextCompat.getDrawable(
+                requireContext(),
+                R.drawable.ic_baseline_play_arrow_voice_message_24
+            )
+            chatActivity.messageInputViewModel.stopMediaPlayer()
+        } else {
+            binding.playPauseBtn.icon = ContextCompat.getDrawable(
+                requireContext(),
+                R.drawable.ic_baseline_pause_voice_message_24
+            )
+            chatActivity.messageInputViewModel.startMediaPlayer(path)
+        }
+    }
+
+    private fun themeVoiceRecordingView() {
+        binding.playPauseBtn.let {
+            viewThemeUtils.material.colorMaterialButtonText(it)
+        }
+
+        binding.seekbar.let {
+            viewThemeUtils.platform.themeHorizontalSeekBar(it)
+        }
+
+        binding.deleteVoiceRecording.let {
+            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
+        }
+        binding.sendVoiceRecording.let {
+            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
+        }
+
+        binding.voicePreviewContainer.let {
+            viewThemeUtils.talk.themeOutgoingMessageBubble(it, true, false)
+        }
+
+        binding.micInputCloud.let {
+            viewThemeUtils.talk.themeMicInputCloud(it)
+        }
+    }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/io/AudioFocusRequestManager.kt b/app/src/main/java/com/nextcloud/talk/chat/data/io/AudioFocusRequestManager.kt
new file mode 100644
index 000000000..922475cb2
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/chat/data/io/AudioFocusRequestManager.kt
@@ -0,0 +1,112 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.chat.data.io
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.media.AudioFocusRequest
+import android.media.AudioManager
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+
+/**
+ * Abstraction over the [AudioFocusManager](https://developer.android.com/reference/kotlin/android/media/AudioFocusRequest)
+ * class used to manage audio focus requests automatically
+ */
+class AudioFocusRequestManager(private val context: Context) {
+    companion object {
+        val TAG: String? = AudioFocusRequestManager::class.java.simpleName
+    }
+
+    enum class ManagerState {
+        AUDIO_FOCUS_CHANGE_LOSS,
+        AUDIO_FOCUS_CHANGE_LOSS_TRANSIENT,
+        BROADCAST_RECEIVED
+    }
+
+    private val _getManagerState: MutableLiveData<ManagerState> = MutableLiveData()
+    val getManagerState: LiveData<ManagerState>
+        get() = _getManagerState
+
+    private var isPausedDueToBecomingNoisy = false
+    private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+    private val duration = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
+    private val audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener =
+        AudioManager.OnAudioFocusChangeListener { flag ->
+            when (flag) {
+                AudioManager.AUDIOFOCUS_LOSS -> {
+                    isPausedDueToBecomingNoisy = false
+                    _getManagerState.value = ManagerState.AUDIO_FOCUS_CHANGE_LOSS
+                }
+
+                AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
+                    isPausedDueToBecomingNoisy = false
+                    _getManagerState.value = ManagerState.AUDIO_FOCUS_CHANGE_LOSS_TRANSIENT
+                }
+            }
+        }
+    private val noisyAudioStreamReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent?) {
+            isPausedDueToBecomingNoisy = true
+            _getManagerState.value = ManagerState.BROADCAST_RECEIVED
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    private val focusRequest = AudioFocusRequest.Builder(duration)
+        .setOnAudioFocusChangeListener(audioFocusChangeListener)
+        .build()
+
+    /**
+     * Requests the OS for audio focus, before executing the callback on success
+     */
+    fun audioFocusRequest(shouldRequestFocus: Boolean, onGranted: () -> Unit) {
+        if (isPausedDueToBecomingNoisy) {
+            onGranted()
+            return
+        }
+
+        val isGranted: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            if (shouldRequestFocus) {
+                audioManager.requestAudioFocus(focusRequest)
+            } else {
+                audioManager.abandonAudioFocusRequest(focusRequest)
+            }
+        } else {
+            @Deprecated("This method was deprecated in API level 26.")
+            if (shouldRequestFocus) {
+                audioManager.requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, duration)
+            } else {
+                audioManager.abandonAudioFocus(audioFocusChangeListener)
+            }
+        }
+        if (isGranted == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+            onGranted()
+            handleBecomingNoisyBroadcast(shouldRequestFocus)
+        }
+    }
+
+    private fun handleBecomingNoisyBroadcast(register: Boolean) {
+        try {
+            if (register) {
+                context.registerReceiver(
+                    noisyAudioStreamReceiver,
+                    IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
+                )
+            } else {
+                context.unregisterReceiver(noisyAudioStreamReceiver)
+            }
+        } catch (e: IllegalArgumentException) {
+            e.printStackTrace()
+        }
+    }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/io/AudioRecorderManager.kt b/app/src/main/java/com/nextcloud/talk/chat/data/io/AudioRecorderManager.kt
new file mode 100644
index 000000000..2a91e52c6
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/chat/data/io/AudioRecorderManager.kt
@@ -0,0 +1,143 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.chat.data.io
+
+import android.Manifest
+import android.content.Context
+import android.media.AudioFormat
+import android.media.AudioRecord
+import android.media.MediaRecorder
+import android.util.Log
+import androidx.core.content.ContextCompat
+import androidx.core.content.PermissionChecker
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.nextcloud.talk.ui.MicInputCloud
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlin.math.abs
+import kotlin.math.log10
+
+/**
+ * Abstraction over the [AudioRecord](https://developer.android.com/reference/android/media/AudioRecord) class used
+ * to manage the AudioRecord instance and the asynchronous updating of the MicInputCloud. Allows access to the raw
+ * bytes recorded from hardware.
+ */
+class AudioRecorderManager : LifecycleAwareManager {
+
+    companion object {
+        val TAG: String = AudioRecorderManager::class.java.simpleName
+        private const val SAMPLE_RATE = 8000
+        private const val AUDIO_MAX = 40
+        private const val AUDIO_MIN = 20
+        private const val AUDIO_INTERVAL = 50L
+    }
+    private val _getAudioValues: MutableLiveData<Pair<Float, Float>> = MutableLiveData()
+    val getAudioValues: LiveData<Pair<Float, Float>>
+        get() = _getAudioValues
+
+    private var scope = MainScope()
+    private var loop = false
+    private var audioRecorder: AudioRecord? = null
+    private val bufferSize = AudioRecord.getMinBufferSize(
+        SAMPLE_RATE,
+        AudioFormat.CHANNEL_IN_MONO,
+        AudioFormat.ENCODING_PCM_16BIT
+    )
+
+    /**
+     * Initializes and starts the AudioRecorder. Posts updates to the callback every 50 ms.
+     */
+    fun start(context: Context) {
+        if (audioRecorder == null || audioRecorder!!.state == AudioRecord.STATE_UNINITIALIZED) {
+            initAudioRecorder(context)
+        }
+        Log.d(TAG, "AudioRecorder started")
+        audioRecorder!!.startRecording()
+        loop = true
+        scope = MainScope().apply {
+            launch {
+                Log.d(TAG, "MicInputObserver started")
+                micInputObserver()
+            }
+        }
+    }
+
+    /**
+     * Stops and destroys the AudioRecorder. Updates cancelled.
+     */
+    fun stop() {
+        if (audioRecorder == null || audioRecorder!!.state == AudioRecord.STATE_UNINITIALIZED) {
+            Log.e(TAG, "Stopped AudioRecord on invalid state ")
+            return
+        }
+        Log.d(TAG, "AudioRecorder stopped")
+        loop = false
+        audioRecorder!!.stop()
+        audioRecorder!!.release()
+        audioRecorder = null
+    }
+
+    private suspend fun micInputObserver() {
+        withContext(Dispatchers.IO) {
+            while (true) {
+                if (!loop) {
+                    return@withContext
+                }
+                val byteArr = ByteArray(bufferSize / 2)
+                audioRecorder!!.read(byteArr, 0, byteArr.size)
+                val x = abs(byteArr[0].toFloat())
+                val logX = log10(x)
+                if (x > AUDIO_MAX) {
+                    _getAudioValues.postValue(Pair(logX, MicInputCloud.MAXIMUM_RADIUS))
+                } else if (x > AUDIO_MIN) {
+                    _getAudioValues.postValue(Pair(logX, MicInputCloud.EXTENDED_RADIUS))
+                } else {
+                    _getAudioValues.postValue(Pair(1f, MicInputCloud.DEFAULT_RADIUS))
+                }
+
+                delay(AUDIO_INTERVAL)
+            }
+        }
+    }
+
+    private fun initAudioRecorder(context: Context) {
+        val permissionCheck = ContextCompat.checkSelfPermission(
+            context,
+            Manifest.permission.RECORD_AUDIO
+        )
+
+        if (permissionCheck == PermissionChecker.PERMISSION_GRANTED) {
+            Log.d(TAG, "AudioRecorder init")
+            audioRecorder = AudioRecord(
+                MediaRecorder.AudioSource.MIC,
+                SAMPLE_RATE,
+                AudioFormat.CHANNEL_IN_MONO,
+                AudioFormat.ENCODING_PCM_16BIT,
+                bufferSize
+            )
+        }
+    }
+
+    override fun handleOnPause() {
+        // unused atm
+    }
+
+    override fun handleOnResume() {
+        // unused atm
+    }
+
+    override fun handleOnStop() {
+        scope.cancel()
+        stop()
+    }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/io/LifecycleAwareManager.kt b/app/src/main/java/com/nextcloud/talk/chat/data/io/LifecycleAwareManager.kt
new file mode 100644
index 000000000..78436050b
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/chat/data/io/LifecycleAwareManager.kt
@@ -0,0 +1,32 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.chat.data.io
+
+/**
+ * Interface used by manager classes in the data layer. Enforces that every Manager handles the lifecycle events
+ * observed by the view model.
+ */
+interface LifecycleAwareManager {
+    /**
+     * See [onPause](https://developer.android.com/guide/components/activities/activity-lifecycle#onpause)
+     * for more details.
+     */
+    fun handleOnPause()
+
+    /**
+     * See [onResume](https://developer.android.com/guide/components/activities/activity-lifecycle#onresume)
+     * for more details.
+     */
+    fun handleOnResume()
+
+    /**
+     * See [onStop](https://developer.android.com/guide/components/activities/activity-lifecycle#onstop)
+     * for more details.
+     */
+    fun handleOnStop()
+}
diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaPlayerManager.kt b/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaPlayerManager.kt
new file mode 100644
index 000000000..e610d2358
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaPlayerManager.kt
@@ -0,0 +1,138 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.chat.data.io
+
+import android.media.MediaPlayer
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.nextcloud.talk.chat.ChatActivity
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Abstraction over the [MediaPlayer](https://developer.android.com/reference/android/media/MediaPlayer) class used
+ * to manage the MediaPlayer instance.
+ */
+class MediaPlayerManager : LifecycleAwareManager {
+    companion object {
+        val TAG: String = MediaPlayerManager::class.java.simpleName
+        private const val SEEKBAR_UPDATE_DELAY = 15L
+        const val DIVIDER = 100f
+    }
+
+    private var mediaPlayer: MediaPlayer? = null
+    private var mediaPlayerPosition: Int = 0
+    private var loop = false
+    private var scope = MainScope()
+    var mediaPlayerDuration: Int = 0
+    private val _mediaPlayerSeekBarPosition: MutableLiveData<Int> = MutableLiveData()
+    val mediaPlayerSeekBarPosition: LiveData<Int>
+        get() = _mediaPlayerSeekBarPosition
+
+    /**
+     * Starts playing audio from the given path, initializes or resumes if the player is already created.
+     */
+    fun start(path: String) {
+        if (mediaPlayer == null || !scope.isActive) {
+            init(path)
+        } else {
+            mediaPlayer!!.start()
+            loop = true
+            scope.launch { seekbarUpdateObserver() }
+        }
+    }
+
+    /**
+     * Stop and destroys the player.
+     */
+    fun stop() {
+        if (mediaPlayer != null) {
+            Log.d(TAG, "media player destroyed")
+            loop = false
+            mediaPlayer!!.stop()
+            mediaPlayer!!.release()
+            mediaPlayer = null
+        }
+    }
+
+    /**
+     * Pauses the player.
+     */
+    fun pause() {
+        if (mediaPlayer != null) {
+            Log.d(TAG, "media player paused")
+            mediaPlayer!!.pause()
+        }
+    }
+
+    /**
+     * Seeks the player to the given position, saves position for resynchronization.
+     */
+    fun seekTo(progress: Int) {
+        if (mediaPlayer != null) {
+            val pos = mediaPlayer!!.duration * (progress / DIVIDER)
+            mediaPlayer!!.seekTo(pos.toInt())
+            mediaPlayerPosition = pos.toInt()
+        }
+    }
+
+    private suspend fun seekbarUpdateObserver() {
+        withContext(Dispatchers.IO) {
+            while (true) {
+                if (!loop) {
+                    return@withContext
+                }
+                if (mediaPlayer != null && mediaPlayer!!.isPlaying) {
+                    val pos = mediaPlayer!!.currentPosition
+                    val progress = (pos.toFloat() / mediaPlayerDuration) * DIVIDER
+                    _mediaPlayerSeekBarPosition.postValue(progress.toInt())
+                }
+
+                delay(SEEKBAR_UPDATE_DELAY)
+            }
+        }
+    }
+
+    @Suppress("Detekt.TooGenericExceptionCaught")
+    private fun init(path: String) {
+        try {
+            mediaPlayer = MediaPlayer().apply {
+                setDataSource(path)
+                prepareAsync()
+                setOnPreparedListener {
+                    mediaPlayerDuration = it.duration
+                    start()
+                    loop = true
+                    scope = MainScope()
+                    scope.launch { seekbarUpdateObserver() }
+                }
+            }
+        } catch (e: Exception) {
+            Log.e(ChatActivity.TAG, "failed to initialize mediaPlayer", e)
+        }
+    }
+
+    override fun handleOnPause() {
+        // unused atm
+    }
+
+    override fun handleOnResume() {
+        // unused atm
+    }
+
+    override fun handleOnStop() {
+        stop()
+        scope.cancel()
+    }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaRecorderManager.kt b/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaRecorderManager.kt
new file mode 100644
index 000000000..6e2bae64b
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaRecorderManager.kt
@@ -0,0 +1,165 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.chat.data.io
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.media.MediaRecorder
+import android.util.Log
+import com.nextcloud.talk.R
+import com.nextcloud.talk.models.domain.ConversationModel
+import java.io.IOException
+import java.text.SimpleDateFormat
+import java.util.Date
+
+/**
+ * Abstraction over the [MediaRecorder](https://developer.android.com/reference/android/media/MediaRecorder) class
+ * used to manage the MediaRecorder instance and it's state changes. Google doesn't provide a way of accessing state
+ * directly, so this handles the changes without exposing the user to it.
+ */
+class MediaRecorderManager : LifecycleAwareManager {
+
+    companion object {
+        val TAG: String = MediaRecorderManager::class.java.simpleName
+        private const val VOICE_MESSAGE_SAMPLING_RATE = 22050
+        private const val VOICE_MESSAGE_ENCODING_BIT_RATE = 32000
+        private const val VOICE_MESSAGE_CHANNELS = 1
+        private const val FILE_DATE_PATTERN = "yyyy-MM-dd HH-mm-ss"
+        private const val VOICE_MESSAGE_FILE_SUFFIX = ".mp3"
+    }
+
+    var currentVoiceRecordFile: String = ""
+
+    enum class MediaRecorderState {
+        INITIAL,
+        INITIALIZED,
+        CONFIGURED,
+        PREPARED,
+        RECORDING,
+        RELEASED,
+        ERROR
+    }
+    private var _mediaRecorderState: MediaRecorderState = MediaRecorderState.INITIAL
+    val mediaRecorderState: MediaRecorderState
+        get() = _mediaRecorderState
+    private var recorder: MediaRecorder? = null
+
+    /**
+     * Initializes and starts the MediaRecorder
+     */
+    fun start(context: Context, currentConversation: ConversationModel) {
+        if (_mediaRecorderState == MediaRecorderState.ERROR ||
+            _mediaRecorderState == MediaRecorderState.RELEASED
+        ) {
+            _mediaRecorderState = MediaRecorderState.INITIAL
+        }
+
+        if (_mediaRecorderState == MediaRecorderState.INITIAL) {
+            setVoiceRecordFileName(context, currentConversation)
+            initAndStartRecorder()
+        } else {
+            Log.e(TAG, "Started MediaRecorder with invalid state ${_mediaRecorderState.name}")
+        }
+    }
+
+    /**
+     * Stops and destroys the MediaRecorder
+     */
+    fun stop() {
+        if (_mediaRecorderState != MediaRecorderState.RELEASED) {
+            stopAndDestroyRecorder()
+        } else {
+            Log.e(TAG, "Stopped MediaRecorder with invalid state ${_mediaRecorderState.name}")
+        }
+    }
+
+    private fun initAndStartRecorder() {
+        recorder = MediaRecorder().apply {
+            setAudioSource(MediaRecorder.AudioSource.MIC)
+            _mediaRecorderState = MediaRecorderState.INITIALIZED
+
+            setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
+            _mediaRecorderState = MediaRecorderState.CONFIGURED
+
+            setOutputFile(currentVoiceRecordFile)
+            setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
+            setAudioSamplingRate(VOICE_MESSAGE_SAMPLING_RATE)
+            setAudioEncodingBitRate(VOICE_MESSAGE_ENCODING_BIT_RATE)
+            setAudioChannels(VOICE_MESSAGE_CHANNELS)
+
+            try {
+                prepare()
+                _mediaRecorderState = MediaRecorderState.PREPARED
+            } catch (e: IOException) {
+                _mediaRecorderState = MediaRecorderState.ERROR
+                Log.e(TAG, "prepare for audio recording failed")
+            }
+
+            try {
+                start()
+                _mediaRecorderState = MediaRecorderState.RECORDING
+                Log.d(TAG, "recording started")
+            } catch (e: IllegalStateException) {
+                _mediaRecorderState = MediaRecorderState.ERROR
+                Log.e(TAG, "start for audio recording failed")
+            }
+        }
+    }
+
+    @Suppress("TooGenericExceptionCaught")
+    private fun stopAndDestroyRecorder() {
+        recorder?.apply {
+            try {
+                if (_mediaRecorderState == MediaRecorderState.RECORDING) {
+                    stop()
+                    reset()
+                    _mediaRecorderState = MediaRecorderState.INITIAL
+                    Log.d(TAG, "stopped recorder")
+                }
+                release()
+                _mediaRecorderState = MediaRecorderState.RELEASED
+            } catch (e: Exception) {
+                when (e) {
+                    is java.lang.IllegalStateException,
+                    is java.lang.RuntimeException -> {
+                        _mediaRecorderState = MediaRecorderState.ERROR
+                        Log.e(TAG, "error while stopping recorder! with state $_mediaRecorderState $e")
+                    }
+                }
+            }
+        }
+        recorder = null
+    }
+
+    @SuppressLint("SimpleDateFormat")
+    private fun setVoiceRecordFileName(context: Context, currentConversation: ConversationModel) {
+        val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN)
+        val date: String = simpleDateFormat.format(Date())
+
+        val fileNameWithoutSuffix = String.format(
+            context.resources.getString(R.string.nc_voice_message_filename),
+            date,
+            currentConversation.displayName
+        )
+        val fileName = fileNameWithoutSuffix + VOICE_MESSAGE_FILE_SUFFIX
+
+        currentVoiceRecordFile = "${context.cacheDir.absolutePath}/$fileName"
+    }
+
+    override fun handleOnPause() {
+        // unused atm
+    }
+
+    override fun handleOnResume() {
+        // unused atm
+    }
+
+    override fun handleOnStop() {
+        stop()
+    }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
index c681b0774..12c55b37e 100644
--- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
+++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
@@ -6,6 +6,8 @@
  */
 package com.nextcloud.talk.chat.viewmodels
 
+import android.content.Context
+import android.net.Uri
 import android.util.Log
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.LifecycleOwner
@@ -13,7 +15,10 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import com.nextcloud.talk.chat.data.ChatRepository
+import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager
+import com.nextcloud.talk.chat.data.io.MediaRecorderManager
 import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
 import com.nextcloud.talk.models.domain.ConversationModel
 import com.nextcloud.talk.models.domain.ReactionAddedModel
 import com.nextcloud.talk.models.domain.ReactionDeletedModel
@@ -31,34 +36,58 @@ import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.disposables.Disposable
 import io.reactivex.schedulers.Schedulers
 import retrofit2.Response
+import java.io.File
 import javax.inject.Inject
 
 @Suppress("TooManyFunctions", "LongParameterList")
 class ChatViewModel @Inject constructor(
     private val chatRepository: ChatRepository,
-    private val reactionsRepository: ReactionsRepository
-) : ViewModel() {
+    private val reactionsRepository: ReactionsRepository,
+    private val mediaRecorderManager: MediaRecorderManager,
+    private val audioFocusRequestManager: AudioFocusRequestManager
+) : ViewModel(), DefaultLifecycleObserver {
 
-    object LifeCycleObserver : DefaultLifecycleObserver {
-        enum class LifeCycleFlag {
-            PAUSED,
-            RESUMED
-        }
-        lateinit var currentLifeCycleFlag: LifeCycleFlag
-        public val disposableSet = mutableSetOf<Disposable>()
-
-        override fun onResume(owner: LifecycleOwner) {
-            super.onResume(owner)
-            currentLifeCycleFlag = LifeCycleFlag.RESUMED
-        }
-
-        override fun onPause(owner: LifecycleOwner) {
-            super.onPause(owner)
-            currentLifeCycleFlag = LifeCycleFlag.PAUSED
-            disposableSet.forEach { disposable -> disposable.dispose() }
-            disposableSet.clear()
-        }
+    enum class LifeCycleFlag {
+        PAUSED,
+        RESUMED,
+        STOPPED
     }
+    lateinit var currentLifeCycleFlag: LifeCycleFlag
+    val disposableSet = mutableSetOf<Disposable>()
+
+    override fun onResume(owner: LifecycleOwner) {
+        super.onResume(owner)
+        currentLifeCycleFlag = LifeCycleFlag.RESUMED
+        mediaRecorderManager.handleOnResume()
+    }
+
+    override fun onPause(owner: LifecycleOwner) {
+        super.onPause(owner)
+        currentLifeCycleFlag = LifeCycleFlag.PAUSED
+        disposableSet.forEach { disposable -> disposable.dispose() }
+        disposableSet.clear()
+        mediaRecorderManager.handleOnPause()
+    }
+
+    override fun onStop(owner: LifecycleOwner) {
+        super.onStop(owner)
+        currentLifeCycleFlag = LifeCycleFlag.STOPPED
+        mediaRecorderManager.handleOnStop()
+    }
+    val getAudioFocusChange: LiveData<AudioFocusRequestManager.ManagerState>
+        get() = audioFocusRequestManager.getManagerState
+
+    private val _recordTouchObserver: MutableLiveData<Float> = MutableLiveData()
+    val recordTouchObserver: LiveData<Float>
+        get() = _recordTouchObserver
+
+    private val _getVoiceRecordingInProgress: MutableLiveData<Boolean> = MutableLiveData()
+    val getVoiceRecordingInProgress: LiveData<Boolean>
+        get() = _getVoiceRecordingInProgress
+
+    private val _getVoiceRecordingLocked: MutableLiveData<Boolean> = MutableLiveData()
+    val getVoiceRecordingLocked: LiveData<Boolean>
+        get() = _getVoiceRecordingLocked
 
     private val _getFieldMapForChat: MutableLiveData<HashMap<String, Int>> = MutableLiveData()
     val getFieldMapForChat: LiveData<HashMap<String, Int>>
@@ -70,10 +99,6 @@ class ChatViewModel @Inject constructor(
 
     private val _getReminderExistState: MutableLiveData<ViewState> = MutableLiveData(GetReminderStartState)
 
-    var isPausedDueToBecomingNoisy = false
-    var receiverRegistered = false
-    var receiverUnregistered = false
-
     val getReminderExistState: LiveData<ViewState>
         get() = _getReminderExistState
 
@@ -94,7 +119,8 @@ class ChatViewModel @Inject constructor(
 
     object GetCapabilitiesStartState : ViewState
     object GetCapabilitiesErrorState : ViewState
-    open class GetCapabilitiesSuccessState(val spreedCapabilities: SpreedCapability) : ViewState
+    open class GetCapabilitiesInitialLoadState(val spreedCapabilities: SpreedCapability) : ViewState
+    open class GetCapabilitiesUpdateState(val spreedCapabilities: SpreedCapability) : ViewState
 
     private val _getCapabilitiesViewState: MutableLiveData<ViewState> = MutableLiveData(GetCapabilitiesStartState)
     val getCapabilitiesViewState: LiveData<ViewState>
@@ -156,14 +182,6 @@ class ChatViewModel @Inject constructor(
     val reactionDeletedViewState: LiveData<ViewState>
         get() = _reactionDeletedViewState
 
-    object EditMessageStartState : ViewState
-    object EditMessageErrorState : ViewState
-    class EditMessageSuccessState(val messageEdited: ChatOverallSingleMessage) : ViewState
-
-    private val _editMessageViewState: MutableLiveData<ViewState> = MutableLiveData(EditMessageStartState)
-    val editMessageViewState: LiveData<ViewState>
-        get() = _editMessageViewState
-
     fun refreshChatParams(pullChatMessagesFieldMap: HashMap<String, Int>, overrideRefresh: Boolean = false) {
         if (pullChatMessagesFieldMap != _getFieldMapForChat.value || overrideRefresh) {
             _getFieldMapForChat.postValue(pullChatMessagesFieldMap)
@@ -180,21 +198,30 @@ class ChatViewModel @Inject constructor(
     }
 
     fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) {
-        _getCapabilitiesViewState.value = GetCapabilitiesStartState
-
+        Log.d(TAG, "Remote server ${conversationModel.remoteServer}")
         if (conversationModel.remoteServer.isNullOrEmpty()) {
-            _getCapabilitiesViewState.value = GetCapabilitiesSuccessState(user.capabilities!!.spreedCapability!!)
+            if (_getCapabilitiesViewState.value == GetCapabilitiesStartState) {
+                _getCapabilitiesViewState.value = GetCapabilitiesInitialLoadState(
+                    user.capabilities!!.spreedCapability!!
+                )
+            } else {
+                _getCapabilitiesViewState.value = GetCapabilitiesUpdateState(user.capabilities!!.spreedCapability!!)
+            }
         } else {
             chatRepository.getCapabilities(user, token)
                 .subscribeOn(Schedulers.io())
                 ?.observeOn(AndroidSchedulers.mainThread())
                 ?.subscribe(object : Observer<SpreedCapability> {
                     override fun onSubscribe(d: Disposable) {
-                        LifeCycleObserver.disposableSet.add(d)
+                        disposableSet.add(d)
                     }
 
                     override fun onNext(spreedCapabilities: SpreedCapability) {
-                        _getCapabilitiesViewState.value = GetCapabilitiesSuccessState(spreedCapabilities)
+                        if (_getCapabilitiesViewState.value == GetCapabilitiesStartState) {
+                            _getCapabilitiesViewState.value = GetCapabilitiesInitialLoadState(spreedCapabilities)
+                        } else {
+                            _getCapabilitiesViewState.value = GetCapabilitiesUpdateState(spreedCapabilities)
+                        }
                     }
 
                     override fun onError(e: Throwable) {
@@ -238,7 +265,7 @@ class ChatViewModel @Inject constructor(
             ?.observeOn(AndroidSchedulers.mainThread())
             ?.subscribe(object : Observer<GenericOverall> {
                 override fun onSubscribe(d: Disposable) {
-                    LifeCycleObserver.disposableSet.add(d)
+                    disposableSet.add(d)
                 }
 
                 override fun onNext(genericOverall: GenericOverall) {
@@ -262,7 +289,7 @@ class ChatViewModel @Inject constructor(
             ?.observeOn(AndroidSchedulers.mainThread())
             ?.subscribe(object : Observer<GenericOverall> {
                 override fun onSubscribe(d: Disposable) {
-                    LifeCycleObserver.disposableSet.add(d)
+                    disposableSet.add(d)
                 }
 
                 override fun onError(e: Throwable) {
@@ -275,6 +302,8 @@ class ChatViewModel @Inject constructor(
 
                 override fun onNext(t: GenericOverall) {
                     _leaveRoomViewState.value = LeaveRoomSuccessState(funToCallWhenLeaveSuccessful)
+                    _getCapabilitiesViewState.value = GetCapabilitiesStartState
+                    _getRoomViewState.value = GetRoomStartState
                 }
             })
     }
@@ -285,7 +314,7 @@ class ChatViewModel @Inject constructor(
             .observeOn(AndroidSchedulers.mainThread())
             .subscribe(object : Observer<RoomOverall> {
                 override fun onSubscribe(d: Disposable) {
-                    LifeCycleObserver.disposableSet.add(d)
+                    disposableSet.add(d)
                 }
 
                 override fun onError(e: Throwable) {
@@ -322,7 +351,7 @@ class ChatViewModel @Inject constructor(
             ?.observeOn(AndroidSchedulers.mainThread())
             ?.subscribe(object : Observer<GenericOverall> {
                 override fun onSubscribe(d: Disposable) {
-                    LifeCycleObserver.disposableSet.add(d)
+                    disposableSet.add(d)
                 }
 
                 override fun onError(e: Throwable) {
@@ -342,12 +371,12 @@ class ChatViewModel @Inject constructor(
     fun pullChatMessages(credentials: String, url: String) {
         chatRepository.pullChatMessages(credentials, url, _getFieldMapForChat.value!!)
             .subscribeOn(Schedulers.io())
-            .takeUntil { (LifeCycleObserver.currentLifeCycleFlag == LifeCycleObserver.LifeCycleFlag.PAUSED) }
+            .takeUntil { (currentLifeCycleFlag == LifeCycleFlag.PAUSED) }
             ?.observeOn(AndroidSchedulers.mainThread())
             ?.subscribe(object : Observer<Response<*>> {
                 override fun onSubscribe(d: Disposable) {
                     Log.d(TAG, "pullChatMessages - pullChatMessages SUBSCRIBE")
-                    LifeCycleObserver.disposableSet.add(d)
+                    disposableSet.add(d)
                 }
 
                 override fun onError(e: Throwable) {
@@ -373,7 +402,7 @@ class ChatViewModel @Inject constructor(
             ?.observeOn(AndroidSchedulers.mainThread())
             ?.subscribe(object : Observer<ChatOverallSingleMessage> {
                 override fun onSubscribe(d: Disposable) {
-                    LifeCycleObserver.disposableSet.add(d)
+                    disposableSet.add(d)
                 }
 
                 override fun onError(e: Throwable) {
@@ -402,7 +431,7 @@ class ChatViewModel @Inject constructor(
             .observeOn(AndroidSchedulers.mainThread())
             .subscribe(object : Observer<GenericOverall> {
                 override fun onSubscribe(d: Disposable) {
-                    LifeCycleObserver.disposableSet.add(d)
+                    disposableSet.add(d)
                 }
 
                 override fun onError(e: Throwable) {
@@ -425,7 +454,7 @@ class ChatViewModel @Inject constructor(
             ?.observeOn(AndroidSchedulers.mainThread())
             ?.subscribe(object : Observer<GenericOverall> {
                 override fun onSubscribe(d: Disposable) {
-                    LifeCycleObserver.disposableSet.add(d)
+                    disposableSet.add(d)
                 }
 
                 override fun onNext(genericOverall: GenericOverall) {
@@ -454,7 +483,7 @@ class ChatViewModel @Inject constructor(
             ?.observeOn(AndroidSchedulers.mainThread())
             ?.subscribe(object : Observer<GenericOverall> {
                 override fun onSubscribe(d: Disposable) {
-                    LifeCycleObserver.disposableSet.add(d)
+                    disposableSet.add(d)
                 }
 
                 override fun onNext(genericOverall: GenericOverall) {
@@ -477,7 +506,7 @@ class ChatViewModel @Inject constructor(
             ?.observeOn(AndroidSchedulers.mainThread())
             ?.subscribe(object : Observer<ReactionDeletedModel> {
                 override fun onSubscribe(d: Disposable) {
-                    LifeCycleObserver.disposableSet.add(d)
+                    disposableSet.add(d)
                 }
 
                 override fun onError(e: Throwable) {
@@ -502,7 +531,7 @@ class ChatViewModel @Inject constructor(
             ?.observeOn(AndroidSchedulers.mainThread())
             ?.subscribe(object : Observer<ReactionAddedModel> {
                 override fun onSubscribe(d: Disposable) {
-                    LifeCycleObserver.disposableSet.add(d)
+                    disposableSet.add(d)
                 }
 
                 override fun onError(e: Throwable) {
@@ -521,28 +550,69 @@ class ChatViewModel @Inject constructor(
             })
     }
 
-    fun editChatMessage(credentials: String, url: String, text: String) {
-        chatRepository.editChatMessage(credentials, url, text)
-            .subscribeOn(Schedulers.io())
-            ?.observeOn(AndroidSchedulers.mainThread())
-            ?.subscribe(object : Observer<ChatOverallSingleMessage> {
-                override fun onSubscribe(d: Disposable) {
-                    LifeCycleObserver.disposableSet.add(d)
-                }
+    fun startAudioRecording(context: Context, currentConversation: ConversationModel) {
+        audioFocusRequestManager.audioFocusRequest(true) {
+            Log.d(TAG, "Recording Started")
+            mediaRecorderManager.start(context, currentConversation)
+            _getVoiceRecordingInProgress.postValue(true)
+        }
+    }
 
-                override fun onError(e: Throwable) {
-                    Log.e(TAG, "failed to edit message", e)
-                    _editMessageViewState.value = EditMessageErrorState
-                }
+    fun stopAudioRecording() {
+        audioFocusRequestManager.audioFocusRequest(false) {
+            mediaRecorderManager.stop()
+            _getVoiceRecordingInProgress.postValue(false)
+            Log.d(TAG, "Recording stopped")
+        }
+    }
 
-                override fun onComplete() {
-                    // unused atm
-                }
+    fun stopAndSendAudioRecording(room: String, displayName: String, metaData: String) {
+        stopAudioRecording()
 
-                override fun onNext(messageEdited: ChatOverallSingleMessage) {
-                    _editMessageViewState.value = EditMessageSuccessState(messageEdited)
-                }
-            })
+        if (mediaRecorderManager.mediaRecorderState != MediaRecorderManager.MediaRecorderState.ERROR) {
+            val uri = Uri.fromFile(File(mediaRecorderManager.currentVoiceRecordFile))
+            Log.d(TAG, "File uploaded")
+            uploadFile(uri.toString(), room, displayName, metaData)
+        }
+    }
+    fun stopAndDiscardAudioRecording() {
+        stopAudioRecording()
+        Log.d(TAG, "File discarded")
+        val cachedFile = File(mediaRecorderManager.currentVoiceRecordFile)
+        cachedFile.delete()
+    }
+
+    fun getCurrentVoiceRecordFile(): String {
+        return mediaRecorderManager.currentVoiceRecordFile
+    }
+
+    fun uploadFile(fileUri: String, room: String, displayName: String, metaData: String) {
+        try {
+            require(fileUri.isNotEmpty())
+            UploadAndShareFilesWorker.upload(
+                fileUri,
+                room,
+                displayName,
+                metaData
+            )
+        } catch (e: IllegalArgumentException) {
+            Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
+        }
+    }
+
+    fun postToRecordTouchObserver(float: Float) {
+        _recordTouchObserver.postValue(float)
+    }
+
+    fun setVoiceRecordingLocked(boolean: Boolean) {
+        _getVoiceRecordingLocked.postValue(boolean)
+    }
+
+    // Made this so that the MediaPlayer in ChatActivity can be focused. Eventually the player logic should be moved
+    // to the MediaPlayerManager class, so the audio focus logic can be handled in ChatViewModel, as it's done in
+    // the MessageInputViewModel
+    fun audioRequest(request: Boolean, callback: () -> Unit) {
+        audioFocusRequestManager.audioFocusRequest(request, callback)
     }
 
     inner class GetRoomObserver : Observer<ConversationModel> {
@@ -566,7 +636,7 @@ class ChatViewModel @Inject constructor(
 
     inner class JoinRoomObserver : Observer<ConversationModel> {
         override fun onSubscribe(d: Disposable) {
-            LifeCycleObserver.disposableSet.add(d)
+            disposableSet.add(d)
         }
 
         override fun onNext(conversationModel: ConversationModel) {
@@ -585,7 +655,7 @@ class ChatViewModel @Inject constructor(
 
     inner class SetReminderObserver : Observer<Reminder> {
         override fun onSubscribe(d: Disposable) {
-            LifeCycleObserver.disposableSet.add(d)
+            disposableSet.add(d)
         }
 
         override fun onNext(reminder: Reminder) {
@@ -603,7 +673,7 @@ class ChatViewModel @Inject constructor(
 
     inner class GetReminderObserver : Observer<Reminder> {
         override fun onSubscribe(d: Disposable) {
-            LifeCycleObserver.disposableSet.add(d)
+            disposableSet.add(d)
         }
 
         override fun onNext(reminder: Reminder) {
@@ -622,7 +692,7 @@ class ChatViewModel @Inject constructor(
 
     inner class CheckForNoteToSelfObserver : Observer<RoomsOverall> {
         override fun onSubscribe(d: Disposable) {
-            LifeCycleObserver.disposableSet.add(d)
+            disposableSet.add(d)
         }
 
         override fun onNext(roomsOverall: RoomsOverall) {
diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt
new file mode 100644
index 000000000..509268ac8
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt
@@ -0,0 +1,219 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Julius Linus juliuslinus1@gmail.com
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.chat.viewmodels
+
+import android.content.Context
+import android.util.Log
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.nextcloud.talk.chat.data.ChatRepository
+import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager
+import com.nextcloud.talk.chat.data.io.AudioRecorderManager
+import com.nextcloud.talk.chat.data.io.MediaPlayerManager
+import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
+import com.nextcloud.talk.models.json.generic.GenericOverall
+import com.stfalcon.chatkit.commons.models.IMessage
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import javax.inject.Inject
+
+class MessageInputViewModel @Inject constructor(
+    private val chatRepository: ChatRepository,
+    private val audioRecorderManager: AudioRecorderManager,
+    private val mediaPlayerManager: MediaPlayerManager,
+    private val audioFocusRequestManager: AudioFocusRequestManager
+) : ViewModel(), DefaultLifecycleObserver {
+    enum class LifeCycleFlag {
+        PAUSED,
+        RESUMED,
+        STOPPED
+    }
+    lateinit var currentLifeCycleFlag: LifeCycleFlag
+    val disposableSet = mutableSetOf<Disposable>()
+
+    override fun onResume(owner: LifecycleOwner) {
+        super.onResume(owner)
+        currentLifeCycleFlag = LifeCycleFlag.RESUMED
+        audioRecorderManager.handleOnResume()
+        mediaPlayerManager.handleOnResume()
+    }
+
+    override fun onPause(owner: LifecycleOwner) {
+        super.onPause(owner)
+        currentLifeCycleFlag = LifeCycleFlag.PAUSED
+        disposableSet.forEach { disposable -> disposable.dispose() }
+        disposableSet.clear()
+        audioRecorderManager.handleOnPause()
+        mediaPlayerManager.handleOnPause()
+    }
+
+    override fun onStop(owner: LifecycleOwner) {
+        super.onStop(owner)
+        currentLifeCycleFlag = LifeCycleFlag.STOPPED
+        audioRecorderManager.handleOnStop()
+        mediaPlayerManager.handleOnStop()
+    }
+
+    companion object {
+        private val TAG = MessageInputViewModel::class.java.simpleName
+    }
+    val getAudioFocusChange: LiveData<AudioFocusRequestManager.ManagerState>
+        get() = audioFocusRequestManager.getManagerState
+
+    private val _getRecordingTime: MutableLiveData<Long> = MutableLiveData(0L)
+    val getRecordingTime: LiveData<Long>
+        get() = _getRecordingTime
+
+    val micInputAudioObserver: LiveData<Pair<Float, Float>>
+        get() = audioRecorderManager.getAudioValues
+
+    val mediaPlayerSeekbarObserver: LiveData<Int>
+        get() = mediaPlayerManager.mediaPlayerSeekBarPosition
+
+    private val _getEditChatMessage: MutableLiveData<IMessage?> = MutableLiveData()
+    val getEditChatMessage: LiveData<IMessage?>
+        get() = _getEditChatMessage
+
+    private val _getReplyChatMessage: MutableLiveData<IMessage?> = MutableLiveData()
+    val getReplyChatMessage: LiveData<IMessage?>
+        get() = _getReplyChatMessage
+
+    sealed interface ViewState
+    object SendChatMessageStartState : ViewState
+    class SendChatMessageSuccessState(val message: CharSequence) : ViewState
+    class SendChatMessageErrorState(val e: Throwable, val message: CharSequence) : ViewState
+    private val _sendChatMessageViewState: MutableLiveData<ViewState> = MutableLiveData(SendChatMessageStartState)
+    val sendChatMessageViewState: LiveData<ViewState>
+        get() = _sendChatMessageViewState
+    object EditMessageStartState : ViewState
+    object EditMessageErrorState : ViewState
+    class EditMessageSuccessState(val messageEdited: ChatOverallSingleMessage) : ViewState
+
+    private val _editMessageViewState: MutableLiveData<ViewState> = MutableLiveData()
+    val editMessageViewState: LiveData<ViewState>
+        get() = _editMessageViewState
+
+    private val _isVoicePreviewPlaying: MutableLiveData<Boolean> = MutableLiveData(false)
+    val isVoicePreviewPlaying: LiveData<Boolean>
+        get() = _isVoicePreviewPlaying
+
+    @Suppress("LongParameterList")
+    fun sendChatMessage(
+        credentials: String,
+        url: String,
+        message: CharSequence,
+        displayName: String,
+        replyTo: Int,
+        sendWithoutNotification: Boolean
+    ) {
+        chatRepository.sendChatMessage(
+            credentials,
+            url,
+            message,
+            displayName,
+            replyTo,
+            sendWithoutNotification
+        ).subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<GenericOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    disposableSet.add(d)
+                }
+
+                override fun onError(e: Throwable) {
+                    _sendChatMessageViewState.value = SendChatMessageErrorState(e, message)
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+
+                override fun onNext(t: GenericOverall) {
+                    _sendChatMessageViewState.value = SendChatMessageSuccessState(message)
+                }
+            })
+    }
+
+    fun editChatMessage(credentials: String, url: String, text: String) {
+        chatRepository.editChatMessage(credentials, url, text)
+            .subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<ChatOverallSingleMessage> {
+                override fun onSubscribe(d: Disposable) {
+                    disposableSet.add(d)
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, "failed to edit message", e)
+                    _editMessageViewState.value = EditMessageErrorState
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+
+                override fun onNext(messageEdited: ChatOverallSingleMessage) {
+                    _editMessageViewState.value = EditMessageSuccessState(messageEdited)
+                }
+            })
+    }
+
+    fun reply(message: IMessage?) {
+        _getReplyChatMessage.postValue(message)
+    }
+
+    fun edit(message: IMessage?) {
+        _getEditChatMessage.postValue(message)
+    }
+
+    fun startMicInput(context: Context) {
+        audioFocusRequestManager.audioFocusRequest(true) {
+            audioRecorderManager.start(context)
+        }
+    }
+
+    fun stopMicInput() {
+        audioFocusRequestManager.audioFocusRequest(false) {
+            audioRecorderManager.stop()
+        }
+    }
+
+    fun startMediaPlayer(path: String) {
+        audioFocusRequestManager.audioFocusRequest(true) {
+            mediaPlayerManager.start(path)
+            _isVoicePreviewPlaying.postValue(true)
+        }
+    }
+
+    fun pauseMediaPlayer() {
+        audioFocusRequestManager.audioFocusRequest(false) {
+            mediaPlayerManager.pause()
+            _isVoicePreviewPlaying.postValue(false)
+        }
+    }
+
+    fun stopMediaPlayer() {
+        audioFocusRequestManager.audioFocusRequest(false) {
+            mediaPlayerManager.stop()
+            _isVoicePreviewPlaying.postValue(false)
+        }
+    }
+
+    fun seekMediaPlayerTo(progress: Int) {
+        mediaPlayerManager.seekTo(progress)
+    }
+
+    fun setRecordingTime(time: Long) {
+        _getRecordingTime.postValue(time)
+    }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ManagerModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ManagerModule.kt
new file mode 100644
index 000000000..3ce6cdf19
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ManagerModule.kt
@@ -0,0 +1,40 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.dagger.modules
+
+import android.content.Context
+import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager
+import com.nextcloud.talk.chat.data.io.AudioRecorderManager
+import com.nextcloud.talk.chat.data.io.MediaPlayerManager
+import com.nextcloud.talk.chat.data.io.MediaRecorderManager
+import dagger.Module
+import dagger.Provides
+
+@Module
+class ManagerModule {
+
+    @Provides
+    fun provideMediaRecorderManager(): MediaRecorderManager {
+        return MediaRecorderManager()
+    }
+
+    @Provides
+    fun provideAudioRecorderManager(): AudioRecorderManager {
+        return AudioRecorderManager()
+    }
+
+    @Provides
+    fun provideMediaPlayerManager(): MediaPlayerManager {
+        return MediaPlayerManager()
+    }
+
+    @Provides
+    fun provideAudioFocusManager(context: Context): AudioFocusRequestManager {
+        return AudioFocusRequestManager(context)
+    }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt
index 55cf99769..e800e90af 100644
--- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt
+++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt
@@ -10,6 +10,7 @@ package com.nextcloud.talk.dagger.modules
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
 import com.nextcloud.talk.chat.viewmodels.ChatViewModel
+import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
 import com.nextcloud.talk.conversation.viewmodel.ConversationViewModel
 import com.nextcloud.talk.conversation.viewmodel.RenameConversationViewModel
 import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
@@ -118,6 +119,13 @@ abstract class ViewModelModule {
     @ViewModelKey(ChatViewModel::class)
     abstract fun chatViewModel(viewModel: ChatViewModel): ViewModel
 
+    @Binds
+    @IntoMap
+    @ViewModelKey(MessageInputViewModel::class)
+    abstract fun messageInputViewModel(viewModel: MessageInputViewModel): ViewModel
+
+    // TODO I had a merge conflict here that went weird. choose their version
+
     @Binds
     @IntoMap
     @ViewModelKey(ConversationInfoViewModel::class)
diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt
index b13e3bebf..7fddcf2e9 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt
@@ -335,8 +335,7 @@ class MessageActionsDialog(
 
     private fun initMenuEditMessage(visible: Boolean) {
         dialogMessageActionsBinding.menuEditMessage.setOnClickListener {
-            chatActivity.editMessage(message)
-            Log.d("EDIT MESSAGE", "$message")
+            chatActivity.messageInputViewModel.edit(message)
             dismiss()
         }
 
@@ -357,7 +356,7 @@ class MessageActionsDialog(
     private fun initMenuReplyToMessage(visible: Boolean) {
         if (visible) {
             dialogMessageActionsBinding.menuReplyToMessage.setOnClickListener {
-                chatActivity.replyToMessage(message)
+                chatActivity.messageInputViewModel.reply(message)
                 dismiss()
             }
         }
diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml
index f9efc89c1..554ffbddb 100644
--- a/app/src/main/res/layout/activity_chat.xml
+++ b/app/src/main/res/layout/activity_chat.xml
@@ -233,49 +233,16 @@
                 android:maxLines="2"
                 android:textColor="@color/low_emphasis_text"
                 tools:ignore="Overdraw"
-                tools:text="Marcel is typing"></TextView>
+                tools:text="Marcel is typing"/>
 
         </LinearLayout>
 
     </RelativeLayout>
 
-    <LinearLayout
+    <androidx.fragment.app.FragmentContainerView
+        android:id="@+id/fragment_container_activity_chat"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:orientation="vertical">
-
-        <include
-            android:id="@+id/editView"
-            layout="@layout/edit_message_view"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginEnd="6dp"
-            android:visibility="gone">
-        </include>
-
-        <com.nextcloud.talk.ui.MessageInput
-            android:id="@+id/messageInputView"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:animateLayoutChanges="true"
-            android:inputType="textLongMessage|textAutoComplete"
-            android:maxLength="1000"
-            app:attachmentButtonBackground="@color/transparent"
-            app:attachmentButtonHeight="48dp"
-            app:attachmentButtonIcon="@drawable/ic_baseline_attach_file_24"
-            app:attachmentButtonMargin="0dp"
-            app:attachmentButtonWidth="48dp"
-            app:delayTypingStatus="200"
-            app:inputButtonDefaultBgColor="@color/transparent"
-            app:inputButtonDefaultBgDisabledColor="@color/transparent"
-            app:inputButtonDefaultBgPressedColor="@color/transparent"
-            app:inputButtonDefaultIconColor="@color/colorPrimary"
-            app:inputButtonHeight="48dp"
-            app:inputButtonMargin="0dp"
-            app:inputButtonWidth="48dp"
-            app:inputHint="@string/nc_hint_enter_a_message"
-            app:inputTextColor="@color/nc_incoming_text_default"
-            app:inputTextSize="16sp"
-            app:showAttachmentButton="true" />
-    </LinearLayout>
+        android:padding="0dp"
+        />
 </LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_message_input.xml b/app/src/main/res/layout/fragment_message_input.xml
new file mode 100644
index 000000000..f854414bd
--- /dev/null
+++ b/app/src/main/res/layout/fragment_message_input.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk - Android Client
+  ~
+  ~ SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+  ~ SPDX-License-Identifier: GPL-3.0-or-later
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+        <include
+            android:id="@+id/fragment_editView"
+            layout="@layout/edit_message_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="6dp">
+        </include>
+
+        <com.nextcloud.talk.ui.MessageInput
+            android:id="@+id/fragment_message_input_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:animateLayoutChanges="true"
+            android:inputType="textLongMessage|textAutoComplete"
+            android:maxLength="1000"
+            app:attachmentButtonBackground="@color/transparent"
+            app:attachmentButtonHeight="48dp"
+            app:attachmentButtonIcon="@drawable/ic_baseline_attach_file_24"
+            app:attachmentButtonMargin="0dp"
+            app:attachmentButtonWidth="48dp"
+            app:delayTypingStatus="200"
+            app:inputButtonDefaultBgColor="@color/transparent"
+            app:inputButtonDefaultBgDisabledColor="@color/transparent"
+            app:inputButtonDefaultBgPressedColor="@color/transparent"
+            app:inputButtonDefaultIconColor="@color/colorPrimary"
+            app:inputButtonHeight="48dp"
+            app:inputButtonMargin="0dp"
+            app:inputButtonWidth="48dp"
+            app:inputHint="@string/nc_hint_enter_a_message"
+            app:inputTextColor="@color/nc_incoming_text_default"
+            app:inputTextSize="16sp"
+            app:showAttachmentButton="true" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_message_input_voice_recording.xml b/app/src/main/res/layout/fragment_message_input_voice_recording.xml
new file mode 100644
index 000000000..ee5e1b87e
--- /dev/null
+++ b/app/src/main/res/layout/fragment_message_input_voice_recording.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk - Android Client
+  ~
+  ~ SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+  ~ SPDX-License-Identifier: GPL-3.0-or-later
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:gravity="center"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@+id/voice_preview_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/standard_margin"
+        android:orientation="horizontal"
+        android:gravity="center"
+        android:layout_marginHorizontal="@dimen/standard_margin"
+        android:background="@drawable/shape_grouped_outcoming_message"
+        tools:backgroundTint="@color/nc_grey"
+        android:visibility="gone"
+        tools:visibility="visible">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/playPauseBtn"
+            style="@style/Widget.AppTheme.Button.IconButton"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:layout_marginStart="@dimen/standard_margin"
+            android:contentDescription="@string/play_pause_voice_message"
+            app:cornerRadius="@dimen/button_corner_radius"
+            app:icon="@drawable/ic_baseline_play_arrow_voice_message_24"
+            app:iconSize="30dp"
+            app:iconTint="@color/high_emphasis_text"
+            app:rippleColor="#1FFFFFFF" />
+
+        <SeekBar
+            android:id="@+id/seekbar"
+            style="@style/Nextcloud.Material.Outgoing.SeekBar"
+            android:layout_width="match_parent"
+            android:layout_height="30dp"
+            android:layout_marginEnd="@dimen/standard_margin"
+            android:thumb="@drawable/voice_message_outgoing_seek_bar_slider"
+            tools:progress="50"
+            tools:progressTint="@color/hwSecurityRed"
+            tools:progressBackgroundTint="@color/blue"/>
+    </LinearLayout>
+
+    <Chronometer
+        android:id="@+id/audioRecordDuration"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="16sp"
+        android:textStyle="bold"
+        android:textColor="@color/low_emphasis_text"
+        android:paddingStart="5dp"
+        android:paddingEnd="5dp"
+        android:background="@color/bg_default"
+        />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:gravity="center"
+        android:weightSum="3"
+        >
+
+        <ImageView
+            android:id="@+id/deleteVoiceRecording"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:layout_marginVertical="@dimen/standard_margin"
+            android:scaleType="centerInside"
+            android:src="@drawable/ic_delete"
+            android:contentDescription="@null"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
+            android:layout_weight="1"
+            />
+
+        <com.nextcloud.talk.ui.MicInputCloud
+            android:id="@+id/micInputCloud"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:playIcon="@drawable/ic_refresh"
+            app:pauseIcon="@drawable/baseline_stop_24"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
+            android:layout_weight="1"
+            />
+
+
+        <ImageView
+            android:id="@+id/sendVoiceRecording"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:layout_marginVertical="@dimen/standard_margin"
+            android:scaleType="centerInside"
+            android:src="@drawable/ic_send"
+            android:contentDescription="@null"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
+            android:layout_weight="1"
+            />
+    </LinearLayout>
+
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b9b33e95d..eefd4fc69 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -15,7 +15,7 @@ How to translate with transifex:
 -->
 
 <resources>
-<!--
+    <!--
   ~ Nextcloud Talk - Android Client
   ~
   ~ SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt
index 5b330829b..dc23e72dc 100644
--- a/scripts/analysis/lint-results.txt
+++ b/scripts/analysis/lint-results.txt
@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 119 errors and 81 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 10 errors and 79 warnings</span>