248 lines
9.2 KiB
Kotlin
248 lines
9.2 KiB
Kotlin
/* 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.ViewModel
|
|
import androidx.lifecycle.viewModelScope
|
|
import androidx.paging.Pager
|
|
import androidx.paging.PagingConfig
|
|
import androidx.paging.cachedIn
|
|
import at.connyduck.calladapter.networkresult.NetworkResult
|
|
import at.connyduck.calladapter.networkresult.fold
|
|
import at.connyduck.calladapter.networkresult.map
|
|
import at.connyduck.calladapter.networkresult.onFailure
|
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
|
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.DeletedStatus
|
|
import com.keylesspalace.tusky.entity.Status
|
|
import com.keylesspalace.tusky.network.MastodonApi
|
|
import com.keylesspalace.tusky.usecase.TimelineCases
|
|
import com.keylesspalace.tusky.util.toViewData
|
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
|
import com.keylesspalace.tusky.viewdata.TranslationViewData
|
|
import javax.inject.Inject
|
|
import kotlinx.coroutines.Deferred
|
|
import kotlinx.coroutines.async
|
|
import kotlinx.coroutines.launch
|
|
|
|
class SearchViewModel @Inject constructor(
|
|
mastodonApi: MastodonApi,
|
|
private val timelineCases: TimelineCases,
|
|
private val accountManager: AccountManager,
|
|
private val instanceInfoRepository: InstanceInfoRepository,
|
|
) : ViewModel() {
|
|
|
|
init {
|
|
instanceInfoRepository.precache()
|
|
}
|
|
|
|
var currentQuery: String = ""
|
|
var currentSearchFieldContent: String? = null
|
|
|
|
val activeAccount: AccountEntity?
|
|
get() = accountManager.activeAccount
|
|
|
|
val mediaPreviewEnabled = activeAccount?.mediaPreviewEnabled ?: false
|
|
val alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false
|
|
val alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false
|
|
|
|
private val loadedStatuses: MutableList<StatusViewData.Concrete> = mutableListOf()
|
|
|
|
private val statusesPagingSourceFactory =
|
|
SearchPagingSourceFactory(mastodonApi, SearchType.Status, loadedStatuses) {
|
|
it.statuses.map { status ->
|
|
status.toViewData(
|
|
isShowingContent = alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
|
isExpanded = alwaysOpenSpoiler,
|
|
isCollapsed = true
|
|
)
|
|
}.apply {
|
|
loadedStatuses.addAll(this)
|
|
}
|
|
}
|
|
private val accountsPagingSourceFactory =
|
|
SearchPagingSourceFactory(mastodonApi, SearchType.Account) {
|
|
it.accounts
|
|
}
|
|
private val hashtagsPagingSourceFactory =
|
|
SearchPagingSourceFactory(mastodonApi, SearchType.Hashtag) {
|
|
it.hashtags
|
|
}
|
|
|
|
val statusesFlow = Pager(
|
|
config = PagingConfig(pageSize = DEFAULT_LOAD_SIZE, initialLoadSize = DEFAULT_LOAD_SIZE),
|
|
pagingSourceFactory = statusesPagingSourceFactory
|
|
).flow
|
|
.cachedIn(viewModelScope)
|
|
|
|
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)
|
|
|
|
fun search(query: String) {
|
|
loadedStatuses.clear()
|
|
statusesPagingSourceFactory.newSearch(query)
|
|
accountsPagingSourceFactory.newSearch(query)
|
|
hashtagsPagingSourceFactory.newSearch(query)
|
|
}
|
|
|
|
fun removeItem(statusViewData: StatusViewData.Concrete) {
|
|
viewModelScope.launch {
|
|
if (timelineCases.delete(statusViewData.id).isSuccess) {
|
|
if (loadedStatuses.remove(statusViewData)) {
|
|
statusesPagingSourceFactory.invalidate()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun expandedChange(statusViewData: StatusViewData.Concrete, expanded: Boolean) {
|
|
updateStatusViewData(statusViewData.copy(isExpanded = expanded))
|
|
}
|
|
|
|
fun reblog(statusViewData: StatusViewData.Concrete, reblog: Boolean) {
|
|
viewModelScope.launch {
|
|
timelineCases.reblog(statusViewData.id, reblog).fold({
|
|
updateStatus(
|
|
statusViewData.status.copy(
|
|
reblogged = reblog,
|
|
reblog = statusViewData.status.reblog?.copy(reblogged = reblog)
|
|
)
|
|
)
|
|
}, { t ->
|
|
Log.d(TAG, "Failed to reblog status ${statusViewData.id}", t)
|
|
})
|
|
}
|
|
}
|
|
|
|
fun contentHiddenChange(statusViewData: StatusViewData.Concrete, isShowing: Boolean) {
|
|
updateStatusViewData(statusViewData.copy(isShowingContent = isShowing))
|
|
}
|
|
|
|
fun collapsedChange(statusViewData: StatusViewData.Concrete, collapsed: Boolean) {
|
|
updateStatusViewData(statusViewData.copy(isCollapsed = collapsed))
|
|
}
|
|
|
|
fun voteInPoll(statusViewData: StatusViewData.Concrete, choices: MutableList<Int>) {
|
|
val votedPoll = statusViewData.status.actionableStatus.poll!!.votedCopy(choices)
|
|
updateStatus(statusViewData.status.copy(poll = votedPoll))
|
|
viewModelScope.launch {
|
|
timelineCases.voteInPoll(statusViewData.id, votedPoll.id, choices)
|
|
.onFailure { t -> Log.d(TAG, "Failed to vote in poll: ${statusViewData.id}", t) }
|
|
}
|
|
}
|
|
|
|
fun favorite(statusViewData: StatusViewData.Concrete, isFavorited: Boolean) {
|
|
updateStatus(statusViewData.status.copy(favourited = isFavorited))
|
|
viewModelScope.launch {
|
|
timelineCases.favourite(statusViewData.id, isFavorited)
|
|
}
|
|
}
|
|
|
|
fun bookmark(statusViewData: StatusViewData.Concrete, isBookmarked: Boolean) {
|
|
updateStatus(statusViewData.status.copy(bookmarked = isBookmarked))
|
|
viewModelScope.launch {
|
|
timelineCases.bookmark(statusViewData.id, isBookmarked)
|
|
}
|
|
}
|
|
|
|
fun muteAccount(accountId: String, notifications: Boolean, duration: Int?) {
|
|
viewModelScope.launch {
|
|
timelineCases.mute(accountId, notifications, duration)
|
|
}
|
|
}
|
|
|
|
fun pinAccount(status: Status, isPin: Boolean) {
|
|
viewModelScope.launch {
|
|
timelineCases.pin(status.id, isPin)
|
|
}
|
|
}
|
|
|
|
fun blockAccount(accountId: String) {
|
|
viewModelScope.launch {
|
|
timelineCases.block(accountId)
|
|
}
|
|
}
|
|
|
|
fun deleteStatusAsync(id: String): Deferred<NetworkResult<DeletedStatus>> {
|
|
return viewModelScope.async {
|
|
timelineCases.delete(id)
|
|
}
|
|
}
|
|
|
|
fun muteConversation(statusViewData: StatusViewData.Concrete, mute: Boolean) {
|
|
updateStatus(statusViewData.status.copy(muted = mute))
|
|
viewModelScope.launch {
|
|
timelineCases.muteConversation(statusViewData.id, mute)
|
|
}
|
|
}
|
|
|
|
fun supportsTranslation(): Boolean =
|
|
instanceInfoRepository.cachedInstanceInfoOrFallback.translationEnabled == true
|
|
|
|
suspend fun translate(statusViewData: StatusViewData.Concrete): NetworkResult<Unit> {
|
|
updateStatusViewData(statusViewData.copy(translation = TranslationViewData.Loading))
|
|
return timelineCases.translate(statusViewData.actionableId)
|
|
.map { translation ->
|
|
updateStatusViewData(
|
|
statusViewData.copy(
|
|
translation = TranslationViewData.Loaded(
|
|
translation
|
|
)
|
|
)
|
|
)
|
|
}
|
|
.onFailure {
|
|
updateStatusViewData(statusViewData.copy(translation = null))
|
|
}
|
|
}
|
|
|
|
fun untranslate(statusViewData: StatusViewData.Concrete) {
|
|
updateStatusViewData(statusViewData.copy(translation = null))
|
|
}
|
|
|
|
private fun updateStatusViewData(newStatusViewData: StatusViewData.Concrete) {
|
|
val idx = loadedStatuses.indexOfFirst { it.id == newStatusViewData.id }
|
|
if (idx >= 0) {
|
|
loadedStatuses[idx] = newStatusViewData
|
|
statusesPagingSourceFactory.invalidate()
|
|
}
|
|
}
|
|
|
|
private fun updateStatus(newStatus: Status) {
|
|
val statusViewData = loadedStatuses.find { it.id == newStatus.id }
|
|
if (statusViewData != null) {
|
|
updateStatusViewData(statusViewData.copy(status = newStatus))
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private const val TAG = "SearchViewModel"
|
|
private const val DEFAULT_LOAD_SIZE = 20
|
|
}
|
|
}
|