mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Merge pull request #7353 from hayaksi1/fix/860-enter-sends-message
Send a message with Enter on a physical keyboard, Shift+Enter for a new line
This commit is contained in:
+3
-2
@@ -161,6 +161,8 @@ fun TextComposer(
|
|||||||
} else {
|
} else {
|
||||||
stringResource(id = R.string.rich_text_editor_composer_placeholder)
|
stringResource(id = R.string.rich_text_editor_composer_placeholder)
|
||||||
}
|
}
|
||||||
|
val canSendTextMessage = markdown.isNotBlank() || composerMode is MessageComposerMode.Attachment
|
||||||
|
|
||||||
val textInput: @Composable () -> Unit = when (state) {
|
val textInput: @Composable () -> Unit = when (state) {
|
||||||
is TextEditorState.Rich -> {
|
is TextEditorState.Rich -> {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
@@ -219,14 +221,13 @@ fun TextComposer(
|
|||||||
onReceiveSuggestion = onReceiveSuggestion,
|
onReceiveSuggestion = onReceiveSuggestion,
|
||||||
richTextEditorStyle = style,
|
richTextEditorStyle = style,
|
||||||
onSelectRichContent = onSelectRichContent,
|
onSelectRichContent = onSelectRichContent,
|
||||||
|
onSendMessage = { if (canSendTextMessage) onSendMessage() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val canSendTextMessage = markdown.isNotBlank() || composerMode is MessageComposerMode.Attachment
|
|
||||||
|
|
||||||
val textFormattingOptions: @Composable (() -> Unit)? = (state as? TextEditorState.Rich)?.let {
|
val textFormattingOptions: @Composable (() -> Unit)? = (state as? TextEditorState.Rich)?.let {
|
||||||
@Composable { TextFormatting(state = it.richTextEditorState) }
|
@Composable { TextFormatting(state = it.richTextEditorState) }
|
||||||
}
|
}
|
||||||
|
|||||||
+21
@@ -9,6 +9,8 @@
|
|||||||
package io.element.android.libraries.textcomposer.components.markdown
|
package io.element.android.libraries.textcomposer.components.markdown
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.view.KeyCharacterMap
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.widget.AppCompatEditText
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
|
|
||||||
@@ -17,8 +19,27 @@ internal class MarkdownEditText(
|
|||||||
) : AppCompatEditText(context) {
|
) : AppCompatEditText(context) {
|
||||||
var onSelectionChangeListener: ((Int, Int) -> Unit)? = null
|
var onSelectionChangeListener: ((Int, Int) -> Unit)? = null
|
||||||
|
|
||||||
|
var onEnterKeyListener: (() -> Boolean)? = null
|
||||||
|
|
||||||
private var isModifyingText = false
|
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) {
|
fun updateEditableText(charSequence: CharSequence) {
|
||||||
isModifyingText = true
|
isModifyingText = true
|
||||||
editableText.clear()
|
editableText.clear()
|
||||||
|
|||||||
+7
@@ -54,6 +54,7 @@ fun MarkdownTextInput(
|
|||||||
onReceiveSuggestion: (Suggestion?) -> Unit,
|
onReceiveSuggestion: (Suggestion?) -> Unit,
|
||||||
richTextEditorStyle: RichTextEditorStyle,
|
richTextEditorStyle: RichTextEditorStyle,
|
||||||
onSelectRichContent: ((Uri) -> Unit)?,
|
onSelectRichContent: ((Uri) -> Unit)?,
|
||||||
|
onSendMessage: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
// Copied from io.element.android.wysiwyg.internal.utils.UriContentListener
|
// Copied from io.element.android.wysiwyg.internal.utils.UriContentListener
|
||||||
class ReceiveUriContentListener(
|
class ReceiveUriContentListener(
|
||||||
@@ -132,6 +133,12 @@ fun MarkdownTextInput(
|
|||||||
update = { editText ->
|
update = { editText ->
|
||||||
editText.contentDescription = placeholder
|
editText.contentDescription = placeholder
|
||||||
editText.applyStyleInCompose(richTextEditorStyle)
|
editText.applyStyleInCompose(richTextEditorStyle)
|
||||||
|
editText.onEnterKeyListener = onSendMessage?.let { send ->
|
||||||
|
{
|
||||||
|
send()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
val text = state.text.value()
|
val text = state.text.value()
|
||||||
mentionSpanUpdater.updateMentionSpans(text)
|
mentionSpanUpdater.updateMentionSpans(text)
|
||||||
if (state.text.needsDisplaying()) {
|
if (state.text.needsDisplaying()) {
|
||||||
|
|||||||
+67
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.textcomposer.impl.components.markdown
|
package io.element.android.libraries.textcomposer.impl.components.markdown
|
||||||
|
|
||||||
|
import android.view.KeyCharacterMap
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.AndroidComposeUiTest
|
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.Suggestion
|
||||||
import io.element.android.libraries.textcomposer.model.SuggestionType
|
import io.element.android.libraries.textcomposer.model.SuggestionType
|
||||||
import io.element.android.libraries.textcomposer.model.aMarkdownTextEditorState
|
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.EnsureCalledOnceWithParam
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class MarkdownTextInputTest : RobolectricTest() {
|
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
|
@Test
|
||||||
fun `when user types onTyping is triggered with value 'true'`() = runAndroidComposeUiTest {
|
fun `when user types onTyping is triggered with value 'true'`() = runAndroidComposeUiTest {
|
||||||
val state = aMarkdownTextEditorState(initialFocus = true)
|
val state = aMarkdownTextEditorState(initialFocus = true)
|
||||||
@@ -150,6 +191,7 @@ class MarkdownTextInputTest : RobolectricTest() {
|
|||||||
state: MarkdownTextEditorState = aMarkdownTextEditorState(),
|
state: MarkdownTextEditorState = aMarkdownTextEditorState(),
|
||||||
onTyping: (Boolean) -> Unit = {},
|
onTyping: (Boolean) -> Unit = {},
|
||||||
onSuggestionReceived: (Suggestion?) -> Unit = {},
|
onSuggestionReceived: (Suggestion?) -> Unit = {},
|
||||||
|
onSendMessage: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
val style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus)
|
val style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus)
|
||||||
@@ -161,6 +203,27 @@ class MarkdownTextInputTest : RobolectricTest() {
|
|||||||
onReceiveSuggestion = onSuggestionReceived,
|
onReceiveSuggestion = onSuggestionReceived,
|
||||||
richTextEditorStyle = style,
|
richTextEditorStyle = style,
|
||||||
onSelectRichContent = null,
|
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 {
|
private fun ComponentActivity.findEditor(): EditText {
|
||||||
return window.decorView.findViewWithTag(TestTags.plainTextEditor.value)
|
return window.decorView.findViewWithTag(TestTags.plainTextEditor.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val PHYSICAL_KEYBOARD_DEVICE_ID = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user