diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Block.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Block.kt index e47a55bfd4..9b5ce2d304 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Block.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Block.kt @@ -288,7 +288,8 @@ data class Block( val viewers: List, @Deprecated("To be deleted") val relations: List, - val relationsIndex: List = emptyList() + val relationsIndex: List = emptyList(), + val targetObjectId: Id = "", ) : Content() { data class Viewer( diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt index c6691157ba..f2cabdf64b 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt @@ -21,6 +21,9 @@ import com.anytypeio.anytype.core_ui.databinding.ItemBlockBulletedBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockCalloutBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockCheckboxBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockCodeSnippetBinding +import com.anytypeio.anytype.core_ui.databinding.ItemBlockDataViewDefaultBinding +import com.anytypeio.anytype.core_ui.databinding.ItemBlockDataViewEmptyDataBinding +import com.anytypeio.anytype.core_ui.databinding.ItemBlockDataViewEmptySourceBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockDescriptionBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockDividerDotsBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockDividerLineBinding @@ -67,6 +70,9 @@ import com.anytypeio.anytype.core_ui.databinding.ItemBlockVideoErrorBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockVideoUploadingBinding import com.anytypeio.anytype.core_ui.features.editor.BlockViewDiffUtil.Payload import com.anytypeio.anytype.core_ui.features.editor.decoration.DecoratableViewHolder +import com.anytypeio.anytype.core_ui.features.editor.holders.dataview.DataViewBlockDefaultHolder +import com.anytypeio.anytype.core_ui.features.editor.holders.dataview.DataViewBlockEmptyDataHolder +import com.anytypeio.anytype.core_ui.features.editor.holders.dataview.DataViewBlockEmptySourceHolder import com.anytypeio.anytype.core_ui.features.editor.holders.`interface`.TextHolder import com.anytypeio.anytype.core_ui.features.editor.holders.error.BookmarkError import com.anytypeio.anytype.core_ui.features.editor.holders.error.FileError @@ -136,6 +142,9 @@ import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_CALLOUT import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_CHECKBOX import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_CODE_SNIPPET +import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_DATA_VIEW_DEFAULT +import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_DATA_VIEW_EMPTY_DATA +import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_DATA_VIEW_EMPTY_SOURCE import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_DESCRIPTION import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_DIVIDER_DOTS import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_DIVIDER_LINE @@ -811,6 +820,21 @@ class BlockAdapter( clipboardInterceptor = clipboardInterceptor, onDragAndDropTrigger = onDragAndDropTrigger ) + HOLDER_DATA_VIEW_EMPTY_SOURCE -> { + DataViewBlockEmptySourceHolder( + ItemBlockDataViewEmptySourceBinding.inflate(inflater, parent, false) + ) + } + HOLDER_DATA_VIEW_EMPTY_DATA -> { + DataViewBlockEmptyDataHolder( + ItemBlockDataViewEmptyDataBinding.inflate(inflater, parent, false) + ) + } + HOLDER_DATA_VIEW_DEFAULT -> { + DataViewBlockDefaultHolder( + ItemBlockDataViewDefaultBinding.inflate(inflater, parent, false) + ) + } else -> throw IllegalStateException("Unexpected view type: $viewType") } @@ -1224,6 +1248,24 @@ class BlockAdapter( item = blocks[position] as BlockView.Table ) } + is DataViewBlockDefaultHolder -> { + holder.processChangePayloads( + payloads = payloads.typeOf(), + item = blocks[position] as BlockView.DataView.Default + ) + } + is DataViewBlockEmptySourceHolder -> { + holder.processChangePayloads( + payloads = payloads.typeOf(), + item = blocks[position] as BlockView.DataView.EmptySource + ) + } + is DataViewBlockEmptyDataHolder -> { + holder.processChangePayloads( + payloads = payloads.typeOf(), + item = blocks[position] as BlockView.DataView.EmptyData + ) + } else -> throw IllegalStateException("Unexpected view holder: $holder") } checkIfDecorationChanged(holder, payloads.typeOf(), position) @@ -1544,6 +1586,24 @@ class BlockAdapter( is TableBlockHolder -> { holder.bind(item = blocks[position] as BlockView.Table) } + is DataViewBlockEmptySourceHolder -> { + holder.bind( + item = blocks[position] as BlockView.DataView.EmptySource, + clicked = onClickListener + ) + } + is DataViewBlockEmptyDataHolder -> { + holder.bind( + item = blocks[position] as BlockView.DataView.EmptyData, + clicked = onClickListener + ) + } + is DataViewBlockDefaultHolder -> { + holder.bind( + item = blocks[position] as BlockView.DataView.Default, + clicked = onClickListener + ) + } } if (holder is Text<*>) { diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockViewDiffUtil.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockViewDiffUtil.kt index 38387d9480..331de6c27a 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockViewDiffUtil.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockViewDiffUtil.kt @@ -259,6 +259,18 @@ class BlockViewDiffUtil( } } + if (newBlock is BlockView.DataView && oldBlock is BlockView.DataView) { + if (newBlock.title != oldBlock.title) { + changes.add(DATA_VIEW_TITLE_CHANGED) + } + if (newBlock.icon != oldBlock.icon) { + changes.add(DATA_VIEW_ICON_CHANGED) + } + if (newBlock.background != oldBlock.background) { + changes.add(BACKGROUND_COLOR_CHANGED) + } + } + return if (changes.isNotEmpty()) Payload(changes).also { Timber.d("Returning payload: $it") } else @@ -301,6 +313,10 @@ class BlockViewDiffUtil( val isCalloutIconChanged: Boolean get() = changes.contains(CALLOUT_ICON_CHANGED) + val isDataViewTitleChanged : Boolean get() = changes.contains(DATA_VIEW_TITLE_CHANGED) + val isDataViewIconChanged : Boolean get() = changes.contains(DATA_VIEW_ICON_CHANGED) + val isDataViewBackgroundChanged : Boolean get() = changes.contains(DATA_VIEW_BACKGROUND_CHANGED) + fun markupChanged() = changes.contains(MARKUP_CHANGED) fun textChanged() = changes.contains(TEXT_CHANGED) fun textColorChanged() = changes.contains(TEXT_COLOR_CHANGED) @@ -351,5 +367,9 @@ class BlockViewDiffUtil( const val TABLE_CELLS_SELECTION_CHANGED = 340 const val TABLE_CELLS_CHANGED = 341 + + const val DATA_VIEW_TITLE_CHANGED = 350 + const val DATA_VIEW_ICON_CHANGED = 351 + const val DATA_VIEW_BACKGROUND_CHANGED = 352 } } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/dataview/DataViewBlockViewHolder.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/dataview/DataViewBlockViewHolder.kt new file mode 100644 index 0000000000..a0aae2b50e --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/dataview/DataViewBlockViewHolder.kt @@ -0,0 +1,273 @@ +package com.anytypeio.anytype.core_ui.features.editor.holders.dataview + +import android.text.Spannable +import android.text.SpannableString +import android.view.View +import android.widget.FrameLayout +import android.widget.TextView +import androidx.cardview.widget.CardView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.core_models.ThemeColor +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.SearchHighlightSpan +import com.anytypeio.anytype.core_ui.common.SearchTargetHighlightSpan +import com.anytypeio.anytype.core_ui.databinding.ItemBlockDataViewDefaultBinding +import com.anytypeio.anytype.core_ui.databinding.ItemBlockDataViewEmptyDataBinding +import com.anytypeio.anytype.core_ui.databinding.ItemBlockDataViewEmptySourceBinding +import com.anytypeio.anytype.core_ui.extensions.setBlockBackgroundColor +import com.anytypeio.anytype.core_ui.features.editor.BlockViewDiffUtil +import com.anytypeio.anytype.core_ui.features.editor.BlockViewHolder +import com.anytypeio.anytype.core_ui.features.editor.EditorTouchProcessor +import com.anytypeio.anytype.core_ui.features.editor.SupportCustomTouchProcessor +import com.anytypeio.anytype.core_ui.features.editor.SupportNesting +import com.anytypeio.anytype.core_ui.features.editor.decoration.DecoratableCardViewHolder +import com.anytypeio.anytype.core_ui.features.editor.decoration.EditorDecorationContainer +import com.anytypeio.anytype.core_ui.widgets.ObjectIconWidget +import com.anytypeio.anytype.core_utils.ext.gone +import com.anytypeio.anytype.core_utils.ext.removeSpans +import com.anytypeio.anytype.core_utils.ext.visible +import com.anytypeio.anytype.presentation.editor.editor.listener.ListenerType +import com.anytypeio.anytype.presentation.editor.editor.model.BlockView +import com.anytypeio.anytype.presentation.objects.ObjectIcon + +class DataViewBlockEmptySourceHolder(binding: ItemBlockDataViewEmptySourceBinding) : + DataViewBlockViewHolder(binding.root) { + + override val decoratableContainer: EditorDecorationContainer = binding.decorationContainer + override val rootView: View = binding.root + override val containerView: ConstraintLayout = binding.containerWithBackground + override val objectIconView: ObjectIconWidget = binding.cardIcon + override val titleView: TextView = binding.cardName + override val descriptionView: TextView = binding.cardDescription + override val selectedView: View = binding.selected + override val decoratableCard: CardView = binding.card + + init { + itemView.setOnTouchListener { v, e -> editorTouchProcessor.process(v, e) } + } + + fun bind( + item: BlockView.DataView.EmptySource, + clicked: (ListenerType) -> Unit + ) { + super.bind(item = item, clicked = clicked) + } + + fun processChangePayloads( + payloads: List, + item: BlockView.DataView + ) { + payloads.forEach { payload -> + processChangeBasePayloads(payload, item) + } + } +} + +class DataViewBlockEmptyDataHolder(binding: ItemBlockDataViewEmptyDataBinding) : + DataViewBlockViewHolder(binding.root) { + + override val decoratableContainer: EditorDecorationContainer = binding.decorationContainer + override val rootView: View = binding.root + override val containerView: ConstraintLayout = binding.containerWithBackground + override val objectIconView: ObjectIconWidget = binding.cardIcon + override val titleView: TextView = binding.cardName + override val descriptionView: TextView = binding.cardDescription + override val selectedView: View = binding.selected + override val decoratableCard: CardView = binding.card + + init { + itemView.setOnTouchListener { v, e -> editorTouchProcessor.process(v, e) } + } + + fun bind( + item: BlockView.DataView.EmptyData, + clicked: (ListenerType) -> Unit + ) { + super.bind(item = item, clicked = clicked) + } + + fun processChangePayloads( + payloads: List, + item: BlockView.DataView + ) { + payloads.forEach { payload -> + processChangeBasePayloads(payload, item) + } + } +} + +data class DataViewBlockDefaultHolder( + val binding: ItemBlockDataViewDefaultBinding +) : DataViewBlockViewHolder(binding.root) { + + override val decoratableContainer: EditorDecorationContainer = binding.decorationContainer + override val rootView: View = binding.root + override val containerView: ConstraintLayout = binding.containerWithBackground + override val objectIconView: ObjectIconWidget = binding.cardIcon + override val titleView: TextView = binding.cardName + override val descriptionView: TextView = binding.cardDescription + override val selectedView: View = binding.selected + override val decoratableCard: CardView = binding.card + + init { + itemView.setOnTouchListener { v, e -> editorTouchProcessor.process(v, e) } + } + + fun bind( + item: BlockView.DataView.Default, + clicked: (ListenerType) -> Unit + ) { + super.bind(item = item, clicked = clicked) + } + + fun processChangePayloads( + payloads: List, + item: BlockView.DataView + ) { + payloads.forEach { payload -> + processChangeBasePayloads(payload, item) + } + } +} + +sealed class DataViewBlockViewHolder( + view: View +) : BlockViewHolder(view), + BlockViewHolder.DragAndDropHolder, + DecoratableCardViewHolder, + SupportCustomTouchProcessor, + SupportNesting { + + protected abstract val rootView: View + abstract val containerView: ConstraintLayout + + private val untitled = itemView.resources.getString(R.string.untitled_set) + abstract val objectIconView: ObjectIconWidget + abstract val titleView: TextView + abstract val descriptionView: TextView + abstract val selectedView: View + abstract override val decoratableCard: CardView + + override val editorTouchProcessor = EditorTouchProcessor( + fallback = { e -> itemView.onTouchEvent(e) } + ) + + protected fun bind( + item: BlockView.DataView, + clicked: (ListenerType) -> Unit + ) { + selected(item) + + applyName(item) + + applyBackground(item.background) + + applySearchHighlight(item) + + applyImageOrEmoji(item) + + itemView.setOnClickListener { clicked(ListenerType.DataViewClick(item.id)) } + } + + private fun selected(item: BlockView.DataView) { + selectedView.isSelected = item.isSelected + } + + private fun applyName(item: BlockView.DataView) { + val name = item.title + val sb = if (name.isNullOrBlank()) "" else SpannableString(name) + titleView.append(sb) + } + + private fun applyImageOrEmoji(item: BlockView.DataView) { + when (item.icon) { + ObjectIcon.None -> { + objectIconView.gone() + } + else -> { + objectIconView.visible() + objectIconView.setIcon(item.icon) + } + } + } + + private fun applySearchHighlight(item: BlockView.Searchable) { + item.searchFields.find { it.key == BlockView.Searchable.Field.DEFAULT_SEARCH_FIELD_KEY } + ?.let { field -> + applySearchHighlight(field, titleView) + } ?: clearSearchHighlights() + } + + private fun applySearchHighlight(field: BlockView.Searchable.Field, input: TextView) { + val content = input.text as Spannable + content.removeSpans() + content.removeSpans() + field.highlights.forEach { highlight -> + content.setSpan( + SearchHighlightSpan(), + highlight.first, + highlight.last, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + if (field.isTargeted) { + content.setSpan( + SearchTargetHighlightSpan(), + field.target.first, + field.target.last, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + + private fun clearSearchHighlights() { + titleView.editableText?.removeSpans() + titleView.editableText?.removeSpans() + } + + protected fun processChangeBasePayloads( + payload: BlockViewDiffUtil.Payload, + item: BlockView.DataView + ) { + if (payload.isSelectionChanged) { + selected(item) + } + if (payload.isDataViewTitleChanged) { + applyName(item) + } + if (payload.isDataViewIconChanged) { + applyImageOrEmoji(item) + } + if (payload.isDataViewBackgroundChanged) { + applyBackground(item.background) + } + if (payload.isSearchHighlightChanged) { + applySearchHighlight(item) + } + } + + private fun applyBackground(background: ThemeColor) { + containerView.setBlockBackgroundColor(background) + } + + override fun applyDecorations(decorations: List) { + super.applyDecorations(decorations) + decoratableContainer.decorate(decorations) { rect -> + rootView.updateLayoutParams { + topMargin = if (rect.left == 0) { + itemView.resources.getDimension(R.dimen.dp_10).toInt() + } else { + 0 + } + } + selectedView.updateLayoutParams { + val defaultIndentOffset = + itemView.resources.getDimension(R.dimen.default_indent).toInt() + leftMargin = defaultIndentOffset + rect.left + rightMargin = defaultIndentOffset + rect.right + } + } + } +} diff --git a/core-ui/src/main/res/drawable/bg_rect_3_radius.xml b/core-ui/src/main/res/drawable/bg_rect_3_radius.xml new file mode 100644 index 0000000000..b06e0d75ab --- /dev/null +++ b/core-ui/src/main/res/drawable/bg_rect_3_radius.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/layout/item_block_data_view_default.xml b/core-ui/src/main/res/layout/item_block_data_view_default.xml new file mode 100644 index 0000000000..f55b0a5e50 --- /dev/null +++ b/core-ui/src/main/res/layout/item_block_data_view_default.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/layout/item_block_data_view_empty_data.xml b/core-ui/src/main/res/layout/item_block_data_view_empty_data.xml new file mode 100644 index 0000000000..c393e3ab4d --- /dev/null +++ b/core-ui/src/main/res/layout/item_block_data_view_empty_data.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/layout/item_block_data_view_empty_source.xml b/core-ui/src/main/res/layout/item_block_data_view_empty_source.xml new file mode 100644 index 0000000000..7bed398752 --- /dev/null +++ b/core-ui/src/main/res/layout/item_block_data_view_empty_source.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/values/dimens.xml b/core-ui/src/main/res/values/dimens.xml index aa91cd8b8e..1ebce287a5 100644 --- a/core-ui/src/main/res/values/dimens.xml +++ b/core-ui/src/main/res/values/dimens.xml @@ -306,4 +306,6 @@ 13sp + 96dp + \ No newline at end of file diff --git a/core-ui/src/main/res/values/strings.xml b/core-ui/src/main/res/values/strings.xml index b12684bfff..40a20a5808 100644 --- a/core-ui/src/main/res/values/strings.xml +++ b/core-ui/src/main/res/values/strings.xml @@ -174,6 +174,7 @@ Untitled + Untitled set Archived @@ -487,6 +488,8 @@ 📄 Proto type to start with Non-existent object + No data + No source Deleted object Open set Deleted @@ -519,6 +522,8 @@ Visible Card Inline + Set View + Inline set Fit image Image preview Cover @@ -563,6 +568,7 @@ Row Column Open object + Open source Reload object content Open link Copy email @@ -589,6 +595,9 @@ My types My relations Type \"%1$s\" added to your library + The name of the source set of the inline set + The inline set has no source + The source of the inline set has no type Recently opened diff --git a/core-ui/src/main/res/values/styles.xml b/core-ui/src/main/res/values/styles.xml index f4515aca83..e6c43cab26 100644 --- a/core-ui/src/main/res/values/styles.xml +++ b/core-ui/src/main/res/values/styles.xml @@ -1096,4 +1096,82 @@ 1 end + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToCoreModelMappers.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToCoreModelMappers.kt index cd2e5e7d5b..977eb1af43 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToCoreModelMappers.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToCoreModelMappers.kt @@ -354,7 +354,8 @@ fun MBlock.toCoreModelsDataView(): Block.Content.DataView { sources = content.source, viewers = content.views.map { it.toCoreModels() }, relations = content.relations.map { it.toCoreModels() }, - relationsIndex = content.relationLinks.map { it.toCoreModels() } + relationsIndex = content.relationLinks.map { it.toCoreModels() }, + targetObjectId = content.TargetObjectId ) } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt index 8b1712d44a..68eb561b21 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt @@ -3022,12 +3022,27 @@ class EditorViewModel( sendToast("Couldn't find the target of the link") } } + is Content.DataView -> { + proceedWithOpeningInlineSetTarget(target = content.targetObjectId) + } else -> { sendToast("Couldn't find the target of the link") } } } + private fun proceedWithOpeningInlineSetTarget(target: Id) { + if (target.isNotEmpty()) { + proceedWithOpeningSet(target) + viewModelScope.sendAnalyticsOpenAsObject( + analytics = analytics, + type = EventsDictionary.Type.dataView + ) + } else { + sendToast("This inline set doesn’t have a source.") + } + } + private fun proceedWithOpeningObjectByLayout(target: String) { proceedWithClearingFocus() val details = orchestrator.stores.details.current() @@ -3907,6 +3922,14 @@ class EditorViewModel( else -> Unit } } + is ListenerType.DataViewClick -> { + when (mode) { + EditorMode.Edit -> onPageClicked(blockLinkId = clicked.target) + EditorMode.Locked -> onPageClicked(blockLinkId = clicked.target) + EditorMode.Select -> onBlockMultiSelectClicked(clicked.target) + else -> Unit + } + } else -> {} } } @@ -5277,15 +5300,7 @@ class EditorViewModel( onSendBlockActionAnalyticsEvent(EventsDictionary.BlockAction.paste) } ActionItemType.OpenObject -> { - val selected = blocks.firstOrNull { currentSelection().contains(it.id) } - proceedWithExitingMultiSelectMode() - if (selected != null) { - proceedWithMultiSelectOpenObjectAction( - selected = selected - ) - } else { - sendToast("No blocks were selected. Please, try again.") - } + proceedWithOpeningTargetForCurrentSelection() onSendBlockActionAnalyticsEvent(EventsDictionary.BlockAction.openObject) } else -> { @@ -5294,19 +5309,27 @@ class EditorViewModel( } } - private fun proceedWithMultiSelectOpenObjectAction(selected: Block) { - when (val content = selected.content) { - is Content.Bookmark -> { - val target = content.targetObjectId - if (target != null) { - proceedWithOpeningObject(target) - viewModelScope.sendAnalyticsOpenAsObject( - analytics = analytics, - type = EventsDictionary.Type.bookmark - ) + private fun proceedWithOpeningTargetForCurrentSelection() { + val selected = blocks.firstOrNull { currentSelection().contains(it.id) } + proceedWithExitingMultiSelectMode() + if (selected != null) { + when (val content = selected.content) { + is Content.Bookmark -> { + val target = content.targetObjectId + if (target != null) { + proceedWithOpeningObject(target) + viewModelScope.sendAnalyticsOpenAsObject( + analytics = analytics, + type = EventsDictionary.Type.bookmark + ) + } else { + sendToast("This bookmark doesn’t have a source.") + } } + else -> sendToast("Unexpected object") } - else -> sendToast("Unexpected object") + } else { + sendToast("No blocks were selected. Please, try again.") } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/ext/BlockViewExt.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/ext/BlockViewExt.kt index 224ae4477f..677e3da700 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/ext/BlockViewExt.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/ext/BlockViewExt.kt @@ -359,6 +359,15 @@ fun List.enterSAM( isSelected = isSelected, cells = view.cells.updateCellsMode(mode = BlockView.Mode.READ), ) + is BlockView.DataView.Default -> view.copy( + isSelected = isSelected + ) + is BlockView.DataView.EmptyData -> view.copy( + isSelected = isSelected + ) + is BlockView.DataView.EmptySource -> view.copy( + isSelected = isSelected + ) else -> view.also { check(view !is BlockView.Permission) } } } @@ -587,6 +596,9 @@ fun List.clearSearchHighlights(): List = map { view -> is BlockView.LinkToObject.Default.Card.MediumIconCover -> view.copy(searchFields = emptyList()) is BlockView.LinkToObject.Archived -> view.copy(searchFields = emptyList()) is BlockView.Table -> view.copy(cells = view.cells.map { cell -> cell.clearHighlight() }) + is BlockView.DataView.Default -> view.copy(searchFields = emptyList()) + is BlockView.DataView.EmptyData -> view.copy(searchFields = emptyList()) + is BlockView.DataView.EmptySource -> view.copy(searchFields = emptyList()) else -> view.also { check(view !is BlockView.Searchable) } } } @@ -687,6 +699,18 @@ fun List.highlight( val updatedCells = view.cells.map { it.addHighlight(highlighter) } view.copy(cells = updatedCells) } + is BlockView.DataView.EmptySource -> { + val fields = listOf(DEFAULT_SEARCH_FIELD_KEY to view.title.orEmpty()) + view.copy(searchFields = highlighter(fields)) + } + is BlockView.DataView.EmptyData -> { + val fields = listOf(DEFAULT_SEARCH_FIELD_KEY to view.title.orEmpty()) + view.copy(searchFields = highlighter(fields)) + } + is BlockView.DataView.Default -> { + val fields = listOf(DEFAULT_SEARCH_FIELD_KEY to view.title.orEmpty()) + view.copy(searchFields = highlighter(fields)) + } else -> { view.also { v -> if (v is BlockView.Searchable) { @@ -742,6 +766,9 @@ fun BlockView.setHighlight( is BlockView.LinkToObject.Default.Card.SmallIconCover -> copy(searchFields = highlights) is BlockView.LinkToObject.Default.Card.MediumIconCover -> copy(searchFields = highlights) is BlockView.LinkToObject.Archived -> copy(searchFields = highlights) + is BlockView.DataView.EmptySource -> copy(searchFields = highlights) + is BlockView.DataView.EmptyData -> copy(searchFields = highlights) + is BlockView.DataView.Default -> copy(searchFields = highlights) else -> this.also { check(this !is BlockView.Searchable) } } @@ -1034,6 +1061,9 @@ fun BlockView.updateSelection(newSelection: Boolean) = when (this) { is BlockView.Latex -> copy(isSelected = newSelection) is BlockView.TableOfContents -> copy(isSelected = newSelection) is BlockView.Table -> copy(isSelected = newSelection) + is BlockView.DataView.EmptyData -> copy(isSelected = newSelection) + is BlockView.DataView.EmptySource -> copy(isSelected = newSelection) + is BlockView.DataView.Default -> copy(isSelected = newSelection) else -> this.also { if (this is BlockView.Selectable) Timber.e("Error when change selection for Selectable BlockView $this") diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/listener/ListenerType.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/listener/ListenerType.kt index de43969bb6..f92dc24c54 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/listener/ListenerType.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/listener/ListenerType.kt @@ -78,4 +78,6 @@ sealed interface ListenerType { data class TableOfContents(val target: Id) : ListenerType data class TableEmptyCell(val cell: BlockView.Table.Cell) : ListenerType data class TableTextCell(val cell: BlockView.Table.Cell) : ListenerType + + data class DataViewClick(val target: Id) : ListenerType } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt index e6102fd602..cc76c42004 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt @@ -16,6 +16,9 @@ import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_CALLOUT import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_CHECKBOX import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_CODE_SNIPPET +import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_DATA_VIEW_DEFAULT +import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_DATA_VIEW_EMPTY_DATA +import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_DATA_VIEW_EMPTY_SOURCE import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_DESCRIPTION import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_DIVIDER_DOTS import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_DIVIDER_LINE @@ -1379,4 +1382,56 @@ sealed class BlockView : ViewType { enum class Tab { CELL, COLUMN, ROW } } + + sealed class DataView : BlockView(), Selectable, Decoratable, Searchable { + + abstract val title: String? + abstract val icon: ObjectIcon + abstract val background: ThemeColor + + /** + * UI-model for a data view block. There is no master set. + */ + data class EmptySource( + override val id: String, + override val isSelected: Boolean, + override val decorations: List, + override val searchFields: List = emptyList(), + override val title: String?, + override val icon: ObjectIcon, + override val background: ThemeColor + ) : DataView() { + override fun getViewType(): Int = HOLDER_DATA_VIEW_EMPTY_SOURCE + } + + /** + * UI-model for a data view block. There is a master set but no source for the set + */ + data class EmptyData( + override val id: String, + override val isSelected: Boolean, + override val decorations: List, + override val searchFields: List = emptyList(), + override val title: String?, + override val icon: ObjectIcon, + override val background: ThemeColor + ) : DataView() { + override fun getViewType(): Int = HOLDER_DATA_VIEW_EMPTY_DATA + } + + /** + * UI-model for a data view block. There is a master set and source for the set + */ + data class Default( + override val id: String, + override val isSelected: Boolean, + override val decorations: List, + override val searchFields: List = emptyList(), + override val title: String?, + override val icon: ObjectIcon, + override val background: ThemeColor + ) : DataView() { + override fun getViewType(): Int = HOLDER_DATA_VIEW_DEFAULT + } + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/types/Types.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/types/Types.kt index bcbcccd6ae..1fa010b458 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/types/Types.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/types/Types.kt @@ -70,4 +70,8 @@ object Types { const val HOLDER_TOC = 54 const val HOLDER_CALLOUT = 55 const val HOLDER_TABLE = 56 + + const val HOLDER_DATA_VIEW_DEFAULT = 300 + const val HOLDER_DATA_VIEW_EMPTY_SOURCE = 301 + const val HOLDER_DATA_VIEW_EMPTY_DATA = 302 } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt index 659d85ec93..474d38c338 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt @@ -777,6 +777,20 @@ class DefaultBlockViewRenderer @Inject constructor( ) ) } + is Content.DataView -> { + isPreviousBlockMedia = false + mCounter = 0 + result.add( + dataView( + mode = mode, + block = block, + content = content, + details = details, + selection = selection, + schema = parentScheme + ) + ) + } else -> {} } } @@ -2145,4 +2159,67 @@ class DefaultBlockViewRenderer @Inject constructor( is Cursor.Range -> cursor.range.first } } + + private fun dataView( + mode: EditorMode, + block: Block, + content: Content.DataView, + details: Block.Details, + selection: Set, + schema: NestedDecorationData + ): BlockView.DataView { + val targetObjectId = content.targetObjectId + val isSelected = checkIfSelected( + mode = mode, + block = block, + selection = selection + ) + val background = block.parseThemeBackgroundColor() + val decorations = buildNestedDecorationData( + block = block, + parentScheme = schema, + currentDecoration = DecorationData( + style = DecorationData.Style.Card, + background = background + ) + ).toBlockViewDecoration(block) + if (targetObjectId.isBlank()) { + return BlockView.DataView.EmptySource( + id = block.id, + decorations = decorations, + isSelected = isSelected, + background = background, + icon = ObjectIcon.None, + title = null + ) + } else { + val targetSet = ObjectWrapper.Basic( + map = details.details[content.targetObjectId]?.map ?: emptyMap() + ) + val icon = ObjectIcon.getEditorLinkToObjectIcon( + obj = targetSet, + layout = targetSet.layout, + builder = urlBuilder + ) + if (targetSet.setOf.isEmpty()) { + return BlockView.DataView.EmptyData( + id = block.id, + decorations = decorations, + isSelected = isSelected, + title = targetSet.name, + background = background, + icon = icon + ) + } else { + return BlockView.DataView.Default( + id = block.id, + decorations = decorations, + isSelected = isSelected, + title = targetSet.name, + background = background, + icon = icon + ) + } + } + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/selection/TableCellExt.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/selection/TableCellExt.kt index 69a581d2d6..5f4448569e 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/selection/TableCellExt.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/selection/TableCellExt.kt @@ -348,6 +348,9 @@ fun List.toggleTableMode( is BlockView.Unsupported -> view.copy(isSelected = false) is BlockView.Upload.Bookmark -> view.copy(isSelected = false) is BlockView.Relation.Deleted -> view.copy(isSelected = false) + is BlockView.DataView.Default -> view.copy(isSelected = false) + is BlockView.DataView.EmptyData -> view.copy(isSelected = false) + is BlockView.DataView.EmptySource -> view.copy(isSelected = false) } } } diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/ObjectSetReducerTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/ObjectSetReducerTest.kt index d197b8828a..20e1d1c63a 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/ObjectSetReducerTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/sets/ObjectSetReducerTest.kt @@ -473,7 +473,8 @@ class ObjectSetReducerTest { filters = listOf() ) ), - relations = listOf() + relations = listOf(), + targetObjectId = (dataView.content as Block.Content.DataView).targetObjectId ), fields = Block.Fields.empty(), children = listOf() @@ -573,7 +574,8 @@ class ObjectSetReducerTest { filters = expectedFilters ) ), - relations = listOf() + relations = listOf(), + targetObjectId = (dataView.content as Block.Content.DataView).targetObjectId ), fields = Block.Fields.empty(), children = listOf() @@ -673,7 +675,8 @@ class ObjectSetReducerTest { filters = viewer1.filters ) ), - relations = listOf() + relations = listOf(), + targetObjectId = (dataView.content as Block.Content.DataView).targetObjectId ), fields = Block.Fields.empty(), children = listOf() diff --git a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/DataView.kt b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/DataView.kt index 695536451a..b4b033a90e 100644 --- a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/DataView.kt +++ b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/DataView.kt @@ -6,14 +6,16 @@ fun StubDataView( id : Id = MockDataFactory.randomUuid(), views: List = emptyList(), relations: List = emptyList(), - sources: List = emptyList() + sources: List = emptyList(), + targetObjectId: Id = MockDataFactory.randomUuid() ) : Block = Block( id = id, content = DV( sources = sources, relations = emptyList(), relationsIndex= relations, - viewers = views + viewers = views, + targetObjectId = targetObjectId ), children = emptyList(), fields = Block.Fields.empty()