Fix search bugs (#1624)

* fix toggling media visibility

* cleanup search code to make it more readable

* remove redundant OnQueryTextListener

this is the default behavior

* fix bookmarking

* fix status interaction causing unnecessary network requests
This commit is contained in:
Konrad Pozniak 2020-01-13 13:57:44 +01:00 committed by GitHub
parent f8c7bedfa6
commit 7cb76aad97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 101 additions and 129 deletions

View File

@ -28,13 +28,12 @@ import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.search.adapter.SearchPagerAdapter import com.keylesspalace.tusky.components.search.adapter.SearchPagerAdapter
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.android.synthetic.main.activity_search.* import kotlinx.android.synthetic.main.activity_search.*
import javax.inject.Inject import javax.inject.Inject
class SearchActivity : BottomSheetActivity(), SearchView.OnQueryTextListener, HasAndroidInjector { class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
@Inject @Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any> lateinit var androidInjector: DispatchingAndroidInjector<Any>
@ -94,14 +93,6 @@ class SearchActivity : BottomSheetActivity(), SearchView.OnQueryTextListener, Ha
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
override fun onQueryTextChange(newText: String): Boolean {
return false
}
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
private fun getPageTitle(position: Int): CharSequence? { private fun getPageTitle(position: Int): CharSequence? {
return when (position) { return when (position) {
0 -> getString(R.string.title_statuses) 0 -> getString(R.string.title_statuses)
@ -123,15 +114,12 @@ class SearchActivity : BottomSheetActivity(), SearchView.OnQueryTextListener, Ha
searchView.setSearchableInfo((getSystemService(Context.SEARCH_SERVICE) as? SearchManager)?.getSearchableInfo(componentName)) searchView.setSearchableInfo((getSystemService(Context.SEARCH_SERVICE) as? SearchManager)?.getSearchableInfo(componentName))
searchView.setOnQueryTextListener(this)
searchView.requestFocus() searchView.requestFocus()
searchView.maxWidth = Integer.MAX_VALUE searchView.maxWidth = Integer.MAX_VALUE
} }
override fun androidInjector(): AndroidInjector<Any>? { override fun androidInjector() = androidInjector
return androidInjector
}
companion object { companion object {
@JvmStatic @JvmStatic

View File

@ -3,18 +3,15 @@ package com.keylesspalace.tusky.components.search
import android.util.Log import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.paging.PagedList import androidx.paging.PagedList
import com.keylesspalace.tusky.components.search.adapter.SearchRepository import com.keylesspalace.tusky.components.search.adapter.SearchRepository
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.* import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.util.Listing import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.ViewDataUtils
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
@ -23,7 +20,8 @@ import javax.inject.Inject
class SearchViewModel @Inject constructor( class SearchViewModel @Inject constructor(
mastodonApi: MastodonApi, mastodonApi: MastodonApi,
private val timelineCases: TimelineCases, private val timelineCases: TimelineCases,
private val accountManager: AccountManager) : RxAwareViewModel() { private val accountManager: AccountManager
) : RxAwareViewModel() {
var currentQuery: String = "" var currentQuery: String = ""
@ -33,49 +31,46 @@ class SearchViewModel @Inject constructor(
accountManager.activeAccount = value accountManager.activeAccount = value
} }
val mediaPreviewEnabled: Boolean val mediaPreviewEnabled = activeAccount?.mediaPreviewEnabled ?: false
get() = activeAccount?.mediaPreviewEnabled ?: false val alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false
val alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false
private val statusesRepository = SearchRepository<Pair<Status, StatusViewData.Concrete>>(mastodonApi) private val statusesRepository = SearchRepository<Pair<Status, StatusViewData.Concrete>>(mastodonApi)
private val accountsRepository = SearchRepository<Account>(mastodonApi) private val accountsRepository = SearchRepository<Account>(mastodonApi)
private val hashtagsRepository = SearchRepository<HashTag>(mastodonApi) private val hashtagsRepository = SearchRepository<HashTag>(mastodonApi)
val alwaysShowSensitiveMedia: Boolean = activeAccount?.alwaysShowSensitiveMedia
?: false
val alwaysOpenSpoiler: Boolean = activeAccount?.alwaysOpenSpoiler
?: false
private val repoResultStatus = MutableLiveData<Listing<Pair<Status, StatusViewData.Concrete>>>() private val repoResultStatus = MutableLiveData<Listing<Pair<Status, StatusViewData.Concrete>>>()
val statuses: LiveData<PagedList<Pair<Status, StatusViewData.Concrete>>> = Transformations.switchMap(repoResultStatus) { it.pagedList } val statuses: LiveData<PagedList<Pair<Status, StatusViewData.Concrete>>> = repoResultStatus.switchMap { it.pagedList }
val networkStateStatus: LiveData<NetworkState> = Transformations.switchMap(repoResultStatus) { it.networkState } val networkStateStatus: LiveData<NetworkState> = repoResultStatus.switchMap { it.networkState }
val networkStateStatusRefresh: LiveData<NetworkState> = Transformations.switchMap(repoResultStatus) { it.refreshState } val networkStateStatusRefresh: LiveData<NetworkState> = repoResultStatus.switchMap { it.refreshState }
private val repoResultAccount = MutableLiveData<Listing<Account>>() private val repoResultAccount = MutableLiveData<Listing<Account>>()
val accounts: LiveData<PagedList<Account>> = Transformations.switchMap(repoResultAccount) { it.pagedList } val accounts: LiveData<PagedList<Account>> = repoResultAccount.switchMap { it.pagedList }
val networkStateAccount: LiveData<NetworkState> = Transformations.switchMap(repoResultAccount) { it.networkState } val networkStateAccount: LiveData<NetworkState> = repoResultAccount.switchMap { it.networkState }
val networkStateAccountRefresh: LiveData<NetworkState> = Transformations.switchMap(repoResultAccount) { it.refreshState } val networkStateAccountRefresh: LiveData<NetworkState> = repoResultAccount.switchMap { it.refreshState }
private val repoResultHashTag = MutableLiveData<Listing<HashTag>>() private val repoResultHashTag = MutableLiveData<Listing<HashTag>>()
val hashtags: LiveData<PagedList<HashTag>> = Transformations.switchMap(repoResultHashTag) { it.pagedList } val hashtags: LiveData<PagedList<HashTag>> = repoResultHashTag.switchMap { it.pagedList }
val networkStateHashTag: LiveData<NetworkState> = Transformations.switchMap(repoResultHashTag) { it.networkState } val networkStateHashTag: LiveData<NetworkState> = repoResultHashTag.switchMap { it.networkState }
val networkStateHashTagRefresh: LiveData<NetworkState> = Transformations.switchMap(repoResultHashTag) { it.refreshState } val networkStateHashTagRefresh: LiveData<NetworkState> = repoResultHashTag.switchMap { it.refreshState }
private val loadedStatuses = ArrayList<Pair<Status, StatusViewData.Concrete>>() private val loadedStatuses = ArrayList<Pair<Status, StatusViewData.Concrete>>()
fun search(query: String) { fun search(query: String) {
loadedStatuses.clear() loadedStatuses.clear()
repoResultStatus.value = statusesRepository.getSearchData(SearchType.Status, query, disposables, initialItems = loadedStatuses) { repoResultStatus.value = statusesRepository.getSearchData(SearchType.Status, query, disposables, initialItems = loadedStatuses) {
(it?.statuses?.map { status -> Pair(status, ViewDataUtils.statusToViewData(status, alwaysShowSensitiveMedia, alwaysOpenSpoiler)!!) } it?.statuses?.map { status -> Pair(status, ViewDataUtils.statusToViewData(status, alwaysShowSensitiveMedia, alwaysOpenSpoiler)!!) }
?: emptyList()) .orEmpty()
.apply { .apply {
loadedStatuses.addAll(this) loadedStatuses.addAll(this)
} }
} }
repoResultAccount.value = accountsRepository.getSearchData(SearchType.Account, query, disposables) { repoResultAccount.value = accountsRepository.getSearchData(SearchType.Account, query, disposables) {
it?.accounts ?: emptyList() it?.accounts.orEmpty()
} }
val hashtagQuery = if (query.startsWith("#")) query else "#$query" val hashtagQuery = if (query.startsWith("#")) query else "#$query"
repoResultHashTag.value = repoResultHashTag.value =
hashtagsRepository.getSearchData(SearchType.Hashtag, hashtagQuery, disposables) { hashtagsRepository.getSearchData(SearchType.Hashtag, hashtagQuery, disposables) {
it?.hashtags ?: emptyList() it?.hashtags.orEmpty()
} }
} }
@ -184,11 +179,11 @@ class SearchViewModel @Inject constructor(
fun bookmark(status: Pair<Status, StatusViewData.Concrete>, isBookmarked: Boolean) { fun bookmark(status: Pair<Status, StatusViewData.Concrete>, isBookmarked: Boolean) {
val idx = loadedStatuses.indexOf(status) val idx = loadedStatuses.indexOf(status)
if (idx >= 0) { if (idx >= 0) {
val newPair = Pair(status.first, StatusViewData.Builder(status.second).setFavourited(isBookmarked).createStatusViewData()) val newPair = Pair(status.first, StatusViewData.Builder(status.second).setBookmarked(isBookmarked).createStatusViewData())
loadedStatuses[idx] = newPair loadedStatuses[idx] = newPair
repoResultStatus.value?.refresh?.invoke() repoResultStatus.value?.refresh?.invoke()
} }
timelineCases.favourite(status.first, isBookmarked) timelineCases.bookmark(status.first, isBookmarked)
.onErrorReturnItem(status.first) .onErrorReturnItem(status.first)
.subscribe() .subscribe()
.autoDispose() .autoDispose()

View File

@ -26,8 +26,7 @@ import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
class SearchAccountsAdapter(private val linkListener: LinkListener) class SearchAccountsAdapter(private val linkListener: LinkListener)
: PagedListAdapter<Account, RecyclerView.ViewHolder>(STATUS_COMPARATOR) { : PagedListAdapter<Account, RecyclerView.ViewHolder>(ACCOUNT_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
@ -37,21 +36,16 @@ class SearchAccountsAdapter(private val linkListener: LinkListener)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { item -> getItem(position)?.let { item ->
(holder as? AccountViewHolder)?.apply { (holder as AccountViewHolder).apply {
setupWithAccount(item) setupWithAccount(item)
setupLinkListener(linkListener) setupLinkListener(linkListener)
} }
} }
}
public override fun getItem(position: Int): Account? {
return super.getItem(position)
} }
companion object { companion object {
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Account>() { val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<Account>() {
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean = override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean =
oldItem.deepEquals(newItem) oldItem.deepEquals(newItem)

View File

@ -15,7 +15,6 @@
package com.keylesspalace.tusky.components.search.adapter package com.keylesspalace.tusky.components.search.adapter
import android.annotation.SuppressLint
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.paging.PositionalDataSource import androidx.paging.PositionalDataSource
import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.components.search.SearchType
@ -23,16 +22,18 @@ import com.keylesspalace.tusky.entity.SearchResult
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import java.util.concurrent.Executor import java.util.concurrent.Executor
class SearchDataSource<T>( class SearchDataSource<T>(
private val mastodonApi: MastodonApi, private val mastodonApi: MastodonApi,
private val searchType: SearchType, private val searchType: SearchType,
private val searchRequest: String?, private val searchRequest: String,
private val disposables: CompositeDisposable, private val disposables: CompositeDisposable,
private val retryExecutor: Executor, private val retryExecutor: Executor,
private val initialItems: List<T>? = null, private val initialItems: List<T>? = null,
private val parser: (SearchResult?) -> List<T>) : PositionalDataSource<T>() { private val parser: (SearchResult?) -> List<T>,
private val source: SearchDataSourceFactory<T>) : PositionalDataSource<T>() {
val networkState = MutableLiveData<NetworkState>() val networkState = MutableLiveData<NetworkState>()
@ -48,24 +49,20 @@ class SearchDataSource<T>(
} }
} }
@SuppressLint("CheckResult")
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) { override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) {
if (!initialItems.isNullOrEmpty()) { if (!initialItems.isNullOrEmpty()) {
callback.onResult(initialItems, 0) callback.onResult(initialItems.toList(), 0)
} else { } else {
networkState.postValue(NetworkState.LOADED) networkState.postValue(NetworkState.LOADED)
retry = null retry = null
initialLoad.postValue(NetworkState.LOADING) initialLoad.postValue(NetworkState.LOADING)
mastodonApi.searchObservable( mastodonApi.searchObservable(
query = searchRequest ?: "", query = searchRequest,
type = searchType.apiParameter, type = searchType.apiParameter,
resolve = true, resolve = true,
limit = params.requestedLoadSize, limit = params.requestedLoadSize,
offset = 0, offset = 0,
following =false) following =false)
.doOnSubscribe {
disposables.add(it)
}
.subscribe( .subscribe(
{ data -> { data ->
val res = parser(data) val res = parser(data)
@ -79,19 +76,18 @@ class SearchDataSource<T>(
} }
initialLoad.postValue(NetworkState.error(error.message)) initialLoad.postValue(NetworkState.error(error.message))
} }
) ).addTo(disposables)
} }
} }
@SuppressLint("CheckResult")
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) { override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) {
networkState.postValue(NetworkState.LOADING) networkState.postValue(NetworkState.LOADING)
retry = null retry = null
mastodonApi.searchObservable(searchType.apiParameter, searchRequest, true, params.loadSize, params.startPosition, false) if(source.exhausted) {
.doOnSubscribe { return callback.onResult(emptyList())
disposables.add(it)
} }
mastodonApi.searchObservable(searchType.apiParameter, searchRequest, true, params.loadSize, params.startPosition, false)
.subscribe( .subscribe(
{ data -> { data ->
// Working around Mastodon bug where exact match is returned no matter // Working around Mastodon bug where exact match is returned no matter
@ -105,9 +101,11 @@ class SearchDataSource<T>(
} else { } else {
parser(data) parser(data)
} }
if(res.isEmpty()) {
source.exhausted = true
}
callback.onResult(res) callback.onResult(res)
networkState.postValue(NetworkState.LOADED) networkState.postValue(NetworkState.LOADED)
}, },
{ error -> { error ->
retry = { retry = {
@ -115,7 +113,7 @@ class SearchDataSource<T>(
} }
networkState.postValue(NetworkState.error(error.message)) networkState.postValue(NetworkState.error(error.message))
} }
) ).addTo(disposables)
} }

View File

@ -26,14 +26,18 @@ import java.util.concurrent.Executor
class SearchDataSourceFactory<T>( class SearchDataSourceFactory<T>(
private val mastodonApi: MastodonApi, private val mastodonApi: MastodonApi,
private val searchType: SearchType, private val searchType: SearchType,
private val searchRequest: String?, private val searchRequest: String,
private val disposables: CompositeDisposable, private val disposables: CompositeDisposable,
private val retryExecutor: Executor, private val retryExecutor: Executor,
private val cacheData: List<T>? = null, private val cacheData: List<T>? = null,
private val parser: (SearchResult?) -> List<T>) : DataSource.Factory<Int, T>() { private val parser: (SearchResult?) -> List<T>) : DataSource.Factory<Int, T>() {
val sourceLiveData = MutableLiveData<SearchDataSource<T>>() val sourceLiveData = MutableLiveData<SearchDataSource<T>>()
var exhausted = false
override fun create(): DataSource<Int, T> { override fun create(): DataSource<Int, T> {
val source = SearchDataSource(mastodonApi, searchType, searchRequest, disposables, retryExecutor, cacheData, parser) val source = SearchDataSource(mastodonApi, searchType, searchRequest, disposables, retryExecutor, cacheData, parser, this)
sourceLiveData.postValue(source) sourceLiveData.postValue(source)
return source return source
} }

View File

@ -26,8 +26,7 @@ import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
class SearchHashtagsAdapter(private val linkListener: LinkListener) class SearchHashtagsAdapter(private val linkListener: LinkListener)
: PagedListAdapter<HashTag, RecyclerView.ViewHolder>(STATUS_COMPARATOR) { : PagedListAdapter<HashTag, RecyclerView.ViewHolder>(HASHTAG_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
@ -36,17 +35,14 @@ class SearchHashtagsAdapter(private val linkListener: LinkListener)
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { item -> getItem(position)?.let { (name) ->
(holder as? HashtagViewHolder)?.apply { (holder as HashtagViewHolder).setup(name, linkListener)
setup(item.name, linkListener)
} }
} }
}
companion object { companion object {
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<HashTag>() { val HASHTAG_COMPARATOR = object : DiffUtil.ItemCallback<HashTag>() {
override fun areContentsTheSame(oldItem: HashTag, newItem: HashTag): Boolean = override fun areContentsTheSame(oldItem: HashTag, newItem: HashTag): Boolean =
oldItem.name == newItem.name oldItem.name == newItem.name

View File

@ -29,7 +29,7 @@ class SearchRepository<T>(private val mastodonApi: MastodonApi) {
private val executor = Executors.newSingleThreadExecutor() private val executor = Executors.newSingleThreadExecutor()
fun getSearchData(searchType: SearchType, searchRequest: String?, disposables: CompositeDisposable, pageSize: Int = 20, fun getSearchData(searchType: SearchType, searchRequest: String, disposables: CompositeDisposable, pageSize: Int = 20,
initialItems: List<T>? = null, parser: (SearchResult?) -> List<T>): Listing<T> { initialItems: List<T>? = null, parser: (SearchResult?) -> List<T>): Listing<T> {
val sourceFactory = SearchDataSourceFactory(mastodonApi, searchType, searchRequest, disposables, executor, initialItems, parser) val sourceFactory = SearchDataSourceFactory(mastodonApi, searchType, searchRequest, disposables, executor, initialItems, parser)
val livePagedList = sourceFactory.toLiveData( val livePagedList = sourceFactory.toLiveData(

View File

@ -32,7 +32,6 @@ class SearchStatusesAdapter(
private val statusListener: StatusActionListener private val statusListener: StatusActionListener
) : PagedListAdapter<Pair<Status, StatusViewData.Concrete>, RecyclerView.ViewHolder>(STATUS_COMPARATOR) { ) : PagedListAdapter<Pair<Status, StatusViewData.Concrete>, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_status, parent, false) .inflate(R.layout.item_status, parent, false)
@ -41,8 +40,7 @@ class SearchStatusesAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { item -> getItem(position)?.let { item ->
(holder as? StatusViewHolder)?.setupWithStatus(item.second, statusListener, (holder as StatusViewHolder).setupWithStatus(item.second, statusListener, statusDisplayOptions)
statusDisplayOptions)
} }
} }

View File

@ -12,6 +12,7 @@ import androidx.paging.PagedList
import androidx.paging.PagedListAdapter import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.AccountActivity import com.keylesspalace.tusky.AccountActivity
@ -62,8 +63,8 @@ abstract class SearchFragment<T> : Fragment(),
swipeRefreshLayout.setOnRefreshListener(this) swipeRefreshLayout.setOnRefreshListener(this)
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeRefreshLayout.setProgressBackgroundColorSchemeColor( swipeRefreshLayout.setProgressBackgroundColorSchemeColor(
ThemeUtils.getColor(swipeRefreshLayout.context, android.R.attr.colorBackground)) ThemeUtils.getColor(swipeRefreshLayout.context, android.R.attr.colorBackground)
)
} }
private fun subscribeObservables() { private fun subscribeObservables() {
@ -75,8 +76,9 @@ abstract class SearchFragment<T> : Fragment(),
searchProgressBar.visible(it == NetworkState.LOADING) searchProgressBar.visible(it == NetworkState.LOADING)
if (it.status == Status.FAILED) if (it.status == Status.FAILED) {
showError(it.msg) showError()
}
checkNoData() checkNoData()
}) })
@ -85,8 +87,9 @@ abstract class SearchFragment<T> : Fragment(),
progressBarBottom.visible(it == NetworkState.LOADING) progressBarBottom.visible(it == NetworkState.LOADING)
if (it.status == Status.FAILED) if (it.status == Status.FAILED) {
showError(it.msg) showError()
}
}) })
} }
@ -99,7 +102,8 @@ abstract class SearchFragment<T> : Fragment(),
searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context) searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context)
adapter = createAdapter() adapter = createAdapter()
searchRecyclerView.adapter = adapter searchRecyclerView.adapter = adapter
searchRecyclerView.setHasFixedSize(true)
(searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
} }
private fun showNoData(isEmpty: Boolean) { private fun showNoData(isEmpty: Boolean) {
@ -109,7 +113,7 @@ abstract class SearchFragment<T> : Fragment(),
searchNoResultsText.hide() searchNoResultsText.hide()
} }
private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) { private fun showError() {
if (snackbarErrorRetry?.isShown != true) { if (snackbarErrorRetry?.isShown != true) {
snackbarErrorRetry = Snackbar.make(layoutRoot, R.string.failed_search, Snackbar.LENGTH_INDEFINITE) snackbarErrorRetry = Snackbar.make(layoutRoot, R.string.failed_search, Snackbar.LENGTH_INDEFINITE)
snackbarErrorRetry?.setAction(R.string.action_retry) { snackbarErrorRetry?.setAction(R.string.action_retry) {
@ -135,7 +139,6 @@ abstract class SearchFragment<T> : Fragment(),
// Dismissed here because the RecyclerView bottomProgressBar is shown as soon as the retry begins. // Dismissed here because the RecyclerView bottomProgressBar is shown as soon as the retry begins.
swipeRefreshLayout.post { swipeRefreshLayout.post {
swipeRefreshLayout.isRefreshing = false swipeRefreshLayout.isRefreshing = false
} }
viewModel.retryAllSearches() viewModel.retryAllSearches()

View File

@ -59,7 +59,6 @@ import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose import com.uber.autodispose.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_search.* import kotlinx.android.synthetic.main.fragment_search.*
import java.util.*
class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concrete>>(), StatusActionListener { class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concrete>>(), StatusActionListener {
@ -70,6 +69,9 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
override val data: LiveData<PagedList<Pair<Status, StatusViewData.Concrete>>> override val data: LiveData<PagedList<Pair<Status, StatusViewData.Concrete>>>
get() = viewModel.statuses get() = viewModel.statuses
private val searchAdapter
get() = super.adapter as SearchStatusesAdapter
override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> { override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context)
val statusDisplayOptions = StatusDisplayOptions( val statusDisplayOptions = StatusDisplayOptions(
@ -87,37 +89,37 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
override fun onContentHiddenChange(isShowing: Boolean, position: Int) { override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let { searchAdapter.getItem(position)?.let {
viewModel.contentHiddenChange(it, isShowing) viewModel.contentHiddenChange(it, isShowing)
} }
} }
override fun onReply(position: Int) { override fun onReply(position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let { status -> searchAdapter.getItem(position)?.first?.let { status ->
reply(status) reply(status)
} }
} }
override fun onFavourite(favourite: Boolean, position: Int) { override fun onFavourite(favourite: Boolean, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let { status -> searchAdapter.getItem(position)?.let { status ->
viewModel.favorite(status, favourite) viewModel.favorite(status, favourite)
} }
} }
override fun onBookmark(bookmark: Boolean, position: Int) { override fun onBookmark(bookmark: Boolean, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let { status -> searchAdapter.getItem(position)?.let { status ->
viewModel.bookmark(status, bookmark) viewModel.bookmark(status, bookmark)
} }
} }
override fun onMore(view: View, position: Int) { override fun onMore(view: View, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let { searchAdapter.getItem(position)?.first?.let {
more(it, view, position) more(it, view, position)
} }
} }
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.actionableStatus?.let { actionable -> searchAdapter.getItem(position)?.first?.actionableStatus?.let { actionable ->
when (actionable.attachments[attachmentIndex].type) { when (actionable.attachments[attachmentIndex].type) {
Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> {
val attachments = AttachmentViewData.list(actionable) val attachments = AttachmentViewData.list(actionable)
@ -142,48 +144,48 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
} }
override fun onViewThread(position: Int) { override fun onViewThread(position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let { status -> searchAdapter.getItem(position)?.first?.let { status ->
val actionableStatus = status.actionableStatus val actionableStatus = status.actionableStatus
bottomSheetActivity?.viewThread(actionableStatus.id, actionableStatus.url) bottomSheetActivity?.viewThread(actionableStatus.id, actionableStatus.url)
} }
} }
override fun onOpenReblog(position: Int) { override fun onOpenReblog(position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let { status -> searchAdapter.getItem(position)?.first?.let { status ->
bottomSheetActivity?.viewAccount(status.account.id) bottomSheetActivity?.viewAccount(status.account.id)
} }
} }
override fun onExpandedChange(expanded: Boolean, position: Int) { override fun onExpandedChange(expanded: Boolean, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let { searchAdapter.getItem(position)?.let {
viewModel.expandedChange(it, expanded) viewModel.expandedChange(it, expanded)
} }
} }
override fun onLoadMore(position: Int) { override fun onLoadMore(position: Int) {
//Ignore // Not possible here
} }
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let { searchAdapter.getItem(position)?.let {
viewModel.collapsedChange(it, isCollapsed) viewModel.collapsedChange(it, isCollapsed)
} }
} }
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) { override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let { searchAdapter.getItem(position)?.let {
viewModel.voteInPoll(it, choices) viewModel.voteInPoll(it, choices)
} }
} }
private fun removeItem(position: Int) { private fun removeItem(position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let { searchAdapter.getItem(position)?.let {
viewModel.removeItem(it) viewModel.removeItem(it)
} }
} }
override fun onReblog(reblog: Boolean, position: Int) { override fun onReblog(reblog: Boolean, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.let { status -> searchAdapter.getItem(position)?.let { status ->
viewModel.reblog(status, reblog) viewModel.reblog(status, reblog)
} }
} }
@ -193,27 +195,23 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
} }
private fun reply(status: Status) { private fun reply(status: Status) {
val inReplyToId = status.actionableId
val actionableStatus = status.actionableStatus val actionableStatus = status.actionableStatus
val replyVisibility = actionableStatus.visibility val mentionedUsernames = actionableStatus.mentions.map { it.username }
val contentWarning = actionableStatus.spoilerText .toMutableSet()
val mentions = actionableStatus.mentions .apply {
val mentionedUsernames = LinkedHashSet<String>() add(actionableStatus.account.username)
mentionedUsernames.add(actionableStatus.account.username) remove(viewModel.activeAccount?.username)
val loggedInUsername = viewModel.activeAccount?.username
for ((_, _, username) in mentions) {
mentionedUsernames.add(username)
} }
mentionedUsernames.remove(loggedInUsername)
val intent = ComposeActivity.startIntent(context!!, ComposeOptions( val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions(
inReplyToId = inReplyToId, inReplyToId = status.actionableId,
replyVisibility = replyVisibility, replyVisibility = actionableStatus.visibility,
contentWarning = contentWarning, contentWarning = actionableStatus.spoilerText,
mentionedUsernames = mentionedUsernames, mentionedUsernames = mentionedUsernames,
replyingStatusAuthor = actionableStatus.account.localUsername, replyingStatusAuthor = actionableStatus.account.localUsername,
replyingStatusContent = actionableStatus.content.toString() replyingStatusContent = actionableStatus.content.toString()
)) ))
requireActivity().startActivity(intent) startActivity(intent)
} }
private fun more(status: Status, view: View, position: Int) { private fun more(status: Status, view: View, position: Int) {
@ -252,8 +250,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
} }
} }
val menu = popup.menu val openAsItem = popup.menu.findItem(R.id.status_open_as)
val openAsItem = menu.findItem(R.id.status_open_as)
when (accounts.size) { when (accounts.size) {
0, 1 -> openAsItem.isVisible = false 0, 1 -> openAsItem.isVisible = false
2 -> for (account in accounts) { 2 -> for (account in accounts) {
@ -269,13 +266,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
popup.setOnMenuItemClickListener { item -> popup.setOnMenuItemClickListener { item ->
when (item.itemId) { when (item.itemId) {
R.id.status_share_content -> { R.id.status_share_content -> {
var statusToShare: Status? = status val statusToShare: Status = status.actionableStatus
if (statusToShare!!.reblog != null) statusToShare = statusToShare.reblog
val sendIntent = Intent() val sendIntent = Intent()
sendIntent.action = Intent.ACTION_SEND sendIntent.action = Intent.ACTION_SEND
val stringToShare = statusToShare!!.account.username + val stringToShare = statusToShare.account.username +
" - " + " - " +
statusToShare.content.toString() statusToShare.content.toString()
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare) sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare)
@ -292,7 +288,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
} }
R.id.status_copy_link -> { R.id.status_copy_link -> {
val clipboard = activity!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText(null, statusUrl)) clipboard.setPrimaryClip(ClipData.newPlainText(null, statusUrl))
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
} }
@ -365,7 +361,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
val uri = Uri.parse(url) val uri = Uri.parse(url)
val filename = uri.lastPathSegment val filename = uri.lastPathSegment
val downloadManager = activity!!.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager val downloadManager = requireActivity().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(uri) val request = DownloadManager.Request(uri)
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename) request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
downloadManager.enqueue(request) downloadManager.enqueue(request)
@ -417,7 +413,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
deletedStatus deletedStatus
} }
val intent = ComposeActivity.startIntent(context!!, ComposeOptions( val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions(
tootText = redraftStatus.text ?: "", tootText = redraftStatus.text ?: "",
inReplyToId = redraftStatus.inReplyToId, inReplyToId = redraftStatus.inReplyToId,
visibility = redraftStatus.visibility, visibility = redraftStatus.visibility,