diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/TypingIndicator.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/TypingIndicator.kt index a311cb38d3..e727005c72 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/TypingIndicator.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/TypingIndicator.kt @@ -152,6 +152,16 @@ private fun AvatarStack( } } + // A typist can change without changing key, such as when their profile resolves after they + // started typing. The effect above only runs when the keys change, so without this the avatar + // keeps drawing the typist as they first arrived. + LaunchedEffect(shown) { + shown.forEach { item -> + val itemKey = key(item) + entries.firstOrNull { it.key == itemKey && !it.leaving }?.item = item + } + } + // Stacking order: later slots on top, and a leaving avatar beneath everything, so the ones // that close the gap slide over it. fun StackEntry.z(): Float = if (leaving) -1f else slot.value diff --git a/ui/components/src/test/kotlin/com/getcode/ui/components/chat/TypingIndicatorTest.kt b/ui/components/src/test/kotlin/com/getcode/ui/components/chat/TypingIndicatorTest.kt new file mode 100644 index 0000000000..b92ab163fe --- /dev/null +++ b/ui/components/src/test/kotlin/com/getcode/ui/components/chat/TypingIndicatorTest.kt @@ -0,0 +1,45 @@ +package com.getcode.ui.components.chat + +import androidx.compose.material.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import com.getcode.theme.DesignSystem +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class TypingIndicatorTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private data class Typist(val id: String, val name: String?) + + // A typist can appear before their profile resolves, and the resolved profile arrives as the + // same typist (same key) with more to draw. The avatar has to pick that up. + @Test + fun avatarRedrawsWhenATypistChangesUnderTheSameKey() { + var typists by mutableStateOf(listOf(Typist("a", name = null))) + composeTestRule.setContent { + DesignSystem { + TypingIndicator( + typists = typists, + key = { it.id }, + avatar = { Text(it.name ?: "fallback") }, + ) + } + } + composeTestRule.onNodeWithText("fallback").assertExists() + + typists = listOf(Typist("a", name = "Alice")) + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithText("Alice").assertExists() + composeTestRule.onNodeWithText("fallback").assertDoesNotExist() + } +}