From df2763bc47cd98fa5d3dfa3d4d8cf1c224f2e9f3 Mon Sep 17 00:00:00 2001 From: konstantiniiv Date: Mon, 1 Dec 2025 12:14:27 +0100 Subject: [PATCH 1/5] DROID-4175 participants names for widgets --- .../anytype/di/feature/home/HomescreenDI.kt | 2 + .../presentation/home/HomeScreenViewModel.kt | 5 ++ .../presentation/vault/VaultViewModel.kt | 26 ++++------ .../widgets/ChatListWidgetContainer.kt | 47 ++++++++++--------- .../widgets/WidgetContainerDelegate.kt | 8 +++- 5 files changed, 49 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/home/HomescreenDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/home/HomescreenDI.kt index 4190b7da1b..e065b713a2 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/home/HomescreenDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/home/HomescreenDI.kt @@ -25,6 +25,7 @@ import com.anytypeio.anytype.domain.misc.AppActionManager import com.anytypeio.anytype.domain.misc.DateProvider import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer +import com.anytypeio.anytype.domain.multiplayer.ParticipantSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider @@ -307,6 +308,7 @@ interface HomeScreenDependencies : ComponentDependencies { fun analyticSpaceHelperDelegate(): AnalyticSpaceHelperDelegate fun storeOfRelations(): StoreOfRelations fun spaceViewSubscriptionContainer(): SpaceViewSubscriptionContainer + fun participantSubscriptionContainer(): ParticipantSubscriptionContainer fun featureToggles(): FeatureToggles fun payloadDelegator(): PayloadDelegator fun fieldParser(): FieldParser diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt index a4d3b2eef7..2b3ab9e11f 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt @@ -66,6 +66,7 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.CopyInviteLinkToClipboard import com.anytypeio.anytype.domain.multiplayer.GetSpaceInviteLink +import com.anytypeio.anytype.domain.multiplayer.ParticipantSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider @@ -242,6 +243,7 @@ class HomeScreenViewModel( private val spaceMembers: ActiveSpaceMemberSubscriptionContainer, private val setAsFavourite: SetObjectListIsFavorite, private val chatPreviews: ChatPreviewContainer, + private val participantContainer: ParticipantSubscriptionContainer, private val notificationPermissionManager: NotificationPermissionManager, private val copyInviteLinkToClipboard: CopyInviteLinkToClipboard, private val userSettingsRepository: UserSettingsRepository, @@ -3205,6 +3207,7 @@ class HomeScreenViewModel( WidgetContainerDelegateImpl( spaceId = vmParams.spaceId, chatPreviews = chatPreviews, + participantContainer = participantContainer, spaceViewSubscriptionContainer = spaceViewSubscriptionContainer, notificationPermissionManager = notificationPermissionManager, fieldParser = fieldParser, @@ -3283,6 +3286,7 @@ class HomeScreenViewModel( private val activeSpaceMemberSubscriptionContainer: ActiveSpaceMemberSubscriptionContainer, private val setObjectListIsFavorite: SetObjectListIsFavorite, private val chatPreviews: ChatPreviewContainer, + private val participantContainer: ParticipantSubscriptionContainer, private val notificationPermissionManager: NotificationPermissionManager, private val copyInviteLinkToClipboard: CopyInviteLinkToClipboard, private val userRepo: UserSettingsRepository, @@ -3345,6 +3349,7 @@ class HomeScreenViewModel( spaceMembers = activeSpaceMemberSubscriptionContainer, setAsFavourite = setObjectListIsFavorite, chatPreviews = chatPreviews, + participantContainer = participantContainer, notificationPermissionManager = notificationPermissionManager, copyInviteLinkToClipboard = copyInviteLinkToClipboard, userSettingsRepository = userRepo, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt index 846a3bf796..bdbc4842a7 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt @@ -23,11 +23,11 @@ import com.anytypeio.anytype.domain.base.fold import com.anytypeio.anytype.domain.chats.ChatPreviewContainer import com.anytypeio.anytype.domain.chats.ChatsDetailsSubscriptionContainer import com.anytypeio.anytype.domain.deeplink.PendingIntentStore -import com.anytypeio.anytype.domain.multiplayer.ParticipantSubscriptionContainer import com.anytypeio.anytype.domain.misc.AppActionManager import com.anytypeio.anytype.domain.misc.DateProvider import com.anytypeio.anytype.domain.misc.DeepLinkResolver import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.ParticipantSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider @@ -46,13 +46,13 @@ import com.anytypeio.anytype.domain.vault.UnpinSpace import com.anytypeio.anytype.domain.wallpaper.GetSpaceWallpapers import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.presentation.BuildConfig +import com.anytypeio.anytype.presentation.extension.resolveParticipantName import com.anytypeio.anytype.presentation.home.OpenObjectNavigation import com.anytypeio.anytype.presentation.home.navigation import com.anytypeio.anytype.presentation.mapper.objectIcon import com.anytypeio.anytype.presentation.navigation.DeepLinkToObjectDelegate import com.anytypeio.anytype.presentation.notifications.NotificationPermissionManager import com.anytypeio.anytype.presentation.notifications.NotificationPermissionManagerImpl -import com.anytypeio.anytype.presentation.notifications.NotificationStateCalculator import com.anytypeio.anytype.presentation.notifications.NotificationStateCalculator.calculateChatNotificationState import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.objects.ObjectIcon.FileDefault @@ -493,16 +493,13 @@ class VaultViewModel( chatDetailsMap: Map, participantsByIdentity: Map ): VaultSpaceView.ChatSpace { - val creatorId = chatPreview?.message?.creator val messageText = chatPreview?.message?.content?.text // Resolve creator name from cross-space participants container - val creatorName = if (!creatorId.isNullOrEmpty()) { - val participant = participantsByIdentity[creatorId] - participant?.name ?: participant?.globalName ?: stringResourceProvider.getUntitledCreatorName() - } else { - null - } + val creatorName = participantsByIdentity.resolveParticipantName( + identity = chatPreview?.message?.creator, + fallback = stringResourceProvider.getUntitledCreatorName() + ) val messageTime = chatPreview?.message?.createdAt?.let { timeInSeconds -> if (timeInSeconds > 0) { @@ -565,16 +562,13 @@ class VaultViewModel( chatDetailsMap: Map, participantsByIdentity: Map ): VaultSpaceView.DataSpaceWithChat { - val creatorId = chatPreview.message?.creator val messageText = chatPreview.message?.content?.text // Resolve creator name from cross-space participants container - val creatorName = if (!creatorId.isNullOrEmpty()) { - val participant = participantsByIdentity[creatorId] - participant?.name ?: participant?.globalName ?: stringResourceProvider.getUntitledCreatorName() - } else { - null - } + val creatorName = participantsByIdentity.resolveParticipantName( + identity = chatPreview.message?.creator, + fallback = stringResourceProvider.getUntitledCreatorName() + ) val messageTime = chatPreview.message?.createdAt?.let { timeInSeconds -> if (timeInSeconds > 0) { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/ChatListWidgetContainer.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/ChatListWidgetContainer.kt index 395e8cd2d9..17fd9d151d 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/ChatListWidgetContainer.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/ChatListWidgetContainer.kt @@ -8,34 +8,36 @@ import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.chats.Chat import com.anytypeio.anytype.core_models.chats.NotificationState -import com.anytypeio.anytype.core_utils.const.MimeTypes import com.anytypeio.anytype.core_models.ext.content import com.anytypeio.anytype.core_models.ext.isValidObject import com.anytypeio.anytype.core_models.getSingleValue import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.core_utils.const.MimeTypes import com.anytypeio.anytype.domain.chats.ChatPreviewContainer import com.anytypeio.anytype.domain.library.StoreSearchParams import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.misc.DateProvider import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.ParticipantSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer -import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.domain.`object`.GetObject import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.domain.objects.getTypeOfObject import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider +import com.anytypeio.anytype.presentation.extension.resolveParticipantName import com.anytypeio.anytype.presentation.mapper.objectIcon import com.anytypeio.anytype.presentation.notifications.NotificationStateCalculator import com.anytypeio.anytype.presentation.objects.ObjectIcon -import com.anytypeio.anytype.presentation.relations.cover -import com.anytypeio.anytype.presentation.vault.VaultSpaceView -import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.presentation.search.ObjectSearchConstants import com.anytypeio.anytype.presentation.sets.subscription.updateWithRelationFormat +import com.anytypeio.anytype.presentation.vault.VaultSpaceView import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emptyFlow @@ -69,6 +71,7 @@ class ChatListWidgetContainer( private val dateProvider: DateProvider, private val stringResourceProvider: StringResourceProvider, private val spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer, + private val participantContainer: ParticipantSubscriptionContainer, isSessionActiveFlow: Flow, onRequestCache: () -> WidgetView? = { null }, ) : WidgetContainer { @@ -174,6 +177,14 @@ class ChatListWidgetContainer( .observe() .distinctUntilChanged() + // Observe global participants for creator name resolution + val participantsFlow = participantContainer + .observe() + .map { participants -> + participants.associateBy { it.identity } + } + .distinctUntilChanged() + val chats = view.flatMapLatest { view -> val chats = view.elements.map { it.obj.id } previews @@ -184,7 +195,10 @@ class ChatListWidgetContainer( } .distinctUntilChanged() .flatMapLatest { previewList -> - spaceViews.map { spaces -> + combine( + spaceViews, + participantsFlow + ) { spaces, participantsByIdentity -> view.copy( elements = view.elements.map { element -> val preview = previewList.find { p -> @@ -192,8 +206,12 @@ class ChatListWidgetContainer( } val state = preview?.state if (preview != null && state != null) { - // Extract preview data - val creatorName = extractCreatorName(preview) + // Extract preview data using participant subscription for creator names + val creatorName = + participantsByIdentity.resolveParticipantName( + identity = preview.message?.creator, + fallback = stringResourceProvider.getUntitledCreatorName() + ) val messageText = preview.message?.content?.text val messageTime = preview.message?.createdAt?.let { timeInSeconds -> if (timeInSeconds > 0) { @@ -449,19 +467,6 @@ class ChatListWidgetContainer( ) } } - - /** - * Extracts creator name from chat preview dependencies. - */ - private fun extractCreatorName(preview: Chat.Preview): String? { - val creatorId = preview.message?.creator - if (creatorId.isNullOrEmpty()) return null - - val creatorObj = preview.dependencies.find { - it.getSingleValue(Relations.IDENTITY) == creatorId - } - return creatorObj?.name ?: stringResourceProvider.getUntitledCreatorName() - } /** * Transforms a Chat.Message.Attachment to VaultSpaceView.AttachmentPreview. diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetContainerDelegate.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetContainerDelegate.kt index 7b6e283515..ab5af5635a 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetContainerDelegate.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/WidgetContainerDelegate.kt @@ -11,6 +11,7 @@ import com.anytypeio.anytype.domain.config.UserSettingsRepository import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.misc.DateProvider import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.ParticipantSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.`object`.GetObject import com.anytypeio.anytype.domain.objects.ObjectWatcher @@ -52,6 +53,7 @@ interface WidgetContainerDelegate { class WidgetContainerDelegateImpl( private val spaceId: SpaceId, private val chatPreviews: ChatPreviewContainer, + private val participantContainer: ParticipantSubscriptionContainer, private val spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer, private val notificationPermissionManager: NotificationPermissionManager, private val fieldParser: FieldParser, @@ -214,7 +216,8 @@ class WidgetContainerDelegateImpl( chatPreviewContainer = chatPreviews, dateProvider = dateProvider, stringResourceProvider = stringResourceProvider, - spaceViewSubscriptionContainer = spaceViewSubscriptionContainer + spaceViewSubscriptionContainer = spaceViewSubscriptionContainer, + participantContainer = participantContainer ) } else { DataViewListWidgetContainer( @@ -282,7 +285,8 @@ class WidgetContainerDelegateImpl( chatPreviewContainer = chatPreviews, dateProvider = dateProvider, stringResourceProvider = stringResourceProvider, - spaceViewSubscriptionContainer = spaceViewSubscriptionContainer + spaceViewSubscriptionContainer = spaceViewSubscriptionContainer, + participantContainer = participantContainer ) } else { DataViewListWidgetContainer( From d9884b4821194d45c15cf9c980f21220f89ce46e Mon Sep 17 00:00:00 2001 From: konstantiniiv Date: Mon, 1 Dec 2025 12:14:41 +0100 Subject: [PATCH 2/5] DROID-4175 ext --- .../extension/ParticipantExtensions.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/extension/ParticipantExtensions.kt diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/ParticipantExtensions.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/ParticipantExtensions.kt new file mode 100644 index 0000000000..53cb7f0c38 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/ParticipantExtensions.kt @@ -0,0 +1,23 @@ +package com.anytypeio.anytype.presentation.extension + +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectWrapper + +/** + * Resolves a participant's display name by identity from the participant map. + * Uses the standard fallback chain: name → globalName → fallback. + * + * @param identity The identity ID to look up + * @param fallback The fallback string if participant not found or has no name + * @return The resolved name, or null if identity is null/empty + */ +fun Map.resolveParticipantName( + identity: Id?, + fallback: String +): String? { + if (identity.isNullOrEmpty()) return null + val participant = this[identity] + return participant?.name + ?: participant?.globalName + ?: fallback +} From 495c35b1f3fc5c428b09793e8c05facb1ebb9cc4 Mon Sep 17 00:00:00 2001 From: konstantiniiv Date: Mon, 1 Dec 2025 14:24:32 +0100 Subject: [PATCH 3/5] DROID-4175 tests --- .../anytype/presentation/home/HomeScreenViewModelTest.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt index d1c4dc6af2..0d1ba420cd 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModelTest.kt @@ -54,6 +54,7 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.CopyInviteLinkToClipboard import com.anytypeio.anytype.domain.multiplayer.GetSpaceInviteLink +import com.anytypeio.anytype.domain.multiplayer.ParticipantSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider @@ -293,6 +294,9 @@ class HomeScreenViewModelTest { @Mock lateinit var notificationPermissionManager: NotificationPermissionManager + @Mock + lateinit var participantContainer: ParticipantSubscriptionContainer + lateinit var userPermissionProvider: UserPermissionProvider private val objectPayloadDispatcher = Dispatcher.Default() @@ -3037,6 +3041,7 @@ class HomeScreenViewModelTest { deleteSpace = deleteSpace, setAsFavourite = setObjectListIsFavorite, chatPreviews = chacPreviewContainer, + participantContainer = participantContainer, notificationPermissionManager = notificationPermissionManager, copyInviteLinkToClipboard = copyInviteLinkToClipboard, userSettingsRepository = userSettingsRepository, From d455d166d6676b446509e67c5c68588caeafaa59 Mon Sep 17 00:00:00 2001 From: konstantiniiv Date: Mon, 1 Dec 2025 14:33:24 +0100 Subject: [PATCH 4/5] DROID-4175 tests --- .../anytype/presentation/vault/VaultViewModelTest.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/vault/VaultViewModelTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/vault/VaultViewModelTest.kt index b50e7a0b38..2f09f88d09 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/vault/VaultViewModelTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/vault/VaultViewModelTest.kt @@ -85,13 +85,11 @@ class VaultViewModelTest { shouldShowCreateSpaceBadge.stub { onBlocking { async(any()) }.thenReturn(Resultat.Success(false)) } - participantSubscriptionContainer.stub { - onBlocking { observe() }.thenReturn(flowOf(emptyList())) - } - chatsDetailsSubscriptionContainer.stub { - onBlocking { observe() }.thenReturn(flowOf(emptyList())) - } + whenever(participantSubscriptionContainer.observe()).thenReturn(flowOf(emptyList())) + whenever(chatsDetailsSubscriptionContainer.observe()).thenReturn(flowOf(emptyList())) + whenever(notificationPermissionManager.areNotificationsEnabled()).thenReturn(true) whenever(appInfo.versionName).thenReturn("1.0.0") + whenever(stringResourceProvider.getUntitledCreatorName()).thenReturn("Untitled") } @Test From 4ff899e288c6e6bcc4fcc96bdf6a004c426c60d2 Mon Sep 17 00:00:00 2001 From: konstantiniiv Date: Mon, 1 Dec 2025 14:37:42 +0100 Subject: [PATCH 5/5] DROID-4175 fix --- .../presentation/extension/ParticipantExtensions.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/ParticipantExtensions.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/ParticipantExtensions.kt index 53cb7f0c38..638c760e8d 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/ParticipantExtensions.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/ParticipantExtensions.kt @@ -8,14 +8,14 @@ import com.anytypeio.anytype.core_models.ObjectWrapper * Uses the standard fallback chain: name → globalName → fallback. * * @param identity The identity ID to look up - * @param fallback The fallback string if participant not found or has no name - * @return The resolved name, or null if identity is null/empty + * @param fallback The fallback string if participant not found, has no name, or identity is null/empty + * @return The resolved name or fallback - always returns a non-null value */ fun Map.resolveParticipantName( identity: Id?, fallback: String -): String? { - if (identity.isNullOrEmpty()) return null +): String { + if (identity.isNullOrEmpty()) return fallback val participant = this[identity] return participant?.name ?: participant?.globalName