From 12ec6a05d4693b8a985dc002f7d218146630d548 Mon Sep 17 00:00:00 2001 From: Rajat Mittal Date: Wed, 9 Oct 2024 13:08:35 -0700 Subject: [PATCH] Added support for interactive messages --- .../viewmodel/ChatViewModel.kt | 2 +- .../views/ChatComponents.kt | 2 +- .../androidchatexample/views/ConfigPicker.kt | 10 + .../views/ListPickerContentView.kt | 63 +++--- .../amazon/connect/chat/sdk/model/Message.kt | 8 +- .../connect/chat/sdk/model/MessageContent.kt | 190 +++++++++++++++++- .../sdk/network/MessageReceiptsManager.kt | 5 +- 7 files changed, 246 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/amazon/connect/chat/androidchatexample/viewmodel/ChatViewModel.kt b/app/src/main/java/com/amazon/connect/chat/androidchatexample/viewmodel/ChatViewModel.kt index 7d7c233..5bf126c 100644 --- a/app/src/main/java/com/amazon/connect/chat/androidchatexample/viewmodel/ChatViewModel.kt +++ b/app/src/main/java/com/amazon/connect/chat/androidchatexample/viewmodel/ChatViewModel.kt @@ -266,10 +266,10 @@ class ChatViewModel @Inject constructor( } fun endChat(){ + clearParticipantToken() viewModelScope.launch { chatSession.disconnect() } - clearParticipantToken() } fun clearErrorMessage() { diff --git a/app/src/main/java/com/amazon/connect/chat/androidchatexample/views/ChatComponents.kt b/app/src/main/java/com/amazon/connect/chat/androidchatexample/views/ChatComponents.kt index 5672b02..0d5f3ae 100644 --- a/app/src/main/java/com/amazon/connect/chat/androidchatexample/views/ChatComponents.kt +++ b/app/src/main/java/com/amazon/connect/chat/androidchatexample/views/ChatComponents.kt @@ -59,7 +59,7 @@ fun ChatMessageView( } is QuickReplyContent -> QuickReplyContentView(transcriptItem, content) is ListPickerContent -> ListPickerContentView(transcriptItem, content) - else -> Text(text = "Unsupported message type") + else -> Text(text = "Unsupported message type, View is missing") } } MessageDirection.COMMON -> CommonChatBubble(transcriptItem) diff --git a/app/src/main/java/com/amazon/connect/chat/androidchatexample/views/ConfigPicker.kt b/app/src/main/java/com/amazon/connect/chat/androidchatexample/views/ConfigPicker.kt index d581cf3..6ea7151 100644 --- a/app/src/main/java/com/amazon/connect/chat/androidchatexample/views/ConfigPicker.kt +++ b/app/src/main/java/com/amazon/connect/chat/androidchatexample/views/ConfigPicker.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.amazon.connect.chat.androidchatexample.Config import com.amazon.connect.chat.androidchatexample.viewmodel.ChatViewModel @@ -52,8 +53,17 @@ fun ConfigPicker(viewModel: ChatViewModel) { .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { + Text( + text = "Note: If you see Participant Token available, DO NOT change your account from dropdown, just start the chat to resume previous session", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 36.dp).fillMaxWidth().align(Alignment.CenterHorizontally), + color = Color.Red, + textAlign = TextAlign.Center + ) + Text("Select Configuration", style = MaterialTheme.typography.bodyLarge) + // Exposed dropdown menu for selecting configuration ExposedDropdownMenuBox( expanded = expanded, diff --git a/app/src/main/java/com/amazon/connect/chat/androidchatexample/views/ListPickerContentView.kt b/app/src/main/java/com/amazon/connect/chat/androidchatexample/views/ListPickerContentView.kt index 9199fc2..1c0fccb 100644 --- a/app/src/main/java/com/amazon/connect/chat/androidchatexample/views/ListPickerContentView.kt +++ b/app/src/main/java/com/amazon/connect/chat/androidchatexample/views/ListPickerContentView.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.AsyncImage @@ -49,21 +50,44 @@ fun ListPickerContentView( ) { var showListPicker by remember { mutableStateOf(true) } val viewModel: ChatViewModel = hiltViewModel() + Column( + modifier = Modifier + .padding(8.dp) + .background(Color.White, RoundedCornerShape(8.dp)) + .fillMaxWidth(0.80f), + horizontalAlignment = Alignment.End, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically + ) { + message.displayName?.let { + Text( + text = it.ifEmpty { message.participant }, + color = Color.Black, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .weight(1f) + .padding(end = 4.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Text( + text = CommonUtils.formatTime(message.timeStamp) ?: "", + color = Color.Gray, + style = MaterialTheme.typography.bodySmall + ) + } - if (message.participant != null) { - Text( - text = message.participant!!, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(start = 8.dp, bottom = 4.dp) - ) - } if (showListPicker) { Column( modifier = Modifier - .padding(start = 8.dp) .clip(RoundedCornerShape(size = 10.dp)) .widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.75f) - .background(color = Color(0xFF9FB980)) + .background(color = Color(0xFFEDEDED)) .padding(horizontal = 10.dp, vertical = 10.dp) ) { if (!content.imageUrl.isNullOrEmpty()) { @@ -101,29 +125,22 @@ fun ListPickerContentView( } }else { Surface( - color = Color(0xFF8BC34A), + color = Color(0xFFEDEDED), shape = RoundedCornerShape(10.dp), modifier = Modifier - .padding(start = 8.dp) - .background(Color(0xFF8BC34A), shape = RoundedCornerShape(10.dp)) - .widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.75f) + .background(Color(0xFFEDEDED), shape = RoundedCornerShape(10.dp)) + .widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.80f) ) { - Column(modifier = Modifier.padding(10.dp)) { + Column(modifier = Modifier.padding(10.dp).fillMaxWidth(), + horizontalAlignment = Alignment.Start) { Text( text = content.title, - color = Color.White + color = Color.Black ) - message.timeStamp?.let { - Text( - text = CommonUtils.formatTime(it), - style = MaterialTheme.typography.bodySmall, - color = Color.White, - modifier = Modifier.align(Alignment.End).alpha(0.7f) - ) - } } } } + } } @Composable diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/Message.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/Message.kt index d703b0f..f34eb29 100644 --- a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/Message.kt +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/Message.kt @@ -52,9 +52,11 @@ data class Message( val jsonData = text.toByteArray(Charsets.UTF_8) val genericTemplate = Json { ignoreUnknownKeys = true }.decodeFromString(String(jsonData)) when (genericTemplate.templateType) { - QuickReplyContent.templateType -> QuickReplyContent.decode(text) - ListPickerContent.templateType -> ListPickerContent.decode(text) - // Add cases for each interactive message type, decoding as appropriate. + QuickReplyContent.TEMPLATE_TYPE -> QuickReplyContent.decode(text) + ListPickerContent.TEMPLATE_TYPE -> ListPickerContent.decode(text) + TimePickerContent.TEMPLATE_TYPE -> TimePickerContent.decode(text) + CarouselContent.TEMPLATE_TYPE -> CarouselContent.decode(text) + PanelContent.TEMPLATE_TYPE -> PanelContent.decode(text) else -> { logUnsupportedContentType(genericTemplate.templateType) null diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/MessageContent.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/MessageContent.kt index bfaaecc..e45fb44 100644 --- a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/MessageContent.kt +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/MessageContent.kt @@ -1,6 +1,8 @@ package com.amazon.connect.chat.sdk.model +import com.amazon.connect.chat.sdk.utils.Constants import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json interface MessageContent { @@ -52,7 +54,7 @@ data class QuickReplyContent( val options: List ) : InteractiveContent { companion object { - const val templateType = "QuickReply" + const val TEMPLATE_TYPE = Constants.QUICK_REPLY fun decode(text: String): InteractiveContent? { return try { val quickReply = Json.decodeFromString(text) @@ -102,7 +104,7 @@ data class ListPickerContent( val options: List ) : InteractiveContent { companion object { - const val templateType = "ListPicker" + const val TEMPLATE_TYPE = Constants.LIST_PICKER fun decode(text: String): InteractiveContent? { return try { val listPicker = Json.decodeFromString(text) @@ -118,3 +120,187 @@ data class ListPickerContent( } } } + + +// Time Picker +@Serializable +data class TimeSlot( + val date: String, + val duration: Int +) + +@Serializable +data class Location( + val latitude: Double, + val longitude: Double, + val title: String, + val radius: Int? = null +) + +@Serializable +data class TimePickerContentData( + val title: String, + val subtitle: String? = null, + val timeZoneOffset: Int? = null, + val location: Location? = null, + val timeslots: List +) + +@Serializable +data class TimePickerReplyMessage( + val title: String? = null, + val subtitle: String? = null +) + +@Serializable +data class TimePickerData( + val replyMessage: TimePickerReplyMessage? = null, + val content: TimePickerContentData +) + +@Serializable +data class TimePickerTemplate( + val templateType: String, + val version: String, + val data: TimePickerData +) + +data class TimePickerContent( + val title: String, + val subtitle: String? = null, + val timeZoneOffset: Int? = null, + val location: Location? = null, + val timeslots: List +): InteractiveContent { + companion object { + const val TEMPLATE_TYPE = Constants.TIME_PICKER + fun decode(text: String): InteractiveContent? { + return try { + val timePicker = Json.decodeFromString(text) + val contentData = timePicker.data.content + TimePickerContent( + title = contentData.title, + subtitle = contentData.subtitle, + timeZoneOffset = contentData.timeZoneOffset, + location = contentData.location, + timeslots = contentData.timeslots + ) + } catch (e: SerializationException) { + println("Error decoding TimePickerContent: ${e.localizedMessage}") + null + } + } + } +} + +// Carousel +@Serializable +data class CarouselElement( + val templateIdentifier: String, + val templateType: String, + val version: String, + val data: PanelData +) + +@Serializable +data class CarouselContentData( + val title: String, + val elements: List +) + +@Serializable +data class CarouselData( + val content: CarouselContentData +) + +@Serializable +data class CarouselTemplate( + val templateType: String, + val version: String, + val data: CarouselData +) + +data class CarouselContent( + val title: String, + val elements: List +): InteractiveContent { + companion object { + const val TEMPLATE_TYPE = Constants.CAROUSEL + fun decode(text: String): InteractiveContent? { + return try { + val carousel = Json.decodeFromString(text) + val contentData = carousel.data.content + CarouselContent( + title = contentData.title, + elements = contentData.elements + ) + } catch (e: SerializationException) { + println("Error decoding CarouselContent: ${e.localizedMessage}") + null + } + } + } +} + +// Panel +@Serializable +data class PanelElement( + val title: String +) + +@Serializable +data class PanelContentData( + val title: String, + val subtitle: String? = null, + val imageType: String? = null, + val imageData: String? = null, + val imageDescription: String? = null, + val elements: List +) + +@Serializable +data class PanelReplyMessage( + val title: String, + val subtitle: String? = null +) + +@Serializable +data class PanelData( + val replyMessage: PanelReplyMessage? = null, + val content: PanelContentData +) + +@Serializable +data class PanelTemplate( + val templateType: String, + val version: String, + val data: PanelData +) + +data class PanelContent( + val title: String, + val subtitle: String? = null, + val imageUrl: String? = null, + val imageDescription: String? = null, + val options: List +): InteractiveContent { + companion object { + const val TEMPLATE_TYPE = Constants.PANEL + fun decode(text: String): InteractiveContent? { + return try { + val panel = Json.decodeFromString(text) + val contentData = panel.data.content + PanelContent( + title = contentData.title, + subtitle = contentData.subtitle, + imageUrl = contentData.imageData, + imageDescription = contentData.imageDescription, + options = contentData.elements + ) + } catch (e: SerializationException) { + println("Error decoding PanelContent: ${e.localizedMessage}") + null + } + } + } +} \ No newline at end of file diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/MessageReceiptsManager.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/MessageReceiptsManager.kt index 60f2053..60312dd 100644 --- a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/MessageReceiptsManager.kt +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/MessageReceiptsManager.kt @@ -85,7 +85,6 @@ class MessageReceiptsManagerImpl : MessageReceiptsManager { pendingMessageReceipts.checkAndRemoveDuplicateReceipt() continuation.resume(Result.success(pendingMessageReceipts)) } catch (e: Exception) { - SDKLogger.logger.logError { "Error during throttling: ${e.message}" } continuation.resumeWithException(e) } } @@ -108,9 +107,7 @@ class MessageReceiptsManagerImpl : MessageReceiptsManager { CoroutineScope(Dispatchers.Default).launch { numPendingDeliveredReceipts++ delay((deliveredThrottleTime * 1000).toLong()) - if (readReceiptSet.contains(messageId)) { - SDKLogger.logger.logDebug { "Read receipt already sent for messageId: $messageId" } - } else { + if (!readReceiptSet.contains(messageId)) { pendingMessageReceipts.deliveredReceiptMessageId = messageId } numPendingDeliveredReceipts--