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 {
|
sealed class SearchAction : VectorViewModelAction {
|
||||||
data class SearchWith(val searchTerm: String) : SearchAction()
|
data class SearchWith(val searchTerm: String) : SearchAction()
|
||||||
object ScrolledToTop : SearchAction()
|
object LoadMore : SearchAction()
|
||||||
object Retry : SearchAction()
|
object Retry : SearchAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,14 +21,15 @@ import android.os.Parcelable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
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.args
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.cleanup
|
import im.vector.app.core.extensions.cleanup
|
||||||
import im.vector.app.core.extensions.configureWith
|
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.hideKeyboard
|
||||||
import im.vector.app.core.extensions.trackItemsVisibilityChange
|
import im.vector.app.core.extensions.trackItemsVisibilityChange
|
||||||
import im.vector.app.core.platform.StateView
|
import im.vector.app.core.platform.StateView
|
||||||
|
@ -62,39 +63,19 @@ class SearchFragment @Inject constructor(
|
||||||
stateView.eventCallback = this
|
stateView.eventCallback = this
|
||||||
|
|
||||||
configureRecyclerView()
|
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() {
|
private fun configureRecyclerView() {
|
||||||
searchResultRecycler.trackItemsVisibilityChange()
|
searchResultRecycler.trackItemsVisibilityChange()
|
||||||
searchResultRecycler.configureWith(controller, showDivider = false)
|
searchResultRecycler.configureWith(controller, showDivider = false)
|
||||||
|
(searchResultRecycler.layoutManager as? LinearLayoutManager)?.stackFromEnd = true
|
||||||
controller.listener = this
|
controller.listener = this
|
||||||
|
|
||||||
controller.addModelBuildListener {
|
controller.addModelBuildListener {
|
||||||
pendingScrollToPosition?.let {
|
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() {
|
override fun onDestroy() {
|
||||||
|
@ -104,18 +85,26 @@ class SearchFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(searchViewModel) { state ->
|
override fun invalidate() = withState(searchViewModel) { state ->
|
||||||
if (state.searchResult?.results?.isNotEmpty() == true) {
|
if (state.searchResult?.results.isNullOrEmpty()) {
|
||||||
stateView.state = StateView.State.Content
|
when (state.asyncEventsRequest) {
|
||||||
controller.setData(state)
|
is Loading -> {
|
||||||
|
stateView.state = StateView.State.Loading
|
||||||
val lastBatchSize = state.lastBatch?.results?.size ?: 0
|
}
|
||||||
val scrollPosition = if (lastBatchSize > 0) lastBatchSize - 1 else 0
|
is Fail -> {
|
||||||
pendingScrollToPosition = scrollPosition
|
stateView.state = StateView.State.Error(errorFormatter.toHumanReadable(state.asyncEventsRequest.error))
|
||||||
} else {
|
}
|
||||||
|
is Success -> {
|
||||||
stateView.state = StateView.State.Empty(
|
stateView.state = StateView.State.Empty(
|
||||||
title = getString(R.string.search_no_results),
|
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)
|
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
|
package im.vector.app.features.home.room.detail.search
|
||||||
|
|
||||||
import com.airbnb.epoxy.TypedEpoxyController
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
|
import com.airbnb.epoxy.VisibilityState
|
||||||
import im.vector.app.core.date.DateFormatKind
|
import im.vector.app.core.date.DateFormatKind
|
||||||
import im.vector.app.core.date.VectorDateFormatter
|
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.core.ui.list.genericItemHeader
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
@ -34,8 +36,11 @@ class SearchResultController @Inject constructor(
|
||||||
|
|
||||||
var listener: Listener? = null
|
var listener: Listener? = null
|
||||||
|
|
||||||
|
private var idx = 0
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun onItemClicked(event: Event)
|
fun onItemClicked(event: Event)
|
||||||
|
fun loadMore()
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -45,6 +50,18 @@ class SearchResultController @Inject constructor(
|
||||||
override fun buildModels(data: SearchViewState?) {
|
override fun buildModels(data: SearchViewState?) {
|
||||||
data?.searchResult?.results ?: return
|
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!!)
|
buildSearchResultItems(data.searchResult.results!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,5 +20,4 @@ import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
|
||||||
sealed class SearchViewEvents : VectorViewEvents {
|
sealed class SearchViewEvents : VectorViewEvents {
|
||||||
data class Failure(val throwable: Throwable) : SearchViewEvents()
|
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
|
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.FragmentViewModelContext
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
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.Session
|
||||||
import org.matrix.android.sdk.api.session.search.SearchResult
|
import org.matrix.android.sdk.api.session.search.SearchResult
|
||||||
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
|
|
||||||
class SearchViewModel @AssistedInject constructor(
|
class SearchViewModel @AssistedInject constructor(
|
||||||
@Assisted private val initialState: SearchViewState,
|
@Assisted private val initialState: SearchViewState,
|
||||||
|
@ -49,7 +54,7 @@ class SearchViewModel @AssistedInject constructor(
|
||||||
override fun handle(action: SearchAction) {
|
override fun handle(action: SearchAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is SearchAction.SearchWith -> handleSearchWith(action)
|
is SearchAction.SearchWith -> handleSearchWith(action)
|
||||||
is SearchAction.ScrolledToTop -> handleScrolledToTop()
|
is SearchAction.LoadMore -> handleLoadMore()
|
||||||
is SearchAction.Retry -> handleRetry()
|
is SearchAction.Retry -> handleRetry()
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
@ -57,17 +62,13 @@ class SearchViewModel @AssistedInject constructor(
|
||||||
private fun handleSearchWith(action: SearchAction.SearchWith) {
|
private fun handleSearchWith(action: SearchAction.SearchWith) {
|
||||||
if (action.searchTerm.length > 1) {
|
if (action.searchTerm.length > 1) {
|
||||||
setState {
|
setState {
|
||||||
copy(searchTerm = action.searchTerm, isNextBatch = false)
|
copy(searchTerm = action.searchTerm)
|
||||||
}
|
}
|
||||||
|
|
||||||
startSearching()
|
startSearching()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleScrolledToTop() {
|
private fun handleLoadMore() {
|
||||||
setState {
|
|
||||||
copy(isNextBatch = true)
|
|
||||||
}
|
|
||||||
startSearching(true)
|
startSearching(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,14 +76,24 @@ class SearchViewModel @AssistedInject constructor(
|
||||||
startSearching()
|
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
|
if (state.roomId == null || state.searchTerm == null) return@withState
|
||||||
|
|
||||||
// There is no batch to retrieve
|
// 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
|
session
|
||||||
.getRoom(state.roomId)
|
.getRoom(state.roomId)
|
||||||
?.search(
|
?.search(
|
||||||
|
@ -93,26 +104,23 @@ class SearchViewModel @AssistedInject constructor(
|
||||||
afterLimit = 0,
|
afterLimit = 0,
|
||||||
includeProfile = true,
|
includeProfile = true,
|
||||||
limit = 20,
|
limit = 20,
|
||||||
callback = object : MatrixCallback<SearchResult> {
|
callback = it
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
onSearchFailure(failure)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: SearchResult) {
|
|
||||||
onSearchResultSuccess(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onSearchResultSuccess(result, isNextBatch)
|
||||||
private fun onSearchFailure(failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
setState {
|
|
||||||
copy(searchResult = null)
|
|
||||||
}
|
|
||||||
_viewEvents.post(SearchViewEvents.Failure(failure))
|
_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(
|
val accumulatedResult = SearchResult(
|
||||||
nextBatch = searchResult.nextBatch,
|
nextBatch = searchResult.nextBatch,
|
||||||
results = searchResult.results,
|
results = searchResult.results,
|
||||||
|
@ -120,7 +128,7 @@ class SearchViewModel @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
// Accumulate results if it is the next batch
|
// Accumulate results if it is the next batch
|
||||||
if (state.isNextBatch) {
|
if (isNextBatch) {
|
||||||
if (state.searchResult != null) {
|
if (state.searchResult != null) {
|
||||||
accumulatedResult.results = accumulatedResult.results?.plus(state.searchResult.results!!)
|
accumulatedResult.results = accumulatedResult.results?.plus(state.searchResult.results!!)
|
||||||
}
|
}
|
||||||
|
@ -130,7 +138,11 @@ class SearchViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
setState {
|
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
|
package im.vector.app.features.home.room.detail.search
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import org.matrix.android.sdk.api.session.search.SearchResult
|
import org.matrix.android.sdk.api.session.search.SearchResult
|
||||||
|
|
||||||
data class SearchViewState(
|
data class SearchViewState(
|
||||||
|
@ -26,7 +28,8 @@ data class SearchViewState(
|
||||||
val lastBatch: SearchResult? = null,
|
val lastBatch: SearchResult? = null,
|
||||||
val searchTerm: String? = null,
|
val searchTerm: String? = null,
|
||||||
val roomId: String? = null,
|
val roomId: String? = null,
|
||||||
val isNextBatch: Boolean = false
|
// Current pagination request
|
||||||
|
val asyncEventsRequest: Async<Unit> = Uninitialized
|
||||||
) : MvRxState {
|
) : MvRxState {
|
||||||
|
|
||||||
constructor(args: SearchArgs) : this(roomId = args.roomId)
|
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"
|
<im.vector.app.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/stateView"
|
android:id="@+id/stateView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:background="?riotx_header_panel_background">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/searchResultRecycler"
|
android:id="@+id/searchResultRecycler"
|
||||||
|
|
Loading…
Reference in New Issue