Timeline refactor (#2175)
* Move Timeline files into their own package * Introduce TimelineViewModel, add coroutines * Simplify StatusViewData * Handle timeilne fetch errors * Rework filters, fix ViewThreadFragment * Fix NotificationsFragment * Simplify Notifications and Thread, handle pin * Redo loading in TimelineViewModel * Improve error handling in TimelineViewModel * Rewrite actions in TimelineViewModel * Apply feedback after timeline factoring review * Handle initial failure in timeline correctly
This commit is contained in:
parent
0a992480c2
commit
44a5b42cac
58 changed files with 3956 additions and 3618 deletions
|
|
@ -15,13 +15,12 @@ import com.keylesspalace.tusky.util.*
|
|||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class SearchViewModel @Inject constructor(
|
||||
mastodonApi: MastodonApi,
|
||||
private val timelineCases: TimelineCases,
|
||||
private val accountManager: AccountManager
|
||||
mastodonApi: MastodonApi,
|
||||
private val timelineCases: TimelineCases,
|
||||
private val accountManager: AccountManager
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
var currentQuery: String = ""
|
||||
|
|
@ -36,93 +35,109 @@ 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 statusesRepository =
|
||||
SearchRepository<Pair<Status, StatusViewData.Concrete>>(mastodonApi)
|
||||
private val accountsRepository = SearchRepository<Account>(mastodonApi)
|
||||
private val hashtagsRepository = SearchRepository<HashTag>(mastodonApi)
|
||||
|
||||
private val repoResultStatus = MutableLiveData<Listing<Pair<Status, StatusViewData.Concrete>>>()
|
||||
val statuses: LiveData<PagedList<Pair<Status, StatusViewData.Concrete>>> = repoResultStatus.switchMap { it.pagedList }
|
||||
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 }
|
||||
val networkStateStatusRefresh: LiveData<NetworkState> =
|
||||
repoResultStatus.switchMap { it.refreshState }
|
||||
|
||||
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 networkStateAccount: LiveData<NetworkState> =
|
||||
repoResultAccount.switchMap { it.networkState }
|
||||
val networkStateAccountRefresh: LiveData<NetworkState> =
|
||||
repoResultAccount.switchMap { it.refreshState }
|
||||
|
||||
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 networkStateHashTag: LiveData<NetworkState> =
|
||||
repoResultHashTag.switchMap { it.networkState }
|
||||
val networkStateHashTagRefresh: LiveData<NetworkState> =
|
||||
repoResultHashTag.switchMap { it.refreshState }
|
||||
|
||||
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, ViewDataUtils.statusToViewData(status, alwaysShowSensitiveMedia, alwaysOpenSpoiler)!!) }
|
||||
.orEmpty()
|
||||
.apply {
|
||||
loadedStatuses.addAll(this)
|
||||
}
|
||||
}
|
||||
repoResultAccount.value = accountsRepository.getSearchData(SearchType.Account, query, disposables) {
|
||||
it?.accounts.orEmpty()
|
||||
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()
|
||||
}
|
||||
hashtagsRepository.getSearchData(SearchType.Hashtag, hashtagQuery, disposables) {
|
||||
it?.hashtags.orEmpty()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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))
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
}, { 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, StatusViewData.Builder(status.second).setIsExpanded(expanded).createStatusViewData())
|
||||
val newPair = Pair(status.first, status.second.copy(isExpanded = expanded))
|
||||
loadedStatuses[idx] = newPair
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun reblog(status: Pair<Status, StatusViewData.Concrete>, reblog: Boolean) {
|
||||
timelineCases.reblog(status.first, reblog)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ setRebloggedForStatus(status, reblog) },
|
||||
{ err -> Log.d(TAG, "Failed to reblog status ${status.first.id}", err) }
|
||||
)
|
||||
.autoDispose()
|
||||
timelineCases.reblog(status.first.id, reblog)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ setRebloggedForStatus(status, reblog) },
|
||||
{ err -> Log.d(TAG, "Failed to reblog status ${status.first.id}", err) }
|
||||
)
|
||||
.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
|
||||
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
if (idx >= 0) {
|
||||
val newPair = Pair(status.first, StatusViewData.Builder(status.second).setReblogged(reblog).createStatusViewData())
|
||||
loadedStatuses[idx] = newPair
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
}
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
}
|
||||
|
||||
fun contentHiddenChange(status: Pair<Status, StatusViewData.Concrete>, isShowing: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
if (idx >= 0) {
|
||||
val newPair = Pair(status.first, StatusViewData.Builder(status.second).setIsShowingSensitiveContent(isShowing).createStatusViewData())
|
||||
val newPair = Pair(status.first, status.second.copy(isShowingContent = isShowing))
|
||||
loadedStatuses[idx] = newPair
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
}
|
||||
|
|
@ -131,7 +146,7 @@ class SearchViewModel @Inject constructor(
|
|||
fun collapsedChange(status: Pair<Status, StatusViewData.Concrete>, collapsed: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
if (idx >= 0) {
|
||||
val newPair = Pair(status.first, StatusViewData.Builder(status.second).setCollapsed(collapsed).createStatusViewData())
|
||||
val newPair = Pair(status.first, status.second.copy(isCollapsed = collapsed))
|
||||
loadedStatuses[idx] = newPair
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
}
|
||||
|
|
@ -140,54 +155,46 @@ class SearchViewModel @Inject constructor(
|
|||
fun voteInPoll(status: Pair<Status, StatusViewData.Concrete>, choices: MutableList<Int>) {
|
||||
val votedPoll = status.first.actionableStatus.poll!!.votedCopy(choices)
|
||||
updateStatus(status, votedPoll)
|
||||
timelineCases.voteInPoll(status.first, choices)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ newPoll -> updateStatus(status, newPoll) },
|
||||
{ t ->
|
||||
Log.d(TAG,
|
||||
"Failed to vote in poll: ${status.first.id}", t)
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
timelineCases.voteInPoll(status.first.id, votedPoll.id, choices)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ newPoll -> updateStatus(status, newPoll) },
|
||||
{ t ->
|
||||
Log.d(
|
||||
TAG,
|
||||
"Failed to vote in poll: ${status.first.id}", t
|
||||
)
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
private fun updateStatus(status: Pair<Status, StatusViewData.Concrete>, newPoll: Poll) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
if (idx >= 0) {
|
||||
|
||||
val newViewData = StatusViewData.Builder(status.second)
|
||||
.setPoll(newPoll)
|
||||
.createStatusViewData()
|
||||
loadedStatuses[idx] = Pair(status.first, newViewData)
|
||||
val newStatus = status.first.copy(poll = newPoll)
|
||||
val newViewData = status.second.copy(status = newStatus)
|
||||
loadedStatuses[idx] = Pair(newStatus, newViewData)
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun favorite(status: Pair<Status, StatusViewData.Concrete>, isFavorited: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
if (idx >= 0) {
|
||||
val newPair = Pair(status.first, StatusViewData.Builder(status.second).setFavourited(isFavorited).createStatusViewData())
|
||||
loadedStatuses[idx] = newPair
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
}
|
||||
timelineCases.favourite(status.first, isFavorited)
|
||||
.onErrorReturnItem(status.first)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
status.first.favourited = isFavorited
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
timelineCases.favourite(status.first.id, isFavorited)
|
||||
.onErrorReturnItem(status.first)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun bookmark(status: Pair<Status, StatusViewData.Concrete>, isBookmarked: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
if (idx >= 0) {
|
||||
val newPair = Pair(status.first, StatusViewData.Builder(status.second).setBookmarked(isBookmarked).createStatusViewData())
|
||||
loadedStatuses[idx] = newPair
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
}
|
||||
timelineCases.bookmark(status.first, isBookmarked)
|
||||
.onErrorReturnItem(status.first)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
status.first.bookmarked = isBookmarked
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
timelineCases.bookmark(status.first.id, isBookmarked)
|
||||
.onErrorReturnItem(status.first)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun getAllAccountsOrderedByActive(): List<AccountEntity> {
|
||||
|
|
@ -199,7 +206,7 @@ class SearchViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun pinAccount(status: Status, isPin: Boolean) {
|
||||
timelineCases.pin(status, isPin)
|
||||
timelineCases.pin(status.id, isPin)
|
||||
}
|
||||
|
||||
fun blockAccount(accountId: String) {
|
||||
|
|
@ -217,14 +224,18 @@ class SearchViewModel @Inject constructor(
|
|||
fun muteConversation(status: Pair<Status, StatusViewData.Concrete>, mute: Boolean) {
|
||||
val idx = loadedStatuses.indexOf(status)
|
||||
if (idx >= 0) {
|
||||
val newPair = Pair(status.first, StatusViewData.Builder(status.second).setMuted(mute).createStatusViewData())
|
||||
val newStatus = status.first.copy(muted = mute)
|
||||
val newPair = Pair(
|
||||
newStatus,
|
||||
status.second.copy(status = newStatus)
|
||||
)
|
||||
loadedStatuses[idx] = newPair
|
||||
repoResultStatus.value?.refresh?.invoke()
|
||||
}
|
||||
timelineCases.muteConversation(status.first, mute)
|
||||
.onErrorReturnItem(status.first)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
timelineCases.muteConversation(status.first.id, mute)
|
||||
.onErrorReturnItem(status.first)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class SearchStatusesAdapter(
|
|||
|
||||
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.deepEquals(newItem.second)
|
||||
oldItem.second == newItem.second
|
||||
|
||||
override fun areItemsTheSame(oldItem: Pair<Status, StatusViewData.Concrete>, newItem: Pair<Status, StatusViewData.Concrete>): Boolean =
|
||||
oldItem.second.id == newItem.second.id
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
}
|
||||
}
|
||||
|
||||
private fun accountIsInMentions(account: AccountEntity?, mentions: Array<Mention>): Boolean {
|
||||
private fun accountIsInMentions(account: AccountEntity?, mentions: List<Mention>): Boolean {
|
||||
return mentions.firstOrNull {
|
||||
account?.username == it.username && account.domain == Uri.parse(it.url)?.host
|
||||
} != null
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue