Use loading item instead of full screen loading.
This commit is contained in:
parent
0d16fe019e
commit
5d190a8137
|
@ -20,6 +20,6 @@ import im.vector.app.core.platform.VectorViewModelAction
|
|||
|
||||
sealed class SearchAction : VectorViewModelAction {
|
||||
data class SearchWith(val searchTerm: String) : SearchAction()
|
||||
object ScrolledToTop : SearchAction()
|
||||
object LoadMore : SearchAction()
|
||||
object Retry : SearchAction()
|
||||
}
|
||||
|
|
|
@ -21,14 +21,15 @@ import android.os.Parcelable
|
|||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.cleanup
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.extensions.trackItemsVisibilityChange
|
||||
import im.vector.app.core.platform.StateView
|
||||
|
@ -62,39 +63,19 @@ class SearchFragment @Inject constructor(
|
|||
stateView.eventCallback = this
|
||||
|
||||
configureRecyclerView()
|
||||
|
||||
searchViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is SearchViewEvents.Failure -> {
|
||||
stateView.state = StateView.State.Error(errorFormatter.toHumanReadable(it.throwable))
|
||||
}
|
||||
is SearchViewEvents.Loading -> {
|
||||
stateView.state = StateView.State.Loading
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureRecyclerView() {
|
||||
searchResultRecycler.trackItemsVisibilityChange()
|
||||
searchResultRecycler.configureWith(controller, showDivider = false)
|
||||
(searchResultRecycler.layoutManager as? LinearLayoutManager)?.stackFromEnd = true
|
||||
controller.listener = this
|
||||
|
||||
controller.addModelBuildListener {
|
||||
pendingScrollToPosition?.let {
|
||||
searchResultRecycler.scrollToPosition(it)
|
||||
searchResultRecycler.smoothScrollToPosition(it)
|
||||
}
|
||||
}
|
||||
|
||||
searchResultRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
// Load next batch when scrolled to the top
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE
|
||||
&& (searchResultRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() == 0) {
|
||||
searchViewModel.handle(SearchAction.ScrolledToTop)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -104,18 +85,26 @@ class SearchFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun invalidate() = withState(searchViewModel) { state ->
|
||||
if (state.searchResult?.results?.isNotEmpty() == true) {
|
||||
stateView.state = StateView.State.Content
|
||||
controller.setData(state)
|
||||
|
||||
val lastBatchSize = state.lastBatch?.results?.size ?: 0
|
||||
val scrollPosition = if (lastBatchSize > 0) lastBatchSize - 1 else 0
|
||||
pendingScrollToPosition = scrollPosition
|
||||
} else {
|
||||
if (state.searchResult?.results.isNullOrEmpty()) {
|
||||
when (state.asyncEventsRequest) {
|
||||
is Loading -> {
|
||||
stateView.state = StateView.State.Loading
|
||||
}
|
||||
is Fail -> {
|
||||
stateView.state = StateView.State.Error(errorFormatter.toHumanReadable(state.asyncEventsRequest.error))
|
||||
}
|
||||
is Success -> {
|
||||
stateView.state = StateView.State.Empty(
|
||||
title = getString(R.string.search_no_results),
|
||||
image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_search_no_results)
|
||||
)
|
||||
image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_search_no_results))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val lastBatchSize = state.lastBatch?.results?.size ?: 0
|
||||
pendingScrollToPosition = if (lastBatchSize > 0) lastBatchSize - 1 else 0
|
||||
|
||||
stateView.state = StateView.State.Content
|
||||
controller.setData(state)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,4 +122,8 @@ class SearchFragment @Inject constructor(
|
|||
|
||||
navigator.openRoom(requireContext(), event.roomId!!, event.eventId)
|
||||
}
|
||||
|
||||
override fun loadMore() {
|
||||
searchViewModel.handle(SearchAction.LoadMore)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,10 @@
|
|||
package im.vector.app.features.home.room.detail.search
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.core.epoxy.loadingItem
|
||||
import im.vector.app.core.ui.list.genericItemHeader
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
@ -34,8 +36,11 @@ class SearchResultController @Inject constructor(
|
|||
|
||||
var listener: Listener? = null
|
||||
|
||||
private var idx = 0
|
||||
|
||||
interface Listener {
|
||||
fun onItemClicked(event: Event)
|
||||
fun loadMore()
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -45,6 +50,18 @@ class SearchResultController @Inject constructor(
|
|||
override fun buildModels(data: SearchViewState?) {
|
||||
data?.searchResult?.results ?: return
|
||||
|
||||
if (!data.searchResult.nextBatch.isNullOrEmpty()) {
|
||||
loadingItem {
|
||||
// Always use a different id, because we can be notified several times of visibility state changed
|
||||
id("loadMore${idx++}")
|
||||
onVisibilityStateChanged { _, _, visibilityState ->
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
listener?.loadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildSearchResultItems(data.searchResult.results!!)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,5 +20,4 @@ import im.vector.app.core.platform.VectorViewEvents
|
|||
|
||||
sealed class SearchViewEvents : VectorViewEvents {
|
||||
data class Failure(val throwable: Throwable) : SearchViewEvents()
|
||||
data class Loading(val message: CharSequence? = null) : SearchViewEvents()
|
||||
}
|
||||
|
|
|
@ -16,16 +16,21 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.search
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.search.SearchResult
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
|
||||
class SearchViewModel @AssistedInject constructor(
|
||||
@Assisted private val initialState: SearchViewState,
|
||||
|
@ -49,7 +54,7 @@ class SearchViewModel @AssistedInject constructor(
|
|||
override fun handle(action: SearchAction) {
|
||||
when (action) {
|
||||
is SearchAction.SearchWith -> handleSearchWith(action)
|
||||
is SearchAction.ScrolledToTop -> handleScrolledToTop()
|
||||
is SearchAction.LoadMore -> handleLoadMore()
|
||||
is SearchAction.Retry -> handleRetry()
|
||||
}.exhaustive
|
||||
}
|
||||
|
@ -57,17 +62,13 @@ class SearchViewModel @AssistedInject constructor(
|
|||
private fun handleSearchWith(action: SearchAction.SearchWith) {
|
||||
if (action.searchTerm.length > 1) {
|
||||
setState {
|
||||
copy(searchTerm = action.searchTerm, isNextBatch = false)
|
||||
copy(searchTerm = action.searchTerm)
|
||||
}
|
||||
|
||||
startSearching()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleScrolledToTop() {
|
||||
setState {
|
||||
copy(isNextBatch = true)
|
||||
}
|
||||
private fun handleLoadMore() {
|
||||
startSearching(true)
|
||||
}
|
||||
|
||||
|
@ -75,14 +76,24 @@ class SearchViewModel @AssistedInject constructor(
|
|||
startSearching()
|
||||
}
|
||||
|
||||
private fun startSearching(scrolledToTop: Boolean = false) = withState { state ->
|
||||
private fun startSearching(isNextBatch: Boolean = false) = withState { state ->
|
||||
if (state.roomId == null || state.searchTerm == null) return@withState
|
||||
|
||||
// There is no batch to retrieve
|
||||
if (scrolledToTop && state.searchResult?.nextBatch == null) return@withState
|
||||
if (isNextBatch && state.searchResult?.nextBatch == null) return@withState
|
||||
|
||||
_viewEvents.post(SearchViewEvents.Loading())
|
||||
// Show full screen loading just for the clean search
|
||||
if (!isNextBatch) {
|
||||
setState {
|
||||
copy(
|
||||
asyncEventsRequest = Loading()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = awaitCallback<SearchResult> {
|
||||
session
|
||||
.getRoom(state.roomId)
|
||||
?.search(
|
||||
|
@ -93,26 +104,23 @@ class SearchViewModel @AssistedInject constructor(
|
|||
afterLimit = 0,
|
||||
includeProfile = true,
|
||||
limit = 20,
|
||||
callback = object : MatrixCallback<SearchResult> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
onSearchFailure(failure)
|
||||
}
|
||||
|
||||
override fun onSuccess(data: SearchResult) {
|
||||
onSearchResultSuccess(data)
|
||||
}
|
||||
}
|
||||
callback = it
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSearchFailure(failure: Throwable) {
|
||||
setState {
|
||||
copy(searchResult = null)
|
||||
}
|
||||
onSearchResultSuccess(result, isNextBatch)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(SearchViewEvents.Failure(failure))
|
||||
setState {
|
||||
copy(
|
||||
asyncEventsRequest = Fail(failure),
|
||||
searchResult = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchResultSuccess(searchResult: SearchResult) = withState { state ->
|
||||
private fun onSearchResultSuccess(searchResult: SearchResult, isNextBatch: Boolean) = withState { state ->
|
||||
val accumulatedResult = SearchResult(
|
||||
nextBatch = searchResult.nextBatch,
|
||||
results = searchResult.results,
|
||||
|
@ -120,7 +128,7 @@ class SearchViewModel @AssistedInject constructor(
|
|||
)
|
||||
|
||||
// Accumulate results if it is the next batch
|
||||
if (state.isNextBatch) {
|
||||
if (isNextBatch) {
|
||||
if (state.searchResult != null) {
|
||||
accumulatedResult.results = accumulatedResult.results?.plus(state.searchResult.results!!)
|
||||
}
|
||||
|
@ -130,7 +138,11 @@ class SearchViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
setState {
|
||||
copy(searchResult = accumulatedResult, lastBatch = searchResult)
|
||||
copy(
|
||||
searchResult = accumulatedResult,
|
||||
lastBatch = searchResult,
|
||||
asyncEventsRequest = Success(Unit)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.search
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import org.matrix.android.sdk.api.session.search.SearchResult
|
||||
|
||||
data class SearchViewState(
|
||||
|
@ -26,7 +28,8 @@ data class SearchViewState(
|
|||
val lastBatch: SearchResult? = null,
|
||||
val searchTerm: String? = null,
|
||||
val roomId: String? = null,
|
||||
val isNextBatch: Boolean = false
|
||||
// Current pagination request
|
||||
val asyncEventsRequest: Async<Unit> = Uninitialized
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: SearchArgs) : this(roomId = args.roomId)
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
<im.vector.app.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/stateView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?riotx_header_panel_background">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/searchResultRecycler"
|
||||
|
|
Loading…
Reference in New Issue