From 48159c24af697f85ea80ae00eaaed8b832acfaa0 Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Sat, 8 Feb 2020 23:28:08 +0300 Subject: [PATCH] Refactor event handler (#201) --- .../anytype/di/feature/DashboardDi.kt | 27 +- .../agileburo/anytype/di/feature/PageDI.kt | 9 - .../agileburo/anytype/di/main/DataModule.kt | 10 +- .../ui/desktop/HomeDashboardFragment.kt | 6 +- app/src/main/res/xml/collapsing_toolbar.xml | 26 +- .../data/auth/event/EventDataChannel.kt | 7 +- .../data/auth/event/EventRemoteChannel.kt | 2 +- .../data/auth/mapper/MapperExtension.kt | 21 +- .../anytype/data/auth/model/EventEntity.kt | 16 +- .../auth/repo/block/BlockDataRepository.kt | 8 - .../data/auth/repo/block/BlockDataStore.kt | 7 - .../data/auth/repo/block/BlockRemote.kt | 8 - .../auth/repo/block/BlockRemoteDataStore.kt | 4 - .../domain/block/repo/BlockRepository.kt | 10 - .../agileburo/anytype/domain/config/Config.kt | 8 +- .../dashboard/interactor/CloseDashboard.kt | 2 +- .../interactor/ObserveHomeDashboard.kt | 38 -- .../dashboard/interactor/OpenDashboard.kt | 4 +- .../domain/event/interactor/EventChannel.kt | 3 +- .../event/interactor/InterceptEvents.kt | 14 +- .../domain/event/interactor/ObserveEvents.kt | 17 - .../anytype/domain/event/model/Event.kt | 25 +- .../anytype/domain/page/CreatePage.kt | 2 +- .../anytype/domain/page/ObservePage.kt | 12 - .../dashboard/ObserveHomeDashboardTest.kt | 172 -------- .../domain/dashboard/OpenDashboardTest.kt | 6 +- .../anytype/middleware/MapperExtension.kt | 11 + .../middleware/block/BlockMiddleware.kt | 383 +----------------- .../middleware/interactor/Middleware.java | 2 + .../interactor/MiddlewareEventChannel.kt | 112 +++-- .../anytype/MiddlewareEventChannelTest.kt | 153 +++++++ .../anytype/common/MockDataFactory.kt | 64 +++ .../desktop/HomeDashboardStateMachine.kt | 139 +++++++ .../desktop/HomeDashboardViewModel.kt | 192 +++------ .../desktop/HomeDashboardViewModelFactory.kt | 9 +- .../anytype/presentation/desktop/Machine.kt | 2 - .../presentation/page/PageViewModel.kt | 1 + .../home/HomeDashboardViewModelTest.kt | 272 +++++++------ .../presentation/page/PageViewModelTest.kt | 105 +++-- 39 files changed, 829 insertions(+), 1080 deletions(-) delete mode 100644 domain/src/main/java/com/agileburo/anytype/domain/dashboard/interactor/ObserveHomeDashboard.kt delete mode 100644 domain/src/main/java/com/agileburo/anytype/domain/event/interactor/ObserveEvents.kt delete mode 100644 domain/src/main/java/com/agileburo/anytype/domain/page/ObservePage.kt delete mode 100644 domain/src/test/java/com/agileburo/anytype/domain/dashboard/ObserveHomeDashboardTest.kt create mode 100644 middleware/src/test/java/com/agileburo/anytype/MiddlewareEventChannelTest.kt create mode 100644 middleware/src/test/java/com/agileburo/anytype/common/MockDataFactory.kt create mode 100644 presentation/src/main/java/com/agileburo/anytype/presentation/desktop/HomeDashboardStateMachine.kt delete mode 100644 presentation/src/main/java/com/agileburo/anytype/presentation/desktop/Machine.kt diff --git a/app/src/main/java/com/agileburo/anytype/di/feature/DashboardDi.kt b/app/src/main/java/com/agileburo/anytype/di/feature/DashboardDi.kt index ebbfd58a41..3c651912c3 100644 --- a/app/src/main/java/com/agileburo/anytype/di/feature/DashboardDi.kt +++ b/app/src/main/java/com/agileburo/anytype/di/feature/DashboardDi.kt @@ -7,9 +7,9 @@ import com.agileburo.anytype.domain.block.interactor.DragAndDrop import com.agileburo.anytype.domain.block.repo.BlockRepository import com.agileburo.anytype.domain.config.GetConfig import com.agileburo.anytype.domain.dashboard.interactor.CloseDashboard -import com.agileburo.anytype.domain.dashboard.interactor.ObserveHomeDashboard import com.agileburo.anytype.domain.dashboard.interactor.OpenDashboard -import com.agileburo.anytype.domain.event.interactor.ObserveEvents +import com.agileburo.anytype.domain.event.interactor.EventChannel +import com.agileburo.anytype.domain.event.interactor.InterceptEvents import com.agileburo.anytype.domain.image.ImageLoader import com.agileburo.anytype.domain.image.LoadImage import com.agileburo.anytype.domain.page.CreatePage @@ -48,9 +48,8 @@ class HomeDashboardModule { createPage: CreatePage, closeDashboard: CloseDashboard, getConfig: GetConfig, - observeHomeDashboard: ObserveHomeDashboard, dnd: DragAndDrop, - observeEvents: ObserveEvents + interceptEvents: InterceptEvents ): HomeDashboardViewModelFactory = HomeDashboardViewModelFactory( getCurrentAccount = getCurrentAccount, loadImage = loadImage, @@ -58,9 +57,8 @@ class HomeDashboardModule { createPage = createPage, closeDashboard = closeDashboard, getConfig = getConfig, - observeHomeDashboard = observeHomeDashboard, dnd = dnd, - observeEvents = observeEvents + interceptEvents = interceptEvents ) @Provides @@ -112,15 +110,6 @@ class HomeDashboardModule { repo = repo ) - @Provides - @PerScreen - fun provideObserveHomeDashboardUseCase( - repo: BlockRepository - ): ObserveHomeDashboard = ObserveHomeDashboard( - context = Dispatchers.IO, - repo = repo - ) - @Provides @PerScreen fun provideDragAndDropUseCase( @@ -131,10 +120,10 @@ class HomeDashboardModule { @Provides @PerScreen - fun provideObserveEventsUseCase( - repo: BlockRepository - ): ObserveEvents = ObserveEvents( + fun provideInterceptEvents( + channel: EventChannel + ): InterceptEvents = InterceptEvents( context = Dispatchers.IO, - repo = repo + channel = channel ) } \ No newline at end of file diff --git a/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt b/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt index c013e21fc0..01157fde75 100644 --- a/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt +++ b/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt @@ -6,7 +6,6 @@ import com.agileburo.anytype.domain.block.repo.BlockRepository import com.agileburo.anytype.domain.event.interactor.EventChannel import com.agileburo.anytype.domain.event.interactor.InterceptEvents import com.agileburo.anytype.domain.page.ClosePage -import com.agileburo.anytype.domain.page.ObservePage import com.agileburo.anytype.domain.page.OpenPage import com.agileburo.anytype.presentation.page.PageViewModelFactory import com.agileburo.anytype.ui.page.PageFragment @@ -69,14 +68,6 @@ class PageModule { repo = repo ) - @Provides - @PerScreen - fun provideObservePageUseCase( - repo: BlockRepository - ): ObservePage = ObservePage( - repo = repo - ) - @Provides @PerScreen fun provideClosePageUseCase( diff --git a/app/src/main/java/com/agileburo/anytype/di/main/DataModule.kt b/app/src/main/java/com/agileburo/anytype/di/main/DataModule.kt index e741c7f1f4..5fd5c5e4c2 100644 --- a/app/src/main/java/com/agileburo/anytype/di/main/DataModule.kt +++ b/app/src/main/java/com/agileburo/anytype/di/main/DataModule.kt @@ -143,14 +143,8 @@ class DataModule { @Provides @Singleton fun provideBlockRemote( - middleware: Middleware, - eventProxy: EventProxy - ): BlockRemote { - return BlockMiddleware( - middleware = middleware, - events = eventProxy - ) - } + middleware: Middleware + ): BlockRemote = BlockMiddleware(middleware = middleware) @Provides @Singleton diff --git a/app/src/main/java/com/agileburo/anytype/ui/desktop/HomeDashboardFragment.kt b/app/src/main/java/com/agileburo/anytype/ui/desktop/HomeDashboardFragment.kt index cb9a689817..97c6a634ee 100644 --- a/app/src/main/java/com/agileburo/anytype/ui/desktop/HomeDashboardFragment.kt +++ b/app/src/main/java/com/agileburo/anytype/ui/desktop/HomeDashboardFragment.kt @@ -12,8 +12,8 @@ import com.agileburo.anytype.core_utils.ext.invisible import com.agileburo.anytype.core_utils.ext.toast import com.agileburo.anytype.core_utils.ext.visible import com.agileburo.anytype.di.common.componentManager +import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine.State import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModel -import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModel.Machine.State import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModelFactory import com.agileburo.anytype.presentation.mapper.toView import com.agileburo.anytype.presentation.profile.ProfileView @@ -92,10 +92,10 @@ class HomeDashboardFragment : ViewStateFragment(R.layout.fragment_desktop progress.invisible() requireActivity().toast("Error: ${state.error}") } - state.homeDashboard != null -> { + state.dashboard != null -> { progress.invisible() fab.visible() - dashboardAdapter.update(state.homeDashboard!!.toView()) + state.dashboard?.let { dashboardAdapter.update(it.toView()) } } } } diff --git a/app/src/main/res/xml/collapsing_toolbar.xml b/app/src/main/res/xml/collapsing_toolbar.xml index 368a72814a..eebaebc84c 100644 --- a/app/src/main/res/xml/collapsing_toolbar.xml +++ b/app/src/main/res/xml/collapsing_toolbar.xml @@ -20,20 +20,20 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="@+id/avatar" /> + app:layout_constraintTop_toTopOf="@+id/avatar" /> events.map { it.toDomain() } } + + override fun observeEvents( + context: Id? + ) = remote.observeEvents(context).map { events -> events.map { it.toDomain() } } } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/event/EventRemoteChannel.kt b/data/src/main/java/com/agileburo/anytype/data/auth/event/EventRemoteChannel.kt index aad228fddc..ac91d8ee46 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/event/EventRemoteChannel.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/event/EventRemoteChannel.kt @@ -4,5 +4,5 @@ import com.agileburo.anytype.data.auth.model.EventEntity import kotlinx.coroutines.flow.Flow interface EventRemoteChannel { - fun observeEvents(): Flow> + fun observeEvents(context: String? = null): Flow> } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt b/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt index 0ded5ab1e7..48a3c03ead 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt @@ -202,7 +202,7 @@ fun Block.Content.Text.Mark.toEntity(): BlockEntity.Content.Text.Mark { fun ConfigEntity.toDomain(): Config { return Config( - homeDashboardId = homeId + home = homeId ) } @@ -272,18 +272,21 @@ fun EventEntity.toDomain(): Event { is EventEntity.Command.ShowBlock -> { Event.Command.ShowBlock( rootId = rootId, - blocks = blocks.map { it.toDomain() } + blocks = blocks.map { it.toDomain() }, + context = context ) } is EventEntity.Command.AddBlock -> { Event.Command.AddBlock( - blocks = blocks.map { it.toDomain() } + blocks = blocks.map { it.toDomain() }, + context = context ) } is EventEntity.Command.UpdateBlockText -> { Event.Command.UpdateBlockText( id = id, - text = text + text = text, + context = context ) } is EventEntity.Command.UpdateStructure -> { @@ -295,11 +298,13 @@ fun EventEntity.toDomain(): Event { } is EventEntity.Command.DeleteBlock -> { Event.Command.DeleteBlock( + context = context, target = target ) } is EventEntity.Command.GranularChange -> { Event.Command.GranularChange( + context = context, id = id, text = text, style = if (style != null) @@ -309,6 +314,14 @@ fun EventEntity.toDomain(): Event { color = color ) } + is EventEntity.Command.LinkGranularChange -> { + Event.Command.LinkGranularChange( + context = context, + id = id, + target = target, + fields = fields?.let { Block.Fields(it.map) } + ) + } } } diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/model/EventEntity.kt b/data/src/main/java/com/agileburo/anytype/data/auth/model/EventEntity.kt index e9588ad6d0..fa36b94af9 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/model/EventEntity.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/model/EventEntity.kt @@ -2,36 +2,50 @@ package com.agileburo.anytype.data.auth.model sealed class EventEntity { + abstract val context: String + sealed class Command : EventEntity() { data class ShowBlock( + override val context: String, val rootId: String, val blocks: List ) : Command() data class AddBlock( + override val context: String, val blocks: List ) : Command() data class UpdateBlockText( + override val context: String, val id: String, val text: String ) : Command() data class GranularChange( + override val context: String, val id: String, val text: String? = null, val style: BlockEntity.Content.Text.Style? = null, val color: String? = null ) : Command() + data class LinkGranularChange( + override val context: String, + val id: String, + val target: String, + val fields: BlockEntity.Fields? + ) : Command() + data class UpdateStructure( - val context: String, + override val context: String, val id: String, val children: List ) : Command() data class DeleteBlock( + override val context: String, val target: String ) : Command() } diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt index cb0c0b91e2..f5065ccf99 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt @@ -4,7 +4,6 @@ import com.agileburo.anytype.data.auth.mapper.toDomain import com.agileburo.anytype.data.auth.mapper.toEntity import com.agileburo.anytype.domain.block.model.Command import com.agileburo.anytype.domain.block.repo.BlockRepository -import kotlinx.coroutines.flow.map class BlockDataRepository( private val factory: BlockDataStoreFactory @@ -30,13 +29,6 @@ class BlockDataRepository( factory.remote.closePage(id) } - override fun observeBlocks() = - factory.remote.observeBlocks().map { blocks -> blocks.map { it.toDomain() } } - - override fun observeEvents() = factory.remote.observeEvents().map { it.toDomain() } - override fun observePages() = - factory.remote.observePages().map { blocks -> blocks.map { it.toDomain() } } - override suspend fun updateText(command: Command.UpdateText) { factory.remote.updateText(command.toEntity()) } diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt index 7f0b1714a2..204a8df593 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt @@ -1,11 +1,8 @@ package com.agileburo.anytype.data.auth.repo.block -import com.agileburo.anytype.data.auth.model.BlockEntity import com.agileburo.anytype.data.auth.model.CommandEntity import com.agileburo.anytype.data.auth.model.ConfigEntity -import com.agileburo.anytype.data.auth.model.EventEntity import com.agileburo.anytype.domain.common.Id -import kotlinx.coroutines.flow.Flow interface BlockDataStore { suspend fun create(command: CommandEntity.Create) @@ -22,8 +19,4 @@ interface BlockDataStore { suspend fun closePage(id: String) suspend fun openDashboard(contextId: String, id: String) suspend fun closeDashboard(id: String) - - fun observeBlocks(): Flow> - fun observeEvents(): Flow - fun observePages(): Flow> } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt index 2c3252dca4..2d6c901f4a 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt @@ -1,11 +1,8 @@ package com.agileburo.anytype.data.auth.repo.block -import com.agileburo.anytype.data.auth.model.BlockEntity import com.agileburo.anytype.data.auth.model.CommandEntity import com.agileburo.anytype.data.auth.model.ConfigEntity -import com.agileburo.anytype.data.auth.model.EventEntity import com.agileburo.anytype.domain.common.Id -import kotlinx.coroutines.flow.Flow interface BlockRemote { suspend fun create(command: CommandEntity.Create) @@ -20,11 +17,6 @@ interface BlockRemote { suspend fun createPage(parentId: String): String suspend fun openPage(id: String) suspend fun closePage(id: String) - - fun observeBlocks(): Flow> - fun observeEvents(): Flow - fun observePages(): Flow> - suspend fun openDashboard(contextId: String, id: String) suspend fun closeDashboard(id: String) } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt index 334db83529..02c2c067dd 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt @@ -14,10 +14,6 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore { remote.closeDashboard(id = id) } - override fun observeBlocks() = remote.observeBlocks() - override fun observePages() = remote.observePages() - override fun observeEvents() = remote.observeEvents() - override suspend fun createPage(parentId: String): String = remote.createPage(parentId) override suspend fun openPage(id: String) { remote.openPage(id) diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt index 8da40fcc2d..8424ca33c7 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt @@ -1,11 +1,8 @@ package com.agileburo.anytype.domain.block.repo -import com.agileburo.anytype.domain.block.model.Block import com.agileburo.anytype.domain.block.model.Command import com.agileburo.anytype.domain.common.Id import com.agileburo.anytype.domain.config.Config -import com.agileburo.anytype.domain.event.model.Event -import kotlinx.coroutines.flow.Flow interface BlockRepository { suspend fun dnd(command: Command.Dnd) @@ -22,11 +19,4 @@ interface BlockRepository { suspend fun closePage(id: String) suspend fun openDashboard(contextId: String, id: String) suspend fun closeDashboard(id: String) - - @Deprecated("Will be removed and replaced by observeEvents()") - fun observeBlocks(): Flow> - - @Deprecated("Will be removed and replaced by [EventChannel]") - fun observeEvents(): Flow - fun observePages(): Flow> } \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/config/Config.kt b/domain/src/main/java/com/agileburo/anytype/domain/config/Config.kt index 4a81f52730..e46475a9bc 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/config/Config.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/config/Config.kt @@ -1,3 +1,9 @@ package com.agileburo.anytype.domain.config -data class Config(val homeDashboardId: String) \ No newline at end of file +import com.agileburo.anytype.domain.common.Id + +/** + * Anytype app configuration properties. + * @property home id of the home dashboard + */ +data class Config(val home: Id) \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/dashboard/interactor/CloseDashboard.kt b/domain/src/main/java/com/agileburo/anytype/domain/dashboard/interactor/CloseDashboard.kt index ab7f143c9e..78b6bdef7c 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/dashboard/interactor/CloseDashboard.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/dashboard/interactor/CloseDashboard.kt @@ -17,7 +17,7 @@ class CloseDashboard( override suspend fun run(params: Param) = try { if (params.id == MainConfig.HOME_DASHBOARD_ID) repo.getConfig().let { config -> - repo.closeDashboard(id = config.homeDashboardId) + repo.closeDashboard(id = config.home) }.let { Either.Right(it) } diff --git a/domain/src/main/java/com/agileburo/anytype/domain/dashboard/interactor/ObserveHomeDashboard.kt b/domain/src/main/java/com/agileburo/anytype/domain/dashboard/interactor/ObserveHomeDashboard.kt deleted file mode 100644 index 253a3032db..0000000000 --- a/domain/src/main/java/com/agileburo/anytype/domain/dashboard/interactor/ObserveHomeDashboard.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.agileburo.anytype.domain.dashboard.interactor - -import com.agileburo.anytype.domain.base.FlowUseCase -import com.agileburo.anytype.domain.block.model.Block -import com.agileburo.anytype.domain.block.repo.BlockRepository -import com.agileburo.anytype.domain.dashboard.model.HomeDashboard -import com.agileburo.anytype.domain.event.model.Event -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlin.coroutines.CoroutineContext - -class ObserveHomeDashboard( - private val context: CoroutineContext, - private val repo: BlockRepository -) : FlowUseCase() { - - override fun build(params: Param?) = repo - .observeEvents() - .filter { it is Event.Command.ShowBlock } - .map { it as Event.Command.ShowBlock } - .filter { isDashboardBlock(it) } - .map { event -> event.blocks.toHomeDashboard(event.rootId) } - .flowOn(context) - - private fun isDashboardBlock( - event: Event.Command.ShowBlock - ): Boolean { - val target = event.blocks.find { it.id == event.rootId } - if (target != null) - return target.content is Block.Content.Dashboard - else - throw IllegalStateException("Could not found any block corresponding to the root id") - } - - data class Param(val id: String) - -} \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/dashboard/interactor/OpenDashboard.kt b/domain/src/main/java/com/agileburo/anytype/domain/dashboard/interactor/OpenDashboard.kt index d9dd26bd53..6811c1ba9c 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/dashboard/interactor/OpenDashboard.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/dashboard/interactor/OpenDashboard.kt @@ -24,8 +24,8 @@ class OpenDashboard( else { repo.getConfig().let { config -> repo.openDashboard( - contextId = config.homeDashboardId, - id = config.homeDashboardId + contextId = config.home, + id = config.home ).let { Either.Right(it) } diff --git a/domain/src/main/java/com/agileburo/anytype/domain/event/interactor/EventChannel.kt b/domain/src/main/java/com/agileburo/anytype/domain/event/interactor/EventChannel.kt index f71c828cfd..0424e1ac86 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/event/interactor/EventChannel.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/event/interactor/EventChannel.kt @@ -1,8 +1,9 @@ package com.agileburo.anytype.domain.event.interactor +import com.agileburo.anytype.domain.common.Id import com.agileburo.anytype.domain.event.model.Event import kotlinx.coroutines.flow.Flow interface EventChannel { - fun observeEvents(): Flow> + fun observeEvents(context: Id? = null): Flow> } \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/event/interactor/InterceptEvents.kt b/domain/src/main/java/com/agileburo/anytype/domain/event/interactor/InterceptEvents.kt index a7b15320c7..22ec9452d9 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/event/interactor/InterceptEvents.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/event/interactor/InterceptEvents.kt @@ -1,7 +1,7 @@ package com.agileburo.anytype.domain.event.interactor -import com.agileburo.anytype.domain.base.BaseUseCase import com.agileburo.anytype.domain.base.FlowUseCase +import com.agileburo.anytype.domain.common.Id import com.agileburo.anytype.domain.event.model.Event import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn @@ -15,9 +15,15 @@ import kotlin.coroutines.CoroutineContext class InterceptEvents( private val context: CoroutineContext, private val channel: EventChannel -) : FlowUseCase, BaseUseCase.None>() { +) : FlowUseCase, InterceptEvents.Params>() { - override fun build(params: BaseUseCase.None?): Flow> { - return channel.observeEvents().flowOn(context) + override fun build(params: Params?): Flow> { + return channel.observeEvents(params?.context).flowOn(context) } + + /** + * @property context optional event's context used for filtering. + * If a context is provided, only events related to this context will be intercepted. + */ + data class Params(val context: Id? = null) } \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/event/interactor/ObserveEvents.kt b/domain/src/main/java/com/agileburo/anytype/domain/event/interactor/ObserveEvents.kt deleted file mode 100644 index 9b866b4c4b..0000000000 --- a/domain/src/main/java/com/agileburo/anytype/domain/event/interactor/ObserveEvents.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.agileburo.anytype.domain.event.interactor - -import com.agileburo.anytype.domain.base.BaseUseCase -import com.agileburo.anytype.domain.base.FlowUseCase -import com.agileburo.anytype.domain.block.repo.BlockRepository -import com.agileburo.anytype.domain.event.model.Event -import kotlinx.coroutines.flow.flowOn -import kotlin.coroutines.CoroutineContext - -@Deprecated("Should use InterceptEvents") -class ObserveEvents( - private val context: CoroutineContext, - private val repo: BlockRepository -) : FlowUseCase() { - - override fun build(params: BaseUseCase.None?) = repo.observeEvents().flowOn(context) -} \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/event/model/Event.kt b/domain/src/main/java/com/agileburo/anytype/domain/event/model/Event.kt index 247cf9ff50..0ff1d511d8 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/event/model/Event.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/event/model/Event.kt @@ -6,22 +6,28 @@ import com.agileburo.anytype.domain.common.Id sealed class Event { + abstract val context: Id + sealed class Command : Event() { data class ShowBlock( + override val context: String, val rootId: Id, val blocks: List ) : Command() data class AddBlock( + override val context: String, val blocks: List ) : Command() data class DeleteBlock( + override val context: String, val target: Id ) : Command() data class UpdateBlockText( + override val context: String, val id: Id, val text: String ) : Command() @@ -31,9 +37,10 @@ sealed class Event { * @property id id of the target block * @property text new text (considered updated if not null) * @property style new style (considered updated if not null) - * @property color new color of the whole block + * @property color new color of the whole block (considered updated if not null) */ data class GranularChange( + override val context: String, val id: Id, val text: String? = null, val style: Text.Style? = null, @@ -42,6 +49,20 @@ sealed class Event { fun onlyTextChanged() = style == null && color == null && text != null } + /** + * Command to update link. + * @property context update's context + * @property id id of the link + * @property target id of the linked block + * @property fields link's fields (considered update if not null) + */ + data class LinkGranularChange( + override val context: String, + val id: Id, + val target: Id, + val fields: Block.Fields? + ) : Command() + /** * Command to update a block structure. * @property context context id for this command (i.e page id, dashboard id, etc.) @@ -49,7 +70,7 @@ sealed class Event { * @property children list of children ids for this block [id] */ data class UpdateStructure( - val context: String, + override val context: String, val id: Id, val children: List ) : Command() diff --git a/domain/src/main/java/com/agileburo/anytype/domain/page/CreatePage.kt b/domain/src/main/java/com/agileburo/anytype/domain/page/CreatePage.kt index b8fbdf6350..23c54cc690 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/page/CreatePage.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/page/CreatePage.kt @@ -17,7 +17,7 @@ class CreatePage( override suspend fun run(params: Params) = try { if (params.id == MainConfig.HOME_DASHBOARD_ID) { repo.getConfig().let { config -> - repo.createPage(config.homeDashboardId).let { + repo.createPage(config.home).let { Either.Right(it) } } diff --git a/domain/src/main/java/com/agileburo/anytype/domain/page/ObservePage.kt b/domain/src/main/java/com/agileburo/anytype/domain/page/ObservePage.kt deleted file mode 100644 index 695f7e07cd..0000000000 --- a/domain/src/main/java/com/agileburo/anytype/domain/page/ObservePage.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.agileburo.anytype.domain.page - -import com.agileburo.anytype.domain.base.BaseUseCase -import com.agileburo.anytype.domain.base.FlowUseCase -import com.agileburo.anytype.domain.block.model.Block -import com.agileburo.anytype.domain.block.repo.BlockRepository - -class ObservePage(private val repo: BlockRepository) : - FlowUseCase, BaseUseCase.None>() { - - override fun build(params: BaseUseCase.None?) = repo.observePages() -} \ No newline at end of file diff --git a/domain/src/test/java/com/agileburo/anytype/domain/dashboard/ObserveHomeDashboardTest.kt b/domain/src/test/java/com/agileburo/anytype/domain/dashboard/ObserveHomeDashboardTest.kt deleted file mode 100644 index 967ae229aa..0000000000 --- a/domain/src/test/java/com/agileburo/anytype/domain/dashboard/ObserveHomeDashboardTest.kt +++ /dev/null @@ -1,172 +0,0 @@ -package com.agileburo.anytype.domain.dashboard - -import com.agileburo.anytype.domain.block.model.Block -import com.agileburo.anytype.domain.block.repo.BlockRepository -import com.agileburo.anytype.domain.common.CoroutineTestRule -import com.agileburo.anytype.domain.common.MockDataFactory -import com.agileburo.anytype.domain.dashboard.interactor.ObserveHomeDashboard -import com.agileburo.anytype.domain.dashboard.model.HomeDashboard -import com.agileburo.anytype.domain.event.model.Event -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.stub -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.runBlockingTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.mockito.Mock -import org.mockito.MockitoAnnotations -import kotlin.test.assertTrue - -class ObserveHomeDashboardTest { - - @ExperimentalCoroutinesApi - @get:Rule - var rule = CoroutineTestRule() - - lateinit var useCase: ObserveHomeDashboard - - @Mock - lateinit var repo: BlockRepository - - @Before - fun before() { - MockitoAnnotations.initMocks(this) - useCase = ObserveHomeDashboard( - context = TestCoroutineDispatcher(), - repo = repo - ) - } - - @Test - fun `should ignore other events`() = runBlockingTest { - - val id = MockDataFactory.randomUuid() - - val param = ObserveHomeDashboard.Param(id = id) - - val events = flowOf( - Event.Command.UpdateBlockText( - id = MockDataFactory.randomUuid(), - text = MockDataFactory.randomString() - ) - ) - - stubObserveEvents(events) - - val result = mutableListOf() - - useCase.build(param).toList(result) - - assertTrue { result.isEmpty() } - } - - @Test - fun `should process event and map it to home dashboard`() = runBlockingTest { - - val page = Block( - id = MockDataFactory.randomUuid(), - children = emptyList(), - content = Block.Content.Page( - style = Block.Content.Page.Style.EMPTY - ), - fields = Block.Fields( - map = mapOf("name" to MockDataFactory.randomString()) - ) - ) - - val dashboard = Block( - id = MockDataFactory.randomUuid(), - children = listOf(page.id), - content = Block.Content.Dashboard( - type = Block.Content.Dashboard.Type.MAIN_SCREEN - ), - fields = Block.Fields( - map = mapOf("name" to MockDataFactory.randomString()) - ) - ) - - val event = Event.Command.ShowBlock( - rootId = dashboard.id, - blocks = listOf(dashboard, page) - ) - - val param = ObserveHomeDashboard.Param( - id = dashboard.id - ) - - val flow = flowOf(event) - - stubObserveEvents(flow) - - val result = mutableListOf() - val expected = HomeDashboard( - id = dashboard.id, - fields = dashboard.fields, - children = dashboard.children, - blocks = listOf(page), - type = dashboard.content.asDashboard().type - ) - - useCase.build(param).toList(result) - - assertTrue { - result.first() == expected - } - } - - @Test - fun `should ignore events not related to dashboard`() = runBlockingTest { - - val title = Block( - id = MockDataFactory.randomUuid(), - children = emptyList(), - content = Block.Content.Text( - text = MockDataFactory.randomString(), - marks = emptyList(), - style = Block.Content.Text.Style.P - ), - fields = Block.Fields.empty() - ) - - val page = Block( - id = MockDataFactory.randomUuid(), - children = listOf(title.id), - content = Block.Content.Page( - style = Block.Content.Page.Style.SET - ), - fields = Block.Fields( - map = mapOf("name" to MockDataFactory.randomString()) - ) - ) - - val event = Event.Command.ShowBlock( - rootId = page.id, - blocks = listOf(page, title) - ) - - val param = ObserveHomeDashboard.Param( - id = page.id - ) - - val flow = flowOf(event) - - stubObserveEvents(flow) - - val result = mutableListOf() - - useCase.build(param).toList(result) - - assertTrue { result.isEmpty() } - } - - private fun stubObserveEvents(events: Flow) { - repo.stub { - onBlocking { observeEvents() } doReturn events - } - } -} \ No newline at end of file diff --git a/domain/src/test/java/com/agileburo/anytype/domain/dashboard/OpenDashboardTest.kt b/domain/src/test/java/com/agileburo/anytype/domain/dashboard/OpenDashboardTest.kt index 60e389d140..5b5ead0ded 100644 --- a/domain/src/test/java/com/agileburo/anytype/domain/dashboard/OpenDashboardTest.kt +++ b/domain/src/test/java/com/agileburo/anytype/domain/dashboard/OpenDashboardTest.kt @@ -51,7 +51,7 @@ class OpenDashboardTest { fun `should open a home dashboard if there are no params`() = runBlockingTest { val config = Config( - homeDashboardId = MockDataFactory.randomUuid() + home = MockDataFactory.randomUuid() ) repo.stub { @@ -62,8 +62,8 @@ class OpenDashboardTest { verify(repo, times(1)).getConfig() verify(repo, times(1)).openDashboard( - contextId = config.homeDashboardId, - id = config.homeDashboardId + contextId = config.home, + id = config.home ) verifyNoMoreInteractions(repo) } diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt index b8a861d3a8..164e8fb35e 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt @@ -6,6 +6,7 @@ import anytype.model.Models.Account import anytype.model.Models.Block import com.agileburo.anytype.data.auth.model.AccountEntity import com.agileburo.anytype.data.auth.model.BlockEntity +import com.google.protobuf.Struct import com.google.protobuf.Value @@ -93,6 +94,16 @@ fun Block.fields(): BlockEntity.Fields = BlockEntity.Fields().also { result -> } } +fun Struct.fields(): BlockEntity.Fields = BlockEntity.Fields().also { result -> + fieldsMap.forEach { (key, value) -> + result.map[key] = when (val case = value.kindCase) { + Value.KindCase.NUMBER_VALUE -> value.numberValue + Value.KindCase.STRING_VALUE -> value.stringValue + else -> throw IllegalStateException("$case is not supported.") + } + } +} + fun Block.dashboard(): BlockEntity.Content.Dashboard = BlockEntity.Content.Dashboard( type = when { dashboard.style == Block.Content.Dashboard.Style.Archive -> { diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt index 0397a1af1f..fdcfd4fee4 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt @@ -1,292 +1,21 @@ package com.agileburo.anytype.middleware.block -import anytype.Events -import anytype.model.Models -import anytype.model.Models.Block.Content.Dashboard -import anytype.model.Models.Block.Content.Page -import com.agileburo.anytype.data.auth.model.BlockEntity import com.agileburo.anytype.data.auth.model.CommandEntity import com.agileburo.anytype.data.auth.model.ConfigEntity -import com.agileburo.anytype.data.auth.model.EventEntity import com.agileburo.anytype.data.auth.repo.block.BlockRemote -import com.agileburo.anytype.middleware.EventProxy import com.agileburo.anytype.middleware.interactor.Middleware -import com.agileburo.anytype.middleware.link import com.agileburo.anytype.middleware.toMiddleware -import com.google.protobuf.Value -import kotlinx.coroutines.flow.* class BlockMiddleware( - private val middleware: Middleware, - private val events: EventProxy + private val middleware: Middleware ) : BlockRemote { - private val supportedEvents = listOf( - Events.Event.Message.ValueCase.BLOCKSHOW, - Events.Event.Message.ValueCase.BLOCKADD, - Events.Event.Message.ValueCase.BLOCKSETTEXT, - Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS, - Events.Event.Message.ValueCase.BLOCKDELETE - ) - - private val supportedTextStyles = listOf( - Models.Block.Content.Text.Style.Paragraph, - Models.Block.Content.Text.Style.Header1, - Models.Block.Content.Text.Style.Header2, - Models.Block.Content.Text.Style.Header3, - Models.Block.Content.Text.Style.Title - ) - - private val supportedContent = listOf( - Models.Block.ContentCase.DASHBOARD, - Models.Block.ContentCase.PAGE, - Models.Block.ContentCase.LAYOUT - ) - override suspend fun getConfig(): ConfigEntity { return ConfigEntity( homeId = middleware.provideHomeDashboardId() ) } - override fun observeEvents(): Flow = events - .flow() - .filter { event -> - event.messagesList.any { message -> - supportedEvents.contains(message.valueCase) - } - } - .map { event -> - event.messagesList.filter { message -> - supportedEvents.contains(message.valueCase) - }.map { message -> Pair(event.contextId, message) } - - } - .flatMapConcat { event -> event.asFlow() } - .mapNotNull { (context, event) -> - when (event.valueCase) { - Events.Event.Message.ValueCase.BLOCKADD -> { - EventEntity.Command.AddBlock( - blocks = event.blockAdd.blocksList.mapNotNull { block -> - when (block.contentCase) { - Models.Block.ContentCase.DASHBOARD -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList.toList(), - fields = extractFields(block), - content = extractDashboard(block) - ) - } - Models.Block.ContentCase.PAGE -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList.toList(), - fields = extractFields(block), - content = extractPage(block) - ) - } - Models.Block.ContentCase.TEXT -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList.toList(), - fields = extractFields(block), - content = extractText(block) - ) - } - Models.Block.ContentCase.LAYOUT -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList, - fields = extractFields(block), - content = extractLayout(block) - ) - } - Models.Block.ContentCase.LINK -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList, - fields = extractFields(block), - content = block.link() - ) - } - else -> { - null - } - } - } - ) - } - Events.Event.Message.ValueCase.BLOCKSHOW -> { - EventEntity.Command.ShowBlock( - rootId = event.blockShow.rootId, - blocks = event.blockShow.blocksList.mapNotNull { block -> - when (block.contentCase) { - Models.Block.ContentCase.DASHBOARD -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList.toList(), - fields = extractFields(block), - content = extractDashboard(block) - ) - } - Models.Block.ContentCase.PAGE -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList.toList(), - fields = extractFields(block), - content = extractPage(block) - ) - } - Models.Block.ContentCase.TEXT -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList.toList(), - fields = extractFields(block), - content = extractText(block) - ) - } - Models.Block.ContentCase.LAYOUT -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList, - fields = extractFields(block), - content = extractLayout(block) - ) - } - Models.Block.ContentCase.LINK -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList, - fields = extractFields(block), - content = block.link() - ) - } - else -> { - null - } - } - } - ) - } - Events.Event.Message.ValueCase.BLOCKSETTEXT -> { - EventEntity.Command.UpdateBlockText( - id = event.blockSetText.id, - text = event.blockSetText.text.value - ) - } - Events.Event.Message.ValueCase.BLOCKDELETE -> { - EventEntity.Command.DeleteBlock( - target = event.blockDelete.blockId - ) - } - Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS -> { - EventEntity.Command.UpdateStructure( - id = event.blockSetChildrenIds.id, - children = event.blockSetChildrenIds.childrenIdsList.toList(), - context = context - ) - } - else -> null - } - } - - override fun observeBlocks() = events - .flow() - .filter { event -> - event.messagesList.any { message -> - message.valueCase == Events.Event.Message.ValueCase.BLOCKSHOW - } - } - .map { event -> - event.messagesList.filter { message -> - message.valueCase == Events.Event.Message.ValueCase.BLOCKSHOW - } - } - .flatMapConcat { event -> event.asFlow() } - .map { event -> - event.blockShow.blocksList - .filter { block -> supportedContent.contains(block.contentCase) } - .map { block -> - when (block.contentCase) { - Models.Block.ContentCase.DASHBOARD -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList.toList(), - fields = extractFields(block), - content = extractDashboard(block) - ) - } - Models.Block.ContentCase.PAGE -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList.toList(), - fields = extractFields(block), - content = extractPage(block) - ) - } - Models.Block.ContentCase.TEXT -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList.toList(), - fields = extractFields(block), - content = extractText(block) - ) - } - Models.Block.ContentCase.LAYOUT -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList, - fields = extractFields(block), - content = extractLayout(block) - ) - } - /* - Models.Block.ContentCase.IMAGE -> { - BlockEntity( - id = block.id, - children = block.childrenIdsList, - fields = extractFields(block), - content = BlockEntity.Content.Image( - path = block.image.localFilePath - ) - ) - } - */ - else -> { - throw IllegalStateException("Unexpected content: ${block.contentCase}") - } - } - - } - } - - override fun observePages() = events - .flow() - .filter { event -> - event.messagesList.any { message -> - message.valueCase == Events.Event.Message.ValueCase.BLOCKSHOW - } - } - .map { event -> - event.messagesList.filter { message -> - message.valueCase == Events.Event.Message.ValueCase.BLOCKSHOW - } - } - .flatMapConcat { event -> event.asFlow() } - .map { event -> - event.blockShow.blocksList - .filter { block -> block.contentCase == Models.Block.ContentCase.TEXT } - .filter { block -> supportedTextStyles.contains(block.text.style) } - .map { block -> - BlockEntity( - id = block.id, - children = block.childrenIdsList.toList(), - fields = extractFields(block), - content = extractText(block) - ) - } - } - override suspend fun openDashboard(contextId: String, id: String) { middleware.openDashboard(contextId, id) } @@ -305,18 +34,6 @@ class BlockMiddleware( middleware.closePage(id) } - private fun extractFields(block: Models.Block): BlockEntity.Fields { - return BlockEntity.Fields().also { fields -> - block.fields.fieldsMap.mapValues { (key, value) -> - fields.map[key] = when (val case = value.kindCase) { - Value.KindCase.NUMBER_VALUE -> value.numberValue - Value.KindCase.STRING_VALUE -> value.stringValue - else -> throw IllegalStateException("$case is not supported.") - } - } - } - } - override suspend fun updateText(command: CommandEntity.UpdateText) { middleware.updateText( command.contextId, @@ -361,102 +78,4 @@ class BlockMiddleware( override suspend fun unlink(command: CommandEntity.Unlink) { middleware.unlink(command) } - - private fun extractDashboard(block: Models.Block): BlockEntity.Content.Dashboard { - return BlockEntity.Content.Dashboard( - type = when { - block.dashboard.style == Dashboard.Style.Archive -> { - BlockEntity.Content.Dashboard.Type.ARCHIVE - } - block.dashboard.style == Dashboard.Style.MainScreen -> { - BlockEntity.Content.Dashboard.Type.MAIN_SCREEN - } - else -> throw IllegalStateException("Unexpected dashboard style: ${block.dashboard.style}") - } - ) - } - - private fun extractPage(block: Models.Block): BlockEntity.Content.Page { - return BlockEntity.Content.Page( - style = when { - block.page.style == Page.Style.Empty -> { - BlockEntity.Content.Page.Style.EMPTY - } - block.page.style == Page.Style.Task -> { - BlockEntity.Content.Page.Style.TASK - } - block.page.style == Page.Style.Set -> { - BlockEntity.Content.Page.Style.SET - } - else -> throw IllegalStateException("Unexpected page style: ${block.page.style}") - } - ) - } - - private fun extractText(block: Models.Block): BlockEntity.Content.Text { - return BlockEntity.Content.Text( - text = block.text.text, - marks = block.text.marks.marksList.map { mark -> - BlockEntity.Content.Text.Mark( - range = IntRange(mark.range.from, mark.range.to), - param = if (mark.param.isNotEmpty()) mark.param else null, - type = when (mark.type) { - Models.Block.Content.Text.Mark.Type.Bold -> { - BlockEntity.Content.Text.Mark.Type.BOLD - } - Models.Block.Content.Text.Mark.Type.Italic -> { - BlockEntity.Content.Text.Mark.Type.ITALIC - } - Models.Block.Content.Text.Mark.Type.Strikethrough -> { - BlockEntity.Content.Text.Mark.Type.STRIKETHROUGH - } - Models.Block.Content.Text.Mark.Type.Underscored -> { - BlockEntity.Content.Text.Mark.Type.UNDERSCORED - } - Models.Block.Content.Text.Mark.Type.Keyboard -> { - BlockEntity.Content.Text.Mark.Type.KEYBOARD - } - Models.Block.Content.Text.Mark.Type.TextColor -> { - BlockEntity.Content.Text.Mark.Type.TEXT_COLOR - } - Models.Block.Content.Text.Mark.Type.BackgroundColor -> { - BlockEntity.Content.Text.Mark.Type.BACKGROUND_COLOR - } - Models.Block.Content.Text.Mark.Type.Link -> { - BlockEntity.Content.Text.Mark.Type.LINK - } - else -> throw IllegalStateException("Unexpected mark type: ${mark.type.name}") - } - ) - }, - style = when (block.text.style) { - Models.Block.Content.Text.Style.Paragraph -> BlockEntity.Content.Text.Style.P - Models.Block.Content.Text.Style.Header1 -> BlockEntity.Content.Text.Style.H1 - Models.Block.Content.Text.Style.Header2 -> BlockEntity.Content.Text.Style.H2 - Models.Block.Content.Text.Style.Header3 -> BlockEntity.Content.Text.Style.H3 - Models.Block.Content.Text.Style.Title -> BlockEntity.Content.Text.Style.TITLE - Models.Block.Content.Text.Style.Quote -> BlockEntity.Content.Text.Style.QUOTE - Models.Block.Content.Text.Style.Marked -> BlockEntity.Content.Text.Style.BULLET - Models.Block.Content.Text.Style.Numbered -> BlockEntity.Content.Text.Style.NUMBERED - Models.Block.Content.Text.Style.Toggle -> BlockEntity.Content.Text.Style.TOGGLE - Models.Block.Content.Text.Style.Checkbox -> BlockEntity.Content.Text.Style.CHECKBOX - else -> throw IllegalStateException("Unexpected text style: ${block.text.style}") - }, - isChecked = block.text.checked - ) - } - - private fun extractLayout(block: Models.Block): BlockEntity.Content.Layout { - return BlockEntity.Content.Layout( - type = when { - block.layout.style == Models.Block.Content.Layout.Style.Column -> { - BlockEntity.Content.Layout.Type.COLUMN - } - block.layout.style == Models.Block.Content.Layout.Style.Row -> { - BlockEntity.Content.Layout.Type.ROW - } - else -> throw IllegalStateException("Unexpected layout style: ${block.layout.style}") - } - ) - } } \ No newline at end of file diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java index f82b8d22d0..0f4193c074 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java @@ -114,6 +114,8 @@ public class Middleware { .setBlockId(id) .build(); + Timber.d("Opening home dashboard with the following request:\n%s", request.toString()); + service.blockOpen(request); } diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventChannel.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventChannel.kt index 229c321011..2ca2c15e51 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventChannel.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventChannel.kt @@ -6,6 +6,7 @@ import com.agileburo.anytype.data.auth.model.EventEntity import com.agileburo.anytype.middleware.EventProxy import com.agileburo.anytype.middleware.blocks import com.agileburo.anytype.middleware.entity +import com.agileburo.anytype.middleware.fields import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map @@ -23,61 +24,82 @@ class MiddlewareEventChannel( Events.Event.Message.ValueCase.BLOCKADD, Events.Event.Message.ValueCase.BLOCKSETTEXT, Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS, - Events.Event.Message.ValueCase.BLOCKDELETE + Events.Event.Message.ValueCase.BLOCKDELETE, + Events.Event.Message.ValueCase.BLOCKSETLINK ) - override fun observeEvents(): Flow> = events + override fun observeEvents( + context: String? + ): Flow> = events .flow() + .filter { context == null || it.contextId == context } .map { event -> event.messagesList.filter { message -> supportedEvents.contains(message.valueCase) }.map { message -> Pair(event.contextId, message) } } .filter { it.isNotEmpty() } - .map { events -> - events.mapNotNull { (context, event) -> - when (event.valueCase) { - Events.Event.Message.ValueCase.BLOCKADD -> { - EventEntity.Command.AddBlock( - blocks = event.blockAdd.blocksList.blocks() - ) - } - Events.Event.Message.ValueCase.BLOCKSHOW -> { - EventEntity.Command.ShowBlock( - rootId = event.blockShow.rootId, - blocks = event.blockShow.blocksList.blocks() - ) - } - Events.Event.Message.ValueCase.BLOCKSETTEXT -> { - EventEntity.Command.GranularChange( - id = event.blockSetText.id, - text = if (event.blockSetText.hasText()) - event.blockSetText.text.value - else null, - style = if (event.blockSetText.hasStyle()) - event.blockSetText.style.value.entity() - else - null, - color = if (event.blockSetText.hasColor()) - event.blockSetText.color.value - else - null - ) - } - Events.Event.Message.ValueCase.BLOCKDELETE -> { - EventEntity.Command.DeleteBlock( - target = event.blockDelete.blockId - ) - } - Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS -> { - EventEntity.Command.UpdateStructure( - id = event.blockSetChildrenIds.id, - children = event.blockSetChildrenIds.childrenIdsList.toList(), - context = context - ) - } - else -> null + .map { events -> processEvents(events) } + + private fun processEvents(events: List>): List { + return events.mapNotNull { (context, event) -> + when (event.valueCase) { + Events.Event.Message.ValueCase.BLOCKADD -> { + EventEntity.Command.AddBlock( + context = context, + blocks = event.blockAdd.blocksList.blocks() + ) } + Events.Event.Message.ValueCase.BLOCKSHOW -> { + EventEntity.Command.ShowBlock( + context = context, + rootId = event.blockShow.rootId, + blocks = event.blockShow.blocksList.blocks() + ) + } + Events.Event.Message.ValueCase.BLOCKSETTEXT -> { + EventEntity.Command.GranularChange( + context = context, + id = event.blockSetText.id, + text = if (event.blockSetText.hasText()) + event.blockSetText.text.value + else null, + style = if (event.blockSetText.hasStyle()) + event.blockSetText.style.value.entity() + else + null, + color = if (event.blockSetText.hasColor()) + event.blockSetText.color.value + else + null + ) + } + Events.Event.Message.ValueCase.BLOCKDELETE -> { + EventEntity.Command.DeleteBlock( + context = context, + target = event.blockDelete.blockId + ) + } + Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS -> { + EventEntity.Command.UpdateStructure( + context = context, + id = event.blockSetChildrenIds.id, + children = event.blockSetChildrenIds.childrenIdsList.toList() + ) + } + Events.Event.Message.ValueCase.BLOCKSETLINK -> { + EventEntity.Command.LinkGranularChange( + context = context, + id = event.blockSetLink.id, + target = event.blockSetLink.targetBlockId.value, + fields = if (event.blockSetLink.hasFields()) + event.blockSetLink.fields.value.fields() + else + null + ) + } + else -> null } } + } } \ No newline at end of file diff --git a/middleware/src/test/java/com/agileburo/anytype/MiddlewareEventChannelTest.kt b/middleware/src/test/java/com/agileburo/anytype/MiddlewareEventChannelTest.kt new file mode 100644 index 0000000000..dc842bda3d --- /dev/null +++ b/middleware/src/test/java/com/agileburo/anytype/MiddlewareEventChannelTest.kt @@ -0,0 +1,153 @@ +package com.agileburo.anytype + +import anytype.Events.Event +import anytype.Events.Event.Message +import com.agileburo.anytype.common.MockDataFactory +import com.agileburo.anytype.data.auth.model.EventEntity +import com.agileburo.anytype.middleware.EventProxy +import com.agileburo.anytype.middleware.interactor.MiddlewareEventChannel +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.stub +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import kotlin.test.assertEquals + +class MiddlewareEventChannelTest { + + @Mock + lateinit var proxy: EventProxy + + private lateinit var channel: MiddlewareEventChannel + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + channel = MiddlewareEventChannel(proxy) + } + + @Test + fun `should filter event by context and pass it downstream`() { + + val context = MockDataFactory.randomUuid() + + val msg = Event.Block.Show + .newBuilder() + .setRootId(context) + .addAllBlocks(emptyList()) + .build() + + val message = Message + .newBuilder() + .setBlockShow(msg) + + val event = Event + .newBuilder() + .setContextId(context) + .addMessages(message) + .build() + + proxy.stub { + on { flow() } doReturn flowOf(event) + } + + val expected = listOf( + EventEntity.Command.ShowBlock( + rootId = context, + blocks = emptyList(), + context = context + ) + ) + + runBlocking { + channel.observeEvents(context = context).collect { events -> + assertEquals( + expected = expected, + actual = events + ) + } + } + } + + @Test + fun `should filter event by context and do not pass it downstream`() { + + val context = MockDataFactory.randomUuid() + + val msg = Event.Block.Show + .newBuilder() + .setRootId(MockDataFactory.randomString()) + .addAllBlocks(emptyList()) + .build() + + val message = Message + .newBuilder() + .setBlockShow(msg) + + val event = Event + .newBuilder() + .setContextId(MockDataFactory.randomUuid()) + .addMessages(message) + .build() + + proxy.stub { + on { flow() } doReturn flowOf(event) + } + + runBlocking { + channel.observeEvents(context = context).collect { events -> + assertEquals( + expected = emptyList(), + actual = events + ) + } + } + } + + @Test + fun `should pass event downstream if context for filtering is not provided`() { + + val context = MockDataFactory.randomUuid() + + val msg = Event.Block.Show + .newBuilder() + .setRootId(context) + .addAllBlocks(emptyList()) + .build() + + val message = Message + .newBuilder() + .setBlockShow(msg) + + val event = Event + .newBuilder() + .setContextId(context) + .addMessages(message) + .build() + + proxy.stub { + on { flow() } doReturn flowOf(event) + } + + val expected = listOf( + EventEntity.Command.ShowBlock( + rootId = context, + blocks = emptyList(), + context = context + ) + ) + + runBlocking { + channel.observeEvents(context = context).collect { events -> + assertEquals( + expected = expected, + actual = events + ) + } + } + } +} \ No newline at end of file diff --git a/middleware/src/test/java/com/agileburo/anytype/common/MockDataFactory.kt b/middleware/src/test/java/com/agileburo/anytype/common/MockDataFactory.kt new file mode 100644 index 0000000000..acbebff1d8 --- /dev/null +++ b/middleware/src/test/java/com/agileburo/anytype/common/MockDataFactory.kt @@ -0,0 +1,64 @@ +package com.agileburo.anytype.common + +import java.util.* +import java.util.concurrent.ThreadLocalRandom + +object MockDataFactory { + + fun randomUuid(): String { + return UUID.randomUUID().toString() + } + + fun randomString(): String { + return randomUuid() + } + + + fun randomInt(): Int { + return ThreadLocalRandom.current().nextInt(0, 1000 + 1) + } + + fun randomInt(max: Int): Int { + return ThreadLocalRandom.current().nextInt(0, max) + } + + fun randomLong(): Long { + return randomInt().toLong() + } + + fun randomFloat(): Float { + return randomInt().toFloat() + } + + fun randomDouble(): Double { + return randomInt().toDouble() + } + + fun randomBoolean(): Boolean { + return Math.random() < 0.5 + } + + fun makeIntList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomInt()) + } + return items + } + + fun makeStringList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomUuid()) + } + return items + } + + fun makeDoubleList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomDouble()) + } + return items + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/HomeDashboardStateMachine.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/HomeDashboardStateMachine.kt new file mode 100644 index 0000000000..495db4b711 --- /dev/null +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/HomeDashboardStateMachine.kt @@ -0,0 +1,139 @@ +package com.agileburo.anytype.presentation.desktop + +import com.agileburo.anytype.domain.block.model.Block +import com.agileburo.anytype.domain.dashboard.model.HomeDashboard +import com.agileburo.anytype.presentation.common.StateReducer +import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.launch + +/** + * State machine for this view model consisting of [Interactor], [State], [Event] and [Reducer] + * It reduces [Event] to the immutable [State] by applying [Reducer] fuction. + * This [State] then will be rendered. + */ +sealed class HomeDashboardStateMachine { + + class Interactor( + private val scope: CoroutineScope, + private val reducer: Reducer = Reducer(), + private val channel: Channel = Channel(), + private val events: Flow = channel.consumeAsFlow() + ) { + fun onEvent(event: Event) = scope.launch { channel.send(event) } + fun state(): Flow = events.scan(State.init(), reducer.function) + } + + /** + * @property isInitialized whether this state is initialized + * @property isLoading whether the data is being loaded to prepare a new state + * @property error if present, represents an error occured in this state machine + * @property dashboard current dashboard data state that should be rendered + */ + data class State( + val isInitialzed: Boolean, + val isLoading: Boolean, + val error: String?, + val dashboard: HomeDashboard? + ) : HomeDashboardStateMachine() { + companion object { + fun init() = State( + isInitialzed = true, + isLoading = false, + error = null, + dashboard = null + ) + } + } + + sealed class Event : HomeDashboardStateMachine() { + + data class OnDashboardLoaded( + val dashboard: HomeDashboard + ) : Event() + + data class OnBlocksAdded( + val blocks: List + ) : Event() + + data class OnStructureUpdated( + val children: List + ) : Event() + + data class OnLinkFieldsChanged( + val id: String, + val fields: Block.Fields + ) : Event() + + object OnDashboardLoadingStarted : Event() + + object OnStartedCreatingPage : Event() + + object OnFinishedCreatingPage : Event() + } + + class Reducer : StateReducer { + + override val function: suspend (State, Event) -> State + get() = { state, event -> reduce(state, event) } + + override suspend fun reduce( + state: State, event: Event + ) = when (event) { + is Event.OnDashboardLoadingStarted -> state.copy( + isInitialzed = true, + isLoading = true, + error = null, + dashboard = null + ) + is Event.OnDashboardLoaded -> state.copy( + isInitialzed = true, + isLoading = false, + error = null, + dashboard = event.dashboard + ) + is Event.OnStartedCreatingPage -> state.copy( + isLoading = true + ) + is Event.OnFinishedCreatingPage -> state.copy( + isLoading = false + ) + is Event.OnStructureUpdated -> state.copy( + isInitialzed = true, + isLoading = false, + dashboard = state.dashboard?.copy( + children = event.children + ) + ) + is Event.OnBlocksAdded -> state.copy( + isInitialzed = true, + isLoading = false, + dashboard = state.dashboard?.let { dashboard -> + dashboard.copy(blocks = dashboard.blocks + event.blocks) + } + ) + is Event.OnLinkFieldsChanged -> state.copy( + dashboard = state.dashboard?.let { dashboard -> + dashboard.copy( + blocks = dashboard.blocks.map { block -> + if (block.id == event.id) { + val link = block.content.asLink() + block.copy( + content = link.copy( + fields = event.fields + ) + ) + } else { + block + } + } + ) + } + ) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/HomeDashboardViewModel.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/HomeDashboardViewModel.kt index c5ab72ee96..63e004d2c1 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/HomeDashboardViewModel.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/HomeDashboardViewModel.kt @@ -10,27 +10,25 @@ import com.agileburo.anytype.domain.auth.interactor.GetCurrentAccount import com.agileburo.anytype.domain.auth.model.Account import com.agileburo.anytype.domain.base.BaseUseCase import com.agileburo.anytype.domain.block.interactor.DragAndDrop -import com.agileburo.anytype.domain.block.model.Block import com.agileburo.anytype.domain.block.model.Position import com.agileburo.anytype.domain.config.GetConfig import com.agileburo.anytype.domain.dashboard.interactor.CloseDashboard -import com.agileburo.anytype.domain.dashboard.interactor.ObserveHomeDashboard import com.agileburo.anytype.domain.dashboard.interactor.OpenDashboard -import com.agileburo.anytype.domain.dashboard.model.HomeDashboard -import com.agileburo.anytype.domain.event.interactor.ObserveEvents +import com.agileburo.anytype.domain.dashboard.interactor.toHomeDashboard +import com.agileburo.anytype.domain.event.interactor.InterceptEvents import com.agileburo.anytype.domain.event.model.Event import com.agileburo.anytype.domain.image.LoadImage import com.agileburo.anytype.domain.page.CreatePage -import com.agileburo.anytype.presentation.common.StateReducer -import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModel.Machine.* +import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine.Interactor +import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine.State import com.agileburo.anytype.presentation.navigation.AppNavigation import com.agileburo.anytype.presentation.navigation.SupportNavigation import com.agileburo.anytype.presentation.profile.ProfileView -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine as Machine class HomeDashboardViewModel( private val loadImage: LoadImage, @@ -38,10 +36,9 @@ class HomeDashboardViewModel( private val openDashboard: OpenDashboard, private val closeDashboard: CloseDashboard, private val createPage: CreatePage, - private val observeHomeDashboard: ObserveHomeDashboard, private val getConfig: GetConfig, private val dragAndDrop: DragAndDrop, - private val observeEvents: ObserveEvents + private val interceptEvents: InterceptEvents ) : ViewStateViewModel(), SupportNavigation> { @@ -61,23 +58,55 @@ class HomeDashboardViewModel( init { startProcessingState() - startObservingEvents() proceedWithGettingConfig() } + private fun startProcessingState() { viewModelScope.launch { machine.state().collect { stateData.postValue(it) } } } - private fun startObservingEvents() { - observeEvents - .build() - .onEach { Timber.d("New event: $it") } - .onEach { event -> - if (event is Event.Command.UpdateStructure) - machine.onEvent(Machine.Event.OnStructureUpdated(event.children)) - else if (event is Event.Command.AddBlock) - machine.onEvent(Machine.Event.OnBlocksAdded(event.blocks)) + private fun startInterceptingEvents(context: String) { + // TODO use context when middleware is ready + interceptEvents + .build(InterceptEvents.Params(context = null)) + .onEach { Timber.d("New events: $it") } + .onEach { events -> + events.forEach { event -> + when (event) { + is Event.Command.UpdateStructure -> machine.onEvent( + Machine.Event.OnStructureUpdated( + event.children + ) + ) + is Event.Command.AddBlock -> machine.onEvent( + Machine.Event.OnBlocksAdded( + event.blocks + ) + ) + is Event.Command.ShowBlock -> { + if (event.rootId == context) { + machine.onEvent( + Machine.Event.OnDashboardLoaded( + dashboard = event.blocks.toHomeDashboard(id = context) + ) + ) + } else { + Timber.e("Receiving event from other context!") + } + } + is Event.Command.LinkGranularChange -> { + event.fields?.let { fields -> + machine.onEvent( + Machine.Event.OnLinkFieldsChanged( + id = event.id, + fields = fields + ) + ) + } + } + } + } } .launchIn(viewModelScope) } @@ -85,22 +114,15 @@ class HomeDashboardViewModel( private fun proceedWithGettingConfig() { getConfig.invoke(viewModelScope, Unit) { result -> result.either( - fnR = { - startObservingHomeDashboard(id = it.homeDashboardId) - processDragAndDrop(context = it.homeDashboardId) + fnR = { config -> + startInterceptingEvents(context = config.home) + processDragAndDrop(context = config.home) }, fnL = { Timber.e(it, "Error while getting config") } ) } } - private fun startObservingHomeDashboard(id: String) { - observeHomeDashboard - .build(ObserveHomeDashboard.Param(id)) - .onEach { machine.onEvent(Machine.Event.OnDashboardLoaded(it)) } - .launchIn(viewModelScope) - } - private fun proceedWithGettingAccount() { getCurrentAccount.invoke(viewModelScope, BaseUseCase.None) { result -> result.either( @@ -118,7 +140,7 @@ class HomeDashboardViewModel( dropChanges .withLatestFrom(movementChanges) { a, b -> Pair(a, b) } .onEach { Timber.d("Dnd request: $it") } - .map { (subject, movement) -> + .mapLatest { (subject, movement) -> DragAndDrop.Params( context = context, targetContext = context, @@ -140,7 +162,12 @@ class HomeDashboardViewModel( private fun proceedWithOpeningHomeDashboard() { machine.onEvent(Machine.Event.OnDashboardLoadingStarted) - openDashboard.invoke(viewModelScope, null) { result -> + Timber.d("Opening home dashboard") + // TODO replace params = null by more explicit code + openDashboard.invoke( + scope = viewModelScope, + params = null + ) { result -> result.either( fnL = { Timber.e(it, "Error while opening home dashboard") }, fnR = { Timber.d("Home dashboard opened") } @@ -239,107 +266,4 @@ class HomeDashboardViewModel( val target: String, val direction: Position ) - - /** - * State machine for this view model consisting of [Interactor], [State], [Event] and [Reducer] - * It reduces [Event] to the immutable [State] by applying [Reducer] fuction. - * This [State] then will be rendered. - */ - sealed class Machine { - - class Interactor( - private val scope: CoroutineScope, - private val reducer: Reducer = Reducer(), - private val channel: Channel = Channel(), - private val events: Flow = channel.consumeAsFlow() - ) { - fun onEvent(event: Event) = scope.launch { channel.send(event) } - fun state(): Flow = events.scan(State.init(), reducer.function) - } - - /** - * @property isInitialized whether this state is initialized - * @property isLoading whether the data is being loaded to prepare a new state - * @property error if present, represents an error occured in this state machine - * @property homeDashboard current dashboard data state that should be rendered - */ - data class State( - val isInitialzed: Boolean, - val isLoading: Boolean, - val error: String?, - val homeDashboard: HomeDashboard? - ) : Machine() { - companion object { - fun init() = State( - isInitialzed = true, - isLoading = false, - error = null, - homeDashboard = null - ) - } - } - - sealed class Event : Machine() { - data class OnDashboardLoaded( - val dashboard: HomeDashboard - ) : Event() - - data class OnBlocksAdded( - val blocks: List - ) : Event() - - data class OnStructureUpdated( - val children: List - ) : Event() - - object OnDashboardLoadingStarted : Event() - - object OnStartedCreatingPage : Event() - - object OnFinishedCreatingPage : Event() - } - - class Reducer : StateReducer { - - override val function: suspend (State, Event) -> State - get() = { state, event -> reduce(state, event) } - - override suspend fun reduce( - state: State, event: Event - ) = when (event) { - is Event.OnDashboardLoadingStarted -> state.copy( - isInitialzed = true, - isLoading = true, - error = null, - homeDashboard = null - ) - is Event.OnDashboardLoaded -> state.copy( - isInitialzed = true, - isLoading = false, - error = null, - homeDashboard = event.dashboard - ) - is Event.OnStartedCreatingPage -> state.copy( - isLoading = true - ) - is Event.OnFinishedCreatingPage -> state.copy( - isLoading = false - ) - is Event.OnStructureUpdated -> state.copy( - isInitialzed = true, - isLoading = false, - homeDashboard = state.homeDashboard?.copy( - children = event.children - ) - ) - is Event.OnBlocksAdded -> state.copy( - isInitialzed = true, - isLoading = false, - homeDashboard = state.homeDashboard?.let { dashboard -> - dashboard.copy(blocks = dashboard.blocks + event.blocks) - } - ) - } - } - } } diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/HomeDashboardViewModelFactory.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/HomeDashboardViewModelFactory.kt index 77433105d7..9607113847 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/HomeDashboardViewModelFactory.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/HomeDashboardViewModelFactory.kt @@ -6,9 +6,8 @@ import com.agileburo.anytype.domain.auth.interactor.GetCurrentAccount import com.agileburo.anytype.domain.block.interactor.DragAndDrop import com.agileburo.anytype.domain.config.GetConfig import com.agileburo.anytype.domain.dashboard.interactor.CloseDashboard -import com.agileburo.anytype.domain.dashboard.interactor.ObserveHomeDashboard import com.agileburo.anytype.domain.dashboard.interactor.OpenDashboard -import com.agileburo.anytype.domain.event.interactor.ObserveEvents +import com.agileburo.anytype.domain.event.interactor.InterceptEvents import com.agileburo.anytype.domain.image.LoadImage import com.agileburo.anytype.domain.page.CreatePage @@ -19,9 +18,8 @@ class HomeDashboardViewModelFactory( private val closeDashboard: CloseDashboard, private val createPage: CreatePage, private val getConfig: GetConfig, - private val observeHomeDashboard: ObserveHomeDashboard, private val dnd: DragAndDrop, - private val observeEvents: ObserveEvents + private val interceptEvents: InterceptEvents ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") @@ -33,9 +31,8 @@ class HomeDashboardViewModelFactory( closeDashboard = closeDashboard, createPage = createPage, getConfig = getConfig, - observeHomeDashboard = observeHomeDashboard, dragAndDrop = dnd, - observeEvents = observeEvents + interceptEvents = interceptEvents ) as T } } \ No newline at end of file diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/Machine.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/Machine.kt deleted file mode 100644 index 844918516a..0000000000 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/desktop/Machine.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.agileburo.anytype.presentation.desktop - diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt index 5e162b7351..24b438d735 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt @@ -347,6 +347,7 @@ class PageViewModel( } fun open(id: String) { + pageId = id stateData.postValue(ViewState.Loading) diff --git a/presentation/src/test/java/com/agileburo/anytype/presentation/home/HomeDashboardViewModelTest.kt b/presentation/src/test/java/com/agileburo/anytype/presentation/home/HomeDashboardViewModelTest.kt index b1c172ae7f..d1fb02022e 100644 --- a/presentation/src/test/java/com/agileburo/anytype/presentation/home/HomeDashboardViewModelTest.kt +++ b/presentation/src/test/java/com/agileburo/anytype/presentation/home/HomeDashboardViewModelTest.kt @@ -13,15 +13,15 @@ import com.agileburo.anytype.domain.block.model.Position import com.agileburo.anytype.domain.config.Config import com.agileburo.anytype.domain.config.GetConfig import com.agileburo.anytype.domain.dashboard.interactor.CloseDashboard -import com.agileburo.anytype.domain.dashboard.interactor.ObserveHomeDashboard import com.agileburo.anytype.domain.dashboard.interactor.OpenDashboard +import com.agileburo.anytype.domain.dashboard.interactor.toHomeDashboard import com.agileburo.anytype.domain.dashboard.model.HomeDashboard -import com.agileburo.anytype.domain.event.interactor.ObserveEvents +import com.agileburo.anytype.domain.event.interactor.InterceptEvents import com.agileburo.anytype.domain.event.model.Event import com.agileburo.anytype.domain.image.LoadImage import com.agileburo.anytype.domain.page.CreatePage +import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModel -import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModel.Machine.State import com.agileburo.anytype.presentation.mapper.toView import com.agileburo.anytype.presentation.navigation.AppNavigation import com.agileburo.anytype.presentation.profile.ProfileView @@ -56,9 +56,6 @@ class HomeDashboardViewModelTest { @Mock lateinit var openDashboard: OpenDashboard - @Mock - lateinit var observeHomeDashboard: ObserveHomeDashboard - @Mock lateinit var getConfig: GetConfig @@ -69,7 +66,7 @@ class HomeDashboardViewModelTest { lateinit var createPage: CreatePage @Mock - lateinit var observeEvents: ObserveEvents + lateinit var interceptEvents: InterceptEvents @Mock lateinit var dnd: DragAndDrop @@ -88,23 +85,20 @@ class HomeDashboardViewModelTest { openDashboard = openDashboard, closeDashboard = closeDashboard, createPage = createPage, - observeHomeDashboard = observeHomeDashboard, getConfig = getConfig, dragAndDrop = dnd, - observeEvents = observeEvents + interceptEvents = interceptEvents ) } @Test fun `should only start getting config when view model is initialized`() { - val config = Config(homeDashboardId = MockDataFactory.randomUuid()) - val param = ObserveHomeDashboard.Param(config.homeDashboardId) + val config = Config(home = MockDataFactory.randomUuid()) val response = Either.Right(config) stubGetConfig(response) - stubObserveHomeDashboard(param) - stubObserveEvents() + stubObserveEvents(params = InterceptEvents.Params(context = null)) vm = buildViewModel() @@ -115,35 +109,34 @@ class HomeDashboardViewModelTest { } @Test - fun `should start observing events when view model is initialized`() { + fun `should start observing events after receiving config`() { - val config = Config(homeDashboardId = MockDataFactory.randomUuid()) + val config = Config(home = MockDataFactory.randomUuid()) val response = Either.Right(config) + val params = InterceptEvents.Params(context = null) + stubGetConfig(response) - stubObserveHomeDashboard() - stubObserveEvents() + stubObserveEvents(params = params) vm = buildViewModel() - verify(observeEvents, times(1)).build() + verify(getConfig, times(1)).invoke(any(), any(), any()) + verify(interceptEvents, times(1)).build(params) } @Test fun `should start observing home dashboard after receiving config`() { - val config = Config(homeDashboardId = MockDataFactory.randomUuid()) + val config = Config(home = MockDataFactory.randomUuid()) val response = Either.Right(config) - val param = ObserveHomeDashboard.Param(id = config.homeDashboardId) stubGetConfig(response) - stubObserveHomeDashboard() - stubObserveEvents() + stubObserveEvents(params = InterceptEvents.Params(context = null)) vm = buildViewModel() verify(getConfig, times(1)).invoke(any(), any(), any()) - verify(observeHomeDashboard, times(1)).build(param) verifyZeroInteractions(openDashboard) verifyZeroInteractions(loadImage) verifyZeroInteractions(getCurrentAccount) @@ -152,20 +145,19 @@ class HomeDashboardViewModelTest { @Test fun `should emit loading state when home dashboard loading started`() { - val config = Config(homeDashboardId = MockDataFactory.randomUuid()) + val config = Config(home = MockDataFactory.randomUuid()) stubGetConfig(Either.Right(config)) - stubObserveHomeDashboard() - stubObserveEvents() + stubObserveEvents(params = InterceptEvents.Params(context = null)) vm = buildViewModel() vm.onViewCreated() - val expected = State( + val expected = HomeDashboardStateMachine.State( isLoading = true, isInitialzed = true, - homeDashboard = null, + dashboard = null, error = null ) @@ -175,7 +167,7 @@ class HomeDashboardViewModelTest { @Test fun `should emit view state with dashboard when home dashboard loading started`() { - val config = Config(homeDashboardId = MockDataFactory.randomUuid()) + val config = Config(home = MockDataFactory.randomUuid()) val page = Block( id = MockDataFactory.randomUuid(), @@ -186,27 +178,35 @@ class HomeDashboardViewModelTest { ) ) - val dashboard = HomeDashboard( - id = config.homeDashboardId, - fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())), - type = Block.Content.Dashboard.Type.MAIN_SCREEN, - blocks = listOf(page), - children = listOf(page.id) + val dashboard = Block( + id = config.home, + content = Block.Content.Dashboard( + type = Block.Content.Dashboard.Type.MAIN_SCREEN + ), + children = listOf(page.id), + fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())) ) val delayInMillis = 100L - val flow = flow { + val events = flow { delay(delayInMillis) - emit(dashboard) + emit( + listOf( + Event.Command.ShowBlock( + rootId = config.home, + context = config.home, + blocks = listOf(dashboard, page) + ) + ) + ) } stubGetConfig(Either.Right(config)) - stubObserveHomeDashboard( - flow = flow, - param = ObserveHomeDashboard.Param(id = dashboard.id) + stubObserveEvents( + params = InterceptEvents.Params(context = null), + flow = events ) - stubObserveEvents() stubOpenDashboard() vm = buildViewModel() @@ -214,10 +214,10 @@ class HomeDashboardViewModelTest { vm.onViewCreated() vm.state.test().assertValue( - State( + HomeDashboardStateMachine.State( isLoading = true, isInitialzed = true, - homeDashboard = null, + dashboard = null, error = null ) ) @@ -225,10 +225,10 @@ class HomeDashboardViewModelTest { coroutineTestRule.advanceTime(delayInMillis) vm.state.test().assertValue( - State( + HomeDashboardStateMachine.State( isLoading = false, isInitialzed = true, - homeDashboard = dashboard, + dashboard = listOf(dashboard, page).toHomeDashboard(dashboard.id), error = null ) ) @@ -237,48 +237,62 @@ class HomeDashboardViewModelTest { @Test fun `block dragging events do not alter overall state`() { - val config = Config(homeDashboardId = MockDataFactory.randomUuid()) + val config = Config(home = MockDataFactory.randomUuid()) val pages = listOf( Block( id = MockDataFactory.randomUuid(), children = emptyList(), fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())), - content = Block.Content.Page( - style = Block.Content.Page.Style.SET + content = Block.Content.Link( + target = MockDataFactory.randomUuid(), + type = Block.Content.Link.Type.PAGE, + fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())), + isArchived = MockDataFactory.randomBoolean() ) ), Block( id = MockDataFactory.randomUuid(), children = emptyList(), fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())), - content = Block.Content.Page( - style = Block.Content.Page.Style.SET + content = Block.Content.Link( + target = MockDataFactory.randomUuid(), + type = Block.Content.Link.Type.PAGE, + fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())), + isArchived = MockDataFactory.randomBoolean() ) ) ) - val dashboard = HomeDashboard( - id = config.homeDashboardId, - fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())), - type = Block.Content.Dashboard.Type.MAIN_SCREEN, - blocks = pages, - children = pages.map { it.id } + val dashboard = Block( + id = config.home, + content = Block.Content.Dashboard( + type = Block.Content.Dashboard.Type.MAIN_SCREEN + ), + children = pages.map { page -> page.id }, + fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())) ) val delayInMillis = 100L - val flow = flow { + val events = flow { delay(delayInMillis) - emit(dashboard) + emit( + listOf( + Event.Command.ShowBlock( + rootId = config.home, + context = config.home, + blocks = listOf(dashboard) + pages + ) + ) + ) } stubGetConfig(Either.Right(config)) - stubObserveHomeDashboard( - flow = flow, - param = ObserveHomeDashboard.Param(id = dashboard.id) + stubObserveEvents( + params = InterceptEvents.Params(context = null), + flow = events ) - stubObserveEvents() stubOpenDashboard() vm = buildViewModel() @@ -287,14 +301,19 @@ class HomeDashboardViewModelTest { coroutineTestRule.advanceTime(delayInMillis) - val expected = State( + val expected = HomeDashboardStateMachine.State( isLoading = false, isInitialzed = true, - homeDashboard = dashboard, + dashboard = listOf( + dashboard, + pages.first(), + pages.last() + ).toHomeDashboard(dashboard.id), error = null ) - val views = dashboard.toView() + val views = + listOf(dashboard, pages.first(), pages.last()).toHomeDashboard(dashboard.id).toView() val from = 0 val to = 1 @@ -314,7 +333,7 @@ class HomeDashboardViewModelTest { @Test fun `should start dispatching drag-and-drop actions when the dragged item is dropped`() { - val config = Config(homeDashboardId = MockDataFactory.randomUuid()) + val config = Config(home = MockDataFactory.randomUuid()) val pages = listOf( Block( @@ -342,7 +361,7 @@ class HomeDashboardViewModelTest { ) val dashboard = HomeDashboard( - id = config.homeDashboardId, + id = config.home, fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())), type = Block.Content.Dashboard.Type.MAIN_SCREEN, blocks = pages, @@ -357,11 +376,7 @@ class HomeDashboardViewModelTest { } stubGetConfig(Either.Right(config)) - stubObserveHomeDashboard( - flow = flow, - param = ObserveHomeDashboard.Param(id = dashboard.id) - ) - stubObserveEvents() + stubObserveEvents(params = InterceptEvents.Params(context = null)) stubOpenDashboard() vm = buildViewModel() @@ -387,8 +402,8 @@ class HomeDashboardViewModelTest { scope = any(), params = eq( DragAndDrop.Params( - context = config.homeDashboardId, - targetContext = config.homeDashboardId, + context = config.home, + targetContext = config.home, targetId = pages.last().content.asLink().target, blockIds = listOf(pages.first().content.asLink().target), position = Position.BOTTOM @@ -401,10 +416,9 @@ class HomeDashboardViewModelTest { @Test fun `should proceed with getting account and opening dashboard when view is created`() { - val config = Config(homeDashboardId = MockDataFactory.randomUuid()) + val config = Config(home = MockDataFactory.randomUuid()) stubGetConfig(Either.Right(config)) - stubObserveHomeDashboard() - stubObserveEvents() + stubObserveEvents(params = InterceptEvents.Params(context = null)) vm = buildViewModel() vm.onViewCreated() @@ -425,8 +439,6 @@ class HomeDashboardViewModelTest { val response = Either.Right(account) - stubObserveHomeDashboard() - stubObserveEvents() stubGetCurrentAccount(response) vm = buildViewModel() @@ -456,7 +468,6 @@ class HomeDashboardViewModelTest { val accountResponse = Either.Right(account) val imageResponse = Either.Right(blob) - stubObserveHomeDashboard() stubObserveEvents() stubGetCurrentAccount(accountResponse) @@ -491,7 +502,6 @@ class HomeDashboardViewModelTest { val accountResponse = Either.Right(account) - stubObserveHomeDashboard() stubObserveEvents() stubGetCurrentAccount(accountResponse) @@ -506,7 +516,6 @@ class HomeDashboardViewModelTest { @Test fun `should start creating page when requested from UI`() { - stubObserveHomeDashboard() stubObserveEvents() vm = buildViewModel() @@ -521,7 +530,6 @@ class HomeDashboardViewModelTest { val id = MockDataFactory.randomUuid() - stubObserveHomeDashboard() stubObserveEvents() closeDashboard.stub { @@ -549,60 +557,64 @@ class HomeDashboardViewModelTest { } @Test - fun `should update state when a new block is added`() { + fun `should update state when a new block is added without updating dashboard children structure`() { - val config = Config(homeDashboardId = MockDataFactory.randomUuid()) + val config = Config(home = "HOME_ID") val page = Block( - id = MockDataFactory.randomUuid(), + id = "FIRST_PAGE_ID", children = emptyList(), - fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())), + fields = Block.Fields(map = mapOf("name" to "FIRST_PAGE")), content = Block.Content.Page( style = Block.Content.Page.Style.SET ) ) val new = Block( - id = MockDataFactory.randomUuid(), + id = "NEW_BLOCK_ID", children = emptyList(), - fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())), + fields = Block.Fields(map = mapOf("name" to "SECOND_PAGE")), content = Block.Content.Page( style = Block.Content.Page.Style.SET ) ) - val dashboard = HomeDashboard( - id = config.homeDashboardId, - fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())), - type = Block.Content.Dashboard.Type.MAIN_SCREEN, - blocks = listOf(page), - children = listOf(page.id) + val dashboardName = "HOME" + + val dashboard = Block( + id = config.home, + content = Block.Content.Dashboard( + type = Block.Content.Dashboard.Type.MAIN_SCREEN + ), + children = listOf(page.id), + fields = Block.Fields(map = mapOf("name" to dashboardName)) ) - val event = Event.Command.AddBlock( - blocks = listOf(new) + val showDashboardEvent = Event.Command.ShowBlock( + rootId = config.home, + context = config.home, + blocks = listOf(dashboard, page) + ) + + val addBlockEvent = Event.Command.AddBlock( + blocks = listOf(new), + context = config.home ) val dashboardDelayInMillis = 100L val eventDelayInMillis = 200L - val dashboardFlow = flow { + val events: Flow> = flow { delay(dashboardDelayInMillis) - emit(dashboard) - } - - val eventFlow = flow { + emit(listOf(showDashboardEvent)) delay(eventDelayInMillis) - emit(event) + emit(listOf(addBlockEvent)) } stubGetConfig(Either.Right(config)) - stubObserveHomeDashboard( - flow = dashboardFlow, - param = ObserveHomeDashboard.Param(id = dashboard.id) - ) stubObserveEvents( - flow = eventFlow + flow = events, + params = InterceptEvents.Params(context = null) ) stubOpenDashboard() @@ -612,11 +624,27 @@ class HomeDashboardViewModelTest { coroutineTestRule.advanceTime(dashboardDelayInMillis) + val firstExpectedState = HomeDashboard( + id = config.home, + fields = Block.Fields(map = mapOf("name" to dashboardName)), + type = Block.Content.Dashboard.Type.MAIN_SCREEN, + blocks = listOf(page), + children = listOf(page.id) + ) + + val secondExpectedState = HomeDashboard( + id = config.home, + fields = Block.Fields(map = mapOf("name" to dashboardName)), + type = Block.Content.Dashboard.Type.MAIN_SCREEN, + blocks = listOf(page, new), + children = listOf(page.id) + ) + vm.state.test().assertValue( - State( + HomeDashboardStateMachine.State( isLoading = false, isInitialzed = true, - homeDashboard = dashboard, + dashboard = firstExpectedState, error = null ) ) @@ -624,26 +652,15 @@ class HomeDashboardViewModelTest { coroutineTestRule.advanceTime(eventDelayInMillis) vm.state.test().assertValue( - State( + HomeDashboardStateMachine.State( isLoading = false, isInitialzed = true, - homeDashboard = dashboard.copy( - blocks = dashboard.blocks + listOf(new) - ), + dashboard = secondExpectedState, error = null ) ) } - private fun stubObserveHomeDashboard( - param: ObserveHomeDashboard.Param = any(), - flow: Flow = flowOf() - ) { - observeHomeDashboard.stub { - onBlocking { build(param) } doReturn flow - } - } - private fun stubGetConfig(response: Either.Right) { getConfig.stub { onBlocking { invoke(any(), any(), any()) } doAnswer { answer -> @@ -652,9 +669,12 @@ class HomeDashboardViewModelTest { } } - private fun stubObserveEvents(flow: Flow = flowOf()) { - observeEvents.stub { - onBlocking { build() } doReturn flow + private fun stubObserveEvents( + flow: Flow> = flowOf(), + params: InterceptEvents.Params? = null + ) { + interceptEvents.stub { + onBlocking { build(params) } doReturn flow } } diff --git a/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt b/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt index d019f754ce..6893d551e3 100644 --- a/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt +++ b/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt @@ -144,7 +144,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -318,7 +319,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -335,7 +337,8 @@ class PageViewModelTest { emit( listOf( Event.Command.AddBlock( - blocks = listOf(added) + blocks = listOf(added), + context = root ) ) ) @@ -396,7 +399,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -405,7 +409,8 @@ class PageViewModelTest { listOf( Event.Command.UpdateBlockText( text = text, - id = child + id = child, + context = root ) ) ) @@ -488,7 +493,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = blocks + blocks = blocks, + context = root ) ) ) @@ -617,7 +623,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = blocks + blocks = blocks, + context = root ) ) ) @@ -750,7 +757,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = blocks + blocks = blocks, + context = root ) ) ) @@ -833,7 +841,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = blocks + blocks = blocks, + context = root ) ) ) @@ -924,7 +933,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = blocks + blocks = blocks, + context = root ) ) ) @@ -1007,7 +1017,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = blocks + blocks = blocks, + context = root ) ) ) @@ -1068,7 +1079,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1130,7 +1142,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1193,7 +1206,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1262,7 +1276,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1328,7 +1343,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1412,7 +1428,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1429,7 +1446,8 @@ class PageViewModelTest { emit( listOf( Event.Command.AddBlock( - blocks = listOf(new) + blocks = listOf(new), + context = root ) ) ) @@ -1482,7 +1500,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1524,7 +1543,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1566,7 +1586,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1616,7 +1637,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1624,7 +1646,8 @@ class PageViewModelTest { emit( listOf( Event.Command.DeleteBlock( - target = firstChild + target = firstChild, + context = root ) ) ) @@ -1680,7 +1703,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1725,7 +1749,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1765,7 +1790,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1812,7 +1838,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1864,7 +1891,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1907,7 +1935,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -1960,7 +1989,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -2006,7 +2036,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -2061,7 +2092,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -2117,7 +2149,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -2173,7 +2206,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) ) @@ -2222,7 +2256,8 @@ class PageViewModelTest { listOf( Event.Command.ShowBlock( rootId = root, - blocks = page + blocks = page, + context = root ) ) )