From de92f204725bda5d1b593dd18d1654a5931095b1 Mon Sep 17 00:00:00 2001 From: Rajat Mittal Date: Mon, 12 Aug 2024 15:19:35 -0700 Subject: [PATCH] Added Transcript item base class Updated UI to use Transcript item --- .../chat/androidchatexample/MainActivity.kt | 153 +++++++++--------- .../repository/ChatRepository.kt | 70 ++++---- .../viewmodel/ChatViewModel.kt | 75 ++++----- .../views/ChatComponents.kt | 56 ++++--- .../amazon/connect/chat/sdk/model/Event.kt | 24 +++ .../amazon/connect/chat/sdk/model/Message.kt | 93 +++++++---- .../connect/chat/sdk/model/MessageMetadata.kt | 26 +++ .../connect/chat/sdk/model/TranscriptItem.kt | 36 +++++ .../chat/sdk/model/TranscriptResponse.kt | 17 -- .../chat/sdk/network/WebSocketManager.kt | 152 ++++++++--------- 10 files changed, 409 insertions(+), 293 deletions(-) create mode 100644 chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/Event.kt create mode 100644 chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/MessageMetadata.kt create mode 100644 chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/TranscriptItem.kt diff --git a/app/src/main/java/com/amazon/connect/chat/androidchatexample/MainActivity.kt b/app/src/main/java/com/amazon/connect/chat/androidchatexample/MainActivity.kt index df6086e..4877494 100644 --- a/app/src/main/java/com/amazon/connect/chat/androidchatexample/MainActivity.kt +++ b/app/src/main/java/com/amazon/connect/chat/androidchatexample/MainActivity.kt @@ -48,13 +48,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.amazon.connect.chat.sdk.model.Message -import com.amazon.connect.chat.sdk.model.MessageType +import com.amazon.connect.chat.sdk.model.MessageDirection import com.amazon.connect.chat.sdk.utils.CommonUtils.Companion.keyboardAsState import com.amazon.connect.chat.sdk.utils.ContentType import com.amazon.connect.chat.androidchatexample.viewmodel.ChatViewModel import com.amazon.connect.chat.androidchatexample.views.ChatMessageView import com.amazon.connect.chat.androidchatexample.ui.theme.androidconnectchatandroidTheme -import com.amazon.connect.chat.sdk.GreetingFromSDK +import com.amazon.connect.chat.sdk.model.TranscriptItem import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -274,9 +274,10 @@ fun ChatView(viewModel: ChatViewModel) { } // Logic to determine if the message is visible. // For simplicity, let's say it's visible if it's one of the last three messages. - if (index >= messages.size - 3 && message.messageType == MessageType.RECEIVER) { - viewModel.sendReadEventOnAppear(message) - } + // TODO: Update here to send read receipts from SDK +// if (index >= messages.size - 3 && message.messageDirection == MessageDirection.INCOMING) { +// viewModel.sendReadEventOnAppear(message) +// } } } } @@ -308,9 +309,9 @@ fun ChatView(viewModel: ChatViewModel) { } @Composable -fun ChatMessage(message: Message) { +fun ChatMessage(transcriptItem: TranscriptItem) { // Customize this composable to display each message - ChatMessageView(message = message) + ChatMessageView(transcriptItem = transcriptItem) } @Composable @@ -353,73 +354,73 @@ fun ChatViewPreview(messages: List) { @Preview(showBackground = true) @Composable fun ChatViewPreview() { - val sampleMessages = listOf( - Message( - participant = "CUSTOMER", - text = "Hello asdfioahsdfoas idfuoasdfihjasdlfihjsoadfjopasoaisdfhjoasidjf ", - contentType = "text/plain", - messageType = MessageType.SENDER, - timeStamp = "06=51", - status = "Delivered" - ), - Message( - participant = "SYSTEM", - text = "{\"templateType\":\"ListPicker\",\"version\":\"1.0\",\"data\":{\"content\":{\"title\":\"Which department do you want to select?\",\"subtitle\":\"Tap to select option\",\"imageType\":\"URL\",\"imageData\":\"https://amazon-connect-interactive-message-blog-assets.s3-us-west-2.amazonaws.com/interactive-images/company.jpg\",\"elements\":[{\"title\":\"Billing\",\"subtitle\":\"Request billing information\",\"imageType\":\"URL\",\"imageData\":\"https://amazon-connect-interactive-message-blog-assets.s3-us-west-2.amazonaws.com/interactive-images/billing.jpg\"},{\"title\":\"New Service\",\"subtitle\":\"Set up a new service\",\"imageType\":\"URL\",\"imageData\":\"https://amazon-connect-interactive-message-blog-assets.s3-us-west-2.amazonaws.com/interactive-images/new_service.jpg\"},{\"title\":\"Cancellation\",\"subtitle\":\"Request a cancellation\",\"imageType\":\"URL\",\"imageData\":\"https://amazon-connect-interactive-message-blog-assets.s3-us-west-2.amazonaws.com/interactive-images/cancel.jpg\"}]}}}", - contentType = "application/vnd.amazonaws.connect.message.interactive", - messageType = MessageType.RECEIVER, - timeStamp = "14:18", - messageID = "f905d16e-12a0-4854-9079-d5b34476c3ba", - status = null, - isRead = false - ), - Message( - participant = "AGENT", - text = "...", - contentType = "text/plain", - messageType = MessageType.RECEIVER, - timeStamp = "06:51", - isRead = true - ), - Message( - participant = "AGENT", - text = "Hello, **this** is a agent \n\n speaking.Hello, this is a agent speaking.", - contentType = "text/plain", - messageType = MessageType.RECEIVER, - timeStamp = "06:51", - isRead = true - ), - - Message( - participant = "SYSTEM", - text = "{\"templateType\":\"QuickReply\",\"version\":\"1.0\",\"data\":{\"content\":{\"title\":\"How was your experience?\",\"elements\":[{\"title\":\"Very unsatisfied\"},{\"title\":\"Unsatisfied\"},{\"title\":\"Neutral\"},{\"title\":\"Satisfied\"},{\"title\":\"Very Satisfied\"}]}}}", - contentType = "application/vnd.amazonaws.connect.message.interactive", - messageType = MessageType.RECEIVER, - timeStamp = "06:20", - messageID = "8f76a266-6654-434f-94ea-87ec111ee341", - status = null, - isRead = false - ), - - Message( - participant = "SYSTEM", - text = "{\"templateType\":\"ListPicker\",\"version\":\"1.0\",\"data\":{\"content\":{\"title\":\"Which department would you like?\",\"subtitle\":\"Tap to select option\",\"elements\":[{\"title\":\"Billing\",\"subtitle\":\"For billing issues\"},{\"title\":\"New Service\",\"subtitle\":\"For new service\"},{\"title\":\"Cancellation\",\"subtitle\":\"For new service requests\"}]}}}", - contentType = "application/vnd.amazonaws.connect.message.interactive", - messageType = MessageType.RECEIVER, - timeStamp = "14:18", - messageID = "f905d16e-12a0-4854-9079-d5b34476c3ba", - status = null, - isRead = false - ), - - Message( - participant = "SYSTEM", - text = "Someone joined the chat.Someone joined the chat.Someone joined the chat.", - contentType = "text/plain", - messageType = MessageType.COMMON, - timeStamp = "06:51", - isRead = true - ) - ) - - ChatViewPreview(messages = sampleMessages) +// val sampleMessages = listOf( +// Message( +// participant = "CUSTOMER", +// text = "Hello asdfioahsdfoas idfuoasdfihjasdlfihjsoadfjopasoaisdfhjoasidjf ", +// contentType = "text/plain", +// messageDirection = MessageDirection.OUTGOING, +// timeStamp = "06=51", +// status = "Delivered" +// ), +// Message( +// participant = "SYSTEM", +// text = "{\"templateType\":\"ListPicker\",\"version\":\"1.0\",\"data\":{\"content\":{\"title\":\"Which department do you want to select?\",\"subtitle\":\"Tap to select option\",\"imageType\":\"URL\",\"imageData\":\"https://amazon-connect-interactive-message-blog-assets.s3-us-west-2.amazonaws.com/interactive-images/company.jpg\",\"elements\":[{\"title\":\"Billing\",\"subtitle\":\"Request billing information\",\"imageType\":\"URL\",\"imageData\":\"https://amazon-connect-interactive-message-blog-assets.s3-us-west-2.amazonaws.com/interactive-images/billing.jpg\"},{\"title\":\"New Service\",\"subtitle\":\"Set up a new service\",\"imageType\":\"URL\",\"imageData\":\"https://amazon-connect-interactive-message-blog-assets.s3-us-west-2.amazonaws.com/interactive-images/new_service.jpg\"},{\"title\":\"Cancellation\",\"subtitle\":\"Request a cancellation\",\"imageType\":\"URL\",\"imageData\":\"https://amazon-connect-interactive-message-blog-assets.s3-us-west-2.amazonaws.com/interactive-images/cancel.jpg\"}]}}}", +// contentType = "application/vnd.amazonaws.connect.message.interactive", +// messageDirection = MessageDirection.INCOMING, +// timeStamp = "14:18", +// messageID = "f905d16e-12a0-4854-9079-d5b34476c3ba", +// status = null, +// isRead = false +// ), +// Message( +// participant = "AGENT", +// text = "...", +// contentType = "text/plain", +// messageDirection = MessageDirection.INCOMING, +// timeStamp = "06:51", +// isRead = true +// ), +// Message( +// participant = "AGENT", +// text = "Hello, **this** is a agent \n\n speaking.Hello, this is a agent speaking.", +// contentType = "text/plain", +// messageDirection = MessageDirection.INCOMING, +// timeStamp = "06:51", +// isRead = true +// ), +// +// Message( +// participant = "SYSTEM", +// text = "{\"templateType\":\"QuickReply\",\"version\":\"1.0\",\"data\":{\"content\":{\"title\":\"How was your experience?\",\"elements\":[{\"title\":\"Very unsatisfied\"},{\"title\":\"Unsatisfied\"},{\"title\":\"Neutral\"},{\"title\":\"Satisfied\"},{\"title\":\"Very Satisfied\"}]}}}", +// contentType = "application/vnd.amazonaws.connect.message.interactive", +// messageDirection = MessageDirection.INCOMING, +// timeStamp = "06:20", +// messageID = "8f76a266-6654-434f-94ea-87ec111ee341", +// status = null, +// isRead = false +// ), +// +// Message( +// participant = "SYSTEM", +// text = "{\"templateType\":\"ListPicker\",\"version\":\"1.0\",\"data\":{\"content\":{\"title\":\"Which department would you like?\",\"subtitle\":\"Tap to select option\",\"elements\":[{\"title\":\"Billing\",\"subtitle\":\"For billing issues\"},{\"title\":\"New Service\",\"subtitle\":\"For new service\"},{\"title\":\"Cancellation\",\"subtitle\":\"For new service requests\"}]}}}", +// contentType = "application/vnd.amazonaws.connect.message.interactive", +// messageDirection = MessageDirection.INCOMING, +// timeStamp = "14:18", +// messageID = "f905d16e-12a0-4854-9079-d5b34476c3ba", +// status = null, +// isRead = false +// ), +// +// Message( +// participant = "SYSTEM", +// text = "Someone joined the chat.Someone joined the chat.Someone joined the chat.", +// contentType = "text/plain", +// messageDirection = MessageDirection.COMMON, +// timeStamp = "06:51", +// isRead = true +// ) +// ) +// +// ChatViewPreview(messages = sampleMessages) } diff --git a/app/src/main/java/com/amazon/connect/chat/androidchatexample/repository/ChatRepository.kt b/app/src/main/java/com/amazon/connect/chat/androidchatexample/repository/ChatRepository.kt index d9eff8d..62ed5fb 100644 --- a/app/src/main/java/com/amazon/connect/chat/androidchatexample/repository/ChatRepository.kt +++ b/app/src/main/java/com/amazon/connect/chat/androidchatexample/repository/ChatRepository.kt @@ -1,5 +1,13 @@ package com.amazon.connect.chat.androidchatexample.repository +import com.amazon.connect.chat.androidchatexample.Config +import com.amazon.connect.chat.androidchatexample.models.StartChatRequest +import com.amazon.connect.chat.androidchatexample.models.StartChatResponse +import com.amazon.connect.chat.androidchatexample.network.ApiInterface +import com.amazon.connect.chat.androidchatexample.network.Resource +import com.amazon.connect.chat.sdk.model.TranscriptItem +import com.amazon.connect.chat.sdk.model.TranscriptResponse +import com.amazon.connect.chat.sdk.utils.ContentType import com.amazonaws.AmazonClientException import com.amazonaws.AmazonServiceException import com.amazonaws.handlers.AsyncHandler @@ -13,22 +21,11 @@ import com.amazonaws.services.connectparticipant.model.SendEventRequest import com.amazonaws.services.connectparticipant.model.SendEventResult import com.amazonaws.services.connectparticipant.model.SendMessageRequest import com.amazonaws.services.connectparticipant.model.SendMessageResult -import com.amazon.connect.chat.androidchatexample.Config -import com.amazon.connect.chat.sdk.model.MessageMetadata -import com.amazon.connect.chat.sdk.model.Receipt -import com.amazon.connect.chat.androidchatexample.models.StartChatRequest -import com.amazon.connect.chat.androidchatexample.models.StartChatResponse -import com.amazon.connect.chat.sdk.model.TranscriptItem -import com.amazon.connect.chat.sdk.model.TranscriptResponse -import com.amazon.connect.chat.androidchatexample.network.ApiInterface -import com.amazon.connect.chat.androidchatexample.network.Resource -import com.amazon.connect.chat.sdk.utils.ContentType import dagger.hilt.android.scopes.ActivityScoped import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject import retrofit2.HttpException -import java.lang.Exception import javax.inject.Inject @ActivityScoped @@ -176,36 +173,37 @@ class ChatRepository @Inject constructor( } val result = connectParticipantClient.getTranscript(request) - val transcriptItems: List = result.transcript.map { apiItem -> - TranscriptItem( - absoluteTime = apiItem.absoluteTime, - content = apiItem.content, - contentType = apiItem.contentType, - displayName = apiItem.displayName, - id = apiItem.id, - participantId = apiItem.participantId, - participantRole = apiItem.participantRole, - type = apiItem.type, - messageMetadata = apiItem.messageMetadata?.let { metadata -> - MessageMetadata( - messageId = metadata.messageId, - receipts = metadata.receipts?.map { receipt -> - Receipt( - deliveredTimestamp = receipt.deliveredTimestamp, - readTimestamp = receipt.readTimestamp, - recipientParticipantId = receipt.recipientParticipantId - ) - } - ) - } - ) - } + // TODO : Need to be parsed correctly +// val transcriptItems: List = result.transcript.map { apiItem -> +// TranscriptItem( +// absoluteTime = apiItem.absoluteTime, +// content = apiItem.content, +// contentType = apiItem.contentType, +// displayName = apiItem.displayName, +// id = apiItem.id, +// participantId = apiItem.participantId, +// participantRole = apiItem.participantRole, +// type = apiItem.type, +// messageMetadata = apiItem.messageMetadata?.let { metadata -> +// MessageMetadata( +// messageId = metadata.messageId, +// receipts = metadata.receipts?.map { receipt -> +// Receipt( +// deliveredTimestamp = receipt.deliveredTimestamp, +// readTimestamp = receipt.readTimestamp, +// recipientParticipantId = receipt.recipientParticipantId +// ) +// } +// ) +// } +// ) +// } // Create the full response object val fullResponse = TranscriptResponse( initialContactId = result.initialContactId, nextToken = result.nextToken, - transcript = transcriptItems + transcript = emptyList() // TODO : Need to be updated with actual transcript items ) Resource.Success(fullResponse) 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 fcaa8ef..8a58150 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 @@ -25,6 +25,7 @@ import com.amazon.connect.chat.sdk.utils.ContentType import com.amazon.connect.chat.sdk.ChatSession import com.amazon.connect.chat.sdk.model.ChatDetails import com.amazon.connect.chat.sdk.model.GlobalConfig +import com.amazon.connect.chat.sdk.model.TranscriptItem import kotlinx.coroutines.launch @HiltViewModel @@ -41,8 +42,8 @@ class ChatViewModel @Inject constructor( private val startChatResponse: LiveData> = _startChatResponse private val _createParticipantConnectionResult = MutableLiveData() val createParticipantConnectionResult: MutableLiveData = _createParticipantConnectionResult - private val _messages = MutableLiveData>() - val messages: LiveData> = _messages + private val _messages = MutableLiveData>() + val messages: LiveData> = _messages private val _webSocketUrl = MutableLiveData() val webSocketUrl: MutableLiveData = _webSocketUrl private val _errorMessage = MutableLiveData() @@ -235,41 +236,42 @@ class ChatViewModel @Inject constructor( } } - private fun onMessageReceived(message: Message) { - // Log the current state before the update + private fun onMessageReceived(transcriptItem: TranscriptItem) { viewModelScope.launch { + // Log the current state before the update + Log.i("ChatViewModel", "Received transcript item: $transcriptItem") - Log.i("ChatViewModel", "Received message: $message") - - // Construct the new list with modifications based on the received message + // Construct the new list with modifications based on the received transcript item val updatedMessages = _messages.value.orEmpty().toMutableList().apply { - // Filter out typing indicators and apply message status updates or add new messages - removeIf { it.text == "..." } - if (message.contentType == ContentType.META_DATA.type) { - val index = indexOfFirst { it.messageID == message.messageID } - if (index != -1) { - this[index] = get(index).copy(status = message.status) + // Remove any typing indicators + removeIf { it is Message && it.text == "..." } + + when (transcriptItem) { + is Message -> { + if (!(transcriptItem.text == "..." && transcriptItem.participant == chatConfiguration.customerName)) { + add(transcriptItem) + } + + // Additional logic like sending 'Delivered' events + // TODO : Update here to send read receipts from SDK + if (transcriptItem.participant == chatConfiguration.agentName && transcriptItem.contentType.contains("text")) { + val content = "{\"messageId\":\"${transcriptItem.id}\"}" + sendEvent(content, ContentType.MESSAGE_DELIVERED) + } } - } else { - // Exclude customer's typing events - if (!(message.text == "..." && message.participant == chatConfiguration.customerName)) { - add(message) + else -> { + Log.i("ChatViewModel", "Unhandled transcript item type: ${transcriptItem::class.simpleName}") } } } // Update messages LiveData in a thread-safe manner - _messages.value =updatedMessages - - // Additional logic like sending 'Delivered' events - if (message.participant == chatConfiguration.agentName && message.contentType.contains("text")) { - val content = "{\"messageId\":\"${message.messageID}\"}" - sendEvent(content, ContentType.MESSAGE_DELIVERED) - } + _messages.value = updatedMessages } } + private fun onWebSocketError(errorMessage: String) { // Handle WebSocket errors _isLoading.postValue(false) @@ -306,18 +308,19 @@ class ChatViewModel @Inject constructor( } fun sendReadEventOnAppear(message: Message) { - val messagesList = (_messages.value ?: return).toMutableList() - val index = messagesList.indexOfFirst { - it.text == message.text && it.text.isNotEmpty() && it.contentType.contains("text") - && it.participant != chatConfiguration.customerName && !it.isRead - } - if (index != -1) { - val messageId = messagesList[index].messageID ?: return - val content = "{\"messageId\":\"$messageId\"}" - sendEvent(content, ContentType.MESSAGE_READ) - messagesList[index] = messagesList[index].copy(isRead = true) - _messages.postValue(messagesList) // Safely post the updated list to the LiveData - } + // TODO : Update here to send read receipts from SDK +// val messagesList = (_messages.value ?: return).toMutableList() +// val index = messagesList.indexOfFirst { +// it.text == message.text && it.text.isNotEmpty() && it.contentType.contains("text") +// && it.participant != chatConfiguration.customerName && !it.isRead +// } +// if (index != -1) { +// val messageId = messagesList[index].messageID ?: return +// val content = "{\"messageId\":\"$messageId\"}" +// sendEvent(content, ContentType.MESSAGE_READ) +// messagesList[index] = messagesList[index].copy(isRead = true) +// _messages.postValue(messagesList) // Safely post the updated list to the LiveData +// } } fun endChat(){ 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 0e6c478..60a92ea 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 @@ -17,33 +17,46 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.amazon.connect.chat.sdk.model.Event import com.amazon.connect.chat.sdk.model.ListPickerContent import com.amazon.connect.chat.sdk.model.Message -import com.amazon.connect.chat.sdk.model.MessageType +import com.amazon.connect.chat.sdk.model.MessageDirection import com.amazon.connect.chat.sdk.model.PlainTextContent import com.amazon.connect.chat.sdk.model.QuickReplyContent +import com.amazon.connect.chat.sdk.model.TranscriptItem import com.amazon.connect.chat.sdk.utils.CommonUtils.Companion.MarkdownText @Composable -fun ChatMessageView(message: Message) { - when (message.messageType) { - MessageType.SENDER -> SenderChatBubble(message) - MessageType.RECEIVER -> { - if (message.text == "...") { - TypingIndicator() - } else { - when (val content = message.content) { - is PlainTextContent -> ReceiverChatBubble(message) - is QuickReplyContent -> QuickReplyContentView(message,content) - is ListPickerContent -> ListPickerContentView(message, content) - else -> Text(text = "Unsupported message type") +fun ChatMessageView(transcriptItem: TranscriptItem) { + when (transcriptItem) { + is Message -> { + when (transcriptItem.messageDirection) { + MessageDirection.OUTGOING -> SenderChatBubble(transcriptItem) + MessageDirection.INCOMING -> { + if (transcriptItem.text == "...") { + TypingIndicator() + } else { + when (val content = transcriptItem.content) { + is PlainTextContent -> ReceiverChatBubble(transcriptItem) + is QuickReplyContent -> QuickReplyContentView(transcriptItem, content) + is ListPickerContent -> ListPickerContentView(transcriptItem, content) + else -> Text(text = "Unsupported message type") + } + } } + MessageDirection.COMMON -> CommonChatBubble(transcriptItem) + null -> CommonChatBubble(transcriptItem) } } - MessageType.COMMON -> CommonChatBubble(message) + // Add handling for other TranscriptItem subclasses if necessary + is Event -> { + + } + else -> Text(text = "Unsupported transcript item type") } } + @Composable fun SenderChatBubble(message: Message) { Column( @@ -78,13 +91,14 @@ fun SenderChatBubble(message: Message) { } } } - message.status?.let { - Text( - text = it, - style = MaterialTheme.typography.bodySmall, - color = Color.Gray - ) - } + // TODO : update receipts +// message.status?.let { +// Text( +// text = it, +// style = MaterialTheme.typography.bodySmall, +// color = Color.Gray +// ) +// } } } diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/Event.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/Event.kt new file mode 100644 index 0000000..52d8b74 --- /dev/null +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/Event.kt @@ -0,0 +1,24 @@ +package com.amazon.connect.chat.sdk.model + +interface EventProtocol : TranscriptItemProtocol { + var participant: String? + var text: String? + var displayName: String? + var eventDirection: MessageDirection? +} + +class Event( + override var participant: String? = null, + override var text: String? = null, + override var displayName: String? = null, + override var eventDirection: MessageDirection? = MessageDirection.COMMON, + timeStamp: String, + contentType: String, + id: String, + serializedContent: Map? = null +) : TranscriptItem( + id = id, + timeStamp = timeStamp, + contentType = contentType, + serializedContent = serializedContent +), EventProtocol {} 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 b6e540a..c90abe5 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 @@ -1,58 +1,83 @@ package com.amazon.connect.chat.sdk.model import com.amazon.connect.chat.sdk.utils.ContentType -import com.amazon.connect.chat.sdk.model.GenericInteractiveTemplate -import com.amazon.connect.chat.sdk.model.InteractiveContent -import com.amazon.connect.chat.sdk.model.ListPickerContent -import com.amazon.connect.chat.sdk.model.MessageContent -import com.amazon.connect.chat.sdk.model.PlainTextContent -import com.amazon.connect.chat.sdk.model.QuickReplyContent import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import java.util.UUID -enum class MessageType{ - SENDER, - RECEIVER, +enum class MessageDirection { + OUTGOING, + INCOMING, COMMON } -data class Message ( - var participant: String?, - var text: String, - val id: UUID = UUID.randomUUID(), - var contentType: String, - var messageType: MessageType, - var timeStamp: String? = null, - var messageID: String? = null, - var status: String? = null, - var isRead: Boolean = false -){ +interface MessageProtocol : TranscriptItemProtocol { + var participant: String + var text: String + var displayName: String? + var messageDirection: MessageDirection? + var metadata: MessageMetadataProtocol? +} + +class Message( + override var participant: String, + override var text: String, + contentType: String, + override var displayName: String? = null, + override var messageDirection: MessageDirection? = null, + timeStamp: String, + private var attachmentId: String? = null, + id: String? = null, + override var metadata: MessageMetadataProtocol? = null, + serializedContent: Map? = null +) : TranscriptItem( + id = id ?: UUID.randomUUID().toString(), + timeStamp = timeStamp, + contentType = contentType, + serializedContent = serializedContent +), MessageProtocol { + val content: MessageContent? get() = when (contentType) { ContentType.PLAIN_TEXT.type -> PlainTextContent.decode(text) - ContentType.RICH_TEXT.type -> PlainTextContent.decode(text) // You can replace this with a rich text class later + ContentType.RICH_TEXT.type -> PlainTextContent.decode(text) // Replace with a rich text class later ContentType.INTERACTIVE_TEXT.type -> decodeInteractiveContent(text) - else -> null // Handle unsupported content types + else -> { + if (attachmentId != null){ + // Placeholder for a future rich text content class + PlainTextContent.decode(text) + } else { + logUnsupportedContentType(contentType) + null + } + } } // Helper method to decode interactive content private fun decodeInteractiveContent(text: String): InteractiveContent? { - val jsonData = text.toByteArray(Charsets.UTF_8) - val genericTemplate = try { - Json { ignoreUnknownKeys = true }.decodeFromString(String(jsonData)) + return try { + 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. + else -> { + logUnsupportedContentType(genericTemplate.templateType) + null + } + } } catch (e: SerializationException) { + logSerializationException(e) null } - return when (genericTemplate?.templateType) { - QuickReplyContent.templateType -> QuickReplyContent.decode(text) - ListPickerContent.templateType -> ListPickerContent.decode(text) - // Add cases for each interactive message type, decoding as appropriate. - else -> { - println("Unsupported interactive content type: ${genericTemplate?.templateType}") - null - } - } } + private fun logUnsupportedContentType(templateType: String?) { + // Log the unsupported content type + } + + private fun logSerializationException(e: SerializationException) { + // Log the serialization exception + } } \ No newline at end of file diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/MessageMetadata.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/MessageMetadata.kt new file mode 100644 index 0000000..0b134b9 --- /dev/null +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/MessageMetadata.kt @@ -0,0 +1,26 @@ +package com.amazon.connect.chat.sdk.model + +import java.util.UUID + +enum class MessageStatus { + Delivered, Read, Sending, Failed, Sent, Unknown +} + +interface MessageMetadataProtocol : TranscriptItemProtocol { + var status: MessageStatus? + var eventDirection: MessageDirection? +} + +class MessageMetadata( + override var status: MessageStatus? = null, + override var eventDirection: MessageDirection? = MessageDirection.COMMON, + timeStamp: String, + contentType: String, + id: String = UUID.randomUUID().toString(), + serializedContent: Map? = null +) : TranscriptItem( + id = id, + timeStamp = timeStamp, + contentType = contentType, + serializedContent = serializedContent +), MessageMetadataProtocol {} diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/TranscriptItem.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/TranscriptItem.kt new file mode 100644 index 0000000..97a3711 --- /dev/null +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/TranscriptItem.kt @@ -0,0 +1,36 @@ +package com.amazon.connect.chat.sdk.model + +import java.util.UUID + +interface TranscriptItemProtocol { + val id: String + val timeStamp: String + var contentType: String + var serializedContent: Map? +} + +open class TranscriptItem( + id: String = UUID.randomUUID().toString(), + timeStamp: String, + override var contentType: String, + override var serializedContent: Map? = null +) : TranscriptItemProtocol { + + private var _id: String = id + private var _timeStamp: String = timeStamp + + override val id: String + get() = _id + + override val timeStamp: String + get() = _timeStamp + + // Internal methods to update id and timeStamp if needed + protected fun updateId(newId: String) { + _id = newId + } + + protected fun updateTimeStamp(newTimeStamp: String) { + _timeStamp = newTimeStamp + } +} diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/TranscriptResponse.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/TranscriptResponse.kt index e416345..0f56743 100644 --- a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/TranscriptResponse.kt +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/model/TranscriptResponse.kt @@ -6,23 +6,6 @@ data class TranscriptResponse( val transcript: List ) -data class TranscriptItem( - val absoluteTime: String, - val content: String?, - val contentType: String, - val displayName: String?, - val id: String, - val participantId: String?, - val participantRole: String?, - val type: String, - val messageMetadata: MessageMetadata? -) - -data class MessageMetadata( - val messageId: String, - val receipts: List? -) - data class Receipt( val deliveredTimestamp: String?, val readTimestamp: String?, diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/WebSocketManager.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/WebSocketManager.kt index 1e4b1e3..b39c3b9 100644 --- a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/WebSocketManager.kt +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/network/WebSocketManager.kt @@ -7,8 +7,11 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.ProcessLifecycleOwner import com.amazon.connect.chat.sdk.Config +import com.amazon.connect.chat.sdk.model.Event import com.amazon.connect.chat.sdk.model.Message -import com.amazon.connect.chat.sdk.model.MessageType +import com.amazon.connect.chat.sdk.model.MessageDirection +import com.amazon.connect.chat.sdk.model.MessageMetadata +import com.amazon.connect.chat.sdk.model.MessageStatus import com.amazon.connect.chat.sdk.model.TranscriptItem import com.amazon.connect.chat.sdk.utils.CommonUtils import com.amazon.connect.chat.sdk.utils.ContentType @@ -40,7 +43,7 @@ class WebSocketManager @Inject constructor( .pingInterval(60, TimeUnit.SECONDS) .build() private var webSocket: WebSocket? = null - private lateinit var messageCallBack : (Message) -> Unit + private lateinit var messageCallBack : (TranscriptItem) -> Unit private val chatConfiguration = Config private var heartbeatManager: HeartbeatManager = HeartbeatManager( sendHeartbeatCallback = ::sendHeartbeat, @@ -98,7 +101,7 @@ class WebSocketManager @Inject constructor( } } - fun createWebSocket(url: String, onMessageReceived: (Message) -> Unit, onConnectionFailed: (String) -> Unit) { + fun createWebSocket(url: String, onMessageReceived: (TranscriptItem) -> Unit, onConnectionFailed: (String) -> Unit) { val request = Request.Builder().url(url).build() this.messageCallBack = onMessageReceived closeWebSocket(); @@ -170,8 +173,8 @@ class WebSocketManager @Inject constructor( val contentType = it.getString("ContentType") when { type == "MESSAGE" -> handleMessage(it) - contentType == ContentType.JOINED.type -> handleParticipantJoined(it) - contentType == ContentType.LEFT.type -> handleParticipantLeft(it) + contentType == ContentType.JOINED.type -> handleParticipantEvent(it) + contentType == ContentType.LEFT.type -> handleParticipantEvent(it) contentType == ContentType.TYPING.type -> handleTyping(it) contentType == ContentType.ENDED.type -> handleChatEnded(it) contentType == ContentType.META_DATA.type -> handleMetadata(it) @@ -220,93 +223,95 @@ class WebSocketManager @Inject constructor( private fun handleMessage(innerJson: JSONObject) { val participantRole = innerJson.getString("ParticipantRole") val messageId = innerJson.getString("Id") - var messageText = innerJson.getString("Content") - val messageType = if (participantRole.equals(chatConfiguration.customerName, ignoreCase = true)) MessageType.SENDER else MessageType.RECEIVER - val time = CommonUtils.formatTime(innerJson.getString("AbsoluteTime")) + val messageText = innerJson.getString("Content") + val displayName = innerJson.getString("DisplayName") + val time = innerJson.getString("AbsoluteTime") + + // TODO: Pass raw data val message = Message( participant = participantRole, text = messageText, contentType = innerJson.getString("ContentType"), - messageType = messageType, timeStamp = time, - messageID = messageId + id = messageId, + displayName = displayName ) this.messageCallBack(message) } - private fun handleParticipantJoined(innerJson: JSONObject) { + private fun handleParticipantEvent(innerJson: JSONObject) { val participantRole = innerJson.getString("ParticipantRole") - val messageText = "$participantRole has joined" - val message = Message( - participant = participantRole, - text = messageText, - contentType = innerJson.getString("ContentType"), - messageType = MessageType.COMMON - ) - this.messageCallBack(message) - } + val displayName = innerJson.getString("DisplayName") + val time = innerJson.getString("AbsoluteTime") + val eventId = innerJson.getString("Id") - private fun handleParticipantLeft(innerJson: JSONObject) { - val participantRole = innerJson.getString("ParticipantRole") - val messageText = "$participantRole has left" - val message = Message( + val message = Event( + id = eventId, + timeStamp = time, + displayName = displayName, participant = participantRole, - text = messageText, + text = "Participant Joined", contentType = innerJson.getString("ContentType"), - messageType = MessageType.COMMON + eventDirection = MessageDirection.COMMON, ) this.messageCallBack(message) } private fun handleTyping(innerJson: JSONObject) { val participantRole = innerJson.getString("ParticipantRole") - val time = CommonUtils.formatTime(innerJson.getString("AbsoluteTime")) - val messageType = if (participantRole.equals(chatConfiguration.customerName, ignoreCase = true)) MessageType.SENDER else MessageType.RECEIVER - val message = Message( - participant = participantRole, - text = "...", - contentType = innerJson.getString("ContentType"), - messageType = messageType, - timeStamp = time + val time = innerJson.getString("AbsoluteTime") + val displayName = innerJson.getString("DisplayName") + val eventId = innerJson.getString("Id") + + val event = Event( + timeStamp = time, + contentType = innerJson.getString("ContentType"), + id = eventId, + displayName = displayName, + participant = participantRole ) - this.messageCallBack(message) } + + this.messageCallBack(event) + } private fun handleChatEnded(innerJson: JSONObject) { closeWebSocket(); isChatActive = false; - val message = Message( - participant = "System Message", - text = "The chat has ended.", - contentType = innerJson.getString("ContentType"), - messageType = MessageType.COMMON + val time = innerJson.getString("AbsoluteTime") + val eventId = innerJson.getString("Id") + val event = Event( + timeStamp = time, + contentType = innerJson.getString("ContentType"), + id = eventId, + eventDirection = MessageDirection.COMMON ) - this.messageCallBack(message) + + this.messageCallBack(event) } private fun handleMetadata(innerJson: JSONObject) { val messageMetadata = innerJson.getJSONObject("MessageMetadata") val messageId = messageMetadata.getString("MessageId") val receipts = messageMetadata.optJSONArray("Receipts") - var status = "Delivered" - val time = CommonUtils.formatTime(innerJson.getString("AbsoluteTime")) + var status = MessageStatus.Delivered + val time = innerJson.getString("AbsoluteTime") + receipts?.let { for (i in 0 until it.length()) { val receipt = it.getJSONObject(i) if (receipt.optString("ReadTimestamp").isNotEmpty()) { - status = "Read" + status = MessageStatus.Read } } } - val message = Message( - participant = "", - text = "", + val metadata = MessageMetadata( contentType = innerJson.getString("ContentType"), - messageType = MessageType.SENDER, + eventDirection = MessageDirection.OUTGOING, timeStamp = time, - messageID = messageId, + id = messageId, status = status ) - this.messageCallBack(message) + this.messageCallBack(metadata) } @@ -324,29 +329,30 @@ class WebSocketManager @Inject constructor( } fun formatAndProcessTranscriptItems(transcriptItems: List) { - transcriptItems.forEach { item -> - val participantRole = item.participantRole - - // Create the message content in JSON format - val messageContentJson = JSONObject().apply { - put("Id", item.id ?: "") - put("ParticipantRole", participantRole) - put("AbsoluteTime", item.absoluteTime ?: "") - put("ContentType", item.contentType ?: "") - put("Content", item.content ?: "") - put("Type", item.type) - put("DisplayName", item.displayName ?: "") - } - - // Convert JSON object to String format - val messageContentString = messageContentJson.toString() - - // Prepare the message in the format expected by WebSocket - val wrappedMessageString = "{\"content\":\"${messageContentString.replace("\"", "\\\"")}\"}" - - // Send the formatted message string via WebSocket - websocketDidReceiveMessage(wrappedMessageString) - } + // TODO: Need to be updated with latest transcript items format +// transcriptItems.forEach { item -> +// val participantRole = item.participantRole +// +// // Create the message content in JSON format +// val messageContentJson = JSONObject().apply { +// put("Id", item.id ?: "") +// put("ParticipantRole", participantRole) +// put("AbsoluteTime", item.absoluteTime ?: "") +// put("ContentType", item.contentType ?: "") +// put("Content", item.content ?: "") +// put("Type", item.type) +// put("DisplayName", item.displayName ?: "") +// } +// +// // Convert JSON object to String format +// val messageContentString = messageContentJson.toString() +// +// // Prepare the message in the format expected by WebSocket +// val wrappedMessageString = "{\"content\":\"${messageContentString.replace("\"", "\\\"")}\"}" +// +// // Send the formatted message string via WebSocket +// websocketDidReceiveMessage(wrappedMessageString) +// } } }