anytype-kotlin-wild/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagStatusViewModel.kt

397 lines
14 KiB
Kotlin

package com.anytypeio.anytype.presentation.relations.value.tagstatus
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.Relation
import com.anytypeio.anytype.core_models.Struct
import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import com.anytypeio.anytype.core_utils.ext.typeOf
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.`object`.UpdateDetail
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.options.GetOptions
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationValueEvent
import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider
import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider
import com.anytypeio.anytype.presentation.sets.filterIdsById
import com.anytypeio.anytype.presentation.util.Dispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import timber.log.Timber
class TagStatusViewModel(
private val params: Params,
private val relations: ObjectRelationProvider,
private val values: ObjectValueProvider,
private val storage: Editor.Storage,
private val storeOfObjectTypes: StoreOfObjectTypes,
private val urlBuilder: UrlBuilder,
private val dispatcher: Dispatcher<Payload>,
private val setObjectDetails: UpdateDetail,
private val analytics: Analytics,
private val getOptions: GetOptions,
private val spaceManager: SpaceManager
) : BaseViewModel() {
val viewState = MutableStateFlow<TagStatusViewState>(TagStatusViewState.Loading)
private val query = MutableSharedFlow<String>()
private var isRelationNotEditable = false
fun onStart() {
val obj = storage.details.current().details[params.objectId]
isRelationNotEditable = params.isLocked || storage.objectRestrictions.current()
.contains(ObjectRestriction.RELATIONS)
Timber.d("TagStatusViewModel onStart, params: $params, obj: $obj, isRelationNotEditable: $isRelationNotEditable")
viewModelScope.launch {
combine(
relations.observe(
relation = params.relationKey
),
values.subscribe(
ctx = params.ctx,
target = params.objectId
),
query.onStart { emit("") }
) { relation, record, query ->
setupIsRelationNotEditable(relation)
getAllOptions(
relation = relation,
record = record,
query = query
)
}.collect()
}
}
fun onQueryChanged(input: String) {
viewModelScope.launch {
query.emit(input)
}
}
fun onAction(action: TagStatusAction) {
Timber.d("TagStatusViewModel onAction, action: $action")
if (isRelationNotEditable) {
Timber.d("TagStatusViewModel onAction, relation is not editable")
sendToast("Relation is not editable")
return
}
when (action) {
TagStatusAction.Clear -> clearTagsOrStatus()
is TagStatusAction.Click -> onActionClick(action.item)
is TagStatusAction.LongClick -> {
val currentState = viewState.value
if (currentState is TagStatusViewState.Content) {
viewState.value = currentState.copy(showItemMenu = action.item)
}
}
TagStatusAction.Plus -> TODO()
is TagStatusAction.Delete -> TODO()
is TagStatusAction.Duplicate -> TODO()
is TagStatusAction.Edit -> TODO()
}
}
private fun onActionClick(item: RelationsListItem) {
when (item) {
is RelationsListItem.Item.Status -> {
if (item.isSelected) {
clearTagsOrStatus()
} else {
addStatus(item.optionId)
}
}
is RelationsListItem.Item.Tag -> {
if (item.isSelected) {
removeTag(item.optionId)
} else {
addTag(item.optionId)
}
}
is RelationsListItem.CreateItem.Status -> TODO()
is RelationsListItem.CreateItem.Tag -> TODO()
}
}
private suspend fun getAllOptions(
relation: ObjectWrapper.Relation,
record: Struct,
query: String
) {
val params = GetOptions.Params(
space = spaceManager.get(),
relation = relation.key,
fulltext = query
)
getOptions(params).proceed(
success = { options ->
Timber.d("TagStatusViewModel getAllOptions, options: ${options.size}")
initViewState(
relation = relation,
record = record,
options = options,
query = query
)
},
failure = {
Timber.e(it, "Error while getting options by id")
}
)
}
private fun initViewState(
relation: ObjectWrapper.Relation,
record: Map<String, Any?>,
options: List<ObjectWrapper.Option>,
query: String
) {
val result = mutableListOf<RelationsListItem>()
when (relation.format) {
Relation.Format.STATUS -> {
val ids: List<Id> = when (val value = record[params.relationKey]) {
is Id -> listOf(value)
is List<*> -> value.typeOf()
else -> emptyList()
}
result.addAll(
mapStatusOptions(
ids = ids,
options = options
)
)
}
Relation.Format.TAG -> {
val ids: List<Id> = when (val value = record[params.relationKey]) {
is Id -> listOf(value)
is List<*> -> value.typeOf()
else -> emptyList()
}
result.addAll(
mapTagOptions(
ids = ids,
options = options
)
)
if (query.isNotBlank()) {
result.add(RelationsListItem.CreateItem.Tag(query))
}
}
else -> {
Timber.w("Relation format should be Tag or Status but was: ${relation.format}")
}
}
viewState.value = if (result.isEmpty()) {
TagStatusViewState.Empty(
isRelationEditable = !isRelationNotEditable,
title = relation.name.orEmpty(),
)
} else {
TagStatusViewState.Content(
isRelationEditable = !isRelationNotEditable,
title = relation.name.orEmpty(),
items = result
)
}
}
private fun addTag(tag: Id) {
viewModelScope.launch {
val obj = values.get(ctx = params.ctx, target = params.objectId)
val result = mutableListOf<Id>()
val value = obj[params.relationKey]
if (value is List<*>) {
result.addAll(value.typeOf())
} else if (value is Id) {
result.add(value)
}
result.add(tag)
setObjectDetails(
UpdateDetail.Params(
target = params.objectId,
key = params.relationKey,
value = result
)
).process(
failure = { Timber.e(it, "Error while adding tag") },
success = {
dispatcher.send(it)
sendAnalyticsRelationValueEvent(analytics)
}
)
}
}
private fun removeTag(tag: Id) {
viewModelScope.launch {
val obj = values.get(ctx = params.ctx, target = params.objectId)
val remaining = obj[params.relationKey].filterIdsById(tag)
setObjectDetails(
UpdateDetail.Params(
target = params.objectId,
key = params.relationKey,
value = remaining
)
).process(
failure = { Timber.e(it, "Error while adding tag") },
success = {
dispatcher.send(it)
sendAnalyticsRelationValueEvent(analytics)
})
}
}
private fun addStatus(status: Id) {
viewModelScope.launch {
setObjectDetails(
UpdateDetail.Params(
target = params.objectId,
key = params.relationKey,
value = listOf(status)
)
).process(
failure = { Timber.e(it, "Error while adding tag") },
success = {
dispatcher.send(it)
sendAnalyticsRelationValueEvent(analytics)
}
)
}
}
private fun clearTagsOrStatus() {
viewModelScope.launch {
setObjectDetails(
UpdateDetail.Params(
target = params.objectId,
key = params.relationKey,
value = null
)
).process(
failure = { Timber.e(it, "Error while clearing tags or select") },
success = {
dispatcher.send(it)
sendAnalyticsRelationValueEvent(analytics)
})
}
}
private fun mapTagOptions(
ids: List<Id>,
options: List<ObjectWrapper.Option>
) = options.map { option ->
val index = ids.indexOf(option.id)
val isSelected = index != -1
val number = if (isSelected) index + 1 else Int.MAX_VALUE
RelationsListItem.Item.Tag(
optionId = option.id,
name = option.name.orEmpty(),
color = getOrDefault(option.color),
isSelected = isSelected,
number = number
)
}.sortedBy { it.number }
private fun mapStatusOptions(
ids: List<Id>,
options: List<ObjectWrapper.Option>
) = options.map { option ->
val index = ids.indexOf(option.id)
val isSelected = index != -1
RelationsListItem.Item.Status(
optionId = option.id,
name = option.name.orEmpty(),
color = getOrDefault(option.color),
isSelected = isSelected
)
}
private fun getOrDefault(code: String?): ThemeColor {
return ThemeColor.values().find { it.code == code } ?: ThemeColor.DEFAULT
}
private fun setupIsRelationNotEditable(relation: ObjectWrapper.Relation) {
isRelationNotEditable = params.isLocked
|| storage.objectRestrictions.current().contains(ObjectRestriction.RELATIONS)
|| relation.isReadonlyValue
|| relation.isHidden == true
|| relation.isDeleted == true
|| relation.isArchived == true
|| !relation.isValid
}
data class Params(
val ctx: Id,
val objectId: Id,
val relationKey: Id,
val isLocked: Boolean,
val relationContext: RelationContext
)
}
sealed class TagStatusViewState {
object Loading : TagStatusViewState()
data class Empty(val title: String, val isRelationEditable: Boolean) :
TagStatusViewState()
data class Content(
val title: String,
val items: List<RelationsListItem>,
val isRelationEditable: Boolean,
val showItemMenu: RelationsListItem.Item? = null
) : TagStatusViewState()
}
sealed class TagStatusAction {
data class Click(val item: RelationsListItem) : TagStatusAction()
data class LongClick(val item: RelationsListItem.Item) : TagStatusAction()
object Clear : TagStatusAction()
object Plus : TagStatusAction()
data class Edit(val optionId: Id) : TagStatusAction()
data class Delete(val optionId: Id) : TagStatusAction()
data class Duplicate(val optionId: Id) : TagStatusAction()
}
enum class RelationContext{ OBJECT, OBJECT_SET, DATA_VIEW }
sealed class RelationsListItem {
sealed class Item : RelationsListItem() {
abstract val optionId: Id
data class Tag(
override val optionId: Id,
val name: String,
val color: ThemeColor,
val isSelected: Boolean,
val number: Int = Int.MAX_VALUE,
val showMenu: Boolean = false
) : Item()
data class Status(
override val optionId: Id,
val name: String,
val color: ThemeColor,
val isSelected: Boolean
) : Item()
}
sealed class CreateItem(
val text: String
) : RelationsListItem() {
class Tag(text: String) : CreateItem(text)
class Status(text: String) : CreateItem(text)
}
}