diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index b8839cac67..45a9e95bc0 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -161,6 +161,8 @@ fun TextComposer( } else { stringResource(id = R.string.rich_text_editor_composer_placeholder) } + val canSendTextMessage = markdown.isNotBlank() || composerMode is MessageComposerMode.Attachment + val textInput: @Composable () -> Unit = when (state) { is TextEditorState.Rich -> { val coroutineScope = rememberCoroutineScope() @@ -219,14 +221,13 @@ fun TextComposer( onReceiveSuggestion = onReceiveSuggestion, richTextEditorStyle = style, onSelectRichContent = onSelectRichContent, + onSendMessage = { if (canSendTextMessage) onSendMessage() }, ) } } } } - val canSendTextMessage = markdown.isNotBlank() || composerMode is MessageComposerMode.Attachment - val textFormattingOptions: @Composable (() -> Unit)? = (state as? TextEditorState.Rich)?.let { @Composable { TextFormatting(state = it.richTextEditorState) } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownEditText.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownEditText.kt index bd808c9acb..8cc06c77c3 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownEditText.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownEditText.kt @@ -9,6 +9,8 @@ package io.element.android.libraries.textcomposer.components.markdown import android.content.Context +import android.view.KeyCharacterMap +import android.view.KeyEvent import android.view.View import androidx.appcompat.widget.AppCompatEditText @@ -17,8 +19,27 @@ internal class MarkdownEditText( ) : AppCompatEditText(context) { var onSelectionChangeListener: ((Int, Int) -> Unit)? = null + var onEnterKeyListener: (() -> Boolean)? = null + private var isModifyingText = false + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (event.keyCode == KeyEvent.KEYCODE_ENTER && + !event.isShiftPressed && + event.deviceId != KeyCharacterMap.VIRTUAL_KEYBOARD + ) { + val listener = onEnterKeyListener + if (listener != null) { + return when (event.action) { + KeyEvent.ACTION_UP -> true + KeyEvent.ACTION_DOWN -> if (event.repeatCount == 0) listener() else true + else -> super.dispatchKeyEvent(event) + } + } + } + return super.dispatchKeyEvent(event) + } + fun updateEditableText(charSequence: CharSequence) { isModifyingText = true editableText.clear() diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt index 064ef03106..8c6f30eed1 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt @@ -54,6 +54,7 @@ fun MarkdownTextInput( onReceiveSuggestion: (Suggestion?) -> Unit, richTextEditorStyle: RichTextEditorStyle, onSelectRichContent: ((Uri) -> Unit)?, + onSendMessage: (() -> Unit)? = null, ) { // Copied from io.element.android.wysiwyg.internal.utils.UriContentListener class ReceiveUriContentListener( @@ -132,6 +133,12 @@ fun MarkdownTextInput( update = { editText -> editText.contentDescription = placeholder editText.applyStyleInCompose(richTextEditorStyle) + editText.onEnterKeyListener = onSendMessage?.let { send -> + { + send() + true + } + } val text = state.text.value() mentionSpanUpdater.updateMentionSpans(text) if (state.text.needsDisplaying()) { diff --git a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/components/markdown/MarkdownTextInputTest.kt b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/components/markdown/MarkdownTextInputTest.kt index ba49428cae..15687cb4c9 100644 --- a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/components/markdown/MarkdownTextInputTest.kt +++ b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/components/markdown/MarkdownTextInputTest.kt @@ -10,6 +10,8 @@ package io.element.android.libraries.textcomposer.impl.components.markdown +import android.view.KeyCharacterMap +import android.view.KeyEvent import android.widget.EditText import androidx.activity.ComponentActivity import androidx.compose.ui.test.AndroidComposeUiTest @@ -32,12 +34,51 @@ import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.SuggestionType import io.element.android.libraries.textcomposer.model.aMarkdownTextEditorState +import io.element.android.tests.testutils.EnsureCalledOnce import io.element.android.tests.testutils.EnsureCalledOnceWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test class MarkdownTextInputTest : RobolectricTest() { + @Test + fun `pressing enter on a physical keyboard sends the message`() = runAndroidComposeUiTest { + val state = aMarkdownTextEditorState(initialText = "Hello", initialFocus = true) + val onSendMessage = EnsureCalledOnce() + setMarkdownTextInput(state = state, onSendMessage = onSendMessage) + val editor = activity!!.findEditor() + editor.pressEnter() + awaitIdle() + onSendMessage.assertSuccess() + assertThat(editor.editableText.toString()).isEqualTo("Hello") + } + + @Test + fun `pressing shift enter on a physical keyboard inserts a new line`() = runAndroidComposeUiTest { + val state = aMarkdownTextEditorState(initialText = "Hello", initialFocus = true) + var sendCount = 0 + setMarkdownTextInput(state = state, onSendMessage = { sendCount++ }) + val editor = activity!!.findEditor() + editor.setSelection(editor.editableText.length) + editor.pressEnter(metaState = KeyEvent.META_SHIFT_ON) + awaitIdle() + assertThat(sendCount).isEqualTo(0) + assertThat(editor.editableText.toString()).isEqualTo("Hello\n") + } + + @Test + fun `pressing enter on the soft keyboard does not send the message`() = runAndroidComposeUiTest { + val state = aMarkdownTextEditorState(initialText = "Hello", initialFocus = true) + var sendCount = 0 + setMarkdownTextInput(state = state, onSendMessage = { sendCount++ }) + val editor = activity!!.findEditor() + editor.setSelection(editor.editableText.length) + editor.pressEnter(deviceId = KeyCharacterMap.VIRTUAL_KEYBOARD) + awaitIdle() + assertThat(sendCount).isEqualTo(0) + assertThat(editor.editableText.toString()).isEqualTo("Hello\n") + } + @Test fun `when user types onTyping is triggered with value 'true'`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialFocus = true) @@ -150,6 +191,7 @@ class MarkdownTextInputTest : RobolectricTest() { state: MarkdownTextEditorState = aMarkdownTextEditorState(), onTyping: (Boolean) -> Unit = {}, onSuggestionReceived: (Suggestion?) -> Unit = {}, + onSendMessage: (() -> Unit)? = null, ) { setContent { val style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus) @@ -161,6 +203,27 @@ class MarkdownTextInputTest : RobolectricTest() { onReceiveSuggestion = onSuggestionReceived, richTextEditorStyle = style, onSelectRichContent = null, + onSendMessage = onSendMessage, + ) + } + } + + private fun EditText.pressEnter( + metaState: Int = 0, + deviceId: Int = PHYSICAL_KEYBOARD_DEVICE_ID, + ) { + listOf(KeyEvent.ACTION_DOWN, KeyEvent.ACTION_UP).forEach { action -> + dispatchKeyEvent( + KeyEvent( + 0L, + 0L, + action, + KeyEvent.KEYCODE_ENTER, + 0, + metaState, + deviceId, + 0, + ) ) } } @@ -168,4 +231,8 @@ class MarkdownTextInputTest : RobolectricTest() { private fun ComponentActivity.findEditor(): EditText { return window.decorView.findViewWithTag(TestTags.plainTextEditor.value) } + + private companion object { + const val PHYSICAL_KEYBOARD_DEVICE_ID = 1 + } }