From fa1b32f0ba39814950c401f530c45ee149ef0720 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 22 Jan 2026 16:34:22 +0100 Subject: [PATCH] Let SearchBar/SearchField use TextFieldState --- .../impl/DefaultInvitePeopleEvents.kt | 1 - .../impl/DefaultInvitePeoplePresenter.kt | 17 ++-- .../impl/DefaultInvitePeopleState.kt | 3 +- .../impl/DefaultInvitePeopleStateProvider.kt | 3 +- .../invitepeople/impl/InvitePeopleView.kt | 10 +-- .../impl/DefaultInvitePeoplePresenterTest.kt | 35 ++++---- .../customreaction/picker/EmojiPicker.kt | 4 +- .../picker/EmojiPickerEvents.kt | 1 - .../picker/EmojiPickerPresenter.kt | 7 +- .../customreaction/picker/EmojiPickerState.kt | 3 +- .../picker/EmojiPickerStateProvider.kt | 3 +- .../picker/EmojiPickerPresenterTest.kt | 11 +-- .../impl/roles/ChangeRolesEvent.kt | 1 - .../impl/roles/ChangeRolesPresenter.kt | 11 ++- .../impl/roles/ChangeRolesState.kt | 3 +- .../impl/roles/ChangeRolesStateProvider.kt | 11 ++- .../impl/roles/ChangeRolesView.kt | 3 +- .../impl/roles/ChangeRolesPresenterTest.kt | 7 +- .../impl/roles/ChangeRolesViewTest.kt | 29 ++----- .../impl/members/RoomMemberListEvents.kt | 1 - .../impl/members/RoomMemberListPresenter.kt | 9 +- .../impl/members/RoomMemberListState.kt | 3 +- .../members/RoomMemberListStateProvider.kt | 3 +- .../impl/members/RoomMemberListView.kt | 10 +-- .../members/RoomMemberListPresenterTest.kt | 13 +-- .../space/impl/addroom/AddRoomToSpaceEvent.kt | 2 - .../impl/addroom/AddRoomToSpacePresenter.kt | 19 ++--- .../space/impl/addroom/AddRoomToSpaceState.kt | 3 +- .../addroom/AddRoomToSpaceStateProvider.kt | 3 +- .../space/impl/addroom/AddRoomToSpaceView.kt | 7 +- .../addroom/AddRoomToSpacePresenterTest.kt | 41 ++------- .../impl/addroom/AddRoomToSpaceViewTest.kt | 16 +--- .../impl/components/SearchUserBar.kt | 11 ++- .../startchat/impl/components/UserListView.kt | 5 +- .../impl/root/StartChatStateProvider.kt | 8 +- .../impl/userlist/DefaultUserListPresenter.kt | 7 +- .../startchat/impl/userlist/UserListEvents.kt | 1 - .../startchat/impl/userlist/UserListState.kt | 3 +- .../impl/userlist/UserListStateProvider.kt | 3 +- .../userlist/DefaultUserListPresenterTest.kt | 17 ++-- .../theme/components/SearchBar.kt | 34 ++++---- .../theme/components/SearchField.kt | 83 +++++-------------- .../roomselect/impl/RoomSelectEvents.kt | 1 - .../roomselect/impl/RoomSelectPresenter.kt | 7 +- .../roomselect/impl/RoomSelectState.kt | 3 +- .../impl/RoomSelectStateProvider.kt | 11 +-- .../roomselect/impl/RoomSelectView.kt | 3 +- .../impl/RoomSelectPresenterTest.kt | 5 +- 48 files changed, 197 insertions(+), 298 deletions(-) diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt index b0c8994a62..b1f18b1df9 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt @@ -13,6 +13,5 @@ import io.element.android.libraries.matrix.api.user.MatrixUser sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents { data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents - data class UpdateSearchQuery(val query: String) : DefaultInvitePeopleEvents data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents } diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt index bba6e80ebe..3450587e82 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt @@ -8,6 +8,8 @@ package io.element.android.features.invitepeople.impl +import androidx.compose.foundation.text.input.clearText +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -80,7 +82,7 @@ class DefaultInvitePeoplePresenter( val roomMembers = remember { mutableStateOf>>(AsyncData.Loading()) } val selectedUsers = remember { mutableStateOf>(persistentListOf()) } val searchResults = remember { mutableStateOf>>(SearchBarResultState.Initial()) } - var searchQuery by rememberSaveable { mutableStateOf("") } + val queryState = rememberTextFieldState() var searchActive by rememberSaveable { mutableStateOf(false) } val showSearchLoader = rememberSaveable { mutableStateOf(false) } val sendInvitesAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } @@ -129,6 +131,7 @@ class DefaultInvitePeoplePresenter( fetchMembers(it, roomMembers) } } + val searchQuery = queryState.text.toString() LaunchedEffect(searchQuery, roomMembers) { performSearch( searchResults = searchResults, @@ -143,11 +146,9 @@ class DefaultInvitePeoplePresenter( when (event) { is DefaultInvitePeopleEvents.OnSearchActiveChanged -> { searchActive = event.active - searchQuery = "" - } - - is DefaultInvitePeopleEvents.UpdateSearchQuery -> { - searchQuery = event.query + if (!event.active) { + queryState.clearText() + } } is DefaultInvitePeopleEvents.ToggleUser -> { @@ -162,7 +163,7 @@ class DefaultInvitePeoplePresenter( } is InvitePeopleEvents.CloseSearch -> { searchActive = false - searchQuery = "" + queryState.clearText() } } } @@ -171,7 +172,7 @@ class DefaultInvitePeoplePresenter( room = room.map { }, canInvite = selectedUsers.value.isNotEmpty() && !sendInvitesAction.value.isLoading(), selectedUsers = selectedUsers.value, - searchQuery = searchQuery, + searchQuery = queryState, isSearchActive = searchActive, searchResults = searchResults.value, showSearchLoader = showSearchLoader.value, diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt index 0b9f2740c0..842bcf1148 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt @@ -8,6 +8,7 @@ package io.element.android.features.invitepeople.impl +import androidx.compose.foundation.text.input.TextFieldState import io.element.android.features.invitepeople.api.InvitePeopleEvents import io.element.android.features.invitepeople.api.InvitePeopleState import io.element.android.libraries.architecture.AsyncAction @@ -19,7 +20,7 @@ import kotlinx.collections.immutable.ImmutableList data class DefaultInvitePeopleState( val room: AsyncData, override val canInvite: Boolean, - val searchQuery: String, + val searchQuery: TextFieldState, val showSearchLoader: Boolean, val searchResults: SearchBarResultState>, val selectedUsers: ImmutableList, diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt index c274f877d5..15ded2ae3f 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.invitepeople.impl +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData @@ -108,7 +109,7 @@ private fun aDefaultInvitePeopleState( return DefaultInvitePeopleState( room = room, canInvite = canInvite, - searchQuery = searchQuery, + searchQuery = TextFieldState(initialText = searchQuery), searchResults = searchResults, selectedUsers = selectedUsers, isSearchActive = isSearchActive, diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt index 8e11f42655..55c5dba030 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -90,7 +91,7 @@ private fun InvitePeopleContentView( InvitePeopleSearchBar( modifier = Modifier.fillMaxWidth(), - query = state.searchQuery, + queryState = state.searchQuery, showLoader = state.showSearchLoader, selectedUsers = state.selectedUsers, state = state.searchResults, @@ -102,7 +103,6 @@ private fun InvitePeopleContentView( ) ) }, - onTextChange = { state.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery(it)) }, onToggleUser = ::toggleUser, ) @@ -149,20 +149,18 @@ private fun InvitePeopleContentView( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun InvitePeopleSearchBar( - query: String, + queryState: TextFieldState, state: SearchBarResultState>, showLoader: Boolean, selectedUsers: ImmutableList, active: Boolean, onActiveChange: (Boolean) -> Unit, - onTextChange: (String) -> Unit, onToggleUser: (MatrixUser) -> Unit, modifier: Modifier = Modifier, placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone), ) { SearchBar( - query = query, - onQueryChange = onTextChange, + queryState = queryState, active = active, onActiveChange = onActiveChange, modifier = modifier, diff --git a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt index 155282d0be..ab9e20437e 100644 --- a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt +++ b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt @@ -8,6 +8,7 @@ package io.element.android.features.invitepeople.impl +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat import io.element.android.features.invitepeople.api.InvitePeopleEvents @@ -68,7 +69,7 @@ internal class DefaultInvitePeoplePresenterTest { assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.canInvite).isFalse() - assertThat(initialState.searchQuery).isEmpty() + assertThat(initialState.searchQuery.text.toString()).isEmpty() cancelAndIgnoreRemainingEvents() } @@ -85,15 +86,15 @@ internal class DefaultInvitePeoplePresenterTest { initialState.eventSink(DefaultInvitePeopleEvents.OnSearchActiveChanged(true)) - val resultState = awaitItem() + val resultState = awaitItemAsDefault() assertThat(resultState.isSearchActive).isTrue() - resultState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) - assertThat(awaitItemAsDefault().searchQuery).isEqualTo("some query") + resultState.searchQuery.setTextAndPlaceCursorAtEnd("some query") + assertThat(awaitItemAsDefault().searchQuery.text.toString()).isEqualTo("some query") resultState.eventSink(InvitePeopleEvents.CloseSearch) skipItems(2) awaitItemAsDefault().also { assertThat(it.isSearchActive).isFalse() - assertThat(it.searchQuery).isEmpty() + assertThat(it.searchQuery.text.toString()).isEmpty() } cancelAndIgnoreRemainingEvents() } @@ -107,8 +108,8 @@ internal class DefaultInvitePeoplePresenterTest { coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) presenter.test { - val initialState = awaitItem() - initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) + val initialState = awaitItemAsDefault() + initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query") assertThat(repository.providedQuery).isEqualTo("some query") repository.emitState(UserSearchResultState(results = emptyList(), isSearching = true)) skipItems(3) @@ -132,10 +133,10 @@ internal class DefaultInvitePeoplePresenterTest { coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) presenter.test { - val initialState = awaitItem() + val initialState = awaitItemAsDefault() skipItems(1) - initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) + initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query") skipItems(1) assertThat(repository.providedQuery).isEqualTo("some query") @@ -185,10 +186,10 @@ internal class DefaultInvitePeoplePresenterTest { coroutineDispatchers = coroutineDispatchers, ) presenter.test { - val initialState = awaitItem() + val initialState = awaitItemAsDefault() skipItems(1) - initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) + initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query") skipItems(1) assertThat(repository.providedQuery).isEqualTo("some query") @@ -245,10 +246,10 @@ internal class DefaultInvitePeoplePresenterTest { ) presenter.test { - val initialState = awaitItem() + val initialState = awaitItemAsDefault() skipItems(1) - initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) + initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query") skipItems(1) assertThat(repository.providedQuery).isEqualTo("some query") @@ -312,14 +313,14 @@ internal class DefaultInvitePeoplePresenterTest { coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) presenter.test { - val initialState = awaitItem() + val initialState = awaitItemAsDefault() skipItems(1) val selectedUser = aMatrixUser() initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser)) - initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) + initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query") skipItems(1) assertThat(repository.providedQuery).isEqualTo("some query") @@ -350,13 +351,13 @@ internal class DefaultInvitePeoplePresenterTest { coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) presenter.test { - val initialState = awaitItem() + val initialState = awaitItemAsDefault() skipItems(1) val selectedUser = aMatrixUser() // Given a query is made - initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) + initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query") skipItems(1) assertThat(repository.providedQuery).isEqualTo("some query") diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt index 70d962ff49..e3bffd2137 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPicker.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.emojibasebindings.Emoji import io.element.android.features.messages.impl.timeline.components.customreaction.EmojiItem -import io.element.android.features.messages.impl.timeline.components.customreaction.icon import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toSp @@ -58,8 +57,7 @@ fun EmojiPicker( Column(modifier) { SearchBar( modifier = Modifier.padding(bottom = 10.dp), - query = state.searchQuery, - onQueryChange = { state.eventSink(EmojiPickerEvents.UpdateSearchQuery(it)) }, + queryState = state.searchQuery, resultState = state.searchResults, active = state.isSearchActive, onActiveChange = { state.eventSink(EmojiPickerEvents.ToggleSearchActive(it)) }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvents.kt index d0c4907b82..c353085eac 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerEvents.kt @@ -10,5 +10,4 @@ package io.element.android.features.messages.impl.timeline.components.customreac sealed interface EmojiPickerEvents { data class ToggleSearchActive(val isActive: Boolean) : EmojiPickerEvents - data class UpdateSearchQuery(val query: String) : EmojiPickerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt index aed568420a..060e890457 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenter.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.timeline.components.customreaction.picker +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -38,7 +39,7 @@ class EmojiPickerPresenter( ) : Presenter { @Composable override fun present(): EmojiPickerState { - var searchQuery by remember { mutableStateOf("") } + val queryState = rememberTextFieldState() var isSearchActive by remember { mutableStateOf(false) } var emojiResults by remember { mutableStateOf>>(SearchBarResultState.Initial()) } @@ -67,6 +68,7 @@ class EmojiPickerPresenter( } } + val searchQuery = queryState.text.toString() LaunchedEffect(searchQuery) { emojiResults = if (searchQuery.isEmpty()) { SearchBarResultState.Initial() @@ -97,14 +99,13 @@ class EmojiPickerPresenter( is EmojiPickerEvents.ToggleSearchActive -> if (!isInPreview) { isSearchActive = event.isActive } - is EmojiPickerEvents.UpdateSearchQuery -> searchQuery = event.query } } return EmojiPickerState( categories = categories, allEmojis = emojibaseStore.allEmojis, - searchQuery = searchQuery, + searchQuery = queryState, isSearchActive = isSearchActive, searchResults = emojiResults, eventSink = ::handleEvent, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt index b050c346a5..a2ea66a9fb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerState.kt @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.timeline.components.customreaction.picker import androidx.annotation.StringRes +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Immutable import io.element.android.emojibasebindings.Emoji import io.element.android.libraries.designsystem.theme.components.IconSource @@ -20,7 +21,7 @@ import kotlinx.collections.immutable.ImmutableList data class EmojiPickerState( val categories: ImmutableList, val allEmojis: ImmutableList, - val searchQuery: String, + val searchQuery: TextFieldState, val isSearchActive: Boolean, val searchResults: SearchBarResultState>, val eventSink: (EmojiPickerEvents) -> Unit, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.kt index e26941e172..b9de6166a6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.timeline.components.customreaction.picker +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.EmojibaseCategory @@ -76,7 +77,7 @@ internal fun anEmojiPickerState( ) = EmojiPickerState( categories = categories, allEmojis = allEmojis, - searchQuery = searchQuery, + searchQuery = TextFieldState(initialText = searchQuery), isSearchActive = isSearchActive, searchResults = searchResults, eventSink = eventSink, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt index aa177b07e2..473abedcfc 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.timeline.components.customreaction.picker +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.currentComposer import androidx.compose.ui.platform.LocalConfiguration @@ -40,19 +41,19 @@ class EmojiPickerPresenterTest { val warmUpRule = WarmUpRule() @Test - fun `UpdateSearchQuery loads new results`() = runTest { + fun `updating search query loads new results`() = runTest { testPresenter { skipItems(1) val initialState = awaitItem() - assertThat(initialState.searchQuery).isEmpty() + assertThat(initialState.searchQuery.text.toString()).isEmpty() assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) - initialState.eventSink(EmojiPickerEvents.UpdateSearchQuery("smile")) - assertThat(awaitItem().searchQuery).isEqualTo("smile") + initialState.searchQuery.setTextAndPlaceCursorAtEnd("smile") + assertThat(awaitItem().searchQuery.text.toString()).isEqualTo("smile") val stateWithResults = awaitItem() - assertThat(stateWithResults.searchQuery).isEqualTo("smile") + assertThat(stateWithResults.searchQuery.text.toString()).isEqualTo("smile") assertThat(stateWithResults.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) } } diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesEvent.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesEvent.kt index 2867273e80..49aa7373bd 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesEvent.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesEvent.kt @@ -12,7 +12,6 @@ import io.element.android.libraries.matrix.api.user.MatrixUser sealed interface ChangeRolesEvent { data object ToggleSearchActive : ChangeRolesEvent - data class QueryChanged(val query: String?) : ChangeRolesEvent data class UserSelectionToggled(val matrixUser: MatrixUser) : ChangeRolesEvent data object Save : ChangeRolesEvent data object Exit : ChangeRolesEvent diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesPresenter.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesPresenter.kt index 3989a76df3..d2c9175f38 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesPresenter.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesPresenter.kt @@ -8,6 +8,7 @@ package io.element.android.features.rolesandpermissions.impl.roles +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -68,7 +69,7 @@ class ChangeRolesPresenter( @Composable override fun present(): ChangeRolesState { - var query by rememberSaveable { mutableStateOf(null) } + val queryState = rememberTextFieldState() var searchActive by rememberSaveable { mutableStateOf(false) } var searchResults by remember { mutableStateOf>(SearchBarResultState.Initial()) @@ -105,9 +106,10 @@ class ChangeRolesPresenter( val roomMemberState by room.membersStateFlow.collectAsState() // Update search results for every query change + val query = queryState.text.toString() LaunchedEffect(query, roomMemberState) { val results = dataSource - .search(query.orEmpty()) + .search(query) .groupedByRole() searchResults = if (results.isEmpty()) { @@ -136,9 +138,6 @@ class ChangeRolesPresenter( is ChangeRolesEvent.ToggleSearchActive -> { searchActive = !searchActive } - is ChangeRolesEvent.QueryChanged -> { - query = event.query - } is ChangeRolesEvent.UserSelectionToggled -> { val newList = selectedUsers.value.toMutableList() val index = newList.indexOfFirst { it.userId == event.matrixUser.userId } @@ -188,7 +187,7 @@ class ChangeRolesPresenter( } return ChangeRolesState( role = role, - query = query, + searchQuery = queryState, isSearchActive = searchActive, searchResults = searchResults, selectedUsers = selectedUsers.value, diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesState.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesState.kt index 71fef01fa7..173db04524 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesState.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesState.kt @@ -8,6 +8,7 @@ package io.element.android.features.rolesandpermissions.impl.roles +import androidx.compose.foundation.text.input.TextFieldState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.UserId @@ -19,7 +20,7 @@ import kotlinx.collections.immutable.toImmutableList data class ChangeRolesState( val role: RoomMember.Role, - val query: String?, + val searchQuery: TextFieldState, val isSearchActive: Boolean, val searchResults: SearchBarResultState, val selectedUsers: ImmutableList, diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStateProvider.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStateProvider.kt index 654259c02a..ebaf619d4e 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStateProvider.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.rolesandpermissions.impl.roles +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState @@ -34,8 +35,8 @@ class ChangeRolesStateProvider : PreviewParameterProvider { aChangeRolesStateWithSelectedUsers().copy( selectedUsers = aMatrixUserList().take(2).toImmutableList(), ), - aChangeRolesStateWithSelectedUsers().copy( - query = "Alice", + aChangeRolesState( + searchQuery = "Alice", isSearchActive = true, searchResults = SearchBarResultState.Results( MembersByRole( @@ -44,6 +45,8 @@ class ChangeRolesStateProvider : PreviewParameterProvider { ) ), selectedUsers = aMatrixUserList().take(1).toImmutableList(), + hasPendingChanges = true, + canRemoveMember = { it != UserId("@alice:server.org") }, ), aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.ConfirmingCancellation), aChangeRolesStateWithSelectedUsers().copy(savingState = ConfirmingModifyingAdmins), @@ -59,7 +62,7 @@ class ChangeRolesStateProvider : PreviewParameterProvider { internal fun aChangeRolesState( role: RoomMember.Role = RoomMember.Role.Admin, - query: String? = null, + searchQuery: String = "", isSearchActive: Boolean = false, searchResults: SearchBarResultState = SearchBarResultState.NoResultsFound(), selectedUsers: ImmutableList = persistentListOf(), @@ -69,7 +72,7 @@ internal fun aChangeRolesState( eventSink: (ChangeRolesEvent) -> Unit = {}, ) = ChangeRolesState( role = role, - query = query, + searchQuery = TextFieldState(initialText = searchQuery), isSearchActive = isSearchActive, searchResults = searchResults, selectedUsers = selectedUsers, diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesView.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesView.kt index 20abc70bb7..b2c88efede 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesView.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesView.kt @@ -118,8 +118,7 @@ fun ChangeRolesView( .fillMaxWidth() .padding(bottom = 16.dp), placeHolderTitle = stringResource(CommonStrings.common_search_for_someone), - query = state.query.orEmpty(), - onQueryChange = { state.eventSink(ChangeRolesEvent.QueryChanged(it)) }, + queryState = state.searchQuery, active = state.isSearchActive, onActiveChange = { state.eventSink(ChangeRolesEvent.ToggleSearchActive) }, resultState = state.searchResults, diff --git a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesPresenterTest.kt b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesPresenterTest.kt index 8747585354..4cde70b3bc 100644 --- a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesPresenterTest.kt +++ b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesPresenterTest.kt @@ -8,6 +8,7 @@ package io.element.android.features.rolesandpermissions.impl.roles +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.RoomModeration import io.element.android.features.rolesandpermissions.impl.RoomMemberListDataSource @@ -49,7 +50,7 @@ class ChangeRolesPresenterTest { presenter.test { with(awaitItem()) { assertThat(role).isEqualTo(RoomMember.Role.Admin) - assertThat(query).isNull() + assertThat(searchQuery.text.toString()).isEmpty() assertThat(isSearchActive).isFalse() assertThat(searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(selectedUsers).isEmpty() @@ -206,7 +207,7 @@ class ChangeRolesPresenterTest { } @Test - fun `present - QueryChanged produces new results`() = runTest { + fun `present - updating query produces new results`() = runTest { val room = FakeJoinedRoom().apply { givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) } @@ -219,7 +220,7 @@ class ChangeRolesPresenterTest { assertThat(initialResults?.moderators).hasSize(1) assertThat(initialResults?.admins).hasSize(1) - initialState.eventSink(ChangeRolesEvent.QueryChanged("Alice")) + initialState.searchQuery.setTextAndPlaceCursorAtEnd("Alice") skipItems(1) val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results diff --git a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesViewTest.kt b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesViewTest.kt index 39967f9160..af14d5be55 100644 --- a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesViewTest.kt +++ b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesViewTest.kt @@ -76,7 +76,7 @@ class ChangeRolesViewTest { ), ) rule.pressBackKey() - eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""), ChangeRolesEvent.Exit)) + eventsRecorder.assertSingle(ChangeRolesEvent.Exit) } @Test @@ -89,7 +89,7 @@ class ChangeRolesViewTest { ), ) rule.pressBack() - eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""), ChangeRolesEvent.Exit)) + eventsRecorder.assertSingle(ChangeRolesEvent.Exit) } @Test @@ -102,7 +102,7 @@ class ChangeRolesViewTest { ), ) rule.clickOn(CommonStrings.action_save) - eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""), ChangeRolesEvent.Save)) + eventsRecorder.assertSingle(ChangeRolesEvent.Save) } @Test @@ -115,7 +115,7 @@ class ChangeRolesViewTest { ), ) rule.clickOn(CommonStrings.action_save) - eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""))) + eventsRecorder.assertEmpty() } @Test @@ -238,12 +238,7 @@ class ChangeRolesViewTest { label = contentDescription, useUnmergedTree = true, ).performClick() - eventsRecorder.assertList( - listOf( - ChangeRolesEvent.QueryChanged(""), - ChangeRolesEvent.UserSelectionToggled(userToDeselect), - ) - ) + eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToDeselect)) } @Test @@ -262,12 +257,7 @@ class ChangeRolesViewTest { ) // Select the user from the user list rule.onNodeWithText("Carol").performClick() - eventsRecorder.assertList( - listOf( - ChangeRolesEvent.QueryChanged(""), - ChangeRolesEvent.UserSelectionToggled(userToSelect), - ) - ) + eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect)) } @Test @@ -288,12 +278,7 @@ class ChangeRolesViewTest { text = "Bob", useUnmergedTree = true, )[1].performClick() - eventsRecorder.assertList( - listOf( - ChangeRolesEvent.QueryChanged(""), - ChangeRolesEvent.UserSelectionToggled(userToSelect), - ) - ) + eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect)) } private fun AndroidComposeTestRule.setChangeRolesContent( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt index ec1b130b3e..f33bb2df00 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt @@ -12,6 +12,5 @@ import io.element.android.libraries.matrix.api.room.RoomMember sealed interface RoomMemberListEvents { data class ChangeSelectedSection(val section: SelectedSection) : RoomMemberListEvents - data class UpdateSearchQuery(val query: String) : RoomMemberListEvents data class RoomMemberSelected(val roomMember: RoomMember) : RoomMemberListEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 6917057a06..7ad98a373d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -8,6 +8,7 @@ package io.element.android.features.roomdetails.impl.members +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect @@ -16,7 +17,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents.ShowActionsForUser @@ -56,7 +56,7 @@ class RoomMemberListPresenter( @Composable override fun present(): RoomMemberListState { - var searchQuery by rememberSaveable { mutableStateOf("") } + val searchQuery = rememberTextFieldState() val membersState by room.membersStateFlow.collectAsState() val canInvite by room.permissionsAsState(false) { perms -> perms.canOwnUserInvite() } val roomModerationState = roomMembersModerationPresenter.present() @@ -117,17 +117,16 @@ class RoomMemberListPresenter( } } - LaunchedEffect(searchQuery, roomMembers) { + LaunchedEffect(searchQuery.text, roomMembers) { filteredRoomMembers = roomMembers.map { members -> withContext(coroutineDispatchers.io) { - members.filter(searchQuery) + members.filter(searchQuery.text.toString()) } } } fun handleEvent(event: RoomMemberListEvents) { when (event) { - is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query is RoomMemberListEvents.RoomMemberSelected -> roomModerationState.eventSink(ShowActionsForUser(event.roomMember.toMatrixUser())) is RoomMemberListEvents.ChangeSelectedSection -> selectedSection = event.section diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt index 7c928fb27a..3cc795a8db 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -8,6 +8,7 @@ package io.element.android.features.roomdetails.impl.members +import androidx.compose.foundation.text.input.TextFieldState import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.bool.orFalse @@ -20,7 +21,7 @@ data class RoomMemberListState( // Only used to know if we can show the banned section private val roomMembers: AsyncData, val filteredRoomMembers: AsyncData, - val searchQuery: String, + val searchQuery: TextFieldState, val canInvite: Boolean, val selectedSection: SelectedSection, val moderationState: RoomMemberModerationState, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index b37738c30f..63dfec56e5 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.roomdetails.impl.members +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions @@ -88,7 +89,7 @@ internal fun aRoomMemberListState( ) = RoomMemberListState( roomMembers = roomMembers, filteredRoomMembers = roomMembers.map { it.filter(searchQuery) }, - searchQuery = searchQuery, + searchQuery = TextFieldState(searchQuery), canInvite = canInvite, moderationState = moderationState, selectedSection = selectedSection, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index 7c83f74f00..bc37bbbe74 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -42,7 +42,6 @@ import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubti import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon @@ -89,13 +88,8 @@ fun RoomMemberListView( .consumeWindowInsets(padding), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - var searchQuery by textFieldState(state.searchQuery) SearchField( - value = searchQuery, - onValueChange = { newQuery -> - searchQuery = newQuery - state.eventSink(RoomMemberListEvents.UpdateSearchQuery(newQuery)) - }, + state = state.searchQuery, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), @@ -105,7 +99,7 @@ fun RoomMemberListView( roomMembersData = state.filteredRoomMembers, selectedSection = state.selectedSection, showBannedSection = state.showBannedSection, - searchQuery = state.searchQuery, + searchQuery = state.searchQuery.text.toString(), onSelectedSectionChange = { state.eventSink(RoomMemberListEvents.ChangeSelectedSection(it)) }, onSelectUser = ::onSelectUser, ) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt index f1cab1524a..028d4b2781 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenterTest.kt @@ -8,6 +8,7 @@ package io.element.android.features.roomdetails.impl.members +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import com.google.common.truth.Truth.assertThat import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.Presenter @@ -42,7 +43,7 @@ class RoomMemberListPresenterTest { skipItems(1) val initialState = awaitItem() assertThat(initialState.filteredRoomMembers.isLoading()).isTrue() - assertThat(initialState.searchQuery).isEmpty() + assertThat(initialState.searchQuery.text.toString()).isEmpty() assertThat(initialState.selectedSection).isEqualTo(SelectedSection.MEMBERS) } } @@ -87,7 +88,7 @@ class RoomMemberListPresenterTest { skipItems(1) val initialState = awaitItem() assertThat(initialState.filteredRoomMembers.isLoading()).isTrue() - assertThat(initialState.searchQuery).isEmpty() + assertThat(initialState.searchQuery.text.toString()).isEmpty() room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) // Skip items while the new members state is processed skipItems(2) @@ -116,9 +117,9 @@ class RoomMemberListPresenterTest { assertThat(loadedRoomMembers.invited).isNotEmpty() assertThat(loadedRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse() assertThat(loadedRoomMembers.isEmpty(SelectedSection.BANNED)).isFalse() - loadedState.eventSink(RoomMemberListEvents.UpdateSearchQuery("something")) + loadedState.searchQuery.setTextAndPlaceCursorAtEnd("something") val searchQueryUpdatedState = awaitItem() - assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("something") + assertThat(searchQueryUpdatedState.searchQuery.text).isEqualTo("something") val searchSearchResultDelivered = awaitItem() val emptyRoomMembers = searchSearchResultDelivered.filteredRoomMembers.dataOrNull()!! assertThat(emptyRoomMembers.joined).isEmpty() @@ -144,9 +145,9 @@ class RoomMemberListPresenterTest { assertThat(loadedRoomMembers.invited).isNotEmpty() assertThat(loadedRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse() assertThat(loadedRoomMembers.isEmpty(SelectedSection.BANNED)).isFalse() - loadedState.eventSink(RoomMemberListEvents.UpdateSearchQuery("alice")) + loadedState.searchQuery.setTextAndPlaceCursorAtEnd("alice") val searchQueryUpdatedState = awaitItem() - assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("alice") + assertThat(searchQueryUpdatedState.searchQuery.text).isEqualTo("alice") val searchSearchResultDelivered = awaitItem() val emptyRoomMembers = searchSearchResultDelivered.filteredRoomMembers.dataOrNull()!! assertThat(emptyRoomMembers.joined).isNotEmpty() diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt index eb3f2d0829..ee4dc12ba4 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt @@ -11,9 +11,7 @@ import io.element.android.libraries.matrix.ui.model.SelectRoomInfo sealed interface AddRoomToSpaceEvent { data class ToggleRoom(val room: SelectRoomInfo) : AddRoomToSpaceEvent - data class UpdateSearchQuery(val query: String) : AddRoomToSpaceEvent data class OnSearchActiveChanged(val active: Boolean) : AddRoomToSpaceEvent data object Save : AddRoomToSpaceEvent - data object CloseSearch : AddRoomToSpaceEvent data object ResetSaveAction : AddRoomToSpaceEvent } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt index 25fa421d1f..657d0702f8 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt @@ -7,6 +7,8 @@ package io.element.android.features.space.impl.addroom +import androidx.compose.foundation.text.input.clearText +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -43,7 +45,7 @@ class AddRoomToSpacePresenter( @Composable override fun present(): AddRoomToSpaceState { var selectedRooms: ImmutableList by remember { mutableStateOf(persistentListOf()) } - var searchQuery by remember { mutableStateOf("") } + var searchQuery = rememberTextFieldState() var isSearchActive by remember { mutableStateOf(false) } val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } @@ -51,8 +53,8 @@ class AddRoomToSpacePresenter( val dataSource = remember { dataSourceFactory.create(coroutineScope) } // Update search query in data source - LaunchedEffect(searchQuery) { - dataSource.setSearchQuery(searchQuery) + LaunchedEffect(searchQuery.text) { + dataSource.setSearchQuery(searchQuery.text.toString()) } LaunchedEffect(isSearchActive) { dataSource.setIsActive(isSearchActive) @@ -65,7 +67,7 @@ class AddRoomToSpacePresenter( derivedStateOf { when { filteredRooms.isNotEmpty() -> SearchBarResultState.Results(filteredRooms) - isSearchActive && searchQuery.isNotEmpty() -> SearchBarResultState.NoResultsFound() + isSearchActive && searchQuery.text.isNotEmpty() -> SearchBarResultState.NoResultsFound() else -> SearchBarResultState.Initial() } } @@ -80,19 +82,12 @@ class AddRoomToSpacePresenter( (selectedRooms + event.room).toImmutableList() } } - is AddRoomToSpaceEvent.UpdateSearchQuery -> { - searchQuery = event.query - } is AddRoomToSpaceEvent.OnSearchActiveChanged -> { isSearchActive = event.active if (!event.active) { - searchQuery = "" + searchQuery.clearText() } } - AddRoomToSpaceEvent.CloseSearch -> { - isSearchActive = false - searchQuery = "" - } AddRoomToSpaceEvent.Save -> { coroutineScope.addRoomsToSpace( selectedRooms = selectedRooms, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt index 8d7ff0ce6b..99526be25f 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt @@ -7,13 +7,14 @@ package io.element.android.features.space.impl.addroom +import androidx.compose.foundation.text.input.TextFieldState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.ui.model.SelectRoomInfo import kotlinx.collections.immutable.ImmutableList data class AddRoomToSpaceState( - val searchQuery: String, + val searchQuery: TextFieldState, val isSearchActive: Boolean, val searchResults: SearchBarResultState>, val selectedRooms: ImmutableList, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt index 640e7ede7d..870b72ecb7 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt @@ -7,6 +7,7 @@ package io.element.android.features.space.impl.addroom +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState @@ -69,7 +70,7 @@ internal fun anAddRoomToSpaceState( eventSink: (AddRoomToSpaceEvent) -> Unit = {}, ): AddRoomToSpaceState { return AddRoomToSpaceState( - searchQuery = searchQuery, + searchQuery = TextFieldState(searchQuery), searchResults = searchResults, selectedRooms = selectedRooms, isSearchActive = isSearchActive, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt index c3696dcac0..854924041b 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt @@ -64,7 +64,7 @@ fun AddRoomToSpaceView( fun onBack() { if (state.isSearchActive) { - state.eventSink(AddRoomToSpaceEvent.CloseSearch) + state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(false)) } else { onBackClick() } @@ -105,12 +105,11 @@ fun AddRoomToSpaceView( SearchBar( modifier = Modifier.fillMaxWidth(), placeHolderTitle = stringResource(CommonStrings.action_search), - query = state.searchQuery, - onQueryChange = { state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery(it)) }, + queryState = state.searchQuery, active = state.isSearchActive, onActiveChange = { state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(it)) }, - resultState = state.searchResults, showBackButton = false, + resultState = state.searchResults, contentPrefix = { if (state.selectedRooms.isNotEmpty()) { SelectedRoomsRow( diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt index c0b39c6a69..b9a67704c9 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenterTest.kt @@ -9,6 +9,7 @@ package io.element.android.features.space.impl.addroom +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import com.google.common.truth.Truth.assertThat import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState @@ -38,7 +39,7 @@ class AddRoomToSpacePresenterTest { presenter.test { val state = awaitItem() assertThat(state.selectedRooms).isEmpty() - assertThat(state.searchQuery).isEmpty() + assertThat(state.searchQuery.text.toString()).isEmpty() assertThat(state.isSearchActive).isFalse() assertThat(state.saveAction).isEqualTo(AsyncAction.Uninitialized) assertThat(state.canSave).isFalse() @@ -77,17 +78,6 @@ class AddRoomToSpacePresenterTest { } } - @Test - fun `present - UpdateSearchQuery updates query`() = runTest { - val presenter = createAddRoomToSpacePresenter() - presenter.test { - val state = awaitItem() - state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("test")) - val updatedState = awaitItem() - assertThat(updatedState.searchQuery).isEqualTo("test") - } - } - @Test fun `present - OnSearchActiveChanged activates search`() = runTest { val presenter = createAddRoomToSpacePresenter() @@ -107,33 +97,14 @@ class AddRoomToSpacePresenterTest { // Activate search and set query state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true)) awaitItem() - state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("test")) + state.searchQuery.setTextAndPlaceCursorAtEnd("test") awaitItem() // Deactivate search state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(false)) advanceUntilIdle() val finalState = expectMostRecentItem() assertThat(finalState.isSearchActive).isFalse() - assertThat(finalState.searchQuery).isEmpty() - } - } - - @Test - fun `present - CloseSearch deactivates and clears query`() = runTest { - val presenter = createAddRoomToSpacePresenter() - presenter.test { - val state = awaitItem() - // Activate search and set query - state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true)) - awaitItem() - state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("test")) - awaitItem() - // Close search - state.eventSink(AddRoomToSpaceEvent.CloseSearch) - advanceUntilIdle() - val finalState = expectMostRecentItem() - assertThat(finalState.isSearchActive).isFalse() - assertThat(finalState.searchQuery).isEmpty() + assertThat(finalState.searchQuery.text.toString()).isEmpty() } } @@ -168,11 +139,11 @@ class AddRoomToSpacePresenterTest { val state = awaitItem() state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true)) awaitItem() - state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("nonexistent")) + state.searchQuery.setTextAndPlaceCursorAtEnd("nonexistent") advanceUntilIdle() val finalState = expectMostRecentItem() assertThat(finalState.isSearchActive).isTrue() - assertThat(finalState.searchQuery).isEqualTo("nonexistent") + assertThat(finalState.searchQuery.text).isEqualTo("nonexistent") assertThat(finalState.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) } } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt index db322c3687..d6a9ff770a 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt @@ -54,7 +54,7 @@ class AddRoomToSpaceViewTest { ), ) rule.pressBack() - eventsRecorder.assertSingle(AddRoomToSpaceEvent.CloseSearch) + eventsRecorder.assertSingle(AddRoomToSpaceEvent.OnSearchActiveChanged(false)) } @Test @@ -67,12 +67,7 @@ class AddRoomToSpaceViewTest { ), ) rule.clickOn(CommonStrings.action_save) - eventsRecorder.assertList( - listOf( - AddRoomToSpaceEvent.UpdateSearchQuery(""), // SearchBar initialization - AddRoomToSpaceEvent.Save, - ) - ) + eventsRecorder.assertSingle(AddRoomToSpaceEvent.Save) } @Config(qualifiers = "h1024dp") @@ -87,12 +82,7 @@ class AddRoomToSpaceViewTest { ), ) rule.onNodeWithText(suggestions.first().name!!).performClick() - eventsRecorder.assertList( - listOf( - AddRoomToSpaceEvent.UpdateSearchQuery(""), // SearchBar initialization - AddRoomToSpaceEvent.ToggleRoom(suggestions.first()), - ) - ) + eventsRecorder.assertSingle(AddRoomToSpaceEvent.ToggleRoom(suggestions.first())) } @Test diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/SearchUserBar.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/SearchUserBar.kt index a664ad0912..c8284c6a4f 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/SearchUserBar.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/SearchUserBar.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.surfaceColorAtElevation @@ -40,14 +41,13 @@ import kotlinx.collections.immutable.ImmutableList @OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchUserBar( - query: String, - state: SearchBarResultState>, + queryState: TextFieldState, + resultState: SearchBarResultState>, showLoader: Boolean, selectedUsers: ImmutableList, active: Boolean, isMultiSelectionEnable: Boolean, onActiveChange: (Boolean) -> Unit, - onTextChange: (String) -> Unit, onUserSelect: (MatrixUser) -> Unit, onUserDeselect: (MatrixUser) -> Unit, modifier: Modifier = Modifier, @@ -57,8 +57,7 @@ fun SearchUserBar( val columnState = rememberLazyListState() SearchBar( - query = query, - onQueryChange = onTextChange, + queryState = queryState, active = active, onActiveChange = onActiveChange, modifier = modifier, @@ -98,7 +97,7 @@ fun SearchUserBar( AsyncLoading() } }, - resultState = state, + resultState = resultState, resultHandler = { users -> LazyColumn(state = columnState) { if (isMultiSelectionEnable) { diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/UserListView.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/UserListView.kt index f35c6282ed..cab1c57015 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/UserListView.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/UserListView.kt @@ -46,15 +46,14 @@ fun UserListView( ) { SearchUserBar( modifier = Modifier.fillMaxWidth(), - query = state.searchQuery, - state = state.searchResults, + queryState = state.searchQuery, + resultState = state.searchResults, selectedUsers = state.selectedUsers, active = state.isSearchActive, showLoader = state.showSearchLoader, isMultiSelectionEnable = state.isMultiSelectionEnabled, showBackButton = showBackButton, onActiveChange = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) }, - onTextChange = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) }, onUserSelect = { state.eventSink(UserListEvents.AddToSelection(it)) onSelectUser(it) diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt index 1c82ae373e..448ad1a80a 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt @@ -27,10 +27,10 @@ open class StartChatStateProvider : PreviewParameterProvider { aCreateRoomRootState( startDmAction = AsyncAction.Loading, userListState = aMatrixUser().let { - aUserListState().copy( + aUserListState( searchQuery = it.userId.value, searchResults = SearchBarResultState.Results(persistentListOf(UserSearchResult(it, false))), - selectedUsers = persistentListOf(it), + selectedUsers = listOf(it), isSearchActive = true, ) } @@ -38,10 +38,10 @@ open class StartChatStateProvider : PreviewParameterProvider { aCreateRoomRootState( startDmAction = AsyncAction.Failure(RuntimeException("error")), userListState = aMatrixUser().let { - aUserListState().copy( + aUserListState( searchQuery = it.userId.value, searchResults = SearchBarResultState.Results(persistentListOf(UserSearchResult(it, false))), - selectedUsers = persistentListOf(it), + selectedUsers = listOf(it), isSearchActive = true, ) } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt index 4bef68c757..ce579c5b50 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt @@ -8,6 +8,7 @@ package io.element.android.features.startchat.impl.userlist +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -64,12 +65,13 @@ class DefaultUserListPresenter( } var isSearchActive by rememberSaveable { mutableStateOf(false) } val selectedUsers by userListDataStore.selectedUsers.collectAsState(emptyList()) - var searchQuery by rememberSaveable { mutableStateOf("") } + val queryState = rememberTextFieldState() var searchResults: SearchBarResultState> by remember { mutableStateOf(SearchBarResultState.Initial()) } var showSearchLoader by remember { mutableStateOf(false) } + val searchQuery = queryState.text.toString() LaunchedEffect(searchQuery) { searchResults = SearchBarResultState.Initial() showSearchLoader = false @@ -86,14 +88,13 @@ class DefaultUserListPresenter( fun handleEvent(event: UserListEvents) { when (event) { is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active - is UserListEvents.UpdateSearchQuery -> searchQuery = event.query is UserListEvents.AddToSelection -> userListDataStore.selectUser(event.matrixUser) is UserListEvents.RemoveFromSelection -> userListDataStore.removeUserFromSelection(event.matrixUser) } } return UserListState( - searchQuery = searchQuery, + searchQuery = queryState, searchResults = searchResults, selectedUsers = selectedUsers.toImmutableList(), isSearchActive = isSearchActive, diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListEvents.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListEvents.kt index 99e910b89f..f7601f6d36 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListEvents.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListEvents.kt @@ -11,7 +11,6 @@ package io.element.android.features.startchat.impl.userlist import io.element.android.libraries.matrix.api.user.MatrixUser sealed interface UserListEvents { - data class UpdateSearchQuery(val query: String) : UserListEvents data class AddToSelection(val matrixUser: MatrixUser) : UserListEvents data class RemoveFromSelection(val matrixUser: MatrixUser) : UserListEvents data class OnSearchActiveChanged(val active: Boolean) : UserListEvents diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListState.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListState.kt index 33b74d240c..6a507f8a75 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListState.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListState.kt @@ -8,6 +8,7 @@ package io.element.android.features.startchat.impl.userlist +import androidx.compose.foundation.text.input.TextFieldState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom import io.element.android.libraries.matrix.api.user.MatrixUser @@ -15,7 +16,7 @@ import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.ImmutableList data class UserListState( - val searchQuery: String, + val searchQuery: TextFieldState, val searchResults: SearchBarResultState>, val showSearchLoader: Boolean, val selectedUsers: ImmutableList, diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListStateProvider.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListStateProvider.kt index cd43f96688..3da8bfdabe 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListStateProvider.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.startchat.impl.userlist +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.RoomId @@ -69,7 +70,7 @@ fun aUserListState( recentDirectRooms: List = emptyList(), eventSink: (UserListEvents) -> Unit = {}, ) = UserListState( - searchQuery = searchQuery, + searchQuery = TextFieldState(initialText = searchQuery), isSearchActive = isSearchActive, searchResults = searchResults, selectedUsers = selectedUsers.toImmutableList(), diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenterTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenterTest.kt index 772392bf92..4d958011a7 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenterTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenterTest.kt @@ -8,6 +8,7 @@ package io.element.android.features.startchat.impl.userlist +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import com.google.common.truth.Truth.assertThat import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.test.FakeMatrixClient @@ -41,7 +42,7 @@ class DefaultUserListPresenterTest { presenter.test { skipItems(1) val initialState = awaitItem() - assertThat(initialState.searchQuery).isEmpty() + assertThat(initialState.searchQuery.text.toString()).isEmpty() assertThat(initialState.isMultiSelectionEnabled).isFalse() assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.selectedUsers).isEmpty() @@ -61,7 +62,7 @@ class DefaultUserListPresenterTest { presenter.test { skipItems(1) val initialState = awaitItem() - assertThat(initialState.searchQuery).isEmpty() + assertThat(initialState.searchQuery.text.toString()).isEmpty() assertThat(initialState.isMultiSelectionEnabled).isTrue() assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.selectedUsers).isEmpty() @@ -86,14 +87,14 @@ class DefaultUserListPresenterTest { assertThat(awaitItem().isSearchActive).isTrue() val matrixIdQuery = "@name:matrix.org" - initialState.eventSink(UserListEvents.UpdateSearchQuery(matrixIdQuery)) - assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery) + initialState.searchQuery.setTextAndPlaceCursorAtEnd(matrixIdQuery) + assertThat(awaitItem().searchQuery.text.toString()).isEqualTo(matrixIdQuery) assertThat(userRepository.providedQuery).isEqualTo(matrixIdQuery) skipItems(1) val notMatrixIdQuery = "name" - initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery)) - assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery) + initialState.searchQuery.setTextAndPlaceCursorAtEnd(notMatrixIdQuery) + assertThat(awaitItem().searchQuery.text.toString()).isEqualTo(notMatrixIdQuery) assertThat(userRepository.providedQuery).isEqualTo(notMatrixIdQuery) skipItems(1) @@ -117,7 +118,7 @@ class DefaultUserListPresenterTest { skipItems(1) val initialState = awaitItem() - initialState.eventSink(UserListEvents.UpdateSearchQuery("alice")) + initialState.searchQuery.setTextAndPlaceCursorAtEnd("alice") assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(userRepository.providedQuery).isEqualTo("alice") skipItems(2) @@ -168,7 +169,7 @@ class DefaultUserListPresenterTest { skipItems(1) val initialState = awaitItem() - initialState.eventSink(UserListEvents.UpdateSearchQuery("alice")) + initialState.searchQuery.setTextAndPlaceCursorAtEnd("alice") assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(userRepository.providedQuery).isEqualTo("alice") skipItems(2) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt index c1fd7bc6f0..2e19fecced 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt @@ -17,6 +17,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarColors @@ -25,9 +28,7 @@ import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape @@ -51,8 +52,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchBar( - query: String, - onQueryChange: (String) -> Unit, + queryState: TextFieldState, active: Boolean, onActiveChange: (Boolean) -> Unit, placeHolderTitle: String, @@ -72,10 +72,9 @@ fun SearchBar( ) { val focusManager = LocalFocusManager.current val colors = if (active) activeBarColors else inactiveBarColors - val updatedOnQueryChange by rememberUpdatedState(onQueryChange) LaunchedEffect(active) { if (!active) { - updatedOnQueryChange("") + queryState.clearText() focusManager.clearFocus() } } @@ -83,8 +82,7 @@ fun SearchBar( SearchBar( inputField = { SearchBarDefaults.InputField( - query = query, - onQueryChange = updatedOnQueryChange, + state = queryState, onSearch = { focusManager.clearFocus() }, expanded = active, onExpandedChange = onActiveChange, @@ -98,9 +96,9 @@ fun SearchBar( null }, trailingIcon = when { - active && query.isNotEmpty() -> { + active && queryState.text.isNotEmpty() -> { { - IconButton(onClick = { onQueryChange("") }) { + IconButton(onClick = { queryState.clearText() }) { Icon( imageVector = CompoundIcons.Close(), contentDescription = stringResource(CommonStrings.action_clear), @@ -221,7 +219,7 @@ internal fun SearchBarInactivePreview() = ElementThemedPreview { ContentToPrevie @Composable internal fun SearchBarActiveNoneQueryPreview() = ElementThemedPreview { ContentToPreview( - query = "", + initialQuery = "", active = true, ) } @@ -230,7 +228,7 @@ internal fun SearchBarActiveNoneQueryPreview() = ElementThemedPreview { @Composable internal fun SearchBarActiveWithQueryPreview() = ElementThemedPreview { ContentToPreview( - query = "search term", + initialQuery = "search term", active = true, ) } @@ -239,7 +237,7 @@ internal fun SearchBarActiveWithQueryPreview() = ElementThemedPreview { @Composable internal fun SearchBarActiveWithQueryNoBackButtonPreview() = ElementThemedPreview { ContentToPreview( - query = "search term", + initialQuery = "search term", active = true, showBackButton = false, ) @@ -249,7 +247,7 @@ internal fun SearchBarActiveWithQueryNoBackButtonPreview() = ElementThemedPrevie @Composable internal fun SearchBarActiveWithNoResultsPreview() = ElementThemedPreview { ContentToPreview( - query = "search term", + initialQuery = "search term", active = true, resultState = SearchBarResultState.NoResultsFound(), ) @@ -259,7 +257,7 @@ internal fun SearchBarActiveWithNoResultsPreview() = ElementThemedPreview { @Composable internal fun SearchBarActiveWithContentPreview() = ElementThemedPreview { ContentToPreview( - query = "search term", + initialQuery = "search term", active = true, resultState = SearchBarResultState.Results("result!"), contentPrefix = { @@ -292,7 +290,7 @@ internal fun SearchBarActiveWithContentPreview() = ElementThemedPreview { @Composable @ExcludeFromCoverage private fun ContentToPreview( - query: String = "", + initialQuery: String = "", active: Boolean = false, showBackButton: Boolean = true, resultState: SearchBarResultState = SearchBarResultState.Initial(), @@ -300,13 +298,13 @@ private fun ContentToPreview( contentSuffix: @Composable ColumnScope.() -> Unit = {}, resultHandler: @Composable ColumnScope.(String) -> Unit = {}, ) { + val queryState = rememberTextFieldState(initialText = initialQuery) SearchBar( modifier = Modifier.heightIn(max = 200.dp), - query = query, + queryState = queryState, active = active, resultState = resultState, showBackButton = showBackButton, - onQueryChange = {}, onActiveChange = {}, placeHolderTitle = "Search for things", contentPrefix = contentPrefix, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchField.kt index fc84abe827..d677a89577 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchField.kt @@ -22,8 +22,10 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -34,7 +36,6 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -50,8 +51,7 @@ import io.element.android.libraries.ui.strings.CommonStrings */ @Composable fun SearchField( - value: String, - onValueChange: (String) -> Unit, + state: TextFieldState, modifier: Modifier = Modifier, placeholder: String? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, @@ -59,67 +59,28 @@ fun SearchField( val focusManager = LocalFocusManager.current val isFocused by interactionSource.collectIsFocusedAsState() BasicTextField( - value = value, - onValueChange = onValueChange, + state = state, modifier = modifier, textStyle = textFieldStyle(), - singleLine = true, + lineLimits = TextFieldLineLimits.SingleLine, interactionSource = interactionSource, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Search, ), - keyboardActions = KeyboardActions( - onSearch = { - focusManager.clearFocus() - } - ), + onKeyboardAction = { + focusManager.clearFocus() + }, cursorBrush = SolidColor(ElementTheme.colors.textActionAccent), - ) { innerTextField -> - DecorationBox( - isFocused = isFocused, - placeholder = placeholder, - isTextEmpty = value.isEmpty(), - innerTextField = innerTextField, - onClear = { onValueChange("") }, - ) - } -} - -@Composable -fun SearchField( - value: TextFieldValue, - onValueChange: (TextFieldValue) -> Unit, - modifier: Modifier = Modifier, - placeholder: String? = null, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, -) { - val focusManager = LocalFocusManager.current - val isFocused by interactionSource.collectIsFocusedAsState() - BasicTextField( - value = value, - onValueChange = onValueChange, - modifier = modifier, - textStyle = textFieldStyle(), - singleLine = true, - interactionSource = interactionSource, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Search, - ), - keyboardActions = KeyboardActions( - onSearch = { - focusManager.clearFocus() - } - ), - cursorBrush = SolidColor(ElementTheme.colors.textActionAccent), - ) { innerTextField -> - DecorationBox( - isFocused = isFocused, - placeholder = placeholder, - isTextEmpty = value.text.isEmpty(), - innerTextField = innerTextField, - onClear = { TextFieldValue() } - ) - } + decorator = { innerTextField -> + DecorationBox( + isFocused = isFocused, + placeholder = placeholder, + isTextEmpty = state.text.isEmpty(), + innerTextField = innerTextField, + onClear = { state.clearText() }, + ) + } + ) } @Composable @@ -211,14 +172,12 @@ private fun ContentToPreview() { verticalArrangement = spacedBy(8.dp) ) { SearchField( - onValueChange = {}, placeholder = "Search", - value = "", + state = TextFieldState(""), ) SearchField( - onValueChange = {}, placeholder = "Search", - value = "Search term", + state = TextFieldState("Search term"), ) } } diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvents.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvents.kt index b5ea0e0727..0dcfb51079 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvents.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvents.kt @@ -16,5 +16,4 @@ sealed interface RoomSelectEvents { // TODO remove to restore multi-selection data object RemoveSelectedRoom : RoomSelectEvents data object ToggleSearchActive : RoomSelectEvents - data class UpdateQuery(val query: String) : RoomSelectEvents } diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt index fe54f574b9..d38d9bde2e 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.roomselect.impl +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State @@ -42,12 +43,13 @@ class RoomSelectPresenter( @Composable override fun present(): RoomSelectState { var selectedRooms by remember { mutableStateOf(persistentListOf()) } - var searchQuery by remember { mutableStateOf("") } + val queryState = rememberTextFieldState() var isSearchActive by remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() val dataSource = remember { dataSourceFactory.create(coroutineScope) } + val searchQuery = queryState.text.toString() LaunchedEffect(searchQuery) { dataSource.setSearchQuery(searchQuery) } @@ -77,7 +79,6 @@ class RoomSelectPresenter( // } } RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf() - is RoomSelectEvents.UpdateQuery -> searchQuery = event.query RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive } } @@ -85,7 +86,7 @@ class RoomSelectPresenter( return RoomSelectState( mode = mode, resultState = searchResults, - query = searchQuery, + searchQuery = queryState, isSearchActive = isSearchActive, selectedRooms = selectedRooms, eventSink = ::handleEvent, diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt index c1ccb0077d..927a1c02d6 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.roomselect.impl +import androidx.compose.foundation.text.input.TextFieldState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.ui.model.SelectRoomInfo import io.element.android.libraries.roomselect.api.RoomSelectMode @@ -16,7 +17,7 @@ import kotlinx.collections.immutable.ImmutableList data class RoomSelectState( val mode: RoomSelectMode, val resultState: SearchBarResultState>, - val query: String, + val searchQuery: TextFieldState, val isSearchActive: Boolean, val selectedRooms: ImmutableList, val eventSink: (RoomSelectEvents) -> Unit diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt index 5b63d6dbd7..6cd2bc6921 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.roomselect.impl +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.RoomAlias @@ -22,16 +23,16 @@ open class RoomSelectStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomSelectState(), - aRoomSelectState(query = "Test", isSearchActive = true), + aRoomSelectState(searchQuery = "Test", isSearchActive = true), aRoomSelectState(resultState = SearchBarResultState.Results(aRoomSelectRoomList())), aRoomSelectState( resultState = SearchBarResultState.Results(aRoomSelectRoomList()), - query = "Test", + searchQuery = "Test", isSearchActive = true, ), aRoomSelectState( resultState = SearchBarResultState.Results(aRoomSelectRoomList()), - query = "Test", + searchQuery = "Test", isSearchActive = true, selectedRooms = aRoomSelectRoomList().subList(0, 1), ), @@ -45,13 +46,13 @@ open class RoomSelectStateProvider : PreviewParameterProvider { private fun aRoomSelectState( mode: RoomSelectMode = RoomSelectMode.Forward, resultState: SearchBarResultState> = SearchBarResultState.Initial(), - query: String = "", + searchQuery: String = "", isSearchActive: Boolean = false, selectedRooms: ImmutableList = persistentListOf(), ) = RoomSelectState( mode = mode, resultState = resultState, - query = query, + searchQuery = TextFieldState(initialText = searchQuery), isSearchActive = isSearchActive, selectedRooms = selectedRooms, eventSink = {} diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt index 4d02e1ba10..19be54d6c4 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt @@ -132,8 +132,7 @@ fun RoomSelectView( SearchBar( modifier = Modifier.fillMaxWidth(), placeHolderTitle = stringResource(CommonStrings.action_search), - query = state.query, - onQueryChange = { state.eventSink(RoomSelectEvents.UpdateQuery(it)) }, + queryState = state.searchQuery, active = state.isSearchActive, onActiveChange = { state.eventSink(RoomSelectEvents.ToggleSearchActive) }, resultState = state.resultState, diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt index 25d0cd7207..bd8ef59482 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.roomselect.impl +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test @@ -78,13 +79,13 @@ class RoomSelectPresenterTest { assertThat(result).isEqualTo(listOf(expectedRoomInfo)) initialState.eventSink(RoomSelectEvents.ToggleSearchActive) skipItems(1) - initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained")) + initialState.searchQuery.setTextAndPlaceCursorAtEnd("string not contained") assertThat( roomListService.allRooms.currentFilter.value ).isEqualTo( RoomListFilter.NormalizedMatchRoomName("string not contained") ) - assertThat(awaitItem().query).isEqualTo("string not contained") + assertThat(awaitItem().searchQuery.text.toString()).isEqualTo("string not contained") roomListService.postAllRooms( emptyList() )