From c776aae9d06052abe2eb0d799ac33cc77dc94e9f Mon Sep 17 00:00:00 2001
From: jonnyandrew <jonny.andrew@protonmail.com>
Date: Wed, 26 Oct 2022 17:37:40 +0100
Subject: [PATCH] [Rich text editor] Add plain text mode and new attachment UI
 (#7459)

* Add new attachments selection dialog

* Add rounded corners to bottom sheet dialog.

Note these are currently only visible in the collapsed state.
- [Google issue](https://issuetracker.google.com/issues/144859239)
- [Rejected PR](https://github.com/material-components/material-components-android/pull/437)
- [Github issue](https://github.com/material-components/material-components-android/issues/1278)

* Add changelog entry

* Remove redundant call to superclass click listener

* Refactor to use view visibility helper

* Change redundant sealed class to interface

* Remove unused string

* Revert "Add rounded corners to bottom sheet dialog."

This reverts commit 17c43c91888162d3c7675511ff910c46c3aa32fc.

* Remove redundant view group

* Remove redundant `this`

* Update rich text editor to latest

* Update rich text editor version

* Allow toggling rich text in the new editor

* Persist the text formatting setting

* Add changelog entry
---
 changelog.d/7429.feature                      |   1 +
 changelog.d/7452.feature                      |   1 +
 dependencies.gradle                           |   2 +-
 .../src/main/res/values/strings.xml           |  10 ++
 .../app/core/di/MavericksViewModelModule.kt   |   6 +
 .../core/ui/views/BottomSheetActionButton.kt  |   4 +
 .../features/attachments/AttachmentType.kt    |  37 +++++
 .../AttachmentTypeSelectorBottomSheet.kt      |  92 ++++++++++++
 ...chmentTypeSelectorSharedActionViewModel.kt |  30 ++++
 .../attachments/AttachmentTypeSelectorView.kt |  70 +++++----
 .../AttachmentTypeSelectorViewModel.kt        |  76 ++++++++++
 .../features/attachments/AttachmentsHelper.kt |   2 +-
 .../composer/MessageComposerFragment.kt       |  74 +++++----
 .../detail/composer/RichTextComposerLayout.kt |  99 ++++++++----
 .../features/settings/VectorPreferences.kt    |  19 +++
 .../main/res/drawable/ic_text_formatting.xml  |  13 ++
 .../drawable/ic_text_formatting_disabled.xml  |  18 +++
 .../bottom_sheet_attachment_type_selector.xml | 106 +++++++++++++
 .../res/layout/composer_rich_text_layout.xml  |  19 ++-
 ...ich_text_layout_constraint_set_compact.xml |  22 ++-
 ...ch_text_layout_constraint_set_expanded.xml |  22 ++-
 ..._text_layout_constraint_set_fullscreen.xml |  23 ++-
 .../AttachmentTypeSelectorViewModelTest.kt    | 142 ++++++++++++++++++
 .../app/test/fakes/FakeVectorFeatures.kt      |   8 +
 .../app/test/fakes/FakeVectorPreferences.kt   |   3 +
 25 files changed, 797 insertions(+), 102 deletions(-)
 create mode 100644 changelog.d/7429.feature
 create mode 100644 changelog.d/7452.feature
 create mode 100644 vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt
 create mode 100644 vector/src/main/res/drawable/ic_text_formatting.xml
 create mode 100644 vector/src/main/res/drawable/ic_text_formatting_disabled.xml
 create mode 100644 vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml
 create mode 100644 vector/src/test/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModelTest.kt

diff --git a/changelog.d/7429.feature b/changelog.d/7429.feature
new file mode 100644
index 0000000000..9857452eca
--- /dev/null
+++ b/changelog.d/7429.feature
@@ -0,0 +1 @@
+Add new UI for selecting an attachment
diff --git a/changelog.d/7452.feature b/changelog.d/7452.feature
new file mode 100644
index 0000000000..a811f87c84
--- /dev/null
+++ b/changelog.d/7452.feature
@@ -0,0 +1 @@
+[Rich text editor] Add plain text mode
diff --git a/dependencies.gradle b/dependencies.gradle
index f081e0a874..db6e92552a 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -101,7 +101,7 @@ ext.libs = [
         ],
         element     : [
                 'opusencoder'             : "io.element.android:opusencoder:1.1.0",
-                'wysiwyg'                 : "io.element.android:wysiwyg:0.2.1"
+                'wysiwyg'                 : "io.element.android:wysiwyg:0.4.0"
         ],
         squareup    : [
                 'moshi'                  : "com.squareup.moshi:moshi:$moshi",
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 450dcab1f7..9edd7d836a 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3205,6 +3205,16 @@
     <string name="tooltip_attachment_location">Share location</string>
     <string name="tooltip_attachment_voice_broadcast">Start a voice broadcast</string>
 
+    <string name="attachment_type_selector_gallery">Photo library</string>
+    <string name="attachment_type_selector_sticker">Stickers</string>
+    <string name="attachment_type_selector_file">Attachments</string>
+    <string name="attachment_type_selector_voice_broadcast">Voice broadcast</string>
+    <string name="attachment_type_selector_poll">Polls</string>
+    <string name="attachment_type_selector_location">Location</string>
+    <string name="attachment_type_selector_camera">Camera</string>
+    <string name="attachment_type_selector_contact">Contact</string>
+    <string name="attachment_type_selector_text_formatting">Text formatting</string>
+
     <string name="message_reaction_show_less">Show less</string>
     <plurals name="message_reaction_show_more">
         <item quantity="one">"%1$d more"</item>
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index 97590028d8..2242abb7aa 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -22,6 +22,7 @@ import dagger.hilt.InstallIn
 import dagger.multibindings.IntoMap
 import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel
 import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel
+import im.vector.app.features.attachments.AttachmentTypeSelectorViewModel
 import im.vector.app.features.auth.ReAuthViewModel
 import im.vector.app.features.call.VectorCallViewModel
 import im.vector.app.features.call.conference.JitsiCallViewModel
@@ -677,4 +678,9 @@ interface MavericksViewModelModule {
     @IntoMap
     @MavericksViewModelKey(VectorSettingsLabsViewModel::class)
     fun vectorSettingsLabsViewModelFactory(factory: VectorSettingsLabsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
+    @Binds
+    @IntoMap
+    @MavericksViewModelKey(AttachmentTypeSelectorViewModel::class)
+    fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
 }
diff --git a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt
index a3e8b3780c..ca3e6a360a 100644
--- a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt
+++ b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt
@@ -38,6 +38,10 @@ class BottomSheetActionButton @JvmOverloads constructor(
 ) : FrameLayout(context, attrs, defStyleAttr) {
     val views: ViewBottomSheetActionButtonBinding
 
+    override fun setOnClickListener(l: OnClickListener?) {
+        views.bottomSheetActionClickableZone.setOnClickListener(l)
+    }
+
     var title: String? = null
         set(value) {
             field = value
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt
new file mode 100644
index 0000000000..f4b97b9f9c
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.attachments
+
+import im.vector.app.core.utils.PERMISSIONS_EMPTY
+import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
+import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
+import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
+import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_BROADCAST
+
+/**
+ * The all possible types to pick with their required permissions.
+ */
+enum class AttachmentType(val permissions: List<String>) {
+    CAMERA(PERMISSIONS_FOR_TAKING_PHOTO),
+    GALLERY(PERMISSIONS_EMPTY),
+    FILE(PERMISSIONS_EMPTY),
+    STICKER(PERMISSIONS_EMPTY),
+    CONTACT(PERMISSIONS_FOR_PICKING_CONTACT),
+    POLL(PERMISSIONS_EMPTY),
+    LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING),
+    VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST),
+}
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt
new file mode 100644
index 0000000000..f8d5d768ef
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.attachments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.viewModels
+import com.airbnb.mvrx.parentFragmentViewModel
+import com.airbnb.mvrx.withState
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
+import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
+import im.vector.app.databinding.BottomSheetAttachmentTypeSelectorBinding
+import im.vector.app.features.home.room.detail.TimelineViewModel
+
+@AndroidEntryPoint
+class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetAttachmentTypeSelectorBinding>() {
+
+    private val viewModel: AttachmentTypeSelectorViewModel by parentFragmentViewModel()
+    private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
+    private val sharedActionViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels(
+            ownerProducer = { requireParentFragment() }
+    )
+
+    override val showExpanded = true
+
+    override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetAttachmentTypeSelectorBinding {
+        return BottomSheetAttachmentTypeSelectorBinding.inflate(inflater, container, false)
+    }
+
+    override fun invalidate() = withState(viewModel, timelineViewModel) { viewState, timelineState ->
+        super.invalidate()
+        views.location.isVisible = viewState.isLocationVisible
+        views.voiceBroadcast.isVisible = viewState.isVoiceBroadcastVisible
+        views.poll.isVisible = !timelineState.isThreadTimeline()
+        views.textFormatting.isChecked = viewState.isTextFormattingEnabled
+        views.textFormatting.setCompoundDrawablesRelativeWithIntrinsicBounds(
+                if (viewState.isTextFormattingEnabled) {
+                    R.drawable.ic_text_formatting
+                } else {
+                    R.drawable.ic_text_formatting_disabled
+                }, 0, 0, 0
+        )
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        views.gallery.debouncedClicks { onAttachmentSelected(AttachmentType.GALLERY) }
+        views.stickers.debouncedClicks { onAttachmentSelected(AttachmentType.STICKER) }
+        views.file.debouncedClicks { onAttachmentSelected(AttachmentType.FILE) }
+        views.voiceBroadcast.debouncedClicks { onAttachmentSelected(AttachmentType.VOICE_BROADCAST) }
+        views.poll.debouncedClicks { onAttachmentSelected(AttachmentType.POLL) }
+        views.location.debouncedClicks { onAttachmentSelected(AttachmentType.LOCATION) }
+        views.camera.debouncedClicks { onAttachmentSelected(AttachmentType.CAMERA) }
+        views.contact.debouncedClicks { onAttachmentSelected(AttachmentType.CONTACT) }
+        views.textFormatting.setOnCheckedChangeListener { _, isChecked -> onTextFormattingToggled(isChecked) }
+    }
+
+    private fun onAttachmentSelected(attachmentType: AttachmentType) {
+        val action = AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction(attachmentType)
+        sharedActionViewModel.post(action)
+        dismiss()
+    }
+
+    private fun onTextFormattingToggled(isEnabled: Boolean) =
+            viewModel.handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled))
+
+    companion object {
+        fun show(fragmentManager: FragmentManager) {
+            val bottomSheet = AttachmentTypeSelectorBottomSheet()
+            bottomSheet.show(fragmentManager, "AttachmentTypeSelectorBottomSheet")
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt
new file mode 100644
index 0000000000..e02b10c54b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package im.vector.app.features.attachments
+
+import im.vector.app.core.platform.VectorSharedAction
+import im.vector.app.core.platform.VectorSharedActionViewModel
+import javax.inject.Inject
+
+class AttachmentTypeSelectorSharedActionViewModel @Inject constructor() :
+        VectorSharedActionViewModel<AttachmentTypeSelectorSharedAction>()
+
+sealed interface AttachmentTypeSelectorSharedAction : VectorSharedAction {
+    data class SelectAttachmentTypeAction(
+            val attachmentType: AttachmentType
+    ) : AttachmentTypeSelectorSharedAction
+}
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt
index 8536b765d4..55805a0728 100644
--- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt
@@ -30,17 +30,11 @@ import android.view.animation.TranslateAnimation
 import android.widget.ImageButton
 import android.widget.LinearLayout
 import android.widget.PopupWindow
-import androidx.annotation.StringRes
 import androidx.appcompat.widget.TooltipCompat
 import androidx.core.view.doOnNextLayout
 import androidx.core.view.isVisible
 import im.vector.app.R
 import im.vector.app.core.epoxy.onClick
-import im.vector.app.core.utils.PERMISSIONS_EMPTY
-import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING
-import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
-import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
-import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_BROADCAST
 import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding
 import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback
 import kotlin.math.max
@@ -59,7 +53,7 @@ class AttachmentTypeSelectorView(
 ) : PopupWindow(context) {
 
     interface Callback {
-        fun onTypeSelected(type: Type)
+        fun onTypeSelected(type: AttachmentType)
     }
 
     private val views: ViewAttachmentTypeSelectorBinding
@@ -69,14 +63,14 @@ class AttachmentTypeSelectorView(
     init {
         contentView = inflater.inflate(R.layout.view_attachment_type_selector, null, false)
         views = ViewAttachmentTypeSelectorBinding.bind(contentView)
-        views.attachmentGalleryButton.configure(Type.GALLERY)
-        views.attachmentCameraButton.configure(Type.CAMERA)
-        views.attachmentFileButton.configure(Type.FILE)
-        views.attachmentStickersButton.configure(Type.STICKER)
-        views.attachmentContactButton.configure(Type.CONTACT)
-        views.attachmentPollButton.configure(Type.POLL)
-        views.attachmentLocationButton.configure(Type.LOCATION)
-        views.attachmentVoiceBroadcast.configure(Type.VOICE_BROADCAST)
+        views.attachmentGalleryButton.configure(AttachmentType.GALLERY)
+        views.attachmentCameraButton.configure(AttachmentType.CAMERA)
+        views.attachmentFileButton.configure(AttachmentType.FILE)
+        views.attachmentStickersButton.configure(AttachmentType.STICKER)
+        views.attachmentContactButton.configure(AttachmentType.CONTACT)
+        views.attachmentPollButton.configure(AttachmentType.POLL)
+        views.attachmentLocationButton.configure(AttachmentType.LOCATION)
+        views.attachmentVoiceBroadcast.configure(AttachmentType.VOICE_BROADCAST)
         width = LinearLayout.LayoutParams.MATCH_PARENT
         height = LinearLayout.LayoutParams.WRAP_CONTENT
         animationStyle = 0
@@ -127,16 +121,16 @@ class AttachmentTypeSelectorView(
         }
     }
 
-    fun setAttachmentVisibility(type: Type, isVisible: Boolean) {
+    fun setAttachmentVisibility(type: AttachmentType, isVisible: Boolean) {
         when (type) {
-            Type.CAMERA -> views.attachmentCameraButton
-            Type.GALLERY -> views.attachmentGalleryButton
-            Type.FILE -> views.attachmentFileButton
-            Type.STICKER -> views.attachmentStickersButton
-            Type.CONTACT -> views.attachmentContactButton
-            Type.POLL -> views.attachmentPollButton
-            Type.LOCATION -> views.attachmentLocationButton
-            Type.VOICE_BROADCAST -> views.attachmentVoiceBroadcast
+            AttachmentType.CAMERA -> views.attachmentCameraButton
+            AttachmentType.GALLERY -> views.attachmentGalleryButton
+            AttachmentType.FILE -> views.attachmentFileButton
+            AttachmentType.STICKER -> views.attachmentStickersButton
+            AttachmentType.CONTACT -> views.attachmentContactButton
+            AttachmentType.POLL -> views.attachmentPollButton
+            AttachmentType.LOCATION -> views.attachmentLocationButton
+            AttachmentType.VOICE_BROADCAST -> views.attachmentVoiceBroadcast
         }.let {
             it.isVisible = isVisible
         }
@@ -200,13 +194,13 @@ class AttachmentTypeSelectorView(
         return Pair(x, y)
     }
 
-    private fun ImageButton.configure(type: Type): ImageButton {
+    private fun ImageButton.configure(type: AttachmentType): ImageButton {
         this.setOnClickListener(TypeClickListener(type))
-        TooltipCompat.setTooltipText(this, context.getString(type.tooltipRes))
+        TooltipCompat.setTooltipText(this, context.getString(attachmentTooltipLabels.getValue(type)))
         return this
     }
 
-    private inner class TypeClickListener(private val type: Type) : View.OnClickListener {
+    private inner class TypeClickListener(private val type: AttachmentType) : View.OnClickListener {
 
         override fun onClick(v: View) {
             dismiss()
@@ -217,14 +211,18 @@ class AttachmentTypeSelectorView(
     /**
      * The all possible types to pick with their required permissions and tooltip resource.
      */
-    enum class Type(val permissions: List<String>, @StringRes val tooltipRes: Int) {
-        CAMERA(PERMISSIONS_FOR_TAKING_PHOTO, R.string.tooltip_attachment_photo),
-        GALLERY(PERMISSIONS_EMPTY, R.string.tooltip_attachment_gallery),
-        FILE(PERMISSIONS_EMPTY, R.string.tooltip_attachment_file),
-        STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker),
-        CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact),
-        POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll),
-        LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, R.string.tooltip_attachment_location),
-        VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST, R.string.tooltip_attachment_voice_broadcast),
+    private companion object {
+        private val attachmentTooltipLabels: Map<AttachmentType, Int> = AttachmentType.values().associateWith {
+            when (it) {
+                AttachmentType.CAMERA -> R.string.tooltip_attachment_photo
+                AttachmentType.GALLERY -> R.string.tooltip_attachment_gallery
+                AttachmentType.FILE -> R.string.tooltip_attachment_file
+                AttachmentType.STICKER -> R.string.tooltip_attachment_sticker
+                AttachmentType.CONTACT -> R.string.tooltip_attachment_contact
+                AttachmentType.POLL -> R.string.tooltip_attachment_poll
+                AttachmentType.LOCATION -> R.string.tooltip_attachment_location
+                AttachmentType.VOICE_BROADCAST -> R.string.tooltip_attachment_voice_broadcast
+            }
+        }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt
new file mode 100644
index 0000000000..cb74661eba
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.attachments
+
+import com.airbnb.mvrx.MavericksState
+import com.airbnb.mvrx.MavericksViewModelFactory
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.core.di.MavericksAssistedViewModelFactory
+import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.core.platform.EmptyViewEvents
+import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.core.platform.VectorViewModelAction
+import im.vector.app.features.VectorFeatures
+import im.vector.app.features.settings.VectorPreferences
+
+class AttachmentTypeSelectorViewModel @AssistedInject constructor(
+        @Assisted initialState: AttachmentTypeSelectorViewState,
+        private val vectorFeatures: VectorFeatures,
+        private val vectorPreferences: VectorPreferences,
+) : VectorViewModel<AttachmentTypeSelectorViewState, AttachmentTypeSelectorAction, EmptyViewEvents>(initialState) {
+    @AssistedFactory
+    interface Factory : MavericksAssistedViewModelFactory<AttachmentTypeSelectorViewModel, AttachmentTypeSelectorViewState> {
+        override fun create(initialState: AttachmentTypeSelectorViewState): AttachmentTypeSelectorViewModel
+    }
+
+    companion object : MavericksViewModelFactory<AttachmentTypeSelectorViewModel, AttachmentTypeSelectorViewState> by hiltMavericksViewModelFactory()
+
+    override fun handle(action: AttachmentTypeSelectorAction) = when (action) {
+        is AttachmentTypeSelectorAction.ToggleTextFormatting -> setTextFormattingEnabled(action.isEnabled)
+    }
+
+    init {
+        setState {
+            copy(
+                    isLocationVisible = vectorFeatures.isLocationSharingEnabled(),
+                    isVoiceBroadcastVisible = vectorFeatures.isVoiceBroadcastEnabled(),
+                    isTextFormattingEnabled = vectorPreferences.isTextFormattingEnabled(),
+            )
+        }
+    }
+
+    private fun setTextFormattingEnabled(isEnabled: Boolean) {
+        vectorPreferences.setTextFormattingEnabled(isEnabled)
+        setState {
+            copy(
+                    isTextFormattingEnabled = isEnabled
+            )
+        }
+    }
+}
+
+data class AttachmentTypeSelectorViewState(
+        val isLocationVisible: Boolean = false,
+        val isVoiceBroadcastVisible: Boolean = false,
+        val isTextFormattingEnabled: Boolean = false,
+) : MavericksState
+
+sealed interface AttachmentTypeSelectorAction : VectorViewModelAction {
+    data class ToggleTextFormatting(val isEnabled: Boolean) : AttachmentTypeSelectorAction
+}
diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt
index 1a8e10d102..9692777e15 100644
--- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt
@@ -54,7 +54,7 @@ class AttachmentsHelper(
     private var captureUri: Uri? = null
 
     // The pending type is set if we have to handle permission request. It must be restored if the activity gets killed.
-    var pendingType: AttachmentTypeSelectorView.Type? = null
+    var pendingType: AttachmentType? = null
 
     // Restorable
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
index 463a8fe440..5666c28605 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
@@ -40,8 +40,10 @@ import androidx.core.text.buildSpannedString
 import androidx.core.view.isGone
 import androidx.core.view.isInvisible
 import androidx.core.view.isVisible
+import androidx.fragment.app.viewModels
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
+import com.airbnb.mvrx.fragmentViewModel
 import com.airbnb.mvrx.parentFragmentViewModel
 import com.airbnb.mvrx.withState
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -63,7 +65,12 @@ import im.vector.app.core.utils.onPermissionDeniedDialog
 import im.vector.app.core.utils.registerForPermissionsResult
 import im.vector.app.databinding.FragmentComposerBinding
 import im.vector.app.features.VectorFeatures
+import im.vector.app.features.attachments.AttachmentType
+import im.vector.app.features.attachments.AttachmentTypeSelectorBottomSheet
+import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction
+import im.vector.app.features.attachments.AttachmentTypeSelectorSharedActionViewModel
 import im.vector.app.features.attachments.AttachmentTypeSelectorView
+import im.vector.app.features.attachments.AttachmentTypeSelectorViewModel
 import im.vector.app.features.attachments.AttachmentsHelper
 import im.vector.app.features.attachments.ContactAttachment
 import im.vector.app.features.attachments.ShareIntentHandler
@@ -91,8 +98,9 @@ import im.vector.app.features.poll.PollMode
 import im.vector.app.features.settings.VectorPreferences
 import im.vector.app.features.share.SharedData
 import im.vector.app.features.voice.VoiceFailure
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterIsInstance
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
@@ -162,6 +170,8 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
     private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
     private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel()
     private lateinit var sharedActionViewModel: MessageSharedActionViewModel
+    private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel()
+    private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels()
 
     private val composer: MessageComposerView get() {
         return if (vectorPreferences.isRichTextEditorEnabled()) {
@@ -227,6 +237,11 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
             }
         }
 
+        attachmentActionsViewModel.stream()
+                .filterIsInstance<AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction>()
+                .onEach { onTypeSelected(it.attachmentType) }
+                .launchIn(lifecycleScope)
+
         if (savedInstanceState != null) {
             handleShareData()
         }
@@ -260,11 +275,14 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
         messageComposerViewModel.endAllVoiceActions()
     }
 
-    override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState ->
+    override fun invalidate() = withState(
+            timelineViewModel, messageComposerViewModel, attachmentViewModel
+    ) { mainState, messageComposerState, attachmentState ->
         if (mainState.tombstoneEvent != null) return@withState
 
         composer.setInvisible(!messageComposerState.isComposerVisible)
         composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
+        (composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled
     }
 
     private fun setupComposer() {
@@ -307,21 +325,25 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
         }
         composer.callback = object : Callback {
             override fun onAddAttachment() {
-                if (!::attachmentTypeSelector.isInitialized) {
-                    attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment)
-                    attachmentTypeSelector.setAttachmentVisibility(
-                            AttachmentTypeSelectorView.Type.LOCATION,
-                            vectorFeatures.isLocationSharingEnabled(),
-                    )
-                    attachmentTypeSelector.setAttachmentVisibility(
-                            AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine()
-                    )
-                    attachmentTypeSelector.setAttachmentVisibility(
-                            AttachmentTypeSelectorView.Type.VOICE_BROADCAST,
-                            vectorPreferences.isVoiceBroadcastEnabled(), // TODO check user permission
-                    )
+                if (vectorPreferences.isRichTextEditorEnabled()) {
+                    AttachmentTypeSelectorBottomSheet.show(childFragmentManager)
+                } else {
+                    if (!::attachmentTypeSelector.isInitialized) {
+                        attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment)
+                        attachmentTypeSelector.setAttachmentVisibility(
+                                AttachmentType.LOCATION,
+                                vectorFeatures.isLocationSharingEnabled(),
+                        )
+                        attachmentTypeSelector.setAttachmentVisibility(
+                                AttachmentType.POLL, !isThreadTimeLine()
+                        )
+                        attachmentTypeSelector.setAttachmentVisibility(
+                                AttachmentType.VOICE_BROADCAST,
+                                vectorPreferences.isVoiceBroadcastEnabled(), // TODO check user permission
+                        )
+                    }
+                    attachmentTypeSelector.show(composer.attachmentButton)
                 }
-                attachmentTypeSelector.show(composer.attachmentButton)
             }
 
             override fun onExpandOrCompactChange() {
@@ -678,20 +700,20 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
         }
     }
 
-    private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
+    private fun launchAttachmentProcess(type: AttachmentType) {
         when (type) {
-            AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(
+            AttachmentType.CAMERA -> attachmentsHelper.openCamera(
                     activity = requireActivity(),
                     vectorPreferences = vectorPreferences,
                     cameraActivityResultLauncher = attachmentCameraActivityResultLauncher,
                     cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher
             )
-            AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
-            AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
-            AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
-            AttachmentTypeSelectorView.Type.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment)
-            AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomId, null, PollMode.CREATE)
-            AttachmentTypeSelectorView.Type.LOCATION -> {
+            AttachmentType.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
+            AttachmentType.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
+            AttachmentType.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
+            AttachmentType.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment)
+            AttachmentType.POLL -> navigator.openCreatePoll(requireContext(), roomId, null, PollMode.CREATE)
+            AttachmentType.LOCATION -> {
                 navigator
                         .openLocationSharing(
                                 context = requireContext(),
@@ -701,11 +723,11 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
                                 locationOwnerId = session.myUserId
                         )
             }
-            AttachmentTypeSelectorView.Type.VOICE_BROADCAST -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Start)
+            AttachmentType.VOICE_BROADCAST -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Start)
         }
     }
 
-    override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) {
+    override fun onTypeSelected(type: AttachmentType) {
         if (checkPermissions(type.permissions, requireActivity(), typeSelectedActivityResultLauncher)) {
             launchAttachmentProcess(type)
         } else {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
index cac8f8bed4..2c09f351bb 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
@@ -38,7 +38,7 @@ import im.vector.app.core.extensions.setTextIfDifferent
 import im.vector.app.databinding.ComposerRichTextLayoutBinding
 import im.vector.app.databinding.ViewRichTextMenuButtonBinding
 import io.element.android.wysiwyg.EditorEditText
-import io.element.android.wysiwyg.InlineFormat
+import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
 import uniffi.wysiwyg_composer.ComposerAction
 import uniffi.wysiwyg_composer.MenuState
 
@@ -57,12 +57,24 @@ class RichTextComposerLayout @JvmOverloads constructor(
 
     private var isFullScreen = false
 
+    var isTextFormattingEnabled = true
+        set(value) {
+            if (field == value) return
+            syncEditTexts()
+            field = value
+            updateEditTextVisibility()
+        }
+
     override val text: Editable?
-        get() = views.composerEditText.text
+        get() = editText.text
     override val formattedText: String?
-        get() = views.composerEditText.getHtmlOutput()
+        get() = (editText as? EditorEditText)?.getHtmlOutput()
     override val editText: EditText
-        get() = views.composerEditText
+        get() = if (isTextFormattingEnabled) {
+            views.richTextComposerEditText
+        } else {
+            views.plainTextComposerEditText
+        }
     override val emojiButton: ImageButton?
         get() = null
     override val sendButton: ImageButton
@@ -91,21 +103,12 @@ class RichTextComposerLayout @JvmOverloads constructor(
 
         collapse(false)
 
-        views.composerEditText.addTextChangedListener(object : TextWatcher {
-            private var previousTextWasExpanded = false
-
-            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
-            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
-            override fun afterTextChanged(s: Editable) {
-                callback?.onTextChanged(s)
-
-                val isExpanded = s.lines().count() > 1
-                if (previousTextWasExpanded != isExpanded) {
-                    updateTextFieldBorder(isExpanded)
-                }
-                previousTextWasExpanded = isExpanded
-            }
-        })
+        views.richTextComposerEditText.addTextChangedListener(
+                TextChangeListener({ callback?.onTextChanged(it) }, ::updateTextFieldBorder)
+        )
+        views.plainTextComposerEditText.addTextChangedListener(
+                TextChangeListener({ callback?.onTextChanged(it) }, ::updateTextFieldBorder)
+        )
 
         views.composerRelatedMessageCloseButton.setOnClickListener {
             collapse()
@@ -130,19 +133,23 @@ class RichTextComposerLayout @JvmOverloads constructor(
 
     private fun setupRichTextMenu() {
         addRichTextMenuItem(R.drawable.ic_composer_bold, R.string.rich_text_editor_format_bold, ComposerAction.Bold) {
-            views.composerEditText.toggleInlineFormat(InlineFormat.Bold)
+            views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Bold)
         }
         addRichTextMenuItem(R.drawable.ic_composer_italic, R.string.rich_text_editor_format_italic, ComposerAction.Italic) {
-            views.composerEditText.toggleInlineFormat(InlineFormat.Italic)
+            views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Italic)
         }
         addRichTextMenuItem(R.drawable.ic_composer_underlined, R.string.rich_text_editor_format_underline, ComposerAction.Underline) {
-            views.composerEditText.toggleInlineFormat(InlineFormat.Underline)
+            views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Underline)
         }
         addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.StrikeThrough) {
-            views.composerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
+            views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
         }
+    }
 
-        views.composerEditText.menuStateChangedListener = EditorEditText.OnMenuStateChangedListener { state ->
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+
+        views.richTextComposerEditText.menuStateChangedListener = EditorEditText.OnMenuStateChangedListener { state ->
             if (state is MenuState.Update) {
                 updateMenuStateFor(ComposerAction.Bold, state)
                 updateMenuStateFor(ComposerAction.Italic, state)
@@ -150,8 +157,26 @@ class RichTextComposerLayout @JvmOverloads constructor(
                 updateMenuStateFor(ComposerAction.StrikeThrough, state)
             }
         }
+
+        updateEditTextVisibility()
     }
 
+    private fun updateEditTextVisibility() {
+        views.richTextComposerEditText.isVisible = isTextFormattingEnabled
+        views.richTextMenu.isVisible = isTextFormattingEnabled
+        views.plainTextComposerEditText.isVisible = !isTextFormattingEnabled
+    }
+
+    /**
+     * Updates the non-active input with the contents of the active input.
+     */
+    private fun syncEditTexts() =
+        if (isTextFormattingEnabled) {
+            views.plainTextComposerEditText.setText(views.richTextComposerEditText.getPlainText())
+        } else {
+            views.richTextComposerEditText.setText(views.plainTextComposerEditText.text.toString())
+        }
+
     private fun addRichTextMenuItem(@DrawableRes iconId: Int, @StringRes description: Int, action: ComposerAction, onClick: () -> Unit) {
         val inflater = LayoutInflater.from(context)
         val button = ViewRichTextMenuButtonBinding.inflate(inflater, views.richTextMenu, true)
@@ -181,7 +206,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
     }
 
     override fun replaceFormattedContent(text: CharSequence) {
-        views.composerEditText.setHtml(text.toString())
+        views.richTextComposerEditText.setHtml(text.toString())
     }
 
     override fun collapse(animate: Boolean, transitionComplete: (() -> Unit)?) {
@@ -191,6 +216,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
         }
         currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_compact
         applyNewConstraintSet(animate, transitionComplete)
+        updateEditTextVisibility()
     }
 
     override fun expand(animate: Boolean, transitionComplete: (() -> Unit)?) {
@@ -200,10 +226,11 @@ class RichTextComposerLayout @JvmOverloads constructor(
         }
         currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_expanded
         applyNewConstraintSet(animate, transitionComplete)
+        updateEditTextVisibility()
     }
 
     override fun setTextIfDifferent(text: CharSequence?): Boolean {
-        return views.composerEditText.setTextIfDifferent(text)
+        return editText.setTextIfDifferent(text)
     }
 
     override fun toggleFullScreen(newValue: Boolean) {
@@ -214,6 +241,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
         }
 
         updateTextFieldBorder(newValue)
+        updateEditTextVisibility()
     }
 
     private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
@@ -233,4 +261,23 @@ class RichTextComposerLayout @JvmOverloads constructor(
     override fun setInvisible(isInvisible: Boolean) {
         this.isInvisible = isInvisible
     }
+
+    private class TextChangeListener(
+            private val onTextChanged: (s: Editable) -> Unit,
+            private val onExpandedChanged: (isExpanded: Boolean) -> Unit,
+    ) : TextWatcher {
+        private var previousTextWasExpanded = false
+
+        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
+        override fun afterTextChanged(s: Editable) {
+            onTextChanged.invoke(s)
+
+            val isExpanded = s.lines().count() > 1
+            if (previousTextWasExpanded != isExpanded) {
+                onExpandedChanged(isExpanded)
+            }
+            previousTextWasExpanded = isExpanded
+        }
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
index 2dc8b12160..9f40a7cede 100755
--- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
@@ -109,6 +109,7 @@ class VectorPreferences @Inject constructor(
         const val SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY"
         private const val SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY"
         private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY"
+        private const val SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY = "SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY"
         private const val SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY"
         private const val SETTINGS_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_KEY"
         private const val SETTINGS_SHOW_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY"
@@ -759,6 +760,24 @@ class VectorPreferences @Inject constructor(
         }
     }
 
+    /**
+     * Tells if text formatting is enabled within the rich text editor.
+     *
+     * @return true if the text formatting is enabled
+     */
+    fun isTextFormattingEnabled(): Boolean =
+        defaultPrefs.getBoolean(SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY, true)
+
+    /**
+     * Update whether text formatting is enabled within the rich text editor.
+     *
+     * @param isEnabled true to enable the text formatting
+     */
+    fun setTextFormattingEnabled(isEnabled: Boolean) =
+        defaultPrefs.edit {
+            putBoolean(SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY, isEnabled)
+        }
+
     /**
      * Tells if a confirmation dialog should be displayed before staring a call.
      */
diff --git a/vector/src/main/res/drawable/ic_text_formatting.xml b/vector/src/main/res/drawable/ic_text_formatting.xml
new file mode 100644
index 0000000000..375c459692
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_text_formatting.xml
@@ -0,0 +1,13 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <group>
+    <clip-path
+        android:pathData="M0,0h24v24h-24z"/>
+    <path
+        android:pathData="M3,20.667C3,21.4 3.6,22 4.333,22H20.333C21.067,22 21.667,21.4 21.667,20.667C21.667,19.933 21.067,19.333 20.333,19.333H4.333C3.6,19.333 3,19.933 3,20.667ZM9,13.733H15.667L16.547,15.867C16.747,16.347 17.213,16.667 17.733,16.667C18.653,16.667 19.267,15.72 18.907,14.88L13.733,2.92C13.493,2.36 12.947,2 12.333,2C11.72,2 11.173,2.36 10.933,2.92L5.76,14.88C5.4,15.72 6.027,16.667 6.947,16.667C7.467,16.667 7.933,16.347 8.133,15.867L9,13.733ZM12.333,4.64L14.827,11.333H9.84L12.333,4.64Z"
+        android:fillColor="#0DBD8B"/>
+  </group>
+</vector>
diff --git a/vector/src/main/res/drawable/ic_text_formatting_disabled.xml b/vector/src/main/res/drawable/ic_text_formatting_disabled.xml
new file mode 100644
index 0000000000..bb34211c7a
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_text_formatting_disabled.xml
@@ -0,0 +1,18 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <group>
+    <clip-path
+        android:pathData="M0,0h24v24h-24z"/>
+    <path
+        android:pathData="M9,15.733H15.667L16.547,17.867C16.747,18.347 17.213,18.667 17.733,18.667C18.653,18.667 19.267,17.72 18.907,16.88L13.733,4.92C13.493,4.36 12.947,4 12.333,4C11.72,4 11.173,4.36 10.933,4.92L5.76,16.88C5.4,17.72 6.027,18.667 6.947,18.667C7.467,18.667 7.933,18.347 8.133,17.867L9,15.733ZM12.333,6.64L14.827,13.333H9.84L12.333,6.64Z"
+        android:fillColor="#0DBD8B"/>
+    <path
+        android:strokeWidth="1"
+        android:pathData="M2.5,11.667C2.5,12.676 3.324,13.5 4.333,13.5H20.333C21.343,13.5 22.167,12.676 22.167,11.667C22.167,10.657 21.343,9.833 20.333,9.833H4.333C3.324,9.833 2.5,10.657 2.5,11.667Z"
+        android:fillColor="#0DBD8B"
+        android:strokeColor="#ffffff"/>
+  </group>
+</vector>
diff --git a/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml b/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml
new file mode 100644
index 0000000000..7a22ab57f8
--- /dev/null
+++ b/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="?colorSurface">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <im.vector.app.core.ui.views.BottomSheetActionButton
+            android:id="@+id/gallery"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:actionTitle="@string/attachment_type_selector_gallery"
+            app:leftIcon="@drawable/ic_attachment_gallery"
+            app:tint="?colorPrimary"
+            app:titleTextColor="?vctr_content_primary" />
+
+        <im.vector.app.core.ui.views.BottomSheetActionButton
+            android:id="@+id/stickers"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:actionTitle="@string/attachment_type_selector_sticker"
+            app:leftIcon="@drawable/ic_attachment_sticker"
+            app:tint="?colorPrimary"
+            app:titleTextColor="?vctr_content_primary" />
+
+        <im.vector.app.core.ui.views.BottomSheetActionButton
+            android:id="@+id/file"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:actionTitle="@string/attachment_type_selector_file"
+            app:leftIcon="@drawable/ic_attachment_file"
+            app:tint="?colorPrimary"
+            app:titleTextColor="?vctr_content_primary" />
+
+        <im.vector.app.core.ui.views.BottomSheetActionButton
+            android:id="@+id/voiceBroadcast"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:actionTitle="@string/attachment_type_selector_voice_broadcast"
+            app:leftIcon="@drawable/ic_attachment_voice_broadcast"
+            app:tint="?colorPrimary"
+            app:titleTextColor="?vctr_content_primary" />
+
+        <im.vector.app.core.ui.views.BottomSheetActionButton
+            android:id="@+id/poll"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:actionTitle="@string/attachment_type_selector_poll"
+            app:leftIcon="@drawable/ic_attachment_poll"
+            app:tint="?colorPrimary"
+            app:titleTextColor="?vctr_content_primary" />
+
+        <im.vector.app.core.ui.views.BottomSheetActionButton
+            android:id="@+id/location"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:actionTitle="@string/attachment_type_selector_location"
+            app:leftIcon="@drawable/ic_attachment_location"
+            app:tint="?colorPrimary"
+            app:titleTextColor="?vctr_content_primary" />
+
+        <im.vector.app.core.ui.views.BottomSheetActionButton
+            android:id="@+id/camera"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:actionTitle="@string/attachment_type_selector_camera"
+            app:leftIcon="@drawable/ic_attachment_camera"
+            app:tint="?colorPrimary"
+            app:titleTextColor="?vctr_content_primary" />
+
+        <im.vector.app.core.ui.views.BottomSheetActionButton
+            android:id="@+id/contact"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:actionTitle="@string/attachment_type_selector_contact"
+            app:leftIcon="@drawable/ic_attachment_contact_white_24dp"
+            app:tint="?colorPrimary"
+            app:titleTextColor="?vctr_content_primary" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:background="?vctr_list_separator" />
+
+        <androidx.appcompat.widget.SwitchCompat
+            android:id="@+id/textFormatting"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:drawableStart="@drawable/ic_text_formatting"
+            android:drawablePadding="20dp"
+            android:padding="20dp"
+            android:paddingStart="28dp"
+            android:text="@string/attachment_type_selector_text_formatting"
+            android:textAppearance="@style/TextAppearance.Vector.Subtitle"
+            android:textColor="?vctr_content_primary"
+            app:drawableTint="?colorPrimary"
+            tools:ignore="RtlSymmetry" />
+
+    </LinearLayout>
+</androidx.core.widget.NestedScrollView>
diff --git a/vector/src/main/res/layout/composer_rich_text_layout.xml b/vector/src/main/res/layout/composer_rich_text_layout.xml
index 9f49b8f9d6..c5afe1eb44 100644
--- a/vector/src/main/res/layout/composer_rich_text_layout.xml
+++ b/vector/src/main/res/layout/composer_rich_text_layout.xml
@@ -104,13 +104,26 @@
         android:background="@drawable/bg_composer_rich_edit_text_single_line" />
 
     <io.element.android.wysiwyg.EditorEditText
-        android:id="@+id/composerEditText"
+        android:id="@+id/richTextComposerEditText"
         style="@style/Widget.Vector.EditText.RichTextComposer"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:gravity="top"
-        android:nextFocusLeft="@id/composerEditText"
-        android:nextFocusUp="@id/composerEditText"
+        android:nextFocusLeft="@id/richTextComposerEditText"
+        android:nextFocusUp="@id/richTextComposerEditText"
+        tools:hint="@string/room_message_placeholder"
+        tools:text="@tools:sample/lorem/random"
+        tools:ignore="MissingConstraints" />
+
+    <!-- Use a separate EditText for plain text editing while the rich text editor doesn't support this mode -->
+    <com.google.android.material.textfield.TextInputEditText
+        android:id="@+id/plainTextComposerEditText"
+        style="@style/Widget.Vector.EditText.RichTextComposer"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:gravity="top"
+        android:nextFocusLeft="@id/plainTextComposerEditText"
+        android:nextFocusUp="@id/plainTextComposerEditText"
         tools:hint="@string/room_message_placeholder"
         tools:text="@tools:sample/lorem/random"
         tools:ignore="MissingConstraints" />
diff --git a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml
index 7aaa9f6a07..1a3023a805 100644
--- a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml
+++ b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml
@@ -136,13 +136,29 @@
         app:layout_constraintEnd_toEndOf="parent" />
 
     <io.element.android.wysiwyg.EditorEditText
-        android:id="@+id/composerEditText"
+        android:id="@+id/richTextComposerEditText"
         style="@style/Widget.Vector.EditText.RichTextComposer"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:hint="@string/room_message_placeholder"
-        android:nextFocusLeft="@id/composerEditText"
-        android:nextFocusUp="@id/composerEditText"
+        android:nextFocusLeft="@id/richTextComposerEditText"
+        android:nextFocusUp="@id/richTextComposerEditText"
+        android:layout_marginHorizontal="12dp"
+        android:layout_marginVertical="10dp"
+        app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
+        tools:text="@tools:sample/lorem/random" />
+
+    <com.google.android.material.textfield.TextInputEditText
+        android:id="@+id/plainTextComposerEditText"
+        style="@style/Widget.Vector.EditText.RichTextComposer"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:hint="@string/room_message_placeholder"
+        android:nextFocusLeft="@id/plainTextComposerEditText"
+        android:nextFocusUp="@id/plainTextComposerEditText"
         android:layout_marginStart="12dp"
         android:layout_marginVertical="10dp"
         app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
diff --git a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml
index 214b3158b5..b0380d2e13 100644
--- a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml
+++ b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml
@@ -149,13 +149,29 @@
         app:layout_constraintEnd_toEndOf="parent" />
 
     <io.element.android.wysiwyg.EditorEditText
-        android:id="@+id/composerEditText"
+        android:id="@+id/richTextComposerEditText"
         style="@style/Widget.Vector.EditText.RichTextComposer"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:hint="@string/room_message_placeholder"
-        android:nextFocusLeft="@id/composerEditText"
-        android:nextFocusUp="@id/composerEditText"
+        android:nextFocusLeft="@id/richTextComposerEditText"
+        android:nextFocusUp="@id/richTextComposerEditText"
+        android:layout_marginStart="12dp"
+        android:layout_marginVertical="10dp"
+        app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
+        app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
+        tools:text="@tools:sample/lorem/random" />
+
+    <com.google.android.material.textfield.TextInputEditText
+        android:id="@+id/plainTextComposerEditText"
+        style="@style/Widget.Vector.EditText.RichTextComposer"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:hint="@string/room_message_placeholder"
+        android:nextFocusLeft="@id/plainTextComposerEditText"
+        android:nextFocusUp="@id/plainTextComposerEditText"
         android:layout_marginStart="12dp"
         android:layout_marginVertical="10dp"
         app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
diff --git a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml
index fd1efc7e44..3105063933 100644
--- a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml
+++ b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml
@@ -136,13 +136,30 @@
         app:layout_constraintEnd_toEndOf="parent" />
 
     <io.element.android.wysiwyg.EditorEditText
-        android:id="@+id/composerEditText"
+        android:id="@+id/richTextComposerEditText"
         style="@style/Widget.Vector.EditText.RichTextComposer"
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:hint="@string/room_message_placeholder"
-        android:nextFocusLeft="@id/composerEditText"
-        android:nextFocusUp="@id/composerEditText"
+        android:nextFocusLeft="@id/richTextComposerEditText"
+        android:nextFocusUp="@id/richTextComposerEditText"
+        android:layout_marginStart="12dp"
+        android:layout_marginVertical="10dp"
+        android:gravity="top"
+        app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
+        app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
+        app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
+        tools:text="@tools:sample/lorem/random" />
+
+    <com.google.android.material.textfield.TextInputEditText
+        android:id="@+id/plainTextComposerEditText"
+        style="@style/Widget.Vector.EditText.RichTextComposer"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:hint="@string/room_message_placeholder"
+        android:nextFocusLeft="@id/plainTextComposerEditText"
+        android:nextFocusUp="@id/plainTextComposerEditText"
         android:layout_marginStart="12dp"
         android:layout_marginVertical="10dp"
         android:gravity="top"
diff --git a/vector/src/test/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModelTest.kt b/vector/src/test/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModelTest.kt
new file mode 100644
index 0000000000..e20d498a37
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModelTest.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.attachments
+
+import com.airbnb.mvrx.test.MavericksTestRule
+import im.vector.app.test.fakes.FakeVectorFeatures
+import im.vector.app.test.fakes.FakeVectorPreferences
+import im.vector.app.test.test
+import io.mockk.verifyOrder
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+internal class AttachmentTypeSelectorViewModelTest {
+
+    @get:Rule
+    val mavericksTestRule = MavericksTestRule()
+
+    private val fakeVectorFeatures = FakeVectorFeatures()
+    private val fakeVectorPreferences = FakeVectorPreferences()
+    private val initialState = AttachmentTypeSelectorViewState()
+
+    @Before
+    fun setUp() {
+        // Disable all features by default
+        fakeVectorFeatures.givenLocationSharing(isEnabled = false)
+        fakeVectorFeatures.givenVoiceBroadcast(isEnabled = false)
+        fakeVectorPreferences.givenTextFormatting(isEnabled = false)
+    }
+
+    @Test
+    fun `given features are not enabled, then options are not visible`() {
+        createViewModel()
+                .test()
+                .assertStates(
+                        listOf(
+                                initialState,
+                        )
+                )
+                .finish()
+    }
+
+    @Test
+    fun `given location sharing is enabled, then location sharing option is visible`() {
+        fakeVectorFeatures.givenLocationSharing(isEnabled = true)
+
+        createViewModel()
+                .test()
+                .assertStates(
+                        listOf(
+                                initialState.copy(
+                                        isLocationVisible = true
+                                ),
+                        )
+                )
+                .finish()
+    }
+
+    @Test
+    fun `given voice broadcast is enabled, then voice broadcast option is visible`() {
+        fakeVectorFeatures.givenVoiceBroadcast(isEnabled = true)
+
+        createViewModel()
+                .test()
+                .assertStates(
+                        listOf(
+                                initialState.copy(
+                                        isVoiceBroadcastVisible = true
+                                ),
+                        )
+                )
+                .finish()
+    }
+
+    @Test
+    fun `given text formatting is enabled, then text formatting option is checked`() {
+        fakeVectorPreferences.givenTextFormatting(isEnabled = true)
+
+        createViewModel()
+                .test()
+                .assertStates(
+                        listOf(
+                                initialState.copy(
+                                        isTextFormattingEnabled = true
+                                ),
+                        )
+                )
+                .finish()
+    }
+
+    @Test
+    fun `when text formatting is changed, then it updates the UI`() {
+        createViewModel()
+                .apply {
+                    handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled = true))
+                }
+                .test()
+                .assertStates(
+                        listOf(
+                                initialState.copy(
+                                        isTextFormattingEnabled = true
+                                ),
+                        )
+                )
+                .finish()
+    }
+
+    @Test
+    fun `when text formatting is changed, then it persists the change`() {
+        createViewModel()
+                .apply {
+                    handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled = true))
+                    handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled = false))
+                }
+        verifyOrder {
+            fakeVectorPreferences.instance.setTextFormattingEnabled(true)
+            fakeVectorPreferences.instance.setTextFormattingEnabled(false)
+        }
+    }
+
+    private fun createViewModel(): AttachmentTypeSelectorViewModel {
+        return AttachmentTypeSelectorViewModel(
+                initialState,
+                vectorFeatures = fakeVectorFeatures,
+                vectorPreferences = fakeVectorPreferences.instance,
+        )
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt
index 4e6b4fc3df..d989abc214 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt
@@ -42,4 +42,12 @@ class FakeVectorFeatures : VectorFeatures by spyk<DefaultVectorFeatures>() {
     fun givenCombinedLoginDisabled() {
         every { isOnboardingCombinedLoginEnabled() } returns false
     }
+
+    fun givenLocationSharing(isEnabled: Boolean) {
+        every { isLocationSharingEnabled() } returns isEnabled
+    }
+
+    fun givenVoiceBroadcast(isEnabled: Boolean) {
+        every { isVoiceBroadcastEnabled() } returns isEnabled
+    }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
index 8b0630c24f..cd4f70bf63 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
@@ -40,4 +40,7 @@ class FakeVectorPreferences {
     fun givenIsClientInfoRecordingEnabled(isEnabled: Boolean) {
         every { instance.isClientInfoRecordingEnabled() } returns isEnabled
     }
+
+    fun givenTextFormatting(isEnabled: Boolean) =
+        every { instance.isTextFormattingEnabled() } returns isEnabled
 }