mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Use TextFieldState in CustomInput state for proper MVI text handling
This commit is contained in:
-2
@@ -22,8 +22,6 @@ sealed interface UserStatusEvent {
|
|||||||
data object CancelCustomInput : UserStatusEvent
|
data object CancelCustomInput : UserStatusEvent
|
||||||
/** User changed the emoji in the custom input row. */
|
/** User changed the emoji in the custom input row. */
|
||||||
data class UpdateCustomEmoji(val emoji: String) : UserStatusEvent
|
data class UpdateCustomEmoji(val emoji: String) : UserStatusEvent
|
||||||
/** User changed the text in the custom input row. */
|
|
||||||
data class UpdateCustomText(val text: String) : UserStatusEvent
|
|
||||||
/** User tapped the clear action on an existing status. */
|
/** User tapped the clear action on an existing status. */
|
||||||
data object Clear : UserStatusEvent
|
data object Clear : UserStatusEvent
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-5
@@ -7,6 +7,9 @@
|
|||||||
|
|
||||||
package io.element.android.features.preferences.impl.userstatus
|
package io.element.android.features.preferences.impl.userstatus
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.foundation.text.input.clearText
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -31,6 +34,7 @@ class UserStatusPresenter(
|
|||||||
override fun present(): UserStatusState {
|
override fun present(): UserStatusState {
|
||||||
val userProfile by matrixClient.userProfile.collectAsState()
|
val userProfile by matrixClient.userProfile.collectAsState()
|
||||||
var pickerState by remember { mutableStateOf<UserStatusPickerState>(UserStatusPickerState.Hidden) }
|
var pickerState by remember { mutableStateOf<UserStatusPickerState>(UserStatusPickerState.Hidden) }
|
||||||
|
val customTextFieldState = remember { TextFieldState() }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
fun handleEvent(event: UserStatusEvent) {
|
fun handleEvent(event: UserStatusEvent) {
|
||||||
@@ -39,9 +43,14 @@ class UserStatusPresenter(
|
|||||||
UserStatusEvent.Dismiss -> pickerState = UserStatusPickerState.Hidden
|
UserStatusEvent.Dismiss -> pickerState = UserStatusPickerState.Hidden
|
||||||
UserStatusEvent.OpenCustomInput -> {
|
UserStatusEvent.OpenCustomInput -> {
|
||||||
val raw = userProfile.rawStatus
|
val raw = userProfile.rawStatus
|
||||||
|
if (raw != null) {
|
||||||
|
customTextFieldState.setTextAndPlaceCursorAtEnd(raw.text)
|
||||||
|
} else {
|
||||||
|
customTextFieldState.clearText()
|
||||||
|
}
|
||||||
pickerState = UserStatusPickerState.CustomInput(
|
pickerState = UserStatusPickerState.CustomInput(
|
||||||
emoji = raw?.emoji ?: "😀",
|
emoji = raw?.emoji ?: "😀",
|
||||||
text = raw?.text ?: "",
|
textFieldState = customTextFieldState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is UserStatusEvent.Set -> {
|
is UserStatusEvent.Set -> {
|
||||||
@@ -57,10 +66,6 @@ class UserStatusPresenter(
|
|||||||
val current = pickerState as? UserStatusPickerState.CustomInput ?: return
|
val current = pickerState as? UserStatusPickerState.CustomInput ?: return
|
||||||
pickerState = current.copy(emoji = event.emoji)
|
pickerState = current.copy(emoji = event.emoji)
|
||||||
}
|
}
|
||||||
is UserStatusEvent.UpdateCustomText -> {
|
|
||||||
val current = pickerState as? UserStatusPickerState.CustomInput ?: return
|
|
||||||
pickerState = current.copy(text = event.text)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -8,6 +8,7 @@
|
|||||||
package io.element.android.features.preferences.impl.userstatus
|
package io.element.android.features.preferences.impl.userstatus
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ sealed interface UserStatusPickerState {
|
|||||||
data object ShowingPicker : UserStatusPickerState
|
data object ShowingPicker : UserStatusPickerState
|
||||||
data class CustomInput(
|
data class CustomInput(
|
||||||
val emoji: String = "😀",
|
val emoji: String = "😀",
|
||||||
val text: String = "",
|
val textFieldState: TextFieldState,
|
||||||
) : UserStatusPickerState
|
) : UserStatusPickerState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-16
@@ -15,8 +15,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.text.input.InputTransformation
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -32,6 +34,7 @@ import io.element.android.libraries.designsystem.components.list.ListItemContent
|
|||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.FilledTextField
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconSource
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
@@ -40,7 +43,6 @@ import io.element.android.libraries.designsystem.theme.components.ModalBottomShe
|
|||||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
|
||||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.api.user.UserStatus
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -77,10 +79,13 @@ fun UserStatusRow(
|
|||||||
is UserStatusPickerState.CustomInput -> {
|
is UserStatusPickerState.CustomInput -> {
|
||||||
CustomStatusInputRow(
|
CustomStatusInputRow(
|
||||||
emoji = pickerState.emoji,
|
emoji = pickerState.emoji,
|
||||||
text = pickerState.text,
|
textFieldState = pickerState.textFieldState,
|
||||||
onEmojiChange = { state.eventSink(UserStatusEvent.UpdateCustomEmoji(it)) },
|
onEmojiChange = { state.eventSink(UserStatusEvent.UpdateCustomEmoji(it)) },
|
||||||
onTextChange = { state.eventSink(UserStatusEvent.UpdateCustomText(it)) },
|
onConfirm = {
|
||||||
onConfirm = { state.eventSink(UserStatusEvent.Set(UserStatus(pickerState.emoji, pickerState.text))) },
|
state.eventSink(
|
||||||
|
UserStatusEvent.Set(UserStatus(pickerState.emoji, pickerState.textFieldState.text.toString()))
|
||||||
|
)
|
||||||
|
},
|
||||||
onCancel = { state.eventSink(UserStatusEvent.CancelCustomInput) },
|
onCancel = { state.eventSink(UserStatusEvent.CancelCustomInput) },
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
@@ -158,24 +163,21 @@ private fun UserStatusPickerBottomSheet(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun CustomStatusInputRow(
|
private fun CustomStatusInputRow(
|
||||||
emoji: String,
|
emoji: String,
|
||||||
text: String,
|
textFieldState: TextFieldState,
|
||||||
onEmojiChange: (String) -> Unit,
|
onEmojiChange: (String) -> Unit,
|
||||||
onTextChange: (String) -> Unit,
|
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
TextField(
|
FilledTextField(
|
||||||
value = text,
|
state = textFieldState,
|
||||||
onValueChange = { if (it.length <= 30) onTextChange(it) },
|
placeholder = { Text(stringResource(R.string.screen_preferences_user_status_custom_hint)) },
|
||||||
placeholder = stringResource(R.string.screen_preferences_user_status_custom_hint),
|
inputTransformation = InputTransformation { if (length > 30) revertAllChanges() },
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = { if (textFieldState.text.isNotBlank()) onConfirm() },
|
||||||
onDone = { if (text.isNotBlank()) onConfirm() }
|
lineLimits = TextFieldLineLimits.SingleLine,
|
||||||
),
|
|
||||||
singleLine = true,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
trailingContent = ListItemContent.Custom({
|
trailingContent = ListItemContent.Custom({
|
||||||
@@ -261,7 +263,7 @@ internal fun UserStatusRowCustomInputPreview() = ElementPreview {
|
|||||||
UserStatusRow(
|
UserStatusRow(
|
||||||
state = UserStatusState(
|
state = UserStatusState(
|
||||||
displayedStatus = null,
|
displayedStatus = null,
|
||||||
pickerState = UserStatusPickerState.CustomInput(emoji = "😀", text = ""),
|
pickerState = UserStatusPickerState.CustomInput(emoji = "😀", textFieldState = TextFieldState()),
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-14
@@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.user.UserStatus
|
|||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
|
||||||
class UserStatusPresenterTest {
|
class UserStatusPresenterTest {
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ class UserStatusPresenterTest {
|
|||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
val pickerState = state.pickerState as UserStatusPickerState.CustomInput
|
val pickerState = state.pickerState as UserStatusPickerState.CustomInput
|
||||||
assertThat(pickerState.emoji).isEqualTo("😀")
|
assertThat(pickerState.emoji).isEqualTo("😀")
|
||||||
assertThat(pickerState.text).isEqualTo("")
|
assertThat(pickerState.textFieldState.text.toString()).isEqualTo("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,19 +99,7 @@ class UserStatusPresenterTest {
|
|||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
val pickerState = state.pickerState as UserStatusPickerState.CustomInput
|
val pickerState = state.pickerState as UserStatusPickerState.CustomInput
|
||||||
assertThat(pickerState.emoji).isEqualTo("🌴")
|
assertThat(pickerState.emoji).isEqualTo("🌴")
|
||||||
assertThat(pickerState.text).isEqualTo("Away")
|
assertThat(pickerState.textFieldState.text.toString()).isEqualTo("Away")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `UpdateCustomText updates text in CustomInput state`() = runTest {
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
createPresenter().present()
|
|
||||||
}.test {
|
|
||||||
awaitItem().eventSink(UserStatusEvent.OpenCustomInput)
|
|
||||||
awaitItem().eventSink(UserStatusEvent.UpdateCustomText("Focusing"))
|
|
||||||
val state = awaitItem()
|
|
||||||
assertThat((state.pickerState as UserStatusPickerState.CustomInput).text).isEqualTo("Focusing")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user