From e77b98697de90a69991cd5e4132ff8810402e6dd Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Thu, 18 Apr 2024 15:19:16 +0200 Subject: [PATCH] DROID-2345 Multiplayer | Enhancement | Support share limit restrictions (#1134) --- .../di/feature/spaces/SpaceSettingsDI.kt | 2 + .../anytype/di/main/SubscriptionsModule.kt | 35 ++++ .../anytype/core_models/ObjectWrapper.kt | 7 + .../anytype/core_models/Relations.kt | 5 +- .../ActiveSpaceMemberSubscriptionContainer.kt | 117 +++++++++++ .../SpaceViewSubscriptionContainer.kt | 160 ++++++++++++++++ .../spaces/SpaceDeletedStatusWatcher.kt | 3 +- .../SpaceViewSubscriptionContainerTest.kt | 181 ++++++++++++++++++ .../search/ObjectSearchConstants.kt | 5 +- .../spaces/SpaceSettingsViewModel.kt | 86 +++------ .../anytypeio/anytype/core_models/DataView.kt | 16 ++ .../anytype/ui_settings/space/Settings.kt | 23 ++- 12 files changed, 566 insertions(+), 74 deletions(-) create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/ActiveSpaceMemberSubscriptionContainer.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/SpaceViewSubscriptionContainer.kt create mode 100644 domain/src/test/java/com/anytypeio/anytype/domain/multiplayer/SpaceViewSubscriptionContainerTest.kt diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/spaces/SpaceSettingsDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/spaces/SpaceSettingsDI.kt index 6301048580..df3c4c38a0 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/spaces/SpaceSettingsDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/spaces/SpaceSettingsDI.kt @@ -15,6 +15,7 @@ import com.anytypeio.anytype.domain.debugging.DebugSpaceContentSaver import com.anytypeio.anytype.domain.debugging.DebugSpaceShareDownloader import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider @@ -99,4 +100,5 @@ interface SpaceSettingsDependencies : ComponentDependencies { fun config(): ConfigStorage fun context(): Context fun userPermission(): UserPermissionProvider + fun spaceViewSubscriptionContainer(): SpaceViewSubscriptionContainer } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/SubscriptionsModule.kt b/app/src/main/java/com/anytypeio/anytype/di/main/SubscriptionsModule.kt index 3e1827021d..02a82884cc 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/main/SubscriptionsModule.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/main/SubscriptionsModule.kt @@ -1,13 +1,16 @@ package com.anytypeio.anytype.di.main import com.anytypeio.anytype.di.main.ConfigModule.DEFAULT_APP_COROUTINE_SCOPE +import com.anytypeio.anytype.domain.account.AwaitAccountStartManager import com.anytypeio.anytype.domain.auth.repo.AuthRepository import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.config.ConfigStorage import com.anytypeio.anytype.domain.debugging.Logger import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer +import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.DefaultUserPermissionProvider +import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.objects.DefaultStoreOfObjectTypes import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelations @@ -126,4 +129,36 @@ object SubscriptionsModule { repo = repo, logger = logger ) + + @JvmStatic + @Provides + @Singleton + fun spaceViewSubscriptionContainer( + dispatchers: AppCoroutineDispatchers, + @Named(DEFAULT_APP_COROUTINE_SCOPE) scope: CoroutineScope, + container: StorelessSubscriptionContainer, + awaitAccountStartManager: AwaitAccountStartManager + ) : SpaceViewSubscriptionContainer = SpaceViewSubscriptionContainer.Default( + dispatchers = dispatchers, + scope = scope, + container = container, + awaitAccountStart = awaitAccountStartManager + ) + + @JvmStatic + @Provides + @Singleton + fun activeSpaceMemberSubscriptionContainer( + dispatchers: AppCoroutineDispatchers, + @Named(DEFAULT_APP_COROUTINE_SCOPE) scope: CoroutineScope, + container: StorelessSubscriptionContainer, + spaceManager: SpaceManager, + awaitAccountStartManager: AwaitAccountStartManager + ) : ActiveSpaceMemberSubscriptionContainer = ActiveSpaceMemberSubscriptionContainer.Default( + dispatchers = dispatchers, + scope = scope, + container = container, + manager = spaceManager, + awaitAccountStart = awaitAccountStartManager + ) } \ No newline at end of file diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt index bc3909ad5f..dc8de79b85 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt @@ -280,6 +280,13 @@ sealed class ObjectWrapper { val writersLimit: Double? by default val readersLimit: Double? by default + + val sharedSpaceLimit: Int + get() { + val value = getValue(Relations.SHARED_SPACES_LIMIT) + return value?.toInt() ?: 0 + } + } inline fun getValue(relation: Key): T? { diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Relations.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Relations.kt index 2ab91769f4..e14978160d 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Relations.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Relations.kt @@ -89,6 +89,8 @@ object Relations { const val READERS_LIMIT = "readersLimit" const val WRITERS_LIMIT = "writersLimit" + const val SHARED_SPACES_LIMIT = "sharedSpacesLimit" + val systemRelationKeys = listOf( "id", "name", @@ -146,6 +148,7 @@ object Relations { "relationReadonlyValue", "relationDefaultValue", "relationFormatObjectTypes", - "relationOptionColor" + "relationOptionColor", + "sharedSpacesLimit" ) } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/ActiveSpaceMemberSubscriptionContainer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/ActiveSpaceMemberSubscriptionContainer.kt new file mode 100644 index 0000000000..f673c5b923 --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/ActiveSpaceMemberSubscriptionContainer.kt @@ -0,0 +1,117 @@ +package com.anytypeio.anytype.domain.multiplayer + +import com.anytypeio.anytype.core_models.DVFilter +import com.anytypeio.anytype.core_models.DVFilterCondition +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.domain.account.AwaitAccountStartManager +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.library.StoreSearchParams +import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer +import com.anytypeio.anytype.domain.workspace.SpaceManager +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +interface ActiveSpaceMemberSubscriptionContainer { + + fun start() + fun stop() + fun observe() : Flow> + fun get() : List + + class Default @Inject constructor( + private val manager: SpaceManager, + private val container: StorelessSubscriptionContainer, + private val scope: CoroutineScope, + private val dispatchers: AppCoroutineDispatchers, + private val awaitAccountStart: AwaitAccountStartManager + ) : ActiveSpaceMemberSubscriptionContainer { + + private val data = MutableStateFlow>(emptyList()) + private val jobs = mutableListOf() + + init { + scope.launch { + awaitAccountStart.isStarted().collect { isStarted -> + if (isStarted) + start() + else + stop() + } + } + } + + override fun observe(): Flow> { + return data + } + + override fun get(): List { + return data.value + } + + override fun start() { + jobs += scope.launch(dispatchers.io) { + manager + .observe() + .flatMapLatest { config -> + container.subscribe( + StoreSearchParams( + subscription = GLOBAL_SUBSCRIPTION, + filters = buildList { + add( + DVFilter( + relation = Relations.LAYOUT, + value = ObjectType.Layout.PARTICIPANT.code.toDouble(), + condition = DVFilterCondition.EQUAL + ) + ) + add( + DVFilter( + relation = Relations.SPACE_ID, + condition = DVFilterCondition.EQUAL, + value = config.space + ) + ) + }, + limit = 0, + keys = listOf( + Relations.ID, + Relations.SPACE_ID, + Relations.IDENTITY, + Relations.PARTICIPANT_PERMISSIONS, + Relations.PARTICIPANT_STATUS, + Relations.LAYOUT, + Relations.NAME, + Relations.ICON_IMAGE, + ) + ) + ) + }.map { objects -> + objects.map { obj -> + ObjectWrapper.SpaceMember(obj.map) + } + }.collect { + data.value = it + } + } + } + + override fun stop() { + jobs.forEach { it.cancel() } + scope.launch(dispatchers.io) { + container.unsubscribe(listOf(DefaultUserPermissionProvider.GLOBAL_SUBSCRIPTION)) + } + } + + companion object { + const val GLOBAL_SUBSCRIPTION = "subscription.global.active-space-members" + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/SpaceViewSubscriptionContainer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/SpaceViewSubscriptionContainer.kt new file mode 100644 index 0000000000..a90c7631ef --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/SpaceViewSubscriptionContainer.kt @@ -0,0 +1,160 @@ +package com.anytypeio.anytype.domain.multiplayer + +import com.anytypeio.anytype.core_models.DVFilter +import com.anytypeio.anytype.core_models.DVFilterCondition +import com.anytypeio.anytype.core_models.DVSort +import com.anytypeio.anytype.core_models.DVSortType +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.multiplayer.SpaceAccessType +import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.core_models.restrictions.SpaceStatus +import com.anytypeio.anytype.domain.account.AwaitAccountStartManager +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.library.StoreSearchParams +import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch + +interface SpaceViewSubscriptionContainer { + + fun start() + fun stop() + fun observe(): Flow> + fun observe(space: SpaceId) : Flow + + fun get(): List + + class Default @Inject constructor( + private val container: StorelessSubscriptionContainer, + private val scope: CoroutineScope, + private val dispatchers: AppCoroutineDispatchers, + private val awaitAccountStart: AwaitAccountStartManager + ) : SpaceViewSubscriptionContainer { + + private val data = MutableStateFlow>(emptyList()) + private val jobs = mutableListOf() + + init { + scope.launch { + awaitAccountStart.isStarted().collect { isStarted -> + if (isStarted) + start() + else + stop() + } + } + } + + override fun observe(): Flow> { + return data + } + + override fun observe(space: SpaceId): Flow { + return data.mapNotNull { all -> + all.firstOrNull { spaceView -> spaceView.targetSpaceId == space.id } + } + } + + override fun get(): List { + return data.value + } + + override fun start() { + jobs += scope.launch(dispatchers.io) { + container.subscribe( + StoreSearchParams( + subscription = GLOBAL_SUBSCRIPTION, + keys = listOf( + Relations.ID, + Relations.TARGET_SPACE_ID, + Relations.SPACE_ACCOUNT_STATUS, + Relations.SPACE_LOCAL_STATUS, + Relations.SPACE_ACCESS_TYPE, + Relations.SHARED_SPACES_LIMIT, + Relations.READERS_LIMIT, + Relations.WRITERS_LIMIT, + Relations.NAME, + Relations.CREATED_DATE, + Relations.CREATOR, + Relations.ICON_IMAGE, + Relations.ICON_OPTION, + ), + filters = listOf( + DVFilter( + relation = Relations.LAYOUT, + value = ObjectType.Layout.SPACE_VIEW.code.toDouble(), + condition = DVFilterCondition.EQUAL + ), + DVFilter( + relation = Relations.SPACE_ACCOUNT_STATUS, + value = buildList { + add(SpaceStatus.SPACE_DELETED.code.toDouble()) + add(SpaceStatus.SPACE_REMOVING.code.toDouble()) + }, + condition = DVFilterCondition.NOT_IN + ), + DVFilter( + relation = Relations.SPACE_LOCAL_STATUS, + value = SpaceStatus.OK.code.toDouble(), + condition = DVFilterCondition.EQUAL + ) + ), + sorts = listOf( + DVSort( + relationKey = Relations.LAST_OPENED_DATE, + type = DVSortType.DESC, + includeTime = true + ) + ) + ) + ).map { objects -> + objects.map { obj -> + ObjectWrapper.SpaceView(obj.map) + } + }.collect { + data.value = it + } + } + } + + override fun stop() { + jobs.forEach { it.cancel() } + scope.launch(dispatchers.io) { + container.unsubscribe(listOf(GLOBAL_SUBSCRIPTION)) + } + } + + companion object { + const val GLOBAL_SUBSCRIPTION = "subscription.global.space-views" + } + } +} + +fun SpaceViewSubscriptionContainer.isSharingLimitReached() : Flow { + val sharedSpacesCount = observe().map { spaceViews -> + spaceViews.count { spaceView -> + spaceView.spaceAccessType == SpaceAccessType.SHARED + } + } + val sharedSpaceLimit = observe().map { spaceViews -> + val defaultSpace = spaceViews.firstOrNull { space -> + space.spaceAccessType == SpaceAccessType.DEFAULT + } + defaultSpace?.sharedSpaceLimit ?: 0 + } + return combine( + sharedSpaceLimit, + sharedSpacesCount + ) { limit, count -> + limit == 0 || count >= limit + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/spaces/SpaceDeletedStatusWatcher.kt b/domain/src/main/java/com/anytypeio/anytype/domain/spaces/SpaceDeletedStatusWatcher.kt index aa85586d99..6ca647e0f8 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/spaces/SpaceDeletedStatusWatcher.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/spaces/SpaceDeletedStatusWatcher.kt @@ -2,7 +2,6 @@ package com.anytypeio.anytype.domain.spaces import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Relations -import com.anytypeio.anytype.core_models.restrictions.SpaceStatus import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.config.ConfigStorage import com.anytypeio.anytype.domain.debugging.Logger @@ -78,6 +77,6 @@ class SpaceDeletedStatusWatcher @Inject constructor( } companion object { - const val GLOBAL_SPACE_VIEW_SUBSCRIPTION = "subscription.global.space-view" + const val GLOBAL_SPACE_VIEW_SUBSCRIPTION = "subscription.global.space-view-deleted-status-watcher" } } \ No newline at end of file diff --git a/domain/src/test/java/com/anytypeio/anytype/domain/multiplayer/SpaceViewSubscriptionContainerTest.kt b/domain/src/test/java/com/anytypeio/anytype/domain/multiplayer/SpaceViewSubscriptionContainerTest.kt new file mode 100644 index 0000000000..1ee945542c --- /dev/null +++ b/domain/src/test/java/com/anytypeio/anytype/domain/multiplayer/SpaceViewSubscriptionContainerTest.kt @@ -0,0 +1,181 @@ +package com.anytypeio.anytype.domain.multiplayer + +import app.cash.turbine.test +import com.anytypeio.anytype.core_models.StubSpaceView +import com.anytypeio.anytype.core_models.multiplayer.SpaceAccessType +import kotlin.test.assertEquals +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.stub + +class SpaceViewSubscriptionContainerTest { + + @Mock + lateinit var container: SpaceViewSubscriptionContainer + + @Before + fun before() { + MockitoAnnotations.openMocks(this) + } + + + @Test + fun `should reach limit if limit is 0`() = runTest { + + val defaultSpaceView = StubSpaceView( + sharedSpaceLimit = 0, + spaceAccessType = SpaceAccessType.DEFAULT + ) + + val privateSpaceView = StubSpaceView( + sharedSpaceLimit = 0, + spaceAccessType = SpaceAccessType.PRIVATE + ) + + container.stub { + on { + observe() + } doReturn flowOf( + listOf(defaultSpaceView, privateSpaceView) + ) + } + + container.isSharingLimitReached().test { + val result = awaitItem() + assertEquals( + expected = true, + actual = result + ) + awaitComplete() + } + } + + @Test + fun `should not reach limit if limit is 1 and there is one private space and zero shared spaces`() = runTest { + + val defaultSpaceView = StubSpaceView( + sharedSpaceLimit = 1, + spaceAccessType = SpaceAccessType.DEFAULT + ) + + val privateSpaceView = StubSpaceView( + sharedSpaceLimit = 0, + spaceAccessType = SpaceAccessType.PRIVATE + ) + + container.stub { + on { + observe() + } doReturn flowOf( + listOf(defaultSpaceView, privateSpaceView) + ) + } + + container.isSharingLimitReached().test { + val result = awaitItem() + assertEquals( + expected = false, + actual = result + ) + awaitComplete() + } + } + + @Test + fun `should reach limit if limit is 1 and there is one shared space already`() = runTest { + + val defaultSpaceView = StubSpaceView( + sharedSpaceLimit = 1, + spaceAccessType = SpaceAccessType.DEFAULT + ) + + val privateSpaceView = StubSpaceView( + sharedSpaceLimit = 0, + spaceAccessType = SpaceAccessType.SHARED + ) + + container.stub { + on { + observe() + } doReturn flowOf( + listOf(defaultSpaceView, privateSpaceView) + ) + } + + container.isSharingLimitReached().test { + val result = awaitItem() + assertEquals( + expected = true, + actual = result + ) + awaitComplete() + } + } + + @Test + fun `should not reach limit if limit is 2 and there is one shared space already`() = runTest { + + val defaultSpaceView = StubSpaceView( + sharedSpaceLimit = 2, + spaceAccessType = SpaceAccessType.DEFAULT + ) + + val privateSpaceView = StubSpaceView( + sharedSpaceLimit = 0, + spaceAccessType = SpaceAccessType.SHARED + ) + + container.stub { + on { + observe() + } doReturn flowOf( + listOf(defaultSpaceView, privateSpaceView) + ) + } + + container.isSharingLimitReached().test { + val result = awaitItem() + assertEquals( + expected = false, + actual = result + ) + awaitComplete() + } + } + + @Test + fun `should consider share limits from default space is one private space already`() = runTest { + + val defaultSpaceView = StubSpaceView( + sharedSpaceLimit = 0, + spaceAccessType = SpaceAccessType.DEFAULT + ) + + val privateSpaceView = StubSpaceView( + sharedSpaceLimit = 2, + spaceAccessType = SpaceAccessType.PRIVATE + ) + + container.stub { + on { + observe() + } doReturn flowOf( + listOf(defaultSpaceView, privateSpaceView) + ) + } + + container.isSharingLimitReached().test { + val result = awaitItem() + assertEquals( + expected = true, + actual = result + ) + awaitComplete() + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchConstants.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchConstants.kt index 467d6c5874..ed3a002a41 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchConstants.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchConstants.kt @@ -13,7 +13,6 @@ import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.primitives.TypeKey import com.anytypeio.anytype.domain.library.StoreSearchParams -import com.anytypeio.anytype.presentation.multiplayer.ShareSpaceViewModel import com.anytypeio.anytype.presentation.objects.SupportedLayouts /** @@ -1132,11 +1131,13 @@ object ObjectSearchConstants { Relations.SPACE_ACCOUNT_STATUS, Relations.SPACE_ACCESS_TYPE, Relations.SPACE_LOCAL_STATUS, + Relations.SHARED_SPACES_LIMIT, Relations.READERS_LIMIT, - Relations.WRITERS_LIMIT + Relations.WRITERS_LIMIT, ) //region SPACE VIEW + fun getSpaceViewSearchParams(subscription: String, targetSpaceId: Id): StoreSearchParams { return StoreSearchParams( subscription = subscription, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt index a3a0b19bff..d19ef1d6ce 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt @@ -8,11 +8,8 @@ import com.anytypeio.anytype.analytics.base.EventsDictionary import com.anytypeio.anytype.analytics.base.EventsPropertiesKey import com.anytypeio.anytype.analytics.base.sendEvent import com.anytypeio.anytype.analytics.props.Props -import com.anytypeio.anytype.core_models.DVFilter -import com.anytypeio.anytype.core_models.DVFilterCondition import com.anytypeio.anytype.core_models.Filepath import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.SpaceType @@ -25,10 +22,10 @@ import com.anytypeio.anytype.core_utils.ui.ViewState import com.anytypeio.anytype.domain.base.fold import com.anytypeio.anytype.domain.config.ConfigStorage import com.anytypeio.anytype.domain.debugging.DebugSpaceShareDownloader -import com.anytypeio.anytype.domain.library.StoreSearchParams -import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider +import com.anytypeio.anytype.domain.multiplayer.isSharingLimitReached import com.anytypeio.anytype.domain.spaces.DeleteSpace import com.anytypeio.anytype.domain.spaces.SetSpaceDetails import com.anytypeio.anytype.domain.workspace.SpaceManager @@ -37,7 +34,6 @@ import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch import timber.log.Timber @@ -46,14 +42,14 @@ class SpaceSettingsViewModel( private val analytics: Analytics, private val setSpaceDetails: SetSpaceDetails, private val spaceManager: SpaceManager, - private val container: StorelessSubscriptionContainer, private val gradientProvider: SpaceGradientProvider, private val urlBuilder: UrlBuilder, private val deleteSpace: DeleteSpace, private val configStorage: ConfigStorage, private val debugSpaceShareDownloader: DebugSpaceShareDownloader, private val spaceGradientProvider: SpaceGradientProvider, - private val userPermissionProvider: UserPermissionProvider + private val userPermissionProvider: UserPermissionProvider, + private val container: SpaceViewSubscriptionContainer ): BaseViewModel() { val commands = MutableSharedFlow() @@ -70,68 +66,35 @@ class SpaceSettingsViewModel( eventName = EventsDictionary.screenSettingSpacesSpaceIndex ) } - proceedWithFetchingSpaceMetaData() + proceedWithObservingSpaceView() } - private fun proceedWithFetchingSpaceMetaData() { + private fun proceedWithObservingSpaceView() { viewModelScope.launch { val config = spaceManager.getConfig(params.space) - container.subscribe( - StoreSearchParams( - subscription = SPACE_SETTINGS_SUBSCRIPTION, - filters = buildList { - add( - DVFilter( - relation = Relations.TARGET_SPACE_ID, - value = params.space.id, - condition = DVFilterCondition.EQUAL - ) - ) - add( - DVFilter( - relation = Relations.LAYOUT, - value = ObjectType.Layout.SPACE_VIEW.code.toDouble(), - condition = DVFilterCondition.EQUAL - ) - ) - }, - keys = listOf( - Relations.ID, - Relations.SPACE_ID, - Relations.NAME, - Relations.ICON_EMOJI, - Relations.ICON_IMAGE, - Relations.ICON_OPTION, - Relations.CREATED_DATE, - Relations.CREATOR, - Relations.TARGET_SPACE_ID, - Relations.SPACE_ACCESS_TYPE, - Relations.SPACE_LOCAL_STATUS, - Relations.SPACE_ACCOUNT_STATUS - ), - limit = 1 - ) - ).mapNotNull { results -> - results.firstOrNull() - }.combine(userPermissionProvider.observe(params.space)) { wrapper, permission -> - val spaceView = ObjectWrapper.SpaceView(wrapper.map) + combine( + container.observe(params.space), + userPermissionProvider.observe(params.space), + container.isSharingLimitReached() + ) { spaceView, permission, shareLimitReached -> SpaceData( - name = wrapper.name.orEmpty(), - icon = wrapper.spaceIcon( + name = spaceView.name.orEmpty(), + icon = spaceView.spaceIcon( builder = urlBuilder, spaceGradientProvider = gradientProvider ), - createdDateInMillis = wrapper + createdDateInMillis = spaceView .getValue(Relations.CREATED_DATE) ?.let { timeInSeconds -> (timeInSeconds * 1000L).toLong() }, - createdBy = wrapper + createdBy = spaceView .getValue(Relations.CREATOR) .toString(), spaceId = params.space.id, network = config?.network.orEmpty(), isDeletable = resolveIsSpaceDeletable(spaceView), spaceType = spaceView.spaceAccessType?.asSpaceType() ?: UNKNOWN_SPACE_TYPE, - permissions = permission ?: SpaceMemberPermissions.NO_PERMISSIONS + permissions = permission ?: SpaceMemberPermissions.NO_PERMISSIONS, + shareLimitReached = shareLimitReached ) }.collect { spaceData -> spaceViewState.value = ViewState.Success(spaceData) @@ -297,9 +260,12 @@ class SpaceSettingsViewModel( fun onSharePrivateSpaceClicked() { viewModelScope.launch { - commands.emit( - Command.SharePrivateSpace(params.space) - ) + val data = spaceViewState.value + if (data is ViewState.Success && !data.data.shareLimitReached) { + commands.emit( + Command.SharePrivateSpace(params.space) + ) + } } } @@ -312,7 +278,8 @@ class SpaceSettingsViewModel( val icon: SpaceIconView, val isDeletable: Boolean = false, val spaceType: SpaceType, - val permissions: SpaceMemberPermissions + val permissions: SpaceMemberPermissions, + val shareLimitReached: Boolean = false ) sealed class Command { @@ -326,7 +293,7 @@ class SpaceSettingsViewModel( class Factory @Inject constructor( private val params: Params, private val analytics: Analytics, - private val container: StorelessSubscriptionContainer, + private val container: SpaceViewSubscriptionContainer, private val urlBuilder: UrlBuilder, private val setSpaceDetails: SetSpaceDetails, private val gradientProvider: SpaceGradientProvider, @@ -360,6 +327,5 @@ class SpaceSettingsViewModel( companion object { const val SPACE_DEBUG_MSG = "Kindly share this debug logs with Anytype developers." - const val SPACE_SETTINGS_SUBSCRIPTION = "subscription.space-settings.space-views" } } \ No newline at end of file diff --git a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/DataView.kt b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/DataView.kt index 6da73954df..bf06b3e052 100644 --- a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/DataView.kt +++ b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/DataView.kt @@ -1,5 +1,6 @@ package com.anytypeio.anytype.core_models +import com.anytypeio.anytype.core_models.multiplayer.SpaceAccessType import com.anytypeio.anytype.test_utils.MockDataFactory fun StubDataView( @@ -92,4 +93,19 @@ fun StubFilter( condition = condition, quickOption = quickOption, value = value +) + +fun StubSpaceView( + id: Id = MockDataFactory.randomUuid(), + targetSpaceId: Id = MockDataFactory.randomUuid(), + spaceAccessType: SpaceAccessType = SpaceAccessType.DEFAULT, + sharedSpaceLimit: Int? = null + +) = ObjectWrapper.SpaceView( + map = mapOf( + Relations.ID to id, + Relations.TARGET_SPACE_ID to targetSpaceId, + Relations.SPACE_ACCESS_TYPE to spaceAccessType.code.toDouble(), + Relations.SHARED_SPACES_LIMIT to sharedSpaceLimit?.toDouble() + ) ) \ No newline at end of file diff --git a/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/Settings.kt b/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/Settings.kt index 3b54ca4772..9379c1e383 100644 --- a/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/Settings.kt +++ b/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/Settings.kt @@ -107,7 +107,8 @@ fun SpaceSettingsScreen( } PRIVATE_SPACE_TYPE -> { PrivateSpaceSharing( - onSharePrivateSpaceClicked = onSharePrivateSpaceClicked + onSharePrivateSpaceClicked = onSharePrivateSpaceClicked, + shareLimitReached = state.data.shareLimitReached ) } SHARED_SPACE_TYPE -> { @@ -344,7 +345,8 @@ fun SpaceSettingsScreenPreview() { @Composable fun PrivateSpaceSharing( - onSharePrivateSpaceClicked: () -> Unit + onSharePrivateSpaceClicked: () -> Unit, + shareLimitReached: Boolean ) { Box( modifier = Modifier @@ -367,12 +369,14 @@ fun PrivateSpaceSharing( Row( modifier = Modifier.align(Alignment.CenterEnd) ) { - Text( - modifier = Modifier.align(Alignment.CenterVertically), - text = stringResource(id = R.string.multiplayer_share), - color = colorResource(id = R.color.text_secondary), - style = BodyRegular - ) + if (!shareLimitReached) { + Text( + modifier = Modifier.align(Alignment.CenterVertically), + text = stringResource(id = R.string.multiplayer_share), + color = colorResource(id = R.color.text_secondary), + style = BodyRegular + ) + } Spacer(Modifier.width(10.dp)) Image( painter = painterResource(R.drawable.ic_arrow_forward), @@ -469,7 +473,8 @@ fun TypeOfSpace(spaceType: SpaceType?) { @Composable private fun PrivateSpaceSharingPreview() { PrivateSpaceSharing( - onSharePrivateSpaceClicked = {} + onSharePrivateSpaceClicked = {}, + shareLimitReached = false ) }