Improve typing indicator

This commit is contained in:
2025-12-24 17:23:19 +03:00
Unverified
parent 52aa1aeced
commit 1454024e76
3 changed files with 49 additions and 64 deletions
@@ -188,13 +188,12 @@ fun ChatScreen(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { title = {
Column( Column(Modifier.fillMaxWidth()) {
horizontalAlignment = Alignment.CenterHorizontally
) {
Text( Text(
text = panelState.title, text = panelState.title,
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge
) )
AnimatedContent( AnimatedContent(
targetState = currentTypingUsers.isNotEmpty(), targetState = currentTypingUsers.isNotEmpty(),
transitionSpec = { transitionSpec = {
@@ -207,9 +206,6 @@ fun ChatScreen(
typingUsers = currentTypingUsers.map { it.username }, typingUsers = currentTypingUsers.map { it.username },
modifier = Modifier.padding(top = 2.dp) modifier = Modifier.padding(top = 2.dp)
) )
} else {
// Empty space to maintain height
Box(modifier = Modifier.height(0.dp))
} }
} }
} }
@@ -29,7 +29,7 @@ class PublicChatPanel(
scope.launch { scope.launch {
typingHandler.typingUsers.collect { users -> typingHandler.typingUsers.collect { users ->
Logger.d("PublicChatPanel", "Typing users updated in handler: ${users.map { it.username }}") Logger.d("PublicChatPanel", "Typing users updated in handler: ${users.map { it.username }}")
updateState { it.copy(typingUsers = users) } updateState { it.copy(typingUsers = users.filter { it.userId != currentUserId }) }
} }
} }
} }
@@ -58,7 +58,7 @@ class PublicChatPanel(
} }
setHasMoreMessages(false) // TODO: Implement has_more from API setHasMoreMessages(false) // TODO: Implement has_more from API
messagesLoaded = true messagesLoaded = true
} catch (e: Exception) { } catch (_: Exception) {
// Handle error // Handle error
} finally { } finally {
setLoading(false) setLoading(false)
@@ -84,7 +84,7 @@ class PublicChatPanel(
} }
} }
setHasMoreMessages(false) // TODO: Implement has_more from API setHasMoreMessages(false) // TODO: Implement has_more from API
} catch (e: Exception) { } catch (_: Exception) {
// Handle error // Handle error
} finally { } finally {
setLoadingMore(false) setLoadingMore(false)
@@ -4,9 +4,12 @@ import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -16,7 +19,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.fromchat.Res import ru.fromchat.Res
@@ -63,67 +65,54 @@ private fun formatTypingText(typingUsers: List<String>): String {
@Composable @Composable
private fun TypingDots() { private fun TypingDots() {
val infiniteTransition = rememberInfiniteTransition(label = "typing_dots") val infiniteTransition = rememberInfiniteTransition(label = "typing_dots")
val duration = 1000
val initialScale = 1f
val targetScale = 1.3f
val dot1Alpha by infiniteTransition.animateFloat( // Функция-хелпер для создания анимации с задержкой
initialValue = 0.3f, @Composable
targetValue = 1f, fun animateDotScale(delay: Int) = infiniteTransition.animateFloat(
initialValue = initialScale,
targetValue = initialScale, // Возвращаемся в начало
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = keyframes { animation = keyframes {
durationMillis = 1400 durationMillis = duration
0.3f at 0 initialScale at delay // Начало подъема
1f at 200 targetScale at delay + 200 // Пик
0.3f at 400 initialScale at delay + 400 // Возврат
initialScale at duration // Удержание до конца цикла
} }
), ),
label = "dot1" label = "dot_scale"
) )
val dot2Alpha by infiniteTransition.animateFloat( val dot1Scale by animateDotScale(delay = 0)
initialValue = 0.3f, val dot2Scale by animateDotScale(delay = 200)
targetValue = 1f, val dot3Scale by animateDotScale(delay = 400)
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 1400
0.3f at 200
1f at 400
0.3f at 600
}
),
label = "dot2"
)
val dot3Alpha by infiniteTransition.animateFloat( Row(
initialValue = 0.3f, verticalAlignment = Alignment.CenterVertically,
targetValue = 1f, horizontalArrangement = Arrangement.spacedBy(2.dp) // Вместо Spacer
animationSpec = infiniteRepeatable( ) {
animation = keyframes { Dot(scale = dot1Scale, maxScale = targetScale)
durationMillis = 1400 Dot(scale = dot2Scale, maxScale = targetScale)
0.3f at 400 Dot(scale = dot3Scale, maxScale = targetScale)
1f at 600
0.3f at 800
}
),
label = "dot3"
)
Row(verticalAlignment = Alignment.CenterVertically) {
Dot(alpha = dot1Alpha)
Spacer(modifier = Modifier.width(2.dp))
Dot(alpha = dot2Alpha)
Spacer(modifier = Modifier.width(2.dp))
Dot(alpha = dot3Alpha)
} }
} }
@Composable @Composable
private fun Dot(alpha: Float) { @Suppress("SameParameterValue")
private fun Dot(maxScale: Float, scale: Float) {
Box(
modifier = Modifier.size(4.dp * maxScale),
contentAlignment = Alignment.Center
) {
Surface( Surface(
modifier = Modifier modifier = Modifier
.width(4.dp) .width(4.dp * scale)
.height(4.dp) .height(4.dp * scale),
.alpha(alpha),
shape = CircleShape, shape = CircleShape,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) {} ) {}
}
} }