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/extension/ParticipantExtensions.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/ParticipantExtensions.kt new file mode 100644 index 0000000000..638c760e8d --- /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, 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 fallback + val participant = this[identity] + return participant?.name + ?: participant?.globalName + ?: fallback +} 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 e79e24234d..9c2efd42f8 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 @@ -12,6 +12,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 @@ -54,6 +55,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, @@ -231,7 +233,8 @@ class WidgetContainerDelegateImpl( chatPreviewContainer = chatPreviews, dateProvider = dateProvider, stringResourceProvider = stringResourceProvider, - spaceViewSubscriptionContainer = spaceViewSubscriptionContainer + spaceViewSubscriptionContainer = spaceViewSubscriptionContainer, + participantContainer = participantContainer ) } else { DataViewListWidgetContainer( @@ -299,7 +302,8 @@ class WidgetContainerDelegateImpl( chatPreviewContainer = chatPreviews, dateProvider = dateProvider, stringResourceProvider = stringResourceProvider, - spaceViewSubscriptionContainer = spaceViewSubscriptionContainer + spaceViewSubscriptionContainer = spaceViewSubscriptionContainer, + participantContainer = participantContainer ) } else { DataViewListWidgetContainer( 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, 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