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