Skip to content

Updating transcriptListPublisher to pass back previousTranscriptNextToken #58

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ From here, you are now ready to interact with the chat via the `ChatSession` obj
The Amazon Connect Chat SDK for Android provides two methods to receive messages.

1. Use [ChatSession.onTranscriptUpdated](#chatsessionontranscriptupdated)
* This event will pass back the entire transcript every time the transcript is updated. This will return the transcript via a List of [TranscriptItem](#transcriptitem)
* This event will pass back the entire transcript every time the transcript is updated. This will return the transcript inside the [TranscriptData](#transcriptdata) object.

2. Use [ChatSession.onMessageReceived](#chatsessiononmessagereceived)
* This event will pass back each message that is received by the WebSocket. The event handler will be passed a single [TranscriptItem](#transcriptitem).
Expand Down Expand Up @@ -420,10 +420,10 @@ var onMessageReceived: ((TranscriptItem) -> Unit)?
--------------------

#### `ChatSession.onTranscriptUpdated`
Callback for when the transcript is updated. See [TranscriptItem](#transcriptitem)
Callback for when the transcript is updated. See [TranscriptData](#transcriptdata)

```
var onTranscriptUpdated: ((List<TranscriptItem>) -> Unit)?
var onTranscriptUpdated: ((TranscriptData) -> Unit)?
```

--------------------
Expand Down Expand Up @@ -646,6 +646,23 @@ open class TranscriptItem(
* The raw JSON format of the received WebSocket message
* Type: Map of `String?`

--------
### TranscriptData
This is the object that is passed back to the registered [ChatSession.onTranscriptUpdated](#chatsessionontranscriptupdated) event handler

```
data class TranscriptData(
val transcriptList: List<TranscriptItem>,
val previousTranscriptNextToken: String?
)
```
* `transcriptList`
* The current in-memory transcript list.
* Type: Array of `TranscriptItem` (See [TranscriptItem](#transcriptitem))
* `previousTranscriptNextToken`
* This is a next token that is used as a `getTranscript` argument to retrieve older messages. This will be `null` if there are no more available messages to fetch from the top of the currently loaded transcript.
* Type: `String`

--------
### Message (extends [TranscriptItem](#transcriptitem))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ class ChatViewModel @Inject constructor(
private val _selectedFileUri = MutableLiveData(Uri.EMPTY)
val selectedFileUri: MutableLiveData<Uri> = _selectedFileUri

private var previousTranscriptNextToken: String? = null

// State to store chat transcript items (messages and events)
var messages = mutableStateListOf<TranscriptItem>()
private set
Expand Down Expand Up @@ -124,10 +126,11 @@ class ChatViewModel @Inject constructor(
// Handle received websocket message if needed
}

chatSession.onTranscriptUpdated = { transcriptList ->
Log.d("ChatViewModel", "Transcript onTranscriptUpdated last 3 items: ${transcriptList.takeLast(3)}")
chatSession.onTranscriptUpdated = { transcriptData ->
Log.d("ChatViewModel", "Transcript onTranscriptUpdated last 3 items: ${transcriptData.transcriptList.takeLast(3)}")
previousTranscriptNextToken = transcriptData.previousTranscriptNextToken
viewModelScope.launch {
onUpdateTranscript(transcriptList)
onUpdateTranscript(transcriptData.transcriptList)
}
}

Expand Down Expand Up @@ -299,27 +302,19 @@ class ChatViewModel @Inject constructor(
chatSession.sendMessageReceipt(message, MessageReceiptType.MESSAGE_READ)
}


// Fetch the chat transcript
fun fetchTranscript(onCompletion: (Boolean) -> Unit, nextToken: String? = null) {
viewModelScope.launch {
chatSession.getTranscript(
ScanDirection.BACKWARD,
SortKey.DESCENDING,
15,
nextToken,
previousTranscriptNextToken,
null
).onSuccess { response ->
Log.d("ChatViewModel", "Transcript fetched successfully")

// Check for nextToken
if (!response.nextToken.isNullOrEmpty()) {
// Call fetchTranscript again with the nextToken
fetchTranscript(onCompletion, response.nextToken)
} else {
// No more pages to fetch, call onCompletion
onCompletion(true)
}
onCompletion(true)
}.onFailure {
Log.e("ChatViewModel", "Error fetching transcript: ${it.message}")
onCompletion(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.amazon.connect.chat.sdk.model.GlobalConfig
import com.amazon.connect.chat.sdk.model.Message
import com.amazon.connect.chat.sdk.model.MessageReceiptType
import com.amazon.connect.chat.sdk.model.MessageStatus
import com.amazon.connect.chat.sdk.model.TranscriptData
import com.amazon.connect.chat.sdk.model.TranscriptItem
import com.amazon.connect.chat.sdk.model.TranscriptResponse
import com.amazon.connect.chat.sdk.provider.ConnectionDetailsProvider
Expand Down Expand Up @@ -148,7 +149,7 @@ interface ChatSession {
var onConnectionBroken: (() -> Unit)?
var onDeepHeartBeatFailure: (() -> Unit)?
var onMessageReceived: ((TranscriptItem) -> Unit)?
var onTranscriptUpdated: ((List<TranscriptItem>) -> Unit)?
var onTranscriptUpdated: ((TranscriptData) -> Unit)?
var onChatEnded: (() -> Unit)?
var isChatSessionActive: Boolean
}
Expand All @@ -161,7 +162,7 @@ class ChatSessionImpl @Inject constructor(private val chatService: ChatService)
override var onConnectionBroken: (() -> Unit)? = null
override var onDeepHeartBeatFailure: (() -> Unit)? = null
override var onMessageReceived: ((TranscriptItem) -> Unit)? = null
override var onTranscriptUpdated: ((List<TranscriptItem>) -> Unit)? = null
override var onTranscriptUpdated: ((TranscriptData) -> Unit)? = null
override var onChatEnded: (() -> Unit)? = null
override var onChatSessionStateChanged: ((Boolean) -> Unit)? = null
override var isChatSessionActive: Boolean = false
Expand Down Expand Up @@ -205,11 +206,11 @@ class ChatSessionImpl @Inject constructor(private val chatService: ChatService)
}

transcriptListCollectionJob = coroutineScope.launch {
chatService.transcriptListPublisher.collect { transcriptList ->
if (transcriptList.isNotEmpty()) {
chatService.transcriptListPublisher.collect { transcriptData ->
if (transcriptData.transcriptList.isNotEmpty()) {
// Make sure it runs on main thread
coroutineScope.launch {
onTranscriptUpdated?.invoke(transcriptList)
onTranscriptUpdated?.invoke(transcriptData)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ open class TranscriptItem(
}
}

data class TranscriptData(
val transcriptList: List<TranscriptItem>,
val previousTranscriptNextToken: String?
)

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package com.amazon.connect.chat.sdk.repository

import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
Expand All @@ -20,6 +21,7 @@ import com.amazon.connect.chat.sdk.model.MessageMetadata
import com.amazon.connect.chat.sdk.model.MessageReceiptType
import com.amazon.connect.chat.sdk.model.MessageStatus
import com.amazon.connect.chat.sdk.model.MetricName
import com.amazon.connect.chat.sdk.model.TranscriptData
import com.amazon.connect.chat.sdk.model.TranscriptItem
import com.amazon.connect.chat.sdk.model.TranscriptResponse
import com.amazon.connect.chat.sdk.network.AWSClient
Expand All @@ -31,6 +33,7 @@ import com.amazon.connect.chat.sdk.utils.Constants
import com.amazon.connect.chat.sdk.utils.TranscriptItemUtils
import com.amazon.connect.chat.sdk.utils.logger.SDKLogger
import com.amazonaws.services.connectparticipant.model.GetTranscriptRequest
import com.amazonaws.services.connectparticipant.model.Item
import com.amazonaws.services.connectparticipant.model.ScanDirection
import com.amazonaws.services.connectparticipant.model.SortKey
import com.amazonaws.services.connectparticipant.model.StartPosition
Expand All @@ -39,6 +42,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -157,7 +161,7 @@ interface ChatService {

val eventPublisher: SharedFlow<ChatEvent>
val transcriptPublisher: SharedFlow<TranscriptItem>
val transcriptListPublisher: SharedFlow<List<TranscriptItem>>
val transcriptListPublisher: SharedFlow<TranscriptData>
val chatSessionStatePublisher: SharedFlow<Boolean>
}

Expand All @@ -181,8 +185,8 @@ class ChatServiceImpl @Inject constructor(
override val transcriptPublisher: SharedFlow<TranscriptItem> get() = _transcriptPublisher
private var transcriptCollectionJob: Job? = null

private val _transcriptListPublisher = MutableSharedFlow<List<TranscriptItem>>()
override val transcriptListPublisher: SharedFlow<List<TranscriptItem>> get() = _transcriptListPublisher
private val _transcriptListPublisher = MutableSharedFlow<TranscriptData>()
override val transcriptListPublisher: SharedFlow<TranscriptData> get() = _transcriptListPublisher

private val _chatSessionStatePublisher = MutableSharedFlow<Boolean>()
override val chatSessionStatePublisher: SharedFlow<Boolean> get() = _chatSessionStatePublisher
Expand All @@ -192,11 +196,15 @@ class ChatServiceImpl @Inject constructor(
private var transcriptDict = mutableMapOf<String, TranscriptItem>()
@VisibleForTesting
internal var internalTranscript = mutableListOf<TranscriptItem>()
@VisibleForTesting
internal var previousTranscriptNextToken: String? = null

private var typingIndicatorTimer: Timer? = null
private var throttleTypingEventTimer: Timer? = null
private var throttleTypingEvent: Boolean = false

private var debounceTranscriptListPublisherEvent: Job? = null

// Dictionary to map attachment IDs to temporary message IDs
private val attachmentIdToTempMessageId = mutableMapOf<String, String>()

Expand Down Expand Up @@ -347,7 +355,7 @@ class ChatServiceImpl @Inject constructor(
// Send the updated transcript list to subscribers if items removed
if (transcriptDict.size != initialCount) {
coroutineScope.launch {
_transcriptListPublisher.emit(internalTranscript)
_transcriptListPublisher.emit(TranscriptData(internalTranscript, previousTranscriptNextToken))
}
}
}
Expand Down Expand Up @@ -405,7 +413,11 @@ class ChatServiceImpl @Inject constructor(
}
}

_transcriptListPublisher.emit(internalTranscript)
debounceTranscriptListPublisherEvent?.cancel()
debounceTranscriptListPublisherEvent = coroutineScope.launch {
delay(300)
_transcriptListPublisher.emit(TranscriptData(internalTranscript, previousTranscriptNextToken))
}
}
}

Expand All @@ -422,7 +434,7 @@ class ChatServiceImpl @Inject constructor(
internalTranscript.removeAll { it.id == oldId }
// Send out updated transcript
coroutineScope.launch {
_transcriptListPublisher.emit(internalTranscript)
_transcriptListPublisher.emit(TranscriptData(internalTranscript, previousTranscriptNextToken))
}
} else {
// Update the placeholder message's ID to the new ID
Expand Down Expand Up @@ -553,7 +565,7 @@ class ChatServiceImpl @Inject constructor(
transcriptDict.remove(messageId)
// Send out updated transcript with old message removed
coroutineScope.launch {
_transcriptListPublisher.emit(internalTranscript)
_transcriptListPublisher.emit(TranscriptData(internalTranscript, previousTranscriptNextToken))
}

// as the next step, attempt to resend the message based on its type
Expand Down Expand Up @@ -781,6 +793,33 @@ class ChatServiceImpl @Inject constructor(
return runCatching {
val response = awsClient.getTranscript(request).getOrThrow()
val transcriptItems = response.transcript

val isStartPositionDefined = request.startPosition != null && (
request.startPosition.id != null ||
request.startPosition.absoluteTime != null ||
request.startPosition.mostRecent != null
)

if ((request.scanDirection == ScanDirection.BACKWARD.toString()) && !(isStartPositionDefined && transcriptItems.isEmpty())) {
if (internalTranscript.isEmpty() || transcriptItems.isEmpty()) {
previousTranscriptNextToken = response.nextToken
_transcriptListPublisher.emit(TranscriptData(internalTranscript, previousTranscriptNextToken))
} else {
val oldestInternalTranscriptItem = internalTranscript.first()
val oldestTranscriptItem: Item;
if (request.sortOrder == SortKey.ASCENDING.toString()) {
oldestTranscriptItem = transcriptItems.first()
} else {
oldestTranscriptItem = transcriptItems.last()
}
val oldestInternalTranscriptItemTimeStamp = oldestInternalTranscriptItem.timeStamp
val oldestTranscriptItemTimeStamp = oldestTranscriptItem.absoluteTime
if (oldestTranscriptItemTimeStamp <= oldestInternalTranscriptItemTimeStamp) {
previousTranscriptNextToken = response.nextToken
}
}
}

// Format and process transcript items
val formattedItems = transcriptItems.mapNotNull { transcriptItem ->
TranscriptItemUtils.serializeTranscriptItem(transcriptItem)?.let { serializedItem ->
Expand Down
Loading
Loading