diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarImage.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarImage.kt index 83e955251f..5c3db23ed4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarImage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarImage.kt @@ -9,23 +9,16 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.viewinterop.AndroidView -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.rx3.asFlow import org.thoughtcrime.securesms.components.AvatarImageView import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails -import org.thoughtcrime.securesms.profiles.AvatarHelper import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.util.NameUtil +import org.thoughtcrime.securesms.recipients.rememberRecipientField @Composable fun AvatarImage( @@ -41,15 +34,12 @@ fun AvatarImage( ) } else { val context = LocalContext.current - var state: AvatarImageState by remember { - mutableStateOf(AvatarImageState(null, recipient, ProfileAvatarFileDetails.NO_DETAILS)) - } - - LaunchedEffect(recipient.id) { - Recipient.observable(recipient.id).asFlow() - .collectLatest { - state = AvatarImageState(NameUtil.getAbbreviation(it.getDisplayName(context)), it, AvatarHelper.getAvatarFileDetails(context, it.id)) - } + val avatarImageState by rememberRecipientField(recipient) { + AvatarImageState( + getDisplayName(context), + this, + profileAvatarFileDetails + ) } AndroidView( @@ -62,9 +52,9 @@ fun AvatarImage( modifier = modifier.background(color = Color.Transparent, shape = CircleShape) ) { if (useProfile) { - it.setAvatarUsingProfile(state.self) + it.setAvatarUsingProfile(avatarImageState.self) } else { - it.setAvatar(state.self) + it.setAvatar(avatarImageState.self) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainToolbar.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainToolbar.kt index 5b61b245a4..da4cb439ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/MainToolbar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainToolbar.kt @@ -67,10 +67,6 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.rx3.asFlow import org.signal.core.ui.compose.DropdownMenus import org.signal.core.ui.compose.IconButtons import org.signal.core.ui.compose.Previews @@ -79,11 +75,11 @@ import org.signal.core.ui.compose.TextFields import org.signal.core.ui.compose.Tooltips import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.avatar.AvatarImage -import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.calls.log.CallLogFilter import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageSmall import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.rememberRecipientField interface MainToolbarCallback { fun onNewGroupClick() @@ -353,16 +349,7 @@ private fun PrimaryToolbar( } ) - var badge by remember { mutableStateOf(null) } - LaunchedEffect(state.self.id) { - Recipient.observable(state.self.id) - .asFlow() - .map { it.featuredBadge } - .distinctUntilChanged() - .collectLatest { - badge = it - } - } + val badge by rememberRecipientField(state.self) { featuredBadge } BadgeImageSmall( badge = badge, diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Composables.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/Composables.kt new file mode 100644 index 0000000000..5daab81ef3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Composables.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.recipients + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import kotlinx.coroutines.rx3.asFlow +import kotlinx.coroutines.withContext +import org.signal.core.util.concurrent.SignalDispatchers + +/** + * Allows composable UI to subscribe to changes for a specific field on a recipient. + */ +@Composable +fun rememberRecipientField(recipient: Recipient, toField: Recipient.() -> T): State { + return rememberRecipientField(recipientId = recipient.id, initialData = recipient, toField = toField) +} + +/** + * Allows composable UI to subscribe to changes for a specific field on a recipient. + */ +@Composable +fun rememberRecipientField(recipientId: RecipientId, toField: Recipient.() -> T): State { + return rememberRecipientField(recipientId = recipientId, initialData = Recipient.UNKNOWN, toField = toField) +} + +@Composable +private fun rememberRecipientField(recipientId: RecipientId, initialData: Recipient, toField: Recipient.() -> T): State { + var recipientAndCounter by remember(recipientId) { mutableStateOf(initialData to 0L) } + + LaunchedEffect(recipientId) { + withContext(SignalDispatchers.IO) { + Recipient.observable(recipientId) + .asFlow() + .collect { + recipientAndCounter = it to (recipientAndCounter.second + 1L) + } + } + } + + return remember(recipientAndCounter) { + derivedStateOf { toField(recipientAndCounter.first) } + } +}