diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayObjectRelationTextValueTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayObjectRelationTextValueTest.kt index fba2e0e218..527b76f87f 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayObjectRelationTextValueTest.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayObjectRelationTextValueTest.kt @@ -23,7 +23,6 @@ import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelations import com.anytypeio.anytype.domain.objects.ObjectStore import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.StoreOfRelations -import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectRelationProvider import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectValueProvider import com.anytypeio.anytype.presentation.sets.ObjectSetDatabase import com.anytypeio.anytype.presentation.sets.RelationTextValueViewModel @@ -72,14 +71,11 @@ class DisplayObjectRelationTextValueTest { fun before() { MockitoAnnotations.openMocks(this) TestRelationTextValueFragment.testVmFactory = RelationTextValueViewModel.Factory( - relations = DataViewObjectRelationProvider( - objectState = state, - storeOfRelations = storeOfRelations - ), values = DataViewObjectValueProvider(db = db, objectState = state), reloadObject = reloadObject, analytics = analytics, - storeOfObjectTypes = storeOfObjectTypes + storeOfObjectTypes = storeOfObjectTypes, + storeOfRelations = storeOfRelations ) } diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationNumberValueTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationNumberValueTest.kt index c013ffeb8c..df411ad4a9 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationNumberValueTest.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/DisplayRelationNumberValueTest.kt @@ -21,7 +21,6 @@ import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelations import com.anytypeio.anytype.domain.objects.ObjectStore import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.StoreOfRelations -import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectRelationProvider import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectValueProvider import com.anytypeio.anytype.presentation.sets.ObjectSetDatabase import com.anytypeio.anytype.presentation.sets.RelationTextValueViewModel @@ -79,14 +78,11 @@ class DisplayRelationNumberValueTest { fun setup() { MockitoAnnotations.openMocks(this) TestRelationTextValueFragment.testVmFactory = RelationTextValueViewModel.Factory( - relations = DataViewObjectRelationProvider( - objectState = state, - storeOfRelations = storeOfRelations - ), values = DataViewObjectValueProvider(db = db, objectState = state), reloadObject = reloadObject, analytics = analytics, - storeOfObjectTypes = storeOfObjectTypes + storeOfObjectTypes = storeOfObjectTypes, + storeOfRelations = storeOfRelations ) } diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/ObjectRelationDateValueTest.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/ObjectRelationDateValueTest.kt index c1026c9345..767c7d586f 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/relations/ObjectRelationDateValueTest.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/relations/ObjectRelationDateValueTest.kt @@ -22,7 +22,6 @@ import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelations import com.anytypeio.anytype.domain.objects.ObjectStore import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.presentation.relations.ObjectSetConfig -import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectRelationProvider import com.anytypeio.anytype.presentation.relations.providers.DataViewObjectValueProvider import com.anytypeio.anytype.presentation.sets.ObjectSetDatabase import com.anytypeio.anytype.presentation.sets.RelationDateValueViewModel @@ -65,12 +64,9 @@ class ObjectRelationDateValueTest { fun setup() { MockitoAnnotations.openMocks(this) TestRelationDateValueFragment.testVmFactory = RelationDateValueViewModel.Factory( - relations = DataViewObjectRelationProvider( - objectState = state, - storeOfRelations = storeOfRelations - ), values = DataViewObjectValueProvider(db = db, objectState = state), - dateProvider = dateProvider + dateProvider = dateProvider, + storeOfRelations = storeOfRelations ) } diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/EditGridCellDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/EditGridCellDI.kt index ea7aaf532f..39cd770472 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/EditGridCellDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/EditGridCellDI.kt @@ -6,7 +6,7 @@ import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.misc.DateProvider import com.anytypeio.anytype.domain.`object`.ReloadObject import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes -import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider +import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider.Companion.DATA_VIEW_PROVIDER_TYPE import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider.Companion.INTRINSIC_PROVIDER_TYPE import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider @@ -38,17 +38,17 @@ object RelationTextValueModule { @Provides @PerModal fun provideRelationTextValueViewModelFactory( - @Named(INTRINSIC_PROVIDER_TYPE) relations: ObjectRelationProvider, @Named(INTRINSIC_PROVIDER_TYPE) values: ObjectValueProvider, reloadObject: ReloadObject, analytics: Analytics, - storeOfObjectTypes: StoreOfObjectTypes + storeOfObjectTypes: StoreOfObjectTypes, + storeOfRelations: StoreOfRelations ) = RelationTextValueViewModel.Factory( - relations = relations, values = values, reloadObject = reloadObject, analytics = analytics, - storeOfObjectTypes = storeOfObjectTypes + storeOfObjectTypes = storeOfObjectTypes, + storeOfRelations = storeOfRelations ) @JvmStatic @@ -78,17 +78,17 @@ object RelationDataViewTextValueModule { @Provides @PerModal fun provideRelationTextValueViewModelFactory( - @Named(DATA_VIEW_PROVIDER_TYPE) relations: ObjectRelationProvider, @Named(DATA_VIEW_PROVIDER_TYPE) values: ObjectValueProvider, reloadObject: ReloadObject, analytics: Analytics, - storeOfObjectTypes: StoreOfObjectTypes + storeOfObjectTypes: StoreOfObjectTypes, + storeOfRelations: StoreOfRelations ) = RelationTextValueViewModel.Factory( - relations = relations, values = values, reloadObject = reloadObject, analytics = analytics, - storeOfObjectTypes = storeOfObjectTypes + storeOfObjectTypes = storeOfObjectTypes, + storeOfRelations = storeOfRelations ) @JvmStatic @@ -118,10 +118,10 @@ object RelationDateValueModule { @Provides @PerModal fun provideEditGridCellViewModelFactory( - @Named(INTRINSIC_PROVIDER_TYPE) relations: ObjectRelationProvider, @Named(INTRINSIC_PROVIDER_TYPE) values: ObjectValueProvider, - dateProvider: DateProvider - ) = RelationDateValueViewModel.Factory(relations, values, dateProvider) + dateProvider: DateProvider, + storeOfRelations: StoreOfRelations + ) = RelationDateValueViewModel.Factory(values, dateProvider, storeOfRelations) } @Subcomponent(modules = [RelationDataViewDateValueModule::class]) @@ -143,8 +143,8 @@ object RelationDataViewDateValueModule { @Provides @PerModal fun provideEditGridCellViewModelFactory( - @Named(DATA_VIEW_PROVIDER_TYPE) relations: ObjectRelationProvider, @Named(DATA_VIEW_PROVIDER_TYPE) values: ObjectValueProvider, + storeOfRelations: StoreOfRelations, dateProvider: DateProvider - ) = RelationDateValueViewModel.Factory(relations, values, dateProvider) + ) = RelationDateValueViewModel.Factory(values, dateProvider, storeOfRelations) } diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectValueDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectValueDI.kt index 04c6e70642..fbb95aa340 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectValueDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectValueDI.kt @@ -60,7 +60,6 @@ object ObjectValueObjectModule { @Provides @PerModal fun provideFactory( - @Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) relations: ObjectRelationProvider, @Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) values: ObjectValueProvider, setObjectDetails: UpdateDetail, dispatcher: Dispatcher, @@ -79,7 +78,6 @@ object ObjectValueObjectModule { ): ObjectValueViewModelFactory = ObjectValueViewModelFactory( params = params, values = values, - relations = relations, setObjectDetails = setObjectDetails, dispatcher = dispatcher, analytics = analytics, @@ -121,7 +119,6 @@ object ObjectValueSetModule { @Provides @PerModal fun provideFactory( - @Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) relations: ObjectRelationProvider, @Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) values: ObjectValueProvider, setObjectDetails: UpdateDetail, dispatcher: Dispatcher, @@ -140,7 +137,6 @@ object ObjectValueSetModule { ): ObjectValueViewModelFactory = ObjectValueViewModelFactory( params = params, values = values, - relations = relations, setObjectDetails = setObjectDetails, dispatcher = dispatcher, analytics = analytics, @@ -182,7 +178,6 @@ object ObjectValueDataViewModule { @Provides @PerModal fun provideFactory( - @Named(ObjectRelationProvider.DATA_VIEW_PROVIDER_TYPE) relations: ObjectRelationProvider, @Named(ObjectRelationProvider.DATA_VIEW_PROVIDER_TYPE) values: ObjectValueProvider, setObjectDetails: UpdateDetail, dispatcher: Dispatcher, @@ -201,7 +196,6 @@ object ObjectValueDataViewModule { ): ObjectValueViewModelFactory = ObjectValueViewModelFactory( params = params, values = values, - relations = relations, setObjectDetails = setObjectDetails, dispatcher = dispatcher, analytics = analytics, diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/TagStatusDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/TagStatusDI.kt index 3818766910..f4fdb74773 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/TagStatusDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/TagStatusDI.kt @@ -5,17 +5,14 @@ import com.anytypeio.anytype.core_models.Payload import com.anytypeio.anytype.core_utils.di.scope.PerModal import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.block.repo.BlockRepository -import com.anytypeio.anytype.domain.debugging.Logger -import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.objects.StoreOfRelationOptions import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.domain.relations.DeleteRelationOptions -import com.anytypeio.anytype.domain.search.SubscriptionEventChannel -import com.anytypeio.anytype.domain.workspace.SpaceManager +import com.anytypeio.anytype.domain.relations.SetRelationOptionOrder import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider -import com.anytypeio.anytype.presentation.relations.value.tagstatus.SUB_MY_OPTIONS import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagOrStatusValueViewModel import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagOrStatusValueViewModelFactory import com.anytypeio.anytype.presentation.util.Dispatcher @@ -49,26 +46,21 @@ object TagOrStatusValueObjectModule { @JvmStatic @Provides @PerModal - @Named(SUB_MY_OPTIONS) - fun provideStoreLessSubscriptionContainer( + fun provideDeleteRelationOptions( repo: BlockRepository, - channel: SubscriptionEventChannel, - dispatchers: AppCoroutineDispatchers, - logger: Logger - ): StorelessSubscriptionContainer = StorelessSubscriptionContainer.Impl( + dispatchers: AppCoroutineDispatchers + ): DeleteRelationOptions = DeleteRelationOptions( repo = repo, - channel = channel, - dispatchers = dispatchers, - logger = logger + dispatchers = dispatchers ) @JvmStatic @Provides @PerModal - fun provideDeleteRelationOptions( + fun provideSetRelationOptionOrder( repo: BlockRepository, dispatchers: AppCoroutineDispatchers - ): DeleteRelationOptions = DeleteRelationOptions( + ): SetRelationOptionOrder = SetRelationOptionOrder( repo = repo, dispatchers = dispatchers ) @@ -77,29 +69,27 @@ object TagOrStatusValueObjectModule { @Provides @PerModal fun provideFactory( - @Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) relations: ObjectRelationProvider, @Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) values: ObjectValueProvider, setObjectDetails: UpdateDetail, dispatcher: Dispatcher, analytics: Analytics, - spaceManager: SpaceManager, params: TagOrStatusValueViewModel.ViewModelParams, - @Named(SUB_MY_OPTIONS) subscription: StorelessSubscriptionContainer, deleteRelationOptions: DeleteRelationOptions, + setRelationOptionOrder: SetRelationOptionOrder, analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate, - storeOfRelations: StoreOfRelations + storeOfRelations: StoreOfRelations, + storeOfRelationOptions: StoreOfRelationOptions ): TagOrStatusValueViewModelFactory = TagOrStatusValueViewModelFactory( params = params, values = values, - relations = relations, setObjectDetails = setObjectDetails, dispatcher = dispatcher, analytics = analytics, - spaceManager = spaceManager, - subscription = subscription, deleteRelationOptions = deleteRelationOptions, + setRelationOptionOrder = setRelationOptionOrder, analyticSpaceHelperDelegate = analyticSpaceHelperDelegate, - storeOfRelations = storeOfRelations + storeOfRelations = storeOfRelations, + storeOfRelationOptions = storeOfRelationOptions ) } //endregion @@ -127,26 +117,21 @@ object TagOrStatusValueSetModule { @JvmStatic @Provides @PerModal - @Named(SUB_MY_OPTIONS) - fun provideStoreLessSubscriptionContainer( + fun provideDeleteRelationOptions( repo: BlockRepository, - channel: SubscriptionEventChannel, - dispatchers: AppCoroutineDispatchers, - logger: Logger - ): StorelessSubscriptionContainer = StorelessSubscriptionContainer.Impl( + dispatchers: AppCoroutineDispatchers + ): DeleteRelationOptions = DeleteRelationOptions( repo = repo, - channel = channel, - dispatchers = dispatchers, - logger = logger + dispatchers = dispatchers ) @JvmStatic @Provides @PerModal - fun provideDeleteRelationOptions( + fun provideSetRelationOptionOrder( repo: BlockRepository, dispatchers: AppCoroutineDispatchers - ): DeleteRelationOptions = DeleteRelationOptions( + ): SetRelationOptionOrder = SetRelationOptionOrder( repo = repo, dispatchers = dispatchers ) @@ -155,29 +140,27 @@ object TagOrStatusValueSetModule { @Provides @PerModal fun provideFactory( - @Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) relations: ObjectRelationProvider, @Named(ObjectRelationProvider.INTRINSIC_PROVIDER_TYPE) values: ObjectValueProvider, setObjectDetails: UpdateDetail, dispatcher: Dispatcher, analytics: Analytics, - spaceManager: SpaceManager, params: TagOrStatusValueViewModel.ViewModelParams, - @Named(SUB_MY_OPTIONS) subscription: StorelessSubscriptionContainer, deleteRelationOptions: DeleteRelationOptions, + setRelationOptionOrder: SetRelationOptionOrder, analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate, - storeOfRelations: StoreOfRelations + storeOfRelations: StoreOfRelations, + storeOfRelationOptions: StoreOfRelationOptions ): TagOrStatusValueViewModelFactory = TagOrStatusValueViewModelFactory( params = params, values = values, - relations = relations, setObjectDetails = setObjectDetails, dispatcher = dispatcher, analytics = analytics, - spaceManager = spaceManager, - subscription = subscription, deleteRelationOptions = deleteRelationOptions, + setRelationOptionOrder = setRelationOptionOrder, analyticSpaceHelperDelegate = analyticSpaceHelperDelegate, - storeOfRelations = storeOfRelations + storeOfRelations = storeOfRelations, + storeOfRelationOptions = storeOfRelationOptions ) } //endregion @@ -205,26 +188,21 @@ object TagOrStatusValueDataViewModule { @JvmStatic @Provides @PerModal - @Named(SUB_MY_OPTIONS) - fun provideStoreLessSubscriptionContainer( + fun provideDeleteRelationOptions( repo: BlockRepository, - channel: SubscriptionEventChannel, - dispatchers: AppCoroutineDispatchers, - logger: Logger - ): StorelessSubscriptionContainer = StorelessSubscriptionContainer.Impl( + dispatchers: AppCoroutineDispatchers + ): DeleteRelationOptions = DeleteRelationOptions( repo = repo, - channel = channel, - dispatchers = dispatchers, - logger = logger + dispatchers = dispatchers ) @JvmStatic @Provides @PerModal - fun provideDeleteRelationOptions( + fun provideSetRelationOptionOrder( repo: BlockRepository, dispatchers: AppCoroutineDispatchers - ): DeleteRelationOptions = DeleteRelationOptions( + ): SetRelationOptionOrder = SetRelationOptionOrder( repo = repo, dispatchers = dispatchers ) @@ -233,29 +211,27 @@ object TagOrStatusValueDataViewModule { @Provides @PerModal fun provideFactory( - @Named(ObjectRelationProvider.DATA_VIEW_PROVIDER_TYPE) relations: ObjectRelationProvider, @Named(ObjectRelationProvider.DATA_VIEW_PROVIDER_TYPE) values: ObjectValueProvider, setObjectDetails: UpdateDetail, dispatcher: Dispatcher, analytics: Analytics, - spaceManager: SpaceManager, params: TagOrStatusValueViewModel.ViewModelParams, - @Named(SUB_MY_OPTIONS) subscription: StorelessSubscriptionContainer, deleteRelationOptions: DeleteRelationOptions, + setRelationOptionOrder: SetRelationOptionOrder, analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate, - storeOfRelations: StoreOfRelations + storeOfRelations: StoreOfRelations, + storeOfRelationOptions: StoreOfRelationOptions ): TagOrStatusValueViewModelFactory = TagOrStatusValueViewModelFactory( params = params, values = values, - relations = relations, setObjectDetails = setObjectDetails, dispatcher = dispatcher, analytics = analytics, - spaceManager = spaceManager, - subscription = subscription, deleteRelationOptions = deleteRelationOptions, + setRelationOptionOrder = setRelationOptionOrder, analyticSpaceHelperDelegate = analyticSpaceHelperDelegate, - storeOfRelations = storeOfRelations + storeOfRelations = storeOfRelations, + storeOfRelationOptions = storeOfRelationOptions ) } //endregion \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/search/GlobalSearchComponent.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/search/GlobalSearchComponent.kt index 83788e734a..4c8d8ec3f9 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/search/GlobalSearchComponent.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/search/GlobalSearchComponent.kt @@ -10,6 +10,7 @@ import com.anytypeio.anytype.domain.config.UserSettingsRepository import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.StoreOfRelationOptions import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.domain.primitives.FieldParser import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate @@ -62,4 +63,5 @@ interface GlobalSearchDependencies : ComponentDependencies { fun userSettingsRepository(): UserSettingsRepository fun fieldParser(): FieldParser fun spaceViews(): SpaceViewSubscriptionContainer + fun storeOfRelationOptions(): StoreOfRelationOptions } 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 a781e94969..359d52b2d6 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 @@ -29,12 +29,16 @@ import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.notifications.PushKeyProvider import com.anytypeio.anytype.domain.notifications.RegisterDeviceToken import com.anytypeio.anytype.domain.objects.DefaultStoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelationOptions import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelations import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.StoreOfRelationOptions import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.domain.search.ObjectTypesSubscriptionContainer import com.anytypeio.anytype.domain.search.ObjectTypesSubscriptionManager import com.anytypeio.anytype.domain.search.ProfileSubscriptionManager +import com.anytypeio.anytype.domain.search.RelationOptionsSubscriptionContainer +import com.anytypeio.anytype.domain.search.RelationOptionsSubscriptionManager import com.anytypeio.anytype.domain.search.RelationsSubscriptionContainer import com.anytypeio.anytype.domain.search.RelationsSubscriptionManager import com.anytypeio.anytype.domain.search.SearchObjects @@ -88,6 +92,23 @@ object SubscriptionsModule { logger = logger ) + @JvmStatic + @Provides + @Singleton + fun relationOptionsSubscriptionContainer( + repo: BlockRepository, + channel: SubscriptionEventChannel, + dispatchers: AppCoroutineDispatchers, + store: StoreOfRelationOptions, + logger: Logger + ): RelationOptionsSubscriptionContainer = RelationOptionsSubscriptionContainer( + repo = repo, + channel = channel, + store = store, + dispatchers = dispatchers, + logger = logger + ) + @JvmStatic @Provides @Singleton @@ -98,6 +119,11 @@ object SubscriptionsModule { @Singleton fun objectTypesStore(): StoreOfObjectTypes = DefaultStoreOfObjectTypes() + @JvmStatic + @Provides + @Singleton + fun relationOptionsStore(): StoreOfRelationOptions = DefaultStoreOfRelationOptions() + @JvmStatic @Provides @Singleton @@ -120,6 +146,17 @@ object SubscriptionsModule { spaceManager = spaceManager ) + @JvmStatic + @Provides + @Singleton + fun relationOptionsSubscriptionManager( + subscription: RelationOptionsSubscriptionContainer, + spaceManager: SpaceManager + ): RelationOptionsSubscriptionManager = RelationOptionsSubscriptionManager( + container = subscription, + spaceManager = spaceManager + ) + @JvmStatic @Provides @Singleton @@ -298,6 +335,7 @@ object SubscriptionsModule { fun globalSubscriptionManager( types: ObjectTypesSubscriptionManager, relations: RelationsSubscriptionManager, + relationOptions: RelationOptionsSubscriptionManager, permissions: UserPermissionProvider, isSpaceDeleted: SpaceDeletedStatusWatcher, profileSubscriptionManager: ProfileSubscriptionManager, @@ -308,6 +346,7 @@ object SubscriptionsModule { ): GlobalSubscriptionManager = GlobalSubscriptionManager.Default( types = types, relations = relations, + relationOptions = relationOptions, permissions = permissions, isSpaceDeleted = isSpaceDeleted, profile = profileSubscriptionManager, diff --git a/app/src/main/java/com/anytypeio/anytype/ui/relations/value/TagOrStatusValueFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/relations/value/TagOrStatusValueFragment.kt index 4a3d60b264..86e7250ba1 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/relations/value/TagOrStatusValueFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/relations/value/TagOrStatusValueFragment.kt @@ -62,6 +62,7 @@ class TagOrStatusValueFragment : BaseBottomSheetComposeFragment() { ) { TagOrStatusValueScreen( state = vm.viewState.collectAsStateWithLifecycle().value, + query = vm.queryState.collectAsStateWithLifecycle().value, action = vm::onAction, onQueryChanged = vm::onQueryChanged ) @@ -71,7 +72,8 @@ class TagOrStatusValueFragment : BaseBottomSheetComposeFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupCollapsedHeight() + skipCollapsed() + expand() } override fun onStart() { diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Command.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Command.kt index 672596cb1b..31d8cb1464 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Command.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Command.kt @@ -5,6 +5,7 @@ import com.anytypeio.anytype.core_models.chats.NotificationState import com.anytypeio.anytype.core_models.membership.MembershipPaymentMethod import com.anytypeio.anytype.core_models.membership.NameServiceNameType import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions +import com.anytypeio.anytype.core_models.primitives.RelationKey import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_models.primitives.TypeKey @@ -514,6 +515,11 @@ sealed class Command { data class ApplyTemplate(val objectId: Id, val template: Id?) data class DeleteRelationOptions(val optionIds: List) + data class SetRelationOptionsOrder( + val space: SpaceId, + val relationKey: RelationKey, + val orderedIds: List + ) data class RelationListWithValue(val space: SpaceId, val value: String) 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 9c71651c66..fcde0cdf16 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 @@ -308,6 +308,8 @@ sealed class ObjectWrapper { val name: String? get() = getSingleValue(Relations.NAME) val color: String = relationOptionColor.orEmpty() val isDeleted: Boolean? get() = getSingleValue(Relations.IS_DELETED) + val relationKey: Key? get() = getSingleValue(Relations.RELATION_KEY) + val orderId: String? get() = getSingleValue(Relations.ORDER_ID) } data class SpaceView(override val map: Struct) : ObjectWrapper() { 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 df4581ffd7..e4a20cd269 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 @@ -46,6 +46,7 @@ object Relations { const val RELATION_KEY = "relationKey" const val RELATION_OPTION_COLOR = "relationOptionColor" const val RELATION_OPTION_DICT = "relationOptionsDict" + const val RELATION_OPTION_ORDER = "relationOptionOrder" const val SCOPE = "scope" const val RESTRICTIONS = "restrictions" const val MAX_COUNT = "relationMaxCount" diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/Common.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/Common.kt index 0b4166e451..b8d092f6c9 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/Common.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/Common.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.foundation.noRippleClickable +import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium import com.anytypeio.anytype.core_ui.views.Relations1 import com.anytypeio.anytype.presentation.relations.value.tagstatus.RelationsListItem @@ -107,7 +108,7 @@ fun ItemTagOrStatusCreate(state: RelationsListItem.CreateItem, action: (TagStatu .padding(start = 10.dp), maxLines = 1, overflow = TextOverflow.Ellipsis, - style = Relations1 + style = BodyCalloutRegular ) } } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/StatusItem.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/StatusItem.kt index 35732a0f93..6a52f8666e 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/StatusItem.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/StatusItem.kt @@ -1,10 +1,16 @@ package com.anytypeio.anytype.core_ui.relations +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.Image +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material.Divider @@ -14,12 +20,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_models.ThemeColor +import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.extensions.dark import com.anytypeio.anytype.core_ui.foundation.noRippleCombinedClickable import com.anytypeio.anytype.core_ui.views.Relations1 @@ -31,28 +42,50 @@ fun StatusItem( state: RelationsListItem.Item.Status, action: (TagStatusAction) -> Unit, isEditable: Boolean, - showDivider: Boolean = true + showDivider: Boolean = true, + isDragging: Boolean = false, + dragHandleModifier: Modifier = Modifier ) { val haptics = LocalHapticFeedback.current val isMenuExpanded = remember { mutableStateOf(false) } + val alpha = animateFloatAsState(if (isDragging) 0.8f else 1.0f, label = "drag_alpha") CommonContainer( modifier = Modifier + .alpha(alpha.value) .noRippleCombinedClickable( onClick = { action(TagStatusAction.Click(state)) }, onLongClicked = { - if (isEditable) { + if (isEditable && !isDragging) { haptics.performHapticFeedback(HapticFeedbackType.LongPress) isMenuExpanded.value = !isMenuExpanded.value } } ) ) { - Box( + Row( modifier = Modifier .fillMaxWidth() .padding(end = 56.dp) - .align(alignment = Alignment.CenterStart) + .align(alignment = Alignment.CenterStart), + verticalAlignment = Alignment.CenterVertically ) { + if (isEditable) { + Box( + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures( + onLongPress = { /* Consumed - do nothing */ } + ) + } + ) { + Image( + modifier = dragHandleModifier.size(24.dp), + painter = painterResource(id = R.drawable.ic_drag_handle_dots), + contentDescription = "Drag to reorder" + ) + } + Spacer(modifier = Modifier.width(8.dp)) + } StatusItemText(state = state) } CheckedIcon( @@ -61,7 +94,10 @@ fun StatusItem( .size(24.dp) .align(Alignment.CenterEnd) ) - if (showDivider) Divider(modifier = Modifier.align(Alignment.BottomCenter)) + if (showDivider) Divider( + modifier = Modifier.align(Alignment.BottomCenter), + color = colorResource(R.color.shape_primary) + ) if (isEditable) { ItemMenu( action = { @@ -123,12 +159,6 @@ fun PreviewStatusItem() { action = {}, isEditable = true ) - ItemTagOrStatusCreate( - state = RelationsListItem.CreateItem.Status( - text = "Personal" - ), - action = {} - ) } } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/TagItem.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/TagItem.kt index 5ecf3a70d8..f2ec1f0393 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/TagItem.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/TagItem.kt @@ -1,11 +1,17 @@ package com.anytypeio.anytype.core_ui.relations +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape @@ -16,12 +22,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_models.ThemeColor +import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.extensions.dark import com.anytypeio.anytype.core_ui.extensions.light import com.anytypeio.anytype.core_ui.foundation.noRippleCombinedClickable @@ -34,28 +44,50 @@ fun TagItem( state: RelationsListItem.Item.Tag, action: (TagStatusAction) -> Unit, isEditable: Boolean, - showDivider: Boolean = true + showDivider: Boolean = true, + isDragging: Boolean = false, + dragHandleModifier: Modifier = Modifier ) { val haptics = LocalHapticFeedback.current val isMenuExpanded = remember { mutableStateOf(false) } + val alpha = animateFloatAsState(if (isDragging) 0.8f else 1.0f, label = "drag_alpha") CommonContainer( modifier = Modifier + .alpha(alpha.value) .noRippleCombinedClickable( onClick = { action(TagStatusAction.Click(state)) }, onLongClicked = { - if (isEditable) { + if (isEditable && !isDragging) { haptics.performHapticFeedback(HapticFeedbackType.LongPress) isMenuExpanded.value = !isMenuExpanded.value } } ) ) { - Box( + Row( modifier = Modifier .fillMaxWidth() .padding(end = 56.dp) - .align(alignment = Alignment.CenterStart) + .align(alignment = Alignment.CenterStart), + verticalAlignment = Alignment.CenterVertically ) { + if (isEditable) { + Box( + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures( + onLongPress = { /* Consumed - do nothing */ } + ) + } + ) { + Image( + modifier = dragHandleModifier.size(24.dp), + painter = painterResource(id = R.drawable.ic_drag_handle_dots), + contentDescription = "Drag to reorder" + ) + } + Spacer(modifier = Modifier.width(8.dp)) + } TagItemText(state = state) } if (isEditable) { @@ -135,11 +167,5 @@ fun PreviewTagItem() { action = {}, isEditable = true ) - ItemTagOrStatusCreate( - state = RelationsListItem.CreateItem.Tag( - text = "Done" - ), - action = {} - ) } } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/TagOrStatusCompose.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/TagOrStatusCompose.kt index 151d8c5a36..8797459bbe 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/TagOrStatusCompose.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/relations/TagOrStatusCompose.kt @@ -1,5 +1,6 @@ package com.anytypeio.anytype.core_ui.relations +import android.view.HapticFeedbackConstants import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -19,12 +20,17 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource @@ -32,15 +38,18 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.core.view.HapticFeedbackConstantsCompat +import androidx.core.view.ViewCompat +import com.anytypeio.anytype.core_models.ThemeColor import com.anytypeio.anytype.core_ui.R -import com.anytypeio.anytype.core_ui.foundation.AlertDescription -import com.anytypeio.anytype.core_ui.foundation.AlertIcon -import com.anytypeio.anytype.core_ui.foundation.AlertTitle +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.extensions.swapList import com.anytypeio.anytype.core_ui.foundation.Divider import com.anytypeio.anytype.core_ui.foundation.Dragger import com.anytypeio.anytype.core_ui.foundation.noRippleClickable import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable import com.anytypeio.anytype.core_ui.views.BodyCalloutMedium +import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular import com.anytypeio.anytype.core_ui.views.ButtonSecondary import com.anytypeio.anytype.core_ui.views.ButtonSize import com.anytypeio.anytype.core_ui.views.Title1 @@ -49,10 +58,13 @@ import com.anytypeio.anytype.core_ui.widgets.SearchField import com.anytypeio.anytype.presentation.relations.value.tagstatus.RelationsListItem import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusAction import com.anytypeio.anytype.presentation.relations.value.tagstatus.TagStatusViewState +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.rememberReorderableLazyListState @Composable fun TagOrStatusValueScreen( state: TagStatusViewState, + query: String, action: (TagStatusAction) -> Unit, onQueryChanged: (String) -> Unit ) { @@ -72,40 +84,18 @@ fun TagOrStatusValueScreen( .padding(bottom = 20.dp) ) { Header(state = state, action = action) - Search(state = state, onQueryChanged = onQueryChanged) + SearchField( + query = query, + enabled = state !is TagStatusViewState.Loading, + onFocused = {}, + onQueryChanged = onQueryChanged + ) + Divider(paddingEnd = 0.dp, paddingStart = 0.dp) RelationsLazyList(state = state, action = action) } } } -@Composable -private fun Search( - state: TagStatusViewState, - onQueryChanged: (String) -> Unit -) { - when (state) { - is TagStatusViewState.Content -> { - if (state.isRelationEditable) { - SearchField( - onFocused = {}, - onQueryChanged = onQueryChanged - ) - Divider(paddingEnd = 0.dp, paddingStart = 0.dp) - } - } - is TagStatusViewState.Empty -> { - if (state.isRelationEditable) { - SearchField( - onFocused = {}, - onQueryChanged = onQueryChanged - ) - Divider(paddingEnd = 0.dp, paddingStart = 0.dp) - } - } - else -> { /* Do nothing */} - } -} - @Composable private fun Header(state: TagStatusViewState, action: (TagStatusAction) -> Unit) { @@ -127,11 +117,12 @@ private fun Header(state: TagStatusViewState, action: (TagStatusAction) -> Unit) ) { if (isClearButtonVisible(state = state)) { // Left-aligned CLEAR button - Box(modifier = Modifier - .wrapContentWidth() - .fillMaxHeight() - .align(Alignment.CenterStart) - .noRippleClickable { action(TagStatusAction.Clear) } + Box( + modifier = Modifier + .wrapContentWidth() + .fillMaxHeight() + .align(Alignment.CenterStart) + .noRippleClickable { action(TagStatusAction.Clear) } ) { Text( modifier = Modifier @@ -196,34 +187,152 @@ fun RelationsViewContent( state: TagStatusViewState.Content, action: (TagStatusAction) -> Unit ) { + val view = LocalView.current val lazyListState = rememberLazyListState() - LazyColumn( - state = lazyListState, - modifier = Modifier - .fillMaxSize() - ) { - itemsIndexed( - items = state.items, - itemContent = { index, item -> - val isLastItem = index == state.items.size - 1 - when (item) { - is RelationsListItem.Item.Tag -> TagItem( - state = item, - action = action, - isEditable = state.isRelationEditable, - showDivider = !isLastItem + + if (state.isRelationEditable) { + // Use reorderable list for both Tags and Status + val items = remember { mutableStateListOf() } + items.swapList(state.items) + + // Track the original dragged item's ID (not index) to handle multiple callback invocations + val draggedItemId = remember { mutableStateOf(null) } + + val onDragStoppedHandler = { + val originalItemId = draggedItemId.value + if (originalItemId != null) { + // Find original index from state.items (unchanged during drag) + val originalIndex = state.items.indexOfFirst { it.optionId == originalItemId } + // Find new index in reordered items list + val newIndex = items.indexOfFirst { it.optionId == originalItemId } + + if (originalIndex != -1 && newIndex != -1 && originalIndex != newIndex) { + action(TagStatusAction.OnMove(from = originalIndex, to = newIndex)) + } + } + draggedItemId.value = null + } + + val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to -> + val fromId = from.key as? String + val toId = to.key as? String + if (fromId == null || toId == null) { + return@rememberReorderableLazyListState + } + + // Capture original dragged item on first move + if (draggedItemId.value == null) { + draggedItemId.value = fromId + } + + // Find current indices by key + val f = items.indexOfFirst { it.optionId == fromId } + val t = items.indexOfFirst { it.optionId == toId } + + if (f != -1 && t != -1 && f != t) { + val newList = items.toMutableList().apply { + add(t, removeAt(f)) + } + items.swapList(newList) + + ViewCompat.performHapticFeedback( + view, + HapticFeedbackConstantsCompat.SEGMENT_FREQUENT_TICK + ) + } + } + + LazyColumn( + state = lazyListState, + modifier = Modifier.fillMaxSize() + ) { + // Reorderable items + items( + count = items.size, + key = { index -> items[index].optionId } + ) { index -> + val item = items[index] + ReorderableItem(reorderableLazyListState, key = item.optionId) { isDragging -> + val currentItem = LocalView.current + if (isDragging) { + currentItem.isHapticFeedbackEnabled = true + currentItem.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } + val dragHandleModifier = Modifier.longPressDraggableHandle( + onDragStarted = { + ViewCompat.performHapticFeedback( + view, + HapticFeedbackConstantsCompat.GESTURE_START + ) + }, + onDragStopped = { + ViewCompat.performHapticFeedback( + view, + HapticFeedbackConstantsCompat.GESTURE_END + ) + onDragStoppedHandler() + } ) + when (item) { + is RelationsListItem.Item.Tag -> TagItem( + state = item, + action = action, + isEditable = state.isRelationEditable, + showDivider = true, + isDragging = isDragging, + dragHandleModifier = dragHandleModifier + ) - is RelationsListItem.Item.Status -> StatusItem( - state = item, - action = action, - isEditable = state.isRelationEditable, - showDivider = !isLastItem + is RelationsListItem.Item.Status -> StatusItem( + state = item, + action = action, + isEditable = state.isRelationEditable, + showDivider = true, + isDragging = isDragging, + dragHandleModifier = dragHandleModifier + ) + } + } + } + + // CreateItem at the bottom (non-reorderable, only for Tags) + state.createItem?.let { createItem -> + item(key = "create_item") { + ItemTagOrStatusCreate( + state = createItem, + action = action ) - is RelationsListItem.CreateItem.Status -> ItemTagOrStatusCreate(item, action) - is RelationsListItem.CreateItem.Tag -> ItemTagOrStatusCreate(item, action) } - }) + } + } + } else { + // Regular list for read-only + LazyColumn( + state = lazyListState, + modifier = Modifier.fillMaxSize() + ) { + itemsIndexed( + items = state.items, + itemContent = { index, item -> + val isLastItem = index == state.items.size - 1 + when (item) { + is RelationsListItem.Item.Tag -> TagItem( + state = item, + action = action, + isEditable = state.isRelationEditable, + showDivider = !isLastItem + ) + + is RelationsListItem.Item.Status -> StatusItem( + state = item, + action = action, + isEditable = state.isRelationEditable, + showDivider = !isLastItem + ) + } + } + ) + } } } @@ -232,45 +341,48 @@ fun RelationsViewEmpty( state: TagStatusViewState.Empty, action: (TagStatusAction) -> Unit ) { - val icon = R.drawable.ic_popup_duck_56 - if (state.isRelationEditable) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - ) { - Spacer(modifier = Modifier.height(154.dp)) - AlertIcon(icon) - Spacer(modifier = Modifier.height(12.dp)) - AlertTitle( - title = stringResource(id = R.string.options_empty_title), - style = BodyCalloutMedium - ) - AlertDescription( - description = stringResource(id = R.string.options_empty_description), - style = BodyCalloutMedium - ) - Spacer(modifier = Modifier.height(16.dp)) - ButtonSecondary( - text = stringResource(id = R.string.create), - onClick = { action(TagStatusAction.Create) }, - size = ButtonSize.Small, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - } - } else { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - ) { - Spacer(modifier = Modifier.height(87.dp)) - AlertIcon(icon) - Spacer(modifier = Modifier.height(12.dp)) - AlertTitle( - title = stringResource(id = R.string.options_empty_not_editable), - style = BodyCalloutMedium - ) + Box( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + contentAlignment = Alignment.Center + ) { + if (state.isRelationEditable) { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(id = R.string.options_empty_title), + style = BodyCalloutMedium, + color = colorResource(id = R.color.text_primary) + ) + Text( + text = stringResource(id = R.string.options_empty_description), + style = BodyCalloutRegular, + color = colorResource(id = R.color.text_secondary) + ) + Spacer(modifier = Modifier.height(13.dp)) + ButtonSecondary( + text = stringResource(id = R.string.create), + onClick = { action(TagStatusAction.Create) }, + size = ButtonSize.Small, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + } else { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(id = R.string.options_empty_not_editable), + style = BodyCalloutMedium, + color = colorResource(id = R.color.text_primary) + ) + } } } } @@ -302,4 +414,122 @@ private fun getTitle(state: TagStatusViewState): String { is TagStatusViewState.Empty -> state.title is TagStatusViewState.Loading -> "" } +} + +@DefaultPreviews +@Composable +private fun TagOrStatusValueScreenEmptyEditablePreview() { + MaterialTheme { + TagOrStatusValueScreen( + state = TagStatusViewState.Empty( + title = "Assignee", + isRelationEditable = true // Key change: user can add new options + ), + query = "", + action = {}, + onQueryChanged = {} + ) + } +} + +@DefaultPreviews +@Composable +private fun TagOrStatusValueScreenEmptyReadOnlyPreview() { + MaterialTheme { + TagOrStatusValueScreen( + state = TagStatusViewState.Empty( + title = "Platform", + isRelationEditable = false // Key change: user cannot add new options + ), + query = "", + action = {}, + onQueryChanged = {} + ) + } +} + + +@DefaultPreviews +@Composable +private fun TagOrStatusValueScreenPreview() { + val items = listOf( + RelationsListItem.Item.Tag( + optionId = "1", + name = "Urgent", + isSelected = true, + color = ThemeColor.RED, + number = 10 + ), + RelationsListItem.Item.Tag( + optionId = "2", + name = "In Progress", + isSelected = false, + color = ThemeColor.GREY, + number = 9 + ), + RelationsListItem.Item.Tag( + optionId = "3", + name = "Low Priority", + isSelected = false, + color = ThemeColor.YELLOW, + number = 1 + ) + ) + + val contentState = TagStatusViewState.Content( + title = "Priority", + isRelationEditable = true, + items = items, + createItem = null + ) + + MaterialTheme { + TagOrStatusValueScreen( + state = contentState, + query = "", + action = {}, + onQueryChanged = {} + ) + } +} + +@DefaultPreviews +@Composable +private fun StatusValueScreenPreview() { + val items = listOf( + RelationsListItem.Item.Status( + optionId = "1", + name = "Not Started", + isSelected = false, + color = ThemeColor.GREY + ), + RelationsListItem.Item.Status( + optionId = "2", + name = "In Progress", + isSelected = true, + color = ThemeColor.BLUE + ), + RelationsListItem.Item.Status( + optionId = "3", + name = "Completed", + isSelected = false, + color = ThemeColor.ORANGE + ) + ) + + val contentState = TagStatusViewState.Content( + title = "Status", + isRelationEditable = true, + items = items, + createItem = null + ) + + MaterialTheme { + TagOrStatusValueScreen( + state = contentState, + query = "", + action = {}, + onQueryChanged = {} + ) + } } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/SearchField.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/SearchField.kt index ce8629ba71..2252c32208 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/SearchField.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/SearchField.kt @@ -17,6 +17,7 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Text import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -40,25 +41,32 @@ import com.anytypeio.anytype.core_ui.views.BodyRegular @OptIn(ExperimentalMaterialApi::class) @Composable fun SearchField( + query: String = "", + enabled: Boolean = true, onQueryChanged: (String) -> Unit, onFocused: () -> Unit ) { Box( modifier = Modifier - .height(48.dp) + .height(60.dp) .fillMaxWidth() .verticalScroll(rememberScrollState()) ) { val focusManager = LocalFocusManager.current val focusRequester = FocusRequester() - val input = remember { mutableStateOf(String()) } + val input = remember { mutableStateOf(query) } + + // Sync external query changes to internal state + LaunchedEffect(query) { + input.value = query + } Box( modifier = Modifier .padding(horizontal = 16.dp) .fillMaxWidth() - .height(36.dp) - .clip(RoundedCornerShape(10.dp)) - .background(color = colorResource(id = R.color.shape_transparent)) + .height(40.dp) + .clip(RoundedCornerShape(20.dp)) + .background(color = colorResource(id = R.color.shape_transparent_secondary)) .align(Alignment.Center) ) { Image( @@ -66,7 +74,7 @@ fun SearchField( contentDescription = "Search icon", modifier = Modifier .align(Alignment.CenterStart) - .padding(start = 8.dp) + .padding(start = 12.dp) ) if (input.value.isNotEmpty()) { Image( @@ -82,6 +90,7 @@ fun SearchField( ) } BasicTextField( + enabled = enabled, value = input.value, onValueChange = { input.value = it @@ -92,7 +101,7 @@ fun SearchField( ), modifier = Modifier .fillMaxWidth() - .padding(start = 32.dp, end = 32.dp) + .padding(start = 36.dp, end = 36.dp) .align(Alignment.CenterStart) .focusRequester(focusRequester) .onFocusChanged { state -> diff --git a/core-ui/src/main/res/drawable/ic_checked_24.xml b/core-ui/src/main/res/drawable/ic_checked_24.xml index 6d63841daf..3231b1d3fc 100644 --- a/core-ui/src/main/res/drawable/ic_checked_24.xml +++ b/core-ui/src/main/res/drawable/ic_checked_24.xml @@ -4,7 +4,7 @@ android:viewportWidth="24" android:viewportHeight="24"> + + diff --git a/core-ui/src/main/res/drawable/ic_search_18.xml b/core-ui/src/main/res/drawable/ic_search_18.xml index b155209b54..2dde6884dd 100644 --- a/core-ui/src/main/res/drawable/ic_search_18.xml +++ b/core-ui/src/main/res/drawable/ic_search_18.xml @@ -5,6 +5,6 @@ android:viewportHeight="18"> diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt index 2e5ac6a526..1b89317ad9 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt @@ -947,6 +947,10 @@ class BlockDataRepository( return remote.deleteRelationOption(command) } + override suspend fun setRelationOptionOrder(command: Command.SetRelationOptionsOrder): List { + return remote.setRelationOptionOrder(command) + } + override suspend fun makeSpaceShareable(space: SpaceId) { remote.makeSpaceShareable(space) } diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt index 04531727e3..8a285e85c2 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt @@ -410,6 +410,7 @@ interface BlockRemote { suspend fun debugStackGoroutines(path: String) suspend fun deleteRelationOption(command: Command.DeleteRelationOptions) + suspend fun setRelationOptionOrder(command: Command.SetRelationOptionsOrder): List suspend fun makeSpaceShareable(space: SpaceId) suspend fun generateSpaceInviteLink( diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt index 84ebfc972d..2f744be0ff 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt @@ -461,6 +461,7 @@ interface BlockRepository { suspend fun createTemplateFromObject(ctx: Id): Id suspend fun debugStackGoroutines(path: String) suspend fun deleteRelationOption(command: Command.DeleteRelationOptions) + suspend fun setRelationOptionOrder(command: Command.SetRelationOptionsOrder): List suspend fun makeSpaceShareable(space: SpaceId) suspend fun generateSpaceInviteLink( diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/object/ObjectWrapperExt.kt b/domain/src/main/java/com/anytypeio/anytype/domain/object/ObjectWrapperExt.kt index 5dd76527d3..2ce63a2909 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/object/ObjectWrapperExt.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/object/ObjectWrapperExt.kt @@ -10,6 +10,7 @@ import com.anytypeio.anytype.core_models.ObjectWrapper fun ObjectWrapper.Basic.amend(diff: Map) = ObjectWrapper.Basic(map + diff) fun ObjectWrapper.Relation.amend(diff: Map) = ObjectWrapper.Relation(map + diff) fun ObjectWrapper.Type.amend(diff: Map) = ObjectWrapper.Type(map + diff) +fun ObjectWrapper.Option.amend(diff: Map) = ObjectWrapper.Option(map + diff) /** * Function for applying granular changes in object. */ @@ -31,6 +32,12 @@ fun ObjectWrapper.Type.unset(keys: List) = ObjectWrapper.Type( } ) +fun ObjectWrapper.Option.unset(keys: List) = ObjectWrapper.Option( + map.toMutableMap().apply { + keys.forEach { k -> remove(k) } + } +) + fun List.move(target: Id, afterId: Id?) : List { val result = toMutableList() val targetIdx = indexOfFirst { it == target } diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/objects/StoreOfRelationOptions.kt b/domain/src/main/java/com/anytypeio/anytype/domain/objects/StoreOfRelationOptions.kt new file mode 100644 index 0000000000..ce282cd71f --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/objects/StoreOfRelationOptions.kt @@ -0,0 +1,217 @@ +package com.anytypeio.anytype.domain.objects + +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Key +import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.Struct +import com.anytypeio.anytype.domain.`object`.amend +import com.anytypeio.anytype.domain.`object`.unset +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import java.util.concurrent.atomic.AtomicInteger +import kotlinx.coroutines.channels.BufferOverflow + +interface StoreOfRelationOptions { + val size: Int + suspend fun observe(): Flow> + suspend fun getById(id: Id): ObjectWrapper.Option? + suspend fun getById(ids: List): List + suspend fun getByRelationKey(key: Key): List + suspend fun getAll(): List + suspend fun merge(options: List) + suspend fun amend(target: Id, diff: Map) + suspend fun unset(target: Id, keys: List) + suspend fun set(target: Id, data: Struct) + suspend fun remove(target: Id) + suspend fun clear() + + fun trackChanges(): Flow + + sealed class TrackedEvent { + object Init : TrackedEvent() + object Change : TrackedEvent() + } +} + +class DefaultStoreOfRelationOptions : StoreOfRelationOptions { + + private val mutex = Mutex() + private val store = mutableMapOf() + private val relationKeyToIds = mutableMapOf>() + private val atomicSize = AtomicInteger(0) + + private val updates = MutableSharedFlow( + replay = 1, + extraBufferCapacity = 64, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + override val size: Int get() = atomicSize.get() + + override suspend fun getById(id: Id): ObjectWrapper.Option? = mutex.withLock { + store[id] + } + + override suspend fun getById(ids: List): List = mutex.withLock { + ids.mapNotNull { id -> store[id] } + } + + override suspend fun getByRelationKey(key: Key): List = mutex.withLock { + relationKeyToIds[key]?.mapNotNull { id -> store[id] } ?: emptyList() + } + + override suspend fun getAll(): List = mutex.withLock { + store.values.toList() + } + + override suspend fun merge(options: List) { + var changed = false + var added = 0 + mutex.withLock { + options.forEach { option -> + val current = store[option.id] + if (current == null) { + store[option.id] = option + addToRelationKeyIndex(option) + added++ + changed = true + } else { + val amended = current.amend(option.map) + if (amended !== current) { + // Update relationKey index if relationKey changed + val oldRelationKey = current.relationKey + val newRelationKey = amended.relationKey + if (oldRelationKey != newRelationKey) { + removeFromRelationKeyIndex(current) + addToRelationKeyIndex(amended) + } + store[option.id] = amended + changed = true + } + } + } + if (added > 0) atomicSize.addAndGet(added) + } + if (changed) updates.tryEmit(StoreOfRelationOptions.TrackedEvent.Change) + } + + override suspend fun amend(target: Id, diff: Map) { + var changed = false + var inserted = false + mutex.withLock { + val current = store[target] + val amended = current?.amend(diff) ?: ObjectWrapper.Option(diff).also { + addToRelationKeyIndex(it) + inserted = true + } + if (amended !== current) { + if (current != null) { + val oldRelationKey = current.relationKey + val newRelationKey = amended.relationKey + if (oldRelationKey != newRelationKey) { + removeFromRelationKeyIndex(current) + addToRelationKeyIndex(amended) + } + } + store[target] = amended + changed = true + if (inserted) atomicSize.incrementAndGet() + } + } + if (changed) updates.tryEmit(StoreOfRelationOptions.TrackedEvent.Change) + } + + override suspend fun set(target: Id, data: Struct) { + var wasAbsent = false + mutex.withLock { + val existed = store[target] + if (existed != null) { + removeFromRelationKeyIndex(existed) + } + val newOption = ObjectWrapper.Option(data) + store[target] = newOption + addToRelationKeyIndex(newOption) + wasAbsent = existed == null + if (wasAbsent) atomicSize.incrementAndGet() + } + updates.tryEmit(StoreOfRelationOptions.TrackedEvent.Change) + } + + override suspend fun unset(target: Id, keys: List) { + var changed = false + mutex.withLock { + val current = store[target] + if (current != null) { + val next = current.unset(keys) + if (next !== current) { + // Update relationKey index if relationKey was unset + if (keys.contains(Relations.RELATION_KEY)) { + removeFromRelationKeyIndex(current) + addToRelationKeyIndex(next) + } + store[target] = next + changed = true + } + } + } + if (changed) updates.tryEmit(StoreOfRelationOptions.TrackedEvent.Change) + } + + override suspend fun remove(target: Id) { + var removed = false + mutex.withLock { + val current = store.remove(target) + if (current != null) { + removeFromRelationKeyIndex(current) + removed = true + atomicSize.decrementAndGet() + } + } + if (removed) updates.tryEmit(StoreOfRelationOptions.TrackedEvent.Change) + } + + override suspend fun clear() { + var hadItems = false + mutex.withLock { + hadItems = store.isNotEmpty() + if (hadItems) { + relationKeyToIds.clear() + store.clear() + atomicSize.set(0) + } + } + if (hadItems) updates.tryEmit(StoreOfRelationOptions.TrackedEvent.Change) + } + + override fun trackChanges(): Flow = updates.onStart { + emit(StoreOfRelationOptions.TrackedEvent.Init) + } + + override suspend fun observe(): Flow> { + return trackChanges().map { + mutex.withLock { store.toMap() } + } + } + + private fun addToRelationKeyIndex(option: ObjectWrapper.Option) { + val relationKey = option.relationKey + if (relationKey != null) { + relationKeyToIds.getOrPut(relationKey) { mutableSetOf() }.add(option.id) + } + } + + private fun removeFromRelationKeyIndex(option: ObjectWrapper.Option) { + val relationKey = option.relationKey + if (relationKey != null) { + relationKeyToIds[relationKey]?.remove(option.id) + if (relationKeyToIds[relationKey]?.isEmpty() == true) { + relationKeyToIds.remove(relationKey) + } + } + } +} diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/relations/SetRelationOptionOrder.kt b/domain/src/main/java/com/anytypeio/anytype/domain/relations/SetRelationOptionOrder.kt new file mode 100644 index 0000000000..c95086fd2b --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/relations/SetRelationOptionOrder.kt @@ -0,0 +1,34 @@ +package com.anytypeio.anytype.domain.relations + +import com.anytypeio.anytype.core_models.Command +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Key +import com.anytypeio.anytype.core_models.primitives.RelationKey +import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import javax.inject.Inject + +class SetRelationOptionOrder @Inject constructor( + private val repo: BlockRepository, + dispatchers: AppCoroutineDispatchers +) : ResultInteractor(dispatchers.io) { + + override suspend fun doWork(params: Params) { + val command = Command.SetRelationOptionsOrder( + space = params.spaceId, + relationKey = params.relationKey, + orderedIds = params.orderedIds + ) + repo.setRelationOptionOrder( + command + ) + } + + data class Params( + val spaceId: SpaceId, + val relationKey: RelationKey, + val orderedIds: List + ) +} diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationOptionsSubscriptionContainer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationOptionsSubscriptionContainer.kt new file mode 100644 index 0000000000..1d47c7cb71 --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationOptionsSubscriptionContainer.kt @@ -0,0 +1,185 @@ +package com.anytypeio.anytype.domain.search + +import com.anytypeio.anytype.core_models.DVFilter +import com.anytypeio.anytype.core_models.DVSort +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.SubscriptionEvent +import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.debugging.Logger +import com.anytypeio.anytype.domain.`object`.move +import com.anytypeio.anytype.domain.objects.StoreOfRelationOptions +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.withContext + +class RelationOptionsSubscriptionContainer( + private val repo: BlockRepository, + private val channel: SubscriptionEventChannel, + private val store: StoreOfRelationOptions, + private val dispatchers: AppCoroutineDispatchers, + private val logger: Logger +) { + + fun observe(params: Params): Flow { + return flow { + val initial = repo.searchObjectsWithSubscription( + space = params.space, + subscription = params.subscription, + sorts = params.sorts, + filters = params.filters, + offset = params.offset, + limit = params.limit, + keys = params.keys, + source = params.sources, + noDepSubscription = true, + ignoreWorkspace = params.ignoreWorkspace, + afterId = null, + beforeId = null, + collection = null + ) + store.clear() + store.merge( + options = initial.results.map { ObjectWrapper.Option(it.map) } + ) + val sub = Index( + objects = initial.results.map { it.id }, + dependencies = initial.dependencies.map { it.id } + ) + + emitAll( + subscribe(listOf(params.subscription)).scan(sub) { s, payload -> + var result = s + payload.forEach { event -> + when (event) { + is SubscriptionEvent.Add -> { + val afterId = event.afterId + if (afterId != null) { + val afterIdx = result.objects.indexOfFirst { id -> + afterId == id + } + val updated = result.objects.toMutableList().apply { + if (afterIdx != -1) { + add(afterIdx.inc(), event.target) + } else { + add(0, event.target) + } + } + result = result.copy( + objects = updated + ) + } else { + result = result.copy( + objects = result.objects.toMutableList().apply { + add(0, event.target) + } + ) + } + } + is SubscriptionEvent.Amend -> { + store.amend( + target = event.target, + diff = event.diff + ) + } + is SubscriptionEvent.Position -> { + result = result.copy( + objects = result.objects.move( + target = event.target, + afterId = event.afterId + ) + ) + } + is SubscriptionEvent.Remove -> { + result = result.copy( + objects = result.objects.filter { id -> + id != event.target + } + ) + store.remove(target = event.target) + } + is SubscriptionEvent.Set -> { + store.set( + target = event.target, + data = event.data + ) + } + is SubscriptionEvent.Unset -> { + store.unset( + target = event.target, + keys = event.keys + ) + } + else -> { + // Do nothing. + } + } + } + result.copy( + lastModified = System.currentTimeMillis() + ) + } + ) + }.catch { + logger.logException(it, "Error in relation options container") + }.flowOn( + context = dispatchers.io + ) + } + + /** + * Returns events for subscriptions and dependent subscriptions + */ + private fun subscribe(subscriptions: List) = channel.subscribe(subscriptions) + + suspend fun unsubscribe() = withContext(dispatchers.io) { + runCatching { + store.clear() + repo.cancelObjectSearchSubscription( + listOf(SUBSCRIPTION_ID) + ) + } + } + + data class Params( + val space: SpaceId, + val subscription: Id, + val sorts: List, + val filters: List, + val sources: List, + val offset: Long, + val limit: Int, + val keys: List, + val ignoreWorkspace: Boolean + ) + + /** + * Index for keeping track of results and its dependencies. + * @property [objects] data for this subscription + * @property [dependencies] its dependencies + * @property [lastModified] timestamp for data modification + */ + data class Index( + val objects: List = emptyList(), + val dependencies: List = emptyList(), + val lastModified: Long = 0L + ) { + companion object { + fun empty() = Index( + objects = emptyList(), + dependencies = emptyList(), + lastModified = 0L + ) + } + } + + companion object { + const val SUBSCRIPTION_ID = "relation-options-store-subscription" + } +} diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationOptionsSubscriptionManager.kt b/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationOptionsSubscriptionManager.kt new file mode 100644 index 0000000000..6cfbf7053f --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationOptionsSubscriptionManager.kt @@ -0,0 +1,128 @@ +package com.anytypeio.anytype.domain.search + +import com.anytypeio.anytype.core_models.Config +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.RelationFormat +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.domain.subscriptions.GlobalSubscription +import com.anytypeio.anytype.domain.workspace.SpaceManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch + +class RelationOptionsSubscriptionManager( + private val scope: CoroutineScope = GlobalScope, + private val container: RelationOptionsSubscriptionContainer, + private val spaceManager: SpaceManager +) : GlobalSubscription { + + val pipeline get() = spaceManager.state().flatMapLatest { state -> + when (state) { + is SpaceManager.State.Space.Active -> { + val params = buildParams(state.config) + container.observe(params) + } + is SpaceManager.State.Space.Idle -> { + flow { + emit(RelationOptionsSubscriptionContainer.Index.empty()) + } + } + is SpaceManager.State.NoSpace -> { + flow { + container.unsubscribe() + emit(RelationOptionsSubscriptionContainer.Index.empty()) + } + } + is SpaceManager.State.Init -> { + emptyFlow() + } + } + } + + private var job: Job? = null + + fun onStart() { + job?.cancel() + job = scope.launch { + pipeline.collect() + } + } + + fun onStop() { + scope.launch { + container.unsubscribe() + job?.cancel() + job = null + } + } + + companion object { + fun buildParams(config: Config) = + RelationOptionsSubscriptionContainer.Params( + space = SpaceId(config.space), + subscription = RelationOptionsSubscriptionContainer.SUBSCRIPTION_ID, + filters = listOf( + DVFilter( + relation = Relations.LAYOUT, + condition = DVFilterCondition.EQUAL, + value = ObjectType.Layout.RELATION_OPTION.code.toDouble() + ), + DVFilter( + relation = Relations.IS_DELETED, + condition = DVFilterCondition.NOT_EQUAL, + value = true + ), + DVFilter( + relation = Relations.IS_ARCHIVED, + condition = DVFilterCondition.NOT_EQUAL, + value = true + ), + DVFilter( + relation = Relations.IS_HIDDEN, + condition = DVFilterCondition.NOT_EQUAL, + value = true + ), + DVFilter( + relation = Relations.IS_HIDDEN_DISCOVERY, + condition = DVFilterCondition.NOT_EQUAL, + value = true + ) + ), + limit = 0, + offset = 0L, + sorts = listOf( + DVSort( + relationKey = Relations.RELATION_OPTION_ORDER, + type = DVSortType.ASC, + relationFormat = RelationFormat.LONG_TEXT + ), + DVSort( + relationKey = Relations.NAME, + type = DVSortType.ASC, + relationFormat = RelationFormat.LONG_TEXT + ) + ), + sources = emptyList(), + keys = listOf( + Relations.ID, + Relations.SPACE_ID, + Relations.NAME, + Relations.RELATION_OPTION_COLOR, + Relations.RELATION_KEY, + Relations.RELATION_OPTION_ORDER, + Relations.ORDER_ID + ), + ignoreWorkspace = true + ) + } +} diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/subscriptions/GlobalSubscriptionManager.kt b/domain/src/main/java/com/anytypeio/anytype/domain/subscriptions/GlobalSubscriptionManager.kt index 8776b5d575..6fe7bfbdd1 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/subscriptions/GlobalSubscriptionManager.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/subscriptions/GlobalSubscriptionManager.kt @@ -7,6 +7,7 @@ import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.notifications.PushKeyProvider import com.anytypeio.anytype.domain.search.ObjectTypesSubscriptionManager import com.anytypeio.anytype.domain.search.ProfileSubscriptionManager +import com.anytypeio.anytype.domain.search.RelationOptionsSubscriptionManager import com.anytypeio.anytype.domain.search.RelationsSubscriptionManager import com.anytypeio.anytype.domain.spaces.SpaceDeletedStatusWatcher import javax.inject.Inject @@ -19,6 +20,7 @@ interface GlobalSubscriptionManager { class Default @Inject constructor( private val types: ObjectTypesSubscriptionManager, private val relations: RelationsSubscriptionManager, + private val relationOptions: RelationOptionsSubscriptionManager, private val permissions: UserPermissionProvider, private val isSpaceDeleted: SpaceDeletedStatusWatcher, private val profile: ProfileSubscriptionManager, @@ -33,6 +35,7 @@ interface GlobalSubscriptionManager { pushKeyProvider.start() types.onStart() relations.onStart() + relationOptions.onStart() permissions.start() isSpaceDeleted.onStart() profile.onStart() @@ -45,6 +48,7 @@ interface GlobalSubscriptionManager { pushKeyProvider.stop() types.onStop() relations.onStop() + relationOptions.onStop() permissions.stop() isSpaceDeleted.onStop() profile.onStop() diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 9e5c92072a..e4b8a76686 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -281,7 +281,7 @@ Paste Select Search - Search... + Search Properties Fields Download diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt index 9379c91fc2..d17ac1f339 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt @@ -910,6 +910,10 @@ class BlockMiddleware( return middleware.deleteRelationOptions(command) } + override suspend fun setRelationOptionOrder(command: Command.SetRelationOptionsOrder): List { + return middleware.setRelationOptionsOrder(command) + } + override suspend fun makeSpaceShareable(space: SpaceId) { middleware.makeSpaceShareable(space = space) } diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt index d9bbe22ec4..42cf47bec3 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt @@ -2490,6 +2490,19 @@ class Middleware @Inject constructor( logResponseIfDebug(response, time) } + @Throws(Exception::class) + fun setRelationOptionsOrder(command: Command.SetRelationOptionsOrder): List { + val request = Rpc.Relation.Option.SetOrder.Request( + spaceId = command.space.id, + relationKey = command.relationKey.key, + relationOptionOrder = command.orderedIds + ) + logRequestIfDebug(request) + val (response, time) = measureTimedValue { service.setRelationOptionsOrder(request) } + logResponseIfDebug(response, time) + return response.relationOptionOrder + } + @Throws(Exception::class) fun debugStackGoroutines(path: String) { val request = Rpc.Debug.StackGoroutines.Request( diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt index a137273396..edc7765354 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt @@ -93,6 +93,9 @@ interface MiddlewareService { @Throws(Exception::class) fun deleteRelationOptions(request: Rpc.Relation.ListRemoveOption.Request): Rpc.Relation.ListRemoveOption.Response + @Throws(Exception::class) + fun setRelationOptionsOrder(request: Rpc.Relation.Option.SetOrder.Request): Rpc.Relation.Option.SetOrder.Response + @Throws(Exception::class) fun objectCreateSet(request: Rpc.Object.CreateSet.Request): Rpc.Object.CreateSet.Response diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt index 4f5efad8b2..5c5fbb8f99 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt @@ -1915,6 +1915,19 @@ class MiddlewareServiceImplementation @Inject constructor( } } + override fun setRelationOptionsOrder(request: Rpc.Relation.Option.SetOrder.Request): Rpc.Relation.Option.SetOrder.Response { + val encoded = Service.relationOptionSetOrder( + Rpc.Relation.Option.SetOrder.Request.ADAPTER.encode(request) + ) + val response = Rpc.Relation.Option.SetOrder.Response.ADAPTER.decode(encoded) + val error = response.error + if (error != null && error.code != Rpc.Relation.Option.SetOrder.Response.Error.Code.NULL) { + throw Exception(error.description) + } else { + return response + } + } + override fun spaceInviteGenerate(request: Rpc.Space.InviteGenerate.Request): Rpc.Space.InviteGenerate.Response { val encoded = Service.spaceInviteGenerate( Rpc.Space.InviteGenerate.Request.ADAPTER.encode(request) diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/DataViewObjectRelationProvider.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/DataViewObjectRelationProvider.kt index 4e4649d03b..a95b5d728d 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/DataViewObjectRelationProvider.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/DataViewObjectRelationProvider.kt @@ -1,11 +1,9 @@ package com.anytypeio.anytype.presentation.relations.providers import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_models.Key import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.presentation.sets.state.ObjectState -import kotlin.collections.mapNotNull import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -13,30 +11,12 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf class DataViewObjectRelationProvider( private val objectState: StateFlow, private val storeOfRelations: StoreOfRelations ) : ObjectRelationProvider { - override suspend fun getOrNull(relation: Key): ObjectWrapper.Relation? { - return storeOfRelations.getByKey(relation) - } - - @OptIn(ExperimentalCoroutinesApi::class) - override suspend fun observeRelation(relation: Key): Flow { - return storeOfRelations.trackChanges() - .flatMapLatest { _ -> - val relation = storeOfRelations.getByKey(relation) - if (relation != null) { - flowOf(relation) - } else { - emptyFlow() - } - } - } - @OptIn(ExperimentalCoroutinesApi::class) override suspend fun observeAll(id: Id): Flow> { return combine( @@ -81,10 +61,6 @@ class SetOrCollectionRelationProvider( private val storeOfRelations: StoreOfRelations ) : ObjectRelationProvider { - override suspend fun getOrNull(relation: Key): ObjectWrapper.Relation? { - return storeOfRelations.getByKey(relation) - } - @OptIn(ExperimentalCoroutinesApi::class) override suspend fun observeAll(id: Id): Flow> { return combine( @@ -114,18 +90,4 @@ class SetOrCollectionRelationProvider( } } } - - @OptIn(ExperimentalCoroutinesApi::class) - override suspend fun observeRelation(relation: Key): Flow { - return storeOfRelations.trackChanges() - .flatMapLatest { _ -> - val relation = storeOfRelations.getByKey(relation) - if (relation != null) { - flowOf(relation) - } else { - emptyFlow() - } - } - } - } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/DefaultObjectRelationProvider.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/DefaultObjectRelationProvider.kt index 9021a68c09..b54d213b95 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/DefaultObjectRelationProvider.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/DefaultObjectRelationProvider.kt @@ -1,27 +1,20 @@ package com.anytypeio.anytype.presentation.relations.providers import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_models.Key import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.presentation.editor.Editor import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf class DefaultObjectRelationProvider( private val storeOfRelations: StoreOfRelations, private val storage: Editor.Storage ) : ObjectRelationProvider { - override suspend fun getOrNull(relation: Key): ObjectWrapper.Relation? { - return storeOfRelations.getByKey(relation) - } - @OptIn(ExperimentalCoroutinesApi::class) override suspend fun observeAll(id: Id): Flow> = combine( @@ -37,17 +30,4 @@ class DefaultObjectRelationProvider( } } } - - @OptIn(ExperimentalCoroutinesApi::class) - override suspend fun observeRelation(relation: Key): Flow { - return storeOfRelations.trackChanges() - .flatMapLatest { _ -> - val relation = storeOfRelations.getByKey(relation) - if (relation != null) { - flowOf(relation) - } else { - emptyFlow() - } - } - } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/ObjectRelationProvider.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/ObjectRelationProvider.kt index b4996b06a3..c5bfd773f5 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/ObjectRelationProvider.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/providers/ObjectRelationProvider.kt @@ -1,13 +1,10 @@ package com.anytypeio.anytype.presentation.relations.providers import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_models.Key import com.anytypeio.anytype.core_models.ObjectWrapper import kotlinx.coroutines.flow.Flow interface ObjectRelationProvider { - suspend fun getOrNull(relation: Key): ObjectWrapper.Relation? - suspend fun observeRelation(relation: Key): Flow suspend fun observeAll(id: Id): Flow> companion object { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/object/ObjectValueViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/object/ObjectValueViewModel.kt index feb02a9315..ce080087a1 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/object/ObjectValueViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/object/ObjectValueViewModel.kt @@ -9,7 +9,6 @@ import com.anytypeio.anytype.core_models.ObjectTypeIds import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Payload import com.anytypeio.anytype.core_models.Relation.Format.FILE -import com.anytypeio.anytype.core_models.multiplayer.SpaceUxType import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction import com.anytypeio.anytype.core_utils.ext.typeOf @@ -31,7 +30,6 @@ import com.anytypeio.anytype.presentation.navigation.DefaultObjectView import com.anytypeio.anytype.presentation.home.OpenObjectNavigation import com.anytypeio.anytype.presentation.home.navigation import com.anytypeio.anytype.presentation.objects.toView -import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider import com.anytypeio.anytype.presentation.relations.value.tagstatus.RelationContext import com.anytypeio.anytype.presentation.search.ObjectSearchConstants @@ -49,7 +47,6 @@ import timber.log.Timber class ObjectValueViewModel( private val viewModelParams: ViewModelParams, - private val relations: ObjectRelationProvider, private val values: ObjectValueProvider, private val dispatcher: Dispatcher, private val setObjectDetails: UpdateDetail, @@ -79,7 +76,7 @@ class ObjectValueViewModel( init { Timber.d("ObjectValueViewModel init, params: $viewModelParams") viewModelScope.launch { - val relation = relations.getOrNull(relation = viewModelParams.relationKey) ?: return@launch + val relation = storeOfRelations.getByKey(viewModelParams.relationKey) ?: return@launch setupIsRelationNotEditable(relation) combine( values.subscribe( diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/object/ObjectValueViewModelFactory.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/object/ObjectValueViewModelFactory.kt index 4bec7cc78c..807db35d07 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/object/ObjectValueViewModelFactory.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/object/ObjectValueViewModelFactory.kt @@ -15,14 +15,12 @@ import com.anytypeio.anytype.domain.primitives.FieldParser import com.anytypeio.anytype.domain.search.SearchObjects import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate -import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider import com.anytypeio.anytype.presentation.util.Dispatcher import javax.inject.Inject class ObjectValueViewModelFactory @Inject constructor( private val params: ObjectValueViewModel.ViewModelParams, - private val relations: ObjectRelationProvider, private val values: ObjectValueProvider, private val dispatcher: Dispatcher, private val setObjectDetails: UpdateDetail, @@ -43,7 +41,6 @@ class ObjectValueViewModelFactory @Inject constructor( modelClass: Class ) = ObjectValueViewModel( viewModelParams = params, - relations = relations, values = values, dispatcher = dispatcher, setObjectDetails = setObjectDetails, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagOrStatusValueViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagOrStatusValueViewModel.kt index 58a4f697dd..64395111ef 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagOrStatusValueViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagOrStatusValueViewModel.kt @@ -9,85 +9,97 @@ import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Payload import com.anytypeio.anytype.core_models.Relation import com.anytypeio.anytype.core_models.ThemeColor +import com.anytypeio.anytype.core_models.primitives.RelationKey import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_utils.ext.typeOf import com.anytypeio.anytype.domain.base.fold -import com.anytypeio.anytype.domain.library.StoreSearchParams -import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.objects.StoreOfRelationOptions import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.domain.relations.DeleteRelationOptions -import com.anytypeio.anytype.domain.workspace.SpaceManager +import com.anytypeio.anytype.domain.relations.SetRelationOptionOrder import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate import com.anytypeio.anytype.presentation.common.BaseViewModel import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationEvent -import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider -import com.anytypeio.anytype.presentation.search.ObjectSearchConstants import com.anytypeio.anytype.presentation.sets.filterIdsById import com.anytypeio.anytype.presentation.util.Dispatcher +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import timber.log.Timber class TagOrStatusValueViewModel( private val viewModelParams: ViewModelParams, - private val relations: ObjectRelationProvider, private val values: ObjectValueProvider, private val dispatcher: Dispatcher, private val setObjectDetails: UpdateDetail, private val analytics: Analytics, - private val spaceManager: SpaceManager, - private val subscription: StorelessSubscriptionContainer, private val deleteRelationOptions: DeleteRelationOptions, + private val setRelationOptionOrder: SetRelationOptionOrder, private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate, - private val storeOfRelations: StoreOfRelations + private val storeOfRelations: StoreOfRelations, + private val storeOfRelationOptions: StoreOfRelationOptions ) : BaseViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate { val viewState = MutableStateFlow(TagStatusViewState.Loading) - private val query = MutableSharedFlow(replay = 0) + + private val input = MutableStateFlow("") + val queryState: StateFlow = input.asStateFlow() + @OptIn(FlowPreview::class) + private val query = input.take(1).onCompletion { + emitAll( + input.drop(1).debounce(300L).distinctUntilChanged() + ) + } private var isEditableRelation = false val commands = MutableSharedFlow(replay = 0) - private val initialIds = mutableListOf() - private var isInitialSortDone = false + private var isInitialExpandDone = false + + // Lock mechanism to prevent race conditions during DnD operations + // When a drag operation completes, we optimistically update the UI and send to middleware + // We then lock event processing for a short period to prevent incoming events from + // overwriting our optimistic update before middleware confirms the change + private var optionEventLockTimestamp: Long? = null init { viewModelScope.launch { - val relation = relations.getOrNull(relation = viewModelParams.relationKey) ?: return@launch + val relation = storeOfRelations.getByKey(key = viewModelParams.relationKey) ?: return@launch setupIsRelationNotEditable(relation) - val searchParams = StoreSearchParams( - // TODO DROID-2916 Provide space id to vm params - space = SpaceId(spaceManager.get()), - subscription = SUB_MY_OPTIONS, - keys = ObjectSearchConstants.keysRelationOptions, - filters = ObjectSearchConstants.filterRelationOptions( - relationKey = viewModelParams.relationKey - ) - ) combine( values.subscribe( ctx = viewModelParams.ctx, target = viewModelParams.objectId ), - query.onStart { emit("") }, - subscription.subscribe(searchParams) - ) { record, query, options -> + query.distinctUntilChanged(), + storeOfRelationOptions.trackChanges() + ) { record, query, _ -> + // Skip state updates during active drag operations to prevent UI flickering + if (isOptionEventLockActive()) { + Timber.d("DROID-3916, Skipping state update due to active option event lock") + return@combine + } + val options = storeOfRelationOptions.getByRelationKey(viewModelParams.relationKey) + .sortedBy { it.orderId } val ids = getRecordValues(record) - if (!isInitialSortDone) { - initialIds.clear() - if (ids.isNotEmpty()) { - initialIds.addAll(ids) - } else { - if (isEditableRelation) { - emitCommand(Command.Expand) - } + if (!isInitialExpandDone) { + isInitialExpandDone = true + if (ids.isEmpty() && isEditableRelation) { + emitCommand(Command.Expand) } } initViewState( @@ -106,22 +118,18 @@ class TagOrStatusValueViewModel( private fun filterOptions( query: String, - options: List, + options: List, ids: List ): List { return if (isEditableRelation) { - options.map { ObjectWrapper.Option(map = it.map) } - .filter { it.name?.contains(query, true) == true } + options.filter { it.name?.contains(query, true) == true } } else { - options.map { ObjectWrapper.Option(map = it.map) } - .filter { ids.contains(it.id) } + options.filter { ids.contains(it.id) } } } fun onQueryChanged(input: String) { - viewModelScope.launch { - query.emit(input) - } + this.input.value = input } fun proceedWithDeleteOptions(optionId: Id) { @@ -154,9 +162,9 @@ class TagOrStatusValueViewModel( } } - TagStatusAction.Plus -> emitCommand( + TagStatusAction.Plus -> openOptionScreen( Command.OpenOptionScreen( - color = ThemeColor.values().drop(1).random().code, + color = ThemeColor.entries.drop(1).random().code, relationKey = viewModelParams.relationKey, ctx = viewModelParams.ctx, objectId = viewModelParams.objectId @@ -167,7 +175,7 @@ class TagOrStatusValueViewModel( } is TagStatusAction.Duplicate -> { val item = action.item - emitCommand( + openOptionScreen( Command.OpenOptionScreen( color = item.color.code, text = item.name, @@ -179,7 +187,7 @@ class TagOrStatusValueViewModel( } is TagStatusAction.Edit -> { val item = action.item - emitCommand( + openOptionScreen( Command.OpenOptionScreen( optionId = item.optionId, color = item.color.code, @@ -191,7 +199,7 @@ class TagOrStatusValueViewModel( ) } TagStatusAction.Create -> { - emitCommand( + openOptionScreen( Command.OpenOptionScreen( text = "", relationKey = viewModelParams.relationKey, @@ -200,6 +208,34 @@ class TagOrStatusValueViewModel( ) ) } + is TagStatusAction.OnMove -> { + Timber.d("OnMove from ${action.from} to ${action.to}") + viewModelScope.launch { + val currentState = viewState.value + if (currentState !is TagStatusViewState.Content) return@launch + val reorderedIds = currentState.items + .toMutableList() + .apply { add(action.to, removeAt(action.from)) } + .map { it.optionId } + // Activate lock before sending to middleware to prevent race conditions + activateOptionEventLock() + setRelationOptionOrder.async( + SetRelationOptionOrder.Params( + spaceId = viewModelParams.space, + relationKey = RelationKey(viewModelParams.relationKey), + orderedIds = reorderedIds + ) + ).fold( + onSuccess = { + Timber.d("Option order saved successfully") + }, + onFailure = { e -> + Timber.e(e, "Failed to save option order") + sendToast("Failed to save order") + } + ) + } + } } } @@ -210,6 +246,12 @@ class TagOrStatusValueViewModel( } } + private fun openOptionScreen(command: Command.OpenOptionScreen) { + viewModelScope.launch { + commands.emit(command) + } + } + private fun onActionClick(item: RelationsListItem) { when (item) { is RelationsListItem.Item.Status -> { @@ -226,17 +268,8 @@ class TagOrStatusValueViewModel( addTag(item.optionId) } } - is RelationsListItem.CreateItem.Status -> { - emitCommand( - Command.OpenOptionScreen( - text = item.text, - relationKey = viewModelParams.relationKey, - ctx = viewModelParams.ctx, - objectId = viewModelParams.objectId - ) - ) - } is RelationsListItem.CreateItem.Tag -> { + input.value = "" // Clear the query so user sees full list after creating option emitCommand( Command.OpenOptionScreen( text = item.text, @@ -255,7 +288,9 @@ class TagOrStatusValueViewModel( options: List, query: String ) { - val result = mutableListOf() + val result = mutableListOf() + val isTagRelation = relation.format == Relation.Format.TAG + when (relation.format) { Relation.Format.STATUS -> { result.addAll( @@ -264,9 +299,6 @@ class TagOrStatusValueViewModel( options = options ) ) - if (query.isNotBlank()) { - result.add(RelationsListItem.CreateItem.Status(query)) - } } Relation.Format.TAG -> { result.addAll( @@ -275,16 +307,20 @@ class TagOrStatusValueViewModel( options = options ) ) - if (query.isNotBlank()) { - result.add(RelationsListItem.CreateItem.Tag(query)) - } } else -> { Timber.w("Relation format should be Tag or Status but was: ${relation.format}") } } - viewState.value = if (result.isEmpty()) { + // CreateItem is only shown for TAG relations when there's a search query + val createItem = if (isTagRelation && query.isNotBlank() && isEditableRelation) { + RelationsListItem.CreateItem.Tag(query) + } else { + null + } + + viewState.value = if (result.isEmpty() && createItem == null) { TagStatusViewState.Empty( isRelationEditable = isEditableRelation, title = relation.name.orEmpty(), @@ -293,8 +329,11 @@ class TagOrStatusValueViewModel( TagStatusViewState.Content( isRelationEditable = isEditableRelation, title = relation.name.orEmpty(), - items = result + items = result, + createItem = createItem ) + }.also { + Timber.d("TagStatusViewModel initViewState, viewState: $it") } } @@ -332,7 +371,7 @@ class TagOrStatusValueViewModel( else EventsDictionary.relationChangeValue, storeOfRelations = storeOfRelations, relationKey = viewModelParams.relationKey, - spaceParams = provideParams(spaceManager.get()) + spaceParams = provideParams(viewModelParams.space.id) ) } ) @@ -358,7 +397,7 @@ class TagOrStatusValueViewModel( else EventsDictionary.relationChangeValue, storeOfRelations = storeOfRelations, relationKey = viewModelParams.relationKey, - spaceParams = provideParams(spaceManager.get()) + spaceParams = provideParams(viewModelParams.space.id) ) }) } @@ -380,7 +419,7 @@ class TagOrStatusValueViewModel( eventName = EventsDictionary.relationChangeValue, storeOfRelations = storeOfRelations, relationKey = viewModelParams.relationKey, - spaceParams = provideParams(spaceManager.get()) + spaceParams = provideParams(viewModelParams.space.id) ) emitCommand(command = Command.Dismiss, delay = DELAY_UNTIL_CLOSE) } @@ -404,61 +443,48 @@ class TagOrStatusValueViewModel( eventName = EventsDictionary.relationDeleteValue, storeOfRelations = storeOfRelations, relationKey = viewModelParams.relationKey, - spaceParams = provideParams(spaceManager.get()) + spaceParams = provideParams(viewModelParams.space.id) ) } ) } } + /** + * Maps options to Tag items. + * Options from store are already sorted by relationOptionOrder. + */ private fun mapTagOptions( ids: List, options: List ) = options.map { option -> - val index = ids.indexOf(option.id) - val isSelected = index != -1 - val number = if (isSelected) index + 1 else Int.MAX_VALUE RelationsListItem.Item.Tag( optionId = option.id, name = option.name.orEmpty(), color = getOrDefault(option.color), - isSelected = isSelected, - number = number + isSelected = ids.contains(option.id), + number = ids.indexOf(option.id).takeIf { it != -1 }?.plus(1) ?: Int.MAX_VALUE ) - }.let { mappedOptions -> - if (!isInitialSortDone) { - isInitialSortDone = true - mappedOptions.sortedWith( - compareBy( - { !initialIds.contains(it.optionId) }, - { it.number }) - ) - } else { - mappedOptions.sortedWith( - compareBy( - { !initialIds.contains(it.optionId) }, - { initialIds.indexOf(it.optionId) }) - ) - } } + /** + * Maps options to Status items. + * Options from store are already sorted by relationOptionOrder, then by name. + */ private fun mapStatusOptions( ids: List, options: List ) = options.map { option -> - val index = ids.indexOf(option.id) - val isSelected = index != -1 - isInitialSortDone = true RelationsListItem.Item.Status( optionId = option.id, name = option.name.orEmpty(), color = getOrDefault(option.color), - isSelected = isSelected + isSelected = ids.contains(option.id) ) } private fun getOrDefault(code: String?): ThemeColor { - return ThemeColor.values().find { it.code == code } ?: ThemeColor.DEFAULT + return ThemeColor.entries.find { it.code == code } ?: ThemeColor.DEFAULT } private fun setupIsRelationNotEditable(relation: ObjectWrapper.Relation) { @@ -470,12 +496,34 @@ class TagOrStatusValueViewModel( || !relation.isValid) } - override fun onCleared() { - super.onCleared() - viewModelScope.launch { - subscription.unsubscribe(listOf(SUB_MY_OPTIONS)) + //region Option Event Lock + /** + * Activates the event lock for options to prevent race conditions. + * Should be called before sending a drag-and-drop order change to middleware. + */ + private fun activateOptionEventLock() { + optionEventLockTimestamp = System.currentTimeMillis() + Timber.d("DROID-3916, Option event lock activated at $optionEventLockTimestamp") + } + + /** + * Checks if the option event lock is currently active. + * The lock is active if it was set within the last OPTION_EVENT_LOCK_DURATION_MS milliseconds. + */ + private fun isOptionEventLockActive(): Boolean { + val lockTimestamp = optionEventLockTimestamp ?: return false + val currentTime = System.currentTimeMillis() + val elapsedTime = currentTime - lockTimestamp + val isActive = elapsedTime < OPTION_EVENT_LOCK_DURATION_MS + + if (!isActive) { + Timber.d("DROID-3916, Option event lock expired (elapsed: ${elapsedTime}ms)") + optionEventLockTimestamp = null } + + return isActive } + //endregion data class ViewModelParams( val ctx: Id, @@ -512,7 +560,8 @@ sealed class TagStatusViewState { data class Content( val title: String, - val items: List, + val items: List, + val createItem: RelationsListItem.CreateItem.Tag? = null, val isRelationEditable: Boolean, val showItemMenu: RelationsListItem.Item? = null ) : TagStatusViewState() @@ -527,6 +576,7 @@ sealed class TagStatusAction { data class Delete(val optionId: Id) : TagStatusAction() data class Duplicate(val item: RelationsListItem.Item) : TagStatusAction() object Create : TagStatusAction() + data class OnMove(val from: Int, val to: Int) : TagStatusAction() } enum class RelationContext { OBJECT, OBJECT_SET, DATA_VIEW } @@ -561,9 +611,11 @@ sealed class RelationsListItem { val text: String ) : RelationsListItem() { class Tag(text: String) : CreateItem(text) - class Status(text: String) : CreateItem(text) } } -const val SUB_MY_OPTIONS = "subscription.relation-options" const val DELAY_UNTIL_CLOSE = 300L + +// Duration in milliseconds to lock option event processing after a drag operation +// This prevents incoming middleware events from overwriting optimistic UI updates +private const val OPTION_EVENT_LOCK_DURATION_MS = 1500L diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagOrStatusValueViewModelFactory.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagOrStatusValueViewModelFactory.kt index 6705bbbc93..c1bff347d0 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagOrStatusValueViewModelFactory.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/value/tagstatus/TagOrStatusValueViewModelFactory.kt @@ -4,44 +4,41 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.core_models.Payload -import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.objects.StoreOfRelationOptions import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.domain.relations.DeleteRelationOptions -import com.anytypeio.anytype.domain.workspace.SpaceManager +import com.anytypeio.anytype.domain.relations.SetRelationOptionOrder import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate -import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider import com.anytypeio.anytype.presentation.util.Dispatcher import javax.inject.Inject class TagOrStatusValueViewModelFactory @Inject constructor( private val params: TagOrStatusValueViewModel.ViewModelParams, - private val relations: ObjectRelationProvider, private val values: ObjectValueProvider, private val dispatcher: Dispatcher, private val setObjectDetails: UpdateDetail, private val analytics: Analytics, - private val spaceManager: SpaceManager, - private val subscription: StorelessSubscriptionContainer, private val deleteRelationOptions: DeleteRelationOptions, + private val setRelationOptionOrder: SetRelationOptionOrder, private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate, - private val storeOfRelations: StoreOfRelations + private val storeOfRelations: StoreOfRelations, + private val storeOfRelationOptions: StoreOfRelationOptions ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( modelClass: Class ) = TagOrStatusValueViewModel( viewModelParams = params, - relations = relations, values = values, dispatcher = dispatcher, setObjectDetails = setObjectDetails, analytics = analytics, - spaceManager = spaceManager, - subscription = subscription, deleteRelationOptions = deleteRelationOptions, + setRelationOptionOrder = setRelationOptionOrder, analyticSpaceHelperDelegate = analyticSpaceHelperDelegate, - storeOfRelations = storeOfRelations + storeOfRelations = storeOfRelations, + storeOfRelationOptions = storeOfRelationOptions ) as T } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/RelationDateValueViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/RelationDateValueViewModel.kt index e55cd2628b..f9be0a84c5 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/RelationDateValueViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/RelationDateValueViewModel.kt @@ -10,7 +10,7 @@ import com.anytypeio.anytype.core_models.TimeInMillis import com.anytypeio.anytype.core_models.ext.DateParser import com.anytypeio.anytype.core_utils.ext.cancel import com.anytypeio.anytype.domain.misc.DateProvider -import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider +import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow @@ -22,9 +22,9 @@ import kotlinx.coroutines.launch import timber.log.Timber class RelationDateValueViewModel( - private val relations: ObjectRelationProvider, private val values: ObjectValueProvider, - private val dateProvider: DateProvider + private val dateProvider: DateProvider, + private val storeOfRelations: StoreOfRelations ) : ViewModel() { val commands = MutableSharedFlow(0) @@ -43,11 +43,12 @@ class RelationDateValueViewModel( Timber.d("onStart: ctx:[$ctx], relationKey:[$relationKey], objectId:[$objectId]") jobs += viewModelScope.launch { val pipeline = combine( - relations.observeRelation(relationKey), + storeOfRelations.trackChanges(), values.subscribe(ctx = ctx, target = objectId) - ) { relation, value -> + ) { _, value -> + val relation = storeOfRelations.getByKey(key = relationKey) setupIsRelationNotEditable(isLocked, relation) - setName(relation.name) + setName(relation?.name) setDate(timeInSeconds = DateParser.parse(value[relationKey])) } pipeline.collect() @@ -144,7 +145,15 @@ class RelationDateValueViewModel( } } - private fun setupIsRelationNotEditable(isLocked: Boolean, relation: ObjectWrapper.Relation) { + private fun setupIsRelationNotEditable(isLocked: Boolean, relation: ObjectWrapper.Relation?) { + if (relation == null) { + _views.value = views.value.copy( + isEditable = false, + title = null, + timeInMillis = null + ) + return + } if (isLocked || relation.isReadonlyValue || relation.isHidden == true @@ -159,13 +168,13 @@ class RelationDateValueViewModel( } class Factory( - private val relations: ObjectRelationProvider, private val values: ObjectValueProvider, - private val dateProvider: DateProvider + private val dateProvider: DateProvider, + private val storeOfRelations: StoreOfRelations ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { - return RelationDateValueViewModel(relations, values, dateProvider) as T + return RelationDateValueViewModel(values, dateProvider, storeOfRelations) as T } } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/RelationTextValueViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/RelationTextValueViewModel.kt index b8b28b7f96..6500c67959 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/RelationTextValueViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/RelationTextValueViewModel.kt @@ -14,13 +14,13 @@ import com.anytypeio.anytype.core_models.Url import com.anytypeio.anytype.core_utils.intents.SystemAction import com.anytypeio.anytype.domain.`object`.ReloadObject import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.presentation.common.BaseViewModel import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectReload import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationUrlCopy import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationUrlEdit import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationUrlOpen import com.anytypeio.anytype.presentation.number.NumberParser -import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationProvider import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvider import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -28,11 +28,11 @@ import kotlinx.coroutines.launch import timber.log.Timber class RelationTextValueViewModel( - private val relations: ObjectRelationProvider, private val values: ObjectValueProvider, private val reloadObject: ReloadObject, private val analytics: Analytics, - private val storeOfObjectTypes: StoreOfObjectTypes + private val storeOfObjectTypes: StoreOfObjectTypes, + private val storeOfRelations: StoreOfRelations ) : BaseViewModel() { val views = MutableStateFlow>(emptyList()) @@ -65,7 +65,7 @@ class RelationTextValueViewModel( Timber.d("onStart, ctx:[$ctx], relationKey:[$relationKey], object:[$objectId], isLocked:[$isLocked]") viewModelScope.launch { val values = values.get(ctx = ctx, target = objectId) - val relation = relations.getOrNull(relationKey) ?: return@launch + val relation = storeOfRelations.getByKey(key = relationKey) ?: return@launch Timber.d("combine, relation:[$relation], values:[$values]") setupIsRelationNotEditable(relation, isLocked) val obj = ObjectWrapper.Basic(values) @@ -268,20 +268,20 @@ class RelationTextValueViewModel( } class Factory( - private val relations: ObjectRelationProvider, private val values: ObjectValueProvider, private val reloadObject: ReloadObject, private val analytics: Analytics, - private val storeOfObjectTypes: StoreOfObjectTypes + private val storeOfObjectTypes: StoreOfObjectTypes, + private val storeOfRelations: StoreOfRelations ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { return RelationTextValueViewModel( - relations = relations, values = values, reloadObject = reloadObject, analytics = analytics, - storeOfObjectTypes = storeOfObjectTypes + storeOfObjectTypes = storeOfObjectTypes, + storeOfRelations = storeOfRelations ) as T } } diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/relations/providers/FakeObjectRelationProvider.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/relations/providers/FakeObjectRelationProvider.kt index 89759b490a..e99e686d62 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/relations/providers/FakeObjectRelationProvider.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/relations/providers/FakeObjectRelationProvider.kt @@ -1,7 +1,6 @@ package com.anytypeio.anytype.presentation.relations.providers import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_models.Key import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.StubRelationObject import kotlinx.coroutines.flow.Flow @@ -22,19 +21,9 @@ internal class FakeObjectRelationProvider : ObjectRelationProvider { constructor(relation: ObjectWrapper.Relation = StubRelationObject()) : this(listOf(relation)) - override suspend fun getOrNull(relation: Key): ObjectWrapper.Relation? { - return relations.find { it.key == relation } - } - override suspend fun observeAll(id: Id): Flow> { return flow { emit(relations) } } - - override suspend fun observeRelation(relation: Key): Flow { - return flow { - emit(this@FakeObjectRelationProvider.relation) - } - } } \ No newline at end of file