User search : show a loader when fetching results
This commit is contained in:
parent
c6d109c424
commit
690a165411
21 changed files with 135 additions and 81 deletions
|
|
@ -59,7 +59,7 @@ fun <T> SearchBar(
|
|||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
showBackButton: Boolean = true,
|
||||
resultState: SearchBarResultState<T> = SearchBarResultState.NotSearching(),
|
||||
resultState: SearchBarResultState<T> = SearchBarResultState.Empty(),
|
||||
shape: Shape = SearchBarDefaults.inputFieldShape,
|
||||
tonalElevation: Dp = SearchBarDefaults.TonalElevation,
|
||||
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
||||
|
|
@ -129,7 +129,7 @@ fun <T> SearchBar(
|
|||
resultHandler(resultState.results)
|
||||
}
|
||||
|
||||
is SearchBarResultState.NoResults<T> -> {
|
||||
is SearchBarResultState.NoResultsFound<T> -> {
|
||||
// No results found, show a message
|
||||
Spacer(Modifier.size(80.dp))
|
||||
|
||||
|
|
@ -184,10 +184,10 @@ object ElementSearchBarDefaults {
|
|||
@Immutable
|
||||
sealed interface SearchBarResultState<in T> {
|
||||
/** No search results are available yet (e.g. because the user hasn't entered a search term). */
|
||||
class NotSearching<T> : SearchBarResultState<T>
|
||||
class Empty<T> : SearchBarResultState<T>
|
||||
|
||||
/** The search has completed, but no results were found. */
|
||||
class NoResults<T> : SearchBarResultState<T>
|
||||
class NoResultsFound<T> : SearchBarResultState<T>
|
||||
|
||||
/** The search has completed, and some matching users were found. */
|
||||
data class Results<T>(val results: T) : SearchBarResultState<T>
|
||||
|
|
@ -199,7 +199,7 @@ internal fun SearchBarInactivePreview() = ElementThemedPreview { ContentToPrevie
|
|||
|
||||
@Preview(group = PreviewGroup.Search)
|
||||
@Composable
|
||||
internal fun SearchBarActiveEmptyQueryPreview() = ElementThemedPreview {
|
||||
internal fun SearchBarActiveNoneQueryPreview() = ElementThemedPreview {
|
||||
ContentToPreview(
|
||||
query = "",
|
||||
active = true,
|
||||
|
|
@ -231,7 +231,7 @@ internal fun SearchBarActiveWithNoResultsPreview() = ElementThemedPreview {
|
|||
ContentToPreview(
|
||||
query = "search term",
|
||||
active = true,
|
||||
resultState = SearchBarResultState.NoResults(),
|
||||
resultState = SearchBarResultState.NoResultsFound<String>(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -257,16 +257,15 @@ internal fun SearchBarActiveWithContentPreview() = ElementThemedPreview {
|
|||
.background(color = Color.Blue)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
resultHandler = {
|
||||
Text(
|
||||
text = "Results go here",
|
||||
modifier = Modifier
|
||||
.background(color = Color.Green)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Results go here",
|
||||
modifier = Modifier
|
||||
.background(color = Color.Green)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -275,7 +274,7 @@ private fun ContentToPreview(
|
|||
query: String = "",
|
||||
active: Boolean = false,
|
||||
showBackButton: Boolean = true,
|
||||
resultState: SearchBarResultState<String> = SearchBarResultState.NotSearching(),
|
||||
resultState: SearchBarResultState<String> = SearchBarResultState.Empty(),
|
||||
contentPrefix: @Composable ColumnScope.() -> Unit = {},
|
||||
contentSuffix: @Composable ColumnScope.() -> Unit = {},
|
||||
resultHandler: @Composable ColumnScope.(String) -> Unit = {},
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class RoomSelectPresenter @AssistedInject constructor(
|
|||
var selectedRooms by remember { mutableStateOf(persistentListOf<RoomSummaryDetails>()) }
|
||||
var query by remember { mutableStateOf("") }
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.NotSearching()) }
|
||||
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.Empty()) }
|
||||
|
||||
val summaries by client.roomListService.allRooms.summaries.collectAsState()
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ class RoomSelectPresenter @AssistedInject constructor(
|
|||
results = if (filteredSummaries.isNotEmpty()) {
|
||||
SearchBarResultState.Results(filteredSummaries)
|
||||
} else {
|
||||
SearchBarResultState.NoResults()
|
||||
SearchBarResultState.NoResultsFound()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
|
|||
}
|
||||
|
||||
private fun aRoomSelectState(
|
||||
resultState: SearchBarResultState<ImmutableList<RoomSummaryDetails>> = SearchBarResultState.NotSearching(),
|
||||
resultState: SearchBarResultState<ImmutableList<RoomSummaryDetails>> = SearchBarResultState.Empty(),
|
||||
query: String = "",
|
||||
isSearchActive: Boolean = false,
|
||||
selectedRooms: ImmutableList<RoomSummaryDetails> = persistentListOf(),
|
||||
|
|
|
|||
|
|
@ -45,11 +45,11 @@ class RoomSelectPresenterTests {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedRooms).isEmpty()
|
||||
assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||
assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.Empty::class.java)
|
||||
assertThat(initialState.isSearchActive).isFalse()
|
||||
// Search is run automatically
|
||||
val searchState = awaitItem()
|
||||
assertThat(searchState.resultState).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
||||
assertThat(searchState.resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ class RoomSelectPresenterTests {
|
|||
|
||||
initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained"))
|
||||
assertThat(awaitItem().query).isEqualTo("string not contained")
|
||||
assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
||||
assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,5 +20,5 @@ import kotlinx.coroutines.flow.Flow
|
|||
|
||||
interface UserRepository {
|
||||
|
||||
suspend fun search(query: String): Flow<List<UserSearchResult>>
|
||||
fun search(query: String): Flow<UserSearchResultsState>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,3 +22,8 @@ data class UserSearchResult(
|
|||
val matrixUser: MatrixUser,
|
||||
val isUnresolved: Boolean = false,
|
||||
)
|
||||
|
||||
data class UserSearchResultsState(
|
||||
val results: List<UserSearchResult>,
|
||||
val isFetchingSearchResults: Boolean
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
|||
import io.element.android.libraries.usersearch.api.UserListDataSource
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResultsState
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
|
@ -36,36 +37,45 @@ class MatrixUserRepository @Inject constructor(
|
|||
private val dataSource: UserListDataSource
|
||||
) : UserRepository {
|
||||
|
||||
override suspend fun search(query: String): Flow<List<UserSearchResult>> = flow {
|
||||
// If the search term is a MXID that's not ours, we'll show a 'fake' result for that user, then update it when we get search results.
|
||||
override fun search(query: String): Flow<UserSearchResultsState> = flow {
|
||||
val shouldQueryProfile = MatrixPatterns.isUserId(query) && !client.isMe(UserId(query))
|
||||
if (shouldQueryProfile) {
|
||||
emit(listOf(UserSearchResult(MatrixUser(UserId(query)))))
|
||||
val shouldFetchSearchResults = query.length >= MINIMUM_SEARCH_LENGTH
|
||||
// If the search term is a MXID that's not ours, we'll show a 'fake' result for that user, then update it when we get search results.
|
||||
val fakeSearchResult = if (shouldQueryProfile) {
|
||||
UserSearchResult(MatrixUser(UserId(query)), isUnresolved = true)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (query.length >= MINIMUM_SEARCH_LENGTH) {
|
||||
// Debounce
|
||||
delay(DEBOUNCE_TIME_MILLIS)
|
||||
|
||||
val results = dataSource
|
||||
.search(query, MAXIMUM_SEARCH_RESULTS)
|
||||
.filter { !client.isMe(it.userId) }
|
||||
.map { UserSearchResult(it) }
|
||||
.toMutableList()
|
||||
|
||||
// If the query is another user's MXID and the result doesn't contain that user ID, query the profile information explicitly
|
||||
if (shouldQueryProfile && results.none { it.matrixUser.userId.value == query }) {
|
||||
results.add(
|
||||
0,
|
||||
dataSource.getProfile(UserId(query))
|
||||
?.let { UserSearchResult(it) }
|
||||
?: UserSearchResult(MatrixUser(UserId(query)), isUnresolved = true))
|
||||
}
|
||||
|
||||
if (shouldQueryProfile || shouldFetchSearchResults) {
|
||||
emit(UserSearchResultsState(isFetchingSearchResults = shouldFetchSearchResults, results = listOfNotNull(fakeSearchResult)))
|
||||
}
|
||||
if (shouldFetchSearchResults) {
|
||||
val results = fetchSearchResults(query, shouldQueryProfile)
|
||||
emit(results)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchSearchResults(query: String, shouldQueryProfile: Boolean): UserSearchResultsState {
|
||||
// Debounce
|
||||
delay(DEBOUNCE_TIME_MILLIS)
|
||||
val results = dataSource
|
||||
.search(query, MAXIMUM_SEARCH_RESULTS)
|
||||
.filter { !client.isMe(it.userId) }
|
||||
.map { UserSearchResult(it) }
|
||||
.toMutableList()
|
||||
|
||||
// If the query is another user's MXID and the result doesn't contain that user ID, query the profile information explicitly
|
||||
if (shouldQueryProfile && results.none { it.matrixUser.userId.value == query }) {
|
||||
results.add(
|
||||
0,
|
||||
dataSource.getProfile(UserId(query))
|
||||
?.let { UserSearchResult(it) }
|
||||
?: UserSearchResult(MatrixUser(UserId(query)), isUnresolved = true))
|
||||
}
|
||||
|
||||
return UserSearchResultsState(results = results, isFetchingSearchResults = false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEBOUNCE_TIME_MILLIS = 250L
|
||||
private const val MINIMUM_SEARCH_LENGTH = 3
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue