diff --git a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls index f33ce82da4..b094965ce3 100644 --- a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls +++ b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls @@ -1280,6 +1280,8 @@ type ClaimIntentStepContentSummary { audioRecordings: [ClaimIntentStepContentSummaryAudioRecording!]! freeTexts: [String!]! fileUploads: [ClaimIntentStepContentSummaryFileUpload!]! + keyDetails: [ClaimIntentStepContentSummaryItem!]! + answers: [ClaimIntentStepContentSummaryAnswer!]! } type ClaimIntentStepContentSummaryAudioRecording { url: Url! @@ -1293,6 +1295,21 @@ type ClaimIntentStepContentSummaryItem { title: String! value: String! } +type ClaimIntentStepContentSummaryAnswer { + title: String! + value: ClaimIntentStepContentSummaryAnswerValue! +} +union ClaimIntentStepContentSummaryAnswerValue = ClaimIntentStepContentSummaryAnswerText|ClaimIntentStepContentSummaryAnswerAudio|ClaimIntentStepContentSummaryAnswerFiles +type ClaimIntentStepContentSummaryAnswerText { + text: String! +} +type ClaimIntentStepContentSummaryAnswerAudio { + url: Url! + transcript: String +} +type ClaimIntentStepContentSummaryAnswerFiles { + files: [ClaimIntentStepContentSummaryFileUpload!]! +} """ A task step is one that eventually completes itself. This is meant to be polled until `isCompleted` is true, and then concluded using `Mutation.claimIntentSubmitTask`. diff --git a/app/audio-player-ui/src/jvmMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.jvm.kt b/app/audio-player-ui/src/jvmMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.jvm.kt index 31a4004c8b..c81ad577bd 100644 --- a/app/audio-player-ui/src/jvmMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.jvm.kt +++ b/app/audio-player-ui/src/jvmMain/kotlin/com/hedvig/android/audio/player/CommonMediaPlayer.jvm.kt @@ -1,5 +1,38 @@ package com.hedvig.android.audio.player -actual fun CommonMediaPlayer(dataSourceUrl: String): CommonMediaPlayer { - TODO("jvm") +import com.hedvig.audio.player.data.ProgressPercentage + +/** + * No JVM/desktop application target ships audio playback — the JVM target exists only for headless + * Compose render tests and previews. This no-op implementation lets audio-backed composables (e.g. + * [HedvigAudioPlayer]) compose and render on the JVM without a real media backend. It never plays + * anything and stays in the initial "preparing" state. + */ +actual fun CommonMediaPlayer(dataSourceUrl: String): CommonMediaPlayer = NoOpCommonMediaPlayer + +private object NoOpCommonMediaPlayer : CommonMediaPlayer { + override val isPlaying: Boolean = false + override val duration: Int = 0 + + override fun pause() {} + + override fun start() {} + + override fun stop() {} + + override fun release() {} + + override suspend fun seekToPercent(percentage: ProgressPercentage) {} + + override fun hasReachedTheEnd(): Boolean = false + + override fun getProgressPercentage(): ProgressPercentage = ProgressPercentage(0f) + + override fun setOnErrorListener(block: (what: Int, extra: Int) -> Boolean) {} + + override fun setOnPreparedListener(block: () -> Unit) {} + + override fun setOnCompletionListener(block: () -> Unit) {} + + override fun prepareAsync() {} } diff --git a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml index 9b00cccdb5..ac565f0233 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -1068,6 +1068,7 @@ Saknas något? Skicka ett meddelande till oss här i appen. Inskickad Detaljer om skadeanmälan + Visa alla svar Ärendet är avslutat. Har du frågor gällande beslutet? Ställ dem direkt i chatten. Filer Du kan lyssna på din inspelade skadeanmälan här. diff --git a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml index eabd4337b2..a4ddd04181 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -1068,6 +1068,7 @@ Is something missing? Send us a message here in the app. Submitted Claim details + Show all answers The claim is closed. Do you have any questions regarding your claim? Please ask them directly in the chat. Files You can listen to your recorded claim audio here. diff --git a/app/feature/feature-claim-chat/build.gradle.kts b/app/feature/feature-claim-chat/build.gradle.kts index a379a3dc2b..71352d5af3 100644 --- a/app/feature/feature-claim-chat/build.gradle.kts +++ b/app/feature/feature-claim-chat/build.gradle.kts @@ -55,6 +55,9 @@ kotlin { implementation(projects.uiForceUpgrade) implementation(projects.partnersDeflect) } + jvmTest.dependencies { + implementation(org.jetbrains.compose.ComposePlugin.Dependencies(project).desktop.currentOs) + } androidMain.dependencies { implementation(libs.accompanist.permissions) implementation(libs.androidx.activity.compose) diff --git a/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql b/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql index 971cf5639e..255006dbe4 100644 --- a/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql +++ b/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql @@ -117,6 +117,29 @@ fragment SummaryFragment on ClaimIntentStepContentSummary { fileName } freeTexts + keyDetails { + title + value + } + answers { + title + value { + ... on ClaimIntentStepContentSummaryAnswerText { + text + } + ... on ClaimIntentStepContentSummaryAnswerAudio { + url + transcript + } + ... on ClaimIntentStepContentSummaryAnswerFiles { + files { + url + contentType + fileName + } + } + } + } } fragment DeflectionFragment on ClaimIntentStepContentDeflection { 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 e6acf85fd5..d389c3fe2b 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 @@ -156,6 +156,8 @@ internal sealed interface StepContent { val audioRecordings: List, val fileUploads: List, val freeTexts: List, + val keyDetails: List, + val answers: List, ) : StepContent { override val isSkippable: Boolean = false @@ -164,6 +166,16 @@ internal sealed interface StepContent { data class AudioRecording(val url: String) data class FileUpload(val url: String, val contentType: String, val fileName: String) + + data class Answer(val title: String, val value: Value) { + sealed interface Value { + data class Text(val text: String) : Value + + data class Audio(val url: String, val transcript: String?) : Value + + data class Files(val files: List) : Value + } + } } @Serializable 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 b771df9794..0fb2953ef2 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 @@ -178,6 +178,34 @@ private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale): ) }, freeTexts = freeTexts, + keyDetails = keyDetails.map { StepContent.Summary.Item(it.title, it.value) }, + answers = answers.map { answer -> + StepContent.Summary.Answer( + title = answer.title, + value = when (val value = answer.value) { + is SummaryFragment.Answer.ClaimIntentStepContentSummaryAnswerTextValue -> { + StepContent.Summary.Answer.Value.Text(value.text) + } + + is SummaryFragment.Answer.ClaimIntentStepContentSummaryAnswerAudioValue -> { + StepContent.Summary.Answer.Value.Audio(value.url, value.transcript) + } + + is SummaryFragment.Answer.ClaimIntentStepContentSummaryAnswerFilesValue -> { + StepContent.Summary.Answer.Value.Files( + value.files.map { + StepContent.Summary.FileUpload(it.url, it.contentType, it.fileName) + }, + ) + } + + else -> { + logcat { "SummaryFragment.Answer: Unknown answer value type" } + raise(ClaimChatErrorMessage.NeedsUpdate) + } + }, + ) + }, ) } 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 255bcb99fc..9a51253505 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 @@ -786,8 +786,9 @@ private fun StepTopContent( Spacer(Modifier.height(16.dp)) if (stepItem.stepContent is StepContent.Summary) { ChatClaimSummaryTopContent( + keyDetails = stepItem.stepContent.keyDetails.ifEmpty { stepItem.stepContent.items }, + answers = stepItem.stepContent.answers, recordingUrls = stepItem.stepContent.audioRecordings.map { it.url }, - displayItems = stepItem.stepContent.items.map { (title, value) -> title to value }, onNavigateToImageViewer = onNavigateToImageViewer, imageLoader = imageLoader, fileUploads = stepItem.stepContent.fileUploads.map { @@ -799,7 +800,6 @@ private fun StepTopContent( id = it.url, ) }, - freeTexts = stepItem.stepContent.freeTexts, ) } } diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/ChatClaimSummaryStep.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/ChatClaimSummaryStep.kt index 06e5cfae63..adb3b9c251 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/ChatClaimSummaryStep.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/ChatClaimSummaryStep.kt @@ -22,7 +22,10 @@ import coil3.ImageLoader import com.hedvig.android.audio.player.HedvigAudioPlayer import com.hedvig.android.audio.player.audioplayer.rememberAudioPlayer import com.hedvig.android.core.uidata.UiFile +import com.hedvig.android.design.system.hedvig.HedvigBottomSheet import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.ButtonDefaults +import com.hedvig.android.design.system.hedvig.HedvigButtonGhostWithBorder import com.hedvig.android.design.system.hedvig.HedvigCard import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigText @@ -30,16 +33,19 @@ import com.hedvig.android.design.system.hedvig.HedvigTheme import com.hedvig.android.design.system.hedvig.HorizontalItemsWithMaximumSpaceTaken import com.hedvig.android.design.system.hedvig.LocalContentColor import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.rememberHedvigBottomSheetState import com.hedvig.android.design.system.hedvig.rememberPreviewImageLoader import com.hedvig.audio.player.data.PlayableAudioSource import com.hedvig.audio.player.data.SignedAudioUrl +import com.hedvig.feature.claim.chat.data.StepContent import com.hedvig.feature.claim.chat.ui.common.FilesRow -import com.hedvig.feature.claim.chat.ui.common.RoundCornersPill import hedvig.resources.CLAIM_CHAT_FILE_TITLE import hedvig.resources.CLAIM_CHAT_RECORDING_TITLE import hedvig.resources.EMBARK_SUBMIT_CLAIM import hedvig.resources.Res import hedvig.resources.claim_status_claim_details_title +import hedvig.resources.claim_status_show_all_answers +import hedvig.resources.general_close_button import org.jetbrains.compose.resources.stringResource @Composable @@ -64,14 +70,23 @@ internal fun ChatClaimSummaryBottomContent( @Composable internal fun ChatClaimSummaryTopContent( + keyDetails: List, + answers: List, recordingUrls: List, fileUploads: List, - freeTexts: List, - displayItems: List>, imageLoader: ImageLoader, onNavigateToImageViewer: (imageUrl: String, cacheKey: String) -> Unit, modifier: Modifier = Modifier, ) { + val answersSheetState = rememberHedvigBottomSheetState>() + HedvigBottomSheet(answersSheetState) { sheetAnswers -> + ClaimSummaryAnswersContent( + answers = sheetAnswers, + imageLoader = imageLoader, + onNavigateToImageViewer = onNavigateToImageViewer, + onClose = { answersSheetState.dismiss() }, + ) + } Column(modifier) { HedvigCard( color = HedvigTheme.colorScheme.fillNegative, @@ -87,7 +102,7 @@ internal fun ChatClaimSummaryTopContent( ), ) { Column(Modifier.padding(vertical = 16.dp)) { - if (displayItems.isNotEmpty()) { + if (keyDetails.isNotEmpty()) { HedvigText( stringResource(Res.string.claim_status_claim_details_title), modifier = Modifier.padding(horizontal = 16.dp).semantics { @@ -97,15 +112,15 @@ internal fun ChatClaimSummaryTopContent( Spacer(Modifier.height(8.dp)) CompositionLocalProvider(LocalContentColor provides HedvigTheme.colorScheme.textSecondary) { Column(Modifier.padding(horizontal = 16.dp)) { - for (displayItem in displayItems) { + for (keyDetail in keyDetails) { HorizontalItemsWithMaximumSpaceTaken( spaceBetween = 8.dp, startSlot = { - HedvigText(text = displayItem.first) + HedvigText(text = keyDetail.title) }, endSlot = { HedvigText( - text = displayItem.second, + text = keyDetail.value, textAlign = TextAlign.End, ) }, @@ -115,6 +130,14 @@ internal fun ChatClaimSummaryTopContent( } } } + if (answers.isNotEmpty()) { + Spacer(Modifier.height(16.dp)) + HedvigButtonGhostWithBorder( + text = stringResource(Res.string.claim_status_show_all_answers), + onClick = { answersSheetState.show(answers) }, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + ) + } if (recordingUrls.isNotEmpty()) { Spacer(Modifier.height(24.dp)) HedvigText( @@ -134,23 +157,6 @@ internal fun ChatClaimSummaryTopContent( } } } - if (freeTexts.isNotEmpty()) { - Column( - Modifier.padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - freeTexts.forEach { string -> - Spacer(Modifier.height(24.dp)) - RoundCornersPill( - modifier = Modifier.fillMaxWidth(), - onClick = null, - isSelected = false, - ) { - HedvigText(string) - } - } - } - } if (fileUploads.isNotEmpty()) { Spacer(Modifier.height(24.dp)) HedvigText( @@ -172,6 +178,87 @@ internal fun ChatClaimSummaryTopContent( } } +@Composable +internal fun ClaimSummaryAnswersContent( + answers: List, + imageLoader: ImageLoader, + onNavigateToImageViewer: (imageUrl: String, cacheKey: String) -> Unit, + onClose: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier.fillMaxWidth()) { + Spacer(Modifier.height(16.dp)) + HedvigText( + stringResource(Res.string.claim_status_claim_details_title), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth().semantics { heading() }, + ) + Spacer(Modifier.height(24.dp)) + answers.forEachIndexed { index, answer -> + HedvigText(text = answer.title) + Spacer(Modifier.height(4.dp)) + CompositionLocalProvider(LocalContentColor provides HedvigTheme.colorScheme.textSecondary) { + AnswerValue( + value = answer.value, + imageLoader = imageLoader, + onNavigateToImageViewer = onNavigateToImageViewer, + ) + } + if (index != answers.lastIndex) { + Spacer(Modifier.height(24.dp)) + } + } + Spacer(Modifier.height(32.dp)) + HedvigButton( + text = stringResource(Res.string.general_close_button), + onClick = onClose, + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Secondary, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun AnswerValue( + value: StepContent.Summary.Answer.Value, + imageLoader: ImageLoader, + onNavigateToImageViewer: (imageUrl: String, cacheKey: String) -> Unit, +) { + when (value) { + is StepContent.Summary.Answer.Value.Text -> { + HedvigText(text = value.text) + } + + is StepContent.Summary.Answer.Value.Audio -> { + val audioPlayer = rememberAudioPlayer( + PlayableAudioSource.RemoteUrl( + SignedAudioUrl.fromSignedAudioUrlString(value.url), + ), + ) + HedvigAudioPlayer(audioPlayer = audioPlayer) + } + + is StepContent.Summary.Answer.Value.Files -> { + FilesRow( + uiFiles = value.files.map { + UiFile( + name = it.fileName, + localPath = null, + url = it.url, + mimeType = it.contentType, + id = it.url, + ) + }, + imageLoader = imageLoader, + onNavigateToImageViewer = onNavigateToImageViewer, + onRemoveFile = null, + alignment = Alignment.Start, + ) + } + } +} + @HedvigPreview @Composable private fun PreviewSummaryTopContent() { @@ -184,17 +271,56 @@ private fun PreviewSummaryTopContent() { modifier = Modifier.padding(horizontal = 16.dp), ) { ChatClaimSummaryTopContent( - recordingUrls = listOf("", ""), - displayItems = listOf( - "Locked" to "Yes", - "Electric bike" to "Yes", + keyDetails = listOf( + StepContent.Summary.Item("Type of claim", "Theft"), + StepContent.Summary.Item("Date", "2026-07-13"), + StepContent.Summary.Item("Location", "Stockholm"), + ), + answers = previewAnswers(), + recordingUrls = listOf(""), + fileUploads = listOf( + UiFile("receipt.pdf", null, "https://example.com/receipt.pdf", "application/pdf", "file-1"), ), - fileUploads = listOf(), imageLoader = rememberPreviewImageLoader(), onNavigateToImageViewer = { _, _ -> }, - freeTexts = listOf("A quite short text short text short text short text"), ) } } } } + +@HedvigPreview +@Composable +private fun PreviewSummaryAnswersContent() { + HedvigTheme { + Surface( + color = HedvigTheme.colorScheme.backgroundPrimary, + ) { + ClaimSummaryAnswersContent( + answers = previewAnswers(), + imageLoader = rememberPreviewImageLoader(), + onNavigateToImageViewer = { _, _ -> }, + onClose = {}, + modifier = Modifier.padding(16.dp), + ) + } + } +} + +internal fun previewAnswers(): List = listOf( + StepContent.Summary.Answer( + title = "Was the bike locked?", + value = StepContent.Summary.Answer.Value.Text("No"), + ), + StepContent.Summary.Answer( + title = "Where did it happen?", + value = StepContent.Summary.Answer.Value.Text("Outside the central station in Stockholm"), + ), + StepContent.Summary.Answer( + title = "Describe what happened", + value = StepContent.Summary.Answer.Value.Audio( + url = "", + transcript = "I parked my bike and when I came back it was gone.", + ), + ), +)