diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt index ffa07f131..2c8959132 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt @@ -154,7 +154,8 @@ internal fun MessageList( // Message insertion animation — scale from 0.95 + opacity with edge anchor var appeared by remember { mutableStateOf(false) } LaunchedEffect(Unit) { appeared = true } - val insertionSpec = spring(dampingRatio = 0.73f, stiffness = Spring.StiffnessHigh) + val insertionSpec = + spring(dampingRatio = 0.73f, stiffness = Spring.StiffnessHigh) val insertionAlpha by animateFloatAsState( targetValue = if (appeared) 1f else 0f, animationSpec = insertionSpec, @@ -197,7 +198,11 @@ internal fun MessageList( val showReceipt = shouldShowReceiptLabel(index, item, messages, otherReadPointer) if (showReceipt && effectiveStatus != null) { - ReceiptLabel(effectiveStatus, otherReadPointer) + ReceiptLabel( + itemKey = item.itemKey, + status = effectiveStatus, + readPointer = otherReadPointer + ) } } } diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ReceiptLabel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ReceiptLabel.kt index 69a83d186..b2a73f4b1 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ReceiptLabel.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ReceiptLabel.kt @@ -44,16 +44,15 @@ internal fun ReceiptLabel( status: ReceiptStatus, readPointer: MessagePointer?, modifier: Modifier = Modifier, + itemKey: Any? = null, ) { - // Delayed pop for "Delivered" — wait 700ms before showing, instant removal - var visible by remember { mutableStateOf(false) } + // Delayed pop for "Delivered" — wait 700ms before showing, instant removal. + // Start visible when not SENT so the animation is oneshot (doesn't replay on scroll). + var visible by remember(itemKey) { mutableStateOf(status != ReceiptStatus.SENT) } LaunchedEffect(status) { - if (status == ReceiptStatus.SENT) { - visible = false + if (status == ReceiptStatus.SENT && !visible) { delay(700.milliseconds) visible = true - } else { - visible = true } } diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/ChatCoordinator.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/ChatCoordinator.kt index cdfbeec32..d7b73aa2a 100644 --- a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/ChatCoordinator.kt +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/ChatCoordinator.kt @@ -92,6 +92,7 @@ class ChatCoordinator @Inject constructor( private var eventStreamRetryJob: Job? = null private var heartbeatJob: Job? = null private var retryAttempt = 0 + private var backgroundedActiveChat: ChatId? = null val state: StateFlow get() = _state.asStateFlow() @@ -143,6 +144,10 @@ class ChatCoordinator @Inject constructor( } override fun onStart(owner: LifecycleOwner) { + backgroundedActiveChat?.let { + setActiveChatId(it) + backgroundedActiveChat = null + } if (cluster.value != null) { trace(tag = TAG, message = "Lifecycle resumed, syncing chat feed", type = TraceType.Process) syncFeed() @@ -152,6 +157,8 @@ class ChatCoordinator @Inject constructor( } override fun onStop(owner: LifecycleOwner) { + backgroundedActiveChat = _state.value.activeChat + setActiveChatId(null) stopHeartbeat() closeEventStream() } diff --git a/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt index 73be1b469..edc5863ae 100644 --- a/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt +++ b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt @@ -118,16 +118,18 @@ class NotificationService : FirebaseMessagingService(), launch { tokenCoordinator.update() } } - launch { - try { - if (payload?.category == NotificationCategory.CONTACT_JOIN) { - launch { contactCoordinator.sync() } + authenticateIfNeeded { + launch { + try { + if (payload?.category == NotificationCategory.CONTACT_JOIN) { + launch { contactCoordinator.sync() } + } + val resolvedTitle = applySubstitutions(title, payload?.titleSubstitutions.orEmpty()) + val resolvedBody = body?.let { applySubstitutions(it, payload?.bodySubstitutions.orEmpty()) } + postNotification(resolvedTitle, resolvedBody, payload) + } catch (e: Exception) { + trace(tag = "NotificationService", message = "Failed to post notification", error = e) } - val resolvedTitle = applySubstitutions(title, payload?.titleSubstitutions.orEmpty()) - val resolvedBody = body?.let { applySubstitutions(it, payload?.bodySubstitutions.orEmpty()) } - postNotification(resolvedTitle, resolvedBody, payload) - } catch (e: Exception) { - trace(tag = "NotificationService", message = "Failed to post notification", error = e) } } } @@ -187,8 +189,18 @@ class NotificationService : FirebaseMessagingService(), body: String?, ): Int { val notificationId = chatId.hashCode() - val e164 = groupKey?.takeIf { it.startsWith("+") } - ?: contactCoordinator.lookupContactByDmChatId(chatId.toString())?.e164 + val groupKeyE164 = groupKey?.takeIf { it.startsWith("+") } + val lookupContact = if (groupKeyE164 == null) { + contactCoordinator.lookupContactByDmChatId(chatId.toString()) + } else null + val e164 = groupKeyE164 ?: lookupContact?.e164 + + trace( + tag = "NotificationService", + message = "applyContactChatStyle: chatId=$chatId, groupKey=$groupKey, groupKeyE164=$groupKeyE164, lookupE164=${lookupContact?.e164}, e164=$e164, authenticated=${userManager.accountCluster != null}", + type = TraceType.Log, + ) + val contactPhoto = e164?.let { resolveContactPhoto(it) } val senderName = e164?.let { contactResolver.resolveName(it) } ?: ""