1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

Object | Fix | Show menu options based on type and restrictions (#2313)

This commit is contained in:
Sergey Boishtyan 2022-05-31 23:38:01 +04:00 committed by GitHub
parent 16d9f6c681
commit ecbbfb8095
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 666 additions and 319 deletions

View file

@ -12,8 +12,9 @@ import com.anytypeio.anytype.domain.objects.SetObjectIsArchived
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.Delegator
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.objects.ObjectMenuViewModel
import com.anytypeio.anytype.presentation.objects.ObjectSetMenuViewModel
import com.anytypeio.anytype.presentation.objects.menu.ObjectMenuOptionsProviderImpl
import com.anytypeio.anytype.presentation.objects.menu.ObjectMenuViewModel
import com.anytypeio.anytype.presentation.objects.menu.ObjectSetMenuViewModel
import com.anytypeio.anytype.presentation.sets.ObjectSet
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.ui.editor.sheets.ObjectMenuFragment
@ -22,6 +23,7 @@ import dagger.Module
import dagger.Provides
import dagger.Subcomponent
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
@Subcomponent(modules = [ObjectMenuModuleBase::class, ObjectMenuModule::class])
@ -29,7 +31,7 @@ import kotlinx.coroutines.flow.StateFlow
interface ObjectMenuComponent {
@Subcomponent.Builder
interface Builder {
fun base(module: ObjectMenuModuleBase) : Builder
fun base(module: ObjectMenuModuleBase): Builder
fun module(module: ObjectMenuModule): Builder
fun build(): ObjectMenuComponent
}
@ -42,7 +44,7 @@ interface ObjectMenuComponent {
interface ObjectSetMenuComponent {
@Subcomponent.Builder
interface Builder {
fun base(module: ObjectMenuModuleBase) : Builder
fun base(module: ObjectMenuModuleBase): Builder
fun module(module: ObjectSetMenuModule): Builder
fun build(): ObjectSetMenuComponent
}
@ -57,14 +59,14 @@ object ObjectMenuModuleBase {
@PerDialog
fun provideAddToFavoriteUseCase(
repo: BlockRepository
) : AddToFavorite = AddToFavorite(repo = repo)
): AddToFavorite = AddToFavorite(repo = repo)
@JvmStatic
@Provides
@PerDialog
fun provideRemoveFromFavoriteUseCase(
repo: BlockRepository
) : RemoveFromFavorite = RemoveFromFavorite(repo = repo)
): RemoveFromFavorite = RemoveFromFavorite(repo = repo)
}
@Module
@ -91,12 +93,21 @@ object ObjectMenuModule {
analytics = analytics,
dispatcher = dispatcher,
updateFields = updateFields,
delegator = delegator
delegator = delegator,
menuOptionsProvider = createMenuOptionsProvider(storage)
)
@JvmStatic
private fun createMenuOptionsProvider(storage: Editor.Storage) =
ObjectMenuOptionsProviderImpl(
details = storage.details.stream().map { it.details },
restrictions = storage.objectRestrictions.stream()
)
}
@Module
object ObjectSetMenuModule {
@JvmStatic
@Provides
@PerDialog
@ -113,6 +124,14 @@ object ObjectSetMenuModule {
removeFromFavorite = removeFromFavorite,
analytics = analytics,
state = state,
dispatcher = dispatcher
dispatcher = dispatcher,
menuOptionsProvider = createMenuOptionsProvider(state)
)
@JvmStatic
private fun createMenuOptionsProvider(state: StateFlow<ObjectSet>) =
ObjectMenuOptionsProviderImpl(
details = state.map { it.details },
restrictions = state.map { it.objectRestrictions }
)
}

View file

@ -5,12 +5,10 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.features.objects.ObjectActionAdapter
import com.anytypeio.anytype.core_ui.layout.SpacingItemDecoration
import com.anytypeio.anytype.core_ui.reactive.clicks
@ -19,9 +17,8 @@ import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment
import com.anytypeio.anytype.databinding.FragmentObjectMenuBinding
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.objects.ObjectMenuViewModel
import com.anytypeio.anytype.presentation.objects.ObjectMenuViewModelBase
import com.anytypeio.anytype.presentation.objects.menu.ObjectMenuOptionsProvider
import com.anytypeio.anytype.presentation.objects.menu.ObjectMenuViewModelBase
import com.anytypeio.anytype.ui.editor.cover.SelectCoverObjectFragment
import com.anytypeio.anytype.ui.editor.cover.SelectCoverObjectSetFragment
import com.anytypeio.anytype.ui.editor.layout.ObjectLayoutFragment
@ -29,7 +26,6 @@ import com.anytypeio.anytype.ui.editor.modals.ObjectIconPickerBaseFragment
import com.anytypeio.anytype.ui.relations.RelationListFragment
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
abstract class ObjectMenuBaseFragment : BaseBottomSheetFragment<FragmentObjectMenuBinding>() {
@ -94,16 +90,37 @@ abstract class ObjectMenuBaseFragment : BaseBottomSheetFragment<FragmentObjectMe
jobs += subscribe(vm.toasts) { toast(it) }
jobs += subscribe(vm.isDismissed) { isDismissed -> if (isDismissed) dismiss() }
jobs += subscribe(vm.commands) { command -> execute(command) }
jobs += subscribe(vm.options) { options -> renderOptions(options) }
}
super.onStart()
vm.onStart(
ctx = ctx,
isArchived = isArchived,
isFavorite = isFavorite,
isProfile = isProfile
isProfile = isProfile,
isLocked = isLocked
)
}
// TODO refactor to recycler view
private fun renderOptions(options: ObjectMenuOptionsProvider.Options) {
val iconVisibility = if (options.hasIcon) View.VISIBLE else View.GONE
val coverVisibility = if (options.hasCover) View.VISIBLE else View.GONE
val layoutVisibility = if (options.hasLayout) View.VISIBLE else View.GONE
val relationsVisibility = if (options.hasRelations) View.VISIBLE else View.GONE
val historyVisibility = if (options.hasHistory) View.VISIBLE else View.GONE
binding.optionIcon.visibility = iconVisibility
binding.optionCover.visibility = coverVisibility
binding.optionLayout.visibility = layoutVisibility
binding.optionRelations.visibility = relationsVisibility
binding.optionHistory.visibility = historyVisibility
binding.iconDivider.visibility = iconVisibility
binding.coverDivider.visibility = coverVisibility
binding.layoutDivider.visibility = layoutVisibility
binding.relationsDivider.visibility = relationsVisibility
binding.historyDivider.visibility = historyVisibility
}
private fun execute(command: ObjectMenuViewModelBase.Command) {
when (command) {
ObjectMenuViewModelBase.Command.OpenObjectCover -> {
@ -186,28 +203,4 @@ abstract class ObjectMenuBaseFragment : BaseBottomSheetFragment<FragmentObjectMe
fun onLayoutClicked()
fun onUndoRedoClicked()
}
}
class ObjectMenuFragment : ObjectMenuBaseFragment() {
@Inject
lateinit var factory: ObjectMenuViewModel.Factory
override val vm by viewModels<ObjectMenuViewModel> { factory }
override fun onStart() {
super.onStart()
with(lifecycleScope) {
subscribe(vm.isObjectArchived) { isArchived ->
if (isArchived) parentFragment?.findNavController()?.popBackStack()
}
}
}
override fun injectDependencies() {
componentManager().objectMenuComponent.get(ctx).inject(this)
}
override fun releaseDependencies() {
componentManager().objectMenuComponent.release(ctx)
}
}

View file

@ -0,0 +1,33 @@
package com.anytypeio.anytype.ui.editor.sheets
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.objects.menu.ObjectMenuViewModel
import javax.inject.Inject
class ObjectMenuFragment : ObjectMenuBaseFragment() {
@Inject
lateinit var factory: ObjectMenuViewModel.Factory
override val vm by viewModels<ObjectMenuViewModel> { factory }
override fun onStart() {
super.onStart()
with(lifecycleScope) {
subscribe(vm.isObjectArchived) { isArchived ->
if (isArchived) parentFragment?.findNavController()?.popBackStack()
}
}
}
override fun injectDependencies() {
componentManager().objectMenuComponent.get(ctx).inject(this)
}
override fun releaseDependencies() {
componentManager().objectMenuComponent.release(ctx)
}
}

View file

@ -6,7 +6,7 @@ import androidx.navigation.fragment.findNavController
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.objects.ObjectSetMenuViewModel
import com.anytypeio.anytype.presentation.objects.menu.ObjectSetMenuViewModel
import com.anytypeio.anytype.ui.editor.sheets.ObjectMenuBaseFragment
import javax.inject.Inject

View file

@ -28,7 +28,7 @@
app:title="@string/icon" />
<View
android:id="@+id/divider1"
android:id="@+id/iconDivider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:layout_marginStart="72dp"
@ -48,12 +48,12 @@
app:icon="@drawable/ic_object_menu_cover"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider1"
app:layout_constraintTop_toBottomOf="@id/iconDivider"
app:subtitle="@string/cover_description"
app:title="@string/cover" />
<View
android:id="@+id/divider2"
android:id="@+id/coverDivider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:layout_marginStart="72dp"
@ -73,12 +73,12 @@
app:icon="@drawable/ic_object_menu_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider2"
app:layout_constraintTop_toBottomOf="@id/coverDivider"
app:subtitle="@string/layout_description"
app:title="@string/layout" />
<View
android:id="@+id/divider3"
android:id="@+id/layoutDivider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:layout_marginStart="72dp"
@ -98,12 +98,12 @@
app:icon="@drawable/ic_object_menu_relations"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider3"
app:layout_constraintTop_toBottomOf="@id/layoutDivider"
app:subtitle="@string/relations_description"
app:title="@string/relations" />
<View
android:id="@+id/divider4"
android:id="@+id/relationsDivider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:layout_marginStart="72dp"
@ -123,12 +123,12 @@
app:icon="@drawable/ic_object_menu_history"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider4"
app:layout_constraintTop_toBottomOf="@id/relationsDivider"
app:subtitle="@string/history_description"
app:title="@string/history" />
<View
android:id="@+id/divider5"
android:id="@+id/historyDivider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:layout_marginTop="21dp"
@ -144,7 +144,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider5">
app:layout_constraintTop_toBottomOf="@+id/historyDivider">
<androidx.recyclerview.widget.RecyclerView
android:layout_gravity="center_vertical"

View file

@ -33,7 +33,7 @@ data class ObjectType(
IMAGE(8),
NOTE(9),
SPACE(10),
DATABASE(20)
DATABASE(20),
}
/**

View file

@ -48,7 +48,7 @@ sealed class ObjectWrapper {
val layout: ObjectType.Layout?
get() = when (val value = map[Relations.LAYOUT]) {
is Double -> ObjectType.Layout.values().find { layout ->
is Double -> ObjectType.Layout.values().singleOrNull { layout ->
layout.code == value.toInt()
}
else -> null

View file

@ -0,0 +1,27 @@
package com.anytypeio.anytype.presentation.objects.menu
import com.anytypeio.anytype.core_models.Id
import kotlinx.coroutines.flow.Flow
interface ObjectMenuOptionsProvider {
data class Options(
val hasIcon: Boolean,
val hasCover: Boolean,
val hasLayout: Boolean,
val hasRelations: Boolean,
val hasHistory: Boolean
) {
companion object {
val ALL = Options(
hasIcon = true,
hasCover = true,
hasLayout = true,
hasRelations = true,
hasHistory = true
)
}
}
fun provide(ctx: Id, isLocked: Boolean): Flow<Options>
}

View file

@ -0,0 +1,85 @@
package com.anytypeio.anytype.presentation.objects.menu
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import com.anytypeio.anytype.presentation.objects.menu.ObjectMenuOptionsProvider.Options
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
class ObjectMenuOptionsProviderImpl(
private val details: Flow<Map<Id, Block.Fields>>,
private val restrictions: Flow<List<ObjectRestriction>>,
) : ObjectMenuOptionsProvider {
private fun observeLayout(ctx: Id): Flow<ObjectType.Layout?> {
return details
.map { details ->
val fields = requireNotNull(details[ctx]) {
"Can't find details by objectId=$ctx"
}
ObjectWrapper.Basic(fields.map).layout
}
}
override fun provide(ctx: Id, isLocked: Boolean): Flow<Options> {
return combine(observeLayout(ctx), restrictions) { layout, restrictions ->
createOptions(layout, restrictions, isLocked)
}
}
private fun createOptions(
layout: ObjectType.Layout?,
restrictions: List<ObjectRestriction>,
isLocked: Boolean,
): Options {
val hasIcon = !isLocked
val hasCover = !isLocked
val hasLayout = !isLocked && !restrictions.contains(ObjectRestriction.LAYOUT_CHANGE)
val options = if (layout != null) {
when (layout) {
ObjectType.Layout.BASIC,
ObjectType.Layout.PROFILE,
ObjectType.Layout.OBJECT_TYPE,
ObjectType.Layout.RELATION,
ObjectType.Layout.FILE,
ObjectType.Layout.DASHBOARD,
ObjectType.Layout.IMAGE,
ObjectType.Layout.SPACE,
ObjectType.Layout.SET,
ObjectType.Layout.DATABASE -> Options.ALL.copy(
hasIcon = hasIcon,
hasCover = hasCover,
hasLayout = hasLayout,
)
ObjectType.Layout.TODO -> Options(
hasIcon = false,
hasCover = hasCover,
hasLayout = hasLayout,
hasRelations = true,
hasHistory = true
)
ObjectType.Layout.NOTE -> Options(
hasIcon = false,
hasCover = false,
hasLayout = hasLayout,
hasRelations = true,
hasHistory = true
)
}
} else {
// unknown layout show all options
Options.ALL.copy(
hasIcon = hasIcon,
hasCover = hasCover,
hasLayout = hasLayout,
)
}
return options
}
}

View file

@ -1,11 +1,10 @@
package com.anytypeio.anytype.presentation.objects
package com.anytypeio.anytype.presentation.objects.menu
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.analytics.base.EventsDictionary.objectLock
import com.anytypeio.anytype.analytics.base.EventsDictionary.objectUnlock
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.analytics.base.sendEvent
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Id
@ -17,154 +16,19 @@ import com.anytypeio.anytype.domain.dashboard.interactor.AddToFavorite
import com.anytypeio.anytype.domain.dashboard.interactor.RemoveFromFavorite
import com.anytypeio.anytype.domain.objects.SetObjectIsArchived
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.common.Delegator
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.extension.sendAnalyticsAddToFavoritesEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsMoveToBinEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsRemoveFromFavoritesEvent
import com.anytypeio.anytype.presentation.sets.ObjectSet
import com.anytypeio.anytype.presentation.objects.ObjectAction
import com.anytypeio.anytype.presentation.util.Dispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
abstract class ObjectMenuViewModelBase(
private val setObjectIsArchived: SetObjectIsArchived,
private val addToFavorite: AddToFavorite,
private val removeFromFavorite: RemoveFromFavorite,
protected val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics
) : BaseViewModel() {
val isDismissed = MutableStateFlow(false)
val isObjectArchived = MutableStateFlow(false)
val commands = MutableSharedFlow<Command>(replay = 0)
val actions = MutableStateFlow(emptyList<ObjectAction>())
abstract fun onIconClicked(ctx: Id)
abstract fun onCoverClicked(ctx: Id)
abstract fun onLayoutClicked(ctx: Id)
abstract fun onRelationsClicked()
abstract fun onHistoryClicked()
fun onStart(
ctx: Id,
isFavorite: Boolean,
isArchived: Boolean,
isProfile: Boolean
) {
actions.value = buildActions(
ctx = ctx,
isArchived = isArchived,
isFavorite = isFavorite,
isProfile = isProfile
)
}
abstract fun onActionClicked(ctx: Id, action: ObjectAction)
abstract fun buildActions(
ctx: Id,
isArchived: Boolean,
isFavorite: Boolean,
isProfile: Boolean
): MutableList<ObjectAction>
protected fun proceedWithRemovingFromFavorites(ctx: Id) {
viewModelScope.launch {
removeFromFavorite(
RemoveFromFavorite.Params(
target = ctx
)
).process(
failure = { Timber.e(it, "Error while removing from favorite.") },
success = {
sendAnalyticsRemoveFromFavoritesEvent(analytics)
dispatcher.send(it)
_toasts.emit(REMOVE_FROM_FAVORITE_SUCCESS_MSG).also {
isDismissed.value = true
}
}
)
}
}
protected fun proceedWithAddingToFavorites(ctx: Id) {
viewModelScope.launch {
addToFavorite(
AddToFavorite.Params(
target = ctx
)
).process(
failure = { Timber.e(it, "Error while adding to favorites.") },
success = {
sendAnalyticsAddToFavoritesEvent(analytics)
dispatcher.send(it)
_toasts.emit(ADD_TO_FAVORITE_SUCCESS_MSG).also {
isDismissed.value = true
}
}
)
}
}
fun proceedWithUpdatingArchivedStatus(ctx: Id, isArchived: Boolean) {
viewModelScope.launch {
setObjectIsArchived(
SetObjectIsArchived.Params(
context = ctx,
isArchived = isArchived
)
).process(
failure = {
Timber.e(it, ARCHIVE_OBJECT_ERR_MSG)
_toasts.emit(ARCHIVE_OBJECT_ERR_MSG)
},
success = {
if (isArchived) {
sendAnalyticsMoveToBinEvent(analytics)
_toasts.emit(ARCHIVE_OBJECT_SUCCESS_MSG)
} else {
_toasts.emit(RESTORE_OBJECT_SUCCESS_MSG)
}
isObjectArchived.value = true
}
)
}
}
sealed class Command {
object OpenObjectIcons : Command()
object OpenSetIcons : Command()
object OpenObjectCover : Command()
object OpenSetCover : Command()
object OpenObjectLayout : Command()
object OpenSetLayout : Command()
object OpenObjectRelations : Command()
object OpenSetRelations : Command()
}
companion object {
const val ARCHIVE_OBJECT_SUCCESS_MSG = "Object archived!"
const val RESTORE_OBJECT_SUCCESS_MSG = "Object restored!"
const val ARCHIVE_OBJECT_ERR_MSG =
"Error while changing is-archived status for this object. Please, try again later."
const val ADD_TO_FAVORITE_SUCCESS_MSG = "Object added to favorites."
const val REMOVE_FROM_FAVORITE_SUCCESS_MSG = "Object removed from favorites."
const val COMING_SOON_MSG = "Coming soon..."
const val NOT_ALLOWED = "Not allowed for this object"
const val OBJECT_IS_LOCKED_MSG = "Your object is locked"
const val OBJECT_IS_UNLOCKED_MSG = "Your object is locked"
const val SOMETHING_WENT_WRONG_MSG = "Something went wrong. Please, try again later."
}
}
class ObjectMenuViewModel(
setObjectIsArchived: SetObjectIsArchived,
addToFavorite: AddToFavorite,
removeFromFavorite: RemoveFromFavorite,
dispatcher: Dispatcher<Payload>,
menuOptionsProvider: ObjectMenuOptionsProvider,
private val duplicateObject: DuplicateObject,
private val storage: Editor.Storage,
private val analytics: Analytics,
@ -175,7 +39,8 @@ class ObjectMenuViewModel(
addToFavorite = addToFavorite,
removeFromFavorite = removeFromFavorite,
dispatcher = dispatcher,
analytics = analytics
analytics = analytics,
menuOptionsProvider = menuOptionsProvider
) {
private val objectRestrictions = storage.objectRestrictions.current()
@ -185,7 +50,7 @@ class ObjectMenuViewModel(
isArchived: Boolean,
isFavorite: Boolean,
isProfile: Boolean
): MutableList<ObjectAction> = mutableListOf<ObjectAction>().apply {
): List<ObjectAction> = buildList {
if (!isProfile) {
if (isArchived) {
add(ObjectAction.RESTORE)
@ -365,7 +230,7 @@ class ObjectMenuViewModel(
if (isLocked) {
sendEvent(
analytics = analytics,
eventName = objectLock
eventName = EventsDictionary.objectLock
)
_toasts.emit(OBJECT_IS_LOCKED_MSG).also {
isDismissed.value = true
@ -373,7 +238,7 @@ class ObjectMenuViewModel(
} else {
sendEvent(
analytics = analytics,
eventName = objectUnlock
eventName = EventsDictionary.objectUnlock
)
_toasts.emit(OBJECT_IS_UNLOCKED_MSG).also {
isDismissed.value = true
@ -405,7 +270,8 @@ class ObjectMenuViewModel(
private val analytics: Analytics,
private val dispatcher: Dispatcher<Payload>,
private val updateFields: UpdateFields,
private val delegator: Delegator<Action>
private val delegator: Delegator<Action>,
private val menuOptionsProvider: ObjectMenuOptionsProvider
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ObjectMenuViewModel(
@ -417,129 +283,9 @@ class ObjectMenuViewModel(
analytics = analytics,
dispatcher = dispatcher,
updateFields = updateFields,
delegator = delegator
delegator = delegator,
menuOptionsProvider = menuOptionsProvider
) as T
}
}
}
class ObjectSetMenuViewModel(
setObjectIsArchived: SetObjectIsArchived,
addToFavorite: AddToFavorite,
removeFromFavorite: RemoveFromFavorite,
dispatcher: Dispatcher<Payload>,
private val analytics: Analytics,
state: StateFlow<ObjectSet>
) : ObjectMenuViewModelBase(
setObjectIsArchived = setObjectIsArchived,
addToFavorite = addToFavorite,
removeFromFavorite = removeFromFavorite,
dispatcher = dispatcher,
analytics = analytics
) {
private val objectRestrictions = state.value.objectRestrictions
@Suppress("UNCHECKED_CAST")
class Factory(
private val setObjectIsArchived: SetObjectIsArchived,
private val addToFavorite: AddToFavorite,
private val removeFromFavorite: RemoveFromFavorite,
private val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics,
private val state: StateFlow<ObjectSet>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ObjectSetMenuViewModel(
setObjectIsArchived = setObjectIsArchived,
addToFavorite = addToFavorite,
removeFromFavorite = removeFromFavorite,
analytics = analytics,
state = state,
dispatcher = dispatcher
) as T
}
}
override fun onIconClicked(ctx: Id) {
viewModelScope.launch {
if (objectRestrictions.contains(ObjectRestriction.DETAILS)) {
_toasts.emit(NOT_ALLOWED)
} else {
commands.emit(Command.OpenSetIcons)
}
}
}
override fun onCoverClicked(ctx: Id) {
viewModelScope.launch {
if (objectRestrictions.contains(ObjectRestriction.DETAILS)) {
_toasts.emit(NOT_ALLOWED)
} else {
commands.emit(Command.OpenSetCover)
}
}
}
override fun onLayoutClicked(ctx: Id) {
viewModelScope.launch {
if (objectRestrictions.contains(ObjectRestriction.LAYOUT_CHANGE)) {
_toasts.emit(NOT_ALLOWED)
} else {
commands.emit(Command.OpenSetLayout)
}
}
}
override fun onRelationsClicked() {
viewModelScope.launch {
if (objectRestrictions.contains(ObjectRestriction.RELATIONS)) {
_toasts.emit(NOT_ALLOWED)
} else {
commands.emit(Command.OpenSetRelations)
}
}
}
override fun onHistoryClicked() {
viewModelScope.launch { _toasts.emit(COMING_SOON_MSG) }
}
override fun buildActions(
ctx: Id,
isArchived: Boolean,
isFavorite: Boolean,
isProfile: Boolean
): MutableList<ObjectAction> = mutableListOf<ObjectAction>().apply {
if (isArchived) {
add(ObjectAction.RESTORE)
} else {
add(ObjectAction.DELETE)
}
if (isFavorite) {
add(ObjectAction.REMOVE_FROM_FAVOURITE)
} else {
add(ObjectAction.ADD_TO_FAVOURITE)
}
}
override fun onActionClicked(ctx: Id, action: ObjectAction) {
when (action) {
ObjectAction.DELETE -> {
proceedWithUpdatingArchivedStatus(ctx = ctx, isArchived = true)
}
ObjectAction.RESTORE -> {
proceedWithUpdatingArchivedStatus(ctx = ctx, isArchived = false)
}
ObjectAction.ADD_TO_FAVOURITE -> {
proceedWithAddingToFavorites(ctx)
}
ObjectAction.REMOVE_FROM_FAVOURITE -> {
proceedWithRemovingFromFavorites(ctx)
}
else -> {
viewModelScope.launch { _toasts.emit(COMING_SOON_MSG) }
}
}
}
}

View file

@ -0,0 +1,175 @@
package com.anytypeio.anytype.presentation.objects.menu
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.domain.dashboard.interactor.AddToFavorite
import com.anytypeio.anytype.domain.dashboard.interactor.RemoveFromFavorite
import com.anytypeio.anytype.domain.objects.SetObjectIsArchived
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.extension.sendAnalyticsAddToFavoritesEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsMoveToBinEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsRemoveFromFavoritesEvent
import com.anytypeio.anytype.presentation.objects.ObjectAction
import com.anytypeio.anytype.presentation.util.Dispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
abstract class ObjectMenuViewModelBase(
private val setObjectIsArchived: SetObjectIsArchived,
private val addToFavorite: AddToFavorite,
private val removeFromFavorite: RemoveFromFavorite,
protected val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics,
private val menuOptionsProvider: ObjectMenuOptionsProvider,
) : BaseViewModel() {
private val jobs = mutableListOf<Job>()
val isDismissed = MutableStateFlow(false)
val isObjectArchived = MutableStateFlow(false)
val commands = MutableSharedFlow<Command>(replay = 0)
val actions = MutableStateFlow(emptyList<ObjectAction>())
private val _options = MutableStateFlow(
ObjectMenuOptionsProvider.Options(
hasIcon = false,
hasCover = false,
hasLayout = false,
hasRelations = false,
hasHistory = false
)
)
val options: Flow<ObjectMenuOptionsProvider.Options> = _options
abstract fun onIconClicked(ctx: Id)
abstract fun onCoverClicked(ctx: Id)
abstract fun onLayoutClicked(ctx: Id)
abstract fun onRelationsClicked()
abstract fun onHistoryClicked()
fun onStop() {
jobs.forEach(Job::cancel)
jobs.clear()
}
fun onStart(
ctx: Id,
isFavorite: Boolean,
isArchived: Boolean,
isProfile: Boolean,
isLocked: Boolean
) {
actions.value = buildActions(
ctx = ctx,
isArchived = isArchived,
isFavorite = isFavorite,
isProfile = isProfile
)
jobs += viewModelScope.launch {
menuOptionsProvider.provide(ctx, isLocked).collect(_options)
}
}
abstract fun onActionClicked(ctx: Id, action: ObjectAction)
abstract fun buildActions(
ctx: Id,
isArchived: Boolean,
isFavorite: Boolean,
isProfile: Boolean
): List<ObjectAction>
protected fun proceedWithRemovingFromFavorites(ctx: Id) {
viewModelScope.launch {
removeFromFavorite(
RemoveFromFavorite.Params(
target = ctx
)
).process(
failure = { Timber.e(it, "Error while removing from favorite.") },
success = {
sendAnalyticsRemoveFromFavoritesEvent(analytics)
dispatcher.send(it)
_toasts.emit(REMOVE_FROM_FAVORITE_SUCCESS_MSG).also {
isDismissed.value = true
}
}
)
}
}
protected fun proceedWithAddingToFavorites(ctx: Id) {
viewModelScope.launch {
addToFavorite(
AddToFavorite.Params(
target = ctx
)
).process(
failure = { Timber.e(it, "Error while adding to favorites.") },
success = {
sendAnalyticsAddToFavoritesEvent(analytics)
dispatcher.send(it)
_toasts.emit(ADD_TO_FAVORITE_SUCCESS_MSG).also {
isDismissed.value = true
}
}
)
}
}
fun proceedWithUpdatingArchivedStatus(ctx: Id, isArchived: Boolean) {
viewModelScope.launch {
setObjectIsArchived(
SetObjectIsArchived.Params(
context = ctx,
isArchived = isArchived
)
).process(
failure = {
Timber.e(it, ARCHIVE_OBJECT_ERR_MSG)
_toasts.emit(ARCHIVE_OBJECT_ERR_MSG)
},
success = {
if (isArchived) {
sendAnalyticsMoveToBinEvent(analytics)
_toasts.emit(ARCHIVE_OBJECT_SUCCESS_MSG)
} else {
_toasts.emit(RESTORE_OBJECT_SUCCESS_MSG)
}
isObjectArchived.value = true
}
)
}
}
sealed class Command {
object OpenObjectIcons : Command()
object OpenSetIcons : Command()
object OpenObjectCover : Command()
object OpenSetCover : Command()
object OpenObjectLayout : Command()
object OpenSetLayout : Command()
object OpenObjectRelations : Command()
object OpenSetRelations : Command()
}
companion object {
const val ARCHIVE_OBJECT_SUCCESS_MSG = "Object archived!"
const val RESTORE_OBJECT_SUCCESS_MSG = "Object restored!"
const val ARCHIVE_OBJECT_ERR_MSG =
"Error while changing is-archived status for this object. Please, try again later."
const val ADD_TO_FAVORITE_SUCCESS_MSG = "Object added to favorites."
const val REMOVE_FROM_FAVORITE_SUCCESS_MSG = "Object removed from favorites."
const val COMING_SOON_MSG = "Coming soon..."
const val NOT_ALLOWED = "Not allowed for this object"
const val OBJECT_IS_LOCKED_MSG = "Your object is locked"
const val OBJECT_IS_UNLOCKED_MSG = "Your object is locked"
const val SOMETHING_WENT_WRONG_MSG = "Something went wrong. Please, try again later."
}
}

View file

@ -0,0 +1,142 @@
package com.anytypeio.anytype.presentation.objects.menu
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import com.anytypeio.anytype.domain.dashboard.interactor.AddToFavorite
import com.anytypeio.anytype.domain.dashboard.interactor.RemoveFromFavorite
import com.anytypeio.anytype.domain.objects.SetObjectIsArchived
import com.anytypeio.anytype.presentation.objects.ObjectAction
import com.anytypeio.anytype.presentation.sets.ObjectSet
import com.anytypeio.anytype.presentation.util.Dispatcher
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class ObjectSetMenuViewModel(
setObjectIsArchived: SetObjectIsArchived,
addToFavorite: AddToFavorite,
removeFromFavorite: RemoveFromFavorite,
dispatcher: Dispatcher<Payload>,
state: StateFlow<ObjectSet>,
menuOptionsProvider: ObjectMenuOptionsProvider,
private val analytics: Analytics,
) : ObjectMenuViewModelBase(
setObjectIsArchived = setObjectIsArchived,
addToFavorite = addToFavorite,
removeFromFavorite = removeFromFavorite,
dispatcher = dispatcher,
analytics = analytics,
menuOptionsProvider = menuOptionsProvider,
) {
private val objectRestrictions = state.value.objectRestrictions
@Suppress("UNCHECKED_CAST")
class Factory(
private val setObjectIsArchived: SetObjectIsArchived,
private val addToFavorite: AddToFavorite,
private val removeFromFavorite: RemoveFromFavorite,
private val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics,
private val state: StateFlow<ObjectSet>,
private val menuOptionsProvider: ObjectMenuOptionsProvider,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ObjectSetMenuViewModel(
setObjectIsArchived = setObjectIsArchived,
addToFavorite = addToFavorite,
removeFromFavorite = removeFromFavorite,
analytics = analytics,
state = state,
dispatcher = dispatcher,
menuOptionsProvider = menuOptionsProvider
) as T
}
}
override fun onIconClicked(ctx: Id) {
viewModelScope.launch {
if (objectRestrictions.contains(ObjectRestriction.DETAILS)) {
_toasts.emit(NOT_ALLOWED)
} else {
commands.emit(Command.OpenSetIcons)
}
}
}
override fun onCoverClicked(ctx: Id) {
viewModelScope.launch {
if (objectRestrictions.contains(ObjectRestriction.DETAILS)) {
_toasts.emit(NOT_ALLOWED)
} else {
commands.emit(Command.OpenSetCover)
}
}
}
override fun onLayoutClicked(ctx: Id) {
viewModelScope.launch {
if (objectRestrictions.contains(ObjectRestriction.LAYOUT_CHANGE)) {
_toasts.emit(NOT_ALLOWED)
} else {
commands.emit(Command.OpenSetLayout)
}
}
}
override fun onRelationsClicked() {
viewModelScope.launch {
if (objectRestrictions.contains(ObjectRestriction.RELATIONS)) {
_toasts.emit(NOT_ALLOWED)
} else {
commands.emit(Command.OpenSetRelations)
}
}
}
override fun onHistoryClicked() {
viewModelScope.launch { _toasts.emit(COMING_SOON_MSG) }
}
override fun buildActions(
ctx: Id,
isArchived: Boolean,
isFavorite: Boolean,
isProfile: Boolean
): List<ObjectAction> = buildList {
if (isArchived) {
add(ObjectAction.RESTORE)
} else {
add(ObjectAction.DELETE)
}
if (isFavorite) {
add(ObjectAction.REMOVE_FROM_FAVOURITE)
} else {
add(ObjectAction.ADD_TO_FAVOURITE)
}
}
override fun onActionClicked(ctx: Id, action: ObjectAction) {
when (action) {
ObjectAction.DELETE -> {
proceedWithUpdatingArchivedStatus(ctx = ctx, isArchived = true)
}
ObjectAction.RESTORE -> {
proceedWithUpdatingArchivedStatus(ctx = ctx, isArchived = false)
}
ObjectAction.ADD_TO_FAVOURITE -> {
proceedWithAddingToFavorites(ctx)
}
ObjectAction.REMOVE_FROM_FAVOURITE -> {
proceedWithRemovingFromFavorites(ctx)
}
else -> {
viewModelScope.launch { _toasts.emit(COMING_SOON_MSG) }
}
}
}
}

View file

@ -0,0 +1,127 @@
package com.anytypeio.anytype.presentation.objects.menu
import app.cash.turbine.test
import com.anytypeio.anytype.core_models.Block.Fields
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.test.assertEquals
@ExperimentalCoroutinesApi
class ObjectMenuOptionsProviderImplTest {
private val objectId: String = "objectId"
private val details = MutableStateFlow<Map<Id, Fields>>(mapOf())
private val restrictions = MutableStateFlow<List<ObjectRestriction>>(emptyList())
private val provider = ObjectMenuOptionsProviderImpl(details, restrictions)
@Test
fun `when layout note - options are layout, relations, history`() {
details.value = mapOf(
objectId to Fields(map = mapOf(Relations.LAYOUT to ObjectType.Layout.NOTE.code.toDouble()))
)
val expected = ObjectMenuOptionsProvider.Options(
hasIcon = false,
hasCover = false,
hasLayout = true,
hasRelations = true,
hasHistory = true
)
assertOptions(
expected = expected
)
}
@Test
fun `when layout task - options are layout, relations, history`() {
details.value = mapOf(
objectId to Fields(map = mapOf(Relations.LAYOUT to ObjectType.Layout.TODO.code.toDouble()))
)
val expected = ObjectMenuOptionsProvider.Options(
hasIcon = false,
hasCover = true,
hasLayout = true,
hasRelations = true,
hasHistory = true
)
assertOptions(
expected = expected
)
}
@Test
fun `when layout basic - all options are visible`() {
details.value = mapOf(
objectId to Fields(map = mapOf(Relations.LAYOUT to ObjectType.Layout.BASIC.code.toDouble()))
)
assertOptions(
expected = ObjectMenuOptionsProvider.Options.ALL
)
}
@Test
fun `when layout null - all options are visible`() {
details.value = mapOf(
objectId to Fields(map = mapOf(Relations.LAYOUT to null))
)
assertOptions(
expected = ObjectMenuOptionsProvider.Options.ALL
)
}
@Test
fun `when restricts layout_change - layout options is invisible`() {
details.value = mapOf(
objectId to Fields(map = mapOf(Relations.LAYOUT to null))
)
restrictions.value = listOf(ObjectRestriction.LAYOUT_CHANGE)
assertOptions(
expected = ObjectMenuOptionsProvider.Options.ALL.copy(hasLayout = false)
)
}
@Test
fun `when object is Locked - show relations and history`() {
details.value = mapOf(
objectId to Fields(map = mapOf(Relations.LAYOUT to null))
)
assertOptions(
isLocked = true,
expected = ObjectMenuOptionsProvider.Options(
hasIcon = false,
hasCover = false,
hasLayout = false,
hasRelations = true,
hasHistory = true,
)
)
}
private fun assertOptions(
expected: ObjectMenuOptionsProvider.Options,
isLocked: Boolean = false,
) {
runTest {
provider.provide(objectId, isLocked).test {
assertEquals(
expected = expected,
actual = awaitItem()
)
}
}
}
}