Skip to content
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

Mob 3730 file preview doesnt work in chat for messages with attachments #1134

Open
wants to merge 2 commits into
base: development
Choose a base branch
from
Open
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
12 changes: 7 additions & 5 deletions widgetssdk/src/main/java/com/glia/widgets/chat/ChatContract.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.net.Uri
import android.widget.Toast
import com.glia.androidsdk.chat.AttachmentFile
import com.glia.androidsdk.chat.SingleChoiceOption
import com.glia.widgets.locale.LocaleString
import com.glia.widgets.base.BaseController
import com.glia.widgets.base.BaseView
import com.glia.widgets.chat.model.ChatItem
Expand All @@ -14,7 +13,8 @@ import com.glia.widgets.chat.model.GvaButton
import com.glia.widgets.chat.model.OperatorMessageItem
import com.glia.widgets.core.dialog.model.ConfirmationDialogLinks
import com.glia.widgets.core.dialog.model.Link
import com.glia.widgets.core.fileupload.model.FileAttachment
import com.glia.widgets.core.fileupload.model.LocalAttachment
import com.glia.widgets.locale.LocaleString

internal interface ChatContract {
interface Controller : BaseController {
Expand All @@ -23,11 +23,11 @@ internal interface ChatContract {
fun setView(view: View)
fun getView(): View?
fun onDestroy(retain: Boolean)
fun onMessageClicked(messageId: String)
fun onTapToRetryClicked(messageId: String)
fun onGvaButtonClicked(button: GvaButton)
fun isCallVisualizerOngoing(): Boolean
fun onFileDownloadClicked(attachmentFile: AttachmentFile)
fun onRemoveAttachment(attachment: FileAttachment)
fun onRemoveAttachment(attachment: LocalAttachment)
fun newMessagesIndicatorClicked()
fun onRecyclerviewPositionChanged(isBottom: Boolean)
fun sendCustomCardResponse(customCard: CustomCardChatItem, text: String, value: String)
Expand Down Expand Up @@ -57,10 +57,11 @@ internal interface ChatContract {
fun onTakePhotoClicked()
fun onImageCaptured(result: Boolean)
fun onContentChosen(uri: Uri)
fun onLocalImageItemClick(attachment: LocalAttachment, view: android.view.View)
}

interface View : BaseView<Controller> {
fun emitUploadAttachments(attachments: List<FileAttachment>)
fun emitUploadAttachments(attachments: List<LocalAttachment>)
fun emitState(chatState: ChatState)
fun emitItems(items: List<ChatItem>)
fun navigateToCall(mediaType: String)
Expand All @@ -82,5 +83,6 @@ internal interface ChatContract {
fun showToast(message: String, duration: Int = Toast.LENGTH_SHORT)
fun dispatchImageCapture(uri: Uri)
fun onFileDownload(attachmentFile: AttachmentFile)
fun navigateToPreview(attachmentFile: LocalAttachment, view: android.view.View)
}
}
162 changes: 89 additions & 73 deletions widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.glia.widgets.chat

import android.text.format.DateUtils
import androidx.annotation.VisibleForTesting
import com.glia.androidsdk.chat.FilesAttachment
import com.glia.androidsdk.chat.SendMessagePayload
import com.glia.androidsdk.chat.SingleChoiceAttachment
import com.glia.androidsdk.chat.VisitorMessage
import com.glia.widgets.chat.domain.AddNewMessagesDividerUseCase
Expand All @@ -16,12 +18,15 @@ import com.glia.widgets.chat.model.ChatItem
import com.glia.widgets.chat.model.CustomCardChatItem
import com.glia.widgets.chat.model.GvaButton
import com.glia.widgets.chat.model.GvaQuickReplies
import com.glia.widgets.chat.model.LocalAttachmentItem
import com.glia.widgets.chat.model.MediaUpgradeStartedTimerItem
import com.glia.widgets.chat.model.NewMessagesDividerItem
import com.glia.widgets.chat.model.OperatorChatItem
import com.glia.widgets.chat.model.OperatorMessageItem
import com.glia.widgets.chat.model.OperatorStatusItem
import com.glia.widgets.chat.model.Unsent
import com.glia.widgets.chat.model.VisitorAttachmentItem
import com.glia.widgets.chat.model.VisitorChatItem
import com.glia.widgets.chat.model.VisitorItemStatus
import com.glia.widgets.core.engagement.domain.model.ChatHistoryResponse
import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
import com.glia.widgets.core.secureconversations.domain.MarkMessagesReadWithDelayUseCase
Expand Down Expand Up @@ -140,12 +145,16 @@ internal class ChatManager(

@VisibleForTesting
fun checkUnsentMessages(state: State) {
val unsent = state.unsentItems.firstOrNull { it.error == null } ?: return
sendUnsentMessagesUseCase(unsent, {
onChatAction(Action.MessageSent(it))
val payload = state.messagePreviews
.takeIf { it.isNotEmpty() }
?.entries
?.first()
?.value ?: return

sendUnsentMessagesUseCase(payload, {
onChatAction(Action.OnMessageSent(it))
}, {
onChatAction(Action.RemoveMessage(unsent.messageId))
onChatAction(Action.MessageSendError(unsent.copy(error = it)))
onChatAction(Action.OnSendMessageError(payload.messageId))
})
}

Expand Down Expand Up @@ -195,23 +204,84 @@ internal class ChatManager(
is Action.OperatorConnected -> mapOperatorConnected(action, state)
Action.Transferring -> mapTransferring(state)
is Action.OperatorJoined -> mapOperatorJoined(action, state)
is Action.UnsentMessageReceived -> addUnsentMessage(action.message, state)
is Action.ResponseCardClicked -> mapResponseCardClicked(action.responseCard, state)
is Action.OnMediaUpgradeStarted -> mapMediaUpgrade(action.isVideo, state)
Action.OnMediaUpgradeToVideo -> mapUpgradeMediaToVideo(state)
Action.OnMediaUpgradeCanceled -> mapMediaUpgradeCanceled(state)
is Action.OnMediaUpgradeTimerUpdated -> mapMediaUpgradeTimerUpdated(action.formattedValue, state)
is Action.CustomCardClicked -> mapCustomCardClicked(action, state)
Action.ChatRestored, Action.None -> state
is Action.MessageClicked -> mapMessageClicked(action.messageId, state)
is Action.MessageSent -> mapMessageSent(action.message, state)
is Action.MessageSendError -> addErrorMessage(action.message, state)
is Action.RemoveMessage -> removeMessage(action.messageId, state)
is Action.AttachmentPreviewAdded -> mapAttachmentPreviewAdded(action.attachments, action.payload, state)
is Action.MessagePreviewAdded -> mapMessagePreviewAdded(action.visitorChatItem, action.payload, state)
is Action.OnMessageSent -> mapMessageSent(action.message, state)
is Action.OnSendMessageError -> mapSendMessageFailed(action.messageId, state)
is Action.OnRetryClicked -> mapRetryClicked(action.messageId, state)
}
}

@VisibleForTesting
fun mapMessageSent(message: VisitorMessage, state: State): State = mapNewMessage(ChatMessageInternal(message), state)
private fun mapRetryClicked(messageId: String, state: State): State = state.apply {
val payload = messagePreviews[messageId] ?: return@apply

val files = (payload.attachment as? FilesAttachment)?.files.orEmpty()

val messageIndex = chatItems.indexOfLast { it.id == payload.messageId }

if (messageIndex != -1) {
chatItems[messageIndex] = (chatItems[messageIndex] as VisitorChatItem).withStatus(VisitorItemStatus.PREVIEW)
}

if (files.isEmpty()) return@apply

val firstFileIndex = if (messageIndex != -1)
messageIndex + 1
else
chatItems.indexOfFirst { it.id == files.first().id }

for (index in firstFileIndex until firstFileIndex + files.size) {
chatItems[index] = (chatItems[index] as VisitorChatItem).withStatus(VisitorItemStatus.PREVIEW)
}
}

private fun mapMessageSent(message: VisitorMessage, state: State): State = mapNewMessage(ChatMessageInternal(message), state)

private fun mapSendMessageFailed(messageId: String, state: State): State = state.apply {
val payload = messagePreviews[messageId] ?: return@apply

val files = (payload.attachment as? FilesAttachment)?.files?.takeIf { it.isNotEmpty() }

val messageIndex = chatItems.indexOfLast { it.id == payload.messageId }

if (messageIndex != -1) {
val messageStatus = if (files != null) {
VisitorItemStatus.ERROR
} else {
VisitorItemStatus.ERROR_INDICATOR
}
chatItems[messageIndex] = (chatItems[messageIndex] as VisitorChatItem).withStatus(messageStatus)
}

if (files == null) return@apply

val indicatorIndex = chatItems.indexOfLast { (it as? LocalAttachmentItem)?.messageId == payload.messageId }

files.forEach { attachment ->
val index = chatItems.indexOfLast { it.id == attachment.id }
val status = if (index == indicatorIndex) VisitorItemStatus.ERROR_INDICATOR else VisitorItemStatus.ERROR

chatItems[index] = (chatItems[index] as VisitorChatItem).withStatus(status)
}
}

private fun mapMessagePreviewAdded(visitorChatItem: VisitorChatItem, payload: SendMessagePayload, state: State): State = state.apply {
val index = indexForMessageItem(chatItems)
chatItems.add(index, visitorChatItem)
messagePreviews[payload.messageId] = payload
}

private fun mapAttachmentPreviewAdded(attachments: List<VisitorAttachmentItem>, payload: SendMessagePayload?, state: State): State = state.apply {
payload?.also { messagePreviews[it.messageId] = it }
chatItems.addAll(attachments)
}

@VisibleForTesting
fun mapCustomCardClicked(action: Action.CustomCardClicked, state: State): State = action.run {
Expand Down Expand Up @@ -274,67 +344,13 @@ internal class ChatManager(
chatItems[index] = responseCard.asPlainText()
}

@VisibleForTesting
fun mapMessageClicked(messageId: String, state: State): State {
val unsent = state.unsentItems.firstOrNull { it.messageId == messageId }

return if (unsent != null) {
state.apply {
onChatAction(Action.RemoveMessage(messageId))
sendUnsentMessagesUseCase(unsent, {
onChatAction(Action.MessageSent(it))
}, {
onChatAction(Action.MessageSendError(unsent))
})
}
} else {
state
}
}

@VisibleForTesting
fun addUnsentMessage(message: Unsent, state: State): State {
state.unsentItems += message
return state.apply {
message.chatMessage?.let {
val index = indexForMessageItem(chatItems)
chatItems.add(index, it)
}
}
}

@VisibleForTesting
fun addErrorMessage(message: Unsent, state: State): State {
return state.apply {
unsentItems += message

message.chatMessage?.let {
val index = indexForMessageItem(chatItems)
chatItems.add(index, it)
}
message.attachmentItems?.let {
chatItems += it
}
}
}

/**
* Returns the index where the new message should be placed.
* If engagement hasn't started yet, the message should be placed before the operator status item.
* */
private fun indexForMessageItem(chatItems: List<ChatItem>) =
if (chatItems.lastOrNull() is OperatorStatusItem.InQueue) chatItems.lastIndex else chatItems.lastIndex + 1

@VisibleForTesting
fun removeMessage(messageId: String, state: State): State {
return state.apply {
unsentItems.removeIf { it.messageId == messageId }
chatItems.removeAll { it.id == messageId }

checkUnsentMessages(state)
}
}

@VisibleForTesting
fun mapOperatorConnected(action: Action.OperatorConnected, state: State): State {
val operatorStatusItem = action.run { OperatorStatusItem.Connected(companyName, operatorFormattedName, operatorImageUrl) }
Expand Down Expand Up @@ -401,7 +417,7 @@ internal class ChatManager(
internal data class State(
val chatItems: MutableList<ChatItem> = mutableListOf(),
val chatItemIds: MutableSet<String> = mutableSetOf(),
val unsentItems: MutableList<Unsent> = mutableListOf(),
val messagePreviews: LinkedHashMap<String, SendMessagePayload> = LinkedHashMap(),
var lastMessageWithVisibleOperatorImage: OperatorChatItem? = null,
var operatorStatusItem: OperatorStatusItem? = null,
var mediaUpgradeTimerItem: MediaUpgradeStartedTimerItem? = null,
Expand All @@ -426,7 +442,6 @@ internal class ChatManager(
data class OperatorConnected(val companyName: String, val operatorFormattedName: String, val operatorImageUrl: String?) : Action
object Transferring : Action
data class OperatorJoined(val companyName: String, val operatorFormattedName: String, val operatorImageUrl: String?) : Action
data class UnsentMessageReceived(val message: Unsent) : Action
data class ResponseCardClicked(val responseCard: OperatorMessageItem.ResponseCard) : Action
data class OnMediaUpgradeStarted(val isVideo: Boolean) : Action
data class OnMediaUpgradeTimerUpdated(val formattedValue: String) : Action
Expand All @@ -435,9 +450,10 @@ internal class ChatManager(
data class CustomCardClicked(val customCard: CustomCardChatItem, val attachment: SingleChoiceAttachment) : Action
object ChatRestored : Action
object None : Action
data class MessageClicked(val messageId: String) : Action
data class MessageSent(val message: VisitorMessage) : Action
data class MessageSendError(val message: Unsent) : Action
data class RemoveMessage(val messageId: String) : Action
data class MessagePreviewAdded(val visitorChatItem: VisitorChatItem, val payload: SendMessagePayload) : Action
data class AttachmentPreviewAdded(val attachments: List<VisitorAttachmentItem>, val payload: SendMessagePayload?) : Action
data class OnMessageSent(val message: VisitorMessage) : Action
data class OnSendMessageError(val messageId: String) : Action
data class OnRetryClicked(val messageId: String) : Action
}
}
Loading