diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectMenuDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectMenuDI.kt index 323223a394..18fc590bcb 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectMenuDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectMenuDI.kt @@ -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) = + ObjectMenuOptionsProviderImpl( + details = state.map { it.details }, + restrictions = state.map { it.objectRestrictions } + ) } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/editor/sheets/ObjectMenuBaseFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/editor/sheets/ObjectMenuBaseFragment.kt index e2bd7df295..d872144db1 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/editor/sheets/ObjectMenuBaseFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/editor/sheets/ObjectMenuBaseFragment.kt @@ -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() { @@ -94,16 +90,37 @@ abstract class ObjectMenuBaseFragment : BaseBottomSheetFragment 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 { 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) - } } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/editor/sheets/ObjectMenuFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/editor/sheets/ObjectMenuFragment.kt new file mode 100644 index 0000000000..ace553ead0 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/editor/sheets/ObjectMenuFragment.kt @@ -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 { 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetMenuFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetMenuFragment.kt index c3c6e20527..4121bbe789 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetMenuFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetMenuFragment.kt @@ -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 diff --git a/app/src/main/res/layout/fragment_object_menu.xml b/app/src/main/res/layout/fragment_object_menu.xml index 4edcef0832..c89893e2e3 100644 --- a/app/src/main/res/layout/fragment_object_menu.xml +++ b/app/src/main/res/layout/fragment_object_menu.xml @@ -28,7 +28,7 @@ app:title="@string/icon" /> + app:layout_constraintTop_toBottomOf="@+id/historyDivider"> ObjectType.Layout.values().find { layout -> + is Double -> ObjectType.Layout.values().singleOrNull { layout -> layout.code == value.toInt() } else -> null diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProvider.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProvider.kt new file mode 100644 index 0000000000..49aa1d1326 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProvider.kt @@ -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 +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImpl.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImpl.kt new file mode 100644 index 0000000000..102d46e729 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImpl.kt @@ -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>, + private val restrictions: Flow>, +) : ObjectMenuOptionsProvider { + + private fun observeLayout(ctx: Id): Flow { + 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 { + return combine(observeLayout(ctx), restrictions) { layout, restrictions -> + createOptions(layout, restrictions, isLocked) + } + } + + private fun createOptions( + layout: ObjectType.Layout?, + restrictions: List, + 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 + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectMenuViewModelBase.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModel.kt similarity index 51% rename from presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectMenuViewModelBase.kt rename to presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModel.kt index a484bf7788..01b76054c9 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectMenuViewModelBase.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModel.kt @@ -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, - private val analytics: Analytics -) : BaseViewModel() { - - val isDismissed = MutableStateFlow(false) - val isObjectArchived = MutableStateFlow(false) - val commands = MutableSharedFlow(replay = 0) - val actions = MutableStateFlow(emptyList()) - - 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 - - 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, + 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 = mutableListOf().apply { + ): List = 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, private val updateFields: UpdateFields, - private val delegator: Delegator + private val delegator: Delegator, + private val menuOptionsProvider: ObjectMenuOptionsProvider ) : ViewModelProvider.Factory { override fun create(modelClass: Class): 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, - private val analytics: Analytics, - state: StateFlow -) : 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, - private val analytics: Analytics, - private val state: StateFlow - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): 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 = mutableListOf().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) } - } - } - } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModelBase.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModelBase.kt new file mode 100644 index 0000000000..bc8520f492 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModelBase.kt @@ -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, + private val analytics: Analytics, + private val menuOptionsProvider: ObjectMenuOptionsProvider, +) : BaseViewModel() { + + private val jobs = mutableListOf() + val isDismissed = MutableStateFlow(false) + val isObjectArchived = MutableStateFlow(false) + val commands = MutableSharedFlow(replay = 0) + val actions = MutableStateFlow(emptyList()) + + private val _options = MutableStateFlow( + ObjectMenuOptionsProvider.Options( + hasIcon = false, + hasCover = false, + hasLayout = false, + hasRelations = false, + hasHistory = false + ) + ) + val options: Flow = _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 + + 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." + } +} + diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectSetMenuViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectSetMenuViewModel.kt new file mode 100644 index 0000000000..0b658c87f4 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectSetMenuViewModel.kt @@ -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, + state: StateFlow, + 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, + private val analytics: Analytics, + private val state: StateFlow, + private val menuOptionsProvider: ObjectMenuOptionsProvider, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): 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 = 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) } + } + } + } +} \ No newline at end of file diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImplTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImplTest.kt new file mode 100644 index 0000000000..81a6d17c57 --- /dev/null +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImplTest.kt @@ -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>(mapOf()) + private val restrictions = MutableStateFlow>(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() + ) + } + } + } +} \ No newline at end of file