Let SearchBar/SearchField use TextFieldState

This commit is contained in:
ganfra 2026-01-22 16:34:22 +01:00
parent 87619e50e8
commit fa1b32f0ba
48 changed files with 197 additions and 298 deletions

View file

@ -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
}

View file

@ -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<ImmutableList<RoomMember>>>(AsyncData.Loading()) }
val selectedUsers = remember { mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf()) }
val searchResults = remember { mutableStateOf<SearchBarResultState<ImmutableList<InvitableUser>>>(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<Unit>>(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,

View file

@ -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<Unit>,
override val canInvite: Boolean,
val searchQuery: String,
val searchQuery: TextFieldState,
val showSearchLoader: Boolean,
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
val selectedUsers: ImmutableList<MatrixUser>,

View file

@ -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,

View file

@ -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<ImmutableList<InvitableUser>>,
showLoader: Boolean,
selectedUsers: ImmutableList<MatrixUser>,
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,

View file

@ -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")