From cb14a23ec0b39ef477b3dd5b60e8fc6d12733d25 Mon Sep 17 00:00:00 2001
From: Taras Smakula <tarassmakula@gmail.com>
Date: Mon, 27 Nov 2023 18:00:11 +0200
Subject: [PATCH] Copy new markdown

---
 app/build.gradle                              |   5 +-
 .../circles/extensions/EditableExtensions.kt  |  48 ++
 .../java/org/futo/circles/view/Callback.kt    |  11 +
 .../org/futo/circles/view/ComposerEditText.kt | 123 ++++
 .../futo/circles/view/DimensionConverter.kt   |  46 ++
 .../futo/circles/view/MessageComposerMode.kt  |  28 +
 .../futo/circles/view/MessageComposerView.kt  |  41 ++
 .../futo/circles/view/PillDisplayHandler.kt   |  80 +++
 .../org/futo/circles/view/PillImageSpan.kt    | 141 ++++
 .../org/futo/circles/view/PreviewPostView.kt  |  62 +-
 .../circles/view/RichTextComposerLayout.kt    | 675 ++++++++++++++++++
 .../circles/view/RichTextEditorException.kt   |   5 +
 .../futo/circles/view/SimpleTextWatcher.kt    |  37 +
 .../futo/circles/view/UriContentListener.kt   |  45 ++
 .../color/selector_rich_text_menu_icon.xml    |   8 +
 app/src/main/res/drawable/bg_code_block.xml   |  22 +
 .../bg_inline_code_multi_line_left.xml        |  23 +
 .../bg_inline_code_multi_line_mid.xml         |  21 +
 .../bg_inline_code_multi_line_right.xml       |  23 +
 .../drawable/bg_inline_code_single_line.xml   |  22 +
 .../res/drawable/bg_rich_text_menu_button.xml |  18 +
 .../main/res/drawable/ic_composer_bold.xml    |  10 +
 .../res/drawable/ic_composer_bullet_list.xml  |  12 +
 .../res/drawable/ic_composer_code_block.xml   |   9 +
 .../res/drawable/ic_composer_collapse.xml     |   9 +
 .../res/drawable/ic_composer_full_screen.xml  |   9 +
 .../main/res/drawable/ic_composer_indent.xml  |  12 +
 .../res/drawable/ic_composer_inline_code.xml  |  15 +
 .../main/res/drawable/ic_composer_italic.xml  |  10 +
 .../main/res/drawable/ic_composer_link.xml    |  12 +
 .../drawable/ic_composer_numbered_list.xml    |  24 +
 .../main/res/drawable/ic_composer_quote.xml   |  18 +
 .../drawable/ic_composer_rich_mic_pressed.xml |  17 +
 .../ic_composer_rich_text_editor_close.xml    |   9 +
 .../ic_composer_rich_text_editor_edit.xml     |  12 +
 .../drawable/ic_composer_rich_text_save.xml   |  16 +
 .../drawable/ic_composer_strikethrough.xml    |  12 +
 .../res/drawable/ic_composer_underlined.xml   |  12 +
 .../res/drawable/ic_composer_unindent.xml     |  12 +
 app/src/main/res/drawable/ic_quote.xml        |  14 +
 .../res/drawable/ic_rich_composer_add.xml     |  15 +
 .../res/drawable/ic_rich_composer_send.xml    |  12 +
 .../res/layout/composer_rich_text_layout.xml  | 206 ++++++
 app/src/main/res/layout/view_preview_post.xml |  46 +-
 .../res/layout/view_rich_text_menu_button.xml |  11 +
 app/src/main/res/values/attrs.xml             |   2 +
 app/src/main/res/values/dimens.xml            |   4 +
 app/src/main/res/values/strings.xml           |  20 +
 build.gradle                                  |   2 +-
 49 files changed, 1998 insertions(+), 48 deletions(-)
 create mode 100644 app/src/main/java/org/futo/circles/view/Callback.kt
 create mode 100644 app/src/main/java/org/futo/circles/view/ComposerEditText.kt
 create mode 100644 app/src/main/java/org/futo/circles/view/DimensionConverter.kt
 create mode 100644 app/src/main/java/org/futo/circles/view/MessageComposerMode.kt
 create mode 100644 app/src/main/java/org/futo/circles/view/MessageComposerView.kt
 create mode 100644 app/src/main/java/org/futo/circles/view/PillDisplayHandler.kt
 create mode 100644 app/src/main/java/org/futo/circles/view/PillImageSpan.kt
 create mode 100644 app/src/main/java/org/futo/circles/view/RichTextComposerLayout.kt
 create mode 100644 app/src/main/java/org/futo/circles/view/RichTextEditorException.kt
 create mode 100644 app/src/main/java/org/futo/circles/view/SimpleTextWatcher.kt
 create mode 100644 app/src/main/java/org/futo/circles/view/UriContentListener.kt
 create mode 100644 app/src/main/res/color/selector_rich_text_menu_icon.xml
 create mode 100644 app/src/main/res/drawable/bg_code_block.xml
 create mode 100644 app/src/main/res/drawable/bg_inline_code_multi_line_left.xml
 create mode 100644 app/src/main/res/drawable/bg_inline_code_multi_line_mid.xml
 create mode 100644 app/src/main/res/drawable/bg_inline_code_multi_line_right.xml
 create mode 100644 app/src/main/res/drawable/bg_inline_code_single_line.xml
 create mode 100644 app/src/main/res/drawable/bg_rich_text_menu_button.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_bold.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_bullet_list.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_code_block.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_collapse.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_full_screen.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_indent.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_inline_code.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_italic.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_link.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_numbered_list.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_quote.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_rich_mic_pressed.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_rich_text_editor_close.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_rich_text_save.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_strikethrough.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_underlined.xml
 create mode 100644 app/src/main/res/drawable/ic_composer_unindent.xml
 create mode 100644 app/src/main/res/drawable/ic_quote.xml
 create mode 100644 app/src/main/res/drawable/ic_rich_composer_add.xml
 create mode 100644 app/src/main/res/drawable/ic_rich_composer_send.xml
 create mode 100644 app/src/main/res/layout/composer_rich_text_layout.xml
 create mode 100644 app/src/main/res/layout/view_rich_text_menu_button.xml

diff --git a/app/build.gradle b/app/build.gradle
index c7f1c027f..dcb78526d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -22,7 +22,7 @@ android {
         viewBinding true
     }
 
-    signingConfigs{
+    signingConfigs {
         release {
             Properties properties = new Properties()
             if (rootProject.file("signing.properties").exists()) {
@@ -77,7 +77,7 @@ dependencies {
     implementation project(path: ':gallery')
 
     //Firebase
-    gplayImplementation platform('com.google.firebase:firebase-bom:32.5.0')
+    gplayImplementation platform('com.google.firebase:firebase-bom:32.6.0')
     gplayImplementation 'com.google.firebase:firebase-crashlytics-ktx'
     gplayImplementation 'com.google.firebase:firebase-analytics-ktx'
     gplayImplementation 'com.google.firebase:firebase-messaging-ktx'
@@ -94,6 +94,7 @@ dependencies {
     implementation "io.noties.markwon:linkify:$markwon_version"
     implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
     implementation "io.noties.markwon:ext-tasklist:$markwon_version"
+    implementation "io.element.android:wysiwyg:2.2.2"
 
     //Log
     implementation 'com.jakewharton.timber:timber:5.0.1'
diff --git a/app/src/main/java/org/futo/circles/extensions/EditableExtensions.kt b/app/src/main/java/org/futo/circles/extensions/EditableExtensions.kt
index bc1f20a2f..2bb56bfad 100644
--- a/app/src/main/java/org/futo/circles/extensions/EditableExtensions.kt
+++ b/app/src/main/java/org/futo/circles/extensions/EditableExtensions.kt
@@ -1,6 +1,11 @@
 package org.futo.circles.extensions
 
 import android.text.Editable
+import android.text.Spanned
+import android.view.View
+import android.view.inputmethod.InputMethodManager
+import android.widget.EditText
+import androidx.core.content.getSystemService
 import org.futo.circles.feature.timeline.post.markdown.span.TextStyle
 import org.futo.circles.feature.timeline.post.markdown.span.toSpanClass
 
@@ -15,3 +20,46 @@ fun Editable.getGivenSpansAt(
     }
     return spanList
 }
+
+fun View.showKeyboard(andRequestFocus: Boolean = false) {
+    if (andRequestFocus) {
+        requestFocus()
+    }
+    val imm = context?.getSystemService<InputMethodManager>()
+    imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
+}
+
+fun EditText.setTextIfDifferent(newText: CharSequence?): Boolean {
+    if (!isTextDifferent(newText, text)) {
+        // Previous text is the same. No op
+        return false
+    }
+    setText(newText)
+    // Since the text changed we move the cursor to the end of the new text.
+    // This allows us to fill in text programmatically with a different value,
+    // but if the user is typing and the view is rebound we won't lose their cursor position.
+    setSelection(newText?.length ?: 0)
+    return true
+}
+
+private fun isTextDifferent(str1: CharSequence?, str2: CharSequence?): Boolean {
+    if (str1 === str2) {
+        return false
+    }
+    if (str1 == null || str2 == null) {
+        return true
+    }
+    val length = str1.length
+    if (length != str2.length) {
+        return true
+    }
+    if (str1 is Spanned) {
+        return str1 != str2
+    }
+    for (i in 0 until length) {
+        if (str1[i] != str2[i]) {
+            return true
+        }
+    }
+    return false
+}
diff --git a/app/src/main/java/org/futo/circles/view/Callback.kt b/app/src/main/java/org/futo/circles/view/Callback.kt
new file mode 100644
index 000000000..28f3adb08
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/view/Callback.kt
@@ -0,0 +1,11 @@
+package org.futo.circles.view
+
+
+interface Callback : ComposerEditText.Callback {
+    fun onCloseRelatedMessage()
+    fun onSendMessage(text: CharSequence)
+    fun onAddAttachment()
+    fun onExpandOrCompactChange()
+    fun onFullScreenModeChanged()
+    fun onSetLink(isTextSupported: Boolean, initialLink: String?)
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/futo/circles/view/ComposerEditText.kt b/app/src/main/java/org/futo/circles/view/ComposerEditText.kt
new file mode 100644
index 000000000..8dba74e1c
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/view/ComposerEditText.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2019 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 org.futo.circles.view
+
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.text.Editable
+import android.util.AttributeSet
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputConnection
+import androidx.annotation.RequiresApi
+import androidx.appcompat.widget.AppCompatEditText
+import androidx.core.view.ViewCompat
+import androidx.core.view.inputmethod.EditorInfoCompat
+import androidx.core.view.inputmethod.InputConnectionCompat
+import timber.log.Timber
+
+class ComposerEditText @JvmOverloads constructor(
+        context: Context,
+        attrs: AttributeSet? = null,
+        defStyleAttr: Int = android.R.attr.editTextStyle
+) : AppCompatEditText(context, attrs, defStyleAttr) {
+
+    interface Callback {
+        fun onRichContentSelected(contentUri: Uri): Boolean
+        fun onTextChanged(text: CharSequence)
+    }
+
+    var callback: Callback? = null
+
+    override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? {
+        var ic = super.onCreateInputConnection(editorInfo) ?: return null
+        val mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this) ?: arrayOf("image/*")
+
+        EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes)
+        ic = InputConnectionCompat.createWrapper(this, ic, editorInfo)
+
+        ViewCompat.setOnReceiveContentListener(
+                this,
+                mimeTypes,
+                UriContentListener { callback?.onRichContentSelected(it) }
+        )
+
+        return ic
+    }
+
+    /** Set whether the keyboard should disable personalized learning. */
+    @RequiresApi(Build.VERSION_CODES.O)
+    fun setUseIncognitoKeyboard(useIncognitoKeyboard: Boolean) {
+        imeOptions = if (useIncognitoKeyboard) {
+            imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
+        } else {
+            imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
+        }
+    }
+
+    /** Set whether enter should send the message or add a new line. */
+    fun setSendMessageWithEnter(sendMessageWithEnter: Boolean) {
+        if (sendMessageWithEnter) {
+            inputType = inputType and EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE.inv()
+            imeOptions = imeOptions or EditorInfo.IME_ACTION_SEND
+        } else {
+            inputType = inputType or EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
+            imeOptions = imeOptions and EditorInfo.IME_ACTION_SEND.inv()
+        }
+    }
+
+    init {
+        addTextChangedListener(
+                object : SimpleTextWatcher() {
+                    var spanToRemove: PillImageSpan? = null
+
+                    override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+                        Timber.v("Pills: beforeTextChanged: start:$start count:$count after:$after")
+
+                        if (count > after) {
+                            // A char has been deleted
+                            val deleteCharPosition = start + count
+                            Timber.v("Pills: beforeTextChanged: deleted char at $deleteCharPosition")
+
+                            // Get the first span at this position
+                            spanToRemove = editableText.getSpans(deleteCharPosition, deleteCharPosition, PillImageSpan::class.java)
+                                    .ooi { Timber.v("Pills: beforeTextChanged: found ${it.size} span(s)") }
+                                    .firstOrNull()
+                        }
+                    }
+
+                    override fun afterTextChanged(s: Editable) {
+                        if (spanToRemove != null) {
+                            val start = editableText.getSpanStart(spanToRemove)
+                            val end = editableText.getSpanEnd(spanToRemove)
+                            Timber.v("Pills: afterTextChanged Removing the span start:$start end:$end")
+                            // Must be done before text replacement
+                            editableText.removeSpan(spanToRemove)
+                            if (start != -1 && end != -1) {
+                                editableText.replace(start, end, "")
+                            }
+                            spanToRemove = null
+                        }
+                        callback?.onTextChanged(s.toString())
+                    }
+                }
+        )
+    }
+}
+
+inline fun <T> T.ooi(block: (T) -> Unit): T = also(block)
diff --git a/app/src/main/java/org/futo/circles/view/DimensionConverter.kt b/app/src/main/java/org/futo/circles/view/DimensionConverter.kt
new file mode 100644
index 000000000..2225c471d
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/view/DimensionConverter.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019 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 org.futo.circles.view
+
+import android.content.res.Resources
+import android.util.TypedValue
+import androidx.annotation.Px
+import javax.inject.Inject
+
+class DimensionConverter @Inject constructor(val resources: Resources) {
+
+    @Px
+    fun dpToPx(dp: Int): Int {
+        return TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP,
+                dp.toFloat(),
+                resources.displayMetrics
+        ).toInt()
+    }
+
+    @Px
+    fun spToPx(sp: Int): Int {
+        return TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_SP,
+                sp.toFloat(),
+                resources.displayMetrics
+        ).toInt()
+    }
+
+    fun pxToDp(@Px px: Int): Int {
+        return (px.toFloat() / resources.displayMetrics.density).toInt()
+    }
+}
diff --git a/app/src/main/java/org/futo/circles/view/MessageComposerMode.kt b/app/src/main/java/org/futo/circles/view/MessageComposerMode.kt
new file mode 100644
index 000000000..c9ebbf983
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/view/MessageComposerMode.kt
@@ -0,0 +1,28 @@
+/*
+ * 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 org.futo.circles.view
+
+
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+
+sealed interface MessageComposerMode {
+    data class Normal(val content: CharSequence?) : MessageComposerMode
+
+    sealed class Special(open val event: TimelineEvent, open val defaultContent: CharSequence) : MessageComposerMode
+    data class Edit(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent)
+    data class Quote(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent)
+    data class Reply(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent)
+}
diff --git a/app/src/main/java/org/futo/circles/view/MessageComposerView.kt b/app/src/main/java/org/futo/circles/view/MessageComposerView.kt
new file mode 100644
index 000000000..e81441dfc
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/view/MessageComposerView.kt
@@ -0,0 +1,41 @@
+/*
+ * 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 org.futo.circles.view
+
+import android.text.Editable
+import android.widget.EditText
+import android.widget.ImageButton
+
+interface MessageComposerView {
+
+    companion object {
+        const val MAX_LINES_WHEN_COLLAPSED = 10
+    }
+
+    val text: Editable?
+    val formattedText: String?
+    val editText: EditText
+    val emojiButton: ImageButton?
+    val sendButton: ImageButton
+    val attachmentButton: ImageButton
+
+    var callback: Callback?
+
+    fun setTextIfDifferent(text: CharSequence?): Boolean
+    fun renderComposerMode(mode: MessageComposerMode)
+}
+
diff --git a/app/src/main/java/org/futo/circles/view/PillDisplayHandler.kt b/app/src/main/java/org/futo/circles/view/PillDisplayHandler.kt
new file mode 100644
index 000000000..b586e28ec
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/view/PillDisplayHandler.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2023 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 org.futo.circles.view
+
+import android.text.style.ReplacementSpan
+import io.element.android.wysiwyg.display.KeywordDisplayHandler
+import io.element.android.wysiwyg.display.LinkDisplayHandler
+import io.element.android.wysiwyg.display.TextDisplay
+import org.matrix.android.sdk.api.session.permalinks.PermalinkData
+import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
+import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.util.MatrixItem
+import org.matrix.android.sdk.api.util.toEveryoneInRoomMatrixItem
+import org.matrix.android.sdk.api.util.toMatrixItem
+import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem
+
+/**
+ * A rich text editor [LinkDisplayHandler] and [KeywordDisplayHandler]
+ * that helps with replacing user and room links with pills.
+ */
+internal class PillDisplayHandler(
+        private val roomId: String,
+        private val getRoom: (roomId: String) -> RoomSummary?,
+        private val getMember: (userId: String) -> RoomMemberSummary?,
+        private val replacementSpanFactory: (matrixItem: MatrixItem) -> ReplacementSpan,
+) : LinkDisplayHandler, KeywordDisplayHandler {
+    override fun resolveLinkDisplay(text: String, url: String): TextDisplay {
+        val matrixItem = when (val permalink = PermalinkParser.parse(url)) {
+            is PermalinkData.UserLink -> {
+                val userId = permalink.userId
+                when (val roomMember = getMember(userId)) {
+                    null -> MatrixItem.UserItem(userId, userId, null)
+                    else -> roomMember.toMatrixItem()
+                }
+            }
+            is PermalinkData.RoomLink -> {
+                val roomId = permalink.roomIdOrAlias
+                val room = getRoom(roomId)
+                when {
+                    room == null -> MatrixItem.RoomItem(roomId, roomId, null)
+                    text == MatrixItem.NOTIFY_EVERYONE -> room.toEveryoneInRoomMatrixItem()
+                    permalink.isRoomAlias -> room.toRoomAliasMatrixItem()
+                    else -> room.toMatrixItem()
+                }
+            }
+            else ->
+                return TextDisplay.Plain
+        }
+        val replacement = replacementSpanFactory.invoke(matrixItem)
+        return TextDisplay.Custom(customSpan = replacement)
+    }
+
+    override val keywords: List<String>
+        get() = listOf(MatrixItem.NOTIFY_EVERYONE)
+
+    override fun resolveKeywordDisplay(text: String): TextDisplay =
+            when (text) {
+                MatrixItem.NOTIFY_EVERYONE -> {
+                    val matrixItem = getRoom(roomId)?.toEveryoneInRoomMatrixItem()
+                            ?: MatrixItem.EveryoneInRoomItem(roomId)
+                    TextDisplay.Custom(replacementSpanFactory.invoke(matrixItem))
+                }
+                else -> TextDisplay.Plain
+            }
+}
diff --git a/app/src/main/java/org/futo/circles/view/PillImageSpan.kt b/app/src/main/java/org/futo/circles/view/PillImageSpan.kt
new file mode 100644
index 000000000..ba4a0f78d
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/view/PillImageSpan.kt
@@ -0,0 +1,141 @@
+package org.futo.circles.view
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.text.TextUtils
+import android.text.style.ReplacementSpan
+import android.widget.TextView
+import androidx.annotation.UiThread
+import androidx.appcompat.widget.ThemeUtils
+import androidx.core.content.ContextCompat
+import com.google.android.material.chip.ChipDrawable
+import org.futo.circles.R
+import org.futo.circles.core.glide.GlideRequests
+import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
+import org.matrix.android.sdk.api.util.MatrixItem
+import java.lang.ref.WeakReference
+
+/**
+ * This span is able to replace a text by a [ChipDrawable]
+ * It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
+ * Implements MatrixItemSpan so that it could be automatically transformed in matrix links and displayed as pills.
+ */
+class PillImageSpan(
+    private val glideRequests: GlideRequests,
+    private val context: Context,
+    override val matrixItem: MatrixItem
+) : ReplacementSpan(), MatrixItemSpan {
+
+    private val pillDrawable = createChipDrawable()
+   // private val target = PillImageSpanTarget(this)
+    private var tv: WeakReference<TextView>? = null
+
+    @UiThread
+    fun bind(textView: TextView) {
+        tv = WeakReference(textView)
+        //avatarRenderer.render(glideRequests, matrixItem, target)
+    }
+
+    // ReplacementSpan *****************************************************************************
+
+    override fun getSize(
+        paint: Paint, text: CharSequence,
+        start: Int,
+        end: Int,
+        fm: Paint.FontMetricsInt?
+    ): Int {
+        val rect = pillDrawable.bounds
+        if (fm != null) {
+            val fmPaint = paint.fontMetricsInt
+            val fontHeight = fmPaint.bottom - fmPaint.top
+            val drHeight = rect.bottom - rect.top
+            val top = drHeight / 2 - fontHeight / 4
+            val bottom = drHeight / 2 + fontHeight / 4
+            fm.ascent = -bottom
+            fm.top = -bottom
+            fm.bottom = top
+            fm.descent = top
+        }
+        return rect.right
+    }
+
+    override fun draw(
+        canvas: Canvas, text: CharSequence,
+        start: Int,
+        end: Int,
+        x: Float,
+        top: Int,
+        y: Int,
+        bottom: Int,
+        paint: Paint
+    ) {
+        canvas.save()
+        val fm = paint.fontMetricsInt
+        val transY: Int = y + (fm.descent + fm.ascent - pillDrawable.bounds.bottom) / 2
+        canvas.save()
+        canvas.translate(x, transY.toFloat())
+
+        val rect = Rect()
+        canvas.getClipBounds(rect)
+        val maxWidth = rect.right
+        if (pillDrawable.intrinsicWidth > maxWidth) {
+            pillDrawable.setBounds(0, 0, maxWidth, pillDrawable.intrinsicHeight)
+            pillDrawable.ellipsize = TextUtils.TruncateAt.END
+        }
+
+        pillDrawable.draw(canvas)
+        canvas.restore()
+    }
+
+    internal fun updateAvatarDrawable(drawable: Drawable?) {
+        pillDrawable.chipIcon = drawable
+        tv?.get()?.invalidate()
+    }
+
+    // Private methods *****************************************************************************
+
+    private fun createChipDrawable(): ChipDrawable {
+        val textPadding = context.resources.getDimension(R.dimen.divider_height)
+        val icon = when {
+//            matrixItem is MatrixItem.RoomAliasItem && matrixItem.avatarUrl.isNullOrEmpty() &&
+//                    matrixItem.displayName == context.getString(R.string.followed_by_format, matrixItem.id) -> {
+//                ContextCompat.getDrawable(context, R.drawable.ic_permalink_round)
+//            }
+//            matrixItem is MatrixItem.RoomItem && matrixItem.avatarUrl.isNullOrEmpty() && (
+//                    matrixItem.displayName == context.getString(R.string.pill_message_in_unknown_room) ||
+//                            matrixItem.displayName == context.getString(R.string.pill_message_unknown_room_or_space) ||
+//                            matrixItem.displayName == context.getString(R.string.pill_message_from_unknown_user)
+//                    ) -> {
+//                ContextCompat.getDrawable(context, R.drawable.ic_composer_bold)
+//            }
+            matrixItem is MatrixItem.UserItem && matrixItem.avatarUrl.isNullOrEmpty()  -> {
+                ContextCompat.getDrawable(context, R.drawable.ic_composer_bold)
+            }
+            else -> {
+                try {
+                  //  avatarRenderer.getCachedDrawable(glideRequests, matrixItem)
+                } catch (exception: Exception) {
+                   // avatarRenderer.getPlaceholderDrawable(matrixItem)
+                }
+            }
+        }
+
+        return ChipDrawable.createFromResource(context, R.xml.bg_chip).apply {
+            text = "matrixItem.getBestName()"
+            textEndPadding = textPadding
+            textStartPadding = textPadding
+            setChipMinHeightResource(R.dimen.rich_text_composer_corner_radius_expanded)
+            setChipIconSizeResource(R.dimen.rich_text_composer_corner_radius_expanded)
+            //chipIcon = icon
+            if (matrixItem is MatrixItem.EveryoneInRoomItem) {
+                // setTextColor API does not exist right now for ChipDrawable, use textAppearance
+            }
+            setBounds(0, 0, intrinsicWidth, intrinsicHeight)
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/futo/circles/view/PreviewPostView.kt b/app/src/main/java/org/futo/circles/view/PreviewPostView.kt
index 06264d5c6..238befaa9 100644
--- a/app/src/main/java/org/futo/circles/view/PreviewPostView.kt
+++ b/app/src/main/java/org/futo/circles/view/PreviewPostView.kt
@@ -59,9 +59,9 @@ class PreviewPostView(
             )
         }
         setOnClickListener { requestFocusOnText() }
-        binding.etTextPost.doAfterTextChanged {
-            listener?.onPostContentAvailable(it?.toString()?.isNotBlank() == true)
-        }
+//        binding.etTextPost.doAfterTextChanged {
+//            listener?.onPostContentAvailable(it?.toString()?.isNotBlank() == true)
+//        }
         binding.ivRemoveImage.setOnClickListener {
             setTextContent()
         }
@@ -74,32 +74,32 @@ class PreviewPostView(
         roomId: String
     ) {
         listener = previewPostListener
-        binding.etTextPost.setHighlightSelectedSpanListener(onHighlightTextStyle)
-        binding.etTextPost.initMentionsAutocomplete(roomId)
+       // binding.etTextPost.setHighlightSelectedSpanListener(onHighlightTextStyle)
+        //binding.etTextPost.initMentionsAutocomplete(roomId)
     }
 
     fun setText(message: String) {
-        binding.etTextPost.setText(
-            MarkdownParser.markwonBuilder(context).toMarkdown(message),
-            TextView.BufferType.SPANNABLE
-        )
+//        binding.etTextPost.setText(
+//            MarkdownParser.markwonBuilder(context).toMarkdown(message),
+//            TextView.BufferType.SPANNABLE
+//        )
         setTextContent()
     }
 
     fun setTextStyle(style: TextStyle, isSelected: Boolean) {
-        binding.etTextPost.triggerStyle(style, isSelected)
+       // binding.etTextPost.triggerStyle(style, isSelected)
     }
 
     fun insertEmoji(unicode: String) {
-        binding.etTextPost.insertText(unicode)
+       // binding.etTextPost.insertText(unicode)
     }
 
     fun insertMention() {
-        binding.etTextPost.insertMentionMark()
+        //binding.etTextPost.insertMentionMark()
     }
 
     fun insertLink(title: String?, link: String) {
-        binding.etTextPost.addLinkSpan(title, link)
+       // binding.etTextPost.addLinkSpan(title, link)
     }
 
     fun setMediaFromExistingPost(mediaContent: MediaContent) {
@@ -121,8 +121,8 @@ class PreviewPostView(
 
 
     fun setMedia(contentUri: Uri, mediaType: MediaType) {
-        val caption = binding.etTextPost.text.toString().trim()
-        postContent = MediaPostContent(caption, contentUri, mediaType)
+       // val caption = binding.etTextPost.text.toString().trim()
+       // postContent = MediaPostContent(caption, contentUri, mediaType)
         updateContentView()
         loadMediaCover(contentUri, mediaType)
         val isVideo = mediaType == MediaType.Video
@@ -134,26 +134,28 @@ class PreviewPostView(
         listener?.onPostContentAvailable(true)
     }
 
-    fun getPostContent() = (postContent as? MediaPostContent)?.copy(
-        caption = binding.etTextPost.getTextWithMarkdown().trim().takeIf { it.isNotEmpty() }
-    ) ?: TextPostContent(binding.etTextPost.getTextWithMarkdown().trim())
+    fun getPostContent() = TextPostContent("binding.etTextPost.getMarkdown().trim()")
+
+//        ?.copy(
+//        caption = binding.etTextPost.getTextWithMarkdown().trim().takeIf { it.isNotEmpty() }
+//    ) ?: TextPostContent(binding.etTextPost.getTextWithMarkdown().trim())
 
     private fun updateContentView() {
         val isTextContent = postContent is TextPostContent || postContent == null
         binding.lMediaContent.lMedia.setIsVisible(!isTextContent)
         binding.ivRemoveImage.setIsVisible(!isTextContent && canEditMedia)
         if (isTextContent) requestFocusOnText()
-        binding.etTextPost.setPadding(
-            context.convertDpToPixel(12f).toInt(),
-            0, context.convertDpToPixel(12f).toInt(),
-            if (isTextContent) context.convertDpToPixel(64f).toInt() else 0
-        )
+//        binding.etTextPost.setPadding(
+//            context.convertDpToPixel(12f).toInt(),
+//            0, context.convertDpToPixel(12f).toInt(),
+//            if (isTextContent) context.convertDpToPixel(64f).toInt() else 0
+//        )
     }
 
     private fun setTextContent() {
         postContent = null
         updateContentView()
-        listener?.onPostContentAvailable(binding.etTextPost.text.toString().isNotBlank())
+        listener?.onPostContentAvailable(false)
     }
 
     private fun loadMediaCover(uri: Uri, mediaType: MediaType) {
@@ -184,17 +186,17 @@ class PreviewPostView(
     }
 
     private fun requestFocusOnText() {
-        binding.etTextPost.post {
-            requestFocus()
-            binding.etTextPost.setSelection(binding.etTextPost.text.length)
-            showKeyboard()
-        }
+//        binding.etTextPost.post {
+//            requestFocus()
+//           // binding.etTextPost.setSelection(binding.etTextPost.text.length)
+//            showKeyboard()
+//        }
     }
 
     private fun showKeyboard() {
         val inputMethodManager: InputMethodManager =
             context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
-        inputMethodManager.showSoftInput(binding.etTextPost, 0)
+       // inputMethodManager.showSoftInput(binding.etTextPost, 0)
     }
 
     private fun getMyUser(): User? {
diff --git a/app/src/main/java/org/futo/circles/view/RichTextComposerLayout.kt b/app/src/main/java/org/futo/circles/view/RichTextComposerLayout.kt
new file mode 100644
index 000000000..d14371955
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/view/RichTextComposerLayout.kt
@@ -0,0 +1,675 @@
+package org.futo.circles.view
+
+/*
+ * 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.
+ */
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.res.ColorStateList
+import android.content.res.Configuration
+import android.graphics.Color
+import android.text.Editable
+import android.text.TextWatcher
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.widget.EditText
+import android.widget.ImageButton
+import android.widget.LinearLayout
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.core.text.toSpannable
+import androidx.core.view.ViewCompat
+import androidx.core.view.isGone
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
+import com.google.android.material.shape.MaterialShapeDrawable
+import io.element.android.wysiwyg.EditorEditText
+import io.element.android.wysiwyg.display.KeywordDisplayHandler
+import io.element.android.wysiwyg.display.LinkDisplayHandler
+import io.element.android.wysiwyg.display.TextDisplay
+import io.element.android.wysiwyg.utils.RustErrorCollector
+import io.element.android.wysiwyg.view.models.InlineFormat
+import io.element.android.wysiwyg.view.models.LinkAction
+import org.futo.circles.R
+import org.futo.circles.databinding.ComposerRichTextLayoutBinding
+import org.futo.circles.databinding.ViewRichTextMenuButtonBinding
+import org.futo.circles.extensions.setTextIfDifferent
+import org.futo.circles.extensions.showKeyboard
+import uniffi.wysiwyg_composer.ActionState
+import uniffi.wysiwyg_composer.ComposerAction
+
+internal class RichTextComposerLayout @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr), MessageComposerView {
+
+    private val views: ComposerRichTextLayoutBinding
+
+    override var callback: Callback? = null
+
+    // There is no need to persist these values since they're always updated by the parent fragment
+    private var isFullScreen = false
+    private var hasRelatedMessage = false
+    private var composerMode: MessageComposerMode? = null
+
+    var isTextFormattingEnabled = true
+        set(value) {
+            if (field == value) return
+            syncEditTexts()
+            field = value
+            updateTextFieldBorder(isFullScreen)
+            updateEditTextVisibility()
+            updateFullScreenButtonVisibility()
+            // If formatting is no longer enabled and it's in full screen, minimise the editor
+            if (!value && isFullScreen) {
+                callback?.onFullScreenModeChanged()
+            }
+        }
+
+    override val text: Editable?
+        get() = editText.text
+    override val formattedText: String?
+        get() = (editText as? EditorEditText)?.getContentAsMessageHtml()
+    override val editText: EditText
+        get() = if (isTextFormattingEnabled) {
+            views.richTextComposerEditText
+        } else {
+            views.plainTextComposerEditText
+        }
+    override val emojiButton: ImageButton?
+        get() = null
+    override val sendButton: ImageButton
+        get() = views.sendButton
+    override val attachmentButton: ImageButton
+        get() = views.attachmentButton
+
+    val richTextEditText: EditText
+        get() =
+            views.richTextComposerEditText
+    val plainTextEditText: EditText
+        get() =
+            views.plainTextComposerEditText
+
+    var pillDisplayHandler: PillDisplayHandler? = null
+
+    // Border of the EditText
+    private val borderShapeDrawable: MaterialShapeDrawable by lazy {
+        MaterialShapeDrawable().apply {
+            val typedData = TypedValue()
+            val lineColor = context.theme.obtainStyledAttributes(
+                typedData.data,
+                intArrayOf(R.attr.vctr_content_quaternary)
+            )
+                .getColor(0, 0)
+            strokeColor = ColorStateList.valueOf(lineColor)
+            strokeWidth = 1 * resources.displayMetrics.scaledDensity
+            fillColor = ColorStateList.valueOf(Color.TRANSPARENT)
+            val cornerSize =
+                resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_single_line)
+            setCornerSize(cornerSize.toFloat())
+        }
+    }
+
+    private val dimensionConverter = DimensionConverter(resources)
+
+    fun setFullScreen(isFullScreen: Boolean, animated: Boolean) {
+        if (!animated && views.composerLayout.layoutParams != null) {
+            views.composerLayout.updateLayoutParams<ViewGroup.LayoutParams> {
+                height =
+                    if (isFullScreen) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT
+            }
+        }
+        editText.updateLayoutParams<ViewGroup.LayoutParams> {
+            height =
+                if (isFullScreen) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT
+        }
+
+        updateTextFieldBorder(isFullScreen)
+        updateEditTextVisibility()
+
+        updateEditTextFullScreenState(views.richTextComposerEditText, isFullScreen)
+        updateEditTextFullScreenState(views.plainTextComposerEditText, isFullScreen)
+
+        views.composerFullScreenButton.setImageResource(
+            if (isFullScreen) R.drawable.ic_composer_collapse else R.drawable.ic_composer_full_screen
+        )
+
+        views.bottomSheetHandle.isVisible = isFullScreen
+        if (isFullScreen) {
+            editText.showKeyboard(true)
+        }
+        this.isFullScreen = isFullScreen
+    }
+
+    fun notifyIsBeingDragged(percentage: Float) {
+        // Calculate a new shape for the border according to the position in screen
+        val isSingleLine = editText.lineCount == 1
+        val cornerSize = if (!isSingleLine || hasRelatedMessage) {
+            resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_expanded)
+                .toFloat()
+        } else {
+            val multilineCornerSize =
+                resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_expanded)
+            val singleLineCornerSize =
+                resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_single_line)
+            val diff = singleLineCornerSize - multilineCornerSize
+            multilineCornerSize + diff * (1 - percentage)
+        }
+        if (cornerSize != borderShapeDrawable.bottomLeftCornerResolvedSize) {
+            borderShapeDrawable.setCornerSize(cornerSize)
+        }
+
+        // Change maxLines while dragging, this should improve the smoothness of animations
+        val maxLines = if (percentage > 0.25f) {
+            Int.MAX_VALUE
+        } else {
+            MessageComposerView.MAX_LINES_WHEN_COLLAPSED
+        }
+        views.richTextComposerEditText.maxLines = maxLines
+        views.plainTextComposerEditText.maxLines = maxLines
+
+        views.bottomSheetHandle.isVisible = true
+    }
+
+    init {
+        inflate(context, R.layout.composer_rich_text_layout, this)
+        views = ComposerRichTextLayoutBinding.bind(this)
+
+        // Workaround to avoid cut-off text caused by padding in scrolled TextView (there is no clipToPadding).
+        // In TextView, clipTop = padding, but also clipTop -= shadowRadius. So if we set the shadowRadius to padding, they cancel each other
+        views.richTextComposerEditText.setShadowLayer(
+            views.richTextComposerEditText.paddingBottom.toFloat(),
+            0f,
+            0f,
+            0
+        )
+        views.plainTextComposerEditText.setShadowLayer(
+            views.richTextComposerEditText.paddingBottom.toFloat(),
+            0f,
+            0f,
+            0
+        )
+
+        renderComposerMode(MessageComposerMode.Normal(null))
+
+        views.richTextComposerEditText.addTextChangedListener(
+            TextChangeListener(
+                { callback?.onTextChanged(it) },
+                { updateTextFieldBorder(isFullScreen) })
+        )
+        views.plainTextComposerEditText.addTextChangedListener(
+            TextChangeListener(
+                { callback?.onTextChanged(it) },
+                { updateTextFieldBorder(isFullScreen) })
+        )
+        ViewCompat.setOnReceiveContentListener(
+            views.richTextComposerEditText,
+            arrayOf("image/*"),
+            UriContentListener { callback?.onRichContentSelected(it) }
+        )
+        ViewCompat.setOnReceiveContentListener(
+            views.plainTextComposerEditText,
+            arrayOf("image/*"),
+            UriContentListener { callback?.onRichContentSelected(it) }
+        )
+
+        disallowParentInterceptTouchEvent(views.richTextComposerEditText)
+        disallowParentInterceptTouchEvent(views.plainTextComposerEditText)
+
+        views.composerModeCloseView.setOnClickListener {
+            callback?.onCloseRelatedMessage()
+        }
+
+        views.sendButton.setOnClickListener {
+            val textMessage = text?.toSpannable() ?: ""
+            callback?.onSendMessage(textMessage)
+        }
+
+        views.attachmentButton.setOnClickListener {
+            callback?.onAddAttachment()
+        }
+
+        views.composerFullScreenButton.apply {
+            updateFullScreenButtonVisibility()
+            setOnClickListener {
+                callback?.onFullScreenModeChanged()
+            }
+        }
+
+        views.composerEditTextOuterBorder.background = borderShapeDrawable
+
+        setupRichTextMenu()
+        views.richTextComposerEditText.linkDisplayHandler = LinkDisplayHandler { text, url ->
+            pillDisplayHandler?.resolveLinkDisplay(text, url) ?: TextDisplay.Plain
+        }
+        views.richTextComposerEditText.keywordDisplayHandler = object : KeywordDisplayHandler {
+            override val keywords: List<String>
+                get() = pillDisplayHandler?.keywords.orEmpty()
+
+            override fun resolveKeywordDisplay(text: String): TextDisplay =
+                pillDisplayHandler?.resolveKeywordDisplay(text) ?: TextDisplay.Plain
+        }
+
+        updateTextFieldBorder(isFullScreen)
+    }
+
+    private fun setupRichTextMenu() {
+        addRichTextMenuItem(
+            R.drawable.ic_composer_bold,
+            R.string.rich_text_editor_format_bold,
+            ComposerAction.BOLD
+        ) {
+            views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Bold)
+        }
+        addRichTextMenuItem(
+            R.drawable.ic_composer_italic,
+            R.string.rich_text_editor_format_italic,
+            ComposerAction.ITALIC
+        ) {
+            views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Italic)
+        }
+        addRichTextMenuItem(
+            R.drawable.ic_composer_underlined,
+            R.string.rich_text_editor_format_underline,
+            ComposerAction.UNDERLINE
+        ) {
+            views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Underline)
+        }
+        addRichTextMenuItem(
+            R.drawable.ic_composer_strikethrough,
+            R.string.rich_text_editor_format_strikethrough,
+            ComposerAction.STRIKE_THROUGH
+        ) {
+            views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
+        }
+        addRichTextMenuItem(
+            R.drawable.ic_composer_bullet_list,
+            R.string.rich_text_editor_bullet_list,
+            ComposerAction.UNORDERED_LIST
+        ) {
+            views.richTextComposerEditText.toggleList(ordered = false)
+        }
+        addRichTextMenuItem(
+            R.drawable.ic_composer_numbered_list,
+            R.string.rich_text_editor_numbered_list,
+            ComposerAction.ORDERED_LIST
+        ) {
+            views.richTextComposerEditText.toggleList(ordered = true)
+        }
+        addRichTextMenuItem(
+            R.drawable.ic_composer_indent,
+            R.string.rich_text_editor_indent,
+            ComposerAction.INDENT
+        ) {
+            views.richTextComposerEditText.indent()
+        }
+        addRichTextMenuItem(
+            R.drawable.ic_composer_unindent,
+            R.string.rich_text_editor_unindent,
+            ComposerAction.UNINDENT
+        ) {
+            views.richTextComposerEditText.unindent()
+        }
+        addRichTextMenuItem(
+            R.drawable.ic_composer_quote,
+            R.string.rich_text_editor_quote,
+            ComposerAction.QUOTE
+        ) {
+            views.richTextComposerEditText.toggleQuote()
+        }
+        addRichTextMenuItem(
+            R.drawable.ic_composer_inline_code,
+            R.string.rich_text_editor_inline_code,
+            ComposerAction.INLINE_CODE
+        ) {
+            views.richTextComposerEditText.toggleInlineFormat(InlineFormat.InlineCode)
+        }
+        addRichTextMenuItem(
+            R.drawable.ic_composer_code_block,
+            R.string.rich_text_editor_code_block,
+            ComposerAction.CODE_BLOCK
+        ) {
+            views.richTextComposerEditText.toggleCodeBlock()
+        }
+        addRichTextMenuItem(
+            R.drawable.ic_composer_link,
+            R.string.rich_text_editor_link,
+            ComposerAction.LINK
+        ) {
+            views.richTextComposerEditText.getLinkAction()?.let {
+                when (it) {
+                    LinkAction.InsertLink -> callback?.onSetLink(
+                        isTextSupported = true,
+                        initialLink = null
+                    )
+
+                    is LinkAction.SetLink -> callback?.onSetLink(
+                        isTextSupported = false,
+                        initialLink = it.currentUrl
+                    )
+                }
+            }
+        }
+    }
+
+    fun setLink(link: String?) =
+        views.richTextComposerEditText.setLink(link)
+
+    fun insertLink(link: String, text: String) =
+        views.richTextComposerEditText.insertLink(link, text)
+
+    fun removeLink() =
+        views.richTextComposerEditText.removeLink()
+
+    // Update the API to insertMention when available
+    fun insertMention(url: String, displayText: String) =
+        views.richTextComposerEditText.insertLink(url, displayText)
+
+    @SuppressLint("ClickableViewAccessibility")
+    private fun disallowParentInterceptTouchEvent(view: View) {
+        view.setOnTouchListener { v, event ->
+            if (v.hasFocus()) {
+                v.parent?.requestDisallowInterceptTouchEvent(true)
+                val action = event.actionMasked
+                if (action == MotionEvent.ACTION_SCROLL) {
+                    v.parent?.requestDisallowInterceptTouchEvent(false)
+                    return@setOnTouchListener true
+                }
+            }
+            false
+        }
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+
+        views.richTextComposerEditText.actionStatesChangedListener =
+            EditorEditText.OnActionStatesChangedListener { state ->
+                for (action in state.keys) {
+                    updateMenuStateFor(action, state)
+                }
+            }
+        updateEditTextVisibility()
+    }
+
+    fun setOnErrorListener(onError: (e: RichTextEditorException) -> Unit) {
+        views.richTextComposerEditText.rustErrorCollector = RustErrorCollector {
+            onError(RichTextEditorException(it))
+        }
+    }
+
+    private fun updateEditTextVisibility() {
+        views.richTextComposerEditText.isVisible = isTextFormattingEnabled
+        views.richTextMenuScrollView.isVisible = isTextFormattingEnabled
+        views.plainTextComposerEditText.isVisible = !isTextFormattingEnabled
+
+        // The layouts for formatted text mode and plain text mode are different, so we need to update the constraints
+        val dpToPx = { dp: Int -> dimensionConverter.dpToPx(dp) }
+        ConstraintSet().apply {
+            clone(views.composerLayoutContent)
+            clear(R.id.composerEditTextOuterBorder, ConstraintSet.TOP)
+            clear(R.id.composerEditTextOuterBorder, ConstraintSet.BOTTOM)
+            clear(R.id.composerEditTextOuterBorder, ConstraintSet.START)
+            clear(R.id.composerEditTextOuterBorder, ConstraintSet.END)
+            if (isTextFormattingEnabled) {
+                connect(
+                    R.id.composerEditTextOuterBorder,
+                    ConstraintSet.TOP,
+                    R.id.composerLayoutContent,
+                    ConstraintSet.TOP,
+                    dpToPx(8)
+                )
+                connect(
+                    R.id.composerEditTextOuterBorder,
+                    ConstraintSet.BOTTOM,
+                    R.id.sendButton,
+                    ConstraintSet.TOP,
+                    0
+                )
+                connect(
+                    R.id.composerEditTextOuterBorder,
+                    ConstraintSet.START,
+                    R.id.composerLayoutContent,
+                    ConstraintSet.START,
+                    dpToPx(12)
+                )
+                connect(
+                    R.id.composerEditTextOuterBorder,
+                    ConstraintSet.END,
+                    R.id.composerLayoutContent,
+                    ConstraintSet.END,
+                    dpToPx(12)
+                )
+            } else {
+                connect(
+                    R.id.composerEditTextOuterBorder,
+                    ConstraintSet.TOP,
+                    R.id.composerLayoutContent,
+                    ConstraintSet.TOP,
+                    dpToPx(8)
+                )
+                connect(
+                    R.id.composerEditTextOuterBorder,
+                    ConstraintSet.BOTTOM,
+                    R.id.composerLayoutContent,
+                    ConstraintSet.BOTTOM,
+                    dpToPx(8)
+                )
+                connect(
+                    R.id.composerEditTextOuterBorder,
+                    ConstraintSet.START,
+                    R.id.attachmentButton,
+                    ConstraintSet.END,
+                    0
+                )
+                connect(
+                    R.id.composerEditTextOuterBorder,
+                    ConstraintSet.END,
+                    R.id.sendButton,
+                    ConstraintSet.START,
+                    0
+                )
+            }
+            applyTo(views.composerLayoutContent)
+        }
+    }
+
+    private fun updateFullScreenButtonVisibility() {
+        val isLargeScreenDevice =
+            resources.configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
+        val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+        // There's no point in having full screen in landscape since there's almost no vertical space
+        views.composerFullScreenButton.isInvisible =
+            !isTextFormattingEnabled || (isLandscape && !isLargeScreenDevice)
+    }
+
+    /**
+     * Updates the non-active input with the contents of the active input.
+     */
+    private fun syncEditTexts() =
+        if (isTextFormattingEnabled) {
+            views.plainTextComposerEditText.setText(views.richTextComposerEditText.getMarkdown())
+        } else {
+            views.richTextComposerEditText.setMarkdown(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)
+        button.root.tag = action
+        with(button.root) {
+            contentDescription = resources.getString(description)
+            setImageResource(iconId)
+            setOnClickListener {
+                onClick()
+            }
+        }
+    }
+
+    private fun updateMenuStateFor(
+        action: ComposerAction,
+        menuState: Map<ComposerAction, ActionState>
+    ) {
+        val button = findViewWithTag<ImageButton>(action) ?: return
+        val stateForAction = menuState[action]
+        button.isEnabled = stateForAction != ActionState.DISABLED
+        button.isSelected = stateForAction == ActionState.REVERSED
+
+        if (action == ComposerAction.INDENT || action == ComposerAction.UNINDENT) {
+            val indentationButtonIsVisible =
+                menuState[ComposerAction.ORDERED_LIST] == ActionState.REVERSED ||
+                        menuState[ComposerAction.UNORDERED_LIST] == ActionState.REVERSED
+            button.isVisible = indentationButtonIsVisible
+        }
+    }
+
+    fun estimateCollapsedHeight(): Int {
+        val editText = this.editText
+        val originalLines = editText.maxLines
+        val originalParamsHeight = editText.layoutParams.height
+        editText.maxLines = MessageComposerView.MAX_LINES_WHEN_COLLAPSED
+        editText.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
+        measure(
+            MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+            MeasureSpec.UNSPECIFIED,
+        )
+        val result = measuredHeight
+        editText.layoutParams.height = originalParamsHeight
+        editText.maxLines = originalLines
+        return result
+    }
+
+    private fun updateTextFieldBorder(isFullScreen: Boolean) {
+        val isMultiline =
+            editText.editableText.lines().count() > 1 || isFullScreen || hasRelatedMessage
+        val cornerSize = if (isMultiline) {
+            resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_expanded)
+        } else {
+            resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_single_line)
+        }.toFloat()
+        borderShapeDrawable.setCornerSize(cornerSize)
+    }
+
+    private fun replaceFormattedContent(text: CharSequence) {
+        views.richTextComposerEditText.setHtml(text.toString())
+        updateTextFieldBorder(isFullScreen)
+    }
+
+    override fun setTextIfDifferent(text: CharSequence?): Boolean {
+        val result = editText.setTextIfDifferent(text)
+        updateTextFieldBorder(isFullScreen)
+        return result
+    }
+
+    private fun updateEditTextFullScreenState(editText: EditText, isFullScreen: Boolean) {
+        if (isFullScreen) {
+            editText.maxLines = Int.MAX_VALUE
+        } else {
+            editText.maxLines = MessageComposerView.MAX_LINES_WHEN_COLLAPSED
+        }
+    }
+
+    override fun renderComposerMode(mode: MessageComposerMode) {
+        if (mode is MessageComposerMode.Special) {
+            views.composerModeGroup.isVisible = true
+            if (isTextFormattingEnabled) {
+                replaceFormattedContent(mode.defaultContent)
+            } else {
+                views.plainTextComposerEditText.setText(mode.defaultContent)
+            }
+            hasRelatedMessage = true
+            editText.showKeyboard(andRequestFocus = true)
+        } else {
+            views.composerModeGroup.isGone = true
+            (mode as? MessageComposerMode.Normal)?.content?.let { text ->
+                if (isTextFormattingEnabled) {
+                    replaceFormattedContent(text)
+                } else {
+                    views.plainTextComposerEditText.setText(text)
+                }
+            }
+            hasRelatedMessage = false
+        }
+
+        updateTextFieldBorder(isFullScreen)
+
+        if (this.composerMode == mode) return
+        this.composerMode = mode
+
+        views.sendButton.apply {
+            if (mode is MessageComposerMode.Edit) {
+                contentDescription = resources.getString(R.string.action_save)
+                setImageResource(R.drawable.ic_composer_rich_text_save)
+            } else {
+                contentDescription = resources.getString(R.string.action_send)
+                setImageResource(R.drawable.ic_rich_composer_send)
+            }
+        }
+
+        when (mode) {
+            is MessageComposerMode.Edit -> {
+                views.composerModeTitleView.setText(R.string.editing)
+                views.composerModeIconView.setImageResource(R.drawable.ic_composer_rich_text_editor_edit)
+            }
+
+            is MessageComposerMode.Quote -> {
+                views.composerModeTitleView.setText(R.string.quoting)
+                views.composerModeIconView.setImageResource(R.drawable.ic_quote)
+            }
+
+            is MessageComposerMode.Reply -> {
+                val senderInfo = mode.event.senderInfo
+                val userName = senderInfo.displayName ?: senderInfo.disambiguatedDisplayName
+                views.composerModeTitleView.text =
+                    resources.getString(R.string.replying_to, userName)
+                views.composerModeIconView.setImageResource(R.drawable.ic_reply)
+            }
+
+            else -> Unit
+        }
+    }
+
+    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/app/src/main/java/org/futo/circles/view/RichTextEditorException.kt b/app/src/main/java/org/futo/circles/view/RichTextEditorException.kt
new file mode 100644
index 000000000..fdc51ac1c
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/view/RichTextEditorException.kt
@@ -0,0 +1,5 @@
+package org.futo.circles.view
+
+internal class RichTextEditorException(
+    cause: Throwable,
+) : Exception(cause)
\ No newline at end of file
diff --git a/app/src/main/java/org/futo/circles/view/SimpleTextWatcher.kt b/app/src/main/java/org/futo/circles/view/SimpleTextWatcher.kt
new file mode 100644
index 000000000..d471738bd
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/view/SimpleTextWatcher.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 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 org.futo.circles.view
+
+import android.text.Editable
+import android.text.TextWatcher
+
+/**
+ * TextWatcher with default no op implementation.
+ */
+open class SimpleTextWatcher : TextWatcher {
+    override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+        // No op
+    }
+
+    override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+        // No op
+    }
+
+    override fun afterTextChanged(s: Editable) {
+        // No op
+    }
+}
diff --git a/app/src/main/java/org/futo/circles/view/UriContentListener.kt b/app/src/main/java/org/futo/circles/view/UriContentListener.kt
new file mode 100644
index 000000000..3cc04857f
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/view/UriContentListener.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2023 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 org.futo.circles.view
+
+import android.content.ClipData
+import android.net.Uri
+import android.view.View
+import androidx.core.view.ContentInfoCompat
+import androidx.core.view.OnReceiveContentListener
+
+class UriContentListener(
+        private val onContent: (uri: Uri) -> Unit
+) : OnReceiveContentListener {
+    override fun onReceiveContent(view: View, payload: ContentInfoCompat): ContentInfoCompat? {
+        val split = payload.partition { item -> item.uri != null }
+        val uriContent = split.first
+        val remaining = split.second
+
+        if (uriContent != null) {
+            val clip: ClipData = uriContent.clip
+            for (i in 0 until clip.itemCount) {
+                val uri = clip.getItemAt(i).uri
+                // ... app-specific logic to handle the URI ...
+                onContent(uri)
+            }
+        }
+        // Return anything that we didn't handle ourselves. This preserves the default platform
+        // behavior for text and anything else for which we are not implementing custom handling.
+        return remaining
+    }
+}
diff --git a/app/src/main/res/color/selector_rich_text_menu_icon.xml b/app/src/main/res/color/selector_rich_text_menu_icon.xml
new file mode 100644
index 000000000..cc4a66618
--- /dev/null
+++ b/app/src/main/res/color/selector_rich_text_menu_icon.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false" android:color="@color/red" />
+    <item android:state_pressed="true" android:color="?attr/colorSecondary" />
+    <item android:state_hovered="true" android:color="?attr/colorSecondary" />
+    <item android:state_selected="true" android:color="?attr/colorSecondary" />
+    <item android:color="@color/blue" />
+</selector>
diff --git a/app/src/main/res/drawable/bg_code_block.xml b/app/src/main/res/drawable/bg_code_block.xml
new file mode 100644
index 000000000..e171c7d90
--- /dev/null
+++ b/app/src/main/res/drawable/bg_code_block.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~ Modifications Copyright 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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@color/blue"/>
+    <stroke android:width="@dimen/code_block_border_width" android:color="@color/blue"/>
+    <corners android:radius="@dimen/code_block_border_radius"/>
+</shape>
diff --git a/app/src/main/res/drawable/bg_inline_code_multi_line_left.xml b/app/src/main/res/drawable/bg_inline_code_multi_line_left.xml
new file mode 100644
index 000000000..0c942b981
--- /dev/null
+++ b/app/src/main/res/drawable/bg_inline_code_multi_line_left.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~ Modifications Copyright 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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@color/blue"/>
+    <stroke android:width="@dimen/inline_code_border_width" android:color="@color/blue"/>
+    <corners android:topLeftRadius="@dimen/inline_code_border_radius"
+        android:bottomLeftRadius="@dimen/inline_code_border_radius"/>
+</shape>
diff --git a/app/src/main/res/drawable/bg_inline_code_multi_line_mid.xml b/app/src/main/res/drawable/bg_inline_code_multi_line_mid.xml
new file mode 100644
index 000000000..0d906c99c
--- /dev/null
+++ b/app/src/main/res/drawable/bg_inline_code_multi_line_mid.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~ Modifications Copyright 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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@color/blue"/>
+    <stroke android:width="@dimen/inline_code_border_width" android:color="@color/blue"/>
+</shape>
diff --git a/app/src/main/res/drawable/bg_inline_code_multi_line_right.xml b/app/src/main/res/drawable/bg_inline_code_multi_line_right.xml
new file mode 100644
index 000000000..495dd1dc4
--- /dev/null
+++ b/app/src/main/res/drawable/bg_inline_code_multi_line_right.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~ Modifications Copyright 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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@color/blue"/>
+    <stroke android:width="@dimen/inline_code_border_width" android:color="@color/blue"/>
+    <corners android:topRightRadius="@dimen/inline_code_border_radius"
+        android:bottomRightRadius="@dimen/inline_code_border_radius"/>
+</shape>
diff --git a/app/src/main/res/drawable/bg_inline_code_single_line.xml b/app/src/main/res/drawable/bg_inline_code_single_line.xml
new file mode 100644
index 000000000..f2f3c587e
--- /dev/null
+++ b/app/src/main/res/drawable/bg_inline_code_single_line.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~ Modifications Copyright 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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@color/blue"/>
+    <stroke android:width="@dimen/inline_code_border_width" android:color="@color/blue"/>
+    <corners android:radius="@dimen/inline_code_border_radius"/>
+</shape>
diff --git a/app/src/main/res/drawable/bg_rich_text_menu_button.xml b/app/src/main/res/drawable/bg_rich_text_menu_button.xml
new file mode 100644
index 000000000..7b436920f
--- /dev/null
+++ b/app/src/main/res/drawable/bg_rich_text_menu_button.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_hovered="true">
+        <shape android:shape="rectangle">
+            <corners android:radius="8dp" />
+            <solid android:color="@color/blue" />
+        </shape>
+    </item>
+    <item android:state_selected="true">
+        <shape android:shape="rectangle">
+            <corners android:radius="8dp" />
+            <solid android:color="@color/blue" />
+        </shape>
+    </item>
+    <item>
+        <ripple android:color="@color/blue" />
+    </item>
+</selector>
diff --git a/app/src/main/res/drawable/ic_composer_bold.xml b/app/src/main/res/drawable/ic_composer_bold.xml
new file mode 100644
index 000000000..d2e26cf1e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_bold.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="44dp"
+    android:height="44dp"
+    android:viewportWidth="44"
+    android:viewportHeight="44">
+    <path
+        android:fillColor="@color/blue"
+        android:fillType="evenOdd"
+        android:pathData="M16,14.5C16,13.672 16.672,13 17.5,13H22.288C25.139,13 27.25,15.466 27.25,18.25C27.25,19.38 26.902,20.458 26.298,21.34C27.765,22.268 28.75,23.882 28.75,25.75C28.75,28.689 26.311,31 23.393,31H17.5C16.672,31 16,30.328 16,29.5V14.5ZM19,16V20.5H22.288C23.261,20.5 24.25,19.608 24.25,18.25C24.25,16.892 23.261,16 22.288,16H19ZM19,23.5V28H23.393C24.735,28 25.75,26.953 25.75,25.75C25.75,24.547 24.735,23.5 23.393,23.5H19Z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_bullet_list.xml b/app/src/main/res/drawable/ic_composer_bullet_list.xml
new file mode 100644
index 000000000..d372209ce
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_bullet_list.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="44dp"
+    android:height="44dp"
+    android:viewportWidth="44"
+    android:viewportHeight="44">
+    <group>
+        <clip-path android:pathData="M10,10h24v24h-24z" />
+        <path
+            android:fillColor="@color/blue"
+            android:pathData="M14,20.5C13.17,20.5 12.5,21.17 12.5,22C12.5,22.83 13.17,23.5 14,23.5C14.83,23.5 15.5,22.83 15.5,22C15.5,21.17 14.83,20.5 14,20.5ZM14,14.5C13.17,14.5 12.5,15.17 12.5,16C12.5,16.83 13.17,17.5 14,17.5C14.83,17.5 15.5,16.83 15.5,16C15.5,15.17 14.83,14.5 14,14.5ZM14,26.5C13.17,26.5 12.5,27.18 12.5,28C12.5,28.82 13.18,29.5 14,29.5C14.82,29.5 15.5,28.82 15.5,28C15.5,27.18 14.83,26.5 14,26.5ZM18,29H30C30.55,29 31,28.55 31,28C31,27.45 30.55,27 30,27H18C17.45,27 17,27.45 17,28C17,28.55 17.45,29 18,29ZM18,23H30C30.55,23 31,22.55 31,22C31,21.45 30.55,21 30,21H18C17.45,21 17,21.45 17,22C17,22.55 17.45,23 18,23ZM17,16C17,16.55 17.45,17 18,17H30C30.55,17 31,16.55 31,16C31,15.45 30.55,15 30,15H18C17.45,15 17,15.45 17,16Z" />
+    </group>
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_code_block.xml b/app/src/main/res/drawable/ic_composer_code_block.xml
new file mode 100644
index 000000000..25155167b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_code_block.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="44dp"
+    android:height="44dp"
+    android:viewportWidth="44"
+    android:viewportHeight="44">
+    <path
+        android:fillColor="@color/blue"
+        android:pathData="m18,22c0.7,-0.72 1.4,-1.4 2.1,-2.1 0.68,-0.98 -0.93,-1.9 -1.5,-0.96 -0.87,0.88 -1.8,1.7 -2.6,2.6 -0.45,0.67 0.27,1.2 0.7,1.6 0.75,0.74 1.5,1.5 2.2,2.2 0.98,0.68 1.9,-0.93 0.96,-1.5l-1.9,-1.9zM26.6,22c-0.71,0.72 -1.5,1.4 -2.1,2.2 -0.68,0.98 0.93,1.9 1.5,0.96 0.88,-0.89 1.8,-1.8 2.6,-2.7 0.45,-0.67 -0.27,-1.2 -0.7,-1.6 -0.75,-0.74 -1.5,-1.5 -2.2,-2.2 -0.99,-0.66 -2,0.94 -0.96,1.5l1.9,1.9zM13.6,32c-1.1,0.021 -1.9,-1 -1.7,-2.1 0.005,-5.7 -0.011,-11 0.008,-17 0.088,-1 1.1,-1.7 2.1,-1.5 5.6,0.005 11,-0.011 17,0.008 1,0.088 1.7,1.1 1.5,2.1 -0.005,5.6 0.011,11 -0.008,17 -0.088,1 -1.1,1.7 -2.1,1.5h-17zM13.6,30.3h17v-17h-17v17z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_collapse.xml b/app/src/main/res/drawable/ic_composer_collapse.xml
new file mode 100644
index 000000000..d123caac5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_collapse.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20">
+    <path
+        android:fillColor="?vctr_content_quaternary"
+        android:pathData="M10.708,10Q10.438,10 10.219,9.781Q10,9.562 10,9.292V4.542Q10,4.354 10.146,4.219Q10.292,4.083 10.458,4.083Q10.646,4.083 10.781,4.219Q10.917,4.354 10.917,4.542V8.438L16.375,3Q16.5,2.854 16.688,2.854Q16.875,2.854 17,3Q17.146,3.125 17.146,3.312Q17.146,3.5 17,3.625L11.562,9.083H15.458Q15.646,9.083 15.781,9.229Q15.917,9.375 15.917,9.542Q15.917,9.729 15.781,9.865Q15.646,10 15.458,10ZM3,17Q2.854,16.875 2.854,16.688Q2.854,16.5 3,16.375L8.438,10.917H4.542Q4.354,10.917 4.219,10.771Q4.083,10.625 4.083,10.458Q4.083,10.271 4.219,10.135Q4.354,10 4.542,10H9.292Q9.562,10 9.781,10.219Q10,10.438 10,10.708V15.458Q10,15.646 9.854,15.781Q9.708,15.917 9.542,15.917Q9.354,15.917 9.219,15.781Q9.083,15.646 9.083,15.458V11.562L3.625,17Q3.5,17.146 3.312,17.146Q3.125,17.146 3,17Z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_full_screen.xml b/app/src/main/res/drawable/ic_composer_full_screen.xml
new file mode 100644
index 000000000..6c7d7d673
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_full_screen.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20">
+    <path
+        android:fillColor="?vctr_content_quaternary"
+        android:pathData="M3.625,17.083Q3.354,17.083 3.135,16.865Q2.917,16.646 2.917,16.375V11.625Q2.917,11.438 3.062,11.302Q3.208,11.167 3.375,11.167Q3.562,11.167 3.698,11.302Q3.833,11.438 3.833,11.625V15.5L15.5,3.833H11.625Q11.438,3.833 11.302,3.688Q11.167,3.542 11.167,3.375Q11.167,3.188 11.302,3.052Q11.438,2.917 11.625,2.917H16.375Q16.646,2.917 16.865,3.135Q17.083,3.354 17.083,3.625V8.375Q17.083,8.562 16.938,8.698Q16.792,8.833 16.625,8.833Q16.438,8.833 16.302,8.698Q16.167,8.562 16.167,8.375V4.5L4.5,16.167H8.375Q8.562,16.167 8.698,16.312Q8.833,16.458 8.833,16.625Q8.833,16.812 8.698,16.948Q8.562,17.083 8.375,17.083Z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_indent.xml b/app/src/main/res/drawable/ic_composer_indent.xml
new file mode 100644
index 000000000..775d434e3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_indent.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="44dp"
+    android:height="44dp"
+    android:viewportWidth="44"
+    android:viewportHeight="44">
+    <group>
+        <clip-path android:pathData="M10,10h24v24h-24z" />
+        <path
+            android:fillColor="@color/blue"
+            android:pathData="M14,31H30C30.55,31 31,30.55 31,30C31,29.45 30.55,29 30,29H14C13.45,29 13,29.45 13,30C13,30.55 13.45,31 14,31ZM13,19.21V24.8C13,25.25 13.54,25.47 13.85,25.15L16.64,22.36C16.84,22.16 16.84,21.85 16.64,21.65L13.85,18.85C13.54,18.54 13,18.76 13,19.21ZM22,27H30C30.55,27 31,26.55 31,26C31,25.45 30.55,25 30,25H22C21.45,25 21,25.45 21,26C21,26.55 21.45,27 22,27ZM13,14C13,14.55 13.45,15 14,15H30C30.55,15 31,14.55 31,14C31,13.45 30.55,13 30,13H14C13.45,13 13,13.45 13,14ZM22,19H30C30.55,19 31,18.55 31,18C31,17.45 30.55,17 30,17H22C21.45,17 21,17.45 21,18C21,18.55 21.45,19 22,19ZM22,23H30C30.55,23 31,22.55 31,22C31,21.45 30.55,21 30,21H22C21.45,21 21,21.45 21,22C21,22.55 21.45,23 22,23Z" />
+    </group>
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_inline_code.xml b/app/src/main/res/drawable/ic_composer_inline_code.xml
new file mode 100644
index 000000000..bc054e639
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_inline_code.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="44dp"
+    android:height="44dp"
+    android:viewportWidth="44"
+    android:viewportHeight="44">
+    <path
+        android:fillColor="@color/blue"
+        android:pathData="M24.958,15.621C25.117,15.092 24.816,14.534 24.287,14.375C23.758,14.217 23.201,14.517 23.042,15.046L19.042,28.379C18.883,28.908 19.184,29.466 19.713,29.624C20.242,29.783 20.799,29.483 20.958,28.954L24.958,15.621Z" />
+    <path
+        android:fillColor="@color/blue"
+        android:pathData="M15.974,17.232C15.549,16.878 14.919,16.936 14.565,17.36L11.232,21.36C10.923,21.731 10.923,22.269 11.232,22.64L14.565,26.64C14.919,27.065 15.549,27.122 15.974,26.768C16.398,26.415 16.455,25.784 16.102,25.36L13.302,22L16.102,18.64C16.455,18.216 16.398,17.585 15.974,17.232Z" />
+    <path
+        android:fillColor="@color/blue"
+        android:pathData="M28.027,17.232C28.451,16.878 29.081,16.936 29.435,17.36L32.768,21.36C33.077,21.731 33.077,22.269 32.768,22.64L29.435,26.64C29.081,27.065 28.451,27.122 28.027,26.768C27.602,26.415 27.545,25.784 27.898,25.36L30.698,22L27.898,18.64C27.545,18.216 27.602,17.585 28.027,17.232Z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_italic.xml b/app/src/main/res/drawable/ic_composer_italic.xml
new file mode 100644
index 000000000..15c40a206
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_italic.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="44dp"
+    android:height="44dp"
+    android:viewportWidth="44"
+    android:viewportHeight="44">
+    <path
+        android:pathData="M22.619,14.999L19.747,29.005H17.2C16.758,29.005 16.4,29.363 16.4,29.805C16.4,30.247 16.758,30.605 17.2,30.605H20.389C20.397,30.605 20.405,30.605 20.412,30.605H23.6C24.042,30.605 24.4,30.247 24.4,29.805C24.4,29.363 24.042,29.005 23.6,29.005H21.381L24.253,14.999H26.8C27.242,14.999 27.6,14.64 27.6,14.199C27.6,13.757 27.242,13.399 26.8,13.399H23.615C23.604,13.398 23.594,13.398 23.583,13.399H20.4C19.958,13.399 19.6,13.757 19.6,14.199C19.6,14.64 19.958,14.999 20.4,14.999H22.619Z"
+        android:fillColor="@color/blue"
+        android:fillType="evenOdd" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_link.xml b/app/src/main/res/drawable/ic_composer_link.xml
new file mode 100644
index 000000000..be76286af
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_link.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="44dp"
+    android:height="44dp"
+    android:viewportWidth="44"
+    android:viewportHeight="44">
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M22.566,16.151L23.101,15.616C24.577,14.14 26.956,14.126 28.415,15.585C29.874,17.044 29.86,19.423 28.383,20.899L25.844,23.438C24.368,24.915 21.989,24.929 20.53,23.47M21.434,27.849L20.899,28.383C19.423,29.86 17.044,29.874 15.585,28.415C14.126,26.956 14.14,24.577 15.616,23.101L18.156,20.562C19.632,19.086 22.011,19.071 23.47,20.53"
+        android:strokeWidth="1.5"
+        android:strokeColor="@color/blue"
+        android:strokeLineCap="round" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_numbered_list.xml b/app/src/main/res/drawable/ic_composer_numbered_list.xml
new file mode 100644
index 000000000..5690e2de9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_numbered_list.xml
@@ -0,0 +1,24 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="44dp"
+    android:height="44dp"
+    android:viewportWidth="44"
+    android:viewportHeight="44">
+    <path
+        android:pathData="m14.5,20h-2c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.28 0.22,0.5 0.5,0.5h1.3l-1.68,1.96C12.04,23.05 12,23.17 12,23.28v0.22c0,0.28 0.22,0.5 0.5,0.5h2C14.78,24 15,23.78 15,23.5 15,23.22 14.78,23 14.5,23h-1.3l1.68,-1.96C14.96,20.95 15,20.83 15,20.72V20.5C15,20.22 14.78,20 14.5,20Z"
+        android:fillColor="@color/blue" />
+    <path
+        android:pathData="M12.5,15H13v2.5c0,0.28 0.22,0.5 0.5,0.5 0.28,0 0.5,-0.22 0.5,-0.5v-3C14,14.22 13.78,14 13.5,14h-1c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.28 0.22,0.5 0.5,0.5z"
+        android:fillColor="@color/blue" />
+    <path
+        android:pathData="m14.5,26h-2c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.28 0.22,0.5 0.5,0.5H14v0.5h-0.5c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.28 0.22,0.5 0.5,0.5H14V29h-1.5c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.28 0.22,0.5 0.5,0.5h2c0.28,0 0.5,-0.22 0.5,-0.5v-3C15,26.22 14.78,26 14.5,26Z"
+        android:fillColor="@color/blue" />
+    <path
+        android:pathData="M30,21H18c-0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1z"
+        android:fillColor="@color/blue" />
+    <path
+        android:pathData="M30,27H18c-0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1z"
+        android:fillColor="@color/blue" />
+    <path
+        android:pathData="m18,17h12c0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1H18c-0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1z"
+        android:fillColor="@color/blue" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_quote.xml b/app/src/main/res/drawable/ic_composer_quote.xml
new file mode 100644
index 000000000..78603bf36
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_quote.xml
@@ -0,0 +1,18 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="36dp"
+    android:height="44dp"
+    android:viewportWidth="36"
+    android:viewportHeight="44">
+    <path
+        android:pathData="M14.719,14.34C14.813,13.698 14.353,13.104 13.691,13.012C13.028,12.92 12.415,13.366 12.32,14.008L11.512,19.486C11.418,20.128 11.878,20.723 12.54,20.814C13.203,20.906 13.816,20.46 13.911,19.818L14.719,14.34Z"
+        android:fillColor="@color/blue" />
+    <path
+        android:pathData="M26.834,24.514C26.928,23.872 26.468,23.277 25.806,23.186C25.143,23.094 24.53,23.54 24.435,24.182L23.628,29.66C23.533,30.302 23.993,30.896 24.656,30.988C25.318,31.08 25.932,30.634 26.026,29.992L26.834,24.514Z"
+        android:fillColor="@color/blue" />
+    <path
+        android:pathData="M19.318,13.009C19.983,13.086 20.456,13.671 20.376,14.315L20.354,14.49C20.34,14.602 20.319,14.763 20.293,14.961C20.242,15.358 20.17,15.902 20.088,16.496C19.927,17.667 19.72,19.075 19.553,19.882C19.422,20.518 18.784,20.931 18.128,20.803C17.472,20.676 17.046,20.058 17.177,19.422C17.326,18.701 17.523,17.37 17.687,16.185C17.767,15.599 17.838,15.061 17.889,14.669C17.915,14.473 17.935,14.314 17.949,14.204L17.97,14.034C18.051,13.39 18.654,12.931 19.318,13.009Z"
+        android:fillColor="@color/blue" />
+    <path
+        android:pathData="M32.488,24.514C32.582,23.872 32.122,23.277 31.46,23.186C30.797,23.094 30.184,23.54 30.089,24.182L29.281,29.66C29.187,30.302 29.647,30.896 30.309,30.988C30.972,31.08 31.585,30.634 31.68,29.992L32.488,24.514Z"
+        android:fillColor="@color/blue" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_rich_mic_pressed.xml b/app/src/main/res/drawable/ic_composer_rich_mic_pressed.xml
new file mode 100644
index 000000000..e9dbe610e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_rich_mic_pressed.xml
@@ -0,0 +1,17 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="52dp"
+    android:height="52dp"
+    android:viewportWidth="52"
+    android:viewportHeight="52">
+  <path
+      android:pathData="M26.173,26.169m-22.763,0a22.763,22.763 0,1 1,45.526 0a22.763,22.763 0,1 1,-45.526 0"
+      android:fillColor="#0DBD8B"/>
+  <path
+      android:pathData="M26,26m-26,0a26,26 0,1 1,52 0a26,26 0,1 1,-52 0"
+      android:strokeAlpha="0.2"
+      android:fillColor="#0DBD8B"
+      android:fillAlpha="0.2"/>
+  <path
+      android:pathData="M26,29.5C27.937,29.5 29.488,27.937 29.488,26L29.5,19C29.5,17.063 27.937,15.5 26,15.5C24.063,15.5 22.5,17.063 22.5,19V26C22.5,27.937 24.063,29.5 26,29.5ZM33.093,26C32.603,26 32.195,26.35 32.125,26.828C31.693,29.873 28.952,31.95 26,31.95C23.048,31.95 20.307,29.885 19.875,26.828C19.805,26.35 19.385,26 18.907,26C18.3,26 17.833,26.537 17.915,27.132C18.452,30.597 21.368,33.315 24.833,33.84V36.5C24.833,37.142 25.358,37.667 26,37.667C26.642,37.667 27.167,37.142 27.167,36.5V33.84C30.62,33.338 33.548,30.597 34.085,27.132C34.167,26.537 33.7,26 33.093,26Z"
+      android:fillColor="#ffffff"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_rich_text_editor_close.xml b/app/src/main/res/drawable/ic_composer_rich_text_editor_close.xml
new file mode 100644
index 000000000..cb80a6587
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_rich_text_editor_close.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="12dp"
+    android:height="12dp"
+    android:viewportWidth="12"
+    android:viewportHeight="12">
+    <path
+        android:pathData="M10.403,2.53C10.696,2.237 10.696,1.763 10.403,1.47C10.111,1.177 9.636,1.177 9.343,1.47L5.946,4.867L2.549,1.47C2.256,1.177 1.781,1.177 1.488,1.47C1.195,1.763 1.195,2.237 1.488,2.53L4.885,5.927L1.343,9.47C1.05,9.763 1.05,10.237 1.343,10.53C1.636,10.823 2.11,10.823 2.403,10.53L5.946,6.988L9.488,10.53C9.781,10.823 10.256,10.823 10.549,10.53C10.842,10.237 10.842,9.763 10.549,9.47L7.006,5.927L10.403,2.53Z"
+        android:fillColor="@color/blue" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml b/app/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml
new file mode 100644
index 000000000..1c3ddde54
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="12dp"
+    android:height="12dp"
+    android:viewportWidth="12"
+    android:viewportHeight="12">
+    <path
+        android:pathData="M2.649,7.355C2.655,7.316 2.672,7.28 2.699,7.251L8.404,1.064C8.479,0.983 8.605,0.978 8.686,1.053L9.863,2.138C9.944,2.213 9.949,2.339 9.874,2.42L4.169,8.607C4.143,8.636 4.108,8.656 4.069,8.665L2.668,9.005C2.529,9.039 2.401,8.92 2.423,8.779L2.649,7.355Z"
+        android:fillColor="@color/blue" />
+    <path
+        android:pathData="M1.75,9.443C1.336,9.443 1,9.779 1,10.193C1,10.608 1.336,10.943 1.75,10.943L10.75,10.943C11.164,10.943 11.5,10.608 11.5,10.193C11.5,9.779 11.164,9.443 10.75,9.443L1.75,9.443Z"
+        android:fillColor="@color/blue" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_rich_text_save.xml b/app/src/main/res/drawable/ic_composer_rich_text_save.xml
new file mode 100644
index 000000000..63eb8d2c4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_rich_text_save.xml
@@ -0,0 +1,16 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="36dp"
+    android:height="36dp"
+    android:viewportWidth="36"
+    android:viewportHeight="36">
+  <path
+      android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
+      android:fillColor="?colorPrimary"/>
+  <path
+      android:pathData="M9.818,18.787L14.705,23.818L26.182,12"
+      android:strokeLineJoin="round"
+      android:strokeWidth="2.5"
+      android:fillColor="#00000000"
+      android:strokeColor="?colorOnPrimary"
+      android:strokeLineCap="round"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_strikethrough.xml b/app/src/main/res/drawable/ic_composer_strikethrough.xml
new file mode 100644
index 000000000..593cd3aca
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_strikethrough.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="44dp"
+    android:height="44dp"
+    android:viewportWidth="44"
+    android:viewportHeight="44">
+    <path
+        android:pathData="M24.897,17.154C24.235,15.821 22.876,15.21 21.374,15.372C19.05,15.622 18.44,17.423 18.722,18.592C19.032,19.872 20.046,20.37 21.839,20.826H29.92C30.517,20.826 31,21.351 31,22C31,22.648 30.517,23.174 29.92,23.174H14.08C13.483,23.174 13,22.648 13,22C13,21.351 13.483,20.826 14.08,20.826H17.355C17.041,20.377 16.791,19.839 16.633,19.189C16.003,16.581 17.554,13.424 21.16,13.036C23.285,12.807 25.615,13.661 26.798,16.038C27.081,16.608 26.886,17.32 26.361,17.629C25.836,17.937 25.181,17.725 24.897,17.154Z"
+        android:fillColor="@color/blue" />
+    <path
+        android:pathData="M25.427,25.13H27.67C27.888,26.306 27.721,27.56 27.05,28.632C26.114,30.125 24.37,31 21.985,31C18.076,31 16.279,28.584 15.912,26.986C15.768,26.357 16.12,25.72 16.698,25.563C17.277,25.406 17.863,25.788 18.008,26.417C18.119,26.902 19.002,28.652 21.985,28.652C23.907,28.652 24.854,27.965 25.264,27.31C25.642,26.707 25.708,25.909 25.427,25.13Z"
+        android:fillColor="@color/blue" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_underlined.xml b/app/src/main/res/drawable/ic_composer_underlined.xml
new file mode 100644
index 000000000..2e3d997d7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_underlined.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="44dp"
+    android:height="44dp"
+    android:viewportWidth="44"
+    android:viewportHeight="44">
+    <group>
+        <clip-path android:pathData="M10,10h24v24h-24z" />
+        <path
+            android:pathData="M22.79,26.95C25.82,26.56 28,23.84 28,20.79V14.25C28,13.56 27.44,13 26.75,13C26.06,13 25.5,13.56 25.5,14.25V20.9C25.5,22.57 24.37,24.09 22.73,24.42C20.48,24.89 18.5,23.17 18.5,21V14.25C18.5,13.56 17.94,13 17.25,13C16.56,13 16,13.56 16,14.25V21C16,24.57 19.13,27.42 22.79,26.95ZM15,30C15,30.55 15.45,31 16,31H28C28.55,31 29,30.55 29,30C29,29.45 28.55,29 28,29H16C15.45,29 15,29.45 15,30Z"
+            android:fillColor="@color/blue" />
+    </group>
+</vector>
diff --git a/app/src/main/res/drawable/ic_composer_unindent.xml b/app/src/main/res/drawable/ic_composer_unindent.xml
new file mode 100644
index 000000000..f7f383c49
--- /dev/null
+++ b/app/src/main/res/drawable/ic_composer_unindent.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="44dp"
+    android:height="44dp"
+    android:viewportWidth="44"
+    android:viewportHeight="44">
+    <group>
+        <clip-path android:pathData="M10,10h24v24h-24z" />
+        <path
+            android:pathData="M22,27H30C30.55,27 31,26.55 31,26C31,25.45 30.55,25 30,25H22C21.45,25 21,25.45 21,26C21,26.55 21.45,27 22,27ZM13.35,22.35L16.14,25.14C16.46,25.46 17,25.24 17,24.79V19.21C17,18.76 16.46,18.54 16.15,18.86L13.36,21.65C13.16,21.84 13.16,22.16 13.35,22.35ZM14,31H30C30.55,31 31,30.55 31,30C31,29.45 30.55,29 30,29H14C13.45,29 13,29.45 13,30C13,30.55 13.45,31 14,31ZM13,14C13,14.55 13.45,15 14,15H30C30.55,15 31,14.55 31,14C31,13.45 30.55,13 30,13H14C13.45,13 13,13.45 13,14ZM22,19H30C30.55,19 31,18.55 31,18C31,17.45 30.55,17 30,17H22C21.45,17 21,17.45 21,18C21,18.55 21.45,19 22,19ZM22,23H30C30.55,23 31,22.55 31,22C31,21.45 30.55,21 30,21H22C21.45,21 21,21.45 21,22C21,22.55 21.45,23 22,23Z"
+            android:fillColor="@color/blue" />
+    </group>
+</vector>
diff --git a/app/src/main/res/drawable/ic_quote.xml b/app/src/main/res/drawable/ic_quote.xml
new file mode 100644
index 000000000..0689651f1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_quote.xml
@@ -0,0 +1,14 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="14dp"
+    android:viewportWidth="20"
+    android:viewportHeight="14">
+  <path
+      android:pathData="M19,5H1M19,1H1M10,9H1M10,13H1"
+      android:strokeLineJoin="round"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:fillType="evenOdd"
+      android:strokeColor="#9E9E9E"
+      android:strokeLineCap="round"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_rich_composer_add.xml b/app/src/main/res/drawable/ic_rich_composer_add.xml
new file mode 100644
index 000000000..a5961c498
--- /dev/null
+++ b/app/src/main/res/drawable/ic_rich_composer_add.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="36dp"
+    android:height="36dp"
+    android:viewportWidth="36"
+    android:viewportHeight="36">
+  <path
+      android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
+      android:fillColor="@color/blue"/>
+  <path
+      android:pathData="M11.251,18H24.751M18.001,11.25V24.75"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:strokeColor="@color/blue"
+      android:strokeLineCap="round"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_rich_composer_send.xml b/app/src/main/res/drawable/ic_rich_composer_send.xml
new file mode 100644
index 000000000..b3df6e92d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_rich_composer_send.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="36dp"
+    android:height="36dp"
+    android:viewportWidth="36"
+    android:viewportHeight="36">
+  <path
+      android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
+      android:fillColor="?colorPrimary"/>
+  <path
+      android:pathData="M27.83,19.085L12.26,26.867C11.21,27.391 10.119,26.266 10.632,25.24C10.632,25.24 12.561,21.343 13.092,20.322C13.623,19.301 14.231,19.124 19.874,18.395C20.083,18.368 20.253,18.21 20.253,18C20.253,17.79 20.083,17.632 19.874,17.605C14.231,16.876 13.623,16.699 13.092,15.678C12.561,14.658 10.632,10.76 10.632,10.76C10.119,9.734 11.21,8.609 12.26,9.133L27.83,16.915C28.725,17.362 28.725,18.638 27.83,19.085Z"
+      android:fillColor="?colorOnPrimary"/>
+</vector>
diff --git a/app/src/main/res/layout/composer_rich_text_layout.xml b/app/src/main/res/layout/composer_rich_text_layout.xml
new file mode 100644
index 000000000..a8f0b4baa
--- /dev/null
+++ b/app/src/main/res/layout/composer_rich_text_layout.xml
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout 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:id="@+id/composerLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <FrameLayout
+        android:id="@+id/bottomSheetHandle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent">
+
+        <View
+            android:layout_width="36dp"
+            android:layout_height="5dp"
+            android:layout_gravity="center_horizontal"
+            android:layout_marginTop="8dp"
+            />
+
+    </FrameLayout>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/composerLayoutContent"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <ImageButton
+            android:id="@+id/attachmentButton"
+            android:layout_width="60dp"
+            android:layout_height="56dp"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
+            android:contentDescription="Send Files"
+            android:src="@drawable/ic_rich_composer_add"
+            app:layout_constraintVertical_bias="1"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_goneMarginBottom="56dp"
+            tools:ignore="MissingPrefix,RtlSymmetry" />
+
+        <!-- Constraints are updated programmatically -->
+        <FrameLayout
+            android:id="@+id/composerEditTextOuterBorder"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:minHeight="40dp"
+            android:layout_marginTop="8dp"
+            android:layout_marginBottom="8dp"
+            android:layout_marginHorizontal="12dp"
+            app:layout_constraintVertical_bias="0"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@id/sendButton"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent" />
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/composerModeIconView"
+            android:layout_width="11dp"
+            android:layout_height="11dp"
+            tools:src="@drawable/ic_quote"
+            android:layout_marginStart="12dp"
+            app:layout_constraintTop_toTopOf="@id/composerModeTitleView"
+            app:layout_constraintBottom_toBottomOf="@id/composerModeTitleView"
+            app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
+            app:tint="@color/blue" />
+
+        <TextView android:id="@+id/composerModeTitleView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="6dp"
+            android:layout_marginTop="8dp"
+            android:paddingBottom="2dp"
+            android:fontFamily="sans-serif-medium"
+            tools:text="Editing"
+            app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
+            app:layout_constraintStart_toEndOf="@id/composerModeIconView" />
+
+        <ImageButton android:id="@+id/composerModeCloseView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/ic_composer_rich_text_editor_close"
+            android:background="?android:selectableItemBackground"
+            android:layout_marginEnd="12dp"
+            app:layout_constraintTop_toTopOf="@id/composerModeIconView"
+            app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder" />
+
+        <androidx.constraintlayout.widget.Barrier
+            android:id="@+id/composerModeBarrier"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            app:barrierDirection="bottom"
+            app:constraint_referenced_ids="composerModeIconView,composerModeTitleView,composerModeCloseView" />
+
+        <androidx.constraintlayout.widget.Group
+            android:id="@+id/composerModeGroup"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:visibility="gone"
+            tools:visibility="visible"
+            app:constraint_referenced_ids="composerModeIconView,composerModeTitleView,composerModeCloseView" />
+
+        <io.element.android.wysiwyg.EditorEditText
+            android:id="@+id/richTextComposerEditText"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            app:layout_constraintHeight_default="wrap"
+            android:gravity="top"
+            android:hint="Room message placeholder"
+            android:nextFocusLeft="@id/richTextComposerEditText"
+            android:nextFocusUp="@id/richTextComposerEditText"
+            android:layout_marginStart="12dp"
+            android:imeOptions="flagNoExtractUi"
+            app:layout_constraintVertical_bias="0"
+            app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+            app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
+            app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
+            app:layout_constraintTop_toBottomOf="@id/composerModeBarrier"
+            app:bulletRadius="4sp"
+            app:bulletGap="8sp"
+            app:codeBlockBackgroundDrawable="@drawable/bg_code_block"
+            app:inlineCodeSingleLineBg="@drawable/bg_inline_code_single_line"
+            app:inlineCodeMultiLineBgLeft="@drawable/bg_inline_code_multi_line_left"
+            app:inlineCodeMultiLineBgMid="@drawable/bg_inline_code_multi_line_mid"
+            app:inlineCodeMultiLineBgRight="@drawable/bg_inline_code_multi_line_right"
+            tools:text="@tools:sample/lorem/random" />
+
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/plainTextComposerEditText"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            app:layout_constraintHeight_default="wrap"
+            android:visibility="gone"
+            android:hint="Room placeholder"
+            android:nextFocusLeft="@id/plainTextComposerEditText"
+            android:nextFocusUp="@id/plainTextComposerEditText"
+            android:layout_marginStart="12dp"
+            android:gravity="top"
+            android:imeOptions="flagNoExtractUi"
+            app:layout_constraintVertical_bias="0"
+            app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+            app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
+            app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
+            app:layout_constraintTop_toBottomOf="@id/composerModeBarrier"
+            tools:text="@tools:sample/lorem/random" />
+
+        <ImageButton
+            android:id="@+id/composerFullScreenButton"
+            android:layout_width="40dp"
+            android:layout_height="40dp"
+            android:layout_marginEnd="4dp"
+            app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
+            app:layout_constraintTop_toBottomOf="@id/composerModeBarrier"
+            app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
+            app:layout_constraintVertical_bias="0"
+            android:src="@drawable/ic_composer_full_screen"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
+            android:contentDescription="@string/rich_text_editor_full_screen_toggle" />
+
+        <ImageButton
+            android:id="@+id/sendButton"
+            android:layout_width="56dp"
+            android:layout_height="56dp"
+            android:paddingEnd="4dp"
+            android:contentDescription="@string/action_send"
+            android:scaleType="center"
+            android:src="@drawable/ic_rich_composer_send"
+            android:visibility="invisible"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
+            app:layout_constraintTop_toBottomOf="@id/composerEditTextOuterBorder"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintVertical_bias="1"
+            tools:ignore="MissingPrefix,RtlSymmetry"
+            tools:visibility="visible" />
+
+        <HorizontalScrollView android:id="@+id/richTextMenuScrollView"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:minHeight="52dp"
+            android:requiresFadingEdge="horizontal"
+            android:fadingEdgeLength="28dp"
+            app:layout_constraintTop_toBottomOf="@id/composerEditTextOuterBorder"
+            app:layout_constraintStart_toEndOf="@id/attachmentButton"
+            app:layout_constraintEnd_toStartOf="@id/sendButton"
+            app:layout_constraintBottom_toBottomOf="parent"
+            android:fillViewport="true">
+
+            <LinearLayout
+                android:id="@+id/richTextMenu"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:orientation="horizontal"
+                android:paddingVertical="4dp">
+
+            </LinearLayout>
+
+        </HorizontalScrollView>
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/view_preview_post.xml b/app/src/main/res/layout/view_preview_post.xml
index 13f7ae6b1..8f3a8baa7 100644
--- a/app/src/main/res/layout/view_preview_post.xml
+++ b/app/src/main/res/layout/view_preview_post.xml
@@ -51,20 +51,37 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content">
 
-            <org.futo.circles.view.MarkdownEditText
-                android:id="@+id/etTextPost"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:background="@null"
-                android:gravity="top"
-                android:hint="@string/enter_your_message_here"
-                android:inputType="textCapSentences|textMultiLine"
-                android:paddingStart="12dp"
-                android:paddingEnd="12dp"
-                app:layout_constraintBottom_toTopOf="@id/lMediaContent"
+            <org.futo.circles.view.RichTextComposerLayout
+                android:id="@+id/richTextComposerLayout"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:minHeight="56dp"
+                android:transitionName="composer"
                 app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintHeight_default="wrap"
                 app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toTopOf="parent" />
+                app:layout_constraintTop_toTopOf="parent"
+                tools:visibility="visible" />
+
+            <!--            <io.element.android.wysiwyg.EditorEditText-->
+            <!--                android:id="@+id/etTextPost"-->
+            <!--                android:layout_width="0dp"-->
+            <!--                android:layout_height="0dp"-->
+            <!--                android:gravity="top"-->
+            <!--                android:hint="Hint"-->
+            <!--                app:bulletGap="8sp"-->
+            <!--                app:bulletRadius="4sp"-->
+            <!--                app:codeBlockBackgroundDrawable="@drawable/ic_text"-->
+            <!--                app:inlineCodeMultiLineBgLeft="@drawable/ic_text"-->
+            <!--                app:inlineCodeMultiLineBgMid="@drawable/ic_text"-->
+            <!--                app:inlineCodeMultiLineBgRight="@drawable/ic_text"-->
+            <!--                app:inlineCodeSingleLineBg="@drawable/ic_text"-->
+            <!--                app:layout_constraintBottom_toTopOf="@id/lMediaContent"-->
+            <!--                app:layout_constraintEnd_toEndOf="parent"-->
+            <!--                app:layout_constraintHeight_default="wrap"-->
+            <!--                app:layout_constraintStart_toStartOf="parent"-->
+            <!--                app:layout_constraintTop_toTopOf="parent"-->
+            <!--                tools:text="@tools:sample/lorem/random" />-->
 
 
             <include
@@ -75,8 +92,7 @@
                 android:layout_marginTop="8dp"
                 app:layout_constraintBottom_toBottomOf="parent"
                 app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/etTextPost" />
+                app:layout_constraintStart_toStartOf="parent" />
 
             <com.google.android.material.imageview.ShapeableImageView
                 android:id="@+id/ivRemoveImage"
@@ -91,7 +107,7 @@
                 app:layout_constraintEnd_toEndOf="@id/lMediaContent"
                 app:layout_constraintTop_toTopOf="@id/lMediaContent"
                 app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.CornerSize50Percent" />
-            
+
         </androidx.constraintlayout.widget.ConstraintLayout>
 
     </ScrollView>
diff --git a/app/src/main/res/layout/view_rich_text_menu_button.xml b/app/src/main/res/layout/view_rich_text_menu_button.xml
new file mode 100644
index 000000000..b99a29da2
--- /dev/null
+++ b/app/src/main/res/layout/view_rich_text_menu_button.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageButton 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="@dimen/rich_text_composer_menu_item_size"
+    android:layout_height="@dimen/rich_text_composer_menu_item_size"
+    android:layout_marginHorizontal="2dp"
+    android:background="@drawable/bg_rich_text_menu_button"
+    app:tint="@color/selector_rich_text_menu_icon"
+    tools:src="@drawable/ic_composer_bold"
+    tools:ignore="ContentDescription" />
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index ac9eed3a5..be23a2f4f 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -1,6 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
 
+    <attr name="vctr_content_quaternary" format="color" />
+
     <declare-styleable name="GroupPostHeaderView">
         <attr name="optionsVisible" format="boolean" />
     </declare-styleable>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 57b80004b..b92aaf03d 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -5,4 +5,8 @@
     <dimen name="post_text_side_margin">24dp</dimen>
     <dimen name="circle_icon_size">100dp</dimen>
     <dimen name="profile_avatar_size">50dp</dimen>
+
+    <dimen name="rich_text_composer_corner_radius_single_line">28dp</dimen>
+    <dimen name="rich_text_composer_corner_radius_expanded">14dp</dimen>
+    <dimen name="rich_text_composer_menu_item_size">44dp</dimen>
 </resources>
\ 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 eade56ad3..fa73109f6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -216,6 +216,26 @@
     <string name="help">Help</string>
     <string name="optional_request_message">Optional: Request message</string>
 
+    <!-- Rich text editor -->
+    <string name="rich_text_editor_format_bold">Apply bold format</string>
+    <string name="rich_text_editor_format_italic">Apply italic format</string>
+    <string name="rich_text_editor_format_strikethrough">Apply strikethrough format</string>
+    <string name="rich_text_editor_format_underline">Apply underline format</string>
+    <string name="rich_text_editor_link">Set link</string>
+    <string name="rich_text_editor_numbered_list">Toggle numbered list</string>
+    <string name="rich_text_editor_bullet_list">Toggle bullet list</string>
+    <string name="rich_text_editor_indent">Indent</string>
+    <string name="rich_text_editor_unindent">Unindent</string>
+    <string name="rich_text_editor_quote">Toggle quote</string>
+    <string name="rich_text_editor_inline_code">Apply inline code format</string>
+    <string name="rich_text_editor_code_block">Toggle code block</string>
+    <string name="rich_text_editor_full_screen_toggle">Toggle full screen mode</string>
+    <string name="action_save">Save</string>
+    <string name="action_send">Send</string>
+    <string name="editing">Editing</string>
+    <string name="quoting">Quoting</string>
+    <string name="replying_to">Replying to %s</string>
+
     <string-array name="report_categories">
         <item>@string/crude_language</item>
         <item>@string/copyright_violation</item>
diff --git a/build.gradle b/build.gradle
index 924f2bdbc..5eb8219f8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -12,7 +12,7 @@ buildscript {
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:8.1.3'
+        classpath 'com.android.tools.build:gradle:8.1.4'
         classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21'
         classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$androidx_nav_version"
         classpath 'com.google.gms:google-services:4.4.0'
-- 
GitLab