From ee42888ea92c56bc65147690841c957c9e6cdffe Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Wed, 28 Jun 2023 15:12:39 +0200 Subject: [PATCH] DROID-1368 Widgets | Enhancement | Syncing active view (#106) --- .../anytype/app/DefaultFeatureToggles.kt | 3 +- .../core_utils/tools/FeatureToggles.kt | 4 +- .../auth/repo/block/BlockDataRepository.kt | 10 + .../data/auth/repo/block/BlockRemote.kt | 6 + .../anytype/domain/base/Interactor.kt | 2 +- .../domain/block/repo/BlockRepository.kt | 6 + .../domain/widgets/SetWidgetActiveView.kt | 29 ++ .../middleware/block/BlockMiddleware.kt | 10 + .../middleware/interactor/Middleware.kt | 16 + .../interactor/MiddlewareEventChannel.kt | 3 +- .../interactor/MiddlewareProtobufLogger.kt | 10 +- .../middleware/mappers/ToCoreModelMappers.kt | 3 +- .../middleware/service/MiddlewareService.kt | 3 + .../MiddlewareServiceImplementation.kt | 15 +- .../presentation/home/HomeScreenViewModel.kt | 39 ++- .../anytype/presentation/widgets/Widget.kt | 14 + .../widgets/WidgetActiveViewStateHolder.kt | 6 +- .../home/HomeScreenViewModelTest.kt | 300 ++++++++++++++++-- 18 files changed, 441 insertions(+), 38 deletions(-) create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/widgets/SetWidgetActiveView.kt diff --git a/app/src/main/java/com/anytypeio/anytype/app/DefaultFeatureToggles.kt b/app/src/main/java/com/anytypeio/anytype/app/DefaultFeatureToggles.kt index fe4e0dd510..b2d52b743d 100644 --- a/app/src/main/java/com/anytypeio/anytype/app/DefaultFeatureToggles.kt +++ b/app/src/main/java/com/anytypeio/anytype/app/DefaultFeatureToggles.kt @@ -14,7 +14,7 @@ class DefaultFeatureToggles @Inject constructor( private val buildProvider: BuildProvider ) : FeatureToggles { - override val isLogFromMiddlewareLibrary = + override val isLogFromGoProcess = BuildConfig.LOG_FROM_MW_LIBRARY && buildProvider.isDebug() override val isLogMiddlewareInteraction = @@ -32,4 +32,5 @@ class DefaultFeatureToggles @Inject constructor( override val isAutoUpdateEnabled: Boolean = false + override val isConciseLogging: Boolean = true } \ No newline at end of file diff --git a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/tools/FeatureToggles.kt b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/tools/FeatureToggles.kt index 1f6fa1a14e..3e7b64ebfa 100644 --- a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/tools/FeatureToggles.kt +++ b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/tools/FeatureToggles.kt @@ -4,10 +4,12 @@ interface FeatureToggles { val isAutoUpdateEnabled: Boolean - val isLogFromMiddlewareLibrary: Boolean + val isLogFromGoProcess: Boolean val isLogMiddlewareInteraction: Boolean + val isConciseLogging: Boolean + val excludeThreadStatusLogging: Boolean val isLogEditorViewModelEvents: Boolean diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt index 1e739769a9..008438b1ef 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt @@ -764,6 +764,16 @@ class BlockDataRepository( type = type ) + override suspend fun setWidgetViewId( + ctx: Id, + widget: Id, + view: Id + ): Payload = remote.setWidgetViewId( + ctx = ctx, + widget = widget, + view = view + ) + override suspend fun addDataViewFilter(command: Command.AddFilter): Payload { return remote.addDataViewFilter(command = command) } diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt index aafdc8c447..fd3ccc4df0 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt @@ -337,6 +337,12 @@ interface BlockRemote { type: Block.Content.Widget.Layout ): Payload + suspend fun setWidgetViewId( + ctx: Id, + widget: Id, + view: Id + ): Payload + suspend fun addDataViewFilter(command: Command.AddFilter): Payload suspend fun removeDataViewFilter(command: Command.RemoveFilter): Payload suspend fun replaceDataViewFilter(command: Command.ReplaceFilter): Payload diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/base/Interactor.kt b/domain/src/main/java/com/anytypeio/anytype/domain/base/Interactor.kt index 1ba4aaf770..123e501dae 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/base/Interactor.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/base/Interactor.kt @@ -88,7 +88,7 @@ abstract class ResultInteractor( suspend fun run(params: P) = doWork(params) /* - * Executes on the caller's thread. + * N.B. Executes on the caller's thread. * */ suspend fun execute(params: P): Resultat = runCatchingL { doWork(params) } diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt index c4397568cc..6526f9279a 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt @@ -389,6 +389,12 @@ interface BlockRepository { type: Block.Content.Widget.Layout ): Payload + suspend fun setWidgetViewId( + ctx: Id, + widget: Id, + view: Id + ): Payload + suspend fun addDataViewFilter(command: Command.AddFilter): Payload suspend fun removeDataViewFilter(command: Command.RemoveFilter): Payload suspend fun replaceDataViewFilter(command: Command.ReplaceFilter): Payload diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/widgets/SetWidgetActiveView.kt b/domain/src/main/java/com/anytypeio/anytype/domain/widgets/SetWidgetActiveView.kt new file mode 100644 index 0000000000..f769d49fef --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/widgets/SetWidgetActiveView.kt @@ -0,0 +1,29 @@ +package com.anytypeio.anytype.domain.widgets + +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import javax.inject.Inject + +/** + * Use-case for setting active view id for widget with list or compact list layout. + */ +class SetWidgetActiveView @Inject constructor( + private val repo: BlockRepository, + dispatchers: AppCoroutineDispatchers +) : ResultInteractor(dispatchers.io) { + + override suspend fun doWork(params: Params) = repo.setWidgetViewId( + ctx = params.ctx, + widget = params.widget, + view = params.view + ) + + data class Params( + val ctx: Id, + val widget: Id, + val view: Id + ) +} \ No newline at end of file diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt index 3204b51d29..6f65b27204 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt @@ -728,6 +728,16 @@ class BlockMiddleware( type = type ) + override suspend fun setWidgetViewId( + ctx: Id, + widget: Id, + view: Id + ): Payload = middleware.setWidgetViewId( + ctx = ctx, + widget = widget, + view = view + ) + override suspend fun addDataViewFilter(command: Command.AddFilter): Payload { return middleware.addDataViewFilter(command) } diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt index 71cb4fbfdd..581574d52e 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt @@ -1957,6 +1957,22 @@ class Middleware( return response.event.toPayload() } + fun setWidgetViewId( + ctx: Id, + widget: Id, + view: Id + ): Payload { + val request = Rpc.BlockWidget.SetViewId.Request( + contextId = ctx, + blockId = widget, + viewId = view + ) + if (BuildConfig.DEBUG) logRequest(request) + val response = service.blockWidgetSetViewId(request) + if (BuildConfig.DEBUG) logResponse(response) + return response.event.toPayload() + } + @Throws(Exception::class) fun updateWidget( ctx: Id, diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareEventChannel.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareEventChannel.kt index 5a9b1b8708..973031e1e2 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareEventChannel.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareEventChannel.kt @@ -38,7 +38,8 @@ class MiddlewareEventChannel( msg.objectRelationsRemove, msg.blockDataviewViewUpdate, msg.blockDataviewTargetObjectIdSet, - msg.blockDataviewIsCollectionSet + msg.blockDataviewIsCollectionSet, + msg.blockSetWidget ) return events.any { it != null } } diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareProtobufLogger.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareProtobufLogger.kt index 81d85b8713..ee522d67ed 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareProtobufLogger.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareProtobufLogger.kt @@ -47,9 +47,13 @@ interface MiddlewareProtobufLogger { } private fun Any.toLogMessage(): String { - return "${this::class.java.canonicalName}:\n${ - protobufConverter.provideConverter().toJson(this) - }" + return if (featureToggles.isConciseLogging) { + this::class.java.canonicalName + } else { + "${this::class.java.canonicalName}:\n${ + protobufConverter.provideConverter().toJson(this) + }" + } } private fun containsOnlyThreadStatusEvents(event: Event) : Boolean { 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 51c43a3395..4d8f5a7036 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 @@ -393,7 +393,8 @@ fun MBlock.toCoreWidget(): Block.Content.Widget { MWidgetLayout.List -> Block.Content.Widget.Layout.LIST MWidgetLayout.CompactList -> Block.Content.Widget.Layout.COMPACT_LIST }, - limit = content.limit + limit = content.limit, + activeView = content.viewId.ifEmpty { null } ) } diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt index 9c4639ff45..9b606ab274 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt @@ -427,6 +427,9 @@ interface MiddlewareService { @Throws(Exception::class) fun blockCreateWidget(request: Rpc.Block.CreateWidget.Request): Rpc.Block.CreateWidget.Response + @Throws(Exception::class) + fun blockWidgetSetViewId(request: Rpc.BlockWidget.SetViewId.Request) : Rpc.BlockWidget.SetViewId.Response + //endregion //region WORKSPACE diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt index 7388624a52..a11feb656f 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt @@ -17,7 +17,7 @@ class MiddlewareServiceImplementation @Inject constructor( ) : MiddlewareService { init { - if (!featureToggles.isLogFromMiddlewareLibrary) { + if (!featureToggles.isLogFromGoProcess) { Service.setEnv("ANYTYPE_LOG_LEVEL", "*=fatal;anytype*=error") } } @@ -164,6 +164,19 @@ class MiddlewareServiceImplementation @Inject constructor( } } + override fun blockWidgetSetViewId(request: Rpc.BlockWidget.SetViewId.Request): Rpc.BlockWidget.SetViewId.Response { + val encoded = Service.blockWidgetSetViewId( + Rpc.BlockWidget.SetViewId.Request.ADAPTER.encode(request) + ) + val response = Rpc.BlockWidget.SetViewId.Response.ADAPTER.decode(encoded) + val error = response.error + if (error != null && error.code != Rpc.BlockWidget.SetViewId.Response.Error.Code.NULL) { + throw Exception(error.description) + } else { + return response + } + } + override fun blockDataViewActiveSet(request: Rpc.BlockDataview.View.SetActive.Request): Rpc.BlockDataview.View.SetActive.Response { val encoded = Service.blockDataviewViewSetActive( Rpc.BlockDataview.View.SetActive.Request.ADAPTER.encode(request) 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 d96917b08e..3d5461d4d3 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 @@ -43,6 +43,7 @@ import com.anytypeio.anytype.domain.widgets.CreateWidget import com.anytypeio.anytype.domain.widgets.DeleteWidget import com.anytypeio.anytype.domain.widgets.GetWidgetSession import com.anytypeio.anytype.domain.widgets.SaveWidgetSession +import com.anytypeio.anytype.domain.widgets.SetWidgetActiveView import com.anytypeio.anytype.domain.widgets.UpdateWidget import com.anytypeio.anytype.presentation.extension.sendAddWidgetEvent import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectCreateEvent @@ -74,6 +75,7 @@ import com.anytypeio.anytype.presentation.widgets.WidgetSessionStateHolder import com.anytypeio.anytype.presentation.widgets.WidgetView import com.anytypeio.anytype.presentation.widgets.collection.Subscription import com.anytypeio.anytype.presentation.widgets.getActiveTabViews +import com.anytypeio.anytype.presentation.widgets.parseActiveViews import com.anytypeio.anytype.presentation.widgets.parseWidgets import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow @@ -128,7 +130,8 @@ class HomeScreenViewModel( private val saveWidgetSession: SaveWidgetSession, private val spaceGradientProvider: SpaceGradientProvider, private val storeOfObjectTypes: StoreOfObjectTypes, - private val objectWatcher: ObjectWatcher + private val objectWatcher: ObjectWatcher, + private val setWidgetActiveView: SetWidgetActiveView ) : NavigationViewModel(), Reducer, WidgetActiveViewStateHolder by widgetActiveViewStateHolder, @@ -269,7 +272,7 @@ class HomeScreenViewModel( root = state.obj.root, details = state.obj.details ).also { - // TODO active view logic here? + widgetActiveViewStateHolder.init(state.obj.blocks.parseActiveViews()) } }.collect { Timber.d("Emitting list of widgets: ${it.size}") @@ -333,7 +336,7 @@ class HomeScreenViewModel( SaveWidgetSession.Params( WidgetSession( collapsed = collapsedWidgetStateHolder.get(), - widgetsToActiveViews = views.value.getActiveTabViews() + widgetsToActiveViews = emptyMap() ) ) ) @@ -783,6 +786,7 @@ class HomeScreenViewModel( ) } is Event.Command.Widgets.SetWidget -> { + Timber.d("Set widget event: $e") curr = curr.copy( blocks = curr.blocks.map { block -> if (block.id == e.widget) { @@ -844,7 +848,6 @@ class HomeScreenViewModel( } if (session != null) { collapsedWidgetStateHolder.set(session.collapsed) - widgetActiveViewStateHolder.init(session.widgetsToActiveViews) } proceedWithOpeningWidgetObject(widgetObject = configStorage.get().widgets) } @@ -1053,6 +1056,28 @@ class HomeScreenViewModel( } } + override fun onChangeCurrentWidgetView(widget: Id, view: Id) { + widgetActiveViewStateHolder.onChangeCurrentWidgetView( + widget = widget, + view = view + ).also { + viewModelScope.launch { + setWidgetActiveView.stream( + SetWidgetActiveView.Params( + ctx = configStorage.get().widgets, + widget = widget, + view = view, + ) + ).collect { result -> + result.fold( + onSuccess = { objectPayloadDispatcher.send(it) }, + onFailure = { Timber.e(it, "Error while updating active view") } + ) + } + } + } + } + override fun onCleared() { super.onCleared() viewModelScope.launch { @@ -1094,7 +1119,8 @@ class HomeScreenViewModel( private val saveWidgetSession: SaveWidgetSession, private val spaceGradientProvider: SpaceGradientProvider, private val storeOfObjectTypes: StoreOfObjectTypes, - private val objectWatcher: ObjectWatcher + private val objectWatcher: ObjectWatcher, + private val setWidgetActiveView: SetWidgetActiveView ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T = HomeScreenViewModel( @@ -1125,7 +1151,8 @@ class HomeScreenViewModel( saveWidgetSession = saveWidgetSession, spaceGradientProvider = spaceGradientProvider, storeOfObjectTypes = storeOfObjectTypes, - objectWatcher = objectWatcher + objectWatcher = objectWatcher, + setWidgetActiveView = setWidgetActiveView ) as T } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt index bbda485125..f755f71094 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt @@ -77,6 +77,20 @@ sealed class Widget { } } +fun List.parseActiveViews() : WidgetToActiveView { + val result = mutableMapOf() + forEach { block -> + val content = block.content + if (content is Block.Content.Widget) { + val view = content.activeView + if (!view.isNullOrEmpty()) { + result[block.id] = view + } + } + } + return result +} + fun List.parseWidgets( root: Id, details: Map diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetActiveViewStateHolder.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetActiveViewStateHolder.kt index bdee874f68..50b732f6d8 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetActiveViewStateHolder.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetActiveViewStateHolder.kt @@ -1,10 +1,12 @@ package com.anytypeio.anytype.presentation.widgets import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_utils.tools.toPrettyString import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.mapLatest +import timber.log.Timber interface WidgetActiveViewStateHolder { @@ -16,6 +18,7 @@ interface WidgetActiveViewStateHolder { private val widgetToActiveView = MutableStateFlow(mapOf()) override fun init(map: WidgetToActiveView) { + Timber.d("Initializing active view: ${map.toPrettyString()}") widgetToActiveView.value = map } @@ -29,4 +32,5 @@ interface WidgetActiveViewStateHolder { } } -typealias WidgetToActiveView = Map \ No newline at end of file +typealias WidgetToActiveView = Map +typealias WidgetActiveViewId = Id \ No newline at end of file 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 c2905f9d7f..e569ee43ec 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 @@ -3,6 +3,9 @@ package com.anytypeio.anytype.presentation.home import app.cash.turbine.test import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.DVFilter +import com.anytypeio.anytype.core_models.DVFilterCondition +import com.anytypeio.anytype.core_models.DVViewer import com.anytypeio.anytype.core_models.Event import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Key @@ -13,6 +16,9 @@ import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Payload import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.StubConfig +import com.anytypeio.anytype.core_models.StubDataView +import com.anytypeio.anytype.core_models.StubDataViewView +import com.anytypeio.anytype.core_models.StubFilter import com.anytypeio.anytype.core_models.StubLinkToObjectBlock import com.anytypeio.anytype.core_models.StubObject import com.anytypeio.anytype.core_models.StubObjectView @@ -42,15 +48,16 @@ import com.anytypeio.anytype.domain.widgets.CreateWidget import com.anytypeio.anytype.domain.widgets.DeleteWidget import com.anytypeio.anytype.domain.widgets.GetWidgetSession import com.anytypeio.anytype.domain.widgets.SaveWidgetSession +import com.anytypeio.anytype.domain.widgets.SetWidgetActiveView import com.anytypeio.anytype.domain.widgets.UpdateWidget import com.anytypeio.anytype.presentation.objects.ObjectIcon +import com.anytypeio.anytype.presentation.search.ObjectSearchConstants import com.anytypeio.anytype.presentation.search.Subscriptions import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider import com.anytypeio.anytype.presentation.util.DefaultCoroutineTestRule import com.anytypeio.anytype.presentation.util.Dispatcher import com.anytypeio.anytype.presentation.widgets.BundledWidgetSourceIds import com.anytypeio.anytype.presentation.widgets.CollapsedWidgetStateHolder -import com.anytypeio.anytype.presentation.widgets.DataViewListWidgetContainer import com.anytypeio.anytype.presentation.widgets.DropDownMenuAction import com.anytypeio.anytype.presentation.widgets.ListWidgetContainer import com.anytypeio.anytype.presentation.widgets.TreeWidgetContainer @@ -81,6 +88,7 @@ import org.mockito.kotlin.stub import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyBlocking +import org.mockito.kotlin.verifyNoMoreInteractions class HomeScreenViewModelTest { @@ -153,6 +161,9 @@ class HomeScreenViewModelTest { @Mock lateinit var getWidgetSession: GetWidgetSession + @Mock + lateinit var setWidgetActiveView: SetWidgetActiveView + @Mock lateinit var storeOfObjectTypes: StoreOfObjectTypes @@ -205,7 +216,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubCollapsedWidgetState(any()) stubGetWidgetSession() @@ -278,7 +289,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubCollapsedWidgetState(any()) stubGetWidgetSession() @@ -349,7 +360,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() @@ -442,7 +453,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = widgetBlock.id, @@ -552,7 +563,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = widgetBlock.id, @@ -649,7 +660,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = widgetBlock.id, @@ -764,7 +775,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubFavoritesObjectWatcher() stubSearchByIds( @@ -956,7 +967,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() @@ -1036,7 +1047,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() @@ -1175,7 +1186,7 @@ class HomeScreenViewModelTest { } ) stubConfig() - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() @@ -1242,7 +1253,7 @@ class HomeScreenViewModelTest { stubInterceptEvents(events = emptyFlow()) stubConfig() - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() @@ -1328,7 +1339,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = favoriteWidgetBlock.id, @@ -1464,7 +1475,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = favoriteWidgetBlock.id, @@ -1676,7 +1687,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = favoriteWidgetBlock.id, @@ -1872,7 +1883,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() @@ -1948,7 +1959,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() @@ -2015,7 +2026,7 @@ class HomeScreenViewModelTest { stubConfig() stubInterceptEvents(events = emptyFlow()) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() @@ -2100,7 +2111,7 @@ class HomeScreenViewModelTest { ) } ) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() @@ -2187,7 +2198,7 @@ class HomeScreenViewModelTest { ) } ) - stubOpenObject(givenObjectView) + stubOpenWidgetObject(givenObjectView) stubSearchByIds( subscription = widgetBlock.id, targets = emptyList() @@ -2222,6 +2233,230 @@ class HomeScreenViewModelTest { } } + @Test + fun `should not re-fetch data after updating active view locally and then on mw`() = runTest { + + val currentWidgetSourceObject = StubObject( + id = "SOURCE OBJECT 1", + links = emptyList(), + objectType = ObjectTypeIds.SET + ) + + val widgetSourceLink = StubLinkToObjectBlock( + id = "SOURCE LINK", + target = currentWidgetSourceObject.id + ) + + val widgetBlock = StubWidgetBlock( + id = "WIDGET BLOCK", + layout = Block.Content.Widget.Layout.LIST, + children = listOf(widgetSourceLink.id) + ) + + val smartWidgetBlock = StubSmartBlock( + id = WIDGET_OBJECT_ID, + children = listOf(widgetBlock.id), + ) + + val givenWidgetObjectView = StubObjectView( + root = WIDGET_OBJECT_ID, + blocks = listOf( + smartWidgetBlock, + widgetBlock, + widgetSourceLink + ), + details = mapOf( + currentWidgetSourceObject.id to currentWidgetSourceObject.map + ) + ) + + val dataViewFirstView = StubDataViewView() + val dataViewSecondView = StubDataViewView( + filters = listOf(StubFilter()) + ) + + val dataViewBlock = StubDataView( + views = listOf(dataViewFirstView, dataViewSecondView) + ) + + val dataViewSmartBlock = StubSmartBlock( + id = currentWidgetSourceObject.id, + children = listOf(dataViewBlock.id) + ) + + val givenDataViewObjectView = StubObjectView( + root = dataViewSmartBlock.id, + blocks = listOf( + dataViewSmartBlock, + dataViewBlock + ), + details = mapOf( + currentWidgetSourceObject.id to mapOf( + Relations.ID to currentWidgetSourceObject.map + ) + ) + ) + + stubConfig() + stubInterceptEvents(events = emptyFlow()) + + stubOpenWidgetObject(givenWidgetObjectView) + stubGetObject(givenDataViewObjectView) + + stubWidgetActiveView(widgetBlock) + + stubSetWidgetActiveView( + widget = widgetBlock.id, + view = dataViewSecondView.id + ) + + val firstTimeParams = StoreSearchParams( + subscription = widgetBlock.id, + filters = buildList { + addAll(ObjectSearchConstants.defaultDataViewFilters(config.workspace)) + add( + DVFilter( + relation = Relations.TYPE, + condition = DVFilterCondition.NOT_IN, + value = listOf( + ObjectTypeIds.OBJECT_TYPE, + ObjectTypeIds.RELATION, + ObjectTypeIds.TEMPLATE, + ObjectTypeIds.IMAGE, + ObjectTypeIds.FILE, + ObjectTypeIds.VIDEO, + ObjectTypeIds.AUDIO, + ObjectTypeIds.DASHBOARD, + ObjectTypeIds.RELATION_OPTION, + ObjectTypeIds.DASHBOARD, + ObjectTypeIds.DATE + ) + ), + ) + }, + sorts = emptyList(), + limit = WidgetConfig.DEFAULT_LIST_MAX_COUNT, + keys = buildList { + addAll(ObjectSearchConstants.defaultDataViewKeys) + add(Relations.DESCRIPTION) + }.distinct(), + source = currentWidgetSourceObject.setOf + ) + + // Params expected after switching active view + val secondTimeParams = firstTimeParams.copy( + filters = buildList { + addAll(dataViewSecondView.filters) + addAll(firstTimeParams.filters) + } + ) + + stubDefaultSearch( + params = firstTimeParams, + results = emptyList() + ) + + stubDefaultSearch( + params = secondTimeParams, + results = emptyList() + ) + + stubCollapsedWidgetState(any()) + stubGetWidgetSession() + stubGetDefaultPageType() + stubObserveSpaceObject() + + // Using real implementation here + activeViewStateHolder = WidgetActiveViewStateHolder.Impl() + + 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.SetOfObjects && firstWidget.tabs.first().isSelected + } + verifyBlocking(getObject, times(1)) { + run(params = currentWidgetSourceObject.id) + } + verify(storelessSubscriptionContainer, times(1)).subscribe( + StoreSearchByIdsParams( + subscription = HomeScreenViewModel.HOME_SCREEN_SPACE_OBJECT_SUBSCRIPTION, + targets = listOf(config.workspace), + keys = listOf( + Relations.ID, + Relations.ICON_EMOJI, + Relations.ICON_IMAGE, + Relations.ICON_OPTION + ) + ) + ) + verify(storelessSubscriptionContainer, times(1)).subscribe( + firstTimeParams + ) + advanceUntilIdle() + + // Changing active view + vm.onChangeCurrentWidgetView( + widget = widgetBlock.id, + view = dataViewSecondView.id + ) + + advanceUntilIdle() + val thirdTimeItem = awaitItem() + advanceUntilIdle() + assertTrue { + val firstWidget = thirdTimeItem.first() + firstWidget is WidgetView.SetOfObjects && firstWidget.tabs.last().isSelected + } + verify(storelessSubscriptionContainer, times(1)).subscribe( + secondTimeParams + ) + verifyNoMoreInteractions(storelessSubscriptionContainer) + } + } + + private fun stubSetWidgetActiveView( + widget: Id, + view: Id, + ) { + setWidgetActiveView.stub { + on { + stream( + params = SetWidgetActiveView.Params( + ctx = config.widgets, + widget = widget, + view = view + ) + ) + } doReturn flowOf( + Resultat.Success( + Payload( + context = WIDGET_OBJECT_ID, + events = listOf( + Event.Command.Widgets.SetWidget( + context = WIDGET_OBJECT_ID, + widget = widget, + activeView = view + ) + ) + ) + ) + ) + } + } + private fun stubInterceptEvents(events: Flow>) { interceptEvents.stub { on { build(InterceptEvents.Params(WIDGET_OBJECT_ID)) } doReturn events @@ -2234,7 +2469,7 @@ class HomeScreenViewModelTest { } } - private fun stubOpenObject(givenObjectView: ObjectView) { + private fun stubOpenWidgetObject(givenObjectView: ObjectView) { openObject.stub { on { stream(OpenObject.Params(WIDGET_OBJECT_ID, false)) @@ -2246,6 +2481,16 @@ class HomeScreenViewModelTest { } } + private fun stubGetObject( + givenObjectView: ObjectView + ) { + getObject.stub { + onBlocking { + run(givenObjectView.root) + } doReturn givenObjectView + } + } + private fun stubCloseObject() { closeObject.stub { onBlocking { @@ -2324,6 +2569,16 @@ class HomeScreenViewModelTest { } } + private fun stubGetDefaultPageType() { + getDefaultPageType.stub { + onBlocking { + execute(any()) + } doReturn Resultat.Success( + GetDefaultPageType.Response(null, null) + ) + } + } + private fun buildViewModel() = HomeScreenViewModel( configStorage = configStorage, interceptEvents = interceptEvents, @@ -2352,7 +2607,8 @@ class HomeScreenViewModelTest { saveWidgetSession = saveWidgetSession, spaceGradientProvider = spaceGradientProvider, storeOfObjectTypes = storeOfObjectTypes, - objectWatcher = objectWatcher + objectWatcher = objectWatcher, + setWidgetActiveView = setWidgetActiveView ) companion object {