From f11b8b9be8d3b9c5780587b48251525d560bdb03 Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Mon, 26 Jun 2023 20:10:30 +0200 Subject: [PATCH] DROID-1351 Widgets | Enhancement | Integrate middleware commands for granular widget updates (#99) --- .../anytypeio/anytype/core_models/Block.kt | 3 +- .../anytypeio/anytype/core_models/Event.kt | 18 +- .../interactor/MiddlewareEventMapper.kt | 19 ++ .../presentation/home/HomeScreenViewModel.kt | 43 +++++ .../home/HomeScreenViewModelTest.kt | 181 ++++++++++++++++++ 5 files changed, 259 insertions(+), 5 deletions(-) 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 54c9fd567f..bb662e838d 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 @@ -368,7 +368,8 @@ data class Block( data class Widget( val layout: Layout, - val limit: Int = 0 + val limit: Int = 0, + val activeView: Id? = null ) : Content() { enum class Layout { TREE, LINK, LIST, COMPACT_LIST diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Event.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Event.kt index 743796e3b1..a3cd558b94 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Event.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Event.kt @@ -83,10 +83,10 @@ sealed class Event { override val context: String, val id: Id, val target: Id, - val iconSize: Block.Content.Link.IconSize?, - val cardStyle: Block.Content.Link.CardStyle?, - val description: Block.Content.Link.Description?, - val relations: Set?, + val iconSize: Block.Content.Link.IconSize? = null, + val cardStyle: Block.Content.Link.CardStyle? = null, + val description: Block.Content.Link.Description? = null, + val relations: Set? = null, ) : Command() /** @@ -221,6 +221,16 @@ sealed class Event { ) : ObjectRelation() } + sealed class Widgets: Command() { + data class SetWidget( + override val context: Id, + val widget: Id, + val layout: WidgetLayout? = null, + val activeView: Id? = null, + val limit: Int? = null + ) : Widgets() + } + sealed class DataView : Command() { /** diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareEventMapper.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareEventMapper.kt index 00b6ce25a1..d4b3a5e9b7 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareEventMapper.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareEventMapper.kt @@ -1,6 +1,8 @@ package com.anytypeio.anytype.middleware.interactor +import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.Event +import com.anytypeio.anytype.middleware.mappers.MWidgetLayout import com.anytypeio.anytype.middleware.mappers.toCoreModel import com.anytypeio.anytype.middleware.mappers.toCoreModels import com.anytypeio.anytype.middleware.mappers.toCoreModelsAlign @@ -167,6 +169,23 @@ fun anytype.Event.Message.toCoreModels( style = style.value_.toCoreModels() ) } + blockSetWidget != null -> { + val event = blockSetWidget + checkNotNull(event) + Event.Command.Widgets.SetWidget( + context = context, + widget = event.id, + activeView = event.viewId?.value_, + limit = event.limit?.value_, + layout = when(event.layout?.value_) { + MWidgetLayout.Link -> Block.Content.Widget.Layout.LINK + MWidgetLayout.Tree -> Block.Content.Widget.Layout.TREE + MWidgetLayout.List -> Block.Content.Widget.Layout.LIST + MWidgetLayout.CompactList -> Block.Content.Widget.Layout.COMPACT_LIST + else -> null + } + ) + } blockDataviewViewSet != null -> { val event = blockDataviewViewSet checkNotNull(event) diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt index f3b62eaf78..3241d4def1 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.analytics.base.EventsDictionary +import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.Config import com.anytypeio.anytype.core_models.Event import com.anytypeio.anytype.core_models.Id @@ -757,6 +758,48 @@ class HomeScreenViewModel( is Event.Command.Details -> { curr = curr.copy(details = curr.details.process(e)) } + is Event.Command.LinkGranularChange -> { + curr = curr.copy( + blocks = curr.blocks.map { block -> + if (block.id == e.id) { + val content = block.content + if (content is Block.Content.Link) { + block.copy( + content = content.copy( + target = e.target + ) + ) + } else { + block + } + } else { + block + } + } + ) + } + is Event.Command.Widgets.SetWidget -> { + curr = curr.copy( + blocks = curr.blocks.map { block -> + if (block.id == e.widget) { + val content = block.content + if (content is Block.Content.Widget) { + block.copy( + content = content.copy( + layout = e.layout ?: content.layout, + limit = e.limit ?: content.limit, + activeView = e.activeView ?: content.activeView + ) + ) + } else { + block + } + } else { + block + } + } + ) + } else -> { Timber.d("Skipping event: $e") } diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt index 74531c6dc9..7688ad6189 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt @@ -7,6 +7,7 @@ import com.anytypeio.anytype.core_models.Event import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Key import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.ObjectTypeIds import com.anytypeio.anytype.core_models.ObjectView import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Payload @@ -2041,6 +2042,186 @@ class HomeScreenViewModelTest { } } + @Test + fun `should react to change-widget-source event when source type is page for old and new source`() = runTest { + + val currentSourceObject = StubObject( + id = "SOURCE OBJECT 1", + links = emptyList(), + objectType = ObjectTypeIds.PAGE + ) + + val newSourceObject = StubObject( + id = "SOURCE OBJECT 2", + links = emptyList(), + objectType = ObjectTypeIds.PAGE + ) + + val sourceLink = StubLinkToObjectBlock( + id = "SOURCE LINK", + target = currentSourceObject.id + ) + + val widgetBlock = StubWidgetBlock( + id = "WIDGET BLOCK", + layout = Block.Content.Widget.Layout.TREE, + children = listOf(sourceLink.id) + ) + + val smartBlock = StubSmartBlock( + id = WIDGET_OBJECT_ID, + children = listOf(widgetBlock.id), + ) + + val givenObjectView = StubObjectView( + root = WIDGET_OBJECT_ID, + blocks = listOf( + smartBlock, + widgetBlock, + sourceLink + ), + details = mapOf( + currentSourceObject.id to currentSourceObject.map + ) + ) + + stubConfig() + stubInterceptEvents( + events = flow { + delay(300) + emit( + listOf( + Event.Command.LinkGranularChange( + context = WIDGET_OBJECT_ID, + id = sourceLink.id, + target = newSourceObject.id + ) + ) + ) + } + ) + stubOpenObject(givenObjectView) + stubSearchByIds( + subscription = widgetBlock.id, + targets = emptyList() + ) + stubCollapsedWidgetState(any()) + stubGetWidgetSession() + + val vm = buildViewModel() + + // TESTING + + vm.onStart() + + vm.views.test { + val firstTimeState = awaitItem() + assertEquals( + actual = firstTimeState, + expected = emptyList() + ) + delay(1) + val secondTimeItem = awaitItem() + assertTrue { + val firstWidget = secondTimeItem.first() + firstWidget is WidgetView.Tree && firstWidget.source.id == currentSourceObject.id + } + val thirdTimeItem = awaitItem() + advanceUntilIdle() + assertTrue { + val firstWidget = thirdTimeItem.first() + firstWidget is WidgetView.Tree && firstWidget.source.id == newSourceObject.id + } + } + } + + @Test + fun `should react to change-widget-layout event when tree changed to link`() = runTest { + + val currentSourceObject = StubObject( + id = "SOURCE OBJECT 1", + links = emptyList(), + objectType = ObjectTypeIds.PAGE + ) + + val sourceLink = StubLinkToObjectBlock( + id = "SOURCE LINK", + target = currentSourceObject.id + ) + + val widgetBlock = StubWidgetBlock( + id = "WIDGET BLOCK", + layout = Block.Content.Widget.Layout.TREE, + children = listOf(sourceLink.id) + ) + + val smartBlock = StubSmartBlock( + id = WIDGET_OBJECT_ID, + children = listOf(widgetBlock.id), + ) + + val givenObjectView = StubObjectView( + root = WIDGET_OBJECT_ID, + blocks = listOf( + smartBlock, + widgetBlock, + sourceLink + ), + details = mapOf( + currentSourceObject.id to currentSourceObject.map + ) + ) + + stubConfig() + stubInterceptEvents( + events = flow { + delay(300) + emit( + listOf( + Event.Command.Widgets.SetWidget( + context = WIDGET_OBJECT_ID, + widget = widgetBlock.id, + layout = Block.Content.Widget.Layout.LINK, + ) + ) + ) + } + ) + stubOpenObject(givenObjectView) + stubSearchByIds( + subscription = widgetBlock.id, + targets = emptyList() + ) + stubCollapsedWidgetState(any()) + stubGetWidgetSession() + + val vm = buildViewModel() + + // TESTING + + vm.onStart() + + vm.views.test { + val firstTimeState = awaitItem() + assertEquals( + actual = firstTimeState, + expected = emptyList() + ) + delay(1) + val secondTimeItem = awaitItem() + assertTrue { + val firstWidget = secondTimeItem.first() + firstWidget is WidgetView.Tree + } + val thirdTimeItem = awaitItem() + advanceUntilIdle() + assertTrue { + val firstWidget = thirdTimeItem.first() + firstWidget is WidgetView.Link + } + } + } + private fun stubInterceptEvents(events: Flow>) { interceptEvents.stub { on { build(InterceptEvents.Params(WIDGET_OBJECT_ID)) } doReturn events