diff --git a/app/audio-player-data/src/commonMain/kotlin/com/hedvig/audio/player/data/AudioPlayerState.kt b/app/audio-player-data/src/commonMain/kotlin/com/hedvig/audio/player/data/AudioPlayerState.kt index 1fa6289b0c..4288833a75 100644 --- a/app/audio-player-data/src/commonMain/kotlin/com/hedvig/audio/player/data/AudioPlayerState.kt +++ b/app/audio-player-data/src/commonMain/kotlin/com/hedvig/audio/player/data/AudioPlayerState.kt @@ -8,6 +8,7 @@ sealed interface AudioPlayerState { data class Ready( val readyState: ReadyState, val progressPercentage: ProgressPercentage = ProgressPercentage(0f), + val durationMillis: Int = 0, ) : AudioPlayerState { sealed interface ReadyState { object NotStarted : ReadyState diff --git a/app/audio-player-ui/src/androidMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.android.kt b/app/audio-player-ui/src/androidMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.android.kt index fcd84e7729..1f9d0d3d6e 100644 --- a/app/audio-player-ui/src/androidMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.android.kt +++ b/app/audio-player-ui/src/androidMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.android.kt @@ -25,6 +25,9 @@ private class AndroidMediaPlayer( override val isPlaying: Boolean get() = mediaPlayer.isPlaying + override val duration: Int + get() = mediaPlayer.duration + override fun pause() { mediaPlayer.pause() } diff --git a/app/audio-player-ui/src/commonMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.kt b/app/audio-player-ui/src/commonMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.kt index c97addd4f3..ca70132518 100644 --- a/app/audio-player-ui/src/commonMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.kt +++ b/app/audio-player-ui/src/commonMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.kt @@ -4,6 +4,7 @@ import com.hedvig.audio.player.data.ProgressPercentage interface CommonMediaPlayer { val isPlaying: Boolean + val duration: Int fun pause() diff --git a/app/audio-player-ui/src/commonMain/kotlin/com/hedvig/android/audio/player/audioplayer/AudioPlayerImpl.kt b/app/audio-player-ui/src/commonMain/kotlin/com/hedvig/android/audio/player/audioplayer/AudioPlayerImpl.kt index e9e13fbf40..9b21755cbd 100644 --- a/app/audio-player-ui/src/commonMain/kotlin/com/hedvig/android/audio/player/audioplayer/AudioPlayerImpl.kt +++ b/app/audio-player-ui/src/commonMain/kotlin/com/hedvig/android/audio/player/audioplayer/AudioPlayerImpl.kt @@ -125,8 +125,24 @@ private class AudioPlayerImpl( _audioPlayerState.update { AudioPlayerState.Failed } true } - setOnPreparedListener { _audioPlayerState.update { AudioPlayerState.Ready.notStarted() } } - setOnCompletionListener { _audioPlayerState.update { AudioPlayerState.Ready.done() } } + setOnPreparedListener { + _audioPlayerState.update { + AudioPlayerState.Ready( + readyState = AudioPlayerState.Ready.ReadyState.NotStarted, + progressPercentage = ProgressPercentage(0f), + durationMillis = duration, + ) + } + } + setOnCompletionListener { + _audioPlayerState.update { + AudioPlayerState.Ready( + readyState = AudioPlayerState.Ready.ReadyState.Done, + progressPercentage = ProgressPercentage(1f), + durationMillis = duration, + ) + } + } prepareAsync() } } diff --git a/app/audio-player-ui/src/commonMain/kotlin/com/hedvig/android/audio/player/internal/FakeAudioWaves.kt b/app/audio-player-ui/src/commonMain/kotlin/com/hedvig/android/audio/player/internal/FakeAudioWaves.kt index 033d2bdf1c..00c00d8f67 100644 --- a/app/audio-player-ui/src/commonMain/kotlin/com/hedvig/android/audio/player/internal/FakeAudioWaves.kt +++ b/app/audio-player-ui/src/commonMain/kotlin/com/hedvig/android/audio/player/internal/FakeAudioWaves.kt @@ -37,7 +37,7 @@ import kotlin.math.absoluteValue import kotlin.math.roundToInt import kotlin.random.Random -private const val waveWidthPercentOfSpaceAvailable = 0.5f +internal const val waveWidthPercentOfSpaceAvailable = 0.5f @Composable internal fun FakeAudioWaves( diff --git a/app/audio-player-ui/src/nativeMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.native.kt b/app/audio-player-ui/src/nativeMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.native.kt index f86227ef35..e9dcb182ce 100644 --- a/app/audio-player-ui/src/nativeMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.native.kt +++ b/app/audio-player-ui/src/nativeMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.native.kt @@ -8,6 +8,9 @@ actual fun CommonMediaPlayer(dataSourceUrl: String): CommonMediaPlayer { override val isPlaying: Boolean get() = false + override val duration: Int + get() = 0 + override fun pause() { } diff --git a/app/auth/auth-core-public/src/test/kotlin/com/hedvig/android/auth/interceptor/AndroidAccessTokenProviderTest.kt b/app/auth/auth-core-public/src/test/kotlin/com/hedvig/android/auth/interceptor/AndroidAccessTokenProviderTest.kt index a10c6eb70f..e16bc7503d 100644 --- a/app/auth/auth-core-public/src/test/kotlin/com/hedvig/android/auth/interceptor/AndroidAccessTokenProviderTest.kt +++ b/app/auth/auth-core-public/src/test/kotlin/com/hedvig/android/auth/interceptor/AndroidAccessTokenProviderTest.kt @@ -67,31 +67,32 @@ class AndroidAccessTokenProviderTest { } @Test - fun `when the access token is expired, and the refresh token is not expired, refresh and return new token`() = runTest { - val clock = TestClock() - val authTokenStorage = authTokenStorage(clock) - authTokenStorage.updateTokens( - AccessToken("", 10.minutes.inWholeSeconds), - RefreshToken("", 1.hours.inWholeSeconds), - ) - val authRepository = FakeAuthRepository() - val authTokenService = authTokenService(authTokenStorage, authRepository) - val accessTokenProvider = AndroidAccessTokenProvider(authTokenService, clock) + fun `when the access token is expired, and the refresh token is not expired, refresh and return new token`() = + runTest { + val clock = TestClock() + val authTokenStorage = authTokenStorage(clock) + authTokenStorage.updateTokens( + AccessToken("", 10.minutes.inWholeSeconds), + RefreshToken("", 1.hours.inWholeSeconds), + ) + val authRepository = FakeAuthRepository() + val authTokenService = authTokenService(authTokenStorage, authRepository) + val accessTokenProvider = AndroidAccessTokenProvider(authTokenService, clock) - clock.advanceTimeBy(30.minutes) - authRepository.exchangeResponse.add( - AuthTokenResult.Success( - AccessToken("refreshedToken", 10.minutes.inWholeSeconds), - RefreshToken("refreshedRefreshToken", 0), - ), - ) - val token = accessTokenProvider.provide() + clock.advanceTimeBy(30.minutes) + authRepository.exchangeResponse.add( + AuthTokenResult.Success( + AccessToken("refreshedToken", 10.minutes.inWholeSeconds), + RefreshToken("refreshedRefreshToken", 0), + ), + ) + val token = accessTokenProvider.provide() - val storedAuthTokens = authTokenStorage.getTokens().first()!! - assertThat(storedAuthTokens.accessToken.token).isEqualTo("refreshedToken") - assertThat(storedAuthTokens.refreshToken.token).isEqualTo("refreshedRefreshToken") - assertThat(token).isEqualTo("refreshedToken") - } + val storedAuthTokens = authTokenStorage.getTokens().first()!! + assertThat(storedAuthTokens.accessToken.token).isEqualTo("refreshedToken") + assertThat(storedAuthTokens.refreshToken.token).isEqualTo("refreshedRefreshToken") + assertThat(token).isEqualTo("refreshedToken") + } @Test fun `when the access token and the refresh token are expired, clear tokens and return null`() = runTest { diff --git a/app/authlib/src/commonMain/kotlin/com/hedvig/authlib/internal/KtorConfiguration.kt b/app/authlib/src/commonMain/kotlin/com/hedvig/authlib/internal/KtorConfiguration.kt index 78b89c432c..1baaec6816 100644 --- a/app/authlib/src/commonMain/kotlin/com/hedvig/authlib/internal/KtorConfiguration.kt +++ b/app/authlib/src/commonMain/kotlin/com/hedvig/authlib/internal/KtorConfiguration.kt @@ -13,9 +13,7 @@ import io.ktor.client.request.header import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json -internal fun buildKtorClient( - additionalHttpHeadersProvider: () -> Map, -): HttpClient { +internal fun buildKtorClient(additionalHttpHeadersProvider: () -> Map): HttpClient { val httpClientConfig: HttpClientConfig<*>.() -> Unit = { commonKtorConfiguration(additionalHttpHeadersProvider).invoke(this) } diff --git a/app/core/core-build-constants/src/commonMain/kotlin/com/hedvig/android/core/buildconstants/AppBuildConfig.kt b/app/core/core-build-constants/src/commonMain/kotlin/com/hedvig/android/core/buildconstants/AppBuildConfig.kt index 5ecd5593be..b1c2e99393 100644 --- a/app/core/core-build-constants/src/commonMain/kotlin/com/hedvig/android/core/buildconstants/AppBuildConfig.kt +++ b/app/core/core-build-constants/src/commonMain/kotlin/com/hedvig/android/core/buildconstants/AppBuildConfig.kt @@ -18,5 +18,5 @@ interface AppBuildConfig { enum class Flavor { Production, Staging, - Develop + Develop, } diff --git a/app/core/core-build-constants/src/commonMain/kotlin/com/hedvig/android/core/buildconstants/CommonHedvigBuildConstants.kt b/app/core/core-build-constants/src/commonMain/kotlin/com/hedvig/android/core/buildconstants/CommonHedvigBuildConstants.kt index 6e4158540b..c30489d7ee 100644 --- a/app/core/core-build-constants/src/commonMain/kotlin/com/hedvig/android/core/buildconstants/CommonHedvigBuildConstants.kt +++ b/app/core/core-build-constants/src/commonMain/kotlin/com/hedvig/android/core/buildconstants/CommonHedvigBuildConstants.kt @@ -53,10 +53,15 @@ private fun makeUserAgent(languageBCP47: String, appBuildConfig: AppBuildConfig) private interface UrlHolder { fun urlGraphqlOctopus(flavor: Flavor): String + fun urlBaseWeb(flavor: Flavor): String + fun urlOdyssey(flavor: Flavor): String + fun urlBotService(flavor: Flavor): String + fun urlClaimsService(flavor: Flavor): String + fun deepLinkHosts(flavor: Flavor): List } @@ -125,6 +130,7 @@ private class AppConfigUrlHolder(private val appBuildConfig: AppBuildConfig) : U } private fun deepLinkDomainPathPrefix(): String = "/deeplink" + private fun deepLinkDomainHostOld(flavor: Flavor): String = when (flavor) { Production -> "hedvig.page.link" Staging -> "hedvigtest.page.link" diff --git a/app/core/core-datastore-public/src/androidMain/kotlin/com/hedvig/android/core/datastore/DeviceIdFetcher.android.kt b/app/core/core-datastore-public/src/androidMain/kotlin/com/hedvig/android/core/datastore/DeviceIdFetcher.android.kt index e8e4ab0db0..2644e06708 100644 --- a/app/core/core-datastore-public/src/androidMain/kotlin/com/hedvig/android/core/datastore/DeviceIdFetcher.android.kt +++ b/app/core/core-datastore-public/src/androidMain/kotlin/com/hedvig/android/core/datastore/DeviceIdFetcher.android.kt @@ -3,7 +3,7 @@ package com.hedvig.android.core.datastore import kotlinx.coroutines.flow.firstOrNull internal class AndroidDeviceIdFetcher( - private val deviceIdDataStore: DeviceIdDataStore + private val deviceIdDataStore: DeviceIdDataStore, ) : DeviceIdFetcher { override suspend fun fetch(): String? { return deviceIdDataStore.observeDeviceId().firstOrNull() diff --git a/app/core/core-datastore-public/src/jvmMain/kotlin/com/hedvig/android/core/datastore/DeviceIdFetcher.jvm.kt b/app/core/core-datastore-public/src/jvmMain/kotlin/com/hedvig/android/core/datastore/DeviceIdFetcher.jvm.kt index 0adf1aa8d9..2faec33ccc 100644 --- a/app/core/core-datastore-public/src/jvmMain/kotlin/com/hedvig/android/core/datastore/DeviceIdFetcher.jvm.kt +++ b/app/core/core-datastore-public/src/jvmMain/kotlin/com/hedvig/android/core/datastore/DeviceIdFetcher.jvm.kt @@ -3,7 +3,7 @@ package com.hedvig.android.core.datastore import kotlinx.coroutines.flow.firstOrNull internal class JvmDeviceIdFetcher( - private val deviceIdDataStore: DeviceIdDataStore + private val deviceIdDataStore: DeviceIdDataStore, ) : DeviceIdFetcher { override suspend fun fetch(): String? { return deviceIdDataStore.observeDeviceId().firstOrNull() diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigDatePicker.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigDatePicker.kt index 6cda752b72..f0b2dd1106 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigDatePicker.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigDatePicker.kt @@ -10,8 +10,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter import com.hedvig.android.compose.ui.preview.BooleanCollectionPreviewParameterProvider -import com.hedvig.android.core.locale.previewCommonLocale import com.hedvig.android.core.locale.CommonLocale +import com.hedvig.android.core.locale.previewCommonLocale import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize.Medium import com.hedvig.android.design.system.hedvig.api.HedvigDisplayMode import com.hedvig.android.design.system.hedvig.api.HedvigSelectableDates diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Shapes.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Shapes.kt index 6a245739f4..27ce2e3111 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Shapes.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Shapes.kt @@ -139,7 +139,7 @@ enum class FigmaShapeDirection { All, TopOnly, BottomOnly, - EndOnly + EndOnly, } private fun RoundedPolygon.toPath( diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt index 909baa9675..63728a02be 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt @@ -64,9 +64,9 @@ internal sealed interface ClaimChatEvent { data class RedoRecording(override val id: StepId) : AudioRecording - data class ShowFreeText(override val id: StepId) : AudioRecording + data class SwitchToFreeText(override val id: StepId) : AudioRecording - data class ShowAudioRecording(override val id: StepId) : AudioRecording + data class SwitchToAudioRecording(override val id: StepId) : AudioRecording } data class UpdateFreeText(val text: String?) : ClaimChatEvent @@ -367,7 +367,7 @@ internal class ClaimChatPresenter( } } - is ClaimChatEvent.AudioRecording.ShowFreeText -> { + is ClaimChatEvent.AudioRecording.SwitchToFreeText -> { val currentContent = currentStep?.stepContent as? StepContent.AudioRecording ?: return@CollectEvents val textTooShort = freeText?.length?.let { @@ -387,7 +387,7 @@ internal class ClaimChatPresenter( } } - is ClaimChatEvent.AudioRecording.ShowAudioRecording -> { + is ClaimChatEvent.AudioRecording.SwitchToAudioRecording -> { steps.updateStepWithSuccess(event.id) { step, content -> step.copy(stepContent = content.copy(recordingState = AudioRecording.NotRecording)) } diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt index a052119423..e25e10a3bc 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt @@ -181,8 +181,6 @@ internal sealed interface StepContent { } sealed interface AudioRecordingStepState { - data object NonDefined : AudioRecordingStepState - data class FreeTextDescription( val showOverlay: Boolean, val errorType: FreeTextErrorType?, diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt index 62a2cff132..4966e3567b 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt @@ -81,7 +81,7 @@ private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale): is AudioRecordingFragment -> StepContent.AudioRecording( uploadUri = uploadUri, isSkippable = isSkippable, - recordingState = AudioRecordingStepState.NonDefined, + recordingState = AudioRecordingStepState.AudioRecording.NotRecording, freeTextMinLength = freeTextMinLength, freeTextMaxLength = freeTextMaxLength, ) 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..8318e0e031 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 @@ -455,9 +455,9 @@ private fun ClaimChatScrollableContent( contentType = { it.stepContent::class }, ) { item -> val isCurrentStep = item.id == uiState.steps.lastOrNull()?.id - val showAnimationSequence = isCurrentStep - && item.stepContent !is StepContent.Task - && !uiState.stepsWithShownAnimations.contains(item.id) + val showAnimationSequence = isCurrentStep && + item.stepContent !is StepContent.Task && + !uiState.stepsWithShownAnimations.contains(item.id) val isLastItem = item == uiState.steps.lastOrNull() val heightModifier = if (isLastItem) { @@ -822,10 +822,10 @@ private fun StepBottomContent( item = stepItem, stepContent = stepItem.stepContent, onShowFreeText = { - onEvent(ClaimChatEvent.AudioRecording.ShowFreeText(stepItem.id)) + onEvent(ClaimChatEvent.AudioRecording.SwitchToFreeText(stepItem.id)) }, - onShowAudioRecording = { - onEvent(ClaimChatEvent.AudioRecording.ShowAudioRecording(stepItem.id)) + onSwitchToAudioRecording = { + onEvent(ClaimChatEvent.AudioRecording.SwitchToAudioRecording(stepItem.id)) }, onLaunchFullScreenEditText = { restrictions -> onEvent(ClaimChatEvent.OpenFreeTextOverlay(restrictions)) @@ -1387,7 +1387,7 @@ private fun AudioRecordingStep( freeText: String?, stepContent: StepContent.AudioRecording, onShowFreeText: () -> Unit, - onShowAudioRecording: () -> Unit, + onSwitchToAudioRecording: () -> Unit, onLaunchFullScreenEditText: (restrictions: FreeTextRestrictions) -> Unit, submitFreeText: () -> Unit, submitAudioFile: () -> Unit, @@ -1418,8 +1418,8 @@ private fun AudioRecordingStep( openAppSettings = openAppSettings, freeTextAvailable = true, submitFreeText = submitFreeText, - onShowFreeText = onShowFreeText, - onShowAudioRecording = onShowAudioRecording, + onSwitchToFreeText = onShowFreeText, + onSwitchToAudioRecording = onSwitchToAudioRecording, onLaunchFullScreenEditText = { onLaunchFullScreenEditText( FreeTextRestrictions( 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..116bdbea26 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 @@ -988,8 +988,8 @@ private fun PreviewClaimChatComponents() { openAppSettings = {}, freeTextAvailable = true, submitFreeText = {}, - onShowFreeText = {}, - onShowAudioRecording = {}, + onSwitchToFreeText = {}, + onSwitchToAudioRecording = {}, onLaunchFullScreenEditText = {}, canSkip = true, onSkip = {}, diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/audiorecording/AudioRecordingStepSections.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/audiorecording/AudioRecordingStepSections.kt index 0cd6b73da2..dea4338250 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/audiorecording/AudioRecordingStepSections.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/audiorecording/AudioRecordingStepSections.kt @@ -1,46 +1,106 @@ package com.hedvig.feature.claim.chat.ui.audiorecording import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsBottomHeight +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.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.lerp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.hedvig.android.audio.player.HedvigAudioPlayer +import com.hedvig.android.audio.player.audioplayer.rememberAudioPlayer +import com.hedvig.android.compose.ui.EmptyContentDescription +import com.hedvig.android.core.uidata.DecimalFormatter import com.hedvig.android.design.system.hedvig.ButtonDefaults +import com.hedvig.android.design.system.hedvig.HedvigBottomSheet import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigCircularProgressIndicator import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigText -import com.hedvig.android.design.system.hedvig.HedvigTextButton import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.Icon +import com.hedvig.android.design.system.hedvig.LocalContentColor import com.hedvig.android.design.system.hedvig.PermissionDialog import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.api.HedvigBottomSheetState import com.hedvig.android.design.system.hedvig.freetext.FreeTextDisplay +import com.hedvig.android.design.system.hedvig.icon.ArrowUp +import com.hedvig.android.design.system.hedvig.icon.HedvigIcons +import com.hedvig.android.design.system.hedvig.icon.Mic +import com.hedvig.android.design.system.hedvig.icon.Pause +import com.hedvig.android.design.system.hedvig.icon.Play +import com.hedvig.android.design.system.hedvig.icon.Reload +import com.hedvig.android.design.system.hedvig.rememberHedvigBottomSheetState +import com.hedvig.audio.player.data.AudioPlayer +import com.hedvig.audio.player.data.AudioPlayerState +import com.hedvig.audio.player.data.PlayableAudioSource +import com.hedvig.audio.player.data.ProgressPercentage import com.hedvig.feature.claim.chat.data.AudioRecordingStepState import com.hedvig.feature.claim.chat.data.FreeTextErrorType import com.hedvig.feature.claim.chat.ui.RoundCornersPill import com.hedvig.feature.claim.chat.ui.SkippedLabel +import hedvig.resources.AUDIO_RECORDER_LISTEN +import hedvig.resources.AUDIO_RECORDER_SEND +import hedvig.resources.AUDIO_RECORDER_START +import hedvig.resources.AUDIO_RECORDER_START_OVER +import hedvig.resources.AUDIO_RECORDER_STOP import hedvig.resources.CLAIMS_TEXT_INPUT_MIN_CHARACTERS_ERROR import hedvig.resources.CLAIMS_TEXT_INPUT_PLACEHOLDER import hedvig.resources.CLAIMS_USE_AUDIO_RECORDING import hedvig.resources.CLAIMS_USE_TEXT_INSTEAD import hedvig.resources.CLAIM_CHAT_USE_AUDIO +import hedvig.resources.CLAIM_TRIAGING_TITLE import hedvig.resources.PERMISSION_DIALOG_RECORD_AUDIO_MESSAGE import hedvig.resources.Res import hedvig.resources.SAVE_AND_CONTINUE_BUTTON_LABEL +import hedvig.resources.TALKBACK_RECORDING_DURATION import hedvig.resources.claims_skip_button +import kotlin.math.abs +import kotlin.math.roundToInt +import kotlin.random.Random import kotlin.time.Clock +import kotlin.time.Instant +import kotlinx.coroutines.delay import org.jetbrains.compose.resources.stringResource @Composable @@ -56,8 +116,8 @@ internal fun AudioRecorderBubble( openAppSettings: () -> Unit, freeTextAvailable: Boolean, submitFreeText: () -> Unit, - onShowFreeText: () -> Unit, - onShowAudioRecording: () -> Unit, + onSwitchToFreeText: () -> Unit, + onSwitchToAudioRecording: () -> Unit, onLaunchFullScreenEditText: () -> Unit, canSkip: Boolean, onSkip: () -> Unit, @@ -72,33 +132,15 @@ internal fun AudioRecorderBubble( when (s) { is AudioRecordingStepState.AudioRecording -> "audio_recording" is AudioRecordingStepState.FreeTextDescription -> "freetext" - AudioRecordingStepState.NonDefined -> "non_defined" } }, ) { uiStateAnimated -> Column(modifier) { when (uiStateAnimated) { - is AudioRecordingStepState.AudioRecording -> { - AudioRecordingSection( - uiState = uiStateAnimated, - clock = clock, - shouldShowRequestPermissionRationale = onShouldShowRequestPermissionRationale, - startRecording = startRecording, - stopRecording = stopRecording, - submitAudioFile = submitAudioFile, - redo = redoRecording, - openAppSettings = openAppSettings, - allowFreeText = freeTextAvailable, - launchFreeText = onShowFreeText, - isCurrentStep = isCurrentStep, - continueButtonLoading = continueButtonLoading, - ) - } - is AudioRecordingStepState.FreeTextDescription -> { FreeTextInputSection( submitFreeText = submitFreeText, - showAudioRecording = onShowAudioRecording, + showAudioRecording = onSwitchToAudioRecording, onLaunchFullScreenEditText = onLaunchFullScreenEditText, freeText = freeText, hasError = uiStateAnimated.hasError, @@ -109,22 +151,55 @@ internal fun AudioRecorderBubble( ) } - AudioRecordingStepState.NonDefined -> { + is AudioRecordingStepState.AudioRecording -> { Column(Modifier.fillMaxWidth()) { - HedvigButton( - enabled = true, - text = stringResource(Res.string.CLAIM_CHAT_USE_AUDIO), - onClick = onShowAudioRecording, - modifier = Modifier.fillMaxWidth(), - ) - Spacer(Modifier.height(8.dp)) - HedvigButton( - enabled = true, - buttonStyle = ButtonDefaults.ButtonStyle.Secondary, - text = stringResource(Res.string.CLAIMS_USE_TEXT_INSTEAD), - onClick = onShowFreeText, - modifier = Modifier.fillMaxWidth(), - ) + val state = rememberHedvigBottomSheetState() + if (isCurrentStep) { + AudioRecordingBottomSheet( + audioRecordingState = uiStateAnimated, + clock = clock, + shouldShowRequestPermissionRationale = onShouldShowRequestPermissionRationale, + startRecording = startRecording, + stopRecording = stopRecording, + submitAudioFile = submitAudioFile, + redo = redoRecording, + openAppSettings = openAppSettings, + continueButtonLoading = continueButtonLoading, + bottomSheetState = state, + ) + HedvigButton( + enabled = true, + text = stringResource(Res.string.CLAIM_CHAT_USE_AUDIO), + onClick = { + state.show(Unit) + }, + modifier = Modifier.fillMaxWidth(), + ) + if (freeTextAvailable) { + Spacer(Modifier.height(8.dp)) + HedvigButton( + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Secondary, + text = stringResource(Res.string.CLAIMS_USE_TEXT_INSTEAD), + onClick = onSwitchToFreeText, + modifier = Modifier.fillMaxWidth(), + ) + } + } else { + if (uiStateAnimated is AudioRecordingStepState.AudioRecording.Playback) { + val audioPlayer = rememberAudioPlayer( + PlayableAudioSource.LocalFilePath(uiStateAnimated.filePath), + ) + HedvigAudioPlayer( + audioPlayer = audioPlayer, + Modifier.padding( + start = 45.dp, + ), + ) + } else { + SkippedLabel() + } + } } } } @@ -145,6 +220,367 @@ internal fun AudioRecorderBubble( } } +@Composable +private fun AudioRecordingBottomSheet( + bottomSheetState: HedvigBottomSheetState, + audioRecordingState: AudioRecordingStepState.AudioRecording, + clock: Clock, + shouldShowRequestPermissionRationale: (String) -> Boolean, + startRecording: () -> Unit, + stopRecording: () -> Unit, + submitAudioFile: () -> Unit, + redo: () -> Unit, + openAppSettings: () -> Unit, + continueButtonLoading: Boolean, + modifier: Modifier = Modifier, +) { + var showPermissionDialog by remember { mutableStateOf(false) } + val recordAudioPermissionState = if (LocalInspectionMode.current) { + object : PermissionState { + override val permission: String = "" + override val status: PermissionStatus = PermissionStatus.Granted + + override fun launchPermissionRequest() {} + } + } else { + rememberPermissionState(RECORD_AUDIO_PERMISSION) { isGranted -> + if (isGranted) { + startRecording() + } else { + showPermissionDialog = true + } + } + } + if (showPermissionDialog) { + PermissionDialog( + permissionDescription = stringResource(Res.string.PERMISSION_DIALOG_RECORD_AUDIO_MESSAGE), + isPermanentlyDeclined = !shouldShowRequestPermissionRationale(RECORD_AUDIO_PERMISSION), + onDismiss = { showPermissionDialog = false }, + okClick = recordAudioPermissionState::launchPermissionRequest, + openAppSettings = openAppSettings, + ) + } + + val audioPlayer = ( + audioRecordingState as? + AudioRecordingStepState.AudioRecording.Playback + )?.let { + rememberAudioPlayer( + PlayableAudioSource.LocalFilePath(it.filePath), + ) + } + + HedvigBottomSheet(bottomSheetState, modifier) { + Column { + HedvigText( + stringResource(Res.string.CLAIM_TRIAGING_TITLE), + modifier = Modifier.fillMaxWidth().semantics { + heading() + }, + textAlign = TextAlign.Center, + ) + DynamicClock(audioRecordingState, clock, audioPlayer) + Spacer(Modifier.height(24.dp)) + + AnimatedContent( + targetState = audioRecordingState, + transitionSpec = { + fadeIn(animationSpec = tween(300)) + .togetherWith(fadeOut(animationSpec = tween(300))) + }, + contentKey = { state -> + when (state) { + is AudioRecordingStepState.AudioRecording.Playback -> { + if (state.isPrepared) "playback" else "loading" + } + + is AudioRecordingStepState.AudioRecording.Recording -> "recording" + else -> "resting" + } + }, + ) { target -> + Box( + modifier = Modifier.height(158.dp), + contentAlignment = Alignment.Center, + ) { + when (target) { + is AudioRecordingStepState.AudioRecording.Playback if !target.isPrepared -> { + HedvigCircularProgressIndicator() + } + + is AudioRecordingStepState.AudioRecording.Playback -> { + val audioPlayerState by audioPlayer?.audioPlayerState?.collectAsStateWithLifecycle() + ?: remember { mutableStateOf(null) } + if (audioPlayerState is AudioPlayerState.Ready) { + AudioWaves( + animated = false, + progressPercentage = (audioPlayerState as AudioPlayerState.Ready).progressPercentage, + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 45.dp, + vertical = 29.dp, + ), + ) + } + } + + is AudioRecordingStepState.AudioRecording.Recording -> { + AudioWaves( + animated = true, + progressPercentage = null, + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 45.dp, + vertical = 29.dp, + ), + ) + } + + else -> { + RestingAudioPlayer( + Modifier + .fillMaxWidth() + .padding(horizontal = 45.dp), + ) + } + } + } + } + Row( + modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + AudioButton( + modifier = Modifier.weight(1f), + type = AudioButtonType.StartOver( + onStartOver = redo, + isEnabled = audioRecordingState is AudioRecordingStepState.AudioRecording.Playback && + !continueButtonLoading, + ), + audioPlayer = null, + ) + Spacer(Modifier.width(4.dp)) + AudioButton( + modifier = Modifier.weight(1f), + audioPlayer = audioPlayer, + type = AudioButtonType.Control( + onStartRecording = startRecording, + onStopRecording = stopRecording, + audioRecordingState = audioRecordingState, + isEnabled = !continueButtonLoading, + ), + ) + Spacer(Modifier.width(4.dp)) + AudioButton( + modifier = Modifier.weight(1f), + type = AudioButtonType.Send( + onSend = submitAudioFile, + isEnabled = audioRecordingState is AudioRecordingStepState.AudioRecording.Playback && + !continueButtonLoading, + ), + audioPlayer = null, + ) + } + Spacer(Modifier.height(16.dp)) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) + } + } +} + +@Composable +private fun DynamicClock( + audioRecordingState: AudioRecordingStepState.AudioRecording, + clock: Clock, + audioPlayer: AudioPlayer?, +) { + val startedRecordingAt by remember { + mutableStateOf(null) + }.apply { + if (audioRecordingState is AudioRecordingStepState.AudioRecording.Recording) { + value = audioRecordingState.startedAt + } + } + + val audioPlayerState by audioPlayer?.audioPlayerState?.collectAsStateWithLifecycle() + ?: remember { mutableStateOf(null) } + + val twoDigitsFormat = remember { DecimalFormatter("00") } + + val label = when (audioRecordingState) { + is AudioRecordingStepState.AudioRecording.Recording -> { + val diff = clock.now() - (startedRecordingAt ?: clock.now()) + "${twoDigitsFormat.format(diff.inWholeMinutes)}:${twoDigitsFormat.format(diff.inWholeSeconds % 60)}" + } + + is AudioRecordingStepState.AudioRecording.Playback -> { + val ready = audioPlayerState as? AudioPlayerState.Ready + if (ready != null) { + val durationSeconds = ready.durationMillis / 1000 + "${twoDigitsFormat.format(durationSeconds / 60)}:${twoDigitsFormat.format(durationSeconds % 60)}" + } else { + null + } + } + + else -> null + } + + val durationDescription = label?.let { + stringResource( + Res.string.TALKBACK_RECORDING_DURATION, + it, + ) + } + + HedvigText( + text = label ?: "", + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth().clearAndSetSemantics { + if (durationDescription != null) { + contentDescription = durationDescription + } + }, + color = HedvigTheme.colorScheme.textSecondary, + ) +} + +@Composable +private fun AudioButton(type: AudioButtonType, audioPlayer: AudioPlayer?, modifier: Modifier = Modifier) { + val audioPlayerState by audioPlayer?.audioPlayerState?.collectAsStateWithLifecycle() + ?: remember { mutableStateOf(null) } + Surface( + shape = HedvigTheme.shapes.cornerLarge, + modifier = modifier + .clip(HedvigTheme.shapes.cornerLarge) + .semantics(true) { + role = Role.Button + } + .clickable( + enabled = type.isEnabled, + onClick = { + when (type) { + is AudioButtonType.Control -> when (type.audioRecordingState) { + AudioRecordingStepState.AudioRecording.NotRecording -> { + type.onStartRecording() + } + + is AudioRecordingStepState.AudioRecording.Playback -> { + val ready = audioPlayerState as? AudioPlayerState.Ready + if (ready?.readyState is AudioPlayerState.Ready.ReadyState.Playing) { + audioPlayer?.pausePlayer() + } else { + audioPlayer?.startPlayer() + } + } + + is AudioRecordingStepState.AudioRecording.Recording -> type.onStopRecording() + } + + is AudioButtonType.Send -> type.onSend() + is AudioButtonType.StartOver -> type.onStartOver() + } + }, + ), + ) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier + .clip(HedvigTheme.shapes.cornerXXLarge) + .background( + color = if (!type.isEnabled) { + HedvigTheme.colorScheme.surfaceSecondaryTransparent + } else { + when (type) { + is AudioButtonType.Control -> when (type.audioRecordingState) { + AudioRecordingStepState.AudioRecording.NotRecording -> HedvigTheme.colorScheme.signalRedElement + is AudioRecordingStepState.AudioRecording.Playback -> HedvigTheme.colorScheme.fillPrimary + is AudioRecordingStepState.AudioRecording.Recording -> HedvigTheme.colorScheme.signalRedElement + } + + is AudioButtonType.Send -> HedvigTheme.colorScheme.signalBlueElement + is AudioButtonType.StartOver -> HedvigTheme.colorScheme.surfaceSecondaryTransparent + } + }, + ), + ) { + Icon( + modifier = Modifier.padding(4.dp).size(24.dp), + imageVector = when (type) { + is AudioButtonType.Control -> when (type.audioRecordingState) { + AudioRecordingStepState.AudioRecording.NotRecording -> HedvigIcons.Mic + is AudioRecordingStepState.AudioRecording.Playback -> { + val ready = audioPlayerState as? AudioPlayerState.Ready + if (ready?.readyState is AudioPlayerState.Ready.ReadyState.Playing) { + HedvigIcons.Pause + } else { + HedvigIcons.Play + } + } + + is AudioRecordingStepState.AudioRecording.Recording -> HedvigIcons.Pause + } + + is AudioButtonType.Send -> HedvigIcons.ArrowUp + is AudioButtonType.StartOver -> HedvigIcons.Reload + }, + contentDescription = EmptyContentDescription, + tint = if (!type.isEnabled) { + HedvigTheme.colorScheme.fillTertiary + } else { + if (type is AudioButtonType.StartOver) { + HedvigTheme.colorScheme.fillPrimary + } else { + HedvigTheme.colorScheme.fillNegative + } + }, + ) + } + Spacer(Modifier.height(4.dp)) + HedvigText( + text = when (type) { + is AudioButtonType.Control -> when (type.audioRecordingState) { + AudioRecordingStepState.AudioRecording.NotRecording -> stringResource(Res.string.AUDIO_RECORDER_START) + is AudioRecordingStepState.AudioRecording.Playback -> stringResource(Res.string.AUDIO_RECORDER_LISTEN) + is AudioRecordingStepState.AudioRecording.Recording -> stringResource(Res.string.AUDIO_RECORDER_STOP) + } + + is AudioButtonType.Send -> stringResource(Res.string.AUDIO_RECORDER_SEND) + is AudioButtonType.StartOver -> stringResource(Res.string.AUDIO_RECORDER_START_OVER) + }, + fontSize = HedvigTheme.typography.label.fontSize, + fontStyle = HedvigTheme.typography.label.fontStyle, + color = if (type.isEnabled) HedvigTheme.colorScheme.textPrimary else HedvigTheme.colorScheme.textTertiary, + ) + } + } +} + +private sealed interface AudioButtonType { + val isEnabled: Boolean + + class StartOver( + val onStartOver: () -> Unit, + override val isEnabled: Boolean, + ) : AudioButtonType + + class Control( + val onStartRecording: () -> Unit, + val onStopRecording: () -> Unit, + val audioRecordingState: AudioRecordingStepState.AudioRecording, + override val isEnabled: Boolean, + ) : AudioButtonType + + class Send( + val onSend: () -> Unit, + override val isEnabled: Boolean, + ) : AudioButtonType +} + @Composable private fun FreeTextInputSection( freeText: String?, @@ -213,62 +649,136 @@ private fun FreeTextInputSection( } @Composable -private fun AudioRecordingSection( - uiState: AudioRecordingStepState.AudioRecording, - clock: Clock, - shouldShowRequestPermissionRationale: (String) -> Boolean, - startRecording: () -> Unit, - stopRecording: () -> Unit, - submitAudioFile: () -> Unit, - redo: () -> Unit, - openAppSettings: () -> Unit, - launchFreeText: () -> Unit, - allowFreeText: Boolean, - isCurrentStep: Boolean, - continueButtonLoading: Boolean, - modifier: Modifier = Modifier, -) { - var showPermissionDialog by remember { mutableStateOf(false) } - val recordAudioPermissionState = if (LocalInspectionMode.current) { - object : PermissionState { - override val permission: String = "" - override val status: PermissionStatus = PermissionStatus.Granted +private fun AudioWaves(animated: Boolean, progressPercentage: ProgressPercentage?, modifier: Modifier = Modifier) { + val playedColor = LocalContentColor.current + val notPlayedColor = LocalContentColor.current.copy(0.38f) + .compositeOver(HedvigTheme.colorScheme.surfacePrimary) + val fixedColor = HedvigTheme.colorScheme.fillPrimary.copy(alpha = 0.6f) - override fun launchPermissionRequest() {} + BoxWithConstraints(modifier) { + val numberOfWaves = remember(maxWidth) { + (maxWidth / 5f).value.roundToInt() } - } else { - rememberPermissionState(RECORD_AUDIO_PERMISSION) { isGranted -> - if (isGranted) { - startRecording() - } else { - showPermissionDialog = true + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().height(maxHeight), + ) { + repeat(numberOfWaves) { waveIndex -> + val isRecording = progressPercentage == null + val baseHeight = remember(waveIndex, numberOfWaves, isRecording) { + // When progressPercentage is null (recording state), start all waves at 2dp height + if (isRecording) { + 0.02f // 2dp out of 100dp container (after padding) + } else { + val wavePosition = waveIndex + 1 + val centerPoint = numberOfWaves / 2 + val distanceFromCenterPoint = abs(centerPoint - wavePosition) + val percentageToCenterPoint = + ((centerPoint - distanceFromCenterPoint).toFloat() / centerPoint) + val minWaveHeightFraction = 0.05f + val maxWaveHeightFractionForSideWaves = 0.05f + val maxWaveHeightFraction = 0.5f + val maxHeightFraction = lerp( + maxWaveHeightFractionForSideWaves, + maxWaveHeightFraction, + percentageToCenterPoint, + ) + if (maxHeightFraction <= minWaveHeightFraction) { + maxHeightFraction + } else { + Random.nextDouble(minWaveHeightFraction.toDouble(), maxHeightFraction.toDouble()) + .toFloat() + } + } + } + + val height = if (animated) { + var animatedHeight by remember { mutableStateOf(baseHeight) } + + LaunchedEffect(waveIndex) { + while (true) { + delay((50..150).random().toLong()) + // For recording state (baseHeight = 0.02), generate random heights within animation range + animatedHeight = if (progressPercentage == null) { + // Side waves (first and last ~5%) have smaller max height + val isSideWave = waveIndex < numberOfWaves * 0.05 || waveIndex > numberOfWaves * 0.95 + if (isSideWave) { + Random.nextFloat() * 0.05f + 0.01f // Range: 0.1f to 0.15f for side waves + } else { + Random.nextFloat() * 0.3f + 0.01f // Range: 0.1f to 0.4f for center waves + } + } else { + val variation = Random.nextFloat() * 0.2f - 0.1f + (baseHeight + variation).coerceIn(0.1f, 0.4f) + } + } + } + + val smoothHeight by animateFloatAsState( + targetValue = animatedHeight, + animationSpec = tween(durationMillis = 200, easing = LinearEasing), + ) + smoothHeight + } else { + baseHeight + } + + val backgroundColor = if (progressPercentage != null) { + val hasPlayedThisWave = remember(progressPercentage, numberOfWaves, waveIndex) { + progressPercentage.value * numberOfWaves > waveIndex + } + if (hasPlayedThisWave) playedColor else notPlayedColor + } else { + fixedColor + } + + WavePill( + heightFraction = height, + backgroundColor = backgroundColor, + ) } } } - if (showPermissionDialog) { - PermissionDialog( - permissionDescription = stringResource(Res.string.PERMISSION_DIALOG_RECORD_AUDIO_MESSAGE), - isPermanentlyDeclined = !shouldShowRequestPermissionRationale(RECORD_AUDIO_PERMISSION), - onDismiss = { showPermissionDialog = false }, - okClick = recordAudioPermissionState::launchPermissionRequest, - openAppSettings = openAppSettings, - ) - } - AudioRecorder( - uiState = uiState, - startRecording = recordAudioPermissionState::launchPermissionRequest, - clock = clock, - stopRecording = stopRecording, - submitAudioFile = submitAudioFile, - redo = redo, - modifier = modifier, - allowFreeText = allowFreeText, - onLaunchFreeText = launchFreeText, - isCurrentStep = isCurrentStep, - continueButtonLoading = continueButtonLoading, +} + +@Composable +private fun WavePill(heightFraction: Float, backgroundColor: Color) { + Box( + modifier = Modifier + .width(WAVE_WIDTH) + .fillMaxHeight(fraction = heightFraction) + .clip(CircleShape) + .background(backgroundColor), ) } +@Composable +fun RestingAudioPlayer(modifier: Modifier = Modifier) { + BoxWithConstraints(modifier) { + val numberOfWaves = remember(maxWidth) { + (maxWidth / 5f).value.roundToInt() + } + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth(), + ) { + repeat(numberOfWaves) { _ -> + Box( + modifier = Modifier + .size(WAVE_WIDTH) + .clip(CircleShape) + .background(HedvigTheme.colorScheme.fillPrimary), + ) + } + } + } +} + +private val WAVE_WIDTH = 2.dp + @HedvigPreview @Composable private fun PreviewFreeTextInput() { diff --git a/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/di/FeatureClaimDetailsModule.kt b/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/di/FeatureClaimDetailsModule.kt index 19f4fdf730..5712938434 100644 --- a/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/di/FeatureClaimDetailsModule.kt +++ b/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/di/FeatureClaimDetailsModule.kt @@ -1,8 +1,8 @@ package com.hedvig.android.feature.claim.details.di import com.apollographql.apollo.ApolloClient -import com.hedvig.android.core.fileupload.DownloadPdfUseCase import com.hedvig.android.core.fileupload.ClaimsServiceUploadFileUseCase +import com.hedvig.android.core.fileupload.DownloadPdfUseCase import com.hedvig.android.data.cross.sell.after.claim.closed.CrossSellAfterClaimClosedRepository import com.hedvig.android.feature.claim.details.data.GetClaimDetailUiStateUseCase import com.hedvig.android.feature.claim.details.ui.AddFilesViewModel diff --git a/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/ui/AddFilesViewModel.kt b/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/ui/AddFilesViewModel.kt index a98841b5c6..96429a1923 100644 --- a/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/ui/AddFilesViewModel.kt +++ b/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/ui/AddFilesViewModel.kt @@ -5,8 +5,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.raise.either import com.hedvig.android.apollo.NetworkCacheManager -import com.hedvig.android.core.fileupload.FileService import com.hedvig.android.core.fileupload.ClaimsServiceUploadFileUseCase +import com.hedvig.android.core.fileupload.FileService import com.hedvig.android.core.uidata.UiFile import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/ui/ClaimDetailsViewModel.kt b/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/ui/ClaimDetailsViewModel.kt index f9067d8638..b14b4a8d42 100644 --- a/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/ui/ClaimDetailsViewModel.kt +++ b/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/ui/ClaimDetailsViewModel.kt @@ -9,8 +9,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import arrow.fx.coroutines.parMap -import com.hedvig.android.core.fileupload.DownloadPdfUseCase import com.hedvig.android.core.fileupload.ClaimsServiceUploadFileUseCase +import com.hedvig.android.core.fileupload.DownloadPdfUseCase import com.hedvig.android.core.uidata.UiFile import com.hedvig.android.data.display.items.DisplayItem import com.hedvig.android.feature.claim.details.data.GetClaimDetailUiStateUseCase diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index b775aa4b5d..0b2a28db50 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -404,7 +404,7 @@ private fun StartClaimBottomSheet( enabled = isChecked, onClick = dropUnlessResumed { state.dismiss { - if (isExperimentalClaimChatEnabled) { + if (isExperimentalClaimChatEnabled) { navigateToClaimChat() } else { navigateToOldClaimFlow() @@ -471,11 +471,7 @@ private fun StartClaimBottomSheet( } @Composable -private fun ImportantInfoCheckBox( - isChecked: Boolean, - onCheckedChange: () -> Unit, - modifier: Modifier = Modifier, -) { +private fun ImportantInfoCheckBox(isChecked: Boolean, onCheckedChange: () -> Unit, modifier: Modifier = Modifier) { Surface( shape = HedvigTheme.shapes.cornerLarge, modifier = modifier, diff --git a/app/feature/feature-login/src/main/kotlin/com/hedvig/android/feature/login/swedishlogin/BankIdState.kt b/app/feature/feature-login/src/main/kotlin/com/hedvig/android/feature/login/swedishlogin/BankIdState.kt index 4efee5358c..4bacc9f4c6 100644 --- a/app/feature/feature-login/src/main/kotlin/com/hedvig/android/feature/login/swedishlogin/BankIdState.kt +++ b/app/feature/feature-login/src/main/kotlin/com/hedvig/android/feature/login/swedishlogin/BankIdState.kt @@ -61,7 +61,7 @@ private class BankIdStateImpl( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { packageManager.getPackageInfo( BankIdAppPackageName, - PackageManager.PackageInfoFlags.of(0) + PackageManager.PackageInfoFlags.of(0), ) } else { @Suppress("DEPRECATION") diff --git a/app/feature/feature-odyssey/src/main/kotlin/com/hedvig/android/feature/odyssey/di/OdysseyModule.kt b/app/feature/feature-odyssey/src/main/kotlin/com/hedvig/android/feature/odyssey/di/OdysseyModule.kt index 55b39702f9..5bc2bf1193 100644 --- a/app/feature/feature-odyssey/src/main/kotlin/com/hedvig/android/feature/odyssey/di/OdysseyModule.kt +++ b/app/feature/feature-odyssey/src/main/kotlin/com/hedvig/android/feature/odyssey/di/OdysseyModule.kt @@ -1,7 +1,7 @@ package com.hedvig.android.feature.odyssey.di -import com.hedvig.android.core.fileupload.FileService import com.hedvig.android.core.fileupload.ClaimsServiceUploadFileUseCase +import com.hedvig.android.core.fileupload.FileService import com.hedvig.android.data.claimflow.ClaimFlowDestination import com.hedvig.android.data.claimflow.ClaimFlowRepository import com.hedvig.android.data.claimflow.LocationOption diff --git a/app/feature/feature-odyssey/src/main/kotlin/com/hedvig/android/feature/odyssey/step/fileupload/FileUploadViewModel.kt b/app/feature/feature-odyssey/src/main/kotlin/com/hedvig/android/feature/odyssey/step/fileupload/FileUploadViewModel.kt index 098c348e32..12e2a37567 100644 --- a/app/feature/feature-odyssey/src/main/kotlin/com/hedvig/android/feature/odyssey/step/fileupload/FileUploadViewModel.kt +++ b/app/feature/feature-odyssey/src/main/kotlin/com/hedvig/android/feature/odyssey/step/fileupload/FileUploadViewModel.kt @@ -4,8 +4,8 @@ import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.raise.either -import com.hedvig.android.core.fileupload.FileService import com.hedvig.android.core.fileupload.ClaimsServiceUploadFileUseCase +import com.hedvig.android.core.fileupload.FileService import com.hedvig.android.core.uidata.UiFile import com.hedvig.android.data.claimflow.ClaimFlowRepository import com.hedvig.android.data.claimflow.ClaimFlowStep diff --git a/app/logging/logging-public/src/androidMain/kotlin/com/hedvig/android/logger/AndroidLogcatLogger.kt b/app/logging/logging-public/src/androidMain/kotlin/com/hedvig/android/logger/AndroidLogcatLogger.kt index 0a12bff970..222d1d516e 100644 --- a/app/logging/logging-public/src/androidMain/kotlin/com/hedvig/android/logger/AndroidLogcatLogger.kt +++ b/app/logging/logging-public/src/androidMain/kotlin/com/hedvig/android/logger/AndroidLogcatLogger.kt @@ -35,7 +35,9 @@ class AndroidLogcatLogger : LogcatLogger { } private fun v(throwable: Throwable?, tag: String?, message: () -> String) { - if (tag != null) { Timber.tag(tag) } + if (tag != null) { + Timber.tag(tag) + } if (throwable != null) { v(throwable, message) } else { @@ -44,7 +46,9 @@ private fun v(throwable: Throwable?, tag: String?, message: () -> String) { } private fun d(throwable: Throwable?, tag: String?, message: () -> String) { - if (tag != null) { Timber.tag(tag) } + if (tag != null) { + Timber.tag(tag) + } if (throwable != null) { d(throwable, message) } else { @@ -53,7 +57,9 @@ private fun d(throwable: Throwable?, tag: String?, message: () -> String) { } private fun i(throwable: Throwable?, tag: String?, message: () -> String) { - if (tag != null) { Timber.tag(tag) } + if (tag != null) { + Timber.tag(tag) + } if (throwable != null) { i(throwable, message) } else { @@ -62,7 +68,9 @@ private fun i(throwable: Throwable?, tag: String?, message: () -> String) { } private fun w(throwable: Throwable?, tag: String?, message: () -> String) { - if (tag != null) { Timber.tag(tag) } + if (tag != null) { + Timber.tag(tag) + } if (throwable != null) { w(throwable, message) } else { @@ -71,7 +79,9 @@ private fun w(throwable: Throwable?, tag: String?, message: () -> String) { } private fun e(throwable: Throwable?, tag: String?, message: () -> String) { - if (tag != null) { Timber.tag(tag) } + if (tag != null) { + Timber.tag(tag) + } if (throwable != null) { e(throwable, message) } else { @@ -80,7 +90,9 @@ private fun e(throwable: Throwable?, tag: String?, message: () -> String) { } private fun wtf(throwable: Throwable?, tag: String?, message: () -> String) { - if (tag != null) { Timber.tag(tag) } + if (tag != null) { + Timber.tag(tag) + } if (throwable != null) { wtf(throwable, message) } else { diff --git a/app/shareddi/src/nativeMain/kotlin/com/hedvig/android/shareddi/SharedModule.native.kt b/app/shareddi/src/nativeMain/kotlin/com/hedvig/android/shareddi/SharedModule.native.kt index 462303a47f..f567bad381 100644 --- a/app/shareddi/src/nativeMain/kotlin/com/hedvig/android/shareddi/SharedModule.native.kt +++ b/app/shareddi/src/nativeMain/kotlin/com/hedvig/android/shareddi/SharedModule.native.kt @@ -8,16 +8,12 @@ import org.koin.core.module.Module import org.koin.dsl.module internal actual val platformModule: Module = module { - } /** * Like [platformModule] but allows for dynamic input, for pieces that need to be injected from iOS */ -internal fun iosPlatformModule( - accessTokenFetcher: AccessTokenFetcher, - deviceIdFetcher: DeviceIdFetcher, -) = module { +internal fun iosPlatformModule(accessTokenFetcher: AccessTokenFetcher, deviceIdFetcher: DeviceIdFetcher) = module { single { accessTokenFetcher }