migrate to paging 3 (#2182)
* migrate conversations and search to paging 3 * delete SearchRepository * remove unneeded executor from search * fix bugs in conversations * update license headers * fix conversations refreshing * fix search refresh indicators * show fullscreen loading while conversations are empty * search bugfixes * error handling * error handling * remove mastodon bug workaround * update ConversationsFragment * fix conversations more menu and deleting conversations * delete unused class * catch exceptions in ConversationsViewModel * fix bug where items are not diffed correctly / cleanup code * fix search progressbar display conditions
This commit is contained in:
parent
31da851f28
commit
6d4f5ad027
32 changed files with 1612 additions and 1022 deletions
|
|
@ -1,3 +1,18 @@
|
|||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.search
|
||||
|
||||
enum class SearchType(val apiParameter: String) {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,35 @@
|
|||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.search
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.PagedList
|
||||
import com.keylesspalace.tusky.components.search.adapter.SearchRepository
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import com.keylesspalace.tusky.components.search.adapter.SearchPagingSourceFactory
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.entity.*
|
||||
import com.keylesspalace.tusky.entity.DeletedStatus
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.TimelineCases
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||
import com.keylesspalace.tusky.util.toViewData
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
|
|
@ -35,82 +53,62 @@ class SearchViewModel @Inject constructor(
|
|||
val alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false
|
||||
val alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false
|
||||
|
||||
private val statusesRepository =
|
||||
SearchRepository<Pair<Status, StatusViewData.Concrete>>(mastodonApi)
|
||||
private val accountsRepository = SearchRepository<Account>(mastodonApi)
|
||||
private val hashtagsRepository = SearchRepository<HashTag>(mastodonApi)
|
||||
private val loadedStatuses: MutableList<Pair<Status, StatusViewData.Concrete>> = mutableListOf()
|
||||
|
||||
private val repoResultStatus = MutableLiveData<Listing<Pair<Status, StatusViewData.Concrete>>>()
|
||||
val statuses: LiveData<PagedList<Pair<Status, StatusViewData.Concrete>>> =
|
||||
repoResultStatus.switchMap { it.pagedList }
|
||||
val networkStateStatus: LiveData<NetworkState> = repoResultStatus.switchMap { it.networkState }
|
||||
val networkStateStatusRefresh: LiveData<NetworkState> =
|
||||
repoResultStatus.switchMap { it.refreshState }
|
||||
private val statusesPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Status, loadedStatuses) {
|
||||
it.statuses.map { status -> Pair(status, status.toViewData(alwaysShowSensitiveMedia, alwaysOpenSpoiler)) }
|
||||
.apply {
|
||||
loadedStatuses.addAll(this)
|
||||
}
|
||||
}
|
||||
private val accountsPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Account) {
|
||||
it.accounts
|
||||
}
|
||||
private val hashtagsPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Hashtag) {
|
||||
it.hashtags
|
||||
}
|
||||
|
||||
private val repoResultAccount = MutableLiveData<Listing<Account>>()
|
||||
val accounts: LiveData<PagedList<Account>> = repoResultAccount.switchMap { it.pagedList }
|
||||
val networkStateAccount: LiveData<NetworkState> =
|
||||
repoResultAccount.switchMap { it.networkState }
|
||||
val networkStateAccountRefresh: LiveData<NetworkState> =
|
||||
repoResultAccount.switchMap { it.refreshState }
|
||||
val statusesFlow = Pager(
|
||||
config = PagingConfig(pageSize = DEFAULT_LOAD_SIZE, initialLoadSize = DEFAULT_LOAD_SIZE),
|
||||
pagingSourceFactory = statusesPagingSourceFactory
|
||||
).flow
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
private val repoResultHashTag = MutableLiveData<Listing<HashTag>>()
|
||||
val hashtags: LiveData<PagedList<HashTag>> = repoResultHashTag.switchMap { it.pagedList }
|
||||
val networkStateHashTag: LiveData<NetworkState> =
|
||||
repoResultHashTag.switchMap { it.networkState }
|
||||
val networkStateHashTagRefresh: LiveData<NetworkState> =
|
||||
repoResultHashTag.switchMap { it.refreshState }
|
||||
val accountsFlow = Pager(
|
||||
config = PagingConfig(pageSize = DEFAULT_LOAD_SIZE, initialLoadSize = DEFAULT_LOAD_SIZE),
|
||||
pagingSourceFactory = accountsPagingSourceFactory
|
||||
).flow
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
val hashtagsFlow = Pager(
|
||||
config = PagingConfig(pageSize = DEFAULT_LOAD_SIZE, initialLoadSize = DEFAULT_LOAD_SIZE),
|
||||
pagingSourceFactory = hashtagsPagingSourceFactory
|
||||
).flow
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
private val loadedStatuses = ArrayList<Pair<Status, StatusViewData.Concrete>>()
|
||||
fun search(query: String) {
|
||||
loadedStatuses.clear()
|
||||
repoResultStatus.value = statusesRepository.getSearchData(
|
||||
SearchType.Status,
|
||||
query,
|
||||
disposables,
|
||||
initialItems = loadedStatuses
|
||||
) {
|
||||
it?.statuses?.map { status ->
|
||||
Pair(
|
||||
status,
|
||||
status.toViewData(alwaysShowSensitiveMedia, alwaysOpenSpoiler)
|
||||
)
|
||||
}
|
||||
.orEmpty()
|
||||
.apply {
|
||||
loadedStatuses.addAll(this)
|
||||
}
|
||||
}
|
||||
repoResultAccount.value =
|
||||
accountsRepository.getSearchData(SearchType.Account, query, disposables) {
|
||||
it?.accounts.orEmpty()
|
||||
}
|
||||
val hashtagQuery = if (query.startsWith("#")) query else "#$query"
|
||||
repoResultHashTag.value =
|
||||
hashtagsRepository.getSearchData(SearchType.Hashtag, hashtagQuery, disposables) {
|
||||
it?.hashtags.orEmpty()
|
||||
}
|
||||
|
||||
statusesPagingSourceFactory.newSearch(query)
|
||||
accountsPagingSourceFactory.newSearch(query)
|
||||
hashtagsPagingSourceFactory.newSearch(query)
|
||||
}
|
||||
|
||||
fun removeItem(status: Pair<Status, StatusViewData.Concrete>) {
|
||||
timelineCases.delete(status.first.id)
|
||||
.subscribe({
|
||||
if (loadedStatuses.remove(status))
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
}, { err ->
|
||||
Log.d(TAG, "Failed to delete status", err)
|
||||
})
|
||||
.autoDispose()
|
||||
|
||||
.subscribe({
|
||||
if (loadedStatuses.remove(status))
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}, {
|
||||
err -> Log.d(TAG, "Failed to delete status", err)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun expandedChange(status: Pair<Status, StatusViewData.Concrete>, expanded: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
if (idx >= 0) {
|
||||
val newPair = Pair(status.first, status.second.copy(isExpanded = expanded))
|
||||
loadedStatuses[idx] = newPair
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
loadedStatuses[idx] = Pair(status.first, status.second.copy(isExpanded = expanded))
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,36 +117,30 @@ class SearchViewModel @Inject constructor(
|
|||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ setRebloggedForStatus(status, reblog) },
|
||||
{ err -> Log.d(TAG, "Failed to reblog status ${status.first.id}", err) }
|
||||
{ t -> Log.d(TAG, "Failed to reblog status ${status.first.id}", t) }
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
private fun setRebloggedForStatus(
|
||||
status: Pair<Status, StatusViewData.Concrete>,
|
||||
reblog: Boolean
|
||||
) {
|
||||
private fun setRebloggedForStatus(status: Pair<Status, StatusViewData.Concrete>, reblog: Boolean) {
|
||||
status.first.reblogged = reblog
|
||||
status.first.reblog?.reblogged = reblog
|
||||
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
|
||||
fun contentHiddenChange(status: Pair<Status, StatusViewData.Concrete>, isShowing: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
if (idx >= 0) {
|
||||
val newPair = Pair(status.first, status.second.copy(isShowingContent = isShowing))
|
||||
loadedStatuses[idx] = newPair
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
loadedStatuses[idx] = Pair(status.first, status.second.copy(isShowingContent = isShowing))
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun collapsedChange(status: Pair<Status, StatusViewData.Concrete>, collapsed: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
if (idx >= 0) {
|
||||
val newPair = Pair(status.first, status.second.copy(isCollapsed = collapsed))
|
||||
loadedStatuses[idx] = newPair
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
loadedStatuses[idx] = Pair(status.first, status.second.copy(isCollapsed = collapsed))
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,12 +151,7 @@ class SearchViewModel @Inject constructor(
|
|||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ newPoll -> updateStatus(status, newPoll) },
|
||||
{ t ->
|
||||
Log.d(
|
||||
TAG,
|
||||
"Failed to vote in poll: ${status.first.id}", t
|
||||
)
|
||||
}
|
||||
{ t -> Log.d(TAG, "Failed to vote in poll: ${status.first.id}", t) }
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
|
@ -175,13 +162,13 @@ class SearchViewModel @Inject constructor(
|
|||
val newStatus = status.first.copy(poll = newPoll)
|
||||
val newViewData = status.second.copy(status = newStatus)
|
||||
loadedStatuses[idx] = Pair(newStatus, newViewData)
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun favorite(status: Pair<Status, StatusViewData.Concrete>, isFavorited: Boolean) {
|
||||
status.first.favourited = isFavorited
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
timelineCases.favourite(status.first.id, isFavorited)
|
||||
.onErrorReturnItem(status.first)
|
||||
.subscribe()
|
||||
|
|
@ -190,7 +177,7 @@ class SearchViewModel @Inject constructor(
|
|||
|
||||
fun bookmark(status: Pair<Status, StatusViewData.Concrete>, isBookmarked: Boolean) {
|
||||
status.first.bookmarked = isBookmarked
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
timelineCases.bookmark(status.first.id, isBookmarked)
|
||||
.onErrorReturnItem(status.first)
|
||||
.subscribe()
|
||||
|
|
@ -217,10 +204,6 @@ class SearchViewModel @Inject constructor(
|
|||
return timelineCases.delete(id)
|
||||
}
|
||||
|
||||
fun retryAllSearches() {
|
||||
search(currentQuery)
|
||||
}
|
||||
|
||||
fun muteConversation(status: Pair<Status, StatusViewData.Concrete>, mute: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
if (idx >= 0) {
|
||||
|
|
@ -230,7 +213,7 @@ class SearchViewModel @Inject constructor(
|
|||
status.second.copy(status = newStatus)
|
||||
)
|
||||
loadedStatuses[idx] = newPair
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
statusesPagingSourceFactory.invalidate()
|
||||
}
|
||||
timelineCases.muteConversation(status.first.id, mute)
|
||||
.onErrorReturnItem(status.first)
|
||||
|
|
@ -240,5 +223,6 @@ class SearchViewModel @Inject constructor(
|
|||
|
||||
companion object {
|
||||
private const val TAG = "SearchViewModel"
|
||||
private const val DEFAULT_LOAD_SIZE = 20
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
|
|
@ -17,26 +17,25 @@ package com.keylesspalace.tusky.components.search.adapter
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.AccountViewHolder
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
|
||||
class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean)
|
||||
: PagedListAdapter<Account, RecyclerView.ViewHolder>(ACCOUNT_COMPARATOR) {
|
||||
: PagingDataAdapter<Account, AccountViewHolder>(ACCOUNT_COMPARATOR) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_account, parent, false)
|
||||
return AccountViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
override fun onBindViewHolder(holder: AccountViewHolder, position: Int) {
|
||||
getItem(position)?.let { item ->
|
||||
(holder as AccountViewHolder).apply {
|
||||
holder.apply {
|
||||
setupWithAccount(item, animateAvatars, animateEmojis)
|
||||
setupLinkListener(linkListener)
|
||||
}
|
||||
|
|
@ -52,7 +51,5 @@ class SearchAccountsAdapter(private val linkListener: LinkListener, private val
|
|||
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.search.adapter
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.PositionalDataSource
|
||||
import com.keylesspalace.tusky.components.search.SearchType
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.addTo
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
class SearchDataSource<T>(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val searchType: SearchType,
|
||||
private val searchRequest: String,
|
||||
private val disposables: CompositeDisposable,
|
||||
private val retryExecutor: Executor,
|
||||
private val initialItems: List<T>? = null,
|
||||
private val parser: (SearchResult?) -> List<T>,
|
||||
private val source: SearchDataSourceFactory<T>) : PositionalDataSource<T>() {
|
||||
|
||||
val networkState = MutableLiveData<NetworkState>()
|
||||
|
||||
private var retry: (() -> Any)? = null
|
||||
|
||||
val initialLoad = MutableLiveData<NetworkState>()
|
||||
|
||||
fun retry() {
|
||||
retry?.let {
|
||||
retryExecutor.execute {
|
||||
it.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) {
|
||||
if (!initialItems.isNullOrEmpty()) {
|
||||
callback.onResult(initialItems.toList(), 0)
|
||||
} else {
|
||||
networkState.postValue(NetworkState.LOADED)
|
||||
retry = null
|
||||
initialLoad.postValue(NetworkState.LOADING)
|
||||
mastodonApi.searchObservable(
|
||||
query = searchRequest,
|
||||
type = searchType.apiParameter,
|
||||
resolve = true,
|
||||
limit = params.requestedLoadSize,
|
||||
offset = 0,
|
||||
following = false)
|
||||
.subscribe(
|
||||
{ data ->
|
||||
val res = parser(data)
|
||||
callback.onResult(res, params.requestedStartPosition)
|
||||
initialLoad.postValue(NetworkState.LOADED)
|
||||
|
||||
},
|
||||
{ error ->
|
||||
retry = {
|
||||
loadInitial(params, callback)
|
||||
}
|
||||
initialLoad.postValue(NetworkState.error(error.message))
|
||||
}
|
||||
).addTo(disposables)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) {
|
||||
networkState.postValue(NetworkState.LOADING)
|
||||
retry = null
|
||||
if (source.exhausted) {
|
||||
return callback.onResult(emptyList())
|
||||
}
|
||||
mastodonApi.searchObservable(
|
||||
query = searchRequest,
|
||||
type = searchType.apiParameter,
|
||||
resolve = true,
|
||||
limit = params.loadSize,
|
||||
offset = params.startPosition,
|
||||
following = false)
|
||||
.subscribe(
|
||||
{ data ->
|
||||
// Working around Mastodon bug where exact match is returned no matter
|
||||
// which offset is requested (so if we search for a full username, it's
|
||||
// infinite)
|
||||
// see https://github.com/tootsuite/mastodon/issues/11365
|
||||
// see https://github.com/tootsuite/mastodon/issues/13083
|
||||
val res = if ((data.accounts.size == 1 && data.accounts[0].username.equals(searchRequest, ignoreCase = true))
|
||||
|| (data.statuses.size == 1 && data.statuses[0].url.equals(searchRequest))) {
|
||||
listOf()
|
||||
} else {
|
||||
parser(data)
|
||||
}
|
||||
if (res.isEmpty()) {
|
||||
source.exhausted = true
|
||||
}
|
||||
callback.onResult(res)
|
||||
networkState.postValue(NetworkState.LOADED)
|
||||
},
|
||||
{ error ->
|
||||
retry = {
|
||||
loadRange(params, callback)
|
||||
}
|
||||
networkState.postValue(NetworkState.error(error.message))
|
||||
}
|
||||
).addTo(disposables)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
|
|
@ -17,7 +17,7 @@ package com.keylesspalace.tusky.components.search.adapter
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.keylesspalace.tusky.databinding.ItemHashtagBinding
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
|
|
@ -25,7 +25,7 @@ import com.keylesspalace.tusky.interfaces.LinkListener
|
|||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
|
||||
class SearchHashtagsAdapter(private val linkListener: LinkListener)
|
||||
: PagedListAdapter<HashTag, BindingHolder<ItemHashtagBinding>>(HASHTAG_COMPARATOR) {
|
||||
: PagingDataAdapter<HashTag, BindingHolder<ItemHashtagBinding>>(HASHTAG_COMPARATOR) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemHashtagBinding> {
|
||||
val binding = ItemHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
|
|
@ -48,7 +48,5 @@ class SearchHashtagsAdapter(private val linkListener: LinkListener)
|
|||
override fun areItemsTheSame(oldItem: HashTag, newItem: HashTag): Boolean =
|
||||
oldItem.name == newItem.name
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.search.adapter
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.keylesspalace.tusky.components.search.SearchType
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import kotlinx.coroutines.rx3.await
|
||||
|
||||
class SearchPagingSource<T: Any>(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val searchType: SearchType,
|
||||
private val searchRequest: String,
|
||||
private val initialItems: List<T>?,
|
||||
private val parser: (SearchResult) -> List<T>) : PagingSource<Int, T>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, T>): Int? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
|
||||
if (searchRequest.isEmpty()) {
|
||||
return LoadResult.Page(
|
||||
data = emptyList(),
|
||||
prevKey = null,
|
||||
nextKey = null
|
||||
)
|
||||
}
|
||||
|
||||
if (params.key == null && !initialItems.isNullOrEmpty()) {
|
||||
return LoadResult.Page(
|
||||
data = initialItems.toList(),
|
||||
prevKey = null,
|
||||
nextKey = initialItems.size
|
||||
)
|
||||
}
|
||||
|
||||
val currentKey = params.key ?: 0
|
||||
|
||||
try {
|
||||
|
||||
val data = mastodonApi.searchObservable(
|
||||
query = searchRequest,
|
||||
type = searchType.apiParameter,
|
||||
resolve = true,
|
||||
limit = params.loadSize,
|
||||
offset = currentKey,
|
||||
following = false
|
||||
).await()
|
||||
|
||||
val res = parser(data)
|
||||
|
||||
val nextKey = if (res.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
currentKey + res.size
|
||||
}
|
||||
|
||||
return LoadResult.Page(
|
||||
data = res,
|
||||
prevKey = null,
|
||||
nextKey = nextKey
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
|
|
@ -15,30 +15,39 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.search.adapter
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import com.keylesspalace.tusky.components.search.SearchType
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
class SearchDataSourceFactory<T>(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val searchType: SearchType,
|
||||
private val searchRequest: String,
|
||||
private val disposables: CompositeDisposable,
|
||||
private val retryExecutor: Executor,
|
||||
private val cacheData: List<T>? = null,
|
||||
private val parser: (SearchResult?) -> List<T>) : DataSource.Factory<Int, T>() {
|
||||
class SearchPagingSourceFactory<T : Any>(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val searchType: SearchType,
|
||||
private val initialItems: List<T>? = null,
|
||||
private val parser: (SearchResult) -> List<T>
|
||||
) : () -> SearchPagingSource<T> {
|
||||
|
||||
val sourceLiveData = MutableLiveData<SearchDataSource<T>>()
|
||||
private var searchRequest: String = ""
|
||||
|
||||
var exhausted = false
|
||||
private var currentSource: SearchPagingSource<T>? = null
|
||||
|
||||
override fun create(): DataSource<Int, T> {
|
||||
val source = SearchDataSource(mastodonApi, searchType, searchRequest, disposables, retryExecutor, cacheData, parser, this)
|
||||
sourceLiveData.postValue(source)
|
||||
return source
|
||||
override fun invoke(): SearchPagingSource<T> {
|
||||
return SearchPagingSource(
|
||||
mastodonApi = mastodonApi,
|
||||
searchType = searchType,
|
||||
searchRequest = searchRequest,
|
||||
initialItems = initialItems,
|
||||
parser = parser
|
||||
).also { source ->
|
||||
currentSource = source
|
||||
}
|
||||
}
|
||||
|
||||
fun newSearch(newSearchRequest: String) {
|
||||
this.searchRequest = newSearchRequest
|
||||
currentSource?.invalidate()
|
||||
}
|
||||
|
||||
fun invalidate() {
|
||||
currentSource?.invalidate()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.search.adapter
|
||||
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.Config
|
||||
import androidx.paging.toLiveData
|
||||
import com.keylesspalace.tusky.components.search.SearchType
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.Listing
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class SearchRepository<T>(private val mastodonApi: MastodonApi) {
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
fun getSearchData(searchType: SearchType, searchRequest: String, disposables: CompositeDisposable, pageSize: Int = 20,
|
||||
initialItems: List<T>? = null, parser: (SearchResult?) -> List<T>): Listing<T> {
|
||||
val sourceFactory = SearchDataSourceFactory(mastodonApi, searchType, searchRequest, disposables, executor, initialItems, parser)
|
||||
val livePagedList = sourceFactory.toLiveData(
|
||||
config = Config(pageSize = pageSize, enablePlaceholders = false, initialLoadSizeHint = pageSize * 2),
|
||||
fetchExecutor = executor
|
||||
)
|
||||
return Listing(
|
||||
pagedList = livePagedList,
|
||||
networkState = Transformations.switchMap(sourceFactory.sourceLiveData) {
|
||||
it.networkState
|
||||
},
|
||||
retry = {
|
||||
sourceFactory.sourceLiveData.value?.retry()
|
||||
},
|
||||
refresh = {
|
||||
sourceFactory.sourceLiveData.value?.invalidate()
|
||||
},
|
||||
refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) {
|
||||
it.initialLoad
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
|
|
@ -17,9 +17,8 @@ package com.keylesspalace.tusky.components.search.adapter
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.StatusViewHolder
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
|
@ -28,36 +27,34 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions
|
|||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
|
||||
class SearchStatusesAdapter(
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val statusListener: StatusActionListener
|
||||
) : PagedListAdapter<Pair<Status, StatusViewData.Concrete>, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val statusListener: StatusActionListener
|
||||
) : PagingDataAdapter<Pair<Status, StatusViewData.Concrete>, StatusViewHolder>(STATUS_COMPARATOR) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_status, parent, false)
|
||||
.inflate(R.layout.item_status, parent, false)
|
||||
return StatusViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
override fun onBindViewHolder(holder: StatusViewHolder, position: Int) {
|
||||
getItem(position)?.let { item ->
|
||||
(holder as StatusViewHolder).setupWithStatus(item.second, statusListener, statusDisplayOptions)
|
||||
holder.setupWithStatus(item.second, statusListener, statusDisplayOptions)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun getItem(position: Int): Pair<Status, StatusViewData.Concrete>? {
|
||||
return super.getItem(position)
|
||||
fun item(position: Int): Pair<Status, StatusViewData.Concrete>? {
|
||||
return getItem(position)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Pair<Status, StatusViewData.Concrete>>() {
|
||||
override fun areContentsTheSame(oldItem: Pair<Status, StatusViewData.Concrete>, newItem: Pair<Status, StatusViewData.Concrete>): Boolean =
|
||||
oldItem.second == newItem.second
|
||||
oldItem == newItem
|
||||
|
||||
override fun areItemsTheSame(oldItem: Pair<Status, StatusViewData.Concrete>, newItem: Pair<Status, StatusViewData.Concrete>): Boolean =
|
||||
oldItem.second.id == newItem.second.id
|
||||
oldItem.second.id == newItem.second.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
|
|
@ -15,17 +15,16 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.search.fragments
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class SearchAccountsFragment : SearchFragment<Account>() {
|
||||
override fun createAdapter(): PagedListAdapter<Account, *> {
|
||||
override fun createAdapter(): PagingDataAdapter<Account, *> {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
|
||||
|
||||
return SearchAccountsAdapter(
|
||||
|
|
@ -35,12 +34,8 @@ class SearchAccountsFragment : SearchFragment<Account>() {
|
|||
)
|
||||
}
|
||||
|
||||
override val networkStateRefresh: LiveData<NetworkState>
|
||||
get() = viewModel.networkStateAccountRefresh
|
||||
override val networkState: LiveData<NetworkState>
|
||||
get() = viewModel.networkStateAccount
|
||||
override val data: LiveData<PagedList<Account>>
|
||||
get() = viewModel.accounts
|
||||
override val data: Flow<PagingData<Account>>
|
||||
get() = viewModel.accountsFlow
|
||||
|
||||
companion object {
|
||||
fun newInstance() = SearchAccountsFragment()
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ import android.os.Bundle
|
|||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
|
|
@ -21,10 +22,14 @@ import com.keylesspalace.tusky.databinding.FragmentSearchBinding
|
|||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
|
||||
abstract class SearchFragment<T: Any> : Fragment(R.layout.fragment_search),
|
||||
LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener {
|
||||
|
||||
@Inject
|
||||
|
|
@ -36,12 +41,12 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
|
|||
|
||||
private var snackbarErrorRetry: Snackbar? = null
|
||||
|
||||
abstract fun createAdapter(): PagedListAdapter<T, *>
|
||||
abstract fun createAdapter(): PagingDataAdapter<T, *>
|
||||
|
||||
abstract val networkStateRefresh: LiveData<NetworkState>
|
||||
abstract val networkState: LiveData<NetworkState>
|
||||
abstract val data: LiveData<PagedList<T>>
|
||||
protected lateinit var adapter: PagedListAdapter<T, *>
|
||||
abstract val data: Flow<PagingData<T>>
|
||||
protected lateinit var adapter: PagingDataAdapter<T, *>
|
||||
|
||||
private var currentQuery: String = ""
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
initAdapter()
|
||||
|
|
@ -55,32 +60,32 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
|
|||
}
|
||||
|
||||
private fun subscribeObservables() {
|
||||
data.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
|
||||
networkStateRefresh.observe(viewLifecycleOwner) {
|
||||
|
||||
binding.searchProgressBar.visible(it == NetworkState.LOADING)
|
||||
|
||||
if (it.status == Status.FAILED) {
|
||||
showError()
|
||||
}
|
||||
checkNoData()
|
||||
}
|
||||
|
||||
networkState.observe(viewLifecycleOwner) {
|
||||
|
||||
binding.progressBarBottom.visible(it == NetworkState.LOADING)
|
||||
|
||||
if (it.status == Status.FAILED) {
|
||||
showError()
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
data.collectLatest { pagingData ->
|
||||
adapter.submitData(pagingData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkNoData() {
|
||||
showNoData(adapter.itemCount == 0)
|
||||
adapter.addLoadStateListener { loadState ->
|
||||
|
||||
if (loadState.refresh is LoadState.Error) {
|
||||
showError()
|
||||
}
|
||||
|
||||
val isNewSearch = currentQuery != viewModel.currentQuery
|
||||
|
||||
binding.searchProgressBar.visible(loadState.refresh == LoadState.Loading && isNewSearch && !binding.swipeRefreshLayout.isRefreshing)
|
||||
binding.searchRecyclerView.visible(loadState.refresh is LoadState.NotLoading || !isNewSearch || binding.swipeRefreshLayout.isRefreshing)
|
||||
|
||||
if (loadState.refresh != LoadState.Loading) {
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
currentQuery = viewModel.currentQuery
|
||||
}
|
||||
|
||||
binding.progressBarBottom.visible(loadState.append == LoadState.Loading)
|
||||
|
||||
binding.searchNoResultsText.visible(loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0 && viewModel.currentQuery.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAdapter() {
|
||||
|
|
@ -92,20 +97,12 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
|
|||
(binding.searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
|
||||
private fun showNoData(isEmpty: Boolean) {
|
||||
if (isEmpty && networkStateRefresh.value == NetworkState.LOADED) {
|
||||
binding.searchNoResultsText.show()
|
||||
} else {
|
||||
binding.searchNoResultsText.hide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError() {
|
||||
if (snackbarErrorRetry?.isShown != true) {
|
||||
snackbarErrorRetry = Snackbar.make(binding.root, R.string.failed_search, Snackbar.LENGTH_INDEFINITE)
|
||||
snackbarErrorRetry?.setAction(R.string.action_retry) {
|
||||
snackbarErrorRetry = null
|
||||
viewModel.retryAllSearches()
|
||||
adapter.retry()
|
||||
}
|
||||
snackbarErrorRetry?.show()
|
||||
}
|
||||
|
|
@ -123,11 +120,6 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
|
|||
get() = (activity as? BottomSheetActivity)
|
||||
|
||||
override fun onRefresh() {
|
||||
|
||||
// Dismissed here because the RecyclerView bottomProgressBar is shown as soon as the retry begins.
|
||||
binding.swipeRefreshLayout.post {
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
viewModel.retryAllSearches()
|
||||
adapter.refresh()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
|
|
@ -15,22 +15,18 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.search.fragments
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import com.keylesspalace.tusky.components.search.adapter.SearchHashtagsAdapter
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class SearchHashtagsFragment : SearchFragment<HashTag>() {
|
||||
override val networkStateRefresh: LiveData<NetworkState>
|
||||
get() = viewModel.networkStateHashTagRefresh
|
||||
override val networkState: LiveData<NetworkState>
|
||||
get() = viewModel.networkStateHashTag
|
||||
override val data: LiveData<PagedList<HashTag>>
|
||||
get() = viewModel.hashtags
|
||||
|
||||
override fun createAdapter(): PagedListAdapter<HashTag, *> = SearchHashtagsAdapter(this)
|
||||
override val data: Flow<PagingData<HashTag>>
|
||||
get() = viewModel.hashtagsFlow
|
||||
|
||||
override fun createAdapter(): PagingDataAdapter<HashTag, *> = SearchHashtagsAdapter(this)
|
||||
|
||||
companion object {
|
||||
fun newInstance() = SearchHashtagsFragment()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2019 Joel Pyska
|
||||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
|
|
@ -32,9 +32,8 @@ import androidx.appcompat.widget.PopupMenu
|
|||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
|
@ -57,26 +56,22 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener
|
|||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.CardViewMode
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.view.showMuteAccountDialog
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concrete>>(), StatusActionListener {
|
||||
|
||||
override val networkStateRefresh: LiveData<NetworkState>
|
||||
get() = viewModel.networkStateStatusRefresh
|
||||
override val networkState: LiveData<NetworkState>
|
||||
get() = viewModel.networkStateStatus
|
||||
override val data: LiveData<PagedList<Pair<Status, StatusViewData.Concrete>>>
|
||||
get() = viewModel.statuses
|
||||
override val data: Flow<PagingData<Pair<Status, StatusViewData.Concrete>>>
|
||||
get() = viewModel.statusesFlow
|
||||
|
||||
private val searchAdapter
|
||||
get() = super.adapter as SearchStatusesAdapter
|
||||
|
||||
override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> {
|
||||
override fun createAdapter(): PagingDataAdapter<Pair<Status, StatusViewData.Concrete>, *> {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
|
||||
val statusDisplayOptions = StatusDisplayOptions(
|
||||
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
|
||||
|
|
@ -96,37 +91,37 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
}
|
||||
|
||||
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||
searchAdapter.getItem(position)?.let {
|
||||
searchAdapter.item(position)?.let {
|
||||
viewModel.contentHiddenChange(it, isShowing)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReply(position: Int) {
|
||||
searchAdapter.getItem(position)?.first?.let { status ->
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
reply(status)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||
searchAdapter.getItem(position)?.let { status ->
|
||||
searchAdapter.item(position)?.let { status ->
|
||||
viewModel.favorite(status, favourite)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBookmark(bookmark: Boolean, position: Int) {
|
||||
searchAdapter.getItem(position)?.let { status ->
|
||||
searchAdapter.item(position)?.let { status ->
|
||||
viewModel.bookmark(status, bookmark)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMore(view: View, position: Int) {
|
||||
searchAdapter.getItem(position)?.first?.let {
|
||||
searchAdapter.item(position)?.first?.let {
|
||||
more(it, view, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||
searchAdapter.getItem(position)?.first?.actionableStatus?.let { actionable ->
|
||||
searchAdapter.item(position)?.first?.actionableStatus?.let { actionable ->
|
||||
when (actionable.attachments[attachmentIndex].type) {
|
||||
Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> {
|
||||
val attachments = AttachmentViewData.list(actionable)
|
||||
|
|
@ -146,26 +141,24 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
LinkHelper.openLink(actionable.attachments[attachmentIndex].url, context)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onViewThread(position: Int) {
|
||||
searchAdapter.getItem(position)?.first?.let { status ->
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
val actionableStatus = status.actionableStatus
|
||||
bottomSheetActivity?.viewThread(actionableStatus.id, actionableStatus.url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenReblog(position: Int) {
|
||||
searchAdapter.getItem(position)?.first?.let { status ->
|
||||
searchAdapter.item(position)?.first?.let { status ->
|
||||
bottomSheetActivity?.viewAccount(status.account.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||
searchAdapter.getItem(position)?.let {
|
||||
searchAdapter.item(position)?.let {
|
||||
viewModel.expandedChange(it, expanded)
|
||||
}
|
||||
}
|
||||
|
|
@ -175,25 +168,25 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
}
|
||||
|
||||
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||
searchAdapter.getItem(position)?.let {
|
||||
searchAdapter.item(position)?.let {
|
||||
viewModel.collapsedChange(it, isCollapsed)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||
searchAdapter.getItem(position)?.let {
|
||||
searchAdapter.item(position)?.let {
|
||||
viewModel.voteInPoll(it, choices)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeItem(position: Int) {
|
||||
searchAdapter.getItem(position)?.let {
|
||||
searchAdapter.item(position)?.let {
|
||||
viewModel.removeItem(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReblog(reblog: Boolean, position: Int) {
|
||||
searchAdapter.getItem(position)?.let { status ->
|
||||
searchAdapter.item(position)?.let { status ->
|
||||
viewModel.reblog(status, reblog)
|
||||
}
|
||||
}
|
||||
|
|
@ -323,7 +316,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
R.id.status_mute_conversation -> {
|
||||
searchAdapter.getItem(position)?.let { foundStatus ->
|
||||
searchAdapter.item(position)?.let { foundStatus ->
|
||||
viewModel.muteConversation(foundStatus, status.muted != true)
|
||||
}
|
||||
return@setOnMenuItemClickListener true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue