diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt index 258f4c3478..4cf1fc2b23 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt @@ -134,6 +134,8 @@ import hedvig.resources.general_continue_button import hedvig.resources.general_error import hedvig.resources.something_went_wrong import kotlin.time.Clock +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource @@ -754,18 +756,24 @@ private fun AnimatedRevealText( visibleState: MutableTransitionState, modifier: Modifier = Modifier, style: TextStyle = LocalTextStyle.current, - delayPerChar: Int = 30, - charAnimDuration: Int = 150, onAnimationFinished: () -> Unit = {}, ) { + val charAnimDuration: Int = 150 var visibleChars by remember { mutableStateOf(0) } + val charDelay = calculateCharDelay(text) + LaunchedEffect(visibleState.targetState, text) { if (visibleState.targetState) { visibleChars = 0 - repeat(text.length) { index -> - delay(delayPerChar.toLong()) + text.toCharArray().forEachIndexed { index, char -> visibleChars = index + 1 + val specialCharDelayMultiplier = when (char) { + ',' -> 5 + in listOf('.', '?', '!', '\n', '\t') -> 10 + else -> 1 + } + delay(charDelay * specialCharDelayMultiplier) } delay(charAnimDuration.toLong()) onAnimationFinished() @@ -800,6 +808,44 @@ private fun AnimatedRevealText( ) } +/** + * Speed multiplier decreases for longer text to avoid tedious animations + * Short text (≤50 chars): full speed (1.0x multiplier) + * Medium text (50-450 chars): linear interpolation + * Long text (≥450 chars): 5x faster (0.2x multiplier) + */ +@Composable +private fun calculateCharDelay(text: String): Duration = remember(text) { + val textLength = text.length + val baseRegularDelayMillis = 20 + + val shortTextThreshold = 50 + val longTextThreshold = 350 + val slowestMultiplier = 1.0 + val fastestMultiplier = 0.2 + + // Extreme fallback for extreme cases + val superLongThreshold = 800 + val superFastestMultiplier = 0.05 + + val speedMultiplier = when { + textLength <= shortTextThreshold -> slowestMultiplier + textLength >= superLongThreshold -> superFastestMultiplier + textLength >= longTextThreshold -> fastestMultiplier + else -> { + val characterRange = longTextThreshold - shortTextThreshold + val charactersAboveThreshold = textLength - shortTextThreshold + val interpolationProgress = charactersAboveThreshold.toDouble() / characterRange + slowestMultiplier - interpolationProgress * (slowestMultiplier - fastestMultiplier) + } + }.coerceIn(fastestMultiplier, slowestMultiplier) + + val minimumDelayRatio = 0.2 + (baseRegularDelayMillis * speedMultiplier) + .coerceAtLeast((baseRegularDelayMillis * minimumDelayRatio)) + .milliseconds +} + @Composable private fun StepBottomContent( stepItem: ClaimIntentStep, diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatUiComponents.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatUiComponents.kt index e26ed022e0..8ab6a68e29 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatUiComponents.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatUiComponents.kt @@ -149,7 +149,7 @@ internal fun ContentSelectChips( StepContent.ContentSelectStyle.BINARY -> { Row( - Modifier.fillMaxWidth(), + modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly, ) { for (item in options) { @@ -287,9 +287,9 @@ internal fun YesNoBubble( stringResource(Res.string.GENERAL_NO), ), ) - Column { + Column(modifier) { Row( - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End, ) { @@ -312,7 +312,7 @@ internal fun YesNoBubble( if (errorText != null) { Spacer(Modifier.height(4.dp)) Row( - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End, ) { @@ -349,14 +349,14 @@ internal fun SingleSelectBubbleWithDialog( }, ) } - Column { + Column(modifier) { HedvigBigCard( onClick = { showDialog = true }, labelText = questionLabel, inputText = options.firstOrNull { it.id == selectedOptionId }?.text, - modifier = modifier, + modifier = Modifier.fillMaxWidth(), enabled = true, ) AnimatedVisibility(errorText != null) { @@ -395,7 +395,7 @@ internal fun MultiSelectBubbleWithDialog( buttonText = stringResource(Res.string.general_save_button), ) } - Column { + Column(modifier) { HedvigBigCard( onClick = { showDialog = true }, labelText = questionLabel, @@ -404,7 +404,7 @@ internal fun MultiSelectBubbleWithDialog( else -> options.filter { it.id in selectedOptionIds } .joinToString(transform = RadioOption::text) }, - modifier = modifier, + modifier = Modifier.fillMaxWidth(), enabled = true, ) AnimatedVisibility(errorText != null) { @@ -697,12 +697,12 @@ internal fun DateSelectBubble( modifier: Modifier = Modifier, errorText: String? = null, ) { - Column { + Column(modifier) { DatePickerWithDialog( datePickerState, canInteract = true, startText = questionLabel ?: "", - modifier = modifier, + Modifier.fillMaxWidth() ) AnimatedVisibility( errorText != null && @@ -818,7 +818,7 @@ internal fun ChatClaimSummaryTopContent( ) Spacer(Modifier.height(8.dp)) CompositionLocalProvider(LocalContentColor provides HedvigTheme.colorScheme.textSecondary) { - Column(modifier) { + Column(Modifier) { for (displayItem in displayItems) { HorizontalItemsWithMaximumSpaceTaken( spaceBetween = 8.dp,