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 717d5c7eae..bfd76f1544 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 @@ -230,14 +230,14 @@ object ObjectMenuModule { @JvmStatic @Provides @PerDialog - fun addToFeaturedRelations(repo: BlockRepository): AddToFeaturedRelations = - AddToFeaturedRelations(repo) + fun addToFeaturedRelations(repo: BlockRepository, dispatchers: AppCoroutineDispatchers): AddToFeaturedRelations = + AddToFeaturedRelations(repo, dispatchers) @JvmStatic @Provides @PerDialog - fun removeFromFeaturedRelations(repo: BlockRepository): RemoveFromFeaturedRelations = - RemoveFromFeaturedRelations(repo) + fun removeFromFeaturedRelations(repo: BlockRepository, dispatchers: AppCoroutineDispatchers): RemoveFromFeaturedRelations = + RemoveFromFeaturedRelations(repo, dispatchers) } @Module @@ -404,12 +404,12 @@ object ObjectSetMenuModule { @JvmStatic @Provides @PerDialog - fun addToFeaturedRelations(repo: BlockRepository): AddToFeaturedRelations = - AddToFeaturedRelations(repo) + fun addToFeaturedRelations(repo: BlockRepository, dispatchers: AppCoroutineDispatchers): AddToFeaturedRelations = + AddToFeaturedRelations(repo, dispatchers) @JvmStatic @Provides @PerDialog - fun removeFromFeaturedRelations(repo: BlockRepository): RemoveFromFeaturedRelations = - RemoveFromFeaturedRelations(repo) + fun removeFromFeaturedRelations(repo: BlockRepository, dispatchers: AppCoroutineDispatchers): RemoveFromFeaturedRelations = + RemoveFromFeaturedRelations(repo, dispatchers) } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationListDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationListDI.kt index d0c1342b52..cd68695151 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationListDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationListDI.kt @@ -94,14 +94,14 @@ object ObjectRelationListModule { @JvmStatic @Provides @PerModal - fun addToFeaturedRelations(repo: BlockRepository): AddToFeaturedRelations = - AddToFeaturedRelations(repo) + fun addToFeaturedRelations(repo: BlockRepository, dispatchers: AppCoroutineDispatchers): AddToFeaturedRelations = + AddToFeaturedRelations(repo, dispatchers) @JvmStatic @Provides @PerModal - fun removeFromFeaturedRelations(repo: BlockRepository): RemoveFromFeaturedRelations = - RemoveFromFeaturedRelations(repo) + fun removeFromFeaturedRelations(repo: BlockRepository, dispatchers: AppCoroutineDispatchers): RemoveFromFeaturedRelations = + RemoveFromFeaturedRelations(repo = repo, dispatchers = dispatchers) @JvmStatic @Provides diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/PrimitivesObjectTypeDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/PrimitivesObjectTypeDI.kt index 88f4fbe9f7..88e1b34b82 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/PrimitivesObjectTypeDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/PrimitivesObjectTypeDI.kt @@ -7,6 +7,7 @@ import com.anytypeio.anytype.core_utils.di.scope.CreateFromScratch import com.anytypeio.anytype.core_utils.di.scope.PerScreen import com.anytypeio.anytype.di.common.ComponentDependencies import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.block.interactor.UpdateText import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.config.ConfigStorage import com.anytypeio.anytype.domain.config.UserSettingsRepository @@ -28,7 +29,9 @@ import com.anytypeio.anytype.domain.primitives.FieldParser import com.anytypeio.anytype.domain.primitives.GetObjectTypeConflictingFields import com.anytypeio.anytype.domain.primitives.SetObjectTypeHeaderRecommendedFields import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields +import com.anytypeio.anytype.domain.relations.AddToFeaturedRelations import com.anytypeio.anytype.domain.relations.CreateRelation +import com.anytypeio.anytype.domain.relations.RemoveFromFeaturedRelations import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.domain.search.SubscriptionEventChannel import com.anytypeio.anytype.domain.types.CreateObjectType @@ -168,6 +171,31 @@ object ObjectTypeModule { dispatchers: AppCoroutineDispatchers ): SetObjectListIsArchived = SetObjectListIsArchived(repo, dispatchers) + @JvmStatic + @Provides + @PerScreen + fun provideAddToFeaturedRelations( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): AddToFeaturedRelations = AddToFeaturedRelations(repo, dispatchers) + + @JvmStatic + @Provides + @PerScreen + fun provideRemoveFromFeaturedRelations( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): RemoveFromFeaturedRelations = RemoveFromFeaturedRelations(repo, dispatchers) + + @JvmStatic + @Provides + @PerScreen + fun provideUpdateBlockUseCase( + repo: BlockRepository + ): UpdateText = UpdateText( + repo = repo + ) + @Module interface Declarations { @PerScreen diff --git a/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFragment.kt index 52abfd8546..6db8953ade 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFragment.kt @@ -24,7 +24,6 @@ import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_ui.views.BaseAlertDialog import com.anytypeio.anytype.core_utils.ext.argString -import com.anytypeio.anytype.core_utils.ext.safeNavigate import com.anytypeio.anytype.core_utils.ext.subscribe import com.anytypeio.anytype.core_utils.ext.toast import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment @@ -41,7 +40,6 @@ import com.anytypeio.anytype.feature_object_type.ui.menu.ObjectTypeMenu import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeVMFactory import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeViewModel import com.anytypeio.anytype.ui.editor.EditorModalFragment -import com.anytypeio.anytype.ui.editor.sheets.ObjectMenuBaseFragment import com.anytypeio.anytype.ui.templates.EditorTemplateFragment.Companion.TYPE_TEMPLATE_EDIT import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi import com.google.accompanist.navigation.material.rememberBottomSheetNavigator @@ -143,6 +141,7 @@ class ObjectTypeFragment : BaseComposeFragment() { uiSyncStatusBadgeState = vm.uiSyncStatusBadgeState.collectAsStateWithLifecycle().value, uiIconState = vm.uiIconState.collectAsStateWithLifecycle().value, uiTitleState = vm.uiTitleState.collectAsStateWithLifecycle().value, + uiDescriptionState = vm.uiDescriptionState.collectAsStateWithLifecycle().value, uiHorizontalButtonsState = vm.uiHorizontalButtonsState.collectAsStateWithLifecycle().value, uiTemplatesModalListState = vm.uiTemplatesModalListState.collectAsStateWithLifecycle().value, uiLayoutTypeState = vm.uiTypeLayoutsState.collectAsStateWithLifecycle().value, @@ -167,6 +166,8 @@ class ObjectTypeFragment : BaseComposeFragment() { if (menuState.isVisible) { ObjectTypeMenu( isPinned = menuState.isPinned, + canDelete = menuState.canDelete, + isDescriptionFeatured = menuState.isDescriptionFeatured, onEvent = vm::onMenuEvent ) } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/primitives/WithSetScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/primitives/WithSetScreen.kt index fb875b4cc6..294180cf48 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/primitives/WithSetScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/primitives/WithSetScreen.kt @@ -40,7 +40,9 @@ import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState import com.anytypeio.anytype.feature_object_type.ui.UiSyncStatusBadgeState import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesModalListState import com.anytypeio.anytype.feature_object_type.ui.UiTitleState +import com.anytypeio.anytype.feature_object_type.ui.UiDescriptionState import com.anytypeio.anytype.feature_object_type.ui.alerts.DeleteAlertScreen +import com.anytypeio.anytype.feature_object_type.ui.header.DescriptionWidget import com.anytypeio.anytype.feature_object_type.ui.header.HorizontalButtons import com.anytypeio.anytype.feature_object_type.ui.header.IconAndTitleWidget import com.anytypeio.anytype.feature_object_type.ui.layouts.TypeLayoutsScreen @@ -57,6 +59,7 @@ fun WithSetScreen( //header uiIconState: UiIconState, uiTitleState: UiTitleState, + uiDescriptionState: UiDescriptionState, //layout, properties and templates buttons uiHorizontalButtonsState: UiHorizontalButtonsState, uiLayoutTypeState: UiLayoutTypeState, @@ -99,6 +102,7 @@ fun WithSetScreen( paddingValues = paddingValues, uiIconState = uiIconState, uiTitleState = uiTitleState, + uiDescriptionState = uiDescriptionState, uiHorizontalButtonsState = uiHorizontalButtonsState, objectId = objectId, space = space, @@ -142,6 +146,7 @@ private fun MainContentSet( paddingValues: PaddingValues, uiIconState: UiIconState, uiTitleState: UiTitleState, + uiDescriptionState: UiDescriptionState, uiHorizontalButtonsState: UiHorizontalButtonsState, objectId: String, space: String, @@ -170,6 +175,21 @@ private fun MainContentSet( uiTitleState = uiTitleState, onTypeEvent = onTypeEvent ) + + if (uiDescriptionState.isVisible) { + Spacer(modifier = Modifier.height(8.dp)) + DescriptionWidget( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 20.dp), + uiDescriptionState = uiDescriptionState, + onDescriptionChanged = { text -> + onTypeEvent(TypeEvent.OnDescriptionChanged(text)) + } + ) + } + if (uiHorizontalButtonsState.isVisible) { Spacer(modifier = Modifier.height(20.dp)) HorizontalButtons( diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt index d3848e0ee9..0600b8459a 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt @@ -234,6 +234,8 @@ sealed class ObjectWrapper { val orderId: String? get() = getSingleValue(Relations.ORDER_ID) + val featuredRelations: List get() = getValues(Relations.FEATURED_RELATIONS) + val allRecommendedRelations: List get() = recommendedFeaturedRelations + recommendedRelations + recommendedFileRelations + recommendedHiddenRelations diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/relations/AddToFeaturedRelations.kt b/domain/src/main/java/com/anytypeio/anytype/domain/relations/AddToFeaturedRelations.kt index 2c17b51912..028814fd7e 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/relations/AddToFeaturedRelations.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/relations/AddToFeaturedRelations.kt @@ -2,18 +2,20 @@ package com.anytypeio.anytype.domain.relations import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Payload -import com.anytypeio.anytype.domain.base.BaseUseCase +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor import com.anytypeio.anytype.domain.block.repo.BlockRepository /** * Use-case for adding one or more relations to featured relations list. */ class AddToFeaturedRelations( - private val repo: BlockRepository -) : BaseUseCase() { + private val repo: BlockRepository, + dispatchers: AppCoroutineDispatchers +) : ResultInteractor(dispatchers.io) { - override suspend fun run(params: Params) = safe { - repo.addToFeaturedRelations( + override suspend fun doWork(params: Params): Payload { + return repo.addToFeaturedRelations( ctx = params.ctx, relations = params.relations ) diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/relations/RemoveFromFeaturedRelations.kt b/domain/src/main/java/com/anytypeio/anytype/domain/relations/RemoveFromFeaturedRelations.kt index 151b4c4668..dcccbf05b7 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/relations/RemoveFromFeaturedRelations.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/relations/RemoveFromFeaturedRelations.kt @@ -2,18 +2,20 @@ package com.anytypeio.anytype.domain.relations import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Payload -import com.anytypeio.anytype.domain.base.BaseUseCase +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor import com.anytypeio.anytype.domain.block.repo.BlockRepository /** * Use-case for removing one or more relations from featured relations list. */ class RemoveFromFeaturedRelations( - private val repo: BlockRepository -) : BaseUseCase() { + private val repo: BlockRepository, + dispatchers: AppCoroutineDispatchers +) : ResultInteractor(dispatchers.io) { - override suspend fun run(params: Params) = safe { - repo.removeFromFeaturedRelations( + override suspend fun doWork(params: Params): Payload { + return repo.removeFromFeaturedRelations( ctx = params.ctx, relations = params.relations ) diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionManager.kt b/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionManager.kt index 6f7ca6d22d..94cc25c018 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionManager.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionManager.kt @@ -122,7 +122,8 @@ class ObjectTypesSubscriptionManager ( Relations.WIDGET_LAYOUT, Relations.WIDGET_LIMIT, Relations.WIDGET_VIEW_ID, - Relations.ORDER_ID + Relations.ORDER_ID, + Relations.FEATURED_RELATIONS ), ignoreWorkspace = true ) diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiEvent.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiEvent.kt index d39f6c0e41..5ca9059740 100644 --- a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiEvent.kt +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiEvent.kt @@ -21,6 +21,7 @@ sealed class TypeEvent { data object OnObjectTypeIconClick : TypeEvent() data class OnObjectTypeTitleUpdate(val title: String) : TypeEvent() data object OnObjectTypeTitleClick : TypeEvent() + data class OnDescriptionChanged(val text: String) : TypeEvent() //endregion //region Templates diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiState.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiState.kt index 9740f96b0f..6ff6db79aa 100644 --- a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiState.kt +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiState.kt @@ -46,6 +46,20 @@ data class UiIconState(val icon: ObjectIcon.TypeIcon, val isEditable: Boolean) { val EMPTY = UiIconState(icon = ObjectIcon.TypeIcon.Default.DEFAULT, isEditable = false) } } + +data class UiDescriptionState( + val description: String, + val isVisible: Boolean, + val isEditable: Boolean +) { + companion object { + val EMPTY = UiDescriptionState( + description = "", + isVisible = false, + isEditable = false + ) + } +} //endregion //region LAYOUTS diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/DescriptionWidget.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/DescriptionWidget.kt new file mode 100644 index 0000000000..0df2889abe --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/DescriptionWidget.kt @@ -0,0 +1,129 @@ +package com.anytypeio.anytype.feature_object_type.ui.header + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.views.Relations1 +import com.anytypeio.anytype.feature_object_type.ui.UiDescriptionState +import kotlinx.coroutines.delay + +/** + * Description widget for ObjectType screen. + * Displays an editable description field styled as Relations1 (15sp, Inter Regular). + * Shows placeholder text when empty. + */ +@Composable +fun DescriptionWidget( + modifier: Modifier = Modifier, + uiDescriptionState: UiDescriptionState, + onDescriptionChanged: (String) -> Unit +) { + if (!uiDescriptionState.isVisible) return + + var text by remember { + mutableStateOf(uiDescriptionState.description) + } + + LaunchedEffect(uiDescriptionState.description) { + if (text != uiDescriptionState.description) { + text = uiDescriptionState.description + } + } + + LaunchedEffect(text) { + if (text != uiDescriptionState.description) { + delay(500L) // Debounce for 500ms + onDescriptionChanged(text) + } + } + + val focusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current + + BasicTextField( + value = text, + onValueChange = { text = it }, + modifier = modifier, + textStyle = Relations1.copy(color = colorResource(id = R.color.text_primary)), + enabled = uiDescriptionState.isEditable, + cursorBrush = SolidColor(colorResource(id = R.color.text_primary)), + decorationBox = { innerTextField -> + Box { + if (text.isEmpty()) { + Text( + text = stringResource(R.string.description), + style = Relations1, + color = colorResource(id = R.color.text_tertiary) + ) + } + innerTextField() + } + }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + // Send the final text immediately, bypassing the debounce + if (text != uiDescriptionState.description) { + onDescriptionChanged(text) + } + + // Then hide the keyboard and clear focus as before + keyboardController?.hide() + focusManager.clearFocus() + } + ) + ) +} + +@DefaultPreviews +@Composable +private fun DescriptionWidgetPreview() { + DescriptionWidget( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + uiDescriptionState = UiDescriptionState( + description = "", + isEditable = true, + isVisible = true + ), + onDescriptionChanged = {} + ) +} + +@DefaultPreviews +@Composable +private fun DescriptionWidgetWithTextPreview() { + DescriptionWidget( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + uiDescriptionState = UiDescriptionState( + description = "This is a description of the object type.", + isEditable = true, + isVisible = true + ), + onDescriptionChanged = {} + ) +} diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/menu/ObjectTypeMenu.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/menu/ObjectTypeMenu.kt index f998485331..589f2bc194 100644 --- a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/menu/ObjectTypeMenu.kt +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/menu/ObjectTypeMenu.kt @@ -34,13 +34,13 @@ import com.anytypeio.anytype.core_ui.foundation.Dragger import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable import com.anytypeio.anytype.core_ui.views.Caption2Regular import com.anytypeio.anytype.core_ui.views.PreviewTitle1Regular -import com.anytypeio.anytype.core_ui.views.Relations3 -import com.anytypeio.anytype.presentation.objects.ObjectIcon @OptIn(ExperimentalMaterial3Api::class) @Composable fun ObjectTypeMenu( isPinned: Boolean, + canDelete: Boolean, + isDescriptionFeatured: Boolean, onEvent: (ObjectTypeMenuEvent) -> Unit ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -86,18 +86,21 @@ fun ObjectTypeMenu( Divider(paddingStart = 20.dp, paddingEnd = 20.dp) - // Description cell (disabled for now) + // Description cell MenuCell( iconRes = R.drawable.ic_obj_settings_description_24, title = stringResource(R.string.description), trailingContent = { Text( - text = stringResource(R.string.show), + text = if (isDescriptionFeatured) + stringResource(R.string.modal_hide) + else + stringResource(R.string.show), style = PreviewTitle1Regular, color = colorResource(R.color.text_secondary) ) }, - onClick = { /* onEvent(ObjectTypeMenuEvent.OnDescriptionClick) - Leave for later */ }, + onClick = { onEvent(ObjectTypeMenuEvent.OnDescriptionClick) } ) // Divider before bottom section @@ -125,15 +128,17 @@ fun ObjectTypeMenu( onClick = { onEvent(ObjectTypeMenuEvent.OnPinToggleClick) } ) - Spacer(modifier = Modifier.width(12.dp)) + // To Bin button (only show if user has delete permission) + if (canDelete) { + Spacer(modifier = Modifier.width(12.dp)) - // To Bin button - BottomActionButton( - iconRes = R.drawable.ic_object_action_archive, - text = stringResource(R.string.action_bar_to_bin), - isDestructive = true, - onClick = { onEvent(ObjectTypeMenuEvent.OnToBinClick) } - ) + BottomActionButton( + iconRes = R.drawable.ic_object_action_archive, + text = stringResource(R.string.action_bar_to_bin), + isDestructive = true, + onClick = { onEvent(ObjectTypeMenuEvent.OnToBinClick) } + ) + } } Spacer(modifier = Modifier.height(16.dp)) @@ -216,6 +221,8 @@ private fun BottomActionButton( fun ObjectTypeMenuPreview() { ObjectTypeMenu( isPinned = false, + canDelete = true, + isDescriptionFeatured = false, onEvent = {} ) } diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/menu/ObjectTypeMenuState.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/menu/ObjectTypeMenuState.kt index 9905afde87..b19945644e 100644 --- a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/menu/ObjectTypeMenuState.kt +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/menu/ObjectTypeMenuState.kt @@ -9,7 +9,8 @@ data class UiObjectTypeMenuState( val isVisible: Boolean = false, val isPinned: Boolean = false, val canDelete: Boolean = true, - val icon: ObjectIcon = ObjectIcon.None + val icon: ObjectIcon = ObjectIcon.None, + val isDescriptionFeatured: Boolean = false ) { companion object { val Hidden = UiObjectTypeMenuState(isVisible = false) diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/ObjectTypeViewModel.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/ObjectTypeViewModel.kt index 1705419f3d..1025c97931 100644 --- a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/ObjectTypeViewModel.kt +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/ObjectTypeViewModel.kt @@ -9,11 +9,15 @@ 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.Payload +import com.anytypeio.anytype.core_models.Position import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.WidgetLayout import com.anytypeio.anytype.core_models.permissions.ObjectPermissions import com.anytypeio.anytype.core_models.permissions.toObjectPermissionsForTypes +import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_ui.extensions.simpleIcon import com.anytypeio.anytype.domain.base.fold +import com.anytypeio.anytype.domain.block.interactor.UpdateText import com.anytypeio.anytype.domain.dataview.SetDataViewProperties import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider import com.anytypeio.anytype.domain.library.StoreSearchParams @@ -30,6 +34,8 @@ import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.domain.primitives.FieldParser import com.anytypeio.anytype.domain.primitives.GetObjectTypeConflictingFields import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields +import com.anytypeio.anytype.domain.relations.AddToFeaturedRelations +import com.anytypeio.anytype.domain.relations.RemoveFromFeaturedRelations import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.domain.templates.CreateTemplate import com.anytypeio.anytype.domain.widgets.CreateWidget @@ -40,21 +46,24 @@ import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListState import com.anytypeio.anytype.feature_object_type.fields.UiLocalsFieldsInfoState import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeCommand -import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeCommand.* +import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeCommand.Back +import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeCommand.OpenAddNewPropertyScreen import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams import com.anytypeio.anytype.feature_object_type.ui.TypeEvent import com.anytypeio.anytype.feature_object_type.ui.UiDeleteAlertState +import com.anytypeio.anytype.feature_object_type.ui.UiDescriptionState import com.anytypeio.anytype.feature_object_type.ui.UiEditButton import com.anytypeio.anytype.feature_object_type.ui.UiErrorState -import com.anytypeio.anytype.feature_object_type.ui.UiErrorState.* -import com.anytypeio.anytype.feature_object_type.ui.UiErrorState.Reason.* +import com.anytypeio.anytype.feature_object_type.ui.UiErrorState.Reason.ErrorEditingTypeDetails +import com.anytypeio.anytype.feature_object_type.ui.UiErrorState.Reason.Other +import com.anytypeio.anytype.feature_object_type.ui.UiErrorState.Show import com.anytypeio.anytype.feature_object_type.ui.UiHorizontalButtonsState -import com.anytypeio.anytype.feature_object_type.ui.UiPropertiesButtonState -import com.anytypeio.anytype.feature_object_type.ui.UiIconsPickerState import com.anytypeio.anytype.feature_object_type.ui.UiIconState +import com.anytypeio.anytype.feature_object_type.ui.UiIconsPickerState import com.anytypeio.anytype.feature_object_type.ui.UiLayoutButtonState import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState -import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState.* +import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState.Visible +import com.anytypeio.anytype.feature_object_type.ui.UiPropertiesButtonState import com.anytypeio.anytype.feature_object_type.ui.UiSyncStatusBadgeState import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesButtonState import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesModalListState @@ -70,7 +79,6 @@ import com.anytypeio.anytype.feature_properties.edit.UiPropertyLimitTypeItem import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider import com.anytypeio.anytype.presentation.extension.sendAnalyticsLocalPropertyResolve -import com.anytypeio.anytype.presentation.widgets.findWidgetBlockForObject import com.anytypeio.anytype.presentation.extension.sendAnalyticsPropertiesLocalInfo import com.anytypeio.anytype.presentation.extension.sendAnalyticsReorderRelationEvent import com.anytypeio.anytype.presentation.extension.sendAnalyticsScreenObjectType @@ -83,10 +91,7 @@ import com.anytypeio.anytype.presentation.sync.toSyncStatusWidgetState import com.anytypeio.anytype.presentation.sync.updateStatus import com.anytypeio.anytype.presentation.templates.TemplateView import com.anytypeio.anytype.presentation.util.Dispatcher -import com.anytypeio.anytype.core_models.Position -import com.anytypeio.anytype.core_models.WidgetLayout -import com.anytypeio.anytype.core_models.primitives.SpaceId -import kotlin.collections.map +import com.anytypeio.anytype.presentation.widgets.findWidgetBlockForObject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -129,7 +134,10 @@ class ObjectTypeViewModel( private val createWidget: CreateWidget, private val deleteWidget: DeleteWidget, private val spaceManager: SpaceManager, - private val getObject: GetObject + private val getObject: GetObject, + private val addToFeaturedRelations: AddToFeaturedRelations, + private val removeFromFeaturedRelations: RemoveFromFeaturedRelations, + private val updateText: UpdateText ) : ViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate { //region UI STATE @@ -143,6 +151,7 @@ class ObjectTypeViewModel( //header val uiTitleState = MutableStateFlow(UiTitleState.Companion.EMPTY) val uiIconState = MutableStateFlow(UiIconState.Companion.EMPTY) + val uiDescriptionState = MutableStateFlow(UiDescriptionState.EMPTY) //layout, properties and templates buttons val uiHorizontalButtonsState = @@ -193,6 +202,7 @@ class ObjectTypeViewModel( private val _objectTypeConflictingFieldIds = MutableStateFlow>(emptyList()) private val pinnedWidgetBlockId = MutableStateFlow(null) private var targetWidgetBlockId: Id? = null // Cached first widget block ID for positioning + private val _isDescriptionFeatured = MutableStateFlow(false) //endregion val showPropertiesScreen = MutableStateFlow(false) @@ -377,6 +387,10 @@ class ObjectTypeViewModel( (uiTitleAndIconUpdateState.value as? UiTypeSetupTitleAndIconState.Visible.EditType)?.let { uiTitleAndIconUpdateState.value = it.copy(icon = newIcon) } + + // Update description state + updateDescriptionState(objType, objectPermissions) + //turn off button, we give Move to Bin logic in Library now // if (objectPermissions.canDelete) { // uiEditButtonState.value = UiEditButton.Visible @@ -543,6 +557,10 @@ class ObjectTypeViewModel( showTitleAndIconUpdateScreen() } + is TypeEvent.OnDescriptionChanged -> { + onDescriptionChanged(event.text) + } + TypeEvent.OnMenuItemDeleteClick -> { uiAlertState.value = UiDeleteAlertState.Show } @@ -611,11 +629,14 @@ class ObjectTypeViewModel( } TypeEvent.OnMenuClick -> { - // Show Compose menu + // Check current description status before showing menu + checkDescriptionFeaturedStatus() uiMenuState.value = uiMenuState.value.copy( isVisible = true, icon = uiIconState.value.icon, - isPinned = pinnedWidgetBlockId.value != null + isPinned = pinnedWidgetBlockId.value != null, + canDelete = _objectTypePermissionsState.value?.canDelete ?: false, + isDescriptionFeatured = _isDescriptionFeatured.value ) } } @@ -632,10 +653,7 @@ class ObjectTypeViewModel( uiIconsPickerScreen.value = UiIconsPickerState.Visible } ObjectTypeMenuEvent.OnDescriptionClick -> { - // TODO: Implement description logic later - viewModelScope.launch { - commands.emit(ObjectTypeCommand.ShowToast("Not implemented yet")) - } + proceedWithDescriptionToggle() } ObjectTypeMenuEvent.OnToBinClick -> { uiMenuState.value = UiObjectTypeMenuState.Hidden @@ -743,6 +761,43 @@ class ObjectTypeViewModel( } } + private fun onDescriptionChanged(text: String) { + viewModelScope.launch { + val params = UpdateText.Params( + context = vmParams.objectId, + target = Relations.DESCRIPTION, + text = text, + marks = listOf() + ) + updateText(params).proceed( + failure = { + Timber.e(it, "Error while updating description") + }, + success = { + Timber.d("Description updated") + } + ) + } + } + + private fun updateDescriptionState( + objType: ObjectWrapper.Type, + objectPermissions: ObjectPermissions + ) { + // Access description and featured relations directly from objType + val descriptionText = objType.description.orEmpty() + val isDescriptionFeatured = objType.featuredRelations.contains(Relations.DESCRIPTION) + + // Update both UI states from the same source of truth (the store) + _isDescriptionFeatured.value = isDescriptionFeatured + + uiDescriptionState.value = UiDescriptionState( + description = descriptionText, + isVisible = isDescriptionFeatured, + isEditable = objectPermissions.canEditDetails + ) + } + private fun updateIcon( iconName: String, newColor: CustomIconColor? @@ -1206,6 +1261,68 @@ class ObjectTypeViewModel( } } + /** + * Checks if description is in featured relations using cached object type state. + * Updates the internal state accordingly. + */ + private fun checkDescriptionFeaturedStatus() { + val objType = _objTypeState.value + _isDescriptionFeatured.value = objType?.featuredRelations?.contains(Relations.DESCRIPTION) ?: false + } + + private fun proceedWithDescriptionToggle() { + viewModelScope.launch { + // Permission check + if (userPermissionProvider.get(space = vmParams.spaceId)?.isOwnerOrEditor() != true) { + Timber.w("User doesn't have permission to modify featured relations") + commands.emit(ObjectTypeCommand.ShowToast("Permission denied")) + uiMenuState.value = UiObjectTypeMenuState.Hidden + return@launch + } + + val isCurrentlyFeatured = _isDescriptionFeatured.value + + if (isCurrentlyFeatured) { + // Remove description from featured relations + val params = RemoveFromFeaturedRelations.Params( + ctx = vmParams.objectId, + relations = listOf(Relations.DESCRIPTION) + ) + removeFromFeaturedRelations.async(params = params).fold( + onSuccess = { payload -> + dispatcher.send(payload) + uiMenuState.value = UiObjectTypeMenuState.Hidden + Timber.d("Description removed from featured relations") + }, + onFailure = { error -> + Timber.e(error, "Error removing description from featured relations") + commands.emit(ObjectTypeCommand.ShowToast("Failed to hide description")) + uiMenuState.value = UiObjectTypeMenuState.Hidden + } + ) + } else { + // Add description to featured relations + addToFeaturedRelations.async( + params = AddToFeaturedRelations.Params( + ctx = vmParams.objectId, + relations = listOf(Relations.DESCRIPTION) + ) + ).fold( + onSuccess = { payload -> + dispatcher.send(payload) + uiMenuState.value = UiObjectTypeMenuState.Hidden + Timber.d("Description added to featured relations") + }, + onFailure = { error -> + Timber.e(error, "Error adding description to featured relations") + commands.emit(ObjectTypeCommand.ShowToast("Failed to show description")) + uiMenuState.value = UiObjectTypeMenuState.Hidden + } + ) + } + } + } + /** * Checks if the given object type is pinned as a widget in the space's home screen. * Updates [pinnedWidgetBlockId] with the widget block ID if found, or null otherwise. diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/VmFactory.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/VmFactory.kt index c756b6bd20..4523581129 100644 --- a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/VmFactory.kt +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/VmFactory.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.domain.block.interactor.UpdateText import com.anytypeio.anytype.domain.dataview.SetDataViewProperties import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer @@ -19,6 +20,8 @@ import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.domain.primitives.FieldParser import com.anytypeio.anytype.domain.primitives.GetObjectTypeConflictingFields import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields +import com.anytypeio.anytype.domain.relations.AddToFeaturedRelations +import com.anytypeio.anytype.domain.relations.RemoveFromFeaturedRelations import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.domain.templates.CreateTemplate import com.anytypeio.anytype.domain.widgets.CreateWidget @@ -55,7 +58,10 @@ class ObjectTypeVMFactory @Inject constructor( private val createWidget: CreateWidget, private val deleteWidget: DeleteWidget, private val spaceManager: SpaceManager, - private val getObject: GetObject + private val getObject: GetObject, + private val addToFeaturedRelations: AddToFeaturedRelations, + private val removeFromFeaturedRelations: RemoveFromFeaturedRelations, + private val updateText: UpdateText ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") @@ -85,6 +91,9 @@ class ObjectTypeVMFactory @Inject constructor( createWidget = createWidget, deleteWidget = deleteWidget, spaceManager = spaceManager, - getObject = getObject + getObject = getObject, + addToFeaturedRelations = addToFeaturedRelations, + removeFromFeaturedRelations = removeFromFeaturedRelations, + updateText = updateText ) as T } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModel.kt index c55a592667..65ff919227 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModel.kt @@ -291,33 +291,33 @@ class ObjectMenuViewModel( Relations.DESCRIPTION ) == true if (isDescriptionAlreadyInFeatured) { - removeFromFeaturedRelations( + removeFromFeaturedRelations.async( params = RemoveFromFeaturedRelations.Params( ctx = ctx, relations = listOf(Relations.DESCRIPTION) ) - ).proceed( - success = { payload -> + ).fold( + onSuccess = { payload -> dispatcher.send(payload) Timber.d("Description was removed from featured relations") }, - failure = { + onFailure = { Timber.e(it, "Error while removing description from featured relations") _toasts.emit(SOMETHING_WENT_WRONG_MSG) } ) } else { - addToFeaturedRelations( + addToFeaturedRelations.async( params = AddToFeaturedRelations.Params( ctx = ctx, relations = listOf(Relations.DESCRIPTION) ) - ).proceed( - success = { payload -> + ).fold( + onSuccess = { payload -> dispatcher.send(payload) Timber.d("Description was added to featured relations") }, - failure = { + onFailure = { Timber.e(it, "Error while adding description to featured relations") _toasts.emit(SOMETHING_WENT_WRONG_MSG) } 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 index 710210666e..554e2b0717 100644 --- 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 @@ -203,33 +203,33 @@ class ObjectSetMenuViewModel( Relations.DESCRIPTION ) == true if (isDescriptionAlreadyInFeatured) { - removeFromFeaturedRelations.run( + removeFromFeaturedRelations.async( params = RemoveFromFeaturedRelations.Params( ctx = ctx, relations = listOf(Relations.DESCRIPTION) ) - ).proceed( - success = { payload -> + ).fold( + onSuccess = { payload -> dispatcher.send(payload) Timber.d("Description was removed from featured relations") }, - failure = { + onFailure = { Timber.e(it, "Error while removing description from featured relations") _toasts.emit(SOMETHING_WENT_WRONG_MSG) } ) } else { - addToFeaturedRelations.run( + addToFeaturedRelations.async( params = AddToFeaturedRelations.Params( ctx = ctx, relations = listOf(Relations.DESCRIPTION) ) - ).proceed( - success = { payload -> + ).fold( + onSuccess = { payload -> dispatcher.send(payload) Timber.d("Description was added to featured relations") }, - failure = { + onFailure = { Timber.e(it, "Error while adding description to featured relations") _toasts.emit(SOMETHING_WENT_WRONG_MSG) } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationListViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationListViewModel.kt index 5219b971f1..2c84e20126 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationListViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationListViewModel.kt @@ -427,14 +427,14 @@ class RelationListViewModel( viewModelScope.launch { if (view.featured) { viewModelScope.launch { - removeFromFeaturedRelations( + removeFromFeaturedRelations.async( RemoveFromFeaturedRelations.Params( ctx = ctx, relations = listOf(relationKey) ) - ).process( - failure = { Timber.e(it, "Error while removing from featured relations") }, - success = { + ).fold( + onFailure = { Timber.e(it, "Error while removing from featured relations") }, + onSuccess = { dispatcher.send(it) analytics.sendAnalyticsRelationEvent( eventName = objectRelationUnfeature, @@ -447,14 +447,14 @@ class RelationListViewModel( } } else { viewModelScope.launch { - addToFeaturedRelations( + addToFeaturedRelations.async( AddToFeaturedRelations.Params( ctx = ctx, relations = listOf(relationKey) ) - ).process( - failure = { Timber.e(it, "Error while adding to featured relations") }, - success = { + ).fold( + onFailure = { Timber.e(it, "Error while adding to featured relations") }, + onSuccess = { dispatcher.send(it) analytics.sendAnalyticsRelationEvent( eventName = objectRelationFeature,