Merge pull request #6072 from element-hq/feature/fga/search_bar_text_field_state

Let SearchBar/SearchField use TextFieldState
This commit is contained in:
ganfra 2026-01-23 11:07:37 +01:00 committed by GitHub
commit 3e0bd904c9
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 { sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents {
data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents
data class UpdateSearchQuery(val query: String) : DefaultInvitePeopleEvents
data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents
} }

View file

@ -8,6 +8,8 @@
package io.element.android.features.invitepeople.impl 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@ -80,7 +82,7 @@ class DefaultInvitePeoplePresenter(
val roomMembers = remember { mutableStateOf<AsyncData<ImmutableList<RoomMember>>>(AsyncData.Loading()) } val roomMembers = remember { mutableStateOf<AsyncData<ImmutableList<RoomMember>>>(AsyncData.Loading()) }
val selectedUsers = remember { mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf()) } val selectedUsers = remember { mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf()) }
val searchResults = remember { mutableStateOf<SearchBarResultState<ImmutableList<InvitableUser>>>(SearchBarResultState.Initial()) } val searchResults = remember { mutableStateOf<SearchBarResultState<ImmutableList<InvitableUser>>>(SearchBarResultState.Initial()) }
var searchQuery by rememberSaveable { mutableStateOf("") } val queryState = rememberTextFieldState()
var searchActive by rememberSaveable { mutableStateOf(false) } var searchActive by rememberSaveable { mutableStateOf(false) }
val showSearchLoader = rememberSaveable { mutableStateOf(false) } val showSearchLoader = rememberSaveable { mutableStateOf(false) }
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) } val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
@ -129,6 +131,7 @@ class DefaultInvitePeoplePresenter(
fetchMembers(it, roomMembers) fetchMembers(it, roomMembers)
} }
} }
val searchQuery = queryState.text.toString()
LaunchedEffect(searchQuery, roomMembers) { LaunchedEffect(searchQuery, roomMembers) {
performSearch( performSearch(
searchResults = searchResults, searchResults = searchResults,
@ -143,11 +146,9 @@ class DefaultInvitePeoplePresenter(
when (event) { when (event) {
is DefaultInvitePeopleEvents.OnSearchActiveChanged -> { is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
searchActive = event.active searchActive = event.active
searchQuery = "" if (!event.active) {
} queryState.clearText()
}
is DefaultInvitePeopleEvents.UpdateSearchQuery -> {
searchQuery = event.query
} }
is DefaultInvitePeopleEvents.ToggleUser -> { is DefaultInvitePeopleEvents.ToggleUser -> {
@ -162,7 +163,7 @@ class DefaultInvitePeoplePresenter(
} }
is InvitePeopleEvents.CloseSearch -> { is InvitePeopleEvents.CloseSearch -> {
searchActive = false searchActive = false
searchQuery = "" queryState.clearText()
} }
} }
} }
@ -171,7 +172,7 @@ class DefaultInvitePeoplePresenter(
room = room.map { }, room = room.map { },
canInvite = selectedUsers.value.isNotEmpty() && !sendInvitesAction.value.isLoading(), canInvite = selectedUsers.value.isNotEmpty() && !sendInvitesAction.value.isLoading(),
selectedUsers = selectedUsers.value, selectedUsers = selectedUsers.value,
searchQuery = searchQuery, searchQuery = queryState,
isSearchActive = searchActive, isSearchActive = searchActive,
searchResults = searchResults.value, searchResults = searchResults.value,
showSearchLoader = showSearchLoader.value, showSearchLoader = showSearchLoader.value,

View file

@ -8,6 +8,7 @@
package io.element.android.features.invitepeople.impl 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.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleState import io.element.android.features.invitepeople.api.InvitePeopleState
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -19,7 +20,7 @@ import kotlinx.collections.immutable.ImmutableList
data class DefaultInvitePeopleState( data class DefaultInvitePeopleState(
val room: AsyncData<Unit>, val room: AsyncData<Unit>,
override val canInvite: Boolean, override val canInvite: Boolean,
val searchQuery: String, val searchQuery: TextFieldState,
val showSearchLoader: Boolean, val showSearchLoader: Boolean,
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>, val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
val selectedUsers: ImmutableList<MatrixUser>, val selectedUsers: ImmutableList<MatrixUser>,

View file

@ -8,6 +8,7 @@
package io.element.android.features.invitepeople.impl package io.element.android.features.invitepeople.impl
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -108,7 +109,7 @@ private fun aDefaultInvitePeopleState(
return DefaultInvitePeopleState( return DefaultInvitePeopleState(
room = room, room = room,
canInvite = canInvite, canInvite = canInvite,
searchQuery = searchQuery, searchQuery = TextFieldState(initialText = searchQuery),
searchResults = searchResults, searchResults = searchResults,
selectedUsers = selectedUsers, selectedUsers = selectedUsers,
isSearchActive = isSearchActive, isSearchActive = isSearchActive,

View file

@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -90,7 +91,7 @@ private fun InvitePeopleContentView(
InvitePeopleSearchBar( InvitePeopleSearchBar(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
query = state.searchQuery, queryState = state.searchQuery,
showLoader = state.showSearchLoader, showLoader = state.showSearchLoader,
selectedUsers = state.selectedUsers, selectedUsers = state.selectedUsers,
state = state.searchResults, state = state.searchResults,
@ -102,7 +103,6 @@ private fun InvitePeopleContentView(
) )
) )
}, },
onTextChange = { state.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery(it)) },
onToggleUser = ::toggleUser, onToggleUser = ::toggleUser,
) )
@ -149,20 +149,18 @@ private fun InvitePeopleContentView(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun InvitePeopleSearchBar( private fun InvitePeopleSearchBar(
query: String, queryState: TextFieldState,
state: SearchBarResultState<ImmutableList<InvitableUser>>, state: SearchBarResultState<ImmutableList<InvitableUser>>,
showLoader: Boolean, showLoader: Boolean,
selectedUsers: ImmutableList<MatrixUser>, selectedUsers: ImmutableList<MatrixUser>,
active: Boolean, active: Boolean,
onActiveChange: (Boolean) -> Unit, onActiveChange: (Boolean) -> Unit,
onTextChange: (String) -> Unit,
onToggleUser: (MatrixUser) -> Unit, onToggleUser: (MatrixUser) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone), placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone),
) { ) {
SearchBar( SearchBar(
query = query, queryState = queryState,
onQueryChange = onTextChange,
active = active, active = active,
onActiveChange = onActiveChange, onActiveChange = onActiveChange,
modifier = modifier, modifier = modifier,

View file

@ -8,6 +8,7 @@
package io.element.android.features.invitepeople.impl package io.element.android.features.invitepeople.impl
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import app.cash.turbine.ReceiveTurbine import app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.invitepeople.api.InvitePeopleEvents 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.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.isSearchActive).isFalse()
assertThat(initialState.canInvite).isFalse() assertThat(initialState.canInvite).isFalse()
assertThat(initialState.searchQuery).isEmpty() assertThat(initialState.searchQuery.text.toString()).isEmpty()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
@ -85,15 +86,15 @@ internal class DefaultInvitePeoplePresenterTest {
initialState.eventSink(DefaultInvitePeopleEvents.OnSearchActiveChanged(true)) initialState.eventSink(DefaultInvitePeopleEvents.OnSearchActiveChanged(true))
val resultState = awaitItem() val resultState = awaitItemAsDefault()
assertThat(resultState.isSearchActive).isTrue() assertThat(resultState.isSearchActive).isTrue()
resultState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) resultState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
assertThat(awaitItemAsDefault().searchQuery).isEqualTo("some query") assertThat(awaitItemAsDefault().searchQuery.text.toString()).isEqualTo("some query")
resultState.eventSink(InvitePeopleEvents.CloseSearch) resultState.eventSink(InvitePeopleEvents.CloseSearch)
skipItems(2) skipItems(2)
awaitItemAsDefault().also { awaitItemAsDefault().also {
assertThat(it.isSearchActive).isFalse() assertThat(it.isSearchActive).isFalse()
assertThat(it.searchQuery).isEmpty() assertThat(it.searchQuery.text.toString()).isEmpty()
} }
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
@ -107,8 +108,8 @@ internal class DefaultInvitePeoplePresenterTest {
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItemAsDefault()
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
assertThat(repository.providedQuery).isEqualTo("some query") assertThat(repository.providedQuery).isEqualTo("some query")
repository.emitState(UserSearchResultState(results = emptyList(), isSearching = true)) repository.emitState(UserSearchResultState(results = emptyList(), isSearching = true))
skipItems(3) skipItems(3)
@ -132,10 +133,10 @@ internal class DefaultInvitePeoplePresenterTest {
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItemAsDefault()
skipItems(1) skipItems(1)
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
skipItems(1) skipItems(1)
assertThat(repository.providedQuery).isEqualTo("some query") assertThat(repository.providedQuery).isEqualTo("some query")
@ -185,10 +186,10 @@ internal class DefaultInvitePeoplePresenterTest {
coroutineDispatchers = coroutineDispatchers, coroutineDispatchers = coroutineDispatchers,
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItemAsDefault()
skipItems(1) skipItems(1)
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
skipItems(1) skipItems(1)
assertThat(repository.providedQuery).isEqualTo("some query") assertThat(repository.providedQuery).isEqualTo("some query")
@ -245,10 +246,10 @@ internal class DefaultInvitePeoplePresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItemAsDefault()
skipItems(1) skipItems(1)
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
skipItems(1) skipItems(1)
assertThat(repository.providedQuery).isEqualTo("some query") assertThat(repository.providedQuery).isEqualTo("some query")
@ -312,14 +313,14 @@ internal class DefaultInvitePeoplePresenterTest {
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItemAsDefault()
skipItems(1) skipItems(1)
val selectedUser = aMatrixUser() val selectedUser = aMatrixUser()
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser)) initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
skipItems(1) skipItems(1)
assertThat(repository.providedQuery).isEqualTo("some query") assertThat(repository.providedQuery).isEqualTo("some query")
@ -350,13 +351,13 @@ internal class DefaultInvitePeoplePresenterTest {
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItemAsDefault()
skipItems(1) skipItems(1)
val selectedUser = aMatrixUser() val selectedUser = aMatrixUser()
// Given a query is made // Given a query is made
initialState.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery("some query")) initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
skipItems(1) skipItems(1)
assertThat(repository.providedQuery).isEqualTo("some query") assertThat(repository.providedQuery).isEqualTo("some query")

View file

@ -32,7 +32,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.emojibasebindings.Emoji 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.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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toSp import io.element.android.libraries.designsystem.text.toSp
@ -58,8 +57,7 @@ fun EmojiPicker(
Column(modifier) { Column(modifier) {
SearchBar( SearchBar(
modifier = Modifier.padding(bottom = 10.dp), modifier = Modifier.padding(bottom = 10.dp),
query = state.searchQuery, queryState = state.searchQuery,
onQueryChange = { state.eventSink(EmojiPickerEvents.UpdateSearchQuery(it)) },
resultState = state.searchResults, resultState = state.searchResults,
active = state.isSearchActive, active = state.isSearchActive,
onActiveChange = { state.eventSink(EmojiPickerEvents.ToggleSearchActive(it)) }, onActiveChange = { state.eventSink(EmojiPickerEvents.ToggleSearchActive(it)) },

View file

@ -10,5 +10,4 @@ package io.element.android.features.messages.impl.timeline.components.customreac
sealed interface EmojiPickerEvents { sealed interface EmojiPickerEvents {
data class ToggleSearchActive(val isActive: Boolean) : EmojiPickerEvents data class ToggleSearchActive(val isActive: Boolean) : EmojiPickerEvents
data class UpdateSearchQuery(val query: String) : EmojiPickerEvents
} }

View file

@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.timeline.components.customreaction.picker 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -38,7 +39,7 @@ class EmojiPickerPresenter(
) : Presenter<EmojiPickerState> { ) : Presenter<EmojiPickerState> {
@Composable @Composable
override fun present(): EmojiPickerState { override fun present(): EmojiPickerState {
var searchQuery by remember { mutableStateOf("") } val queryState = rememberTextFieldState()
var isSearchActive by remember { mutableStateOf(false) } var isSearchActive by remember { mutableStateOf(false) }
var emojiResults by remember { mutableStateOf<SearchBarResultState<ImmutableList<Emoji>>>(SearchBarResultState.Initial()) } var emojiResults by remember { mutableStateOf<SearchBarResultState<ImmutableList<Emoji>>>(SearchBarResultState.Initial()) }
@ -67,6 +68,7 @@ class EmojiPickerPresenter(
} }
} }
val searchQuery = queryState.text.toString()
LaunchedEffect(searchQuery) { LaunchedEffect(searchQuery) {
emojiResults = if (searchQuery.isEmpty()) { emojiResults = if (searchQuery.isEmpty()) {
SearchBarResultState.Initial() SearchBarResultState.Initial()
@ -97,14 +99,13 @@ class EmojiPickerPresenter(
is EmojiPickerEvents.ToggleSearchActive -> if (!isInPreview) { is EmojiPickerEvents.ToggleSearchActive -> if (!isInPreview) {
isSearchActive = event.isActive isSearchActive = event.isActive
} }
is EmojiPickerEvents.UpdateSearchQuery -> searchQuery = event.query
} }
} }
return EmojiPickerState( return EmojiPickerState(
categories = categories, categories = categories,
allEmojis = emojibaseStore.allEmojis, allEmojis = emojibaseStore.allEmojis,
searchQuery = searchQuery, searchQuery = queryState,
isSearchActive = isSearchActive, isSearchActive = isSearchActive,
searchResults = emojiResults, searchResults = emojiResults,
eventSink = ::handleEvent, eventSink = ::handleEvent,

View file

@ -9,6 +9,7 @@
package io.element.android.features.messages.impl.timeline.components.customreaction.picker package io.element.android.features.messages.impl.timeline.components.customreaction.picker
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.Emoji
import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.IconSource
@ -20,7 +21,7 @@ import kotlinx.collections.immutable.ImmutableList
data class EmojiPickerState( data class EmojiPickerState(
val categories: ImmutableList<EmojiCategory>, val categories: ImmutableList<EmojiCategory>,
val allEmojis: ImmutableList<Emoji>, val allEmojis: ImmutableList<Emoji>,
val searchQuery: String, val searchQuery: TextFieldState,
val isSearchActive: Boolean, val isSearchActive: Boolean,
val searchResults: SearchBarResultState<ImmutableList<Emoji>>, val searchResults: SearchBarResultState<ImmutableList<Emoji>>,
val eventSink: (EmojiPickerEvents) -> Unit, val eventSink: (EmojiPickerEvents) -> Unit,

View file

@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.timeline.components.customreaction.picker 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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.Emoji
import io.element.android.emojibasebindings.EmojibaseCategory import io.element.android.emojibasebindings.EmojibaseCategory
@ -76,7 +77,7 @@ internal fun anEmojiPickerState(
) = EmojiPickerState( ) = EmojiPickerState(
categories = categories, categories = categories,
allEmojis = allEmojis, allEmojis = allEmojis,
searchQuery = searchQuery, searchQuery = TextFieldState(initialText = searchQuery),
isSearchActive = isSearchActive, isSearchActive = isSearchActive,
searchResults = searchResults, searchResults = searchResults,
eventSink = eventSink, eventSink = eventSink,

View file

@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.timeline.components.customreaction.picker 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.InternalComposeApi
import androidx.compose.runtime.currentComposer import androidx.compose.runtime.currentComposer
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
@ -40,19 +41,19 @@ class EmojiPickerPresenterTest {
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@Test @Test
fun `UpdateSearchQuery loads new results`() = runTest { fun `updating search query loads new results`() = runTest {
testPresenter { testPresenter {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.searchQuery).isEmpty() assertThat(initialState.searchQuery.text.toString()).isEmpty()
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
initialState.eventSink(EmojiPickerEvents.UpdateSearchQuery("smile")) initialState.searchQuery.setTextAndPlaceCursorAtEnd("smile")
assertThat(awaitItem().searchQuery).isEqualTo("smile") assertThat(awaitItem().searchQuery.text.toString()).isEqualTo("smile")
val stateWithResults = awaitItem() val stateWithResults = awaitItem()
assertThat(stateWithResults.searchQuery).isEqualTo("smile") assertThat(stateWithResults.searchQuery.text.toString()).isEqualTo("smile")
assertThat(stateWithResults.searchResults).isInstanceOf(SearchBarResultState.Results::class.java) assertThat(stateWithResults.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
} }
} }

View file

@ -12,7 +12,6 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
sealed interface ChangeRolesEvent { sealed interface ChangeRolesEvent {
data object ToggleSearchActive : ChangeRolesEvent data object ToggleSearchActive : ChangeRolesEvent
data class QueryChanged(val query: String?) : ChangeRolesEvent
data class UserSelectionToggled(val matrixUser: MatrixUser) : ChangeRolesEvent data class UserSelectionToggled(val matrixUser: MatrixUser) : ChangeRolesEvent
data object Save : ChangeRolesEvent data object Save : ChangeRolesEvent
data object Exit : ChangeRolesEvent data object Exit : ChangeRolesEvent

View file

@ -8,6 +8,7 @@
package io.element.android.features.rolesandpermissions.impl.roles package io.element.android.features.rolesandpermissions.impl.roles
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@ -68,7 +69,7 @@ class ChangeRolesPresenter(
@Composable @Composable
override fun present(): ChangeRolesState { override fun present(): ChangeRolesState {
var query by rememberSaveable { mutableStateOf<String?>(null) } val queryState = rememberTextFieldState()
var searchActive by rememberSaveable { mutableStateOf(false) } var searchActive by rememberSaveable { mutableStateOf(false) }
var searchResults by remember { var searchResults by remember {
mutableStateOf<SearchBarResultState<MembersByRole>>(SearchBarResultState.Initial()) mutableStateOf<SearchBarResultState<MembersByRole>>(SearchBarResultState.Initial())
@ -105,9 +106,10 @@ class ChangeRolesPresenter(
val roomMemberState by room.membersStateFlow.collectAsState() val roomMemberState by room.membersStateFlow.collectAsState()
// Update search results for every query change // Update search results for every query change
val query = queryState.text.toString()
LaunchedEffect(query, roomMemberState) { LaunchedEffect(query, roomMemberState) {
val results = dataSource val results = dataSource
.search(query.orEmpty()) .search(query)
.groupedByRole() .groupedByRole()
searchResults = if (results.isEmpty()) { searchResults = if (results.isEmpty()) {
@ -136,9 +138,6 @@ class ChangeRolesPresenter(
is ChangeRolesEvent.ToggleSearchActive -> { is ChangeRolesEvent.ToggleSearchActive -> {
searchActive = !searchActive searchActive = !searchActive
} }
is ChangeRolesEvent.QueryChanged -> {
query = event.query
}
is ChangeRolesEvent.UserSelectionToggled -> { is ChangeRolesEvent.UserSelectionToggled -> {
val newList = selectedUsers.value.toMutableList() val newList = selectedUsers.value.toMutableList()
val index = newList.indexOfFirst { it.userId == event.matrixUser.userId } val index = newList.indexOfFirst { it.userId == event.matrixUser.userId }
@ -188,7 +187,7 @@ class ChangeRolesPresenter(
} }
return ChangeRolesState( return ChangeRolesState(
role = role, role = role,
query = query, searchQuery = queryState,
isSearchActive = searchActive, isSearchActive = searchActive,
searchResults = searchResults, searchResults = searchResults,
selectedUsers = selectedUsers.value, selectedUsers = selectedUsers.value,

View file

@ -8,6 +8,7 @@
package io.element.android.features.rolesandpermissions.impl.roles 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.architecture.AsyncAction
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -19,7 +20,7 @@ import kotlinx.collections.immutable.toImmutableList
data class ChangeRolesState( data class ChangeRolesState(
val role: RoomMember.Role, val role: RoomMember.Role,
val query: String?, val searchQuery: TextFieldState,
val isSearchActive: Boolean, val isSearchActive: Boolean,
val searchResults: SearchBarResultState<MembersByRole>, val searchResults: SearchBarResultState<MembersByRole>,
val selectedUsers: ImmutableList<MatrixUser>, val selectedUsers: ImmutableList<MatrixUser>,

View file

@ -8,6 +8,7 @@
package io.element.android.features.rolesandpermissions.impl.roles package io.element.android.features.rolesandpermissions.impl.roles
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
@ -34,8 +35,8 @@ class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
aChangeRolesStateWithSelectedUsers().copy( aChangeRolesStateWithSelectedUsers().copy(
selectedUsers = aMatrixUserList().take(2).toImmutableList(), selectedUsers = aMatrixUserList().take(2).toImmutableList(),
), ),
aChangeRolesStateWithSelectedUsers().copy( aChangeRolesState(
query = "Alice", searchQuery = "Alice",
isSearchActive = true, isSearchActive = true,
searchResults = SearchBarResultState.Results( searchResults = SearchBarResultState.Results(
MembersByRole( MembersByRole(
@ -44,6 +45,8 @@ class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
) )
), ),
selectedUsers = aMatrixUserList().take(1).toImmutableList(), selectedUsers = aMatrixUserList().take(1).toImmutableList(),
hasPendingChanges = true,
canRemoveMember = { it != UserId("@alice:server.org") },
), ),
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.ConfirmingCancellation), aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.ConfirmingCancellation),
aChangeRolesStateWithSelectedUsers().copy(savingState = ConfirmingModifyingAdmins), aChangeRolesStateWithSelectedUsers().copy(savingState = ConfirmingModifyingAdmins),
@ -59,7 +62,7 @@ class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
internal fun aChangeRolesState( internal fun aChangeRolesState(
role: RoomMember.Role = RoomMember.Role.Admin, role: RoomMember.Role = RoomMember.Role.Admin,
query: String? = null, searchQuery: String = "",
isSearchActive: Boolean = false, isSearchActive: Boolean = false,
searchResults: SearchBarResultState<MembersByRole> = SearchBarResultState.NoResultsFound(), searchResults: SearchBarResultState<MembersByRole> = SearchBarResultState.NoResultsFound(),
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(), selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
@ -69,7 +72,7 @@ internal fun aChangeRolesState(
eventSink: (ChangeRolesEvent) -> Unit = {}, eventSink: (ChangeRolesEvent) -> Unit = {},
) = ChangeRolesState( ) = ChangeRolesState(
role = role, role = role,
query = query, searchQuery = TextFieldState(initialText = searchQuery),
isSearchActive = isSearchActive, isSearchActive = isSearchActive,
searchResults = searchResults, searchResults = searchResults,
selectedUsers = selectedUsers, selectedUsers = selectedUsers,

View file

@ -118,8 +118,7 @@ fun ChangeRolesView(
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 16.dp), .padding(bottom = 16.dp),
placeHolderTitle = stringResource(CommonStrings.common_search_for_someone), placeHolderTitle = stringResource(CommonStrings.common_search_for_someone),
query = state.query.orEmpty(), queryState = state.searchQuery,
onQueryChange = { state.eventSink(ChangeRolesEvent.QueryChanged(it)) },
active = state.isSearchActive, active = state.isSearchActive,
onActiveChange = { state.eventSink(ChangeRolesEvent.ToggleSearchActive) }, onActiveChange = { state.eventSink(ChangeRolesEvent.ToggleSearchActive) },
resultState = state.searchResults, resultState = state.searchResults,

View file

@ -8,6 +8,7 @@
package io.element.android.features.rolesandpermissions.impl.roles package io.element.android.features.rolesandpermissions.impl.roles
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.RoomModeration import im.vector.app.features.analytics.plan.RoomModeration
import io.element.android.features.rolesandpermissions.impl.RoomMemberListDataSource import io.element.android.features.rolesandpermissions.impl.RoomMemberListDataSource
@ -49,7 +50,7 @@ class ChangeRolesPresenterTest {
presenter.test { presenter.test {
with(awaitItem()) { with(awaitItem()) {
assertThat(role).isEqualTo(RoomMember.Role.Admin) assertThat(role).isEqualTo(RoomMember.Role.Admin)
assertThat(query).isNull() assertThat(searchQuery.text.toString()).isEmpty()
assertThat(isSearchActive).isFalse() assertThat(isSearchActive).isFalse()
assertThat(searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
assertThat(selectedUsers).isEmpty() assertThat(selectedUsers).isEmpty()
@ -206,7 +207,7 @@ class ChangeRolesPresenterTest {
} }
@Test @Test
fun `present - QueryChanged produces new results`() = runTest { fun `present - updating query produces new results`() = runTest {
val room = FakeJoinedRoom().apply { val room = FakeJoinedRoom().apply {
givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
} }
@ -219,7 +220,7 @@ class ChangeRolesPresenterTest {
assertThat(initialResults?.moderators).hasSize(1) assertThat(initialResults?.moderators).hasSize(1)
assertThat(initialResults?.admins).hasSize(1) assertThat(initialResults?.admins).hasSize(1)
initialState.eventSink(ChangeRolesEvent.QueryChanged("Alice")) initialState.searchQuery.setTextAndPlaceCursorAtEnd("Alice")
skipItems(1) skipItems(1)
val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results

View file

@ -76,7 +76,7 @@ class ChangeRolesViewTest {
), ),
) )
rule.pressBackKey() rule.pressBackKey()
eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""), ChangeRolesEvent.Exit)) eventsRecorder.assertSingle(ChangeRolesEvent.Exit)
} }
@Test @Test
@ -89,7 +89,7 @@ class ChangeRolesViewTest {
), ),
) )
rule.pressBack() rule.pressBack()
eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""), ChangeRolesEvent.Exit)) eventsRecorder.assertSingle(ChangeRolesEvent.Exit)
} }
@Test @Test
@ -102,7 +102,7 @@ class ChangeRolesViewTest {
), ),
) )
rule.clickOn(CommonStrings.action_save) rule.clickOn(CommonStrings.action_save)
eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""), ChangeRolesEvent.Save)) eventsRecorder.assertSingle(ChangeRolesEvent.Save)
} }
@Test @Test
@ -115,7 +115,7 @@ class ChangeRolesViewTest {
), ),
) )
rule.clickOn(CommonStrings.action_save) rule.clickOn(CommonStrings.action_save)
eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""))) eventsRecorder.assertEmpty()
} }
@Test @Test
@ -238,12 +238,7 @@ class ChangeRolesViewTest {
label = contentDescription, label = contentDescription,
useUnmergedTree = true, useUnmergedTree = true,
).performClick() ).performClick()
eventsRecorder.assertList( eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToDeselect))
listOf(
ChangeRolesEvent.QueryChanged(""),
ChangeRolesEvent.UserSelectionToggled(userToDeselect),
)
)
} }
@Test @Test
@ -262,12 +257,7 @@ class ChangeRolesViewTest {
) )
// Select the user from the user list // Select the user from the user list
rule.onNodeWithText("Carol").performClick() rule.onNodeWithText("Carol").performClick()
eventsRecorder.assertList( eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect))
listOf(
ChangeRolesEvent.QueryChanged(""),
ChangeRolesEvent.UserSelectionToggled(userToSelect),
)
)
} }
@Test @Test
@ -288,12 +278,7 @@ class ChangeRolesViewTest {
text = "Bob", text = "Bob",
useUnmergedTree = true, useUnmergedTree = true,
)[1].performClick() )[1].performClick()
eventsRecorder.assertList( eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect))
listOf(
ChangeRolesEvent.QueryChanged(""),
ChangeRolesEvent.UserSelectionToggled(userToSelect),
)
)
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChangeRolesContent( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChangeRolesContent(

View file

@ -12,6 +12,5 @@ import io.element.android.libraries.matrix.api.room.RoomMember
sealed interface RoomMemberListEvents { sealed interface RoomMemberListEvents {
data class ChangeSelectedSection(val section: SelectedSection) : RoomMemberListEvents data class ChangeSelectedSection(val section: SelectedSection) : RoomMemberListEvents
data class UpdateSearchQuery(val query: String) : RoomMemberListEvents
data class RoomMemberSelected(val roomMember: RoomMember) : RoomMemberListEvents data class RoomMemberSelected(val roomMember: RoomMember) : RoomMemberListEvents
} }

View file

@ -8,6 +8,7 @@
package io.element.android.features.roomdetails.impl.members package io.element.android.features.roomdetails.impl.members
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
@ -16,7 +17,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents.ShowActionsForUser import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents.ShowActionsForUser
@ -56,7 +56,7 @@ class RoomMemberListPresenter(
@Composable @Composable
override fun present(): RoomMemberListState { override fun present(): RoomMemberListState {
var searchQuery by rememberSaveable { mutableStateOf("") } val searchQuery = rememberTextFieldState()
val membersState by room.membersStateFlow.collectAsState() val membersState by room.membersStateFlow.collectAsState()
val canInvite by room.permissionsAsState(false) { perms -> perms.canOwnUserInvite() } val canInvite by room.permissionsAsState(false) { perms -> perms.canOwnUserInvite() }
val roomModerationState = roomMembersModerationPresenter.present() val roomModerationState = roomMembersModerationPresenter.present()
@ -117,17 +117,16 @@ class RoomMemberListPresenter(
} }
} }
LaunchedEffect(searchQuery, roomMembers) { LaunchedEffect(searchQuery.text, roomMembers) {
filteredRoomMembers = roomMembers.map { members -> filteredRoomMembers = roomMembers.map { members ->
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
members.filter(searchQuery) members.filter(searchQuery.text.toString())
} }
} }
} }
fun handleEvent(event: RoomMemberListEvents) { fun handleEvent(event: RoomMemberListEvents) {
when (event) { when (event) {
is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query
is RoomMemberListEvents.RoomMemberSelected -> is RoomMemberListEvents.RoomMemberSelected ->
roomModerationState.eventSink(ShowActionsForUser(event.roomMember.toMatrixUser())) roomModerationState.eventSink(ShowActionsForUser(event.roomMember.toMatrixUser()))
is RoomMemberListEvents.ChangeSelectedSection -> selectedSection = event.section is RoomMemberListEvents.ChangeSelectedSection -> selectedSection = event.section

View file

@ -8,6 +8,7 @@
package io.element.android.features.roomdetails.impl.members 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.features.roommembermoderation.api.RoomMemberModerationState
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.bool.orFalse 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 // Only used to know if we can show the banned section
private val roomMembers: AsyncData<RoomMembers>, private val roomMembers: AsyncData<RoomMembers>,
val filteredRoomMembers: AsyncData<RoomMembers>, val filteredRoomMembers: AsyncData<RoomMembers>,
val searchQuery: String, val searchQuery: TextFieldState,
val canInvite: Boolean, val canInvite: Boolean,
val selectedSection: SelectedSection, val selectedSection: SelectedSection,
val moderationState: RoomMemberModerationState, val moderationState: RoomMemberModerationState,

View file

@ -8,6 +8,7 @@
package io.element.android.features.roomdetails.impl.members package io.element.android.features.roomdetails.impl.members
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions
@ -88,7 +89,7 @@ internal fun aRoomMemberListState(
) = RoomMemberListState( ) = RoomMemberListState(
roomMembers = roomMembers, roomMembers = roomMembers,
filteredRoomMembers = roomMembers.map { it.filter(searchQuery) }, filteredRoomMembers = roomMembers.map { it.filter(searchQuery) },
searchQuery = searchQuery, searchQuery = TextFieldState(searchQuery),
canInvite = canInvite, canInvite = canInvite,
moderationState = moderationState, moderationState = moderationState,
selectedSection = selectedSection, selectedSection = selectedSection,

View file

@ -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.BigIcon
import io.element.android.libraries.designsystem.components.avatar.AvatarSize 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.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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
@ -89,13 +88,8 @@ fun RoomMemberListView(
.consumeWindowInsets(padding), .consumeWindowInsets(padding),
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
var searchQuery by textFieldState(state.searchQuery)
SearchField( SearchField(
value = searchQuery, state = state.searchQuery,
onValueChange = { newQuery ->
searchQuery = newQuery
state.eventSink(RoomMemberListEvents.UpdateSearchQuery(newQuery))
},
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
@ -105,7 +99,7 @@ fun RoomMemberListView(
roomMembersData = state.filteredRoomMembers, roomMembersData = state.filteredRoomMembers,
selectedSection = state.selectedSection, selectedSection = state.selectedSection,
showBannedSection = state.showBannedSection, showBannedSection = state.showBannedSection,
searchQuery = state.searchQuery, searchQuery = state.searchQuery.text.toString(),
onSelectedSectionChange = { state.eventSink(RoomMemberListEvents.ChangeSelectedSection(it)) }, onSelectedSectionChange = { state.eventSink(RoomMemberListEvents.ChangeSelectedSection(it)) },
onSelectUser = ::onSelectUser, onSelectUser = ::onSelectUser,
) )

View file

@ -8,6 +8,7 @@
package io.element.android.features.roomdetails.impl.members package io.element.android.features.roomdetails.impl.members
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
@ -42,7 +43,7 @@ class RoomMemberListPresenterTest {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.filteredRoomMembers.isLoading()).isTrue() assertThat(initialState.filteredRoomMembers.isLoading()).isTrue()
assertThat(initialState.searchQuery).isEmpty() assertThat(initialState.searchQuery.text.toString()).isEmpty()
assertThat(initialState.selectedSection).isEqualTo(SelectedSection.MEMBERS) assertThat(initialState.selectedSection).isEqualTo(SelectedSection.MEMBERS)
} }
} }
@ -87,7 +88,7 @@ class RoomMemberListPresenterTest {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.filteredRoomMembers.isLoading()).isTrue() assertThat(initialState.filteredRoomMembers.isLoading()).isTrue()
assertThat(initialState.searchQuery).isEmpty() assertThat(initialState.searchQuery.text.toString()).isEmpty()
room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList())) room.givenRoomMembersState(RoomMembersState.Ready(aRoomMemberList()))
// Skip items while the new members state is processed // Skip items while the new members state is processed
skipItems(2) skipItems(2)
@ -116,9 +117,9 @@ class RoomMemberListPresenterTest {
assertThat(loadedRoomMembers.invited).isNotEmpty() assertThat(loadedRoomMembers.invited).isNotEmpty()
assertThat(loadedRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse() assertThat(loadedRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse()
assertThat(loadedRoomMembers.isEmpty(SelectedSection.BANNED)).isFalse() assertThat(loadedRoomMembers.isEmpty(SelectedSection.BANNED)).isFalse()
loadedState.eventSink(RoomMemberListEvents.UpdateSearchQuery("something")) loadedState.searchQuery.setTextAndPlaceCursorAtEnd("something")
val searchQueryUpdatedState = awaitItem() val searchQueryUpdatedState = awaitItem()
assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("something") assertThat(searchQueryUpdatedState.searchQuery.text).isEqualTo("something")
val searchSearchResultDelivered = awaitItem() val searchSearchResultDelivered = awaitItem()
val emptyRoomMembers = searchSearchResultDelivered.filteredRoomMembers.dataOrNull()!! val emptyRoomMembers = searchSearchResultDelivered.filteredRoomMembers.dataOrNull()!!
assertThat(emptyRoomMembers.joined).isEmpty() assertThat(emptyRoomMembers.joined).isEmpty()
@ -144,9 +145,9 @@ class RoomMemberListPresenterTest {
assertThat(loadedRoomMembers.invited).isNotEmpty() assertThat(loadedRoomMembers.invited).isNotEmpty()
assertThat(loadedRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse() assertThat(loadedRoomMembers.isEmpty(SelectedSection.MEMBERS)).isFalse()
assertThat(loadedRoomMembers.isEmpty(SelectedSection.BANNED)).isFalse() assertThat(loadedRoomMembers.isEmpty(SelectedSection.BANNED)).isFalse()
loadedState.eventSink(RoomMemberListEvents.UpdateSearchQuery("alice")) loadedState.searchQuery.setTextAndPlaceCursorAtEnd("alice")
val searchQueryUpdatedState = awaitItem() val searchQueryUpdatedState = awaitItem()
assertThat(searchQueryUpdatedState.searchQuery).isEqualTo("alice") assertThat(searchQueryUpdatedState.searchQuery.text).isEqualTo("alice")
val searchSearchResultDelivered = awaitItem() val searchSearchResultDelivered = awaitItem()
val emptyRoomMembers = searchSearchResultDelivered.filteredRoomMembers.dataOrNull()!! val emptyRoomMembers = searchSearchResultDelivered.filteredRoomMembers.dataOrNull()!!
assertThat(emptyRoomMembers.joined).isNotEmpty() assertThat(emptyRoomMembers.joined).isNotEmpty()

View file

@ -11,9 +11,7 @@ import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
sealed interface AddRoomToSpaceEvent { sealed interface AddRoomToSpaceEvent {
data class ToggleRoom(val room: SelectRoomInfo) : AddRoomToSpaceEvent data class ToggleRoom(val room: SelectRoomInfo) : AddRoomToSpaceEvent
data class UpdateSearchQuery(val query: String) : AddRoomToSpaceEvent
data class OnSearchActiveChanged(val active: Boolean) : AddRoomToSpaceEvent data class OnSearchActiveChanged(val active: Boolean) : AddRoomToSpaceEvent
data object Save : AddRoomToSpaceEvent data object Save : AddRoomToSpaceEvent
data object CloseSearch : AddRoomToSpaceEvent
data object ResetSaveAction : AddRoomToSpaceEvent data object ResetSaveAction : AddRoomToSpaceEvent
} }

View file

@ -7,6 +7,8 @@
package io.element.android.features.space.impl.addroom 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@ -43,7 +45,7 @@ class AddRoomToSpacePresenter(
@Composable @Composable
override fun present(): AddRoomToSpaceState { override fun present(): AddRoomToSpaceState {
var selectedRooms: ImmutableList<SelectRoomInfo> by remember { mutableStateOf(persistentListOf()) } var selectedRooms: ImmutableList<SelectRoomInfo> by remember { mutableStateOf(persistentListOf()) }
var searchQuery by remember { mutableStateOf("") } var searchQuery = rememberTextFieldState()
var isSearchActive by remember { mutableStateOf(false) } var isSearchActive by remember { mutableStateOf(false) }
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) } val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
@ -51,8 +53,8 @@ class AddRoomToSpacePresenter(
val dataSource = remember { dataSourceFactory.create(coroutineScope) } val dataSource = remember { dataSourceFactory.create(coroutineScope) }
// Update search query in data source // Update search query in data source
LaunchedEffect(searchQuery) { LaunchedEffect(searchQuery.text) {
dataSource.setSearchQuery(searchQuery) dataSource.setSearchQuery(searchQuery.text.toString())
} }
LaunchedEffect(isSearchActive) { LaunchedEffect(isSearchActive) {
dataSource.setIsActive(isSearchActive) dataSource.setIsActive(isSearchActive)
@ -65,7 +67,7 @@ class AddRoomToSpacePresenter(
derivedStateOf { derivedStateOf {
when { when {
filteredRooms.isNotEmpty() -> SearchBarResultState.Results(filteredRooms) filteredRooms.isNotEmpty() -> SearchBarResultState.Results(filteredRooms)
isSearchActive && searchQuery.isNotEmpty() -> SearchBarResultState.NoResultsFound() isSearchActive && searchQuery.text.isNotEmpty() -> SearchBarResultState.NoResultsFound()
else -> SearchBarResultState.Initial() else -> SearchBarResultState.Initial()
} }
} }
@ -80,19 +82,12 @@ class AddRoomToSpacePresenter(
(selectedRooms + event.room).toImmutableList() (selectedRooms + event.room).toImmutableList()
} }
} }
is AddRoomToSpaceEvent.UpdateSearchQuery -> {
searchQuery = event.query
}
is AddRoomToSpaceEvent.OnSearchActiveChanged -> { is AddRoomToSpaceEvent.OnSearchActiveChanged -> {
isSearchActive = event.active isSearchActive = event.active
if (!event.active) { if (!event.active) {
searchQuery = "" searchQuery.clearText()
} }
} }
AddRoomToSpaceEvent.CloseSearch -> {
isSearchActive = false
searchQuery = ""
}
AddRoomToSpaceEvent.Save -> { AddRoomToSpaceEvent.Save -> {
coroutineScope.addRoomsToSpace( coroutineScope.addRoomsToSpace(
selectedRooms = selectedRooms, selectedRooms = selectedRooms,

View file

@ -7,13 +7,14 @@
package io.element.android.features.space.impl.addroom 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.architecture.AsyncAction
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class AddRoomToSpaceState( data class AddRoomToSpaceState(
val searchQuery: String, val searchQuery: TextFieldState,
val isSearchActive: Boolean, val isSearchActive: Boolean,
val searchResults: SearchBarResultState<ImmutableList<SelectRoomInfo>>, val searchResults: SearchBarResultState<ImmutableList<SelectRoomInfo>>,
val selectedRooms: ImmutableList<SelectRoomInfo>, val selectedRooms: ImmutableList<SelectRoomInfo>,

View file

@ -7,6 +7,7 @@
package io.element.android.features.space.impl.addroom package io.element.android.features.space.impl.addroom
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
@ -69,7 +70,7 @@ internal fun anAddRoomToSpaceState(
eventSink: (AddRoomToSpaceEvent) -> Unit = {}, eventSink: (AddRoomToSpaceEvent) -> Unit = {},
): AddRoomToSpaceState { ): AddRoomToSpaceState {
return AddRoomToSpaceState( return AddRoomToSpaceState(
searchQuery = searchQuery, searchQuery = TextFieldState(searchQuery),
searchResults = searchResults, searchResults = searchResults,
selectedRooms = selectedRooms, selectedRooms = selectedRooms,
isSearchActive = isSearchActive, isSearchActive = isSearchActive,

View file

@ -64,7 +64,7 @@ fun AddRoomToSpaceView(
fun onBack() { fun onBack() {
if (state.isSearchActive) { if (state.isSearchActive) {
state.eventSink(AddRoomToSpaceEvent.CloseSearch) state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(false))
} else { } else {
onBackClick() onBackClick()
} }
@ -105,12 +105,11 @@ fun AddRoomToSpaceView(
SearchBar( SearchBar(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeHolderTitle = stringResource(CommonStrings.action_search), placeHolderTitle = stringResource(CommonStrings.action_search),
query = state.searchQuery, queryState = state.searchQuery,
onQueryChange = { state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery(it)) },
active = state.isSearchActive, active = state.isSearchActive,
onActiveChange = { state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(it)) }, onActiveChange = { state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(it)) },
resultState = state.searchResults,
showBackButton = false, showBackButton = false,
resultState = state.searchResults,
contentPrefix = { contentPrefix = {
if (state.selectedRooms.isNotEmpty()) { if (state.selectedRooms.isNotEmpty()) {
SelectedRoomsRow( SelectedRoomsRow(

View file

@ -9,6 +9,7 @@
package io.element.android.features.space.impl.addroom package io.element.android.features.space.impl.addroom
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
@ -38,7 +39,7 @@ class AddRoomToSpacePresenterTest {
presenter.test { presenter.test {
val state = awaitItem() val state = awaitItem()
assertThat(state.selectedRooms).isEmpty() assertThat(state.selectedRooms).isEmpty()
assertThat(state.searchQuery).isEmpty() assertThat(state.searchQuery.text.toString()).isEmpty()
assertThat(state.isSearchActive).isFalse() assertThat(state.isSearchActive).isFalse()
assertThat(state.saveAction).isEqualTo(AsyncAction.Uninitialized) assertThat(state.saveAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(state.canSave).isFalse() 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 @Test
fun `present - OnSearchActiveChanged activates search`() = runTest { fun `present - OnSearchActiveChanged activates search`() = runTest {
val presenter = createAddRoomToSpacePresenter() val presenter = createAddRoomToSpacePresenter()
@ -107,33 +97,14 @@ class AddRoomToSpacePresenterTest {
// Activate search and set query // Activate search and set query
state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true)) state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true))
awaitItem() awaitItem()
state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("test")) state.searchQuery.setTextAndPlaceCursorAtEnd("test")
awaitItem() awaitItem()
// Deactivate search // Deactivate search
state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(false)) state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(false))
advanceUntilIdle() advanceUntilIdle()
val finalState = expectMostRecentItem() val finalState = expectMostRecentItem()
assertThat(finalState.isSearchActive).isFalse() assertThat(finalState.isSearchActive).isFalse()
assertThat(finalState.searchQuery).isEmpty() assertThat(finalState.searchQuery.text.toString()).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()
} }
} }
@ -168,11 +139,11 @@ class AddRoomToSpacePresenterTest {
val state = awaitItem() val state = awaitItem()
state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true)) state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(true))
awaitItem() awaitItem()
state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery("nonexistent")) state.searchQuery.setTextAndPlaceCursorAtEnd("nonexistent")
advanceUntilIdle() advanceUntilIdle()
val finalState = expectMostRecentItem() val finalState = expectMostRecentItem()
assertThat(finalState.isSearchActive).isTrue() assertThat(finalState.isSearchActive).isTrue()
assertThat(finalState.searchQuery).isEqualTo("nonexistent") assertThat(finalState.searchQuery.text).isEqualTo("nonexistent")
assertThat(finalState.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java) assertThat(finalState.searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
} }
} }

View file

@ -54,7 +54,7 @@ class AddRoomToSpaceViewTest {
), ),
) )
rule.pressBack() rule.pressBack()
eventsRecorder.assertSingle(AddRoomToSpaceEvent.CloseSearch) eventsRecorder.assertSingle(AddRoomToSpaceEvent.OnSearchActiveChanged(false))
} }
@Test @Test
@ -67,12 +67,7 @@ class AddRoomToSpaceViewTest {
), ),
) )
rule.clickOn(CommonStrings.action_save) rule.clickOn(CommonStrings.action_save)
eventsRecorder.assertList( eventsRecorder.assertSingle(AddRoomToSpaceEvent.Save)
listOf(
AddRoomToSpaceEvent.UpdateSearchQuery(""), // SearchBar initialization
AddRoomToSpaceEvent.Save,
)
)
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@ -87,12 +82,7 @@ class AddRoomToSpaceViewTest {
), ),
) )
rule.onNodeWithText(suggestions.first().name!!).performClick() rule.onNodeWithText(suggestions.first().name!!).performClick()
eventsRecorder.assertList( eventsRecorder.assertSingle(AddRoomToSpaceEvent.ToggleRoom(suggestions.first()))
listOf(
AddRoomToSpaceEvent.UpdateSearchQuery(""), // SearchBar initialization
AddRoomToSpaceEvent.ToggleRoom(suggestions.first()),
)
)
} }
@Test @Test

View file

@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
@ -40,14 +41,13 @@ import kotlinx.collections.immutable.ImmutableList
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SearchUserBar( fun SearchUserBar(
query: String, queryState: TextFieldState,
state: SearchBarResultState<ImmutableList<UserSearchResult>>, resultState: SearchBarResultState<ImmutableList<UserSearchResult>>,
showLoader: Boolean, showLoader: Boolean,
selectedUsers: ImmutableList<MatrixUser>, selectedUsers: ImmutableList<MatrixUser>,
active: Boolean, active: Boolean,
isMultiSelectionEnable: Boolean, isMultiSelectionEnable: Boolean,
onActiveChange: (Boolean) -> Unit, onActiveChange: (Boolean) -> Unit,
onTextChange: (String) -> Unit,
onUserSelect: (MatrixUser) -> Unit, onUserSelect: (MatrixUser) -> Unit,
onUserDeselect: (MatrixUser) -> Unit, onUserDeselect: (MatrixUser) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -57,8 +57,7 @@ fun SearchUserBar(
val columnState = rememberLazyListState() val columnState = rememberLazyListState()
SearchBar( SearchBar(
query = query, queryState = queryState,
onQueryChange = onTextChange,
active = active, active = active,
onActiveChange = onActiveChange, onActiveChange = onActiveChange,
modifier = modifier, modifier = modifier,
@ -98,7 +97,7 @@ fun SearchUserBar(
AsyncLoading() AsyncLoading()
} }
}, },
resultState = state, resultState = resultState,
resultHandler = { users -> resultHandler = { users ->
LazyColumn(state = columnState) { LazyColumn(state = columnState) {
if (isMultiSelectionEnable) { if (isMultiSelectionEnable) {

View file

@ -46,15 +46,14 @@ fun UserListView(
) { ) {
SearchUserBar( SearchUserBar(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
query = state.searchQuery, queryState = state.searchQuery,
state = state.searchResults, resultState = state.searchResults,
selectedUsers = state.selectedUsers, selectedUsers = state.selectedUsers,
active = state.isSearchActive, active = state.isSearchActive,
showLoader = state.showSearchLoader, showLoader = state.showSearchLoader,
isMultiSelectionEnable = state.isMultiSelectionEnabled, isMultiSelectionEnable = state.isMultiSelectionEnabled,
showBackButton = showBackButton, showBackButton = showBackButton,
onActiveChange = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) }, onActiveChange = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) },
onTextChange = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) },
onUserSelect = { onUserSelect = {
state.eventSink(UserListEvents.AddToSelection(it)) state.eventSink(UserListEvents.AddToSelection(it))
onSelectUser(it) onSelectUser(it)

View file

@ -27,10 +27,10 @@ open class StartChatStateProvider : PreviewParameterProvider<StartChatState> {
aCreateRoomRootState( aCreateRoomRootState(
startDmAction = AsyncAction.Loading, startDmAction = AsyncAction.Loading,
userListState = aMatrixUser().let { userListState = aMatrixUser().let {
aUserListState().copy( aUserListState(
searchQuery = it.userId.value, searchQuery = it.userId.value,
searchResults = SearchBarResultState.Results(persistentListOf(UserSearchResult(it, false))), searchResults = SearchBarResultState.Results(persistentListOf(UserSearchResult(it, false))),
selectedUsers = persistentListOf(it), selectedUsers = listOf(it),
isSearchActive = true, isSearchActive = true,
) )
} }
@ -38,10 +38,10 @@ open class StartChatStateProvider : PreviewParameterProvider<StartChatState> {
aCreateRoomRootState( aCreateRoomRootState(
startDmAction = AsyncAction.Failure(RuntimeException("error")), startDmAction = AsyncAction.Failure(RuntimeException("error")),
userListState = aMatrixUser().let { userListState = aMatrixUser().let {
aUserListState().copy( aUserListState(
searchQuery = it.userId.value, searchQuery = it.userId.value,
searchResults = SearchBarResultState.Results(persistentListOf(UserSearchResult(it, false))), searchResults = SearchBarResultState.Results(persistentListOf(UserSearchResult(it, false))),
selectedUsers = persistentListOf(it), selectedUsers = listOf(it),
isSearchActive = true, isSearchActive = true,
) )
} }

View file

@ -8,6 +8,7 @@
package io.element.android.features.startchat.impl.userlist package io.element.android.features.startchat.impl.userlist
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -64,12 +65,13 @@ class DefaultUserListPresenter(
} }
var isSearchActive by rememberSaveable { mutableStateOf(false) } var isSearchActive by rememberSaveable { mutableStateOf(false) }
val selectedUsers by userListDataStore.selectedUsers.collectAsState(emptyList()) val selectedUsers by userListDataStore.selectedUsers.collectAsState(emptyList())
var searchQuery by rememberSaveable { mutableStateOf("") } val queryState = rememberTextFieldState()
var searchResults: SearchBarResultState<ImmutableList<UserSearchResult>> by remember { var searchResults: SearchBarResultState<ImmutableList<UserSearchResult>> by remember {
mutableStateOf(SearchBarResultState.Initial()) mutableStateOf(SearchBarResultState.Initial())
} }
var showSearchLoader by remember { mutableStateOf(false) } var showSearchLoader by remember { mutableStateOf(false) }
val searchQuery = queryState.text.toString()
LaunchedEffect(searchQuery) { LaunchedEffect(searchQuery) {
searchResults = SearchBarResultState.Initial() searchResults = SearchBarResultState.Initial()
showSearchLoader = false showSearchLoader = false
@ -86,14 +88,13 @@ class DefaultUserListPresenter(
fun handleEvent(event: UserListEvents) { fun handleEvent(event: UserListEvents) {
when (event) { when (event) {
is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active
is UserListEvents.UpdateSearchQuery -> searchQuery = event.query
is UserListEvents.AddToSelection -> userListDataStore.selectUser(event.matrixUser) is UserListEvents.AddToSelection -> userListDataStore.selectUser(event.matrixUser)
is UserListEvents.RemoveFromSelection -> userListDataStore.removeUserFromSelection(event.matrixUser) is UserListEvents.RemoveFromSelection -> userListDataStore.removeUserFromSelection(event.matrixUser)
} }
} }
return UserListState( return UserListState(
searchQuery = searchQuery, searchQuery = queryState,
searchResults = searchResults, searchResults = searchResults,
selectedUsers = selectedUsers.toImmutableList(), selectedUsers = selectedUsers.toImmutableList(),
isSearchActive = isSearchActive, isSearchActive = isSearchActive,

View file

@ -11,7 +11,6 @@ package io.element.android.features.startchat.impl.userlist
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
sealed interface UserListEvents { sealed interface UserListEvents {
data class UpdateSearchQuery(val query: String) : UserListEvents
data class AddToSelection(val matrixUser: MatrixUser) : UserListEvents data class AddToSelection(val matrixUser: MatrixUser) : UserListEvents
data class RemoveFromSelection(val matrixUser: MatrixUser) : UserListEvents data class RemoveFromSelection(val matrixUser: MatrixUser) : UserListEvents
data class OnSearchActiveChanged(val active: Boolean) : UserListEvents data class OnSearchActiveChanged(val active: Boolean) : UserListEvents

View file

@ -8,6 +8,7 @@
package io.element.android.features.startchat.impl.userlist 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.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom
import io.element.android.libraries.matrix.api.user.MatrixUser 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 import kotlinx.collections.immutable.ImmutableList
data class UserListState( data class UserListState(
val searchQuery: String, val searchQuery: TextFieldState,
val searchResults: SearchBarResultState<ImmutableList<UserSearchResult>>, val searchResults: SearchBarResultState<ImmutableList<UserSearchResult>>,
val showSearchLoader: Boolean, val showSearchLoader: Boolean,
val selectedUsers: ImmutableList<MatrixUser>, val selectedUsers: ImmutableList<MatrixUser>,

View file

@ -8,6 +8,7 @@
package io.element.android.features.startchat.impl.userlist package io.element.android.features.startchat.impl.userlist
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -69,7 +70,7 @@ fun aUserListState(
recentDirectRooms: List<RecentDirectRoom> = emptyList(), recentDirectRooms: List<RecentDirectRoom> = emptyList(),
eventSink: (UserListEvents) -> Unit = {}, eventSink: (UserListEvents) -> Unit = {},
) = UserListState( ) = UserListState(
searchQuery = searchQuery, searchQuery = TextFieldState(initialText = searchQuery),
isSearchActive = isSearchActive, isSearchActive = isSearchActive,
searchResults = searchResults, searchResults = searchResults,
selectedUsers = selectedUsers.toImmutableList(), selectedUsers = selectedUsers.toImmutableList(),

View file

@ -8,6 +8,7 @@
package io.element.android.features.startchat.impl.userlist package io.element.android.features.startchat.impl.userlist
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
@ -41,7 +42,7 @@ class DefaultUserListPresenterTest {
presenter.test { presenter.test {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.searchQuery).isEmpty() assertThat(initialState.searchQuery.text.toString()).isEmpty()
assertThat(initialState.isMultiSelectionEnabled).isFalse() assertThat(initialState.isMultiSelectionEnabled).isFalse()
assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.isSearchActive).isFalse()
assertThat(initialState.selectedUsers).isEmpty() assertThat(initialState.selectedUsers).isEmpty()
@ -61,7 +62,7 @@ class DefaultUserListPresenterTest {
presenter.test { presenter.test {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.searchQuery).isEmpty() assertThat(initialState.searchQuery.text.toString()).isEmpty()
assertThat(initialState.isMultiSelectionEnabled).isTrue() assertThat(initialState.isMultiSelectionEnabled).isTrue()
assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.isSearchActive).isFalse()
assertThat(initialState.selectedUsers).isEmpty() assertThat(initialState.selectedUsers).isEmpty()
@ -86,14 +87,14 @@ class DefaultUserListPresenterTest {
assertThat(awaitItem().isSearchActive).isTrue() assertThat(awaitItem().isSearchActive).isTrue()
val matrixIdQuery = "@name:matrix.org" val matrixIdQuery = "@name:matrix.org"
initialState.eventSink(UserListEvents.UpdateSearchQuery(matrixIdQuery)) initialState.searchQuery.setTextAndPlaceCursorAtEnd(matrixIdQuery)
assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery) assertThat(awaitItem().searchQuery.text.toString()).isEqualTo(matrixIdQuery)
assertThat(userRepository.providedQuery).isEqualTo(matrixIdQuery) assertThat(userRepository.providedQuery).isEqualTo(matrixIdQuery)
skipItems(1) skipItems(1)
val notMatrixIdQuery = "name" val notMatrixIdQuery = "name"
initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery)) initialState.searchQuery.setTextAndPlaceCursorAtEnd(notMatrixIdQuery)
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery) assertThat(awaitItem().searchQuery.text.toString()).isEqualTo(notMatrixIdQuery)
assertThat(userRepository.providedQuery).isEqualTo(notMatrixIdQuery) assertThat(userRepository.providedQuery).isEqualTo(notMatrixIdQuery)
skipItems(1) skipItems(1)
@ -117,7 +118,7 @@ class DefaultUserListPresenterTest {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(UserListEvents.UpdateSearchQuery("alice")) initialState.searchQuery.setTextAndPlaceCursorAtEnd("alice")
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
assertThat(userRepository.providedQuery).isEqualTo("alice") assertThat(userRepository.providedQuery).isEqualTo("alice")
skipItems(2) skipItems(2)
@ -168,7 +169,7 @@ class DefaultUserListPresenterTest {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(UserListEvents.UpdateSearchQuery("alice")) initialState.searchQuery.setTextAndPlaceCursorAtEnd("alice")
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
assertThat(userRepository.providedQuery).isEqualTo("alice") assertThat(userRepository.providedQuery).isEqualTo("alice")
skipItems(2) skipItems(2)

View file

@ -17,6 +17,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.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.ExperimentalMaterial3Api
import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarColors import androidx.compose.material3.SearchBarColors
@ -25,9 +28,7 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
@ -51,8 +52,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun <T> SearchBar( fun <T> SearchBar(
query: String, queryState: TextFieldState,
onQueryChange: (String) -> Unit,
active: Boolean, active: Boolean,
onActiveChange: (Boolean) -> Unit, onActiveChange: (Boolean) -> Unit,
placeHolderTitle: String, placeHolderTitle: String,
@ -72,10 +72,9 @@ fun <T> SearchBar(
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val colors = if (active) activeBarColors else inactiveBarColors val colors = if (active) activeBarColors else inactiveBarColors
val updatedOnQueryChange by rememberUpdatedState(onQueryChange)
LaunchedEffect(active) { LaunchedEffect(active) {
if (!active) { if (!active) {
updatedOnQueryChange("") queryState.clearText()
focusManager.clearFocus() focusManager.clearFocus()
} }
} }
@ -83,8 +82,7 @@ fun <T> SearchBar(
SearchBar( SearchBar(
inputField = { inputField = {
SearchBarDefaults.InputField( SearchBarDefaults.InputField(
query = query, state = queryState,
onQueryChange = updatedOnQueryChange,
onSearch = { focusManager.clearFocus() }, onSearch = { focusManager.clearFocus() },
expanded = active, expanded = active,
onExpandedChange = onActiveChange, onExpandedChange = onActiveChange,
@ -98,9 +96,9 @@ fun <T> SearchBar(
null null
}, },
trailingIcon = when { trailingIcon = when {
active && query.isNotEmpty() -> { active && queryState.text.isNotEmpty() -> {
{ {
IconButton(onClick = { onQueryChange("") }) { IconButton(onClick = { queryState.clearText() }) {
Icon( Icon(
imageVector = CompoundIcons.Close(), imageVector = CompoundIcons.Close(),
contentDescription = stringResource(CommonStrings.action_clear), contentDescription = stringResource(CommonStrings.action_clear),
@ -221,7 +219,7 @@ internal fun SearchBarInactivePreview() = ElementThemedPreview { ContentToPrevie
@Composable @Composable
internal fun SearchBarActiveNoneQueryPreview() = ElementThemedPreview { internal fun SearchBarActiveNoneQueryPreview() = ElementThemedPreview {
ContentToPreview( ContentToPreview(
query = "", initialQuery = "",
active = true, active = true,
) )
} }
@ -230,7 +228,7 @@ internal fun SearchBarActiveNoneQueryPreview() = ElementThemedPreview {
@Composable @Composable
internal fun SearchBarActiveWithQueryPreview() = ElementThemedPreview { internal fun SearchBarActiveWithQueryPreview() = ElementThemedPreview {
ContentToPreview( ContentToPreview(
query = "search term", initialQuery = "search term",
active = true, active = true,
) )
} }
@ -239,7 +237,7 @@ internal fun SearchBarActiveWithQueryPreview() = ElementThemedPreview {
@Composable @Composable
internal fun SearchBarActiveWithQueryNoBackButtonPreview() = ElementThemedPreview { internal fun SearchBarActiveWithQueryNoBackButtonPreview() = ElementThemedPreview {
ContentToPreview( ContentToPreview(
query = "search term", initialQuery = "search term",
active = true, active = true,
showBackButton = false, showBackButton = false,
) )
@ -249,7 +247,7 @@ internal fun SearchBarActiveWithQueryNoBackButtonPreview() = ElementThemedPrevie
@Composable @Composable
internal fun SearchBarActiveWithNoResultsPreview() = ElementThemedPreview { internal fun SearchBarActiveWithNoResultsPreview() = ElementThemedPreview {
ContentToPreview( ContentToPreview(
query = "search term", initialQuery = "search term",
active = true, active = true,
resultState = SearchBarResultState.NoResultsFound<String>(), resultState = SearchBarResultState.NoResultsFound<String>(),
) )
@ -259,7 +257,7 @@ internal fun SearchBarActiveWithNoResultsPreview() = ElementThemedPreview {
@Composable @Composable
internal fun SearchBarActiveWithContentPreview() = ElementThemedPreview { internal fun SearchBarActiveWithContentPreview() = ElementThemedPreview {
ContentToPreview( ContentToPreview(
query = "search term", initialQuery = "search term",
active = true, active = true,
resultState = SearchBarResultState.Results("result!"), resultState = SearchBarResultState.Results("result!"),
contentPrefix = { contentPrefix = {
@ -292,7 +290,7 @@ internal fun SearchBarActiveWithContentPreview() = ElementThemedPreview {
@Composable @Composable
@ExcludeFromCoverage @ExcludeFromCoverage
private fun ContentToPreview( private fun ContentToPreview(
query: String = "", initialQuery: String = "",
active: Boolean = false, active: Boolean = false,
showBackButton: Boolean = true, showBackButton: Boolean = true,
resultState: SearchBarResultState<String> = SearchBarResultState.Initial(), resultState: SearchBarResultState<String> = SearchBarResultState.Initial(),
@ -300,13 +298,13 @@ private fun ContentToPreview(
contentSuffix: @Composable ColumnScope.() -> Unit = {}, contentSuffix: @Composable ColumnScope.() -> Unit = {},
resultHandler: @Composable ColumnScope.(String) -> Unit = {}, resultHandler: @Composable ColumnScope.(String) -> Unit = {},
) { ) {
val queryState = rememberTextFieldState(initialText = initialQuery)
SearchBar( SearchBar(
modifier = Modifier.heightIn(max = 200.dp), modifier = Modifier.heightIn(max = 200.dp),
query = query, queryState = queryState,
active = active, active = active,
resultState = resultState, resultState = resultState,
showBackButton = showBackButton, showBackButton = showBackButton,
onQueryChange = {},
onActiveChange = {}, onActiveChange = {},
placeHolderTitle = "Search for things", placeHolderTitle = "Search for things",
contentPrefix = contentPrefix, contentPrefix = contentPrefix,

View file

@ -22,8 +22,10 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions 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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember 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.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
@ -50,8 +51,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
*/ */
@Composable @Composable
fun SearchField( fun SearchField(
value: String, state: TextFieldState,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
placeholder: String? = null, placeholder: String? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
@ -59,67 +59,28 @@ fun SearchField(
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val isFocused by interactionSource.collectIsFocusedAsState() val isFocused by interactionSource.collectIsFocusedAsState()
BasicTextField( BasicTextField(
value = value, state = state,
onValueChange = onValueChange,
modifier = modifier, modifier = modifier,
textStyle = textFieldStyle(), textStyle = textFieldStyle(),
singleLine = true, lineLimits = TextFieldLineLimits.SingleLine,
interactionSource = interactionSource, interactionSource = interactionSource,
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Search, imeAction = ImeAction.Search,
), ),
keyboardActions = KeyboardActions( onKeyboardAction = {
onSearch = { focusManager.clearFocus()
focusManager.clearFocus() },
}
),
cursorBrush = SolidColor(ElementTheme.colors.textActionAccent), cursorBrush = SolidColor(ElementTheme.colors.textActionAccent),
) { innerTextField -> decorator = { innerTextField ->
DecorationBox( DecorationBox(
isFocused = isFocused, isFocused = isFocused,
placeholder = placeholder, placeholder = placeholder,
isTextEmpty = value.isEmpty(), isTextEmpty = state.text.isEmpty(),
innerTextField = innerTextField, innerTextField = innerTextField,
onClear = { onValueChange("") }, onClear = { state.clearText() },
) )
} }
} )
@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() }
)
}
} }
@Composable @Composable
@ -211,14 +172,12 @@ private fun ContentToPreview() {
verticalArrangement = spacedBy(8.dp) verticalArrangement = spacedBy(8.dp)
) { ) {
SearchField( SearchField(
onValueChange = {},
placeholder = "Search", placeholder = "Search",
value = "", state = TextFieldState(""),
) )
SearchField( SearchField(
onValueChange = {},
placeholder = "Search", placeholder = "Search",
value = "Search term", state = TextFieldState("Search term"),
) )
} }
} }

View file

@ -16,5 +16,4 @@ sealed interface RoomSelectEvents {
// TODO remove to restore multi-selection // TODO remove to restore multi-selection
data object RemoveSelectedRoom : RoomSelectEvents data object RemoveSelectedRoom : RoomSelectEvents
data object ToggleSearchActive : RoomSelectEvents data object ToggleSearchActive : RoomSelectEvents
data class UpdateQuery(val query: String) : RoomSelectEvents
} }

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.roomselect.impl package io.element.android.libraries.roomselect.impl
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State import androidx.compose.runtime.State
@ -42,12 +43,13 @@ class RoomSelectPresenter(
@Composable @Composable
override fun present(): RoomSelectState { override fun present(): RoomSelectState {
var selectedRooms by remember { mutableStateOf(persistentListOf<SelectRoomInfo>()) } var selectedRooms by remember { mutableStateOf(persistentListOf<SelectRoomInfo>()) }
var searchQuery by remember { mutableStateOf("") } val queryState = rememberTextFieldState()
var isSearchActive by remember { mutableStateOf(false) } var isSearchActive by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val dataSource = remember { dataSourceFactory.create(coroutineScope) } val dataSource = remember { dataSourceFactory.create(coroutineScope) }
val searchQuery = queryState.text.toString()
LaunchedEffect(searchQuery) { LaunchedEffect(searchQuery) {
dataSource.setSearchQuery(searchQuery) dataSource.setSearchQuery(searchQuery)
} }
@ -77,7 +79,6 @@ class RoomSelectPresenter(
// } // }
} }
RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf() RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf()
is RoomSelectEvents.UpdateQuery -> searchQuery = event.query
RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive
} }
} }
@ -85,7 +86,7 @@ class RoomSelectPresenter(
return RoomSelectState( return RoomSelectState(
mode = mode, mode = mode,
resultState = searchResults, resultState = searchResults,
query = searchQuery, searchQuery = queryState,
isSearchActive = isSearchActive, isSearchActive = isSearchActive,
selectedRooms = selectedRooms, selectedRooms = selectedRooms,
eventSink = ::handleEvent, eventSink = ::handleEvent,

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.roomselect.impl 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.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
@ -16,7 +17,7 @@ import kotlinx.collections.immutable.ImmutableList
data class RoomSelectState( data class RoomSelectState(
val mode: RoomSelectMode, val mode: RoomSelectMode,
val resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>>, val resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>>,
val query: String, val searchQuery: TextFieldState,
val isSearchActive: Boolean, val isSearchActive: Boolean,
val selectedRooms: ImmutableList<SelectRoomInfo>, val selectedRooms: ImmutableList<SelectRoomInfo>,
val eventSink: (RoomSelectEvents) -> Unit val eventSink: (RoomSelectEvents) -> Unit

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.roomselect.impl package io.element.android.libraries.roomselect.impl
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
@ -22,16 +23,16 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
override val values: Sequence<RoomSelectState> override val values: Sequence<RoomSelectState>
get() = sequenceOf( get() = sequenceOf(
aRoomSelectState(), aRoomSelectState(),
aRoomSelectState(query = "Test", isSearchActive = true), aRoomSelectState(searchQuery = "Test", isSearchActive = true),
aRoomSelectState(resultState = SearchBarResultState.Results(aRoomSelectRoomList())), aRoomSelectState(resultState = SearchBarResultState.Results(aRoomSelectRoomList())),
aRoomSelectState( aRoomSelectState(
resultState = SearchBarResultState.Results(aRoomSelectRoomList()), resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
query = "Test", searchQuery = "Test",
isSearchActive = true, isSearchActive = true,
), ),
aRoomSelectState( aRoomSelectState(
resultState = SearchBarResultState.Results(aRoomSelectRoomList()), resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
query = "Test", searchQuery = "Test",
isSearchActive = true, isSearchActive = true,
selectedRooms = aRoomSelectRoomList().subList(0, 1), selectedRooms = aRoomSelectRoomList().subList(0, 1),
), ),
@ -45,13 +46,13 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
private fun aRoomSelectState( private fun aRoomSelectState(
mode: RoomSelectMode = RoomSelectMode.Forward, mode: RoomSelectMode = RoomSelectMode.Forward,
resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>> = SearchBarResultState.Initial(), resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>> = SearchBarResultState.Initial(),
query: String = "", searchQuery: String = "",
isSearchActive: Boolean = false, isSearchActive: Boolean = false,
selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(), selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(),
) = RoomSelectState( ) = RoomSelectState(
mode = mode, mode = mode,
resultState = resultState, resultState = resultState,
query = query, searchQuery = TextFieldState(initialText = searchQuery),
isSearchActive = isSearchActive, isSearchActive = isSearchActive,
selectedRooms = selectedRooms, selectedRooms = selectedRooms,
eventSink = {} eventSink = {}

View file

@ -132,8 +132,7 @@ fun RoomSelectView(
SearchBar( SearchBar(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeHolderTitle = stringResource(CommonStrings.action_search), placeHolderTitle = stringResource(CommonStrings.action_search),
query = state.query, queryState = state.searchQuery,
onQueryChange = { state.eventSink(RoomSelectEvents.UpdateQuery(it)) },
active = state.isSearchActive, active = state.isSearchActive,
onActiveChange = { state.eventSink(RoomSelectEvents.ToggleSearchActive) }, onActiveChange = { state.eventSink(RoomSelectEvents.ToggleSearchActive) },
resultState = state.resultState, resultState = state.resultState,

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.roomselect.impl package io.element.android.libraries.roomselect.impl
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
@ -78,13 +79,13 @@ class RoomSelectPresenterTest {
assertThat(result).isEqualTo(listOf(expectedRoomInfo)) assertThat(result).isEqualTo(listOf(expectedRoomInfo))
initialState.eventSink(RoomSelectEvents.ToggleSearchActive) initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
skipItems(1) skipItems(1)
initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained")) initialState.searchQuery.setTextAndPlaceCursorAtEnd("string not contained")
assertThat( assertThat(
roomListService.allRooms.currentFilter.value roomListService.allRooms.currentFilter.value
).isEqualTo( ).isEqualTo(
RoomListFilter.NormalizedMatchRoomName("string not contained") RoomListFilter.NormalizedMatchRoomName("string not contained")
) )
assertThat(awaitItem().query).isEqualTo("string not contained") assertThat(awaitItem().searchQuery.text.toString()).isEqualTo("string not contained")
roomListService.postAllRooms( roomListService.postAllRooms(
emptyList() emptyList()
) )