Support searching and pagination.
This commit is contained in:
parent
5e56e7cf82
commit
62449ee543
|
@ -19,12 +19,24 @@ package im.vector.app.features.home.room.detail.search
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
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 im.vector.app.R
|
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
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import timber.log.Timber
|
import kotlinx.android.synthetic.main.fragment_search.*
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@ -33,19 +45,94 @@ data class SearchArgs(
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
class SearchFragment @Inject constructor(
|
class SearchFragment @Inject constructor(
|
||||||
val viewModelFactory: SearchViewModel.Factory
|
val viewModelFactory: SearchViewModel.Factory,
|
||||||
) : VectorBaseFragment() {
|
val controller: SearchResultController,
|
||||||
|
val stringProvider: StringProvider
|
||||||
|
) : VectorBaseFragment(), StateView.EventCallback, SearchResultController.Listener {
|
||||||
|
|
||||||
private val fragmentArgs: SearchArgs by args()
|
private val fragmentArgs: SearchArgs by args()
|
||||||
private val searchViewModel: SearchViewModel by fragmentViewModel()
|
private val searchViewModel: SearchViewModel by fragmentViewModel()
|
||||||
|
|
||||||
|
private var pendingScrollToPosition: Int? = null
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_search
|
override fun getLayoutResId() = R.layout.fragment_search
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
stateView.contentView = searchResultRecycler
|
||||||
|
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)
|
||||||
|
controller.listener = this
|
||||||
|
|
||||||
|
controller.addModelBuildListener {
|
||||||
|
pendingScrollToPosition?.let {
|
||||||
|
searchResultRecycler.scrollToPosition(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() {
|
||||||
|
super.onDestroy()
|
||||||
|
searchResultRecycler?.cleanup()
|
||||||
|
controller.listener = null
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
stateView.state = StateView.State.Empty(
|
||||||
|
title = stringProvider.getString(R.string.search_no_results),
|
||||||
|
image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_search)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(query: String) {
|
fun search(query: String) {
|
||||||
Timber.d(query)
|
view?.hideKeyboard()
|
||||||
|
searchViewModel.handle(SearchAction.SearchWith(fragmentArgs.roomId, query))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRetryClicked() {
|
||||||
|
searchViewModel.handle(SearchAction.Retry)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClicked(event: Event) {
|
||||||
|
event.roomId ?: return
|
||||||
|
|
||||||
|
navigator.openRoom(requireContext(), event.roomId!!, event.eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,15 @@ import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
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.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.search.SearchResult
|
||||||
|
|
||||||
class SearchViewModel @AssistedInject constructor(
|
class SearchViewModel @AssistedInject constructor(
|
||||||
@Assisted private val initialState: SearchViewState
|
@Assisted private val initialState: SearchViewState,
|
||||||
|
private val session: Session
|
||||||
) : VectorViewModel<SearchViewState, SearchAction, SearchViewEvents>(initialState) {
|
) : VectorViewModel<SearchViewState, SearchAction, SearchViewEvents>(initialState) {
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
|
@ -42,5 +47,90 @@ class SearchViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: SearchAction) {
|
override fun handle(action: SearchAction) {
|
||||||
|
when (action) {
|
||||||
|
is SearchAction.SearchWith -> handleSearchWith(action)
|
||||||
|
is SearchAction.ScrolledToTop -> handleScrolledToTop()
|
||||||
|
is SearchAction.Retry -> handleRetry()
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSearchWith(action: SearchAction.SearchWith) {
|
||||||
|
if (action.searchTerm.length > 1) {
|
||||||
|
setState {
|
||||||
|
copy(searchTerm = action.searchTerm, roomId = action.roomId, isNextBatch = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
startSearching()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleScrolledToTop() {
|
||||||
|
setState {
|
||||||
|
copy(isNextBatch = true)
|
||||||
|
}
|
||||||
|
startSearching(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRetry() {
|
||||||
|
startSearching()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startSearching(scrolledToTop: 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
|
||||||
|
|
||||||
|
_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<SearchResult> {
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
onSearchFailure(failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: SearchResult) {
|
||||||
|
onSearchResultSuccess(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSearchFailure(failure: Throwable) {
|
||||||
|
setState {
|
||||||
|
copy(searchResult = null)
|
||||||
|
}
|
||||||
|
_viewEvents.post(SearchViewEvents.Failure(failure))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSearchResultSuccess(searchResult: SearchResult) = withState { state ->
|
||||||
|
val accumulatedResult = SearchResult(
|
||||||
|
nextBatch = searchResult.nextBatch,
|
||||||
|
results = searchResult.results,
|
||||||
|
highlights = searchResult.highlights
|
||||||
|
)
|
||||||
|
|
||||||
|
// Accumulate results if it is the next batch
|
||||||
|
if (state.isNextBatch) {
|
||||||
|
if (state.searchResult != null) {
|
||||||
|
accumulatedResult.results = accumulatedResult.results?.plus(state.searchResult.results!!)
|
||||||
|
}
|
||||||
|
if (state.searchResult?.highlights != null) {
|
||||||
|
accumulatedResult.highlights = accumulatedResult.highlights?.plus(state.searchResult.highlights!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(searchResult = accumulatedResult, lastBatch = searchResult)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue