From 5d190a81379f7631de18b328c18448fe7f114be0 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 29 Sep 2020 20:38:58 +0300 Subject: [PATCH] Use loading item instead of full screen loading. --- .../home/room/detail/search/SearchAction.kt | 2 +- .../home/room/detail/search/SearchFragment.kt | 61 ++++++------ .../detail/search/SearchResultController.kt | 17 ++++ .../room/detail/search/SearchViewEvents.kt | 1 - .../room/detail/search/SearchViewModel.kt | 96 +++++++++++-------- .../room/detail/search/SearchViewState.kt | 5 +- .../src/main/res/layout/fragment_search.xml | 3 +- 7 files changed, 104 insertions(+), 81 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchAction.kt index 4a2c84e258..36d22f1914 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchAction.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt index 1c355bfbfe..5f943c6031 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt @@ -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) { + 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)) + } + } + } else { + val lastBatchSize = state.lastBatch?.results?.size ?: 0 + pendingScrollToPosition = if (lastBatchSize > 0) lastBatchSize - 1 else 0 + 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 { - stateView.state = StateView.State.Empty( - title = getString(R.string.search_no_results), - image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_search_no_results) - ) } } @@ -133,4 +122,8 @@ class SearchFragment @Inject constructor( navigator.openRoom(requireContext(), event.roomId!!, event.eventId) } + + override fun loadMore() { + searchViewModel.handle(SearchAction.LoadMore) + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index 82d3120311..39e197e54f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -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!!) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewEvents.kt index 41dabd8686..6f07cb765c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt index 5747e83cad..9786560d19 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt @@ -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, @@ -48,26 +53,22 @@ class SearchViewModel @AssistedInject constructor( override fun handle(action: SearchAction) { when (action) { - is SearchAction.SearchWith -> handleSearchWith(action) - is SearchAction.ScrolledToTop -> handleScrolledToTop() - is SearchAction.Retry -> handleRetry() + is SearchAction.SearchWith -> handleSearchWith(action) + is SearchAction.LoadMore -> handleLoadMore() + is SearchAction.Retry -> handleRetry() }.exhaustive } 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,44 +76,51 @@ 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()) - - session - .getRoom(state.roomId) - ?.search( - searchTerm = state.searchTerm, - nextBatch = state.searchResult?.nextBatch, - orderByRecent = true, - beforeLimit = 0, - afterLimit = 0, - includeProfile = true, - limit = 20, - callback = object : MatrixCallback { - override fun onFailure(failure: Throwable) { - onSearchFailure(failure) - } - - override fun onSuccess(data: SearchResult) { - onSearchResultSuccess(data) - } - } + // Show full screen loading just for the clean search + if (!isNextBatch) { + setState { + copy( + asyncEventsRequest = Loading() ) - } - - private fun onSearchFailure(failure: Throwable) { - setState { - copy(searchResult = null) + } + } + + viewModelScope.launch { + try { + val result = awaitCallback { + session + .getRoom(state.roomId) + ?.search( + searchTerm = state.searchTerm, + nextBatch = state.searchResult?.nextBatch, + orderByRecent = true, + beforeLimit = 0, + afterLimit = 0, + includeProfile = true, + limit = 20, + callback = it + ) + } + onSearchResultSuccess(result, isNextBatch) + } catch (failure: Throwable) { + _viewEvents.post(SearchViewEvents.Failure(failure)) + setState { + copy( + asyncEventsRequest = Fail(failure), + searchResult = null + ) + } + } } - _viewEvents.post(SearchViewEvents.Failure(failure)) } - 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) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt index 8c2eb82ad1..3873769c44 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt @@ -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 = Uninitialized ) : MvRxState { constructor(args: SearchArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/res/layout/fragment_search.xml b/vector/src/main/res/layout/fragment_search.xml index 5478e1f68c..757168850b 100644 --- a/vector/src/main/res/layout/fragment_search.xml +++ b/vector/src/main/res/layout/fragment_search.xml @@ -2,8 +2,7 @@ + android:layout_height="match_parent">