From 3e49ea577e4390f419ed43535e9432a56f72500c Mon Sep 17 00:00:00 2001 From: BitriseBot Date: Wed, 12 Jul 2023 13:00:59 +0000 Subject: [PATCH 01/69] Increment project version to 2.0.3 --- version.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.properties b/version.properties index c1d79dd2d..5d3c92139 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ -#Tue Jul 11 09:05:13 UTC 2023 +#Wed Jul 12 13:00:56 UTC 2023 dependency.coreSdk.version=1.0.2 -widgets.versionCode=64 -widgets.versionName=2.0.2 +widgets.versionCode=65 +widgets.versionName=2.0.3 From cb85f841c7d6b04d14d951fd6b1872f55202bedb Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Mon, 10 Jul 2023 10:46:41 +0300 Subject: [PATCH 02/69] - Parse GVA JSON responses. - Convert ChatMessageInternal and ChatState to Kotlin - Add Gva related fields to ChatState and ChatMessageInternal - Add quick replies temporary text field - Add Tests --- .../java/com/glia/widgets/chat/ChatView.kt | 11 +- .../glia/widgets/chat/adapter/ChatAdapter.kt | 92 ++- .../OperatorFileAttachmentViewHolder.java | 24 +- .../OperatorImageAttachmentViewHolder.java | 10 +- .../widgets/chat/controller/ChatController.kt | 279 ++++------ .../domain/CustomCardAdapterTypeUseCase.java | 23 - .../domain/CustomCardAdapterTypeUseCase.kt | 11 + .../chat/domain/GliaLoadHistoryUseCase.kt | 15 +- .../chat/domain/GliaOnMessageUseCase.java | 16 +- .../chat/domain/GliaSendMessageUseCase.kt | 5 +- .../chat/domain/gva/GetGvaTypeUseCase.kt | 10 + .../widgets/chat/domain/gva/IsGvaUseCase.kt | 12 + .../gva/MapGvaGvaGalleryCardsUseCase.kt | 25 + .../gva/MapGvaGvaQuickRepliesUseCase.kt | 25 + .../gva/MapGvaPersistentButtonsUseCase.kt | 27 + .../domain/gva/MapGvaResponseTextUseCase.kt | 23 + .../widgets/chat/domain/gva/MapGvaUseCase.kt | 23 + .../chat/domain/gva/ParseGvaButtonsUseCase.kt | 13 + .../domain/gva/ParseGvaGalleryCardsUseCase.kt | 13 + .../glia/widgets/chat/model/ChatState.java | 525 ------------------ .../com/glia/widgets/chat/model/ChatState.kt | 145 +++++ .../glia/widgets/chat/model/GvaBaseTypes.kt | 70 +++ .../chat/model/history/GvaChatItems.kt | 46 ++ .../history/MediaUpgradeStartedTimerItem.java | 21 +- .../model/history/OperatorAttachmentItem.java | 59 -- .../model/history/OperatorAttachmentItem.kt | 16 + .../chat/model/history/OperatorChatItem.java | 30 +- .../model/history/OperatorMessageItem.java | 40 +- .../chat/model/history/ResponseCardItem.java | 57 -- .../chat/model/history/ResponseCardItem.kt | 20 + .../engagement/domain/MapOperatorUseCase.kt | 26 +- .../domain/model/ChatMessageInternal.java | 51 -- .../domain/model/ChatMessageInternal.kt | 18 + .../glia/widgets/di/ControllerFactory.java | 342 ++++++------ .../com/glia/widgets/di/UseCaseFactory.java | 314 +++++++---- .../glia/widgets/helper/CommonExtensions.kt | 7 + .../com/glia/widgets/view/unifiedui/Merge.kt | 2 +- widgetssdk/src/main/res/layout/chat_view.xml | 29 +- .../src/test/java/android/TestExtensions.kt | 7 + .../widgets/chat/MockChatMessageInternal.kt | 46 ++ .../chat/controller/ChatControllerTest.kt | 10 +- .../CustomCardAdapterTypeUseCaseTest.java | 8 +- .../chat/domain/gva/GetGvaTypeUseCaseTest.kt | 38 ++ .../chat/domain/gva/IsGvaUseCaseTest.kt | 43 ++ .../gva/MapGvaGvaGalleryCardsUseCaseTest.kt | 73 +++ .../gva/MapGvaGvaQuickRepliesUseCaseTest.kt | 87 +++ .../gva/MapGvaPersistentButtonsUseCaseTest.kt | 74 +++ .../gva/MapGvaResponseTextUseCaseTest.kt | 66 +++ .../chat/domain/gva/MapGvaUseCaseTest.kt | 108 ++++ .../domain/gva/ParseGvaButtonsUseCaseTest.kt | 49 ++ .../gva/ParseGvaGalleryCardsUseCaseTest.kt | 48 ++ .../domain/MapOperatorUseCaseTest.kt | 6 +- .../src/test/resources/gva_gallery.json | 47 ++ .../resources/gva_persistent_buttons.json | 41 ++ .../src/test/resources/gva_plain_text.json | 11 + .../src/test/resources/gva_quick_reply.json | 14 + 56 files changed, 1984 insertions(+), 1267 deletions(-) delete mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.java create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.kt create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/GetGvaTypeUseCase.kt create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/IsGvaUseCase.kt create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCase.kt create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCase.kt create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCase.kt create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCase.kt create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaUseCase.kt create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaButtonsUseCase.kt create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaGalleryCardsUseCase.kt delete mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.java create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/model/history/GvaChatItems.kt delete mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorAttachmentItem.java create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorAttachmentItem.kt delete mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ResponseCardItem.java create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ResponseCardItem.kt delete mode 100644 widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.java create mode 100644 widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.kt create mode 100644 widgetssdk/src/test/java/android/TestExtensions.kt create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/MockChatMessageInternal.kt create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/GetGvaTypeUseCaseTest.kt create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/IsGvaUseCaseTest.kt create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCaseTest.kt create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCaseTest.kt create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCaseTest.kt create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCaseTest.kt create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaUseCaseTest.kt create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/ParseGvaButtonsUseCaseTest.kt create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/ParseGvaGalleryCardsUseCaseTest.kt create mode 100644 widgetssdk/src/test/resources/gva_gallery.json create mode 100644 widgetssdk/src/test/resources/gva_persistent_buttons.json create mode 100644 widgetssdk/src/test/resources/gva_plain_text.json create mode 100644 widgetssdk/src/test/resources/gva_quick_reply.json diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt index d68fad930..72fb160ba 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt @@ -388,8 +388,7 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty updateShowSendButton(chatState) updateChatEditText(chatState) updateAppBar(chatState) - binding.newMessagesIndicatorLayout.isVisible = - chatState.showMessagesUnseenIndicator() + binding.newMessagesIndicatorLayout.isVisible = chatState.showMessagesUnseenIndicator updateNewMessageOperatorStatusView(chatState.operatorProfileImgUrl) isInBottom = chatState.isChatInBottom binding.chatRecyclerView.setInBottom(isInBottom) @@ -402,6 +401,7 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty binding.operatorTypingAnimationView.isVisible = chatState.isOperatorTyping updateAttachmentButton(chatState) + updateQuickRepliesState(chatState) } } @@ -481,6 +481,12 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty } } + private fun updateQuickRepliesState(chatState: ChatState) { + val quickReplies = chatState.gvaQuickReplies + binding.gvaQuickRepliesLayout.isVisible = quickReplies.isNotEmpty() + binding.gvaQuickRepliesLayout.text = quickReplies.map { it.text }.toString() + } + private fun updateNewMessageOperatorStatusView(operatorProfileImgUrl: String?) { binding.newMessagesIndicatorImage.apply { operatorProfileImgUrl?.also(::showProfileImage) ?: showPlaceholder() @@ -508,6 +514,7 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty Dialog.MODE_ENABLE_SCREEN_SHARING_NOTIFICATIONS_AND_START_SHARING -> post { showAllowScreenSharingNotificationsAndStartSharingDialog() } + Dialog.MODE_VISITOR_CODE -> { Logger.e(TAG, "DialogController callback in ChatView with MODE_VISITOR_CODE") } // Should never happen inside ChatView diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt index dc71c2990..05eecee11 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt @@ -20,8 +20,12 @@ import com.glia.widgets.chat.adapter.holder.fileattachment.VisitorFileAttachment import com.glia.widgets.chat.adapter.holder.imageattachment.ImageAttachmentViewHolder import com.glia.widgets.chat.adapter.holder.imageattachment.OperatorImageAttachmentViewHolder import com.glia.widgets.chat.adapter.holder.imageattachment.VisitorImageAttachmentViewHolder +import com.glia.widgets.chat.model.Gva import com.glia.widgets.chat.model.history.ChatItem import com.glia.widgets.chat.model.history.CustomCardItem +import com.glia.widgets.chat.model.history.GvaGalleryCards +import com.glia.widgets.chat.model.history.GvaPersistentButtons +import com.glia.widgets.chat.model.history.GvaResponseText import com.glia.widgets.chat.model.history.MediaUpgradeStartedTimerItem import com.glia.widgets.chat.model.history.OperatorAttachmentItem import com.glia.widgets.chat.model.history.OperatorMessageItem @@ -66,11 +70,13 @@ internal class ChatAdapter( uiTheme ) } + VISITOR_FILE_VIEW_TYPE -> { val view = inflater.inflate(R.layout.chat_attachment_visitor_file_layout, parent, false) VisitorFileAttachmentViewHolder(view, uiTheme) } + VISITOR_IMAGE_VIEW_TYPE -> { VisitorImageAttachmentViewHolder( inflater.inflate(R.layout.chat_attachment_visitor_image_layout, parent, false), @@ -80,12 +86,14 @@ internal class ChatAdapter( getImageFileFromNetworkUseCase ) } + VISITOR_MESSAGE_TYPE -> { VisitorMessageViewHolder( ChatVisitorMessageLayoutBinding.inflate(inflater, parent, false), uiTheme ) } + OPERATOR_IMAGE_VIEW_TYPE -> { OperatorImageAttachmentViewHolder( inflater.inflate(R.layout.chat_attachment_operator_image_layout, parent, false), @@ -95,6 +103,7 @@ internal class ChatAdapter( getImageFileFromNetworkUseCase ) } + OPERATOR_FILE_VIEW_TYPE -> { OperatorFileAttachmentViewHolder( inflater.inflate( @@ -105,18 +114,21 @@ internal class ChatAdapter( uiTheme ) } + OPERATOR_MESSAGE_VIEW_TYPE -> { OperatorMessageViewHolder( ChatOperatorMessageLayoutBinding.inflate(inflater, parent, false), uiTheme ) } + MEDIA_UPGRADE_ITEM_TYPE -> { MediaUpgradeStartedViewHolder( ChatMediaUpgradeLayoutBinding.inflate(inflater, parent, false), uiTheme ) } + NEW_MESSAGES_DIVIDER_TYPE -> { NewMessagesDividerViewHolder( ChatNewMessagesDividerLayoutBinding.inflate( @@ -127,6 +139,7 @@ internal class ChatAdapter( uiTheme ) } + SYSTEM_MESSAGE_TYPE -> SystemMessageViewHolder( ChatReceiveMessageContentBinding.inflate( inflater, @@ -135,6 +148,56 @@ internal class ChatAdapter( ), uiTheme ) + +// TODO should be implemented later - MOB 2364 + GVA_RESPONSE_TEXT_TYPE -> { + SystemMessageViewHolder( + ChatReceiveMessageContentBinding.inflate( + inflater, + parent, + false + ), + uiTheme + ) + } + +// TODO should be changed to appropriate ViewHolder later - MOB 2371 + GVA_PERSISTENT_BUTTONS_TYPE -> { + SystemMessageViewHolder( + ChatReceiveMessageContentBinding.inflate( + inflater, + parent, + false + ), + uiTheme + ) + } + +// TODO should be changed to appropriate ViewHolder later - MOB 2396 + GVA_QUICK_REPLIES_TYPE -> { + SystemMessageViewHolder( + ChatReceiveMessageContentBinding.inflate( + inflater, + parent, + false + ), + uiTheme + ) + } + +// TODO should be changed to appropriate ViewHolder later - MOB 2404 + GVA_GALLERY_CARDS_TYPE -> { + SystemMessageViewHolder( + ChatReceiveMessageContentBinding.inflate( + inflater, + parent, + false + ), + uiTheme + ) + } + + else -> { var customCardViewHolder: CustomCardViewHolder? = null if (customCardAdapter != null) { @@ -176,9 +239,11 @@ internal class ChatAdapter( chatItem, onOptionClickedListener ) + is MediaUpgradeStartedTimerItem -> (holder as MediaUpgradeStartedViewHolder).bind( chatItem ) + is OperatorAttachmentItem -> { if (chatItem.getViewType() == OPERATOR_FILE_VIEW_TYPE) { (holder as OperatorFileAttachmentViewHolder).bind( @@ -192,6 +257,7 @@ internal class ChatAdapter( ) } } + is VisitorAttachmentItem -> { if (chatItem.getViewType() == VISITOR_FILE_VIEW_TYPE) { (holder as VisitorFileAttachmentViewHolder).bind( @@ -206,7 +272,15 @@ internal class ChatAdapter( } } } + is SystemChatItem -> (holder as SystemMessageViewHolder).bind(chatItem.message) + + is GvaResponseText -> (holder as SystemMessageViewHolder).bind(Gva.Type.PLAIN_TEXT.name) + + is GvaPersistentButtons -> (holder as SystemMessageViewHolder).bind(Gva.Type.PERSISTENT_BUTTONS.name) + + is GvaGalleryCards -> (holder as SystemMessageViewHolder).bind(Gva.Type.GALLERY_CARDS.name) + is CustomCardItem -> { (holder as CustomCardViewHolder).bind(chatItem.message) { text: String, value: String -> onCustomCardResponse.onCustomCardResponse(chatItem.getId(), text, value) @@ -240,7 +314,7 @@ internal class ChatAdapter( fun onFileDownloadClick(file: AttachmentFile) } - interface OnImageItemClickListener { + fun interface OnImageItemClickListener { fun onImageItemClick(item: AttachmentFile, view: View) } @@ -259,7 +333,15 @@ internal class ChatAdapter( const val VISITOR_IMAGE_VIEW_TYPE = 7 const val NEW_MESSAGES_DIVIDER_TYPE = 8 const val SYSTEM_MESSAGE_TYPE = 9 - const val CUSTOM_CARD_TYPE = 10 // Should be the last type with the highest value + + //GVA Types + const val GVA_RESPONSE_TEXT_TYPE = 10 + const val GVA_PERSISTENT_BUTTONS_TYPE = 11 + const val GVA_QUICK_REPLIES_TYPE = 12 + const val GVA_GALLERY_CARDS_TYPE = 13 + + //Custom Card + const val CUSTOM_CARD_TYPE = 14 // Should be the last type with the highest value } @IntDef( @@ -273,7 +355,11 @@ internal class ChatAdapter( VISITOR_IMAGE_VIEW_TYPE, NEW_MESSAGES_DIVIDER_TYPE, SYSTEM_MESSAGE_TYPE, - CUSTOM_CARD_TYPE + CUSTOM_CARD_TYPE, + GVA_RESPONSE_TEXT_TYPE, + GVA_PERSISTENT_BUTTONS_TYPE, + GVA_QUICK_REPLIES_TYPE, + GVA_GALLERY_CARDS_TYPE ) @Retention(AnnotationRetention.SOURCE) annotation class Type diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/OperatorFileAttachmentViewHolder.java b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/OperatorFileAttachmentViewHolder.java index 94626e57e..708d4d8de 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/OperatorFileAttachmentViewHolder.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/OperatorFileAttachmentViewHolder.java @@ -24,7 +24,7 @@ public OperatorFileAttachmentViewHolder(@NonNull View itemView, UiTheme uiTheme) } public void bind(OperatorAttachmentItem item, ChatAdapter.OnFileItemClickListener listener) { - super.setData(item.isFileExists, item.isDownloading, item.attachmentFile, listener); + super.setData(item.isFileExists(), item.isDownloading(), item.getAttachmentFile(), listener); updateOperatorStatusView(item); } @@ -34,29 +34,29 @@ private void setupOperatorStatusView(UiTheme uiTheme) { } private void updateOperatorStatusView(OperatorAttachmentItem item) { - operatorStatusView.setVisibility(item.showChatHead ? View.VISIBLE : View.GONE); - if (item.operatorProfileImgUrl != null) { - operatorStatusView.showProfileImage(item.operatorProfileImgUrl); + operatorStatusView.setVisibility(item.getShowChatHead() ? View.VISIBLE : View.GONE); + if (item.getOperatorProfileImgUrl() != null) { + operatorStatusView.showProfileImage(item.getOperatorProfileImgUrl()); } else { operatorStatusView.showPlaceholder(); } - String name = item.attachmentFile.getName(); - String byteSize = Formatter.formatFileSize(itemView.getContext(), item.attachmentFile.getSize()); + String name = item.getAttachmentFile().getName(); + String byteSize = Formatter.formatFileSize(itemView.getContext(), item.getAttachmentFile().getSize()); itemView.setContentDescription(itemView.getResources().getString(R.string.glia_chat_operator_file_content_description, name, byteSize)); ViewCompat.setAccessibilityDelegate(itemView, new AccessibilityDelegateCompat() { @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); - String actionLabel = host.getResources().getString(item.isFileExists - ? R.string.glia_chat_attachment_open_button_label - : R.string.glia_chat_attachment_download_button_label); + String actionLabel = host.getResources().getString(item.isFileExists() + ? R.string.glia_chat_attachment_open_button_label + : R.string.glia_chat_attachment_download_button_label); AccessibilityNodeInfoCompat.AccessibilityActionCompat actionClick - = new AccessibilityNodeInfoCompat.AccessibilityActionCompat( - AccessibilityNodeInfoCompat.ACTION_CLICK, actionLabel); + = new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + AccessibilityNodeInfoCompat.ACTION_CLICK, actionLabel); info.addAction(actionClick); } }); diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/OperatorImageAttachmentViewHolder.java b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/OperatorImageAttachmentViewHolder.java index 808988b07..bf7e2dc87 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/OperatorImageAttachmentViewHolder.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/OperatorImageAttachmentViewHolder.java @@ -37,8 +37,8 @@ private void setupOperatorStatus(UiTheme uiTheme) { } public void bind(OperatorAttachmentItem item, ChatAdapter.OnImageItemClickListener onImageItemClickListener) { - super.bind(item.attachmentFile); - itemView.setOnClickListener(v -> onImageItemClickListener.onImageItemClick(item.attachmentFile, v)); + super.bind(item.getAttachmentFile()); + itemView.setOnClickListener(v -> onImageItemClickListener.onImageItemClick(item.getAttachmentFile(), v)); updateOperatorStatus(item); setAccessibilityLabels(); @@ -63,9 +63,9 @@ public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull Acces } private void updateOperatorStatus(OperatorAttachmentItem item) { - operatorStatusView.setVisibility(item.showChatHead ? View.VISIBLE : View.GONE); - if (item.operatorProfileImgUrl != null) { - operatorStatusView.showProfileImage(item.operatorProfileImgUrl); + operatorStatusView.setVisibility(item.getShowChatHead() ? View.VISIBLE : View.GONE); + if (item.getOperatorProfileImgUrl() != null) { + operatorStatusView.showProfileImage(item.getOperatorProfileImgUrl()); } else { operatorStatusView.showPlaceholder(); } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index 6af928ac8..e3af5d56d 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -43,10 +43,17 @@ import com.glia.widgets.chat.domain.IsShowSendButtonUseCase import com.glia.widgets.chat.domain.PreEngagementMessageUseCase import com.glia.widgets.chat.domain.SiteInfoUseCase import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase +import com.glia.widgets.chat.domain.gva.IsGvaUseCase +import com.glia.widgets.chat.domain.gva.MapGvaUseCase import com.glia.widgets.chat.model.ChatInputMode import com.glia.widgets.chat.model.ChatState +import com.glia.widgets.chat.model.GvaButton import com.glia.widgets.chat.model.history.ChatItem import com.glia.widgets.chat.model.history.CustomCardItem +import com.glia.widgets.chat.model.history.GvaGalleryCards +import com.glia.widgets.chat.model.history.GvaPersistentButtons +import com.glia.widgets.chat.model.history.GvaQuickReplies +import com.glia.widgets.chat.model.history.GvaResponseText import com.glia.widgets.chat.model.history.LinkedChatItem import com.glia.widgets.chat.model.history.MediaUpgradeStartedTimerItem import com.glia.widgets.chat.model.history.NewMessagesItem @@ -169,7 +176,9 @@ internal class ChatController( private val preEngagementMessageUseCase: PreEngagementMessageUseCase, private val addNewMessagesDividerUseCase: AddNewMessagesDividerUseCase, private val isFileReadyForPreviewUseCase: IsFileReadyForPreviewUseCase, - private val acceptMediaUpgradeOfferUseCase: AcceptMediaUpgradeOfferUseCase + private val acceptMediaUpgradeOfferUseCase: AcceptMediaUpgradeOfferUseCase, + private val isGvaUseCase: IsGvaUseCase, + private val mapGvaUseCase: MapGvaUseCase ) : GliaOnEngagementUseCase.Listener, GliaOnEngagementEndUseCase.Listener, OnSurveyListener { private var backClickedListener: ChatView.OnBackClickedListener? = null private var viewCallback: ChatViewCallback? = null @@ -197,8 +206,8 @@ internal class ChatController( emitViewState { chatState - .setLastTypedText(EMPTY_MESSAGE) - .setShowSendButton(isShowSendButtonUseCase(EMPTY_MESSAGE)) + .setLastTypedText("") + .setShowSendButton(isShowSendButtonUseCase("")) } } @@ -206,7 +215,6 @@ internal class ChatController( onSendMessageOperatorOffline(message) } - override fun errorMessageInvalid() {} override fun error(ex: GliaException) { onMessageSendError(ex) } @@ -379,6 +387,7 @@ internal class ChatController( Logger.d(TAG, "Send MESSAGE: $message") clearMessagePreview() sendMessageUseCase.execute(message, sendMessageCallback) + addQuickReplyButtons(emptyList()) } private fun sendMessagePreview(message: String) { @@ -389,7 +398,7 @@ internal class ChatController( private fun clearMessagePreview() { // An empty string has to be sent to clear the message preview. - sendMessagePreview(EMPTY_MESSAGE) + sendMessagePreview("") } private fun subscribeToMessages() { @@ -429,6 +438,8 @@ internal class ChatController( } private fun onMessage(messageInternal: ChatMessageInternal) { + addQuickReplyButtons(emptyList()) + emitChatItems { val message = messageInternal.chatMessage if (!isNewMessage(chatState.chatItems, message)) { @@ -681,7 +692,9 @@ internal class ChatController( visitor.visit(engagementState) ) - EngagementStateEvent.Type.ENGAGEMENT_ENDED -> {} + EngagementStateEvent.Type.ENGAGEMENT_ENDED -> { + Logger.d(TAG, "Engagement Ended") + } } } @@ -700,9 +713,7 @@ internal class ChatController( private fun onTransferring() { emitChatItems { val items: MutableList = chatState.chatItems.toMutableList() - if (chatState.operatorStatusItem != null) { - items.remove(chatState.operatorStatusItem) - } + chatState.operatorStatusItem?.also(items::remove) items.add(OperatorStatusItem.TransferringStatusItem()) emitViewState { chatState.transferring() } @@ -745,8 +756,8 @@ internal class ChatController( private fun initMediaUpgradeCallback() { mediaUpgradeOfferRepositoryCallback = object : MediaUpgradeOfferRepositoryCallback { override fun newOffer(offer: MediaUpgradeOffer) { - if (isChatViewPaused) return when { + isChatViewPaused -> return offer.video == MediaDirection.NONE && offer.audio == MediaDirection.TWO_WAY -> { // audio call Logger.d(TAG, "audioUpgradeRequested") @@ -813,9 +824,7 @@ internal class ChatController( Logger.d(TAG, "viewInitQueueing") emitChatItems { val items: MutableList = chatState.chatItems.toMutableList() - if (chatState.operatorStatusItem != null) { - items.remove(chatState.operatorStatusItem) - } + chatState.operatorStatusItem?.also(items::remove) val operatorStatusItem = OperatorStatusItem.QueueingStatusItem(chatState.companyName) items.add(operatorStatusItem) emitViewState { chatState.queueingStarted(operatorStatusItem) } @@ -825,11 +834,11 @@ internal class ChatController( } private fun updateQueueing(items: MutableList) { - if (chatState.operatorStatusItem?.status == OperatorStatusItem.Status.IN_QUEUE) { - items.remove(chatState.operatorStatusItem) - items.add( - OperatorStatusItem.QueueingStatusItem(chatState.companyName) - ) + chatState.operatorStatusItem?.also { + if (it.status == OperatorStatusItem.Status.IN_QUEUE) { + items.remove(it) + items.add(OperatorStatusItem.QueueingStatusItem(chatState.companyName)) + } } } @@ -849,12 +858,12 @@ internal class ChatController( val items: MutableList = chatState.chatItems.toMutableList() if (chatState.operatorStatusItem != null) { // remove previous operator status item - val operatorStatusItemIndex = items.indexOf(chatState.operatorStatusItem) + val operatorStatusItemIndex = items.indexOf(chatState.operatorStatusItem!!) Logger.d( TAG, "operatorStatusItemIndex: " + operatorStatusItemIndex + ", size: " + items.size ) - items.remove(chatState.operatorStatusItem) + items.remove(chatState.operatorStatusItem!!) items.add( operatorStatusItemIndex, OperatorStatusItem.OperatorFoundStatusItem( @@ -912,8 +921,7 @@ internal class ChatController( private fun appendHistoryChatItem( currentChatItems: MutableList, - chatMessageInternal: ChatMessageInternal, - isLastItem: Boolean + chatMessageInternal: ChatMessageInternal ) { val message = chatMessageInternal.chatMessage when (message.senderType) { @@ -923,7 +931,7 @@ internal class ChatController( } Chat.Participant.OPERATOR -> { - appendOperatorMessage(currentChatItems, chatMessageInternal, isLastItem) + appendOperatorMessage(currentChatItems, chatMessageInternal) } Chat.Participant.SYSTEM -> { @@ -970,7 +978,7 @@ internal class ChatController( currentChatItems: MutableList, messageInternal: ChatMessageInternal ) { - appendOperatorMessage(currentChatItems, messageInternal, true) + appendOperatorMessage(currentChatItems, messageInternal) appendMessagesNotSeen() } @@ -1012,9 +1020,8 @@ internal class ChatController( private fun appendMessagesNotSeen() { emitViewState { - chatState.messagesNotSeenChanged( - if (chatState.isChatInBottom) 0 else chatState.messagesNotSeen + 1 - ) + val notSeenCount = chatState.messagesNotSeen + chatState.messagesNotSeenChanged(if (chatState.isChatInBottom) 0 else notSeenCount + 1) } } @@ -1087,98 +1094,47 @@ internal class ChatController( private fun appendOperatorMessage( currentChatItems: MutableList, - chatMessageInternal: ChatMessageInternal, - isLastItem: Boolean + chatMessageInternal: ChatMessageInternal ) { setLastOperatorItemChatHeadVisibility( currentChatItems, isOperatorChanged(currentChatItems, chatMessageInternal) ) - appendOperatorOrCustomCardItem(currentChatItems, chatMessageInternal, isLastItem) + appendOperatorOrCustomCardItem(currentChatItems, chatMessageInternal) appendOperatorAttachmentItems(currentChatItems, chatMessageInternal) setLastOperatorItemChatHeadVisibility(currentChatItems, true) } - private fun isOperatorChanged( - currentChatItems: List, - chatMessageInternal: ChatMessageInternal - ): Boolean { - if (currentChatItems.isEmpty()) return false - val lastItem = currentChatItems.last() - if (lastItem is OperatorChatItem) { - return !chatMessageInternal - .operatorId - .filter { it == lastItem.operatorId } - .isPresent - } - return false - } + private fun isOperatorChanged(currentChatItems: List, chatMessageInternal: ChatMessageInternal): Boolean = + (currentChatItems.lastOrNull() as? OperatorChatItem)?.operatorId != chatMessageInternal.operatorId private fun setLastOperatorItemChatHeadVisibility( currentChatItems: MutableList, showChatHead: Boolean ) { - if (currentChatItems.isNotEmpty()) { - when (val lastItem = currentChatItems.last()) { - is ResponseCardItem -> { - currentChatItems.remove(lastItem) - currentChatItems.add( - ResponseCardItem( - lastItem.id, - lastItem.operatorName, - lastItem.operatorProfileImgUrl, - showChatHead, - lastItem.content, - lastItem.operatorId, - lastItem.timestamp, - lastItem.singleChoiceOptions, - lastItem.choiceCardImageUrl - ) - ) - } + if (currentChatItems.isEmpty() || currentChatItems.last() !is OperatorChatItem) return - is OperatorMessageItem -> { - currentChatItems.remove(lastItem) - currentChatItems.add( - OperatorMessageItem( - lastItem.id, - lastItem.operatorName, - lastItem.operatorProfileImgUrl, - showChatHead, - lastItem.content, - lastItem.operatorId, - lastItem.timestamp - ) - ) - } + currentChatItems.apply { + when (val lastItem = last()) { + is ResponseCardItem -> this[lastIndex] = lastItem.copy(showChatHead = showChatHead) - is OperatorAttachmentItem -> { - currentChatItems.remove(lastItem) - currentChatItems.add( - OperatorAttachmentItem( - lastItem.id, - lastItem.viewType, - showChatHead, - lastItem.attachmentFile, - lastItem.operatorProfileImgUrl, - false, - false, - lastItem.operatorId, - lastItem.messageId, - lastItem.timestamp - ) - ) - } + is OperatorAttachmentItem -> this[lastIndex] = lastItem.copy(showChatHead = showChatHead) - is CustomCardItem -> { - currentChatItems.remove(lastItem) - currentChatItems.add( - CustomCardItem( - lastItem.message, - lastItem.viewType - ) - ) - } + is GvaResponseText -> this[lastIndex] = lastItem.copy(showChatHead = showChatHead) + + is GvaPersistentButtons -> this[lastIndex] = lastItem.copy(showChatHead = showChatHead) + + is GvaGalleryCards -> this[lastIndex] = lastItem.copy(showChatHead = showChatHead) + + is OperatorMessageItem -> this[lastIndex] = OperatorMessageItem( + lastItem.id, + lastItem.operatorName, + lastItem.operatorProfileImgUrl, + showChatHead, + lastItem.content, + lastItem.operatorId, + lastItem.timestamp + ) } } } @@ -1203,12 +1159,12 @@ internal class ChatController( viewType, false, file, - messageInternal.operatorImageUrl.orElse(chatState.operatorProfileImgUrl), - false, - false, - messageInternal.operatorId.orElse(UUID.randomUUID().toString()), - message.id, - message.timestamp + messageInternal.operatorImageUrl ?: chatState.operatorProfileImgUrl, + isFileExists = false, + isDownloading = false, + operatorId = messageInternal.operatorId ?: UUID.randomUUID().toString(), + messageId = message.id, + timestamp = message.timestamp ) ) } @@ -1217,20 +1173,36 @@ internal class ChatController( private fun appendOperatorOrCustomCardItem( currentChatItems: MutableList, - messageInternal: ChatMessageInternal, - isLastItem: Boolean + messageInternal: ChatMessageInternal ) { val message = messageInternal.chatMessage - if (message.content != EMPTY_MESSAGE) { - val viewType = customCardAdapterTypeUseCase.execute(message) - if (viewType != null) { - appendCustomCardItem(currentChatItems, message, viewType) - } else { - appendOperatorMessageItem(currentChatItems, messageInternal, isLastItem) + when { + isGvaUseCase(message) -> appendGvaMessageItem(currentChatItems, messageInternal) + message.content.isBlank() -> return + customCardAdapterTypeUseCase(message) != null -> appendCustomCardItem(currentChatItems, message, customCardAdapterTypeUseCase(message)!!) + else -> appendOperatorMessageItem(currentChatItems, messageInternal) + } + } + + private fun appendGvaMessageItem(currentChatItems: MutableList, messageInternal: ChatMessageInternal) { + + when (val gvaChatItem = mapGvaUseCase(messageInternal, chatState)) { + + is GvaQuickReplies -> { + currentChatItems.add(gvaChatItem.gvaResponseText) + addQuickReplyButtons(gvaChatItem.options) + } + + else -> { + currentChatItems.add(gvaChatItem as OperatorChatItem) } } } + private fun addQuickReplyButtons(options: List) { + emitViewState { chatState.copy(gvaQuickReplies = options) } + } + private fun appendCustomCardItem( currentChatItems: MutableList, message: ChatMessage, @@ -1248,17 +1220,16 @@ internal class ChatController( private fun appendOperatorMessageItem( currentChatItems: MutableList, - messageInternal: ChatMessageInternal, - isLastItem: Boolean + messageInternal: ChatMessageInternal ) { val message = messageInternal.chatMessage val messageAttachment = message.attachment val singleChoiceAttachmentOptions = getSingleChoiceAttachmentOptions(messageAttachment) - val operatorName = messageInternal.operatorName.orElse(chatState.formattedOperatorName) - val operatorImage = messageInternal.operatorImageUrl.orElse(chatState.operatorProfileImgUrl) - val operatorId = messageInternal.operatorId.orElse(UUID.randomUUID().toString()) + val operatorName = messageInternal.operatorName ?: chatState.formattedOperatorName + val operatorImage = messageInternal.operatorImageUrl ?: chatState.operatorProfileImgUrl + val operatorId = messageInternal.operatorId ?: UUID.randomUUID().toString() - val item = if (singleChoiceAttachmentOptions.isNullOrEmpty() || !isLastItem) { + val item = if (singleChoiceAttachmentOptions.isNullOrEmpty() || !messageInternal.isLatest) { OperatorMessageItem( message.id, operatorName, @@ -1303,9 +1274,9 @@ internal class ChatController( val newItems: MutableList = chatState.chatItems.toMutableList() val mediaUpgradeStartedTimerItem = MediaUpgradeStartedTimerItem( MediaUpgradeStartedTimerItem.Type.VIDEO, - chatState.mediaUpgradeStartedTimerItem.time + chatState.mediaUpgradeStartedTimerItem?.time ) - newItems.remove(chatState.mediaUpgradeStartedTimerItem) + chatState.mediaUpgradeStartedTimerItem?.also { newItems.remove(it) } newItems.add(mediaUpgradeStartedTimerItem) return@emitChatItems chatState.changeTimerItem(newItems, mediaUpgradeStartedTimerItem) @@ -1318,12 +1289,14 @@ internal class ChatController( override fun onNewFormattedTimerValue(formatedValue: String) { emitChatItems { if (chatState.isMediaUpgradeStarted) { - val index = - chatState.chatItems.indexOf(chatState.mediaUpgradeStartedTimerItem) + val index = chatState.mediaUpgradeStartedTimerItem?.let { + chatState.chatItems.indexOf(it) + } ?: -1 + if (index != -1) { val newItems: MutableList = chatState.chatItems.toMutableList() - val type = chatState.mediaUpgradeStartedTimerItem.type + val type = chatState.mediaUpgradeStartedTimerItem?.type newItems.removeAt(index) val mediaUpgradeStartedTimerItem = MediaUpgradeStartedTimerItem(type, formatedValue) @@ -1340,14 +1313,14 @@ internal class ChatController( } override fun onFormattedTimerCancelled() { - if (chatState.isMediaUpgradeStarted && - chatState.chatItems.contains(chatState.mediaUpgradeStartedTimerItem) - ) { - emitChatItems { - val newItems: MutableList = chatState.chatItems.toMutableList() - newItems.remove(chatState.mediaUpgradeStartedTimerItem) - - return@emitChatItems chatState.changeTimerItem(newItems, null) + chatState.apply { + if (isMediaUpgradeStarted && chatItems.contains(mediaUpgradeStartedTimerItem ?: return)) { + emitChatItems { + val newItems: MutableList = chatItems.toMutableList() + newItems.remove(mediaUpgradeStartedTimerItem) + + return@emitChatItems changeTimerItem(newItems, null) + } } } } @@ -1372,11 +1345,10 @@ internal class ChatController( emitChatItems { val modifiedItems: MutableList = chatState.chatItems.toMutableList() val indexInList = modifiedItems.indexOf(item) - modifiedItems.remove(item) if (indexInList >= 0) { - modifiedItems.add(indexInList, choiceCardItemWithSelected) + modifiedItems[indexInList] = choiceCardItemWithSelected } else { - Logger.e(TAG, "singleChoiceOptionClicked, ResponseCardItem is not in the list!") + Logger.e(TAG, "singleChoiceOptionClicked, ResponseCardItem is not on the list!") } return@emitChatItems chatState.changeItems(modifiedItems) @@ -1490,9 +1462,7 @@ internal class ChatController( currentItems: MutableList, newMessagesCount: Int ) { - newItems.forEachIndexed { index, message -> - appendHistoryChatItem(currentItems, message, index == newItems.lastIndex) - } + newItems.forEach { appendHistoryChatItem(currentItems, it) } if (isSecureEngagementUseCase() && !isQueueingOrOngoingEngagement) { emitChatTranscriptItems(currentItems, newMessagesCount) @@ -1535,24 +1505,7 @@ internal class ChatController( // and must be protected from race condition synchronized(this) { viewCallback = chatViewCallback } - chatState = ChatState.Builder() - .setFormattedOperatorName(null) - .setCompanyName(null) - .setQueueId(null) - .setVisitorContextAssetId(null) - .setIsVisible(false) - .setIntegratorChatStarted(false) - .setChatItems(ArrayList()) - .setLastTypedText(EMPTY_MESSAGE) - .setChatInputMode(ChatInputMode.ENABLED_NO_ENGAGEMENT) - .setIsAttachmentButtonNeeded(false) - .setIsAttachmentAllowed(true) - .setIsChatInBottom(true) - .setMessagesNotSeen(0) - .setPendingNavigationType(null) - .setUnsentMessages(ArrayList()) - .setIsOperatorTyping(false) - .createChatState() + chatState = ChatState() } @VisibleForTesting @@ -1586,8 +1539,8 @@ internal class ChatController( Logger.d(TAG, "unsentMessage sent!") } emitViewState { chatState.engagementStarted() } - // Loading chat history again on engagement start in case it was an-authenticated visitor that restored ongoing engagement - // Currently there is no direct way to know if Visitor is authenticated. + // Loading chat history again on engagement start in case it was an-authenticated visitor that restored an ongoing engagement + // Currently there is no direct way to know if the Visitor is authenticated. loadChatHistory() } @@ -1761,8 +1714,4 @@ internal class ChatController( fun isCallVisualizerOngoing(): Boolean { return isCallVisualizerUseCase() } - - companion object { - private const val EMPTY_MESSAGE = "" - } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.java b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.java deleted file mode 100644 index 18d3afe2a..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.glia.widgets.chat.domain; - -import androidx.annotation.Nullable; - -import com.glia.androidsdk.chat.ChatMessage; -import com.glia.widgets.chat.adapter.CustomCardAdapter; - -public class CustomCardAdapterTypeUseCase { - @Nullable - private final CustomCardAdapter adapter; - - public CustomCardAdapterTypeUseCase(@Nullable CustomCardAdapter adapter) { - this.adapter = adapter; - } - - @Nullable - public Integer execute(ChatMessage message) { - if (adapter == null || message.getMetadata() == null || message.getMetadata().length() == 0) { - return null; - } - return adapter.getChatAdapterViewType(message); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.kt new file mode 100644 index 000000000..196bd6c42 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCase.kt @@ -0,0 +1,11 @@ +package com.glia.widgets.chat.domain + +import com.glia.androidsdk.chat.ChatMessage +import com.glia.widgets.chat.adapter.CustomCardAdapter + +class CustomCardAdapterTypeUseCase(private val adapter: CustomCardAdapter?) { + operator fun invoke(message: ChatMessage): Int? = when { + adapter == null || message.metadata == null || message.metadata!!.length() == 0 -> null + else -> adapter.getChatAdapterViewType(message) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaLoadHistoryUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaLoadHistoryUseCase.kt index bb576679c..0b3bff44f 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaLoadHistoryUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaLoadHistoryUseCase.kt @@ -3,6 +3,7 @@ package com.glia.widgets.chat.domain import com.glia.widgets.chat.data.GliaChatRepository import com.glia.widgets.core.engagement.domain.MapOperatorUseCase import com.glia.widgets.core.engagement.domain.model.ChatHistoryResponse +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal import com.glia.widgets.core.secureconversations.SecureConversationsRepository import com.glia.widgets.core.secureconversations.domain.GetUnreadMessagesCountWithTimeoutUseCase import com.glia.widgets.core.secureconversations.domain.IsSecureEngagementUseCase @@ -30,10 +31,20 @@ internal class GliaLoadHistoryUseCase( getUnreadMessagesCountUseCase() ) { messages, count -> ChatHistoryResponse(messages, count) } - private fun loadHistoryAndMapOperator() = loadHistory() + private fun loadHistoryAndMapOperator(): Single> = loadHistory() .flatMapPublisher { Flowable.fromArray(*it) } - .concatMapSingle { mapOperatorUseCase(it) } + .concatMapSingle { mapOperatorUseCase(chatMessage = it, isHistory = true) } .toSortedList(Comparator.comparingLong { it.chatMessage.timestamp }) + .map { markLastItem(it) } + + private fun markLastItem(mutableList: MutableList): MutableList { + if (mutableList.isNotEmpty()) { + val lastItem = mutableList.removeLast() + mutableList.add(lastItem.copy(isLatest = true)) + } + + return mutableList + } private fun loadHistory() = Single.create { emitter -> loadHistory { messages, error -> diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.java b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.java index 2419c8eee..3abd70f7a 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.java @@ -11,8 +11,8 @@ import io.reactivex.subjects.PublishSubject; public class GliaOnMessageUseCase implements - GliaOnEngagementUseCase.Listener, - GliaChatRepository.MessageListener { + GliaOnEngagementUseCase.Listener, + GliaChatRepository.MessageListener { private final GliaOnEngagementUseCase onEngagementUseCase; private final GliaChatRepository messageRepository; @@ -20,9 +20,9 @@ public class GliaOnMessageUseCase implements private final PublishSubject publishSubject; public GliaOnMessageUseCase( - GliaChatRepository messageRepository, - GliaOnEngagementUseCase gliaOnEngagementUseCase, - MapOperatorUseCase mapOperatorUseCase) { + GliaChatRepository messageRepository, + GliaOnEngagementUseCase gliaOnEngagementUseCase, + MapOperatorUseCase mapOperatorUseCase) { this.onEngagementUseCase = gliaOnEngagementUseCase; this.messageRepository = messageRepository; this.mapOperatorUseCase = mapOperatorUseCase; @@ -32,9 +32,9 @@ public GliaOnMessageUseCase( public Observable execute() { this.onEngagementUseCase.execute(this); return publishSubject - .flatMapSingle(mapOperatorUseCase::invoke) - .doOnError(Throwable::printStackTrace) - .share(); + .flatMapSingle(chatMessage -> mapOperatorUseCase.invoke(chatMessage, false, true)) + .doOnError(Throwable::printStackTrace) + .share(); } public void unregisterListener() { diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt index 45a3e551e..9634b91c4 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt @@ -27,8 +27,11 @@ class GliaSendMessageUseCase( fun onCardMessageUpdated(message: ChatMessage) fun onMessageValidated() fun errorOperatorNotOnline(message: String) - fun errorMessageInvalid() fun error(ex: GliaException) + + fun errorMessageInvalid() { + // Currently, no need for this method, but have to keep it because it describes case in else branch + } } private val isSecureEngagement: Boolean diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/GetGvaTypeUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/GetGvaTypeUseCase.kt new file mode 100644 index 000000000..d6705f91f --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/GetGvaTypeUseCase.kt @@ -0,0 +1,10 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import org.json.JSONObject + +internal class GetGvaTypeUseCase { + + operator fun invoke(metadata: JSONObject): Gva.Type? = Gva.Type.values().firstOrNull { it.value == metadata.optString(Gva.Keys.TYPE) } + +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/IsGvaUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/IsGvaUseCase.kt new file mode 100644 index 000000000..7b83cb927 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/IsGvaUseCase.kt @@ -0,0 +1,12 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.androidsdk.chat.ChatMessage + +internal class IsGvaUseCase( + private val getGvaTypeUseCase: GetGvaTypeUseCase +) { + + operator fun invoke(chatMessage: ChatMessage): Boolean = chatMessage.metadata?.let { + getGvaTypeUseCase(it) + } != null +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCase.kt new file mode 100644 index 000000000..6c71b0e7a --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCase.kt @@ -0,0 +1,25 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.ChatState +import com.glia.widgets.chat.model.history.GvaGalleryCards +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal +import java.util.UUID + +internal class MapGvaGvaGalleryCardsUseCase( + private val parseGvaGalleryCardsUseCase: ParseGvaGalleryCardsUseCase +) { + operator fun invoke(chatMessage: ChatMessageInternal, chatState: ChatState): GvaGalleryCards { + val message = chatMessage.chatMessage + val metadata = message.metadata + + return GvaGalleryCards( + id = message.id, + galleryCards = parseGvaGalleryCardsUseCase(metadata), + showChatHead = false, + operatorId = chatMessage.operatorId ?: UUID.randomUUID().toString(), + timeStamp = message.timestamp, + operatorProfileImageUrl = chatMessage.operatorImageUrl ?: chatState.operatorProfileImgUrl, + operatorName = chatMessage.operatorName ?: chatState.formattedOperatorName + ) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCase.kt new file mode 100644 index 000000000..a60970d64 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCase.kt @@ -0,0 +1,25 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.ChatState +import com.glia.widgets.chat.model.history.GvaChatItem +import com.glia.widgets.chat.model.history.GvaQuickReplies +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal + +internal class MapGvaGvaQuickRepliesUseCase( + private val parseGvaButtonsUseCase: ParseGvaButtonsUseCase, + private val mapGvaResponseTextUseCase: MapGvaResponseTextUseCase +) { + operator fun invoke(chatMessage: ChatMessageInternal, chatState: ChatState): GvaChatItem { + val message = chatMessage.chatMessage + val metadata = message.metadata + + if (chatMessage.isHistory && !chatMessage.isLatest) { + return mapGvaResponseTextUseCase(chatMessage, chatState) + } + + return GvaQuickReplies( + gvaResponseText = mapGvaResponseTextUseCase(chatMessage, chatState), + options = parseGvaButtonsUseCase(metadata), + ) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCase.kt new file mode 100644 index 000000000..1107a15a6 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCase.kt @@ -0,0 +1,27 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.ChatState +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.history.GvaPersistentButtons +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal +import java.util.UUID + +internal class MapGvaPersistentButtonsUseCase( + private val parseGvaButtonsUseCase: ParseGvaButtonsUseCase +) { + operator fun invoke(chatMessage: ChatMessageInternal, chatState: ChatState): GvaPersistentButtons { + val message = chatMessage.chatMessage + val metadata = message.metadata + + return GvaPersistentButtons( + id = message.id, + content = metadata?.optString(Gva.Keys.CONTENT).orEmpty(), + options = parseGvaButtonsUseCase(metadata), + showChatHead = false, + operatorId = chatMessage.operatorId ?: UUID.randomUUID().toString(), + timeStamp = message.timestamp, + operatorProfileImageUrl = chatMessage.operatorImageUrl ?: chatState.operatorProfileImgUrl, + operatorName = chatMessage.operatorName ?: chatState.formattedOperatorName + ) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCase.kt new file mode 100644 index 000000000..ec2dd9180 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCase.kt @@ -0,0 +1,23 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.ChatState +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.history.GvaResponseText +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal +import java.util.UUID + +internal class MapGvaResponseTextUseCase { + operator fun invoke(chatMessage: ChatMessageInternal, chatState: ChatState): GvaResponseText { + val message = chatMessage.chatMessage + + return GvaResponseText( + id = message.id, + content = message.metadata?.optString(Gva.Keys.CONTENT).orEmpty(), + showChatHead = false, + operatorId = chatMessage.operatorId ?: UUID.randomUUID().toString(), + timeStamp = message.timestamp, + operatorProfileImageUrl = chatMessage.operatorImageUrl ?: chatState.operatorProfileImgUrl, + operatorName = chatMessage.operatorName ?: chatState.formattedOperatorName + ) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaUseCase.kt new file mode 100644 index 000000000..040b75580 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaUseCase.kt @@ -0,0 +1,23 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.ChatState +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.history.GvaChatItem +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal + +internal class MapGvaUseCase( + private val getGvaTypeUseCase: GetGvaTypeUseCase, + private val mapGvaResponseTextUseCase: MapGvaResponseTextUseCase, + private val mapGvaPersistentButtonsUseCase: MapGvaPersistentButtonsUseCase, + private val mapGvaGvaQuickRepliesUseCase: MapGvaGvaQuickRepliesUseCase, + private val mapGvaGvaGalleryCardsUseCase: MapGvaGvaGalleryCardsUseCase +) { + operator fun invoke(chatMessageInternal: ChatMessageInternal, chatState: ChatState): GvaChatItem = + when (getGvaTypeUseCase(chatMessageInternal.chatMessage.metadata!!)) { + Gva.Type.PLAIN_TEXT -> mapGvaResponseTextUseCase(chatMessageInternal, chatState) + Gva.Type.PERSISTENT_BUTTONS -> mapGvaPersistentButtonsUseCase(chatMessageInternal, chatState) + Gva.Type.QUICK_REPLIES -> mapGvaGvaQuickRepliesUseCase(chatMessageInternal, chatState) + Gva.Type.GALLERY_CARDS -> mapGvaGvaGalleryCardsUseCase(chatMessageInternal, chatState) + else -> throw IllegalArgumentException("metadata should contain on of the [${Gva.Type.values().joinToString { it.value }}] types") + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaButtonsUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaButtonsUseCase.kt new file mode 100644 index 000000000..3fa7327af --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaButtonsUseCase.kt @@ -0,0 +1,13 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaButton +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import org.json.JSONObject + +internal class ParseGvaButtonsUseCase(private val gson: Gson) { + operator fun invoke(metadata: JSONObject?): List = metadata?.optString(Gva.Keys.OPTIONS)?.let { + gson.fromJson(it, object : TypeToken>() {}.type) + } ?: emptyList() +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaGalleryCardsUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaGalleryCardsUseCase.kt new file mode 100644 index 000000000..82a680874 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/ParseGvaGalleryCardsUseCase.kt @@ -0,0 +1,13 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaGalleryCard +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import org.json.JSONObject + +internal class ParseGvaGalleryCardsUseCase(private val gson: Gson) { + operator fun invoke(metadata: JSONObject?): List = metadata?.optString(Gva.Keys.GALLERY_CARDS)?.let { + gson.fromJson(it, object : TypeToken>() {}.type) + } ?: emptyList() +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.java deleted file mode 100644 index 469f3cefa..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.java +++ /dev/null @@ -1,525 +0,0 @@ -package com.glia.widgets.chat.model; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.glia.widgets.chat.model.history.ChatItem; -import com.glia.widgets.chat.model.history.MediaUpgradeStartedTimerItem; -import com.glia.widgets.chat.model.history.OperatorStatusItem; -import com.glia.widgets.chat.model.history.VisitorMessageItem; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -public class ChatState { - public final boolean integratorChatStarted; - public final boolean isVisible; - public final boolean isChatInBottom; - public final Integer messagesNotSeen; - private final String formattedOperatorName; - public final String operatorProfileImgUrl; - public final String companyName; - public final String queueId; - public final String visitorContextAssetId; - public final MediaUpgradeStartedTimerItem mediaUpgradeStartedTimerItem; - public final List chatItems; - public final ChatInputMode chatInputMode; - public final String lastTypedText; - public final boolean engagementRequested; - public final String pendingNavigationType; - public final List unsentMessages; - public final OperatorStatusItem operatorStatusItem; - public final boolean showSendButton; - public final boolean isAttachmentButtonEnabled; - public final boolean isAttachmentButtonNeeded; - public final boolean isOperatorTyping; - public final boolean isAttachmentAllowed; - public final boolean isSecureMessaging; - - private ChatState(Builder builder) { - this.formattedOperatorName = builder.formattedOperatorName; - this.operatorProfileImgUrl = builder.operatorProfileImgUrl; - this.companyName = builder.companyName; - this.queueId = builder.queueId; - this.visitorContextAssetId = builder.visitorContextAssetId; - this.isVisible = builder.isVisible; - this.integratorChatStarted = builder.integratorChatStarted; - this.mediaUpgradeStartedTimerItem = builder.mediaUpgradeStartedTimerItem; - this.chatItems = Collections.unmodifiableList(builder.chatItems); - this.chatInputMode = builder.chatInputMode; - this.lastTypedText = builder.lastTypedText; - this.isChatInBottom = builder.isChatInBottom; - this.messagesNotSeen = builder.messagesNotSeen; - this.engagementRequested = builder.engagementRequested; - this.pendingNavigationType = builder.pendingNavigationType; - this.unsentMessages = builder.unsentMessages; - this.operatorStatusItem = builder.operatorStatusItem; - this.showSendButton = builder.showSendButton; - this.isOperatorTyping = builder.isOperatorTyping; - this.isAttachmentButtonEnabled = builder.isAttachmentButtonEnabled; - this.isAttachmentButtonNeeded = builder.isAttachmentButtonNeeded; - this.isAttachmentAllowed = builder.isAttachmentAllowed; - this.isSecureMessaging = builder.isSecureMessaging; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ChatState chatState = (ChatState) o; - return integratorChatStarted == chatState.integratorChatStarted && - isVisible == chatState.isVisible && - Objects.equals(formattedOperatorName, chatState.formattedOperatorName) && - Objects.equals(operatorProfileImgUrl, chatState.operatorProfileImgUrl) && - Objects.equals(companyName, chatState.companyName) && - Objects.equals(queueId, chatState.queueId) && - Objects.equals(visitorContextAssetId, chatState.visitorContextAssetId) && - Objects.equals(mediaUpgradeStartedTimerItem, chatState.mediaUpgradeStartedTimerItem) && - Objects.equals(chatInputMode, chatState.chatInputMode) && - Objects.equals(lastTypedText, chatState.lastTypedText) && - isChatInBottom == chatState.isChatInBottom && - engagementRequested == chatState.engagementRequested && - Objects.equals(pendingNavigationType, chatState.pendingNavigationType) && - Objects.equals(messagesNotSeen, chatState.messagesNotSeen) && - Objects.equals(operatorStatusItem, chatState.operatorStatusItem) && - Objects.equals(unsentMessages, chatState.unsentMessages) && - Objects.equals(chatItems, chatState.chatItems) && - showSendButton == chatState.showSendButton && - isOperatorTyping == chatState.isOperatorTyping && - isAttachmentButtonEnabled == chatState.isAttachmentButtonEnabled && - isAttachmentButtonNeeded == chatState.isAttachmentButtonNeeded && - isAttachmentAllowed == chatState.isAttachmentAllowed && - isSecureMessaging == chatState.isSecureMessaging; - } - - @Override - public int hashCode() { - return Objects.hash(integratorChatStarted, isVisible, isChatInBottom, formattedOperatorName, operatorProfileImgUrl, companyName, queueId, visitorContextAssetId, mediaUpgradeStartedTimerItem, chatItems, chatInputMode, lastTypedText, messagesNotSeen, engagementRequested, pendingNavigationType, unsentMessages, showSendButton, isOperatorTyping, isAttachmentButtonEnabled, isAttachmentButtonNeeded); - } - - @NonNull - @Override - public String toString() { - return "ChatState{" + - "integratorChatStarted=" + integratorChatStarted + - ", isVisible=" + isVisible + - ", operatorName='" + formattedOperatorName + '\'' + - ", operatorProfileImgUrl='" + operatorProfileImgUrl + '\'' + - ", companyName='" + companyName + '\'' + - ", queueId='" + queueId + '\'' + - ", visitorContextAssetId='" + visitorContextAssetId + '\'' + - ", mediaUpgradeStartedTimerItem=" + mediaUpgradeStartedTimerItem + - ", chatInputMode=" + chatInputMode + - ", lastTypedText: " + lastTypedText + - ", messagesNotSeen: " + messagesNotSeen + - ", isChatInBottom: " + isChatInBottom + - ", engagementRequested: " + engagementRequested + - ", pendingNavigationType: " + pendingNavigationType + - ", operatorStatusItem: " + operatorStatusItem + - ", unsentMessages: " + unsentMessages + - ", chatItems=" + chatItems + - ", showSendButton=" + showSendButton + - ", isOperatorTyping=" + isOperatorTyping + - ", isAttachmentButtonEnabled=" + isAttachmentButtonEnabled + - ", isAttachmentButtonEnabled=" + isAttachmentButtonEnabled + - ", isAttachmentAllowed=" + isAttachmentAllowed + - ", isSecureMessaging=" + isSecureMessaging + - '}'; - } - - public boolean isOperatorOnline() { - return formattedOperatorName != null; - } - - public boolean isMediaUpgradeStarted() { - return mediaUpgradeStartedTimerItem != null; - } - - public boolean isAudioCallStarted() { - return isMediaUpgradeStarted() && - mediaUpgradeStartedTimerItem.type == MediaUpgradeStartedTimerItem.Type.AUDIO; - } - - public boolean showMessagesUnseenIndicator() { - return !isChatInBottom && messagesNotSeen != null && messagesNotSeen > 0; - } - - public boolean isAttachmentButtonVisible() { - return isAttachmentButtonNeeded && isAttachmentAllowed; - } - - public ChatState initChat(String companyName, - String queueId, - String visitorContextAssetId) { - return new Builder() - .copyFrom(this) - .setIntegratorChatStarted(true) - .setCompanyName(companyName) - .setQueueId(queueId) - .setVisitorContextAssetId(visitorContextAssetId) - .setIsVisible(true) - .setShowSendButton(false) - .setIsAttachmentButtonEnabled(true) - .setIsAttachmentAllowed(true) - .createChatState(); - } - - public String getFormattedOperatorName() { - return formattedOperatorName; - } - - public ChatState queueingStarted(OperatorStatusItem operatorStatusItem) { - return new Builder() - .copyFrom(this) - .setFormattedOperatorName(null) - .setOperatorProfileImgUrl(null) - .setChatInputMode(ChatInputMode.ENABLED) - .setEngagementRequested(true) - .setOperatorStatusItem(operatorStatusItem) - .createChatState(); - } - - public ChatState setSecureMessagingState() { - return new Builder() - .copyFrom(this) - .setSecureMessaging(true) - .enableChatPanel() - .createChatState(); - } - - public ChatState setLiveChatState() { - return new Builder() - .copyFrom(this) - .setSecureMessaging(false) - .createChatState(); - } - - public ChatState allowSendAttachmentStateChanged(boolean isAttachmentAllowed) { - return new Builder() - .copyFrom(this) - .setIsAttachmentAllowed(isAttachmentAllowed) - .createChatState(); - } - - public ChatState engagementStarted() { - return new Builder() - .copyFrom(this) - .enableChatPanel() - .setEngagementRequested(true) - .createChatState(); - } - - public ChatState transferring() { - return new Builder() - .copyFrom(this) - .setFormattedOperatorName(null) - .setOperatorProfileImgUrl(null) - .setEngagementRequested(true) - .setOperatorStatusItem(OperatorStatusItem.TransferringStatusItem()) - .disableChatPanel() - .createChatState(); - } - - public ChatState operatorConnected(String formattedOperatorName, @Nullable String operatorProfileImgUrl) { - return new Builder() - .copyFrom(this) - .setFormattedOperatorName(formattedOperatorName) - .setOperatorProfileImgUrl(operatorProfileImgUrl) -// .enableChatPanel() // Why is this here? - .createChatState(); - } - - public ChatState historyLoaded(List chatItems) { - return new Builder() - .copyFrom(this) - .setChatInputMode(ChatInputMode.ENABLED_NO_ENGAGEMENT) - .setIsAttachmentButtonNeeded(false) - .setChatItems(chatItems) - .createChatState(); - } - - public ChatState changeItems(List newItems) { - return new Builder() - .copyFrom(this) - .setChatItems(newItems) - .createChatState(); - } - - public ChatState changeTimerItem( - List newItems, - MediaUpgradeStartedTimerItem mediaUpgradeStartedTimerItem - ) { - return new Builder() - .copyFrom(changeItems(newItems)) - .setMediaUpgradeStartedItem(mediaUpgradeStartedTimerItem) - .createChatState(); - } - - public ChatState changeVisibility(boolean isVisible) { - return new Builder() - .copyFrom(this) - .setIsVisible(isVisible) - .createChatState(); - } - - public ChatState setLastTypedText(String text) { - return new Builder() - .copyFrom(this) - .setLastTypedText(text) - .createChatState(); - } - - public ChatState chatInputModeChanged(ChatInputMode chatInputMode) { - return new Builder() - .copyFrom(this) - .setChatInputMode(chatInputMode) - .setIsAttachmentButtonNeeded(chatInputMode == ChatInputMode.ENABLED) - .createChatState(); - } - - public ChatState isInBottomChanged(boolean isChatInBottom) { - return new Builder() - .copyFrom(this) - .setIsChatInBottom(isChatInBottom) - .createChatState(); - } - - public ChatState messagesNotSeenChanged(int messagesNotSeen) { - return new Builder() - .copyFrom(this) - .setMessagesNotSeen(messagesNotSeen) - .createChatState(); - } - - public ChatState setPendingNavigationType(String pendingNavigationType) { - return new Builder() - .copyFrom(this) - .setPendingNavigationType(pendingNavigationType) - .createChatState(); - } - - public ChatState changeUnsentMessages(List unsentMessages) { - return new Builder() - .copyFrom(this) - .setUnsentMessages(Collections.unmodifiableList(unsentMessages)) - .createChatState(); - } - - public ChatState setShowSendButton(boolean isShow) { - return new Builder() - .copyFrom(this) - .setShowSendButton(isShow) - .createChatState(); - } - - public ChatState setIsOperatorTyping(boolean isOperatorTyping) { - return new Builder() - .copyFrom(this) - .setIsOperatorTyping(isOperatorTyping) - .createChatState(); - } - - public ChatState setIsAttachmentButtonEnabled(boolean isAttachmentButtonEnabled) { - return new Builder() - .copyFrom(this) - .setIsAttachmentButtonEnabled(isAttachmentButtonEnabled) - .createChatState(); - } - - public ChatState stop() { - return new Builder() - .copyFrom(this) - .setFormattedOperatorName(null) - .setOperatorProfileImgUrl(null) - .setIsVisible(false) - .setIntegratorChatStarted(false) - .setIsAttachmentButtonNeeded(false) - .createChatState(); - } - - public static class Builder { - public boolean isOperatorTyping; - public boolean isAttachmentButtonEnabled; - public boolean isAttachmentButtonNeeded; - private boolean isChatInBottom; - private String formattedOperatorName; - private String operatorProfileImgUrl; - private String companyName; - private String queueId; - private boolean isVisible; - private boolean integratorChatStarted; - private MediaUpgradeStartedTimerItem mediaUpgradeStartedTimerItem; - private List chatItems; - private ChatInputMode chatInputMode; - private String lastTypedText; - private Integer messagesNotSeen; - private boolean engagementRequested; - private String pendingNavigationType; - private List unsentMessages; - private OperatorStatusItem operatorStatusItem; - private boolean showSendButton; - private boolean isAttachmentAllowed; - private String visitorContextAssetId; - private boolean isSecureMessaging; - - public Builder copyFrom(ChatState chatState) { - isChatInBottom = chatState.isChatInBottom; - formattedOperatorName = chatState.formattedOperatorName; - operatorProfileImgUrl = chatState.operatorProfileImgUrl; - companyName = chatState.companyName; - queueId = chatState.queueId; - visitorContextAssetId = chatState.visitorContextAssetId; - isVisible = chatState.isVisible; - integratorChatStarted = chatState.integratorChatStarted; - mediaUpgradeStartedTimerItem = chatState.mediaUpgradeStartedTimerItem; - chatItems = chatState.chatItems; - chatInputMode = chatState.chatInputMode; - lastTypedText = chatState.lastTypedText; - messagesNotSeen = chatState.messagesNotSeen; - engagementRequested = chatState.engagementRequested; - pendingNavigationType = chatState.pendingNavigationType; - unsentMessages = chatState.unsentMessages; - operatorStatusItem = chatState.operatorStatusItem; - showSendButton = chatState.showSendButton; - isOperatorTyping = chatState.isOperatorTyping; - isAttachmentButtonEnabled = chatState.isAttachmentButtonEnabled; - isAttachmentButtonNeeded = chatState.isAttachmentButtonNeeded; - isAttachmentAllowed = chatState.isAttachmentAllowed; - isSecureMessaging = chatState.isSecureMessaging; - return this; - } - - public Builder setFormattedOperatorName(String formattedOperatorName) { - this.formattedOperatorName = formattedOperatorName; - return this; - } - - public Builder setOperatorProfileImgUrl(String operatorProfileImgUrl) { - this.operatorProfileImgUrl = operatorProfileImgUrl; - return this; - } - - public Builder setCompanyName(String companyName) { - this.companyName = companyName; - return this; - } - - public Builder setQueueId(String queueId) { - this.queueId = queueId; - return this; - } - - public Builder setIsVisible(boolean isVisible) { - this.isVisible = isVisible; - return this; - } - - public Builder setIntegratorChatStarted(boolean integratorChatStarted) { - this.integratorChatStarted = integratorChatStarted; - return this; - } - - public Builder setMediaUpgradeStartedItem(MediaUpgradeStartedTimerItem mediaUpgradeStartedItem) { - this.mediaUpgradeStartedTimerItem = mediaUpgradeStartedItem; - return this; - } - - public Builder setChatItems(List chatItems) { - this.chatItems = chatItems; - return this; - } - - public Builder setChatInputMode(ChatInputMode chatInputMode) { - this.chatInputMode = chatInputMode; - return this; - } - - public Builder setLastTypedText(String lastTypedText) { - this.lastTypedText = lastTypedText; - return this; - } - - public Builder setIsChatInBottom(boolean isChatInBottom) { - this.isChatInBottom = isChatInBottom; - return this; - } - - public Builder setMessagesNotSeen(Integer messagesNotSeen) { - this.messagesNotSeen = messagesNotSeen; - return this; - } - - public Builder setEngagementRequested(boolean engagementRequested) { - this.engagementRequested = engagementRequested; - return this; - } - - public Builder setPendingNavigationType(String pendingNavigationType) { - this.pendingNavigationType = pendingNavigationType; - return this; - } - - public Builder setUnsentMessages(List unsentMessages) { - this.unsentMessages = unsentMessages; - return this; - } - - public Builder setOperatorStatusItem(OperatorStatusItem operatorStatusItem) { - this.operatorStatusItem = operatorStatusItem; - return this; - } - - public Builder setShowSendButton(boolean isShow) { - this.showSendButton = isShow; - return this; - } - - public Builder setIsOperatorTyping(boolean isOperatorTyping) { - this.isOperatorTyping = isOperatorTyping; - return this; - } - - public Builder setIsAttachmentButtonEnabled(boolean isAttachmentButtonEnabled) { - this.isAttachmentButtonEnabled = isAttachmentButtonEnabled; - return this; - } - - public Builder setIsAttachmentButtonNeeded(boolean isAttachmentButtonNeeded) { - this.isAttachmentButtonNeeded = isAttachmentButtonNeeded; - return this; - } - - public Builder setIsAttachmentAllowed(boolean isAttachmentAllowed) { - this.isAttachmentAllowed = isAttachmentAllowed; - return this; - } - - public ChatState createChatState() { - return new ChatState(this); - } - - public Builder setVisitorContextAssetId(String visitorContextAssetId) { - this.visitorContextAssetId = visitorContextAssetId; - return this; - } - - public Builder setSecureMessaging(boolean secureMessaging) { - this.isSecureMessaging = secureMessaging; - return this; - } - - public Builder enableChatPanel() { - setChatInputMode(ChatInputMode.ENABLED); - setIsAttachmentButtonNeeded(true); - return this; - } - - public Builder disableChatPanel() { - setChatInputMode(ChatInputMode.DISABLED); - setShowSendButton(false); - setIsAttachmentButtonNeeded(false); - return this; - } - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt new file mode 100644 index 000000000..04ec9fb66 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt @@ -0,0 +1,145 @@ +package com.glia.widgets.chat.model + +import com.glia.widgets.chat.model.history.ChatItem +import com.glia.widgets.chat.model.history.MediaUpgradeStartedTimerItem +import com.glia.widgets.chat.model.history.OperatorStatusItem +import com.glia.widgets.chat.model.history.VisitorMessageItem + +internal data class ChatState( + val integratorChatStarted: Boolean = false, + val isVisible: Boolean = false, + val isChatInBottom: Boolean = true, + val messagesNotSeen: Int = 0, + val formattedOperatorName: String? = null, + val operatorProfileImgUrl: String? = null, + val companyName: String? = null, + val queueId: String? = null, + val visitorContextAssetId: String? = null, + val mediaUpgradeStartedTimerItem: MediaUpgradeStartedTimerItem? = null, + val chatItems: List = emptyList(), + val chatInputMode: ChatInputMode = ChatInputMode.ENABLED_NO_ENGAGEMENT, + val lastTypedText: String = "", + val engagementRequested: Boolean = false, + val pendingNavigationType: String? = null, + val unsentMessages: List = emptyList(), + val operatorStatusItem: OperatorStatusItem? = null, + val showSendButton: Boolean = false, + val isAttachmentButtonEnabled: Boolean = false, + val isAttachmentButtonNeeded: Boolean = false, + val isOperatorTyping: Boolean = false, + val isAttachmentAllowed: Boolean = true, + val isSecureMessaging: Boolean = false, + val gvaQuickReplies: List = emptyList() +) { + + val isOperatorOnline: Boolean get() = formattedOperatorName != null + + val isMediaUpgradeStarted: Boolean get() = mediaUpgradeStartedTimerItem != null + + val isAudioCallStarted: Boolean + get() = isMediaUpgradeStarted && mediaUpgradeStartedTimerItem?.type == MediaUpgradeStartedTimerItem.Type.AUDIO + + val showMessagesUnseenIndicator: Boolean get() = !isChatInBottom && messagesNotSeen > 0 + + val isAttachmentButtonVisible: Boolean get() = isAttachmentButtonNeeded && isAttachmentAllowed + + fun initChat(companyName: String?, queueId: String?, visitorContextAssetId: String?): ChatState = copy( + integratorChatStarted = true, + companyName = companyName, + queueId = queueId, + visitorContextAssetId = visitorContextAssetId, + isVisible = true, + showSendButton = false, + isAttachmentButtonEnabled = true, + isAttachmentAllowed = true + ) + + fun queueingStarted(operatorStatusItem: OperatorStatusItem?): ChatState = copy( + formattedOperatorName = null, + operatorProfileImgUrl = null, + chatInputMode = ChatInputMode.ENABLED, + engagementRequested = true, + operatorStatusItem = operatorStatusItem + ) + + fun setSecureMessagingState(): ChatState = copy( + isSecureMessaging = true, + chatInputMode = ChatInputMode.ENABLED, + isAttachmentButtonNeeded = true + ) + + fun setLiveChatState(): ChatState = copy(isSecureMessaging = false) + + fun allowSendAttachmentStateChanged(isAttachmentAllowed: Boolean): ChatState = copy(isAttachmentAllowed = isAttachmentAllowed) + + fun engagementStarted(): ChatState = copy( + chatInputMode = ChatInputMode.ENABLED, + isAttachmentButtonNeeded = true, + engagementRequested = true + ) + + fun transferring(): ChatState = copy( + formattedOperatorName = null, + operatorProfileImgUrl = null, + engagementRequested = false, + operatorStatusItem = OperatorStatusItem.TransferringStatusItem(), + chatInputMode = ChatInputMode.DISABLED, + showSendButton = false, + isAttachmentButtonNeeded = false + ) + + fun operatorConnected( + formattedOperatorName: String?, + operatorProfileImgUrl: String? + ): ChatState = copy( + formattedOperatorName = formattedOperatorName, + operatorProfileImgUrl = operatorProfileImgUrl + ) + + fun historyLoaded(chatItems: List): ChatState = copy( + chatInputMode = ChatInputMode.ENABLED_NO_ENGAGEMENT, + isAttachmentButtonNeeded = false, + chatItems = chatItems + ) + + fun changeItems(newItems: List): ChatState = copy(chatItems = newItems) + + fun changeTimerItem( + newItems: List, + mediaUpgradeStartedTimerItem: MediaUpgradeStartedTimerItem? + ): ChatState = copy( + chatItems = newItems, + mediaUpgradeStartedTimerItem = mediaUpgradeStartedTimerItem + ) + + fun changeVisibility(isVisible: Boolean): ChatState = copy(isVisible = isVisible) + + fun setLastTypedText(text: String): ChatState = copy(lastTypedText = text) + + fun chatInputModeChanged(chatInputMode: ChatInputMode): ChatState = copy( + chatInputMode = chatInputMode, + isAttachmentButtonNeeded = chatInputMode == ChatInputMode.ENABLED + ) + + fun isInBottomChanged(isChatInBottom: Boolean): ChatState = copy(isChatInBottom = isChatInBottom) + + fun messagesNotSeenChanged(messagesNotSeen: Int): ChatState = copy(messagesNotSeen = messagesNotSeen) + + fun setPendingNavigationType(pendingNavigationType: String?): ChatState = copy(pendingNavigationType = pendingNavigationType) + + fun changeUnsentMessages(unsentMessages: List): ChatState = copy(unsentMessages = unsentMessages) + + fun setShowSendButton(isShow: Boolean): ChatState = copy(showSendButton = isShow) + + fun setIsOperatorTyping(isOperatorTyping: Boolean): ChatState = copy(isOperatorTyping = isOperatorTyping) + + fun setIsAttachmentButtonEnabled(isAttachmentButtonEnabled: Boolean): ChatState = copy(isAttachmentButtonEnabled = isAttachmentButtonEnabled) + + fun stop(): ChatState = copy( + formattedOperatorName = null, + operatorProfileImgUrl = null, + isVisible = false, + integratorChatStarted = false, + isAttachmentButtonNeeded = false + ) +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt new file mode 100644 index 000000000..67469785c --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt @@ -0,0 +1,70 @@ +package com.glia.widgets.chat.model + +import androidx.annotation.StringDef +import com.google.gson.annotations.SerializedName + +internal object Gva { + + object Keys { + const val TYPE = "type" + const val CONTENT = "content" + const val OPTIONS = "options" + const val GALLERY_CARDS = "galleryCards" + } + + enum class Type(val value: String) { + PLAIN_TEXT("plainText"), + PERSISTENT_BUTTONS("persistentButtons"), + QUICK_REPLIES("quickReplies"), + GALLERY_CARDS("galleryCards") + } + + @StringDef( + UrlTarget.MODAL, + UrlTarget.SELF, + UrlTarget.BLANK + ) + @Retention(AnnotationRetention.SOURCE) + annotation class UrlTarget { + companion object { + const val MODAL = "modal" + const val SELF = "self" + const val BLANK = "blank" + } + } +} + +internal data class GvaButton( + @SerializedName("text") + val text: String = "", + @SerializedName("value") + val value: String = "", + @SerializedName("url") + val url: String? = null, + @Gva.UrlTarget + @SerializedName("urlTarget") + val urlTarget: String? = null, + @SerializedName("destinationPbBroadcastEvent") + val destinationPbBroadcastEvent: String? = null, + @SerializedName("transferPhoneNumber") + val transferPhoneNumber: String? = null +) { + val isPostBack: Boolean + get() = url.isNullOrBlank() + + val isBroadCastEvent: Boolean + get() = !destinationPbBroadcastEvent.isNullOrBlank() + +// fun toResponse(): SingleChoiceAttachment = SingleChoiceAttachment.from(text, value) TODO should be available with core sdk's next release. +} + +internal data class GvaGalleryCard( + @SerializedName("title") + val title: String = "", + @SerializedName("subtitle") + val subtitle: String? = null, + @SerializedName("imageUrl") + val imageUrl: String? = null, + @SerializedName("options") + val options: List = listOf() +) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/GvaChatItems.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/GvaChatItems.kt new file mode 100644 index 000000000..9c2a74eba --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/GvaChatItems.kt @@ -0,0 +1,46 @@ +package com.glia.widgets.chat.model.history + +import com.glia.widgets.chat.adapter.ChatAdapter +import com.glia.widgets.chat.model.GvaButton +import com.glia.widgets.chat.model.GvaGalleryCard + +sealed interface GvaChatItem + +internal data class GvaResponseText( + private val id: String = "", + val content: String = "", + private val showChatHead: Boolean = false, + private val operatorId: String = "", + val timeStamp: Long = 0L, + val operatorProfileImageUrl: String? = null, + val operatorName: String? = null +) : GvaChatItem, + OperatorChatItem(id, ChatAdapter.GVA_RESPONSE_TEXT_TYPE, showChatHead, operatorProfileImageUrl, operatorId, id, timeStamp) + +internal data class GvaPersistentButtons( + private val id: String = "", + val content: String = "", + val options: List = listOf(), + private val showChatHead: Boolean = false, + private val operatorId: String = "", + val timeStamp: Long = 0L, + val operatorProfileImageUrl: String? = null, + val operatorName: String? = null +) : GvaChatItem, + OperatorChatItem(id, ChatAdapter.GVA_PERSISTENT_BUTTONS_TYPE, showChatHead, operatorProfileImageUrl, operatorId, id, timeStamp) + +internal data class GvaGalleryCards( + private val id: String = "", + val galleryCards: List, + private val showChatHead: Boolean = false, + private val operatorId: String = "", + val timeStamp: Long = 0L, + val operatorProfileImageUrl: String? = null, + val operatorName: String? = null +) : GvaChatItem, + OperatorChatItem(id, ChatAdapter.GVA_GALLERY_CARDS_TYPE, showChatHead, operatorProfileImageUrl, operatorId, id, timeStamp) + +internal data class GvaQuickReplies( + val gvaResponseText: GvaResponseText, + val options: List = listOf(), +) : GvaChatItem diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/MediaUpgradeStartedTimerItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/MediaUpgradeStartedTimerItem.java index 013e754c4..84769bb99 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/MediaUpgradeStartedTimerItem.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/MediaUpgradeStartedTimerItem.java @@ -1,11 +1,13 @@ package com.glia.widgets.chat.model.history; +import androidx.annotation.NonNull; + import com.glia.widgets.chat.adapter.ChatAdapter; import java.util.Objects; public class MediaUpgradeStartedTimerItem extends ChatItem { - public final static String ID = "media_upgrade_item"; + public static final String ID = "media_upgrade_item"; public final MediaUpgradeStartedTimerItem.Type type; public final String time; @@ -15,16 +17,13 @@ public MediaUpgradeStartedTimerItem(MediaUpgradeStartedTimerItem.Type type, Stri this.time = time; } - public enum Type { - AUDIO, VIDEO - } - + @NonNull @Override public String toString() { return "MediaUpgradeStartedTimerItem{" + - "type=" + type + - ", time='" + time + '\'' + - '}'; + "type=" + type + + ", time='" + time + '\'' + + '}'; } @Override @@ -34,11 +33,15 @@ public boolean equals(Object o) { if (!super.equals(o)) return false; MediaUpgradeStartedTimerItem that = (MediaUpgradeStartedTimerItem) o; return type == that.type && - Objects.equals(time, that.time); + Objects.equals(time, that.time); } @Override public int hashCode() { return Objects.hash(super.hashCode(), type, time); } + + public enum Type { + AUDIO, VIDEO + } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorAttachmentItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorAttachmentItem.java deleted file mode 100644 index 1c13473e8..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorAttachmentItem.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.glia.widgets.chat.model.history; - -import androidx.annotation.NonNull; - -import com.glia.androidsdk.chat.AttachmentFile; -import com.glia.widgets.helper.Utils; - -import java.util.Objects; - -public class OperatorAttachmentItem extends OperatorChatItem { - - public final AttachmentFile attachmentFile; - public final boolean isFileExists; - public final boolean isDownloading; - - public OperatorAttachmentItem( - String chatItemId, - int viewType, - boolean showChatHead, - AttachmentFile attachmentFile, - String operatorProfileImgUrl, - boolean isFileExists, - boolean isDownloading, - String operatorId, - String messageId, - long timestamp - ) { - super(chatItemId, viewType, showChatHead, operatorProfileImgUrl, operatorId, messageId, timestamp); - this.attachmentFile = attachmentFile; - this.isFileExists = isFileExists; - this.isDownloading = isDownloading; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof OperatorAttachmentItem)) return false; - if (!super.equals(o)) return false; - OperatorAttachmentItem that = (OperatorAttachmentItem) o; - return isFileExists == that.isFileExists && isDownloading == that.isDownloading && Objects.equals(attachmentFile, that.attachmentFile); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), attachmentFile, isFileExists, isDownloading); - } - - @Override - public String toString() { - return "OperatorAttachmentItem{" + - "attachmentFile=" + attachmentFile + - ", isFileExists=" + isFileExists + - ", isDownloading=" + isDownloading + - ", showChatHead=" + showChatHead + - ", operatorProfileImgUrl='" + operatorProfileImgUrl + '\'' + - ", operatorId='" + operatorId + '\'' + - "} " + super.toString(); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorAttachmentItem.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorAttachmentItem.kt new file mode 100644 index 000000000..e8f6346b9 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorAttachmentItem.kt @@ -0,0 +1,16 @@ +package com.glia.widgets.chat.model.history + +import com.glia.androidsdk.chat.AttachmentFile + +data class OperatorAttachmentItem( + private val chatItemId: String?, + private val viewType: Int, + private val showChatHead: Boolean, + val attachmentFile: AttachmentFile, + private val operatorProfileImgUrl: String?, + val isFileExists: Boolean, + val isDownloading: Boolean, + private val operatorId: String?, + private val messageId: String?, + private val timestamp: Long +) : OperatorChatItem(chatItemId, viewType, showChatHead, operatorProfileImgUrl, operatorId, messageId, timestamp) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorChatItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorChatItem.java index 3d17cbc1c..738722d39 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorChatItem.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorChatItem.java @@ -1,20 +1,38 @@ package com.glia.widgets.chat.model.history; +import com.glia.widgets.chat.adapter.ChatAdapter; + +import org.jetbrains.annotations.Nullable; + import java.util.Objects; public abstract class OperatorChatItem extends LinkedChatItem implements ServerChatItem { - public final boolean showChatHead; - public final String operatorProfileImgUrl; - public final String operatorId; + private final boolean showChatHead; + private final String operatorProfileImgUrl; + private final String operatorId; - protected OperatorChatItem(String id, int viewType, boolean showChatHead, String operatorProfileImgUrl, String operatorId, String messageId, long timestamp) { + protected OperatorChatItem(String id, @ChatAdapter.Type int viewType, boolean showChatHead, String operatorProfileImgUrl, String operatorId, String messageId, long timestamp) { super(id, viewType, messageId, timestamp); this.showChatHead = showChatHead; this.operatorProfileImgUrl = operatorProfileImgUrl; this.operatorId = operatorId; } + public boolean getShowChatHead() { + return showChatHead; + } + + @Nullable + public String getOperatorProfileImgUrl() { + return operatorProfileImgUrl; + } + + @Nullable + public String getOperatorId() { + return operatorId; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -22,8 +40,8 @@ public boolean equals(Object o) { if (!super.equals(o)) return false; OperatorChatItem that = (OperatorChatItem) o; return showChatHead == that.showChatHead - && Objects.equals(operatorProfileImgUrl, that.operatorProfileImgUrl) - && Objects.equals(operatorId, that.operatorId); + && Objects.equals(operatorProfileImgUrl, that.operatorProfileImgUrl) + && Objects.equals(operatorId, that.operatorId); } @Override diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorMessageItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorMessageItem.java index 3efe2a5ca..46758d3fd 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorMessageItem.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/OperatorMessageItem.java @@ -7,23 +7,31 @@ import java.util.Objects; public class OperatorMessageItem extends OperatorChatItem { - public final String operatorName; - public final String content; + private final String operatorName; + private final String content; public OperatorMessageItem( - String id, - String operatorName, - String operatorProfileImgUrl, - boolean showChatHead, - String content, - String operatorId, - long timestamp + String id, + String operatorName, + String operatorProfileImgUrl, + boolean showChatHead, + String content, + String operatorId, + long timestamp ) { super(id, ChatAdapter.OPERATOR_MESSAGE_VIEW_TYPE, showChatHead, operatorProfileImgUrl, operatorId, id, timestamp); this.operatorName = operatorName; this.content = content; } + public String getOperatorName() { + return operatorName; + } + + public String getContent() { + return content; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -31,7 +39,7 @@ public boolean equals(Object o) { if (!super.equals(o)) return false; OperatorMessageItem that = (OperatorMessageItem) o; return Objects.equals(operatorName, that.operatorName) - && Objects.equals(content, that.content); + && Objects.equals(content, that.content); } @Override @@ -43,11 +51,11 @@ public int hashCode() { @Override public String toString() { return "OperatorMessageItem{" + - "showChatHead=" + showChatHead + - ", operatorProfileImgUrl='" + operatorProfileImgUrl + '\'' + - ", operatorId='" + operatorId + '\'' + - ", operatorName='" + operatorName + '\'' + - ", content='" + content + '\'' + - "} " + super.toString(); + "showChatHead=" + getShowChatHead() + + ", operatorProfileImgUrl='" + getOperatorProfileImgUrl() + '\'' + + ", operatorId='" + getOperatorId() + '\'' + + ", operatorName='" + operatorName + '\'' + + ", content='" + content + '\'' + + "} " + super.toString(); } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ResponseCardItem.java b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ResponseCardItem.java deleted file mode 100644 index d4985ae8f..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ResponseCardItem.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.glia.widgets.chat.model.history; - -import androidx.annotation.NonNull; - -import com.glia.androidsdk.chat.SingleChoiceOption; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -public class ResponseCardItem extends OperatorMessageItem { - @NonNull - public final List singleChoiceOptions; - public final String choiceCardImageUrl; - - public ResponseCardItem( - @NonNull String id, - String operatorName, - String operatorProfileImgUrl, - boolean showChatHead, - String content, - String operatorId, - long timestamp, - //Must not be empty, otherwise it is OperatorMessageItem - @NonNull List singleChoiceOptions, - String choiceCardImageUrl - ) { - super(id, operatorName, operatorProfileImgUrl, showChatHead, content, operatorId, timestamp); - - assert !singleChoiceOptions.isEmpty(); - this.singleChoiceOptions = Collections.unmodifiableList(singleChoiceOptions); - this.choiceCardImageUrl = choiceCardImageUrl; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof ResponseCardItem)) return false; - if (!super.equals(o)) return false; - ResponseCardItem that = (ResponseCardItem) o; - return singleChoiceOptions.equals(that.singleChoiceOptions) && Objects.equals(choiceCardImageUrl, that.choiceCardImageUrl); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), singleChoiceOptions, choiceCardImageUrl); - } - - @NonNull - @Override - public String toString() { - return "ResponseCardItem{" + - "singleChoiceOptions=" + singleChoiceOptions + - ", choiceCardImageUrl='" + choiceCardImageUrl + '\'' + - "} " + super.toString(); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ResponseCardItem.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ResponseCardItem.kt new file mode 100644 index 000000000..f2e632bb7 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/history/ResponseCardItem.kt @@ -0,0 +1,20 @@ +package com.glia.widgets.chat.model.history + +import com.glia.androidsdk.chat.SingleChoiceOption + +data class ResponseCardItem( + private val id: String, + private val operatorName: String?, + private val operatorProfileImgUrl: String?, + private val showChatHead: Boolean, + private val content: String?, + private val operatorId: String?, + private val timestamp: Long, + val singleChoiceOptions: List, + val choiceCardImageUrl: String? +) : OperatorMessageItem(id, operatorName, operatorProfileImgUrl, showChatHead, content, operatorId, timestamp) { + + init { + require(singleChoiceOptions.isNotEmpty()) { "Response card should have at least one `SingleChoiceOption`" } + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt index 651c7b335..6fc33d1a3 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt @@ -1,28 +1,26 @@ package com.glia.widgets.core.engagement.domain -import com.glia.androidsdk.Operator import com.glia.androidsdk.chat.Chat import com.glia.androidsdk.chat.ChatMessage import com.glia.androidsdk.chat.OperatorMessage import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal import io.reactivex.Single +import kotlin.jvm.optionals.getOrNull internal class MapOperatorUseCase(private val getOperatorUseCase: GetOperatorUseCase) { - operator fun invoke(chatMessage: ChatMessage): Single = + @JvmOverloads + operator fun invoke(chatMessage: ChatMessage, isHistory: Boolean = false, isLast: Boolean = false): Single = when (chatMessage.senderType) { - Chat.Participant.OPERATOR -> Single.just(chatMessage) - .cast(OperatorMessage::class.java) - .flatMap { mapOperator(it) } - - else -> Single.just(chatMessage).map { ChatMessageInternal(it) } + Chat.Participant.OPERATOR -> processOperatorMessage(chatMessage as OperatorMessage, isHistory, isLast) + else -> processVisitorMessage(chatMessage, isHistory, isLast) } - private fun mapOperator(operatorMessage: OperatorMessage): Single { - return getOperatorUseCase.execute(operatorMessage.operatorId!!) - .map { map(operatorMessage, it.orElse(null)) } - } + private fun processOperatorMessage(chatMessage: OperatorMessage, isHistory: Boolean, isLast: Boolean): Single = + getOperatorUseCase.execute(chatMessage.operatorId!!) + .map { ChatMessageInternal(chatMessage, isHistory, isLast, it.getOrNull()) } + + private fun processVisitorMessage(chatMessage: ChatMessage, isHistory: Boolean, isLast: Boolean): Single = Single.just( + ChatMessageInternal(chatMessage, isHistory, isLast) + ) - private fun map(operatorMessage: OperatorMessage, operator: Operator?): ChatMessageInternal { - return ChatMessageInternal(operatorMessage, operator) - } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.java b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.java deleted file mode 100644 index 34ea8cf19..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.glia.widgets.core.engagement.domain.model; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.glia.androidsdk.Operator; -import com.glia.androidsdk.chat.Chat; -import com.glia.androidsdk.chat.ChatMessage; - -import java.util.Optional; - -public class ChatMessageInternal { - @NonNull - private final ChatMessage chatMessage; - @Nullable - private final Operator operator; - - public ChatMessageInternal(@NonNull ChatMessage chatMessage, @Nullable Operator operator) { - this.chatMessage = chatMessage; - this.operator = operator; - } - - public ChatMessageInternal(ChatMessage chatMessage) { - this(chatMessage, null); - } - - @NonNull - public ChatMessage getChatMessage() { - return chatMessage; - } - - public Optional getOperator() { - return Optional.ofNullable(operator); - } - - public Optional getOperatorId() { - return getOperator().map(Operator::getId); - } - - public Optional getOperatorName() { - return getOperator().map(Operator::getName); - } - - public Optional getOperatorImageUrl() { - return getOperator().map(Operator::getPicture).flatMap(Operator.Picture::getURL); - } - - public boolean isNotVisitor() { - return getChatMessage().getSenderType() != Chat.Participant.VISITOR; - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.kt new file mode 100644 index 000000000..7fa84c01d --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.kt @@ -0,0 +1,18 @@ +package com.glia.widgets.core.engagement.domain.model + +import com.glia.androidsdk.Operator +import com.glia.androidsdk.chat.Chat +import com.glia.androidsdk.chat.ChatMessage +import kotlin.jvm.optionals.getOrNull + +internal data class ChatMessageInternal( + val chatMessage: ChatMessage, + val isHistory: Boolean = false, + val isLatest: Boolean = false, + val operator: Operator? = null +) { + val operatorId: String? get() = operator?.id + val operatorName: String? get() = operator?.name + val operatorImageUrl: String? get() = operator?.picture?.url?.getOrNull() + val isNotVisitor: Boolean get() = chatMessage.senderType != Chat.Participant.VISITOR +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java index 9cc5cb334..b63221c54 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java @@ -56,26 +56,26 @@ public class ControllerFactory { private ActivityWatcherForChatHeadController activityWatcherForChatHeadController; public ControllerFactory( - RepositoryFactory repositoryFactory, - UseCaseFactory useCaseFactory, - GliaSdkConfigurationManager sdkConfigurationManager + RepositoryFactory repositoryFactory, + UseCaseFactory useCaseFactory, + GliaSdkConfigurationManager sdkConfigurationManager ) { this.repositoryFactory = repositoryFactory; messagesNotSeenHandler = new MessagesNotSeenHandler( - useCaseFactory.createGliaOnMessageUseCase(), - useCaseFactory.createOnEngagementEndUseCase() + useCaseFactory.createGliaOnMessageUseCase(), + useCaseFactory.createOnEngagementEndUseCase() ); this.useCaseFactory = useCaseFactory; this.dialogController = new DialogController( - useCaseFactory.createSetOverlayPermissionRequestDialogShownUseCase(), - useCaseFactory.createSetEnableCallNotificationChannelDialogShownUseCase() + useCaseFactory.createSetOverlayPermissionRequestDialogShownUseCase(), + useCaseFactory.createSetEnableCallNotificationChannelDialogShownUseCase() ); this.filePreviewController = new FilePreviewController( - useCaseFactory.createGetImageFileFromDownloadsUseCase(), - useCaseFactory.createGetImageFileFromCacheUseCase(), - useCaseFactory.createPutImageFileToDownloadsUseCase(), - useCaseFactory.createOnEngagementEndUseCase() + useCaseFactory.createGetImageFileFromDownloadsUseCase(), + useCaseFactory.createGetImageFileFromCacheUseCase(), + useCaseFactory.createPutImageFileToDownloadsUseCase(), + useCaseFactory.createOnEngagementEndUseCase() ); this.chatHeadPosition = ChatHeadPosition.getInstance(); this.sdkConfigurationManager = sdkConfigurationManager; @@ -85,58 +85,60 @@ public ChatController getChatController(ChatViewCallback chatViewCallback) { if (retainedChatController == null) { Logger.d(TAG, "new for chat activity"); retainedChatController = new ChatController( - chatViewCallback, - repositoryFactory.getMediaUpgradeOfferRepository(), - sharedTimer, - minimizeHandler, - dialogController, - messagesNotSeenHandler, - useCaseFactory.createCallNotificationUseCase(), - useCaseFactory.createGliaLoadHistoryUseCase(), - useCaseFactory.createQueueForChatEngagementUseCase(), - useCaseFactory.createOnEngagementUseCase(), - useCaseFactory.createOnEngagementEndUseCase(), - useCaseFactory.createGliaOnMessageUseCase(), - useCaseFactory.createGliaOnOperatorTypingUseCase(), - useCaseFactory.createGliaSendMessagePreviewUseCase(), - useCaseFactory.createGliaSendMessageUseCase(), - useCaseFactory.createAddOperatorMediaStateListenerUseCase(), - useCaseFactory.createCancelQueueTicketUseCase(), - useCaseFactory.createEndEngagementUseCase(), - useCaseFactory.createAddFileToAttachmentAndUploadUseCase(), - useCaseFactory.createAddFileAttachmentsObserverUseCase(), - useCaseFactory.createRemoveFileAttachmentObserverUseCase(), - useCaseFactory.createGetFileAttachmentsUseCase(), - useCaseFactory.createRemoveFileAttachmentUseCase(), - useCaseFactory.createSupportedFileCountCheckUseCase(), - useCaseFactory.createIsShowSendButtonUseCase(), - useCaseFactory.createIsShowOverlayPermissionRequestDialogUseCase(), - useCaseFactory.createDownloadFileUseCase(), - useCaseFactory.createIsEnableChatEditTextUseCase(), - useCaseFactory.createSiteInfoUseCase(), - useCaseFactory.getGliaSurveyUseCase(), - useCaseFactory.createGetGliaEngagementStateFlowableUseCase(), - useCaseFactory.createIsFromCallScreenUseCase(), - useCaseFactory.createUpdateFromCallScreenUseCase(), - useCaseFactory.createCustomCardAdapterTypeUseCase(), - useCaseFactory.createCustomCardTypeUseCase(), - useCaseFactory.createCustomCardShouldShowUseCase(), - useCaseFactory.createQueueTicketStateChangeToUnstaffedUseCase(), - useCaseFactory.createIsQueueingEngagementUseCase(), - useCaseFactory.createAddMediaUpgradeOfferCallbackUseCase(), - useCaseFactory.createRemoveMediaUpgradeOfferCallbackUseCase(), - useCaseFactory.createIsSecureEngagementUseCase(), - useCaseFactory.createIsOngoingEngagementUseCase(), - useCaseFactory.createSetEngagementConfigUseCase(), - useCaseFactory.createIsSecureConversationsChatAvailableUseCase(), - useCaseFactory.createMarkMessagesReadUseCase(), - useCaseFactory.createHasPendingSurveyUseCase(), - useCaseFactory.createSetPendingSurveyUsed(), - useCaseFactory.createIsCallVisualizerUseCase(), - useCaseFactory.createPreEngagementMessageUseCase(), - useCaseFactory.createAddNewMessagesDividerUseCase(), - useCaseFactory.createIsFileReadyForPreviewUseCase(), - useCaseFactory.createAcceptMediaUpgradeOfferUseCase() + chatViewCallback, + repositoryFactory.getMediaUpgradeOfferRepository(), + sharedTimer, + minimizeHandler, + dialogController, + messagesNotSeenHandler, + useCaseFactory.createCallNotificationUseCase(), + useCaseFactory.createGliaLoadHistoryUseCase(), + useCaseFactory.createQueueForChatEngagementUseCase(), + useCaseFactory.createOnEngagementUseCase(), + useCaseFactory.createOnEngagementEndUseCase(), + useCaseFactory.createGliaOnMessageUseCase(), + useCaseFactory.createGliaOnOperatorTypingUseCase(), + useCaseFactory.createGliaSendMessagePreviewUseCase(), + useCaseFactory.createGliaSendMessageUseCase(), + useCaseFactory.createAddOperatorMediaStateListenerUseCase(), + useCaseFactory.createCancelQueueTicketUseCase(), + useCaseFactory.createEndEngagementUseCase(), + useCaseFactory.createAddFileToAttachmentAndUploadUseCase(), + useCaseFactory.createAddFileAttachmentsObserverUseCase(), + useCaseFactory.createRemoveFileAttachmentObserverUseCase(), + useCaseFactory.createGetFileAttachmentsUseCase(), + useCaseFactory.createRemoveFileAttachmentUseCase(), + useCaseFactory.createSupportedFileCountCheckUseCase(), + useCaseFactory.createIsShowSendButtonUseCase(), + useCaseFactory.createIsShowOverlayPermissionRequestDialogUseCase(), + useCaseFactory.createDownloadFileUseCase(), + useCaseFactory.createIsEnableChatEditTextUseCase(), + useCaseFactory.createSiteInfoUseCase(), + useCaseFactory.getGliaSurveyUseCase(), + useCaseFactory.createGetGliaEngagementStateFlowableUseCase(), + useCaseFactory.createIsFromCallScreenUseCase(), + useCaseFactory.createUpdateFromCallScreenUseCase(), + useCaseFactory.createCustomCardAdapterTypeUseCase(), + useCaseFactory.createCustomCardTypeUseCase(), + useCaseFactory.createCustomCardShouldShowUseCase(), + useCaseFactory.createQueueTicketStateChangeToUnstaffedUseCase(), + useCaseFactory.createIsQueueingEngagementUseCase(), + useCaseFactory.createAddMediaUpgradeOfferCallbackUseCase(), + useCaseFactory.createRemoveMediaUpgradeOfferCallbackUseCase(), + useCaseFactory.createIsSecureEngagementUseCase(), + useCaseFactory.createIsOngoingEngagementUseCase(), + useCaseFactory.createSetEngagementConfigUseCase(), + useCaseFactory.createIsSecureConversationsChatAvailableUseCase(), + useCaseFactory.createMarkMessagesReadUseCase(), + useCaseFactory.createHasPendingSurveyUseCase(), + useCaseFactory.createSetPendingSurveyUsed(), + useCaseFactory.createIsCallVisualizerUseCase(), + useCaseFactory.createPreEngagementMessageUseCase(), + useCaseFactory.createAddNewMessagesDividerUseCase(), + useCaseFactory.createIsFileReadyForPreviewUseCase(), + useCaseFactory.createAcceptMediaUpgradeOfferUseCase(), + useCaseFactory.createIsGvaUseCase(), + useCaseFactory.createMapGvaUseCase() ); } else { Logger.d(TAG, "retained chat controller"); @@ -149,42 +151,42 @@ public CallController getCallController(CallViewCallback callViewCallback) { if (retainedCallController == null) { Logger.d(TAG, "new call controller"); retainedCallController = new CallController( - sdkConfigurationManager, - repositoryFactory.getMediaUpgradeOfferRepository(), - sharedTimer, - callViewCallback, - new TimeCounter(), - new TimeCounter(), - minimizeHandler, - dialogController, - messagesNotSeenHandler, - useCaseFactory.createCallNotificationUseCase(), - useCaseFactory.createQueueForMediaEngagementUseCase(), - useCaseFactory.createCancelQueueTicketUseCase(), - useCaseFactory.createOnEngagementUseCase(), - useCaseFactory.createAddOperatorMediaStateListenerUseCase(), - useCaseFactory.createRemoveOperatorMediaStateListenerUseCase(), - useCaseFactory.createOnEngagementEndUseCase(), - useCaseFactory.createEndEngagementUseCase(), - useCaseFactory.createShouldShowMediaEngagementViewUseCase(), - useCaseFactory.createIsShowOverlayPermissionRequestDialogUseCase(), - useCaseFactory.createHasCallNotificationChannelEnabledUseCase(), - useCaseFactory.createIsShowEnableCallNotificationChannelDialogUseCase(), - useCaseFactory.getGliaSurveyUseCase(), - useCaseFactory.createAddVisitorMediaStateListenerUseCase(), - useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), - useCaseFactory.createAddMediaUpgradeOfferCallbackUseCase(), - useCaseFactory.createRemoveMediaUpgradeOfferCallbackUseCase(), - useCaseFactory.createToggleVisitorAudioMediaMuteUseCase(), - useCaseFactory.createToggleVisitorVideoUseCase(), - useCaseFactory.createGetGliaEngagementStateFlowableUseCase(), - useCaseFactory.createUpdateFromCallScreenUseCase(), - useCaseFactory.createQueueTicketStateChangeToUnstaffedUseCase(), - useCaseFactory.createIsCallVisualizerUseCase(), - useCaseFactory.createIsOngoingEngagementUseCase(), - useCaseFactory.createSetPendingSurveyUsed(), - useCaseFactory.createTurnSpeakerphoneUseCase(), - useCaseFactory.createHandleCallPermissionsUseCase()); + sdkConfigurationManager, + repositoryFactory.getMediaUpgradeOfferRepository(), + sharedTimer, + callViewCallback, + new TimeCounter(), + new TimeCounter(), + minimizeHandler, + dialogController, + messagesNotSeenHandler, + useCaseFactory.createCallNotificationUseCase(), + useCaseFactory.createQueueForMediaEngagementUseCase(), + useCaseFactory.createCancelQueueTicketUseCase(), + useCaseFactory.createOnEngagementUseCase(), + useCaseFactory.createAddOperatorMediaStateListenerUseCase(), + useCaseFactory.createRemoveOperatorMediaStateListenerUseCase(), + useCaseFactory.createOnEngagementEndUseCase(), + useCaseFactory.createEndEngagementUseCase(), + useCaseFactory.createShouldShowMediaEngagementViewUseCase(), + useCaseFactory.createIsShowOverlayPermissionRequestDialogUseCase(), + useCaseFactory.createHasCallNotificationChannelEnabledUseCase(), + useCaseFactory.createIsShowEnableCallNotificationChannelDialogUseCase(), + useCaseFactory.getGliaSurveyUseCase(), + useCaseFactory.createAddVisitorMediaStateListenerUseCase(), + useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), + useCaseFactory.createAddMediaUpgradeOfferCallbackUseCase(), + useCaseFactory.createRemoveMediaUpgradeOfferCallbackUseCase(), + useCaseFactory.createToggleVisitorAudioMediaMuteUseCase(), + useCaseFactory.createToggleVisitorVideoUseCase(), + useCaseFactory.createGetGliaEngagementStateFlowableUseCase(), + useCaseFactory.createUpdateFromCallScreenUseCase(), + useCaseFactory.createQueueTicketStateChangeToUnstaffedUseCase(), + useCaseFactory.createIsCallVisualizerUseCase(), + useCaseFactory.createIsOngoingEngagementUseCase(), + useCaseFactory.createSetPendingSurveyUsed(), + useCaseFactory.createTurnSpeakerphoneUseCase(), + useCaseFactory.createHandleCallPermissionsUseCase()); } else { Logger.d(TAG, "retained call controller"); retainedCallController.setViewCallback(callViewCallback); @@ -196,12 +198,12 @@ public ScreenSharingController getScreenSharingController() { if (retainedScreenSharingController == null) { Logger.d(TAG, "new screen sharing controller"); retainedScreenSharingController = new ScreenSharingController( - repositoryFactory.getGliaScreenSharingRepository(), - dialogController, - useCaseFactory.createShowScreenSharingNotificationUseCase(), - useCaseFactory.createRemoveScreenSharingNotificationUseCase(), - useCaseFactory.createHasScreenSharingNotificationChannelEnabledUseCase(), - sdkConfigurationManager + repositoryFactory.getGliaScreenSharingRepository(), + dialogController, + useCaseFactory.createShowScreenSharingNotificationUseCase(), + useCaseFactory.createRemoveScreenSharingNotificationUseCase(), + useCaseFactory.createHasScreenSharingNotificationChannelEnabledUseCase(), + sdkConfigurationManager ); } return retainedScreenSharingController; @@ -236,8 +238,8 @@ public DialogController getDialogController() { public DialogController createDialogController() { return new DialogController( - useCaseFactory.createSetOverlayPermissionRequestDialogShownUseCase(), - useCaseFactory.createSetEnableCallNotificationChannelDialogShownUseCase() + useCaseFactory.createSetOverlayPermissionRequestDialogShownUseCase(), + useCaseFactory.createSetEnableCallNotificationChannelDialogShownUseCase() ); } @@ -255,19 +257,19 @@ public FilePreviewController getImagePreviewController() { public ServiceChatHeadController getChatHeadController() { if (serviceChatHeadController == null) { serviceChatHeadController = new ServiceChatHeadController( - useCaseFactory.getToggleChatHeadServiceUseCase(), - useCaseFactory.getResolveChatHeadNavigationUseCase(), - useCaseFactory.createOnEngagementUseCase(), - useCaseFactory.createOnCallVisualizerUseCase(), - useCaseFactory.createOnEngagementEndUseCase(), - useCaseFactory.createOnCallVisualizerEndUseCase(), - messagesNotSeenHandler, - useCaseFactory.createAddVisitorMediaStateListenerUseCase(), - useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), - chatHeadPosition, - useCaseFactory.createGetOperatorFlowableUseCase(), - useCaseFactory.createSetPendingSurveyUseCase(), - useCaseFactory.createIsCallVisualizerScreenSharingUseCase() + useCaseFactory.getToggleChatHeadServiceUseCase(), + useCaseFactory.getResolveChatHeadNavigationUseCase(), + useCaseFactory.createOnEngagementUseCase(), + useCaseFactory.createOnCallVisualizerUseCase(), + useCaseFactory.createOnEngagementEndUseCase(), + useCaseFactory.createOnCallVisualizerEndUseCase(), + messagesNotSeenHandler, + useCaseFactory.createAddVisitorMediaStateListenerUseCase(), + useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), + chatHeadPosition, + useCaseFactory.createGetOperatorFlowableUseCase(), + useCaseFactory.createSetPendingSurveyUseCase(), + useCaseFactory.createIsCallVisualizerScreenSharingUseCase() ); } return serviceChatHeadController; @@ -276,18 +278,18 @@ public ServiceChatHeadController getChatHeadController() { public ApplicationChatHeadLayoutController getChatHeadLayoutController() { if (applicationChatHeadController == null) { applicationChatHeadController = new ApplicationChatHeadLayoutController( - useCaseFactory.getIsDisplayApplicationChatHeadUseCase(), - useCaseFactory.getResolveChatHeadNavigationUseCase(), - useCaseFactory.createOnEngagementUseCase(), - useCaseFactory.createOnEngagementEndUseCase(), - useCaseFactory.createOnCallVisualizerUseCase(), - useCaseFactory.createOnCallVisualizerEndUseCase(), - messagesNotSeenHandler, - useCaseFactory.createAddVisitorMediaStateListenerUseCase(), - useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), - useCaseFactory.createGetOperatorFlowableUseCase(), - useCaseFactory.createSetPendingSurveyUseCase(), - useCaseFactory.createIsCallVisualizerScreenSharingUseCase() + useCaseFactory.getIsDisplayApplicationChatHeadUseCase(), + useCaseFactory.getResolveChatHeadNavigationUseCase(), + useCaseFactory.createOnEngagementUseCase(), + useCaseFactory.createOnEngagementEndUseCase(), + useCaseFactory.createOnCallVisualizerUseCase(), + useCaseFactory.createOnCallVisualizerEndUseCase(), + messagesNotSeenHandler, + useCaseFactory.createAddVisitorMediaStateListenerUseCase(), + useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), + useCaseFactory.createGetOperatorFlowableUseCase(), + useCaseFactory.createSetPendingSurveyUseCase(), + useCaseFactory.createIsCallVisualizerScreenSharingUseCase() ); } return applicationChatHeadController; @@ -296,7 +298,7 @@ public ApplicationChatHeadLayoutController getChatHeadLayoutController() { public SurveyContract.Controller getSurveyController() { if (surveyController == null) { surveyController = new SurveyController( - useCaseFactory.getSurveyAnswerUseCase() + useCaseFactory.getSurveyAnswerUseCase() ); } return surveyController; @@ -305,12 +307,12 @@ public SurveyContract.Controller getSurveyController() { public CallVisualizerController getCallVisualizerController() { if (callVisualizerController == null) { callVisualizerController = new CallVisualizerController( - repositoryFactory.getCallVisualizerRepository(), - dialogController, - useCaseFactory.getGliaSurveyUseCase(), - useCaseFactory.createOnCallVisualizerUseCase(), - useCaseFactory.createOnCallVisualizerEndUseCase(), - useCaseFactory.createIsCallOrChatScreenActiveUseCase() + repositoryFactory.getCallVisualizerRepository(), + dialogController, + useCaseFactory.getGliaSurveyUseCase(), + useCaseFactory.createOnCallVisualizerUseCase(), + useCaseFactory.createOnCallVisualizerEndUseCase(), + useCaseFactory.createIsCallOrChatScreenActiveUseCase() ); } return callVisualizerController; @@ -318,29 +320,29 @@ public CallVisualizerController getCallVisualizerController() { public FloatingVisitorVideoContract.Controller getFloatingVisitorVideoController() { return new FloatingVisitorVideoController( - useCaseFactory.createAddVisitorMediaStateListenerUseCase(), - useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), - useCaseFactory.createIsShowVideoUseCase(), - useCaseFactory.createIsShowOnHoldUseCase() + useCaseFactory.createAddVisitorMediaStateListenerUseCase(), + useCaseFactory.createRemoveVisitorMediaStateListenerUseCase(), + useCaseFactory.createIsShowVideoUseCase(), + useCaseFactory.createIsShowOnHoldUseCase() ); } public MessageCenterContract.Controller getMessageCenterController(String queueId) { return new MessageCenterController( - serviceChatHeadController, - useCaseFactory.createSendSecureMessageUseCase(queueId), - useCaseFactory.createIsMessageCenterAvailableUseCase(queueId), - useCaseFactory.createAddSecureFileAttachmentsObserverUseCase(), - useCaseFactory.createAddSecureFileToAttachmentAndUploadUseCase(), - useCaseFactory.createGetSecureFileAttachmentsUseCase(), - useCaseFactory.createRemoveSecureFileAttachmentUseCase(), - useCaseFactory.createIsAuthenticatedUseCase(), - useCaseFactory.createSiteInfoUseCase(), - useCaseFactory.createOnNextMessageUseCase(), - useCaseFactory.createEnableSendMessageButtonUseCase(), - useCaseFactory.createShowMessageLimitErrorUseCase(), - useCaseFactory.createResetMessageCenterUseCase(), - createDialogController() + serviceChatHeadController, + useCaseFactory.createSendSecureMessageUseCase(queueId), + useCaseFactory.createIsMessageCenterAvailableUseCase(queueId), + useCaseFactory.createAddSecureFileAttachmentsObserverUseCase(), + useCaseFactory.createAddSecureFileToAttachmentAndUploadUseCase(), + useCaseFactory.createGetSecureFileAttachmentsUseCase(), + useCaseFactory.createRemoveSecureFileAttachmentUseCase(), + useCaseFactory.createIsAuthenticatedUseCase(), + useCaseFactory.createSiteInfoUseCase(), + useCaseFactory.createOnNextMessageUseCase(), + useCaseFactory.createEnableSendMessageButtonUseCase(), + useCaseFactory.createShowMessageLimitErrorUseCase(), + useCaseFactory.createResetMessageCenterUseCase(), + createDialogController() ); } @@ -350,17 +352,17 @@ public EndScreenSharingContract.Controller getEndScreenSharingController() { public VisitorCodeContract.Controller getVisitorCodeController() { return new VisitorCodeController( - dialogController, - repositoryFactory.getVisitorCodeRepository(), - repositoryFactory.getGliaEngagementRepository()); + dialogController, + repositoryFactory.getVisitorCodeRepository(), + repositoryFactory.getGliaEngagementRepository()); } public ActivityWatcherForCallVisualizerContract.Controller getActivityWatcherForCallVisualizerController() { if (activityWatcherforCallVisualizerController == null) { activityWatcherforCallVisualizerController = new ActivityWatcherForCallVisualizerController( - getCallVisualizerController(), - getScreenSharingController(), - useCaseFactory.createIsShowOverlayPermissionRequestDialogUseCase()); + getCallVisualizerController(), + getScreenSharingController(), + useCaseFactory.createIsShowOverlayPermissionRequestDialogUseCase()); } return activityWatcherforCallVisualizerController; } @@ -368,19 +370,19 @@ public ActivityWatcherForCallVisualizerContract.Controller getActivityWatcherFor public ActivityWatcherForChatHeadContract.Controller getActivityWatcherForChatHeadController() { if (activityWatcherForChatHeadController == null) { activityWatcherForChatHeadController = new ActivityWatcherForChatHeadController( - serviceChatHeadController, - getChatHeadLayoutController(), - getScreenSharingController(), - useCaseFactory.createOnEngagementUseCase(), - useCaseFactory.createIsFromCallScreenUseCase(), - useCaseFactory.createUpdateFromCallScreenUseCase()); + serviceChatHeadController, + getChatHeadLayoutController(), + getScreenSharingController(), + useCaseFactory.createOnEngagementUseCase(), + useCaseFactory.createIsFromCallScreenUseCase(), + useCaseFactory.createUpdateFromCallScreenUseCase()); } return activityWatcherForChatHeadController; } public PermissionsRequestContract.Controller getPermissionsController() { return new PermissionsRequestController( - repositoryFactory.getPermissionsRequestRepository() + repositoryFactory.getPermissionsRequestRepository() ); } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java index 43ea3c552..b07cfc082 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java @@ -27,6 +27,15 @@ import com.glia.widgets.chat.domain.PreEngagementMessageUseCase; import com.glia.widgets.chat.domain.SiteInfoUseCase; import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase; +import com.glia.widgets.chat.domain.gva.GetGvaTypeUseCase; +import com.glia.widgets.chat.domain.gva.IsGvaUseCase; +import com.glia.widgets.chat.domain.gva.MapGvaGvaGalleryCardsUseCase; +import com.glia.widgets.chat.domain.gva.MapGvaGvaQuickRepliesUseCase; +import com.glia.widgets.chat.domain.gva.MapGvaPersistentButtonsUseCase; +import com.glia.widgets.chat.domain.gva.MapGvaResponseTextUseCase; +import com.glia.widgets.chat.domain.gva.MapGvaUseCase; +import com.glia.widgets.chat.domain.gva.ParseGvaButtonsUseCase; +import com.glia.widgets.chat.domain.gva.ParseGvaGalleryCardsUseCase; import com.glia.widgets.core.audio.AudioControlManager; import com.glia.widgets.core.audio.domain.OnAudioStartedUseCase; import com.glia.widgets.core.audio.domain.TurnSpeakerphoneUseCase; @@ -110,8 +119,11 @@ import com.glia.widgets.helper.rx.Schedulers; import com.glia.widgets.view.floatingvisitorvideoview.domain.IsShowOnHoldUseCase; import com.glia.widgets.view.floatingvisitorvideoview.domain.IsShowVideoUseCase; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; public class UseCaseFactory { + private static final SurveyStateManager surveyStateManager = new SurveyStateManager(); private static CallNotificationUseCase callNotificationUseCase; private static ShowScreenSharingNotificationUseCase showScreenSharingNotificationUseCase; private static RemoveScreenSharingNotificationUseCase removeScreenSharingNotificationUseCase; @@ -119,20 +131,20 @@ public class UseCaseFactory { private static IsDisplayApplicationChatHeadUseCase isDisplayApplicationChatHeadUseCase; private static ResolveChatHeadNavigationUseCase resolveChatHeadNavigationUseCase; private static VisitorCodeViewBuilderUseCase visitorCodeViewBuilderUseCase; - private static GliaQueueForChatEngagementUseCase gliaQueueForChatEngagementUseCase; private static GliaQueueForMediaEngagementUseCase gliaQueueForMediaEngagementUseCase; private final RepositoryFactory repositoryFactory; private final PermissionManager permissionManager; private final PermissionDialogManager permissionDialogManager; private final GliaSdkConfigurationManager gliaSdkConfigurationManager; - private static final SurveyStateManager surveyStateManager = new SurveyStateManager(); private final INotificationManager notificationManager; private final ChatHeadManager chatHeadManager; private final AudioControlManager audioControlManager; private final Schedulers schedulers; private final GliaCore gliaCore; + private Gson gvaGson; + public UseCaseFactory(RepositoryFactory repositoryFactory, PermissionManager permissionManager, PermissionDialogManager permissionDialogManager, @@ -157,13 +169,13 @@ public UseCaseFactory(RepositoryFactory repositoryFactory, public ToggleChatHeadServiceUseCase getToggleChatHeadServiceUseCase() { if (toggleChatHeadServiceUseCase == null) { toggleChatHeadServiceUseCase = new ToggleChatHeadServiceUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaScreenSharingRepository(), - chatHeadManager, - permissionManager, - gliaSdkConfigurationManager, - repositoryFactory.getGliaEngagementTypeRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaScreenSharingRepository(), + chatHeadManager, + permissionManager, + gliaSdkConfigurationManager, + repositoryFactory.getGliaEngagementTypeRepository() ); } return toggleChatHeadServiceUseCase; @@ -173,12 +185,12 @@ public ToggleChatHeadServiceUseCase getToggleChatHeadServiceUseCase() { public IsDisplayApplicationChatHeadUseCase getIsDisplayApplicationChatHeadUseCase() { if (isDisplayApplicationChatHeadUseCase == null) { isDisplayApplicationChatHeadUseCase = new IsDisplayApplicationChatHeadUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaScreenSharingRepository(), - permissionManager, - gliaSdkConfigurationManager, - repositoryFactory.getGliaEngagementTypeRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaScreenSharingRepository(), + permissionManager, + gliaSdkConfigurationManager, + repositoryFactory.getGliaEngagementTypeRepository() ); } return isDisplayApplicationChatHeadUseCase; @@ -188,10 +200,10 @@ public IsDisplayApplicationChatHeadUseCase getIsDisplayApplicationChatHeadUseCas public ResolveChatHeadNavigationUseCase getResolveChatHeadNavigationUseCase() { if (resolveChatHeadNavigationUseCase == null) { resolveChatHeadNavigationUseCase = new ResolveChatHeadNavigationUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaEngagementTypeRepository(), - createIsCallVisualizerScreenSharingUseCase() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaEngagementTypeRepository(), + createIsCallVisualizerScreenSharingUseCase() ); } return resolveChatHeadNavigationUseCase; @@ -229,11 +241,11 @@ public RemoveScreenSharingNotificationUseCase createRemoveScreenSharingNotificat @NonNull public GliaLoadHistoryUseCase createGliaLoadHistoryUseCase() { return new GliaLoadHistoryUseCase( - repositoryFactory.getGliaMessageRepository(), - repositoryFactory.getSecureConversationsRepository(), - createIsSecureEngagementUseCase(), - getMapOperatorUseCase(), - createSubscribeToUnreadMessagesCountUseCase() + repositoryFactory.getGliaMessageRepository(), + repositoryFactory.getSecureConversationsRepository(), + createIsSecureEngagementUseCase(), + getMapOperatorUseCase(), + createSubscribeToUnreadMessagesCountUseCase() ); } @@ -246,9 +258,9 @@ public MapOperatorUseCase getMapOperatorUseCase() { public GliaQueueForChatEngagementUseCase createQueueForChatEngagementUseCase() { if (gliaQueueForChatEngagementUseCase == null) { gliaQueueForChatEngagementUseCase = new GliaQueueForChatEngagementUseCase( - schedulers, - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaEngagementRepository() + schedulers, + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaEngagementRepository() ); } return gliaQueueForChatEngagementUseCase; @@ -258,9 +270,9 @@ public GliaQueueForChatEngagementUseCase createQueueForChatEngagementUseCase() { public GliaQueueForMediaEngagementUseCase createQueueForMediaEngagementUseCase() { if (gliaQueueForMediaEngagementUseCase == null) { gliaQueueForMediaEngagementUseCase = new GliaQueueForMediaEngagementUseCase( - schedulers, - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaEngagementRepository() + schedulers, + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaEngagementRepository() ); } return gliaQueueForMediaEngagementUseCase; @@ -269,8 +281,8 @@ public GliaQueueForMediaEngagementUseCase createQueueForMediaEngagementUseCase() @NonNull public GliaCancelQueueTicketUseCase createCancelQueueTicketUseCase() { return new GliaCancelQueueTicketUseCase( - schedulers, - repositoryFactory.getGliaQueueRepository() + schedulers, + repositoryFactory.getGliaQueueRepository() ); } @@ -282,26 +294,26 @@ public GliaEndEngagementUseCase createEndEngagementUseCase() { @NonNull public GliaOnEngagementUseCase createOnEngagementUseCase() { return new GliaOnEngagementUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaOperatorMediaRepository(), - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaVisitorMediaRepository(), - repositoryFactory.getGliaEngagementStateRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaOperatorMediaRepository(), + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaVisitorMediaRepository(), + repositoryFactory.getGliaEngagementStateRepository() ); } @NonNull public GliaOnEngagementEndUseCase createOnEngagementEndUseCase() { return new GliaOnEngagementEndUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaOperatorMediaRepository(), - repositoryFactory.getGliaFileAttachmentRepository(), - createOnEngagementUseCase(), - createCallNotificationUseCase(), - createRemoveScreenSharingNotificationUseCase(), - repositoryFactory.getGliaSurveyRepository(), - repositoryFactory.getGliaVisitorMediaRepository(), - repositoryFactory.getEngagementConfigRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaOperatorMediaRepository(), + repositoryFactory.getGliaFileAttachmentRepository(), + createOnEngagementUseCase(), + createCallNotificationUseCase(), + createRemoveScreenSharingNotificationUseCase(), + repositoryFactory.getGliaSurveyRepository(), + repositoryFactory.getGliaVisitorMediaRepository(), + repositoryFactory.getEngagementConfigRepository() ); } @@ -323,26 +335,26 @@ public SetPendingSurveyUsedUseCase createSetPendingSurveyUsed() { @NonNull public PreEngagementMessageUseCase createPreEngagementMessageUseCase() { return new PreEngagementMessageUseCase( - repositoryFactory.getGliaMessageRepository(), - repositoryFactory.getGliaEngagementRepository(), - createOnEngagementUseCase(), - getMapOperatorUseCase() + repositoryFactory.getGliaMessageRepository(), + repositoryFactory.getGliaEngagementRepository(), + createOnEngagementUseCase(), + getMapOperatorUseCase() ); } @NonNull public GliaOnMessageUseCase createGliaOnMessageUseCase() { return new GliaOnMessageUseCase( - repositoryFactory.getGliaMessageRepository(), - createOnEngagementUseCase(), - getMapOperatorUseCase()); + repositoryFactory.getGliaMessageRepository(), + createOnEngagementUseCase(), + getMapOperatorUseCase()); } @NonNull public GliaOnOperatorTypingUseCase createGliaOnOperatorTypingUseCase() { return new GliaOnOperatorTypingUseCase( - repositoryFactory.getGliaMessageRepository(), - createOnEngagementUseCase() + repositoryFactory.getGliaMessageRepository(), + createOnEngagementUseCase() ); } @@ -354,35 +366,35 @@ public GliaSendMessagePreviewUseCase createGliaSendMessagePreviewUseCase() { @NonNull public GliaSendMessageUseCase createGliaSendMessageUseCase() { return new GliaSendMessageUseCase( - repositoryFactory.getGliaMessageRepository(), - repositoryFactory.getGliaFileAttachmentRepository(), - repositoryFactory.getGliaEngagementStateRepository(), - repositoryFactory.getEngagementConfigRepository(), - repositoryFactory.getSecureConversationsRepository(), - createIsSecureEngagementUseCase() + repositoryFactory.getGliaMessageRepository(), + repositoryFactory.getGliaFileAttachmentRepository(), + repositoryFactory.getGliaEngagementStateRepository(), + repositoryFactory.getEngagementConfigRepository(), + repositoryFactory.getSecureConversationsRepository(), + createIsSecureEngagementUseCase() ); } @NonNull public AddOperatorMediaStateListenerUseCase createAddOperatorMediaStateListenerUseCase() { return new AddOperatorMediaStateListenerUseCase( - repositoryFactory.getGliaOperatorMediaRepository() + repositoryFactory.getGliaOperatorMediaRepository() ); } @NonNull public RemoveOperatorMediaStateListenerUseCase createRemoveOperatorMediaStateListenerUseCase() { return new RemoveOperatorMediaStateListenerUseCase( - repositoryFactory.getGliaOperatorMediaRepository() + repositoryFactory.getGliaOperatorMediaRepository() ); } @NonNull public ShouldShowMediaEngagementViewUseCase createShouldShowMediaEngagementViewUseCase() { return new ShouldShowMediaEngagementViewUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaEngagementTypeRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaEngagementTypeRepository() ); } @@ -394,9 +406,9 @@ public AddFileAttachmentsObserverUseCase createAddFileAttachmentsObserverUseCase @NonNull public AddFileToAttachmentAndUploadUseCase createAddFileToAttachmentAndUploadUseCase() { return new AddFileToAttachmentAndUploadUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaFileAttachmentRepository(), - repositoryFactory.getEngagementConfigRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaFileAttachmentRepository(), + repositoryFactory.getEngagementConfigRepository() ); } @@ -423,9 +435,9 @@ public SupportedFileCountCheckUseCase createSupportedFileCountCheckUseCase() { @NonNull public IsShowSendButtonUseCase createIsShowSendButtonUseCase() { return new IsShowSendButtonUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaFileAttachmentRepository(), - createIsSecureEngagementUseCase() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaFileAttachmentRepository(), + createIsSecureEngagementUseCase() ); } @@ -472,8 +484,8 @@ public GetImageFileFromCacheUseCase createGetImageFileFromCacheUseCase() { @NonNull public GetImageFileFromNetworkUseCase createGetImageFileFromNetworkUseCase() { return new GetImageFileFromNetworkUseCase( - repositoryFactory.getGliaFileRepository(), - createDecodeSampledBitmapFromInputStreamUseCase() + repositoryFactory.getGliaFileRepository(), + createDecodeSampledBitmapFromInputStreamUseCase() ); } @@ -500,27 +512,27 @@ public OnNextMessageUseCase createOnNextMessageUseCase() { @NonNull public SendMessageButtonStateUseCase createEnableSendMessageButtonUseCase() { return new SendMessageButtonStateUseCase( - repositoryFactory.getSendMessageRepository(), - repositoryFactory.getSecureFileAttachmentRepository(), - repositoryFactory.getSecureConversationsRepository(), - createShowMessageLimitErrorUseCase(), - schedulers + repositoryFactory.getSendMessageRepository(), + repositoryFactory.getSecureFileAttachmentRepository(), + repositoryFactory.getSecureConversationsRepository(), + createShowMessageLimitErrorUseCase(), + schedulers ); } @NonNull public ShowMessageLimitErrorUseCase createShowMessageLimitErrorUseCase() { return new ShowMessageLimitErrorUseCase( - repositoryFactory.getSendMessageRepository(), - schedulers + repositoryFactory.getSendMessageRepository(), + schedulers ); } @NonNull public ResetMessageCenterUseCase createResetMessageCenterUseCase() { return new ResetMessageCenterUseCase( - repositoryFactory.getSecureFileAttachmentRepository(), - repositoryFactory.getSendMessageRepository() + repositoryFactory.getSecureFileAttachmentRepository(), + repositoryFactory.getSendMessageRepository() ); } @@ -552,16 +564,16 @@ public RemoveVisitorMediaStateListenerUseCase createRemoveVisitorMediaStateListe @NonNull public ToggleVisitorAudioMediaMuteUseCase createToggleVisitorAudioMediaMuteUseCase() { return new ToggleVisitorAudioMediaMuteUseCase( - schedulers, - repositoryFactory.getGliaVisitorMediaRepository() + schedulers, + repositoryFactory.getGliaVisitorMediaRepository() ); } @NonNull public ToggleVisitorVideoUseCase createToggleVisitorVideoUseCase() { return new ToggleVisitorVideoUseCase( - schedulers, - repositoryFactory.getGliaVisitorMediaRepository() + schedulers, + repositoryFactory.getGliaVisitorMediaRepository() ); } @@ -648,12 +660,12 @@ public RemoveMediaUpgradeOfferCallbackUseCase createRemoveMediaUpgradeOfferCallb @NonNull public SendSecureMessageUseCase createSendSecureMessageUseCase(String queueId) { return new SendSecureMessageUseCase( - queueId, - repositoryFactory.getSendMessageRepository(), - repositoryFactory.getSecureConversationsRepository(), - repositoryFactory.getSecureFileAttachmentRepository(), - repositoryFactory.getGliaMessageRepository(), - repositoryFactory.getGliaEngagementRepository() + queueId, + repositoryFactory.getSendMessageRepository(), + repositoryFactory.getSecureConversationsRepository(), + repositoryFactory.getSecureFileAttachmentRepository(), + repositoryFactory.getGliaMessageRepository(), + repositoryFactory.getGliaEngagementRepository() ); } @@ -670,8 +682,8 @@ public AddSecureFileToAttachmentAndUploadUseCase createAddSecureFileToAttachment @NonNull public AddSecureFileAttachmentsObserverUseCase createAddSecureFileAttachmentsObserverUseCase() { return new AddSecureFileAttachmentsObserverUseCase( - repositoryFactory.getSecureFileAttachmentRepository(), - schedulers + repositoryFactory.getSecureFileAttachmentRepository(), + schedulers ); } @@ -688,9 +700,9 @@ public RemoveSecureFileAttachmentUseCase createRemoveSecureFileAttachmentUseCase @NonNull public IsSecureEngagementUseCase createIsSecureEngagementUseCase() { return new IsSecureEngagementUseCase( - repositoryFactory.getEngagementConfigRepository(), - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaQueueRepository() + repositoryFactory.getEngagementConfigRepository(), + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaQueueRepository() ); } @@ -702,8 +714,8 @@ public IsAuthenticatedUseCase createIsAuthenticatedUseCase() { @NonNull public SetEngagementConfigUseCase createSetEngagementConfigUseCase() { return new SetEngagementConfigUseCase( - repositoryFactory.getEngagementConfigRepository(), - createResetSurveyUseCase() + repositoryFactory.getEngagementConfigRepository(), + createResetSurveyUseCase() ); } @@ -725,8 +737,8 @@ public IsMessagingAvailableUseCase createIsMessagingAvailableUseCase() { @NonNull public IsSecureConversationsChatAvailableUseCase createIsSecureConversationsChatAvailableUseCase() { return new IsSecureConversationsChatAvailableUseCase( - repositoryFactory.getEngagementConfigRepository(), - createIsMessagingAvailableUseCase() + repositoryFactory.getEngagementConfigRepository(), + createIsMessagingAvailableUseCase() ); } @@ -743,24 +755,24 @@ public MarkMessagesReadWithDelayUseCase createMarkMessagesReadUseCase() { @NonNull public GliaOnCallVisualizerUseCase createOnCallVisualizerUseCase() { return new GliaOnCallVisualizerUseCase( - repositoryFactory.getGliaEngagementRepository(), - repositoryFactory.getGliaOperatorMediaRepository(), - repositoryFactory.getGliaQueueRepository(), - repositoryFactory.getGliaVisitorMediaRepository(), - repositoryFactory.getGliaEngagementStateRepository() + repositoryFactory.getGliaEngagementRepository(), + repositoryFactory.getGliaOperatorMediaRepository(), + repositoryFactory.getGliaQueueRepository(), + repositoryFactory.getGliaVisitorMediaRepository(), + repositoryFactory.getGliaEngagementStateRepository() ); } @NonNull public GliaOnCallVisualizerEndUseCase createOnCallVisualizerEndUseCase() { return new GliaOnCallVisualizerEndUseCase( - repositoryFactory.getCallVisualizerRepository(), - repositoryFactory.getGliaOperatorMediaRepository(), - createOnCallVisualizerUseCase(), - callNotificationUseCase, - removeScreenSharingNotificationUseCase, - repositoryFactory.getGliaSurveyRepository(), - repositoryFactory.getGliaVisitorMediaRepository() + repositoryFactory.getCallVisualizerRepository(), + repositoryFactory.getGliaOperatorMediaRepository(), + createOnCallVisualizerUseCase(), + callNotificationUseCase, + removeScreenSharingNotificationUseCase, + repositoryFactory.getGliaSurveyRepository(), + repositoryFactory.getGliaVisitorMediaRepository() ); } @@ -792,31 +804,91 @@ public ResetSurveyUseCase createResetSurveyUseCase() { @NonNull public OnAudioStartedUseCase createOnAudioStartedUseCase() { return new OnAudioStartedUseCase( - repositoryFactory.getGliaOperatorMediaRepository(), - repositoryFactory.getGliaVisitorMediaRepository() + repositoryFactory.getGliaOperatorMediaRepository(), + repositoryFactory.getGliaVisitorMediaRepository() ); } @NonNull public TurnSpeakerphoneUseCase createTurnSpeakerphoneUseCase() { return new TurnSpeakerphoneUseCase( - audioControlManager + audioControlManager ); } @NonNull public AcceptMediaUpgradeOfferUseCase createAcceptMediaUpgradeOfferUseCase() { return new AcceptMediaUpgradeOfferUseCase( - repositoryFactory.getMediaUpgradeOfferRepository(), - permissionManager + repositoryFactory.getMediaUpgradeOfferRepository(), + permissionManager ); } @NonNull public HandleCallPermissionsUseCase createHandleCallPermissionsUseCase() { return new HandleCallPermissionsUseCase( - createIsCallVisualizerUseCase(), - permissionManager + createIsCallVisualizerUseCase(), + permissionManager + ); + } + + @NonNull + private Gson getGvaGson() { + if (gvaGson == null) { + gvaGson = new GsonBuilder().create(); + } + + return gvaGson; + } + + @NonNull + public ParseGvaButtonsUseCase createParseGvaButtonsUseCase() { + return new ParseGvaButtonsUseCase(getGvaGson()); + } + + @NonNull + public ParseGvaGalleryCardsUseCase createParseGvaGalleryCardsUseCase() { + return new ParseGvaGalleryCardsUseCase(getGvaGson()); + } + + @NonNull + public GetGvaTypeUseCase createGetGvaTypeUseCase() { + return new GetGvaTypeUseCase(); + } + + @NonNull + public IsGvaUseCase createIsGvaUseCase() { + return new IsGvaUseCase(createGetGvaTypeUseCase()); + } + + @NonNull + public MapGvaResponseTextUseCase createMapGvaResponseTextUseCase() { + return new MapGvaResponseTextUseCase(); + } + + @NonNull + public MapGvaPersistentButtonsUseCase createMapGvaPersistentButtonsUseCase() { + return new MapGvaPersistentButtonsUseCase(createParseGvaButtonsUseCase()); + } + + @NonNull + public MapGvaGvaQuickRepliesUseCase createMapGvaGvaQuickRepliesUseCase() { + return new MapGvaGvaQuickRepliesUseCase(createParseGvaButtonsUseCase(), createMapGvaResponseTextUseCase()); + } + + @NonNull + public MapGvaGvaGalleryCardsUseCase createMapGvaGvaGalleryCardsUseCase() { + return new MapGvaGvaGalleryCardsUseCase(createParseGvaGalleryCardsUseCase()); + } + + @NonNull + public MapGvaUseCase createMapGvaUseCase() { + return new MapGvaUseCase( + createGetGvaTypeUseCase(), + createMapGvaResponseTextUseCase(), + createMapGvaPersistentButtonsUseCase(), + createMapGvaGvaQuickRepliesUseCase(), + createMapGvaGvaGalleryCardsUseCase() ); } diff --git a/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt b/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt index 866616af9..ce2f03516 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt @@ -2,6 +2,8 @@ package com.glia.widgets.helper import android.content.res.ColorStateList import android.graphics.drawable.Drawable +import android.text.Html +import android.text.Spanned import android.text.format.DateUtils import androidx.annotation.ColorInt import androidx.core.graphics.drawable.DrawableCompat @@ -37,3 +39,8 @@ internal fun UiTheme?.isAlertDialogButtonUseVerticalAlignment(): Boolean = internal fun UiTheme?.getFullHybridTheme(newTheme: UiTheme?): UiTheme = deepMerge(newTheme) ?: UiTheme.UiThemeBuilder().build() + +/** + * Returns styled text from the provided HTML string. + */ +internal fun String.fromHtml(flags: Int = Html.FROM_HTML_MODE_COMPACT): Spanned = Html.fromHtml(this, flags) diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/Merge.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/Merge.kt index 98a9fef81..944edaa99 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/Merge.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/Merge.kt @@ -26,7 +26,7 @@ import kotlin.reflect.full.primaryConstructor * val c = a deepMerge b */ internal inline infix fun T.deepMerge(other: T): T { - if (!T::class.isData) throw UnsupportedOperationException("Merge supports only data classes") + if (!T::class.isData) throw UnsupportedOperationException("Merge supports only data classes, ${T::class.simpleName} is not a data class") return unsafeMerge(other) } diff --git a/widgetssdk/src/main/res/layout/chat_view.xml b/widgetssdk/src/main/res/layout/chat_view.xml index bc064bdfa..e81147654 100644 --- a/widgetssdk/src/main/res/layout/chat_view.xml +++ b/widgetssdk/src/main/res/layout/chat_view.xml @@ -17,9 +17,9 @@ android:id="@+id/chat_recycler_view" android:layout_width="match_parent" android:layout_height="0dp" - android:paddingBottom="@dimen/glia_medium" android:clipToPadding="false" - app:layout_constraintBottom_toTopOf="@+id/operator_typing_animation_view" + android:paddingBottom="@dimen/glia_medium" + app:layout_constraintBottom_toTopOf="@+id/gva_quick_replies_layout" app:layout_constraintTop_toBottomOf="@+id/app_bar_view" tools:listitem="@layout/chat_visitor_message_layout" /> @@ -65,15 +65,26 @@ + + @@ -115,13 +126,13 @@ android:layout_weight="1" android:hint="@string/glia_chat_enter_message" android:importantForAutofill="no" - android:textCursorDrawable="@null" + android:maxLength="10000" + android:maxLines="4" android:minHeight="@dimen/glia_chat_edit_text_min_height" android:paddingStart="@dimen/glia_large" android:textColor="?attr/gliaBaseDarkColor" android:textColorHint="?attr/gliaBaseNormalColor" - android:maxLines="4" - android:maxLength="10000" + android:textCursorDrawable="@null" tools:ignore="RtlSymmetry" tools:textColor="@color/glia_black_color" /> @@ -153,6 +164,6 @@ android:id="@+id/group_chat_controls" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:constraint_referenced_ids="operator_typing_animation_view, divider_view, add_attachment_queue, chat_message_layout"/> + app:constraint_referenced_ids="operator_typing_animation_view, divider_view, add_attachment_queue, chat_message_layout" /> diff --git a/widgetssdk/src/test/java/android/TestExtensions.kt b/widgetssdk/src/test/java/android/TestExtensions.kt new file mode 100644 index 000000000..78f9d382e --- /dev/null +++ b/widgetssdk/src/test/java/android/TestExtensions.kt @@ -0,0 +1,7 @@ +package android + +import java.nio.charset.StandardCharsets + +fun Class.readRawResource(resName: String): String = classLoader?.getResourceAsStream(resName)?.run { + bufferedReader(StandardCharsets.UTF_8).use { it.readText() } +} ?: "" diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/MockChatMessageInternal.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/MockChatMessageInternal.kt new file mode 100644 index 000000000..8732cc151 --- /dev/null +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/MockChatMessageInternal.kt @@ -0,0 +1,46 @@ +package com.glia.widgets.chat + +import com.glia.androidsdk.chat.ChatMessage +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal +import org.json.JSONObject +import org.mockito.Mockito +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +internal class MockChatMessageInternal { + val messageId = "message_id" + val operatorId = "operator_id" + val messageTimeStamp = 123L + val operatorImageUrl = "operator_url" + val operatorName = "operator_name" + + private val chatMessage: ChatMessage = mock() + val chatMessageInternal: ChatMessageInternal = mock() + + init { + whenever(chatMessageInternal.chatMessage) doReturn chatMessage + } + + fun mockChatMessage(metadata: JSONObject = JSONObject()) { + whenever(chatMessage.id) doReturn messageId + whenever(chatMessage.timestamp) doReturn messageTimeStamp + whenever(chatMessage.metadata) doReturn metadata + } + + fun mockOperatorProperties() { + whenever(chatMessageInternal.operatorId) doReturn operatorId + whenever(chatMessageInternal.operatorImageUrl) doReturn operatorImageUrl + whenever(chatMessageInternal.operatorName) doReturn operatorName + } + + fun mockOperatorPropertiesWithNull() { + whenever(chatMessageInternal.operatorId) doReturn null + whenever(chatMessageInternal.operatorImageUrl) doReturn null + whenever(chatMessageInternal.operatorName) doReturn null + } + + fun reset() { + Mockito.reset(chatMessage, chatMessageInternal) + } +} diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt index e4bc54e92..20a8758b8 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt @@ -18,6 +18,8 @@ import com.glia.widgets.chat.domain.IsShowSendButtonUseCase import com.glia.widgets.chat.domain.PreEngagementMessageUseCase import com.glia.widgets.chat.domain.SiteInfoUseCase import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase +import com.glia.widgets.chat.domain.gva.IsGvaUseCase +import com.glia.widgets.chat.domain.gva.MapGvaUseCase import com.glia.widgets.chat.model.history.ChatItem import com.glia.widgets.chat.model.history.LinkedChatItem import com.glia.widgets.core.callvisualizer.domain.IsCallVisualizerUseCase @@ -123,6 +125,8 @@ class ChatControllerTest { private lateinit var addNewMessagesDividerUseCase: AddNewMessagesDividerUseCase private lateinit var isFileReadyForPreviewUseCase: IsFileReadyForPreviewUseCase private lateinit var acceptMediaUpgradeOfferUseCase: AcceptMediaUpgradeOfferUseCase + private lateinit var isGvaUseCase: IsGvaUseCase + private lateinit var mapGvaUseCase: MapGvaUseCase private lateinit var chatController: ChatController @@ -180,6 +184,8 @@ class ChatControllerTest { addNewMessagesDividerUseCase = mock() isFileReadyForPreviewUseCase = mock() acceptMediaUpgradeOfferUseCase = mock() + isGvaUseCase = mock() + mapGvaUseCase = mock() chatController = ChatController( chatViewCallback = chatViewCallback, @@ -233,7 +239,9 @@ class ChatControllerTest { preEngagementMessageUseCase = preEngagementMessageUseCase, addNewMessagesDividerUseCase = addNewMessagesDividerUseCase, isFileReadyForPreviewUseCase = isFileReadyForPreviewUseCase, - acceptMediaUpgradeOfferUseCase = acceptMediaUpgradeOfferUseCase + acceptMediaUpgradeOfferUseCase = acceptMediaUpgradeOfferUseCase, + isGvaUseCase = isGvaUseCase, + mapGvaUseCase = mapGvaUseCase ) } diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCaseTest.java b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCaseTest.java index 05d6b77ec..306b2bea4 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCaseTest.java +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/CustomCardAdapterTypeUseCaseTest.java @@ -32,7 +32,7 @@ public void execute_returnsNull_whenAdapterIsNull() { CustomCardAdapterTypeUseCase useCase = new CustomCardAdapterTypeUseCase(null); ChatMessage message = mock(ChatMessage.class); - Integer result = useCase.execute(message); + Integer result = useCase.invoke(message); assertNull(result); } @@ -41,7 +41,7 @@ public void execute_returnsNull_whenAdapterIsNull() { public void execute_returnsNull_whenMetadataIsNull() { ChatMessage message = mock(ChatMessage.class); - Integer result = useCase.execute(message); + Integer result = useCase.invoke(message); assertNull(result); } @@ -52,7 +52,7 @@ public void execute_returnsNull_whenAdapterReturnsNull() { JSONObject metadata = new JSONObject(); when(message.getMetadata()).thenReturn(metadata); - Integer result = useCase.execute(message); + Integer result = useCase.invoke(message); assertNull(result); } @@ -64,7 +64,7 @@ public void execute_returnsViewType_whenAdapterReturnsViewType() throws JSONExce when(message.getMetadata()).thenReturn(metadata); when(customCardAdapter.getChatAdapterViewType(message)).thenReturn(VIEW_TYPE); - Integer result = useCase.execute(message); + Integer result = useCase.invoke(message); assertEquals(VIEW_TYPE, result); } diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/GetGvaTypeUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/GetGvaTypeUseCaseTest.kt new file mode 100644 index 000000000..dc881b7bd --- /dev/null +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/GetGvaTypeUseCaseTest.kt @@ -0,0 +1,38 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test + +class GetGvaTypeUseCaseTest { + private lateinit var useCase: GetGvaTypeUseCase + + @Before + fun setUp() { + useCase = GetGvaTypeUseCase() + } + + @Test + fun `invoke returns appropriate type when present`() { + val jsonObject = JSONObject().put(Gva.Keys.TYPE, Gva.Type.PLAIN_TEXT.value) + assertEquals(Gva.Type.PLAIN_TEXT, useCase(jsonObject)) + + jsonObject.put(Gva.Keys.TYPE, Gva.Type.PERSISTENT_BUTTONS.value) + assertEquals(Gva.Type.PERSISTENT_BUTTONS, useCase(jsonObject)) + + jsonObject.put(Gva.Keys.TYPE, Gva.Type.QUICK_REPLIES.value) + assertEquals(Gva.Type.QUICK_REPLIES, useCase(jsonObject)) + + jsonObject.put(Gva.Keys.TYPE, Gva.Type.GALLERY_CARDS.value) + assertEquals(Gva.Type.GALLERY_CARDS, useCase(jsonObject)) + } + + @Test + fun `invoke returns null when gva type not found`() { + val jsonObject = JSONObject().put(Gva.Keys.TYPE, "asdfg") + assertNull(useCase(jsonObject)) + } +} diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/IsGvaUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/IsGvaUseCaseTest.kt new file mode 100644 index 000000000..1ff44f24d --- /dev/null +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/IsGvaUseCaseTest.kt @@ -0,0 +1,43 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.androidsdk.chat.ChatMessage +import com.glia.widgets.chat.model.Gva +import org.json.JSONObject +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class IsGvaUseCaseTest { + private lateinit var useCase: IsGvaUseCase + private lateinit var getGvaTypeUseCase: GetGvaTypeUseCase + private lateinit var chatMessage: ChatMessage + + @Before + fun setUp() { + chatMessage = mock() + getGvaTypeUseCase = mock() + useCase = IsGvaUseCase(getGvaTypeUseCase) + } + + @Test + fun `invoke returns true when gva type exists`() { + val metadata = JSONObject().put(Gva.Keys.TYPE, Gva.Type.PLAIN_TEXT.value) + whenever(chatMessage.metadata) doReturn metadata + whenever(getGvaTypeUseCase(metadata)) doReturn Gva.Type.PLAIN_TEXT + + assertTrue(useCase(chatMessage)) + } + + @Test + fun `invoke returns false when gva type doesn't exist`() { + val metadata = JSONObject().put(Gva.Keys.TYPE, "asdfg") + whenever(chatMessage.metadata) doReturn metadata + whenever(getGvaTypeUseCase(metadata)) doReturn null + + assertFalse(useCase(chatMessage)) + } +} diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCaseTest.kt new file mode 100644 index 000000000..e70b003ea --- /dev/null +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCaseTest.kt @@ -0,0 +1,73 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.androidsdk.Operator +import com.glia.widgets.chat.MockChatMessageInternal +import com.glia.widgets.chat.model.ChatState +import com.glia.widgets.chat.model.GvaGalleryCard +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class MapGvaGvaGalleryCardsUseCaseTest { + private lateinit var useCase: MapGvaGvaGalleryCardsUseCase + private lateinit var parseGvaGalleryCardsUseCase: ParseGvaGalleryCardsUseCase + private lateinit var chatState: ChatState + private lateinit var operator: Operator + private val mockChatMessageInternal: MockChatMessageInternal = MockChatMessageInternal() + + @Before + fun setUp() { + parseGvaGalleryCardsUseCase = mock() + whenever(parseGvaGalleryCardsUseCase(any())) doReturn emptyList() + + chatState = mock() + operator = mock() + + useCase = MapGvaGvaGalleryCardsUseCase(parseGvaGalleryCardsUseCase) + } + + @After + fun tearDown() { + mockChatMessageInternal.reset() + } + + @Test + fun `invoke parses GvaGalleryCard when ChatMessage passed with appropriate metadata`() { + mockChatMessageInternal.apply { + mockChatMessage() + mockOperatorProperties() + + val galleryCard = useCase(chatMessageInternal, chatState) + + assertEquals(galleryCard.messageId, messageId) + assertEquals(galleryCard.galleryCards, emptyList()) + assertEquals(galleryCard.showChatHead, false) + assertEquals(galleryCard.operatorId, operatorId) + assertEquals(galleryCard.messageId, messageId) + assertEquals(galleryCard.timestamp, messageTimeStamp) + assertEquals(galleryCard.operatorProfileImageUrl, operatorImageUrl) + assertEquals(galleryCard.operatorName, operatorName) + } + } + + @Test + fun `invoke takes operator data from chatState when it is null in ChatMessage`() { + mockChatMessageInternal.apply { + mockChatMessage() + mockOperatorPropertiesWithNull() + + whenever(chatState.operatorProfileImgUrl) doReturn operatorImageUrl + whenever(chatState.formattedOperatorName) doReturn operatorName + + val galleryCard = useCase(chatMessageInternal, chatState) + + assertEquals(galleryCard.operatorProfileImageUrl, operatorImageUrl) + assertEquals(galleryCard.operatorName, operatorName) + } + } +} diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCaseTest.kt new file mode 100644 index 000000000..5ad080c17 --- /dev/null +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCaseTest.kt @@ -0,0 +1,87 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.MockChatMessageInternal +import com.glia.widgets.chat.model.ChatState +import com.glia.widgets.chat.model.history.GvaChatItem +import com.glia.widgets.chat.model.history.GvaQuickReplies +import com.glia.widgets.chat.model.history.GvaResponseText +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class MapGvaGvaQuickRepliesUseCaseTest { + private val mockChatMessageInternal: MockChatMessageInternal = MockChatMessageInternal() + private lateinit var chatState: ChatState + private lateinit var useCase: MapGvaGvaQuickRepliesUseCase + private lateinit var parseGvaButtonsUseCase: ParseGvaButtonsUseCase + private lateinit var mapGvaResponseTextUseCase: MapGvaResponseTextUseCase + private lateinit var gvaResponseText: GvaResponseText + + @Before + fun setUp() { + chatState = mock() + + parseGvaButtonsUseCase = mock() + whenever(parseGvaButtonsUseCase(anyOrNull())) doReturn emptyList() + + gvaResponseText = mock() + + mapGvaResponseTextUseCase = mock() + whenever(mapGvaResponseTextUseCase(any(), any())) doReturn gvaResponseText + + useCase = MapGvaGvaQuickRepliesUseCase(parseGvaButtonsUseCase, mapGvaResponseTextUseCase) + } + + @After + fun tearDown() { + mockChatMessageInternal.reset() + } + + @Test + fun `invoke returns GvaResponseText when chat message is not last item in chat transcript`() { + mockChatMessageInternal.apply { + mockChatMessage() + mockOperatorProperties() + + whenever(chatMessageInternal.isHistory) doReturn true + whenever(chatMessageInternal.isLatest) doReturn false + + val gvaChatItem: GvaChatItem = useCase(chatMessageInternal, chatState) + assertTrue(gvaChatItem is GvaResponseText) + } + } + + @Test + fun `invoke returns GvaQuickReplies when chat message is the last item in chat transcript`() { + mockChatMessageInternal.apply { + mockChatMessage() + mockOperatorProperties() + + whenever(chatMessageInternal.isHistory) doReturn true + whenever(chatMessageInternal.isLatest) doReturn true + + val gvaChatItem: GvaChatItem = useCase(chatMessageInternal, chatState) + assertTrue(gvaChatItem is GvaQuickReplies) + } + } + + @Test + fun `invoke returns GvaQuickReplies when chat message is not from chat transcript`() { + mockChatMessageInternal.apply { + mockChatMessage() + mockOperatorProperties() + + whenever(chatMessageInternal.isHistory) doReturn false + whenever(chatMessageInternal.isLatest) doReturn false + + val gvaChatItem: GvaChatItem = useCase(chatMessageInternal, chatState) + assertTrue(gvaChatItem is GvaQuickReplies) + } + } +} diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCaseTest.kt new file mode 100644 index 000000000..d5fbb22bf --- /dev/null +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCaseTest.kt @@ -0,0 +1,74 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.MockChatMessageInternal +import com.glia.widgets.chat.model.ChatState +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaButton +import org.json.JSONObject +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class MapGvaPersistentButtonsUseCaseTest { + private val mockChatMessageInternal: MockChatMessageInternal = MockChatMessageInternal() + private lateinit var chatState: ChatState + private lateinit var useCase: MapGvaPersistentButtonsUseCase + private lateinit var parseGvaButtonsUseCase: ParseGvaButtonsUseCase + + @Before + fun setUp() { + chatState = mock() + + parseGvaButtonsUseCase = mock() + whenever(parseGvaButtonsUseCase(anyOrNull())) doReturn emptyList() + + useCase = MapGvaPersistentButtonsUseCase(parseGvaButtonsUseCase) + } + + @After + fun tearDown() { + mockChatMessageInternal.reset() + } + + @Test + fun `invoke parses GvaPersistentButtons when ChatMessage passed with appropriate metadata`() { + val content = "content" + mockChatMessageInternal.apply { + mockChatMessage(JSONObject().put(Gva.Keys.CONTENT, content)) + mockOperatorProperties() + + val galleryCard = useCase(chatMessageInternal, chatState) + + assertEquals(galleryCard.messageId, messageId) + assertEquals(galleryCard.content, content) + assertEquals(galleryCard.options, emptyList()) + assertEquals(galleryCard.showChatHead, false) + assertEquals(galleryCard.operatorId, operatorId) + assertEquals(galleryCard.messageId, messageId) + assertEquals(galleryCard.timestamp, messageTimeStamp) + assertEquals(galleryCard.operatorProfileImageUrl, operatorImageUrl) + assertEquals(galleryCard.operatorName, operatorName) + } + } + + @Test + fun `invoke takes operator data from chatState when it is null in ChatMessage`() { + mockChatMessageInternal.apply { + mockChatMessage() + mockOperatorPropertiesWithNull() + + whenever(chatState.operatorProfileImgUrl) doReturn operatorImageUrl + whenever(chatState.formattedOperatorName) doReturn operatorName + + val galleryCard = useCase(chatMessageInternal, chatState) + + assertEquals(galleryCard.operatorProfileImageUrl, operatorImageUrl) + assertEquals(galleryCard.operatorName, operatorName) + } + } +} diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCaseTest.kt new file mode 100644 index 000000000..7ff1d49d0 --- /dev/null +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCaseTest.kt @@ -0,0 +1,66 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.MockChatMessageInternal +import com.glia.widgets.chat.model.ChatState +import com.glia.widgets.chat.model.Gva +import org.json.JSONObject +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class MapGvaResponseTextUseCaseTest { + private val mockChatMessageInternal: MockChatMessageInternal = MockChatMessageInternal() + private lateinit var chatState: ChatState + private lateinit var useCase: MapGvaResponseTextUseCase + + @Before + fun setUp() { + chatState = mock() + useCase = MapGvaResponseTextUseCase() + } + + @After + fun tearDown() { + mockChatMessageInternal.reset() + } + + @Test + fun `invoke parses GvaResponseText when ChatMessage passed with appropriate metadata`() { + val content = "content" + mockChatMessageInternal.apply { + mockChatMessage(JSONObject().put(Gva.Keys.CONTENT, content)) + mockOperatorProperties() + + val galleryCard = useCase(chatMessageInternal, chatState) + + assertEquals(galleryCard.messageId, messageId) + assertEquals(galleryCard.content, content) + assertEquals(galleryCard.showChatHead, false) + assertEquals(galleryCard.operatorId, operatorId) + assertEquals(galleryCard.messageId, messageId) + assertEquals(galleryCard.timestamp, messageTimeStamp) + assertEquals(galleryCard.operatorProfileImageUrl, operatorImageUrl) + assertEquals(galleryCard.operatorName, operatorName) + } + } + + @Test + fun `invoke takes operator data from chatState when it is null in ChatMessage`() { + mockChatMessageInternal.apply { + mockChatMessage() + mockOperatorPropertiesWithNull() + + whenever(chatState.operatorProfileImgUrl) doReturn operatorImageUrl + whenever(chatState.formattedOperatorName) doReturn operatorName + + val galleryCard = useCase(chatMessageInternal, chatState) + + assertEquals(galleryCard.operatorProfileImageUrl, operatorImageUrl) + assertEquals(galleryCard.operatorName, operatorName) + } + } +} diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaUseCaseTest.kt new file mode 100644 index 000000000..8e70b06fd --- /dev/null +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaUseCaseTest.kt @@ -0,0 +1,108 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.MockChatMessageInternal +import com.glia.widgets.chat.model.ChatState +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.history.GvaGalleryCards +import com.glia.widgets.chat.model.history.GvaPersistentButtons +import com.glia.widgets.chat.model.history.GvaQuickReplies +import com.glia.widgets.chat.model.history.GvaResponseText +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class MapGvaUseCaseTest { + private var mockChatMessageInternal = MockChatMessageInternal() + private lateinit var getGvaTypeUseCase: GetGvaTypeUseCase + private lateinit var mapGvaResponseTextUseCase: MapGvaResponseTextUseCase + private lateinit var mapGvaPersistentButtonsUseCase: MapGvaPersistentButtonsUseCase + private lateinit var mapGvaGvaQuickRepliesUseCase: MapGvaGvaQuickRepliesUseCase + private lateinit var mapGvaGvaGalleryCardsUseCase: MapGvaGvaGalleryCardsUseCase + + private lateinit var chatState: ChatState + + private lateinit var useCase: MapGvaUseCase + + @Before + fun setUp() { + getGvaTypeUseCase = mock() + mapGvaResponseTextUseCase = mock() + mapGvaPersistentButtonsUseCase = mock() + mapGvaGvaQuickRepliesUseCase = mock() + mapGvaGvaGalleryCardsUseCase = mock() + + chatState = mock() + + useCase = MapGvaUseCase( + getGvaTypeUseCase, + mapGvaResponseTextUseCase, + mapGvaPersistentButtonsUseCase, + mapGvaGvaQuickRepliesUseCase, + mapGvaGvaGalleryCardsUseCase + ) + + mockChatMessageInternal.mockChatMessage() + mockChatMessageInternal.mockOperatorProperties() + } + + @After + fun tearDown() { + mockChatMessageInternal.reset() + } + + @Test + fun `invoke returns GvaResponseText when GVA type is PLAIN_TEXT`() { + whenever(getGvaTypeUseCase(any())) doReturn Gva.Type.PLAIN_TEXT + whenever(mapGvaResponseTextUseCase(any(), any())) doReturn mock() + + mockChatMessageInternal.apply { + val gva = useCase(chatMessageInternal, chatState) + assertTrue(gva is GvaResponseText) + } + } + + @Test + fun `invoke returns GvaPersistentButtons when GVA type is PERSISTENT_BUTTONS`() { + whenever(getGvaTypeUseCase(any())) doReturn Gva.Type.PERSISTENT_BUTTONS + whenever(mapGvaPersistentButtonsUseCase(any(), any())) doReturn mock() + + mockChatMessageInternal.apply { + val gva = useCase(chatMessageInternal, chatState) + assertTrue(gva is GvaPersistentButtons) + } + } + + @Test + fun `invoke returns GvaQuickReplies when GVA type is QUICK_REPLIES`() { + whenever(getGvaTypeUseCase(any())) doReturn Gva.Type.QUICK_REPLIES + whenever(mapGvaGvaQuickRepliesUseCase(any(), any())) doReturn mock() + + mockChatMessageInternal.apply { + val gva = useCase(chatMessageInternal, chatState) + assertTrue(gva is GvaQuickReplies) + } + } + + @Test + fun `invoke returns GvaGalleryCards when GVA type is GALLERY_CARDS`() { + whenever(getGvaTypeUseCase(any())) doReturn Gva.Type.GALLERY_CARDS + whenever(mapGvaGvaGalleryCardsUseCase(any(), any())) doReturn mock() + + mockChatMessageInternal.apply { + val gva = useCase(chatMessageInternal, chatState) + assertTrue(gva is GvaGalleryCards) + } + } + + @Test(expected = IllegalArgumentException::class) + fun `invoke throws exception when ChatMessage is not GVA message`() { + whenever(getGvaTypeUseCase(any())) doReturn null + + mockChatMessageInternal.apply { useCase(chatMessageInternal, chatState) } + } +} diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/ParseGvaButtonsUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/ParseGvaButtonsUseCaseTest.kt new file mode 100644 index 000000000..32602a7f2 --- /dev/null +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/ParseGvaButtonsUseCaseTest.kt @@ -0,0 +1,49 @@ +package com.glia.widgets.chat.domain.gva + +import android.readRawResource +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaButton +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class ParseGvaButtonsUseCaseTest { + private val gson: Gson = GsonBuilder().create() + private lateinit var useCase: ParseGvaButtonsUseCase + + @Before + fun setUp() { + useCase = ParseGvaButtonsUseCase(gson) + } + + @Test + fun `invoke parses GvaButtons list when proper json passed`() { + val jsonString = javaClass.readRawResource("gva_persistent_buttons.json") + + val jsonObject = JSONObject(jsonString).getJSONObject("metadata") + + val buttonsJson = jsonObject.getJSONArray(Gva.Keys.OPTIONS) + val buttons = useCase(jsonObject) + + for (index in 0 until buttons.count()) { + val buttonJson = buttonsJson.getJSONObject(index) + val button = buttons[index] + assertEquals(button.text, buttonJson["text"]) + assertEquals(button.url, buttonJson.opt("url")) + } + } + + @Test + fun `invoke returns empty list when invalid JSON passed`() { + + val jsonObject = JSONObject() + + val buttons = useCase(jsonObject) + + assertEquals(buttons, emptyList()) + + } +} diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/ParseGvaGalleryCardsUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/ParseGvaGalleryCardsUseCaseTest.kt new file mode 100644 index 000000000..14f0e2f1f --- /dev/null +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/ParseGvaGalleryCardsUseCaseTest.kt @@ -0,0 +1,48 @@ +package com.glia.widgets.chat.domain.gva + +import android.readRawResource +import com.glia.widgets.chat.model.Gva +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import org.json.JSONObject +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +class ParseGvaGalleryCardsUseCaseTest { + private val gson: Gson = GsonBuilder().create() + private lateinit var useCase: ParseGvaGalleryCardsUseCase + + @Before + fun setUp() { + useCase = ParseGvaGalleryCardsUseCase(gson) + } + + @Test + fun `invoke parses GvaGalleryCards list when proper json passed`() { + val jsonString = javaClass.readRawResource("gva_gallery.json") + + val jsonObject = JSONObject(jsonString).getJSONObject("metadata") + + val galleryCardsJson = jsonObject.getJSONArray(Gva.Keys.GALLERY_CARDS) + val galleryCards = useCase(jsonObject) + + for (index in 0 until galleryCards.count()) { + val cardJson = galleryCardsJson.getJSONObject(index) + val card = galleryCards[index] + Assert.assertEquals(card.title, cardJson["title"]) + Assert.assertEquals(card.subtitle, cardJson.opt("subtitle")) + + val buttons = card.options + val buttonsJson = cardJson.getJSONArray(Gva.Keys.OPTIONS) + + for (buttonIndex in 0 until buttons.count()) { + val buttonJson = buttonsJson.getJSONObject(index) + val button = buttons[index] + Assert.assertEquals(button.text, buttonJson["text"]) + Assert.assertEquals(button.url, buttonJson.opt("url")) + } + + } + } +} diff --git a/widgetssdk/src/test/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCaseTest.kt index 4f509a7b6..8b6ea4c40 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCaseTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCaseTest.kt @@ -34,7 +34,7 @@ class MapOperatorUseCaseTest { mapOperatorUseCase(chatMessage) .test() .assertComplete() - .assertNever { it.operator.isPresent } + .assertNever { it.operator != null } } @Test @@ -44,9 +44,9 @@ class MapOperatorUseCaseTest { whenever(chatMessage.operatorId) doReturn operator.id whenever(getOperatorUseCase.execute(operator.id)) doReturn (Single.just(Optional.of(operator))) mapOperatorUseCase(chatMessage) - .doOnSuccess { assertEquals(it.operatorId.get(), operator.id) } + .doOnSuccess { assertEquals(it.operatorId, operator.id) } .test() .assertComplete() - .assertNever { !it.operator.isPresent } + .assertNever { it.operator == null } } } diff --git a/widgetssdk/src/test/resources/gva_gallery.json b/widgetssdk/src/test/resources/gva_gallery.json new file mode 100644 index 000000000..308a24ccc --- /dev/null +++ b/widgetssdk/src/test/resources/gva_gallery.json @@ -0,0 +1,47 @@ +{ + "content": "\"\"", + "type": "chat", + "metadata": { + "type": "galleryCards", + "galleryCards": [ + { + "title": "Example Card", + "subtitle": "Subtitle text", + "imageUrl": "http://d2e39xiit5tjik.cloudfront.net/finnapp/galleries/InvestmentAccounts.jpg", + "options": [ + { + "text": "Postback Button", + "value": "{\"utterance\":\"Postback Button\",\"payload\":{\"routingData\":{},\"destinationUtterance\":\"Cancel\",\"messageType\":\"buttonTap\"}}" + }, + { + "text": "Open Url - Modal", + "url": "https://www.glia.com", + "urlTarget": "modal" + }, + { + "text": "Open Url - Same Tab", + "url": "https://www.glia.com", + "urlTarget": "self" + }, + { + "text": "Open Url - New Tab", + "url": "https://www.glia.com", + "urlTarget": "blank" + }, + { + "text": "Call Phone Number", + "url": "tel:555-555-5555", + "urlTarget": "self" + }, + { + "text": "Broadcast Event", + "value": "{\"utterance\":\"Broadcast Event\",\"payload\":{\"userId\":\"dummy-user-id\",\"messageType\":\"buttonTap\",\"broadcastEventName\":\"dummy-event\"}}" + } + ] + } + ], + "glia": { + "message_group_id": "8d1de3fc-4e2f-423a-8ba2-8fa76770c4f3" + } + } +} diff --git a/widgetssdk/src/test/resources/gva_persistent_buttons.json b/widgetssdk/src/test/resources/gva_persistent_buttons.json new file mode 100644 index 000000000..3dc59bd4e --- /dev/null +++ b/widgetssdk/src/test/resources/gva_persistent_buttons.json @@ -0,0 +1,41 @@ +{ + "content": "Example Text with PBs", + "type": "chat", + "metadata": { + "type": "persistentButtons", + "content": "Example Text with PBs", + "options": [ + { + "text": "Postback Button", + "value": "{\"utterance\":\"Postback Button\",\"payload\":{\"routingData\":{},\"destinationUtterance\":\"Cancel\",\"messageType\":\"buttonTap\"}}" + }, + { + "text": "Open Url - Modal", + "url": "https://www.glia.com", + "urlTarget": "modal" + }, + { + "text": "Open Url - Same Tab", + "url": "https://www.glia.com", + "urlTarget": "self" + }, + { + "text": "Open Url - New Tab", + "url": "https://www.glia.com", + "urlTarget": "blank" + }, + { + "text": "Call Phone Number", + "url": "tel:555-555-5555", + "urlTarget": "self" + }, + { + "text": "Broadcast Event", + "value": "{\"utterance\":\"Broadcast Event\",\"payload\":{\"userId\":\"dummy-user-id\",\"messageType\":\"buttonTap\",\"broadcastEventName\":\"dummy-event\"}}" + } + ], + "glia": { + "message_group_id": "a565a0f5-81f2-4ebf-a79a-a0d484bde96f" + } + } +} diff --git a/widgetssdk/src/test/resources/gva_plain_text.json b/widgetssdk/src/test/resources/gva_plain_text.json new file mode 100644 index 000000000..88a8d3ae4 --- /dev/null +++ b/widgetssdk/src/test/resources/gva_plain_text.json @@ -0,0 +1,11 @@ +{ + "content": "Example text", + "type": "chat", + "metadata": { + "type": "plainText", + "content": "Example text", + "glia": { + "message_group_id": "d8a0880f-abb9-4797-ba0a-902cf3a5cd83" + } + } +} diff --git a/widgetssdk/src/test/resources/gva_quick_reply.json b/widgetssdk/src/test/resources/gva_quick_reply.json new file mode 100644 index 000000000..557d39b0b --- /dev/null +++ b/widgetssdk/src/test/resources/gva_quick_reply.json @@ -0,0 +1,14 @@ +{ + "content": "Example Text with QRs", + "type": "chat", + "metadata": { + "type": "quickReplies", + "content": "Example Text with QRs", + "options": [ + { + "text": "Postback QR Button", + "value": "{\"utterance\":\"Postback QR Button\",\"payload\":{\"routingData\":{},\"destinationUtterance\":\"What products do you offer?\",\"messageType\":\"quickReplyTap\"}}" + } + ] + } +} From 5df614c042403e47ca0cd6ccdc0b6841b8e0acad Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Wed, 12 Jul 2023 19:56:45 +0300 Subject: [PATCH 03/69] - Add Use cases to determine GvaButton type - Add callback to handle gva Button click in ChatAdapter --- build.gradle | 111 ++++++------- widgetssdk/build.gradle | 153 +++++++++--------- .../java/com/glia/widgets/chat/ChatView.kt | 5 + .../glia/widgets/chat/adapter/ChatAdapter.kt | 24 +-- .../widgets/chat/controller/ChatController.kt | 15 +- .../gva/DetermineGvaButtonTypeUseCase.kt | 16 ++ .../domain/gva/DetermineGvaUrlTypeUseCase.kt | 16 ++ .../glia/widgets/chat/model/GvaBaseTypes.kt | 18 ++- .../glia/widgets/di/ControllerFactory.java | 3 +- .../com/glia/widgets/di/UseCaseFactory.java | 12 ++ .../chat/controller/ChatControllerTest.kt | 6 +- .../gva/DetermineGvaButtonTypeUseCaseTest.kt | 61 +++++++ .../gva/DetermineGvaUrlTypeUseCaseTest.kt | 36 +++++ 13 files changed, 319 insertions(+), 157 deletions(-) create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaButtonTypeUseCase.kt create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaUrlTypeUseCase.kt create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/DetermineGvaButtonTypeUseCaseTest.kt create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/DetermineGvaUrlTypeUseCaseTest.kt diff --git a/build.gradle b/build.gradle index 030e2b0ed..93a987d10 100644 --- a/build.gradle +++ b/build.gradle @@ -1,49 +1,49 @@ buildscript { - ext { - kotlin_version = '1.8.20' - } - repositories { - google() - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:8.0.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } + ext { + kotlin_version = '1.8.20' + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.0.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } } plugins { - id("io.github.gradle-nexus.publish-plugin") version "1.1.0" - id("org.jetbrains.kotlin.android") version "$kotlin_version" apply false - id("org.jlleitschuh.gradle.ktlint") version "11.3.2" + id("io.github.gradle-nexus.publish-plugin") version "1.1.0" + id("org.jetbrains.kotlin.android") version "$kotlin_version" apply false + id("org.jlleitschuh.gradle.ktlint") version "11.3.2" } allprojects { - repositories { - google() - mavenCentral() - flatDir { - dirs '../libs' - } + repositories { + google() + mavenCentral() + flatDir { + dirs '../libs' } + } } subprojects { - apply plugin: 'org.jlleitschuh.gradle.ktlint' + apply plugin: 'org.jlleitschuh.gradle.ktlint' - repositories { - mavenCentral() - } + repositories { + mavenCentral() + } - ktlint { - outputToConsole.set(true) - debug.set(true) - android.set(true) - } + ktlint { + outputToConsole.set(true) + debug.set(true) + android.set(true) + } } task clean(type: Delete) { - delete rootProject.buildDir + delete rootProject.buildDir } apply from: "${rootDir}/scripts/publish-settings.gradle" @@ -52,31 +52,32 @@ apply from: "${rootDir}/scripts/version-updater.gradle" ext { // gliaSdkVersion = 'X.X.X' // this property is now attached dynamically during gradle sync - appCompatVersion = '1.3.1' - materialVersion = '1.4.0' - constraintLayoutVersion = '2.1.3' - navVersion = '2.3.5' - lifecycle_process = '2.3.1' - rxAndroid = '2.1.0' - rxJava = '2.2.6' - gson = '2.8.6' - preferenceVersion = '1.1.1' - picassoVersion = '2.8' - lottieVersion = '5.2.0' - firebaseVersion = '31.0.2' - audioswitch = '1.1.8' + appCompatVersion = '1.3.1' + materialVersion = '1.4.0' + constraintLayoutVersion = '2.1.3' + navVersion = '2.3.5' + lifecycle_process = '2.3.1' + rxAndroid = '2.1.0' + rxJava = '2.2.6' + gson = '2.8.6' + preferenceVersion = '1.1.1' + picassoVersion = '2.8' + lottieVersion = '5.2.0' + firebaseVersion = '31.0.2' + audioswitch = '1.1.8' - leakCanaryVersion = '2.6' - espressoVersion = '3.5.1' - junitVersion = '4.13.2' - testLibraryVersion = '1.1.5' - androidXTestVersion = '1.1.5' - mockitoVersion = '5.1.1' - mockitoKotlinVersion = '4.1.0' - mockitoAndroidTestVersion = '4.3.1' - archCoreVersion = '2.2.0' - testRulesVersion = '1.5.0' + leakCanaryVersion = '2.6' + espressoVersion = '3.5.1' + junitVersion = '4.13.2' + testLibraryVersion = '1.1.5' + androidXTestVersion = '1.1.5' + mockitoVersion = '5.1.1' + mockitoKotlinVersion = '4.1.0' + mockitoAndroidTestVersion = '4.3.1' + archCoreVersion = '2.2.0' + testRulesVersion = '1.5.0' + robolectricVersion = '4.9' - //kotlin - coreKtxVersion = '1.8.0' + //kotlin + coreKtxVersion = '1.8.0' } diff --git a/widgetssdk/build.gradle b/widgetssdk/build.gradle index 050e41ce7..96c37e70b 100644 --- a/widgetssdk/build.gradle +++ b/widgetssdk/build.gradle @@ -3,99 +3,100 @@ apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'kotlin-parcelize' android { - compileSdkVersion 31 - buildToolsVersion "30.0.3" - defaultPublishConfig "debug" - defaultConfig { - minSdkVersion 24 - targetSdkVersion 31 - versionCode widgetsVersionCode - versionName widgetsVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" - } + compileSdkVersion 31 + buildToolsVersion "30.0.3" + defaultPublishConfig "debug" + defaultConfig { + minSdkVersion 24 + targetSdkVersion 31 + versionCode widgetsVersionCode + versionName widgetsVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } - buildTypes { - all { - buildConfigField("String", "GLIA_CORE_SDK_VERSION", "\"$gliaSdkVersion\"") - buildConfigField("String", "GLIA_WIDGETS_SDK_VERSION", "\"$defaultConfig.versionName\"") - } - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + buildTypes { + all { + buildConfigField("String", "GLIA_CORE_SDK_VERSION", "\"$gliaSdkVersion\"") + buildConfigField("String", "GLIA_WIDGETS_SDK_VERSION", "\"$defaultConfig.versionName\"") } - kotlinOptions { - jvmTarget = "11" - } - lint { - disable 'WrongLayoutName', - 'LayoutFileNameMatchesClass', - 'MatchingViewId', - 'RawDimen', - 'WrongAnnotationOrder', - 'ColorCasing', - 'WrongViewIdFormat', - 'HardcodedText' + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + lint { + disable 'WrongLayoutName', + 'LayoutFileNameMatchesClass', + 'MatchingViewId', + 'RawDimen', + 'WrongAnnotationOrder', + 'ColorCasing', + 'WrongViewIdFormat', + 'HardcodedText' + } - buildFeatures { - viewBinding true - buildConfig true - } - namespace 'com.glia.widgets' + buildFeatures { + viewBinding true + buildConfig true + } + namespace 'com.glia.widgets' } dependencies { - implementation "androidx.appcompat:appcompat:$appCompatVersion" - implementation "com.google.android.material:material:$materialVersion" - implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" - implementation "androidx.lifecycle:lifecycle-process:$lifecycle_process" - implementation "com.squareup.picasso:picasso:$picassoVersion" + implementation "androidx.appcompat:appcompat:$appCompatVersion" + implementation "com.google.android.material:material:$materialVersion" + implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" + implementation "androidx.lifecycle:lifecycle-process:$lifecycle_process" + implementation "com.squareup.picasso:picasso:$picassoVersion" - // Used for audio and video permissions by CallActivity - implementation "io.reactivex.rxjava2:rxandroid:$rxAndroid" - implementation "io.reactivex.rxjava2:rxjava:$rxJava" + // Used for audio and video permissions by CallActivity + implementation "io.reactivex.rxjava2:rxandroid:$rxAndroid" + implementation "io.reactivex.rxjava2:rxjava:$rxJava" - // removed because becomes slow on Android 11. They say it is fixed, but I still have the issue. - // https://square.github.io/leakcanary/changelog/ - // debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion" + // removed because becomes slow on Android 11. They say it is fixed, but I still have the issue. + // https://square.github.io/leakcanary/changelog/ + // debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion" - api "com.glia:android-sdk:$gliaSdkVersion" - // Glia sdk aar - // implementation(name: "glia-core-sdk-$gliaSdkVersion", ext: 'aar') + api "com.glia:android-sdk:$gliaSdkVersion" + // Glia sdk aar + // implementation(name: "glia-core-sdk-$gliaSdkVersion", ext: 'aar') // api "com.glia:android-sdk:0.26.6-SNAPSHOT" // Used when testing local Core SDK snapshot releases - implementation "com.twilio:audioswitch:$audioswitch" - implementation "com.airbnb.android:lottie:$lottieVersion" - implementation "com.google.code.gson:gson:$gson" - implementation "androidx.core:core-ktx:$coreKtxVersion" + implementation "com.twilio:audioswitch:$audioswitch" + implementation "com.airbnb.android:lottie:$lottieVersion" + implementation "com.google.code.gson:gson:$gson" + implementation "androidx.core:core-ktx:$coreKtxVersion" - testImplementation "junit:junit:$junitVersion" - testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" - testImplementation "androidx.arch.core:core-testing:$archCoreVersion" - androidTestImplementation "org.mockito:mockito-android:$mockitoAndroidTestVersion" - androidTestImplementation "androidx.test.ext:junit:$androidXTestVersion" - androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" - androidTestImplementation "androidx.test:rules:$testRulesVersion" + testImplementation "junit:junit:$junitVersion" + testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" + testImplementation "androidx.arch.core:core-testing:$archCoreVersion" + testImplementation "org.robolectric:robolectric:$robolectricVersion" + androidTestImplementation "org.mockito:mockito-android:$mockitoAndroidTestVersion" + androidTestImplementation "androidx.test.ext:junit:$androidXTestVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" + androidTestImplementation "androidx.test:rules:$testRulesVersion" - // Test dependencies to Android native libraries - // To prevent unit tests firing errors like 'Method length in org.json.JSONObject not mocked' - testImplementation "org.json:json:20220924" - implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + // Test dependencies to Android native libraries + // To prevent unit tests firing errors like 'Method length in org.json.JSONObject not mocked' + testImplementation "org.json:json:20220924" + implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" } ext { - PUBLISH_GROUP_ID = 'com.glia' - PUBLISH_VERSION = android.defaultConfig.versionName - PUBLISH_ARTIFACT_ID = 'android-widgets' - PUBLISH_MODULE_DESCRIPTION = 'Glia Android Widgets SDK' - PUBLISH_ORGANISATION_NAME = 'Glia' - PUBLISH_ORGANISATION_URL = 'https://www.glia.com/' + PUBLISH_GROUP_ID = 'com.glia' + PUBLISH_VERSION = android.defaultConfig.versionName + PUBLISH_ARTIFACT_ID = 'android-widgets' + PUBLISH_MODULE_DESCRIPTION = 'Glia Android Widgets SDK' + PUBLISH_ORGANISATION_NAME = 'Glia' + PUBLISH_ORGANISATION_URL = 'https://www.glia.com/' } apply from: "${rootProject.projectDir}/scripts/publish-module.gradle" diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt index 72fb160ba..36f2555f7 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt @@ -210,6 +210,10 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty ) } + private val onGvaButtonsClickListener = ChatAdapter.OnGvaButtonsClickListener { + controller?.onGvaButtonClicked(it) + } + init { initConfigurations() bindViews() @@ -709,6 +713,7 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty this, this, onCustomCardResponse, + onGvaButtonsClickListener, GliaWidgets.getCustomCardAdapter(), Dependencies.getUseCaseFactory().createGetImageFileFromCacheUseCase(), Dependencies.getUseCaseFactory().createGetImageFileFromDownloadsUseCase(), diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt index 05eecee11..298b6c627 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt @@ -21,6 +21,7 @@ import com.glia.widgets.chat.adapter.holder.imageattachment.ImageAttachmentViewH import com.glia.widgets.chat.adapter.holder.imageattachment.OperatorImageAttachmentViewHolder import com.glia.widgets.chat.adapter.holder.imageattachment.VisitorImageAttachmentViewHolder import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaButton import com.glia.widgets.chat.model.history.ChatItem import com.glia.widgets.chat.model.history.CustomCardItem import com.glia.widgets.chat.model.history.GvaGalleryCards @@ -51,6 +52,7 @@ internal class ChatAdapter( private val onFileItemClickListener: OnFileItemClickListener, private val onImageItemClickListener: OnImageItemClickListener, private val onCustomCardResponse: OnCustomCardResponse, + private val onGvaButtonsClickListener: OnGvaButtonsClickListener, private val customCardAdapter: CustomCardAdapter?, private val getImageFileFromCacheUseCase: GetImageFileFromCacheUseCase, private val getImageFileFromDownloadsUseCase: GetImageFileFromDownloadsUseCase, @@ -173,18 +175,6 @@ internal class ChatAdapter( ) } -// TODO should be changed to appropriate ViewHolder later - MOB 2396 - GVA_QUICK_REPLIES_TYPE -> { - SystemMessageViewHolder( - ChatReceiveMessageContentBinding.inflate( - inflater, - parent, - false - ), - uiTheme - ) - } - // TODO should be changed to appropriate ViewHolder later - MOB 2404 GVA_GALLERY_CARDS_TYPE -> { SystemMessageViewHolder( @@ -322,6 +312,10 @@ internal class ChatAdapter( fun onCustomCardResponse(messageId: String, text: String, value: String) } + fun interface OnGvaButtonsClickListener { + fun onGvaButtonClicked(gvaButton: GvaButton) + } + companion object { const val OPERATOR_STATUS_VIEW_TYPE = 0 const val VISITOR_MESSAGE_TYPE = 1 @@ -337,11 +331,10 @@ internal class ChatAdapter( //GVA Types const val GVA_RESPONSE_TEXT_TYPE = 10 const val GVA_PERSISTENT_BUTTONS_TYPE = 11 - const val GVA_QUICK_REPLIES_TYPE = 12 - const val GVA_GALLERY_CARDS_TYPE = 13 + const val GVA_GALLERY_CARDS_TYPE = 12 //Custom Card - const val CUSTOM_CARD_TYPE = 14 // Should be the last type with the highest value + const val CUSTOM_CARD_TYPE = 13 // Should be the last type with the highest value } @IntDef( @@ -358,7 +351,6 @@ internal class ChatAdapter( CUSTOM_CARD_TYPE, GVA_RESPONSE_TEXT_TYPE, GVA_PERSISTENT_BUTTONS_TYPE, - GVA_QUICK_REPLIES_TYPE, GVA_GALLERY_CARDS_TYPE ) @Retention(AnnotationRetention.SOURCE) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index e3af5d56d..11e2bc424 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -43,10 +43,12 @@ import com.glia.widgets.chat.domain.IsShowSendButtonUseCase import com.glia.widgets.chat.domain.PreEngagementMessageUseCase import com.glia.widgets.chat.domain.SiteInfoUseCase import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase +import com.glia.widgets.chat.domain.gva.DetermineGvaButtonTypeUseCase import com.glia.widgets.chat.domain.gva.IsGvaUseCase import com.glia.widgets.chat.domain.gva.MapGvaUseCase import com.glia.widgets.chat.model.ChatInputMode import com.glia.widgets.chat.model.ChatState +import com.glia.widgets.chat.model.Gva import com.glia.widgets.chat.model.GvaButton import com.glia.widgets.chat.model.history.ChatItem import com.glia.widgets.chat.model.history.CustomCardItem @@ -178,7 +180,8 @@ internal class ChatController( private val isFileReadyForPreviewUseCase: IsFileReadyForPreviewUseCase, private val acceptMediaUpgradeOfferUseCase: AcceptMediaUpgradeOfferUseCase, private val isGvaUseCase: IsGvaUseCase, - private val mapGvaUseCase: MapGvaUseCase + private val mapGvaUseCase: MapGvaUseCase, + private val determineGvaButtonTypeUseCase: DetermineGvaButtonTypeUseCase ) : GliaOnEngagementUseCase.Listener, GliaOnEngagementEndUseCase.Listener, OnSurveyListener { private var backClickedListener: ChatView.OnBackClickedListener? = null private var viewCallback: ChatViewCallback? = null @@ -1714,4 +1717,14 @@ internal class ChatController( fun isCallVisualizerOngoing(): Boolean { return isCallVisualizerUseCase() } + + fun onGvaButtonClicked(button: GvaButton) { + when (determineGvaButtonTypeUseCase(button)) { + Gva.ButtonType.BroadcastEvent -> TODO("will be implemented in next tasks") + is Gva.ButtonType.Email -> TODO("will be implemented in next tasks") + is Gva.ButtonType.Phone -> TODO("will be implemented in next tasks") + is Gva.ButtonType.PostBack -> TODO("will be implemented in next tasks") + is Gva.ButtonType.Url -> TODO("will be implemented in next tasks") + } + } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaButtonTypeUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaButtonTypeUseCase.kt new file mode 100644 index 000000000..5807161c6 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaButtonTypeUseCase.kt @@ -0,0 +1,16 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaButton + +internal const val PHONE_SCHEME = "tel" +internal const val EMAIL_SCHEME = "mailto" + +internal class DetermineGvaButtonTypeUseCase(private val determineGvaUrlTypeUseCase: DetermineGvaUrlTypeUseCase) { + + operator fun invoke(button: GvaButton): Gva.ButtonType = when { + !button.destinationPbBroadcastEvent.isNullOrBlank() -> Gva.ButtonType.BroadcastEvent + button.url.isNullOrBlank() -> Gva.ButtonType.PostBack(button.toResponse()) + else -> determineGvaUrlTypeUseCase(button.url) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaUrlTypeUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaUrlTypeUseCase.kt new file mode 100644 index 000000000..28da2474f --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/DetermineGvaUrlTypeUseCase.kt @@ -0,0 +1,16 @@ +package com.glia.widgets.chat.domain.gva + +import android.net.Uri +import com.glia.widgets.chat.model.Gva + +internal class DetermineGvaUrlTypeUseCase { + + operator fun invoke(url: String): Gva.ButtonType { + val uri = Uri.parse(url) + return when (uri.scheme) { + PHONE_SCHEME -> Gva.ButtonType.Phone(uri) + EMAIL_SCHEME -> Gva.ButtonType.Email(uri) + else -> Gva.ButtonType.Url(uri) + } + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt index 67469785c..c27a19dd7 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt @@ -1,6 +1,8 @@ package com.glia.widgets.chat.model +import android.net.Uri import androidx.annotation.StringDef +import com.glia.androidsdk.chat.SingleChoiceAttachment import com.google.gson.annotations.SerializedName internal object Gva { @@ -32,6 +34,14 @@ internal object Gva { const val BLANK = "blank" } } + + sealed interface ButtonType { + object BroadcastEvent : ButtonType + data class PostBack(val singleChoiceAttachment: SingleChoiceAttachment) : ButtonType + data class Phone(val uri: Uri) : ButtonType + data class Email(val uri: Uri) : ButtonType + data class Url(val uri: Uri) : ButtonType + } } internal data class GvaButton( @@ -49,13 +59,7 @@ internal data class GvaButton( @SerializedName("transferPhoneNumber") val transferPhoneNumber: String? = null ) { - val isPostBack: Boolean - get() = url.isNullOrBlank() - - val isBroadCastEvent: Boolean - get() = !destinationPbBroadcastEvent.isNullOrBlank() - -// fun toResponse(): SingleChoiceAttachment = SingleChoiceAttachment.from(text, value) TODO should be available with core sdk's next release. + fun toResponse(): SingleChoiceAttachment = SingleChoiceAttachment.from(text, value) } internal data class GvaGalleryCard( diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java index b63221c54..4b3950a00 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java @@ -138,7 +138,8 @@ public ChatController getChatController(ChatViewCallback chatViewCallback) { useCaseFactory.createIsFileReadyForPreviewUseCase(), useCaseFactory.createAcceptMediaUpgradeOfferUseCase(), useCaseFactory.createIsGvaUseCase(), - useCaseFactory.createMapGvaUseCase() + useCaseFactory.createMapGvaUseCase(), + useCaseFactory.createDetermineGvaButtonTypeUseCase() ); } else { Logger.d(TAG, "retained chat controller"); diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java index b07cfc082..8fc37e4e1 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java @@ -27,6 +27,8 @@ import com.glia.widgets.chat.domain.PreEngagementMessageUseCase; import com.glia.widgets.chat.domain.SiteInfoUseCase; import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase; +import com.glia.widgets.chat.domain.gva.DetermineGvaButtonTypeUseCase; +import com.glia.widgets.chat.domain.gva.DetermineGvaUrlTypeUseCase; import com.glia.widgets.chat.domain.gva.GetGvaTypeUseCase; import com.glia.widgets.chat.domain.gva.IsGvaUseCase; import com.glia.widgets.chat.domain.gva.MapGvaGvaGalleryCardsUseCase; @@ -892,6 +894,16 @@ public MapGvaUseCase createMapGvaUseCase() { ); } + @NonNull + public DetermineGvaUrlTypeUseCase createDetermineGvaUrlTypeUseCase() { + return new DetermineGvaUrlTypeUseCase(); + } + + @NonNull + public DetermineGvaButtonTypeUseCase createDetermineGvaButtonTypeUseCase() { + return new DetermineGvaButtonTypeUseCase(createDetermineGvaUrlTypeUseCase()); + } + public void resetState() { createResetSurveyUseCase().invoke(); } diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt index 20a8758b8..893d5f687 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt @@ -18,6 +18,7 @@ import com.glia.widgets.chat.domain.IsShowSendButtonUseCase import com.glia.widgets.chat.domain.PreEngagementMessageUseCase import com.glia.widgets.chat.domain.SiteInfoUseCase import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase +import com.glia.widgets.chat.domain.gva.DetermineGvaButtonTypeUseCase import com.glia.widgets.chat.domain.gva.IsGvaUseCase import com.glia.widgets.chat.domain.gva.MapGvaUseCase import com.glia.widgets.chat.model.history.ChatItem @@ -127,6 +128,7 @@ class ChatControllerTest { private lateinit var acceptMediaUpgradeOfferUseCase: AcceptMediaUpgradeOfferUseCase private lateinit var isGvaUseCase: IsGvaUseCase private lateinit var mapGvaUseCase: MapGvaUseCase + private lateinit var determineGvaButtonTypeUseCase: DetermineGvaButtonTypeUseCase private lateinit var chatController: ChatController @@ -186,6 +188,7 @@ class ChatControllerTest { acceptMediaUpgradeOfferUseCase = mock() isGvaUseCase = mock() mapGvaUseCase = mock() + determineGvaButtonTypeUseCase = mock() chatController = ChatController( chatViewCallback = chatViewCallback, @@ -241,7 +244,8 @@ class ChatControllerTest { isFileReadyForPreviewUseCase = isFileReadyForPreviewUseCase, acceptMediaUpgradeOfferUseCase = acceptMediaUpgradeOfferUseCase, isGvaUseCase = isGvaUseCase, - mapGvaUseCase = mapGvaUseCase + mapGvaUseCase = mapGvaUseCase, + determineGvaButtonTypeUseCase = determineGvaButtonTypeUseCase ) } diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/DetermineGvaButtonTypeUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/DetermineGvaButtonTypeUseCaseTest.kt new file mode 100644 index 000000000..0d18fb90f --- /dev/null +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/DetermineGvaButtonTypeUseCaseTest.kt @@ -0,0 +1,61 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.androidsdk.chat.SingleChoiceAttachment +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaButton +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DetermineGvaButtonTypeUseCaseTest { + private lateinit var useCase: DetermineGvaButtonTypeUseCase + private lateinit var determineGvaUrlTypeUseCase: DetermineGvaUrlTypeUseCase + private lateinit var gvaButton: GvaButton + + @Before + fun setUp() { + determineGvaUrlTypeUseCase = mock() + useCase = DetermineGvaButtonTypeUseCase(determineGvaUrlTypeUseCase) + gvaButton = mock() + } + + @Test + fun `invoke returns Broadcast type when destinationPbBroadcastEvent is defined`() { + whenever(gvaButton.destinationPbBroadcastEvent) doReturn "stub!" + + val buttonType = useCase(gvaButton) + assertTrue(buttonType is Gva.ButtonType.BroadcastEvent) + } + + @Test + fun `invoke returns PostBack type when url is null or empty`() { + whenever(gvaButton.url) doReturn null + val value = "value" + val text = "text" + whenever(gvaButton.toResponse()) doReturn SingleChoiceAttachment.from(value, text) + + val buttonType = useCase(gvaButton) + assertTrue(buttonType is Gva.ButtonType.PostBack) + (buttonType as Gva.ButtonType.PostBack).apply { + assertEquals(value, singleChoiceAttachment.selectedOption) + assertEquals(text, singleChoiceAttachment.selectedOptionText) + } + } + + @Test + fun `invoke returns Url type when url is defined and not empty`() { + whenever(gvaButton.url) doReturn "asasasasa" + whenever(determineGvaUrlTypeUseCase(any())) doReturn Gva.ButtonType.Url(mock()) + + val buttonType = useCase(gvaButton) + assertTrue(buttonType is Gva.ButtonType.Url) + } +} diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/DetermineGvaUrlTypeUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/DetermineGvaUrlTypeUseCaseTest.kt new file mode 100644 index 000000000..56f46f42b --- /dev/null +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/DetermineGvaUrlTypeUseCaseTest.kt @@ -0,0 +1,36 @@ +package com.glia.widgets.chat.domain.gva + +import com.glia.widgets.chat.model.Gva +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DetermineGvaUrlTypeUseCaseTest { + private lateinit var useCase: DetermineGvaUrlTypeUseCase + + @Before + fun setUp() { + useCase = DetermineGvaUrlTypeUseCase() + } + + @Test + fun `invoke returns Phone type when url contains phone link`() { + val buttonType = useCase("tel:+37200000000") + assertTrue(buttonType is Gva.ButtonType.Phone) + } + + @Test + fun `invoke returns Email type when url contains email link`() { + val buttonType = useCase("mailto:asdfg@sdf.com") + assertTrue(buttonType is Gva.ButtonType.Email) + } + + @Test + fun `invoke returns Url type when url contains url or deep link link`() { + val buttonType = useCase("glia://test_deep_link") + assertTrue(buttonType is Gva.ButtonType.Url) + } +} From e1591c55f82ce7760fe542b6d9b2b97d76da0735 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Wed, 12 Jul 2023 20:27:07 +0300 Subject: [PATCH 04/69] - Handle Broadcast events - Add Test --- .../src/main/java/com/glia/widgets/chat/ChatView.kt | 4 ++++ .../com/glia/widgets/chat/ChatViewCallback.java | 2 ++ .../glia/widgets/chat/controller/ChatController.kt | 2 +- widgetssdk/src/main/res/values/strings.xml | 3 +++ .../widgets/chat/controller/ChatControllerTest.kt | 13 +++++++++++++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt index 36f2555f7..484a60e70 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt @@ -482,6 +482,10 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty override fun fileIsNotReadyForPreview() { showToast(context.getString(R.string.glia_view_file_not_ready_for_preview)) } + + override fun showBroadcastNotSupportedToast() { + showToast(context.getString(R.string.gva_not_supported)) + } } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java index feb922015..d275d563d 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java @@ -43,4 +43,6 @@ public interface ChatViewCallback { void navigateToPreview(AttachmentFile attachmentFile, View view); void fileIsNotReadyForPreview(); + + void showBroadcastNotSupportedToast(); } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index 11e2bc424..74694c4e1 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -1720,7 +1720,7 @@ internal class ChatController( fun onGvaButtonClicked(button: GvaButton) { when (determineGvaButtonTypeUseCase(button)) { - Gva.ButtonType.BroadcastEvent -> TODO("will be implemented in next tasks") + Gva.ButtonType.BroadcastEvent -> viewCallback?.showBroadcastNotSupportedToast() is Gva.ButtonType.Email -> TODO("will be implemented in next tasks") is Gva.ButtonType.Phone -> TODO("will be implemented in next tasks") is Gva.ButtonType.PostBack -> TODO("will be implemented in next tasks") diff --git a/widgetssdk/src/main/res/values/strings.xml b/widgetssdk/src/main/res/values/strings.xml index d7dce1e18..086592d97 100644 --- a/widgetssdk/src/main/res/values/strings.xml +++ b/widgetssdk/src/main/res/values/strings.xml @@ -258,4 +258,7 @@ Could not load the visitor code. Please try refreshing. Refresh + + This action is not currently supported on mobile. + diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt index 893d5f687..7891819b4 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt @@ -21,6 +21,8 @@ import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase import com.glia.widgets.chat.domain.gva.DetermineGvaButtonTypeUseCase import com.glia.widgets.chat.domain.gva.IsGvaUseCase import com.glia.widgets.chat.domain.gva.MapGvaUseCase +import com.glia.widgets.chat.model.Gva +import com.glia.widgets.chat.model.GvaButton import com.glia.widgets.chat.model.history.ChatItem import com.glia.widgets.chat.model.history.LinkedChatItem import com.glia.widgets.core.callvisualizer.domain.IsCallVisualizerUseCase @@ -416,4 +418,15 @@ class ChatControllerTest { chatController.emitChatTranscriptItems(mutableListOf(), 10) verify(markMessagesReadWithDelayUseCase, never()).invoke() } + + @Test + fun `onGvaButtonClicked triggers viewCallback showBroadcastNotSupportedToast when gva type is BroadcastEvent`() { + val gvaButton: GvaButton = mock() + whenever(determineGvaButtonTypeUseCase(any())) doReturn Gva.ButtonType.BroadcastEvent + + chatController.onGvaButtonClicked(gvaButton) + + verify(chatViewCallback).showBroadcastNotSupportedToast() + } + } From 0a270adfaf7631ddd8c724741d5a233069701fc0 Mon Sep 17 00:00:00 2001 From: Andrii Horishnii Date: Mon, 10 Jul 2023 22:58:04 +0300 Subject: [PATCH 05/69] Made possible to synchronize messages between Desktop & Mobile MOB-2340 --- .../widgets/chat/controller/ChatController.kt | 19 ++----- .../chat/domain/GliaOnMessageUseCase.java | 54 ------------------- .../chat/domain/GliaOnMessageUseCase.kt | 31 +++++++++++ .../domain/PreEngagementMessageUseCase.kt | 44 --------------- .../glia/widgets/di/ControllerFactory.java | 1 - .../com/glia/widgets/di/UseCaseFactory.java | 17 ++---- .../widgets/view/MessagesNotSeenHandler.java | 2 +- .../chat/controller/ChatControllerTest.kt | 4 -- 8 files changed, 38 insertions(+), 134 deletions(-) delete mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.java create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.kt delete mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/PreEngagementMessageUseCase.kt diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index 74694c4e1..cdbf2839e 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -40,7 +40,6 @@ import com.glia.widgets.chat.domain.IsEnableChatEditTextUseCase import com.glia.widgets.chat.domain.IsFromCallScreenUseCase import com.glia.widgets.chat.domain.IsSecureConversationsChatAvailableUseCase import com.glia.widgets.chat.domain.IsShowSendButtonUseCase -import com.glia.widgets.chat.domain.PreEngagementMessageUseCase import com.glia.widgets.chat.domain.SiteInfoUseCase import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase import com.glia.widgets.chat.domain.gva.DetermineGvaButtonTypeUseCase @@ -175,7 +174,6 @@ internal class ChatController( private val hasPendingSurveyUseCase: HasPendingSurveyUseCase, private val setPendingSurveyUsedUseCase: SetPendingSurveyUsedUseCase, private val isCallVisualizerUseCase: IsCallVisualizerUseCase, - private val preEngagementMessageUseCase: PreEngagementMessageUseCase, private val addNewMessagesDividerUseCase: AddNewMessagesDividerUseCase, private val isFileReadyForPreviewUseCase: IsFileReadyForPreviewUseCase, private val acceptMediaUpgradeOfferUseCase: AcceptMediaUpgradeOfferUseCase, @@ -188,7 +186,6 @@ internal class ChatController( private var mediaUpgradeOfferRepositoryCallback: MediaUpgradeOfferRepositoryCallback? = null private var timerStatusListener: FormattedTimerStatusListener? = null private var engagementStateEventDisposable: Disposable? = null - private var unengagementMessagesDisposable: Disposable? = null private val disposable = CompositeDisposable() private val operatorMediaStateListener = @@ -355,7 +352,6 @@ internal class ChatController( minimizeHandler.clear() getEngagementUseCase.unregisterListener(this) engagementEndUseCase.unregisterListener(this) - onMessageUseCase.unregisterListener() onOperatorTypingUseCase.unregisterListener() removeFileAttachmentObserverUseCase.execute(fileAttachmentObserver) shouldHandleEndedEngagement = false @@ -406,20 +402,12 @@ internal class ChatController( private fun subscribeToMessages() { disposable.add( - onMessageUseCase.execute() - .doOnNext { onMessage(it) } + onMessageUseCase() + .doOnNext { onPreEngagementMessage(it) } .subscribe() ) } - private fun subscribeToPreEngagementMessage() { - val subscribe = preEngagementMessageUseCase.execute() - .doOnNext { onPreEngagementMessage(it) } - .subscribe() - disposable.add(subscribe) - unengagementMessagesDisposable = subscribe - } - private fun onPreEngagementMessage(messageInternal: ChatMessageInternal) { emitChatItems { val message = messageInternal.chatMessage @@ -1432,7 +1420,6 @@ internal class ChatController( } private fun loadChatHistory() { - unengagementMessagesDisposable?.dispose() val historyDisposable = loadHistoryUseCase() .subscribe({ historyLoaded(it) }, { error(it) }) disposable.add(historyDisposable) @@ -1457,7 +1444,7 @@ internal class ChatController( } initGliaEngagementObserving() - subscribeToPreEngagementMessage() + subscribeToMessages() } private fun submitHistoryItems( diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.java b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.java deleted file mode 100644 index 3abd70f7a..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.glia.widgets.chat.domain; - -import com.glia.androidsdk.chat.ChatMessage; -import com.glia.androidsdk.omnicore.OmnicoreEngagement; -import com.glia.widgets.chat.data.GliaChatRepository; -import com.glia.widgets.core.engagement.domain.GliaOnEngagementUseCase; -import com.glia.widgets.core.engagement.domain.MapOperatorUseCase; -import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal; - -import io.reactivex.Observable; -import io.reactivex.subjects.PublishSubject; - -public class GliaOnMessageUseCase implements - GliaOnEngagementUseCase.Listener, - GliaChatRepository.MessageListener { - - private final GliaOnEngagementUseCase onEngagementUseCase; - private final GliaChatRepository messageRepository; - private final MapOperatorUseCase mapOperatorUseCase; - private final PublishSubject publishSubject; - - public GliaOnMessageUseCase( - GliaChatRepository messageRepository, - GliaOnEngagementUseCase gliaOnEngagementUseCase, - MapOperatorUseCase mapOperatorUseCase) { - this.onEngagementUseCase = gliaOnEngagementUseCase; - this.messageRepository = messageRepository; - this.mapOperatorUseCase = mapOperatorUseCase; - publishSubject = PublishSubject.create(); - } - - public Observable execute() { - this.onEngagementUseCase.execute(this); - return publishSubject - .flatMapSingle(chatMessage -> mapOperatorUseCase.invoke(chatMessage, false, true)) - .doOnError(Throwable::printStackTrace) - .share(); - } - - public void unregisterListener() { - messageRepository.unregisterEngagementMessageListener(this); - onEngagementUseCase.unregisterListener(this); - } - - @Override - public void newEngagementLoaded(OmnicoreEngagement engagement) { - messageRepository.listenForEngagementMessages(this, engagement); - } - - @Override - public void onMessage(ChatMessage chatMessage) { - publishSubject.onNext(chatMessage); - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.kt new file mode 100644 index 000000000..23990e08d --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.kt @@ -0,0 +1,31 @@ +package com.glia.widgets.chat.domain + +import com.glia.androidsdk.chat.ChatMessage +import com.glia.widgets.chat.data.GliaChatRepository +import com.glia.widgets.core.engagement.domain.MapOperatorUseCase +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal +import io.reactivex.Observable +import java.util.function.Consumer + +internal class GliaOnMessageUseCase( + private val messageRepository: GliaChatRepository, + private val mapOperatorUseCase: MapOperatorUseCase +) { + + private val observable = Observable.create { observer -> + val messageListener = Consumer { chatMessage -> + observer.onNext(chatMessage) + } + + messageRepository.listenForAllMessages(messageListener) + + observer.setCancellable { + messageRepository.unregisterAllMessageListener(messageListener) + } + } + .flatMapSingle { chatMessage: ChatMessage -> mapOperatorUseCase(chatMessage, isHistory = false, isLast = true) } + .doOnError { obj: Throwable -> obj.printStackTrace() } + .share() + + operator fun invoke(): Observable = observable +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/PreEngagementMessageUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/PreEngagementMessageUseCase.kt deleted file mode 100644 index add4d49d6..000000000 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/PreEngagementMessageUseCase.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.glia.widgets.chat.domain - -import com.glia.androidsdk.chat.ChatMessage -import com.glia.widgets.chat.data.GliaChatRepository -import com.glia.widgets.core.engagement.GliaEngagementRepository -import com.glia.widgets.core.engagement.domain.GliaOnEngagementUseCase -import com.glia.widgets.core.engagement.domain.MapOperatorUseCase -import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal -import io.reactivex.Observable -import java.util.function.Consumer - -internal class PreEngagementMessageUseCase( - private val messageRepository: GliaChatRepository, - private val engagementRepository: GliaEngagementRepository, - private val onEngagementUseCase: GliaOnEngagementUseCase, - private val mapOperatorUseCase: MapOperatorUseCase -) { - - fun execute(): Observable { - if (engagementRepository.hasOngoingEngagement()) { - return Observable.empty() - } - return Observable.create { observer -> - val messageListener = Consumer { chatMessage -> - observer.onNext(chatMessage) - } - - val engagementListener = GliaOnEngagementUseCase.Listener { - observer.onComplete() - } - - messageRepository.listenForAllMessages(messageListener) - onEngagementUseCase.execute(engagementListener) - - observer.setCancellable { - messageRepository.unregisterAllMessageListener(messageListener) - onEngagementUseCase.unregisterListener(engagementListener) - } - } - .flatMapSingle { chatMessage: ChatMessage -> mapOperatorUseCase(chatMessage) } - .doOnError { obj: Throwable -> obj.printStackTrace() } - .share() - } -} diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java index 4b3950a00..5cd331a0d 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java @@ -133,7 +133,6 @@ public ChatController getChatController(ChatViewCallback chatViewCallback) { useCaseFactory.createHasPendingSurveyUseCase(), useCaseFactory.createSetPendingSurveyUsed(), useCaseFactory.createIsCallVisualizerUseCase(), - useCaseFactory.createPreEngagementMessageUseCase(), useCaseFactory.createAddNewMessagesDividerUseCase(), useCaseFactory.createIsFileReadyForPreviewUseCase(), useCaseFactory.createAcceptMediaUpgradeOfferUseCase(), diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java index 8fc37e4e1..51a8c25b7 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java @@ -24,7 +24,6 @@ import com.glia.widgets.chat.domain.IsFromCallScreenUseCase; import com.glia.widgets.chat.domain.IsSecureConversationsChatAvailableUseCase; import com.glia.widgets.chat.domain.IsShowSendButtonUseCase; -import com.glia.widgets.chat.domain.PreEngagementMessageUseCase; import com.glia.widgets.chat.domain.SiteInfoUseCase; import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase; import com.glia.widgets.chat.domain.gva.DetermineGvaButtonTypeUseCase; @@ -334,22 +333,12 @@ public SetPendingSurveyUsedUseCase createSetPendingSurveyUsed() { return new SetPendingSurveyUsedUseCase(surveyStateManager); } - @NonNull - public PreEngagementMessageUseCase createPreEngagementMessageUseCase() { - return new PreEngagementMessageUseCase( - repositoryFactory.getGliaMessageRepository(), - repositoryFactory.getGliaEngagementRepository(), - createOnEngagementUseCase(), - getMapOperatorUseCase() - ); - } - @NonNull public GliaOnMessageUseCase createGliaOnMessageUseCase() { return new GliaOnMessageUseCase( - repositoryFactory.getGliaMessageRepository(), - createOnEngagementUseCase(), - getMapOperatorUseCase()); + repositoryFactory.getGliaMessageRepository(), + getMapOperatorUseCase() + ); } @NonNull diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/MessagesNotSeenHandler.java b/widgetssdk/src/main/java/com/glia/widgets/view/MessagesNotSeenHandler.java index d84cedb6b..5f8957e2d 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/MessagesNotSeenHandler.java +++ b/widgetssdk/src/main/java/com/glia/widgets/view/MessagesNotSeenHandler.java @@ -27,7 +27,7 @@ public MessagesNotSeenHandler( public void init() { Logger.d(TAG, "init"); - gliaOnMessageUseCase.execute().doOnNext(this::onMessage).subscribe(); + gliaOnMessageUseCase.invoke().doOnNext(this::onMessage).subscribe(); gliaOnEngagementEndUseCase.execute(this); } diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt index 7891819b4..d8b1082ca 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt @@ -15,7 +15,6 @@ import com.glia.widgets.chat.domain.IsEnableChatEditTextUseCase import com.glia.widgets.chat.domain.IsFromCallScreenUseCase import com.glia.widgets.chat.domain.IsSecureConversationsChatAvailableUseCase import com.glia.widgets.chat.domain.IsShowSendButtonUseCase -import com.glia.widgets.chat.domain.PreEngagementMessageUseCase import com.glia.widgets.chat.domain.SiteInfoUseCase import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase import com.glia.widgets.chat.domain.gva.DetermineGvaButtonTypeUseCase @@ -124,7 +123,6 @@ class ChatControllerTest { private lateinit var hasPendingSurveyUseCase: HasPendingSurveyUseCase private lateinit var setPendingSurveyUsedUseCase: SetPendingSurveyUsedUseCase private lateinit var isCallVisualizerUseCase: IsCallVisualizerUseCase - private lateinit var preEngagementMessageUseCase: PreEngagementMessageUseCase private lateinit var addNewMessagesDividerUseCase: AddNewMessagesDividerUseCase private lateinit var isFileReadyForPreviewUseCase: IsFileReadyForPreviewUseCase private lateinit var acceptMediaUpgradeOfferUseCase: AcceptMediaUpgradeOfferUseCase @@ -184,7 +182,6 @@ class ChatControllerTest { hasPendingSurveyUseCase = mock() setPendingSurveyUsedUseCase = mock() isCallVisualizerUseCase = mock() - preEngagementMessageUseCase = mock() addNewMessagesDividerUseCase = mock() isFileReadyForPreviewUseCase = mock() acceptMediaUpgradeOfferUseCase = mock() @@ -241,7 +238,6 @@ class ChatControllerTest { hasPendingSurveyUseCase = hasPendingSurveyUseCase, setPendingSurveyUsedUseCase = setPendingSurveyUsedUseCase, isCallVisualizerUseCase = isCallVisualizerUseCase, - preEngagementMessageUseCase = preEngagementMessageUseCase, addNewMessagesDividerUseCase = addNewMessagesDividerUseCase, isFileReadyForPreviewUseCase = isFileReadyForPreviewUseCase, acceptMediaUpgradeOfferUseCase = acceptMediaUpgradeOfferUseCase, From bf88d80440dd017bcb8631e071beadb562071e81 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Thu, 13 Jul 2023 12:41:44 +0300 Subject: [PATCH 06/69] - Handle Url events - Add Test --- widgetssdk/src/main/AndroidManifest.xml | 5 +++++ .../src/main/java/com/glia/widgets/chat/ChatView.kt | 10 ++++++++++ .../java/com/glia/widgets/chat/ChatViewCallback.java | 3 +++ .../glia/widgets/chat/controller/ChatController.kt | 8 ++++---- .../widgets/chat/controller/ChatControllerTest.kt | 12 ++++++++++++ 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/widgetssdk/src/main/AndroidManifest.xml b/widgetssdk/src/main/AndroidManifest.xml index 940da86b6..b5f963433 100644 --- a/widgetssdk/src/main/AndroidManifest.xml +++ b/widgetssdk/src/main/AndroidManifest.xml @@ -6,6 +6,11 @@ + + + + + diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt index 484a60e70..1e2233dcf 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt @@ -486,6 +486,16 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty override fun showBroadcastNotSupportedToast() { showToast(context.getString(R.string.gva_not_supported)) } + + override fun requestOpenUri(uri: Uri) { + this@ChatView.requestOpenUri(uri) + } + } + } + + private fun requestOpenUri(uri: Uri) { + Intent(Intent.ACTION_VIEW, uri).addCategory(Intent.CATEGORY_BROWSABLE).also { + context.startActivity(it) } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java index d275d563d..6bba30f48 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java @@ -1,5 +1,6 @@ package com.glia.widgets.chat; +import android.net.Uri; import android.view.View; import androidx.annotation.NonNull; @@ -45,4 +46,6 @@ public interface ChatViewCallback { void fileIsNotReadyForPreview(); void showBroadcastNotSupportedToast(); + + void requestOpenUri(@NonNull Uri uri); } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index cdbf2839e..920e9a5d1 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -1370,8 +1370,8 @@ internal class ChatController( } return@emitChatItems null } ?: run { - sendMessageUseCase.execute(null, text, value, sendMessageCallback) - } + sendMessageUseCase.execute(null, text, value, sendMessageCallback) + } return@emitChatItems null } } @@ -1706,12 +1706,12 @@ internal class ChatController( } fun onGvaButtonClicked(button: GvaButton) { - when (determineGvaButtonTypeUseCase(button)) { + when (val buttonType: Gva.ButtonType = determineGvaButtonTypeUseCase(button)) { Gva.ButtonType.BroadcastEvent -> viewCallback?.showBroadcastNotSupportedToast() is Gva.ButtonType.Email -> TODO("will be implemented in next tasks") is Gva.ButtonType.Phone -> TODO("will be implemented in next tasks") is Gva.ButtonType.PostBack -> TODO("will be implemented in next tasks") - is Gva.ButtonType.Url -> TODO("will be implemented in next tasks") + is Gva.ButtonType.Url -> viewCallback?.requestOpenUri(buttonType.uri) } } } diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt index d8b1082ca..ddf7f0e86 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt @@ -1,5 +1,6 @@ package com.glia.widgets.chat.controller +import android.net.Uri import com.glia.androidsdk.chat.ChatMessage import com.glia.widgets.chat.ChatViewCallback import com.glia.widgets.chat.domain.AddNewMessagesDividerUseCase @@ -425,4 +426,15 @@ class ChatControllerTest { verify(chatViewCallback).showBroadcastNotSupportedToast() } + @Test + fun `onGvaButtonClicked triggers viewCallback requestOpenUri when gva type is Url`() { + val gvaButton: GvaButton = mock() + val uri: Uri = mock() + whenever(determineGvaButtonTypeUseCase(any())) doReturn Gva.ButtonType.Url(uri) + + chatController.onGvaButtonClicked(gvaButton) + + verify(chatViewCallback).requestOpenUri(uri) + } + } From a3aa4b6d07393074528c60e71b6797213b6d1bf3 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Thu, 13 Jul 2023 18:13:30 +0300 Subject: [PATCH 07/69] Implement requesting dialer or email client Add queries to the manifest. --- widgetssdk/src/main/AndroidManifest.xml | 9 ++++++ .../java/com/glia/widgets/chat/ChatView.kt | 30 +++++++++++++++++++ .../glia/widgets/chat/ChatViewCallback.java | 6 ++++ .../widgets/chat/controller/ChatController.kt | 4 +-- .../chat/controller/ChatControllerTest.kt | 22 ++++++++++++++ 5 files changed, 69 insertions(+), 2 deletions(-) diff --git a/widgetssdk/src/main/AndroidManifest.xml b/widgetssdk/src/main/AndroidManifest.xml index b5f963433..52468d5d3 100644 --- a/widgetssdk/src/main/AndroidManifest.xml +++ b/widgetssdk/src/main/AndroidManifest.xml @@ -11,6 +11,15 @@ + + + + + + + + + diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt index 1e2233dcf..d6f6205a1 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt @@ -490,6 +490,36 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty override fun requestOpenUri(uri: Uri) { this@ChatView.requestOpenUri(uri) } + + override fun requestOpenDialer(uri: Uri) { + this@ChatView.requestOpenDialer(uri) + } + + override fun requestOpenEmailClient(uri: Uri) { + this@ChatView.requestOpenEmailClient(uri) + } + } + } + + private fun requestOpenEmailClient(uri: Uri) { + val intent = Intent(Intent.ACTION_SENDTO) + .setData(Uri.parse("mailto:")) //This step makes sure that only email apps handle this. + .putExtra(Intent.EXTRA_EMAIL, arrayOf(uri.schemeSpecificPart)) + + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + } else { + showToast(context.getString(R.string.glia_dialog_unexpected_error_title)) + } + } + + private fun requestOpenDialer(uri: Uri) { + val intent = Intent(Intent.ACTION_DIAL).setData(uri) + + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + } else { + showToast(context.getString(R.string.glia_dialog_unexpected_error_title)) } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java index 6bba30f48..82f04d764 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatViewCallback.java @@ -11,6 +11,8 @@ import com.glia.widgets.chat.model.history.ChatItem; import com.glia.widgets.core.fileupload.model.FileAttachment; +import org.jetbrains.annotations.NotNull; + import java.util.List; public interface ChatViewCallback { @@ -48,4 +50,8 @@ public interface ChatViewCallback { void showBroadcastNotSupportedToast(); void requestOpenUri(@NonNull Uri uri); + + void requestOpenDialer(@NotNull Uri uri); + + void requestOpenEmailClient(@NotNull Uri uri); } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index 920e9a5d1..0da009cb4 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -1708,8 +1708,8 @@ internal class ChatController( fun onGvaButtonClicked(button: GvaButton) { when (val buttonType: Gva.ButtonType = determineGvaButtonTypeUseCase(button)) { Gva.ButtonType.BroadcastEvent -> viewCallback?.showBroadcastNotSupportedToast() - is Gva.ButtonType.Email -> TODO("will be implemented in next tasks") - is Gva.ButtonType.Phone -> TODO("will be implemented in next tasks") + is Gva.ButtonType.Email -> viewCallback?.requestOpenEmailClient(buttonType.uri) + is Gva.ButtonType.Phone -> viewCallback?.requestOpenDialer(buttonType.uri) is Gva.ButtonType.PostBack -> TODO("will be implemented in next tasks") is Gva.ButtonType.Url -> viewCallback?.requestOpenUri(buttonType.uri) } diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt index ddf7f0e86..3d4e6520e 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt @@ -437,4 +437,26 @@ class ChatControllerTest { verify(chatViewCallback).requestOpenUri(uri) } + @Test + fun `onGvaButtonClicked triggers viewCallback requestOpenDialer when gva type is Phone`() { + val gvaButton: GvaButton = mock() + val uri: Uri = mock() + whenever(determineGvaButtonTypeUseCase(any())) doReturn Gva.ButtonType.Phone(uri) + + chatController.onGvaButtonClicked(gvaButton) + + verify(chatViewCallback).requestOpenDialer(uri) + } + + @Test + fun `onGvaButtonClicked triggers viewCallback requestOpenEmailClient when gva type is Email`() { + val gvaButton: GvaButton = mock() + val uri: Uri = mock() + whenever(determineGvaButtonTypeUseCase(any())) doReturn Gva.ButtonType.Email(uri) + + chatController.onGvaButtonClicked(gvaButton) + + verify(chatViewCallback).requestOpenEmailClient(uri) + } + } From af647b2405dda930ff17ca740b5936276e09347b Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Fri, 14 Jul 2023 00:02:33 +0300 Subject: [PATCH 08/69] Implement sending utterance when button clicked. --- .../widgets/chat/controller/ChatController.kt | 6 +++++- .../com/glia/widgets/chat/model/GvaBaseTypes.kt | 2 +- .../widgets/chat/controller/ChatControllerTest.kt | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index 0da009cb4..6909a5913 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -1376,6 +1376,10 @@ internal class ChatController( } } + private fun sendGvaResponse(singleChoiceAttachment: SingleChoiceAttachment) { + sendMessageUseCase.execute(singleChoiceAttachment, sendMessageCallback) + } + private fun updateCustomCard(message: ChatMessage) { chatState.chatItems .firstOrNull { message.id == it.id } @@ -1710,7 +1714,7 @@ internal class ChatController( Gva.ButtonType.BroadcastEvent -> viewCallback?.showBroadcastNotSupportedToast() is Gva.ButtonType.Email -> viewCallback?.requestOpenEmailClient(buttonType.uri) is Gva.ButtonType.Phone -> viewCallback?.requestOpenDialer(buttonType.uri) - is Gva.ButtonType.PostBack -> TODO("will be implemented in next tasks") + is Gva.ButtonType.PostBack -> sendGvaResponse(buttonType.singleChoiceAttachment) is Gva.ButtonType.Url -> viewCallback?.requestOpenUri(buttonType.uri) } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt index c27a19dd7..06c7b2cdb 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaBaseTypes.kt @@ -59,7 +59,7 @@ internal data class GvaButton( @SerializedName("transferPhoneNumber") val transferPhoneNumber: String? = null ) { - fun toResponse(): SingleChoiceAttachment = SingleChoiceAttachment.from(text, value) + fun toResponse(): SingleChoiceAttachment = SingleChoiceAttachment.from(value, text) } internal data class GvaGalleryCard( diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt index 3d4e6520e..ef6f1b2e9 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt @@ -2,6 +2,7 @@ package com.glia.widgets.chat.controller import android.net.Uri import com.glia.androidsdk.chat.ChatMessage +import com.glia.androidsdk.chat.SingleChoiceAttachment import com.glia.widgets.chat.ChatViewCallback import com.glia.widgets.chat.domain.AddNewMessagesDividerUseCase import com.glia.widgets.chat.domain.CustomCardAdapterTypeUseCase @@ -459,4 +460,18 @@ class ChatControllerTest { verify(chatViewCallback).requestOpenEmailClient(uri) } + @Test + fun `onGvaButtonClicked triggers sendMessageUseCase when gva type is PostBack`() { + val gvaButton = GvaButton(text = "text", value = "value") + val singleChoiceAttachment = gvaButton.toResponse() + whenever(determineGvaButtonTypeUseCase(any())) doReturn Gva.ButtonType.PostBack(singleChoiceAttachment) + + chatController.onGvaButtonClicked(gvaButton) + + assertEquals(gvaButton.text, singleChoiceAttachment.selectedOptionText) + assertEquals(gvaButton.value, singleChoiceAttachment.selectedOption) + + verify(sendMessageUseCase).execute(any(), any()) + } + } From ba2356d3d03c4b5020a3a5658f2ae3be66b31817 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Mon, 17 Jul 2023 18:57:09 +0300 Subject: [PATCH 09/69] Create a Quick Reply button UI. --- build.gradle | 6 +- widgetssdk/build.gradle | 4 - .../src/main/java/com/glia/widgets/UiTheme.kt | 46 +++++++++- .../java/com/glia/widgets/chat/ChatView.kt | 7 +- .../java/com/glia/widgets/chat/GvaChip.kt | 90 +++++++++++++++++++ .../widgets/chat/controller/ChatController.kt | 3 +- widgetssdk/src/main/res/layout/chat_view.xml | 23 ++--- widgetssdk/src/main/res/values/attrs.xml | 14 ++- widgetssdk/src/main/res/values/styles.xml | 19 ++++ widgetssdk/src/main/res/values/themes.xml | 8 ++ 10 files changed, 192 insertions(+), 28 deletions(-) create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/GvaChip.kt diff --git a/build.gradle b/build.gradle index 93a987d10..892d08092 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ buildscript { plugins { id("io.github.gradle-nexus.publish-plugin") version "1.1.0" id("org.jetbrains.kotlin.android") version "$kotlin_version" apply false - id("org.jlleitschuh.gradle.ktlint") version "11.3.2" + id("org.jlleitschuh.gradle.ktlint") version "11.5.0" } allprojects { @@ -53,8 +53,8 @@ ext { // gliaSdkVersion = 'X.X.X' // this property is now attached dynamically during gradle sync appCompatVersion = '1.3.1' - materialVersion = '1.4.0' - constraintLayoutVersion = '2.1.3' + materialVersion = '1.6.1' + constraintLayoutVersion = '2.1.4' navVersion = '2.3.5' lifecycle_process = '2.3.1' rxAndroid = '2.1.0' diff --git a/widgetssdk/build.gradle b/widgetssdk/build.gradle index 96c37e70b..0b3ac0c20 100644 --- a/widgetssdk/build.gradle +++ b/widgetssdk/build.gradle @@ -61,10 +61,6 @@ dependencies { implementation "io.reactivex.rxjava2:rxandroid:$rxAndroid" implementation "io.reactivex.rxjava2:rxjava:$rxJava" - // removed because becomes slow on Android 11. They say it is fixed, but I still have the issue. - // https://square.github.io/leakcanary/changelog/ - // debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion" - api "com.glia:android-sdk:$gliaSdkVersion" // Glia sdk aar // implementation(name: "glia-core-sdk-$gliaSdkVersion", ext: 'aar') diff --git a/widgetssdk/src/main/java/com/glia/widgets/UiTheme.kt b/widgetssdk/src/main/java/com/glia/widgets/UiTheme.kt index bebb02339..a1b24ed28 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/UiTheme.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/UiTheme.kt @@ -339,7 +339,17 @@ data class UiTheme( @get:Deprecated("Replaced by Unified Ui") val chatHeadConfiguration: ChatHeadConfiguration? = null, @get:Deprecated("Replaced by Unified Ui") - val surveyStyle: SurveyStyle? = null + val surveyStyle: SurveyStyle? = null, + + // GVA + @ColorRes + val gvaQuickReplyBackgroundColor: Int? = null, + + @ColorRes + val gvaQuickReplyStrokeColor: Int? = null, + + @ColorRes + val gvaQuickReplyTextColor: Int? = null ) : Parcelable { @@ -404,7 +414,10 @@ data class UiTheme( gliaChatStartedHeadingTextColor = builder.chatStartedHeadingTextColor, gliaChoiceCardContentTextConfiguration = builder.choiceCardContentTextConfiguration, chatHeadConfiguration = builder.chatHeadConfiguration, - surveyStyle = builder.surveyStyle + surveyStyle = builder.surveyStyle, + gvaQuickReplyBackgroundColor = builder.gvaQuickReplyBackgroundColor, + gvaQuickReplyStrokeColor = builder.gvaQuickReplyStrokeColor, + gvaQuickReplyTextColor = builder.gvaQuickReplyTextColor ) class UiThemeBuilder { @@ -783,6 +796,19 @@ data class UiTheme( var surveyStyle: SurveyStyle? = SurveyStyle.Builder().build() private set + // GVA + @ColorRes + var gvaQuickReplyBackgroundColor: Int? = null + private set + + @ColorRes + var gvaQuickReplyStrokeColor: Int? = null + private set + + @ColorRes + var gvaQuickReplyTextColor: Int? = null + private set + fun setAppBarTitle(appBarTitle: String?) { this.appBarTitle = appBarTitle } @@ -1023,6 +1049,19 @@ data class UiTheme( this.surveyStyle = surveyStyle } + + fun setGvaQuickReplyBackgroundColor(@ColorRes gvaQuickReplyBackgroundColor: Int?) { + this.gvaQuickReplyBackgroundColor = gvaQuickReplyBackgroundColor + } + + fun setGvaQuickReplyStrokeColor(@ColorRes gvaQuickReplyStrokeColor: Int?) { + this.gvaQuickReplyStrokeColor = gvaQuickReplyStrokeColor + } + + fun setGvaQuickReplyTextColor(@ColorRes gvaQuickReplyTextColor: Int?) { + this.gvaQuickReplyTextColor = gvaQuickReplyTextColor + } + fun setTheme(theme: UiTheme) { appBarTitle = theme.appBarTitle brandPrimaryColor = theme.brandPrimaryColor @@ -1080,6 +1119,9 @@ data class UiTheme( choiceCardContentTextConfiguration = theme.gliaChoiceCardContentTextConfiguration chatHeadConfiguration = theme.chatHeadConfiguration surveyStyle = theme.surveyStyle + gvaQuickReplyBackgroundColor = theme.gvaQuickReplyBackgroundColor + gvaQuickReplyStrokeColor = theme.gvaQuickReplyStrokeColor + gvaQuickReplyTextColor = theme.gvaQuickReplyTextColor } fun build(): UiTheme { diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt index d6f6205a1..fecc9e157 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt @@ -530,9 +530,7 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty } private fun updateQuickRepliesState(chatState: ChatState) { - val quickReplies = chatState.gvaQuickReplies - binding.gvaQuickRepliesLayout.isVisible = quickReplies.isNotEmpty() - binding.gvaQuickRepliesLayout.text = quickReplies.map { it.text }.toString() + binding.gvaQuickRepliesLayout.setButtons(chatState.gvaQuickReplies) } private fun updateNewMessageOperatorStatusView(operatorProfileImgUrl: String?) { @@ -818,6 +816,8 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty binding.operatorTypingAnimationView.addColorFilter(color = it) } + binding.gvaQuickRepliesLayout.updateTheme(theme) + applyTheme(Dependencies.getGliaThemeManager().theme) } @@ -846,6 +846,7 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty } binding.appBarView.setOnXClickedListener { controller?.onXButtonClicked() } binding.newMessagesIndicatorCard.setOnClickListener { controller?.newMessagesIndicatorClicked() } + binding.gvaQuickRepliesLayout.onItemClickedListener = GvaChipGroup.OnItemClickedListener { controller?.onGvaButtonClicked(it) } } private fun setupAddAttachmentButton() { diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/GvaChip.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/GvaChip.kt new file mode 100644 index 000000000..051fed483 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/GvaChip.kt @@ -0,0 +1,90 @@ +package com.glia.widgets.chat + +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.transition.TransitionManager +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.model.GvaButton +import com.glia.widgets.helper.getColorCompat +import com.glia.widgets.helper.wrapWithMaterialThemeOverlay +import com.glia.widgets.view.unifiedui.theme.base.ButtonTheme +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import com.google.android.material.transition.MaterialFadeThrough + +class GvaChip @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = com.google.android.material.R.attr.chipStyle +) : Chip(context.wrapWithMaterialThemeOverlay(attrs, defStyleAttr), attrs, defStyleAttr) { + + internal fun applyButtonTheme(buttonTheme: ButtonTheme) { + + } + + internal fun applyUiTheme(uiTheme: UiTheme?) { + with(uiTheme ?: return) { + gvaQuickReplyBackgroundColor?.let { setChipBackgroundColorResource(it) } + gvaQuickReplyStrokeColor?.let { setChipStrokeColorResource(it) } + gvaQuickReplyTextColor?.let { getColorCompat(it) }?.let { setTextColor(it) } + } + } + +} + +class GvaChipGroup @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = com.google.android.material.R.attr.chipGroupStyle +) : ChipGroup(context.wrapWithMaterialThemeOverlay(attrs, defStyleAttr), attrs, defStyleAttr) { + + internal var onItemClickedListener: OnItemClickedListener? = null + private var theme: UiTheme? = null + + init { + isSelectionRequired = false + isSingleLine = false + isSingleSelection = false + } + + internal fun updateTheme(theme: UiTheme?) { + this.theme = theme + + children.forEach { (it as? GvaChip)?.applyUiTheme(theme) } + } + + internal fun setButtons(buttons: List) { + + val hasItems = buttons.isNotEmpty() + + if (hasItems) { + removeAllViews() + buttons.forEach { addButton(it, theme) } + + if (isVisible) return + + TransitionManager.beginDelayedTransition(parent as ViewGroup, MaterialFadeThrough()) + } + + isVisible = hasItems + } + + private fun addButton(gvaButton: GvaButton, uiTheme: UiTheme?) { + GvaChip(context).apply { + applyUiTheme(uiTheme) + text = gvaButton.text + setOnClickListener { onItemClickedListener?.onItemClicked(gvaButton) } + + addView(this) + } + } + + + internal fun interface OnItemClickedListener { + fun onItemClicked(gvaButton: GvaButton) + } + +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index 6909a5913..43e28a2c7 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -429,13 +429,14 @@ internal class ChatController( } private fun onMessage(messageInternal: ChatMessageInternal) { - addQuickReplyButtons(emptyList()) emitChatItems { val message = messageInternal.chatMessage if (!isNewMessage(chatState.chatItems, message)) { return@emitChatItems null } + addQuickReplyButtons(emptyList()) + val isUnsentMessage = chatState.unsentMessages.isNotEmpty() && chatState.unsentMessages[0].message == message.content Logger.d( diff --git a/widgetssdk/src/main/res/layout/chat_view.xml b/widgetssdk/src/main/res/layout/chat_view.xml index e81147654..c3474b2c3 100644 --- a/widgetssdk/src/main/res/layout/chat_view.xml +++ b/widgetssdk/src/main/res/layout/chat_view.xml @@ -23,6 +23,18 @@ app:layout_constraintTop_toBottomOf="@+id/app_bar_view" tools:listitem="@layout/chat_visitor_message_layout" /> + + + - - - + - + @@ -174,9 +174,9 @@ - + - + @@ -208,6 +208,12 @@ + + + + + + diff --git a/widgetssdk/src/main/res/values/styles.xml b/widgetssdk/src/main/res/values/styles.xml index db2402dd4..8709832a5 100644 --- a/widgetssdk/src/main/res/values/styles.xml +++ b/widgetssdk/src/main/res/values/styles.xml @@ -75,6 +75,25 @@ ?attr/baseLightColor + + + + + + + + From 89def8c684b93d74dc2d11537631a4d5af614665 Mon Sep 17 00:00:00 2001 From: Aleksandr Chatsky Date: Tue, 1 Aug 2023 09:29:19 +0300 Subject: [PATCH 18/69] Update project name I think that since both Android and Widgets are names they should start with capital letters. Also in consistency with https://github.com/salemove/android-sdk/pull/501 --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 2cb23498e..2edfc5ce2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,3 @@ include ':widgetssdk' include ':app' -rootProject.name = "Glia android SDK widgets" +rootProject.name = "Glia Android SDK Widgets" From a067bf22538f2cd40ffbaead804515d12bb37b54 Mon Sep 17 00:00:00 2001 From: Andrii Horishnii Date: Wed, 26 Jul 2023 20:10:00 +0300 Subject: [PATCH 19/69] GVA: Create a Gallery list RecycleView was configured according to the design. MOB-2408 --- .../java/com/glia/widgets/chat/ChatView.kt | 2 + .../glia/widgets/chat/adapter/ChatAdapter.kt | 10 +- .../chat/adapter/ChatItemHeightManager.kt | 56 +++++++++++ .../widgets/chat/adapter/GvaGalleryAdapter.kt | 6 +- .../holder/GvaGalleryItemViewHolder.kt | 82 +++++++++------- .../adapter/holder/GvaGalleryViewHolder.kt | 23 +++-- .../holder/GvaPersistentButtonsViewHolder.kt | 2 +- .../holder/GvaResponseTextViewHolder.kt | 2 +- .../adapter/holder/OperatorBaseViewHolder.kt | 20 ++-- .../main/res/layout/chat_gva_gallery_item.xml | 93 ++++++++++--------- .../res/layout/chat_gva_gallery_layout.xml | 39 ++++++-- widgetssdk/src/main/res/values/dimens.xml | 2 + 12 files changed, 229 insertions(+), 108 deletions(-) create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt index b54f111ef..e9799a93a 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt @@ -44,6 +44,7 @@ import com.glia.widgets.chat.adapter.ChatAdapter import com.glia.widgets.chat.adapter.ChatAdapter.OnCustomCardResponse import com.glia.widgets.chat.adapter.ChatAdapter.OnFileItemClickListener import com.glia.widgets.chat.adapter.ChatAdapter.OnImageItemClickListener +import com.glia.widgets.chat.adapter.ChatItemHeightManager import com.glia.widgets.chat.adapter.UploadAttachmentAdapter import com.glia.widgets.chat.adapter.holder.WebViewViewHolder import com.glia.widgets.chat.controller.ChatController @@ -737,6 +738,7 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty this, onCustomCardResponse, onGvaButtonsClickListener, + ChatItemHeightManager(theme, layoutInflater, resources), GliaWidgets.getCustomCardAdapter(), Dependencies.getUseCaseFactory().createGetImageFileFromCacheUseCase(), Dependencies.getUseCaseFactory().createGetImageFileFromDownloadsUseCase(), diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt index cea34662a..1c0017bb4 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt @@ -57,6 +57,7 @@ internal class ChatAdapter( private val onImageItemClickListener: OnImageItemClickListener, private val onCustomCardResponse: OnCustomCardResponse, private val onGvaButtonsClickListener: OnGvaButtonsClickListener, + private val chatItemHeightManager: ChatItemHeightManager, private val customCardAdapter: CustomCardAdapter?, private val getImageFileFromCacheUseCase: GetImageFileFromCacheUseCase, private val getImageFileFromDownloadsUseCase: GetImageFileFromDownloadsUseCase, @@ -182,13 +183,11 @@ internal class ChatAdapter( } GVA_GALLERY_CARDS_TYPE -> { - val operatorMessageBinding = ChatOperatorMessageLayoutBinding.inflate(inflater, parent, false) GvaGalleryViewHolder( - operatorMessageBinding, ChatGvaGalleryLayoutBinding.inflate( inflater, - operatorMessageBinding.contentLayout, - true + parent, + false ), onGvaButtonsClickListener, uiTheme @@ -251,7 +250,7 @@ internal class ChatAdapter( is SystemChatItem -> (holder as SystemMessageViewHolder).bind(chatItem.message) is GvaResponseText -> (holder as GvaResponseTextViewHolder).bind(chatItem) is GvaPersistentButtons -> (holder as GvaPersistentButtonsViewHolder).bind(chatItem, onGvaButtonsClickListener) - is GvaGalleryCards -> (holder as GvaGalleryViewHolder).bind(chatItem) + is GvaGalleryCards -> (holder as GvaGalleryViewHolder).bind(chatItem, chatItemHeightManager.getMeasuredHeight(chatItem)) is CustomCardChatItem -> { (holder as CustomCardViewHolder).bind(chatItem.message) { text: String, value: String -> onCustomCardResponse.onCustomCardResponse(chatItem.id, text, value) @@ -274,6 +273,7 @@ internal class ChatAdapter( } fun submitList(items: List?) { + chatItemHeightManager.measureHeight(items) differ.submitList(items) } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt new file mode 100644 index 000000000..1e0cae348 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt @@ -0,0 +1,56 @@ +package com.glia.widgets.chat.adapter + +import android.content.res.Resources +import android.view.LayoutInflater +import android.view.View +import androidx.collection.ArrayMap +import com.glia.widgets.R +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.adapter.holder.GvaGalleryItemViewHolder +import com.glia.widgets.chat.model.ChatItem +import com.glia.widgets.chat.model.GvaGalleryCard +import com.glia.widgets.chat.model.GvaGalleryCards +import com.glia.widgets.databinding.ChatGvaGalleryItemBinding + +internal class ChatItemHeightManager( + private val uiTheme: UiTheme, + private val layoutInflater: LayoutInflater, + private val resources: Resources +) { + private val measuredHeightsMap = ArrayMap() + + private val gvaGalleryItemViewHolder: GvaGalleryItemViewHolder by lazy { + GvaGalleryItemViewHolder(ChatGvaGalleryItemBinding.inflate(layoutInflater), {}, uiTheme) + } + + private val gvaGalleryCardWidth: Int by lazy { + resources.getDimensionPixelOffset(R.dimen.glia_chat_gva_gallery_card_width) + } + + fun getMeasuredHeight(chatItem: ChatItem): Int? { + return measuredHeightsMap[chatItem] + } + + fun measureHeight(chatItems: List?) { + chatItems + ?.filterIsInstance() // Currently the HeightManager works only for GvaGalleryCards + ?.forEach { chatItem -> + if (!measuredHeightsMap.contains(chatItem)) { + measuredHeightsMap[chatItem] = measureHeight(chatItem) + } + } + } + + private fun measureHeight(gvaGalleryCards: GvaGalleryCards): Int { + return gvaGalleryCards.galleryCards.maxOf(::measureHeight) + } + + private fun measureHeight(gvaGalleryCard: GvaGalleryCard): Int { + gvaGalleryItemViewHolder.bind(gvaGalleryCard) + gvaGalleryItemViewHolder.itemView.measure( + View.MeasureSpec.makeMeasureSpec(gvaGalleryCardWidth, View.MeasureSpec.AT_MOST), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + ) + return gvaGalleryItemViewHolder.itemView.measuredHeight + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt index 444d76fc4..765ee4b76 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt @@ -16,6 +16,7 @@ internal class GvaGalleryAdapter( fun setGalleryCards(galleryCards: List) { this.galleryCards = galleryCards + notifyDataSetChanged() } override fun getItemCount() = galleryCards?.count() ?: 0 @@ -27,6 +28,9 @@ internal class GvaGalleryAdapter( ) override fun onBindViewHolder(holder: GvaGalleryItemViewHolder, position: Int) { - galleryCards?.get(position)?.let { holder.bind(it) } + galleryCards?.get(position)?.let { + holder.bind(it) + holder.bindImage(it) + } } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt index 8da278eab..989c2a581 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt @@ -5,6 +5,7 @@ import android.view.ViewGroup import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.widget.LinearLayoutCompat import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.glia.widgets.R import com.glia.widgets.UiTheme @@ -23,6 +24,7 @@ import com.glia.widgets.view.unifiedui.applyLayerTheme import com.glia.widgets.view.unifiedui.theme.chat.MessageBalloonTheme import com.glia.widgets.view.unifiedui.theme.gva.GvaPersistentButtonTheme import com.google.android.material.button.MaterialButton +import kotlin.properties.Delegates internal class GvaGalleryItemViewHolder( private val binding: ChatGvaGalleryItemBinding, @@ -30,6 +32,8 @@ internal class GvaGalleryItemViewHolder( private val uiTheme: UiTheme ) : ViewHolder(binding.root) { + private var adapter: ButtonsAdapter by Delegates.notNull() + private val operatorTheme: MessageBalloonTheme? by lazy { Dependencies.getGliaThemeManager().theme?.chatTheme?.operatorMessage } @@ -39,7 +43,9 @@ internal class GvaGalleryItemViewHolder( } init { - binding.root.apply { + adapter = ButtonsAdapter(buttonsClickListener, uiTheme, persistentButtonTheme) + binding.buttonsRecyclerView.adapter = adapter + binding.item.apply { uiTheme.operatorMessageBackgroundColor?.let(::getColorStateListCompat)?.also { backgroundTintList = it } @@ -61,56 +67,60 @@ internal class GvaGalleryItemViewHolder( binding.subtitle.isVisible = false } + adapter.setOptions(card.options) + binding.buttonsRecyclerView.isVisible = card.options.isEmpty().not() + } + + fun bindImage(card: GvaGalleryCard) { card.imageUrl?.let { binding.image.load(it) binding.image.isVisible = true } ?: run { binding.image.isVisible = false } + } + + private class ButtonsAdapter( + private val buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, + private val uiTheme: UiTheme, + private val persistentButtonTheme: GvaPersistentButtonTheme? + ) : RecyclerView.Adapter() { + + private var options: List? = null - if (card.options.isEmpty().not()) { - setupButtons(card.options, binding.container) - binding.container.isVisible = true - } else { - binding.container.isVisible = false + fun setOptions(options: List) { + this.options = options + notifyDataSetChanged() } - } - private fun setupButtons( - options: List, - container: ViewGroup - ) { - container.removeAllViews() - for (option in options) { - val onClickListener = buttonsClickListener.run { - View.OnClickListener { onGvaButtonClicked(option) } + class ButtonViewHolder( + private val buttonView: MaterialButton + ) : ViewHolder(buttonView) { + fun bind(button: GvaButton, buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener) { + buttonView.text = button.text + buttonView.setOnClickListener { + buttonsClickListener.onGvaButtonClicked(button) + } } + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ButtonViewHolder { + val styleResId = Utils.getAttrResourceId(parent.context, R.attr.gvaOptionButtonStyle) + val button = MaterialButton(ContextThemeWrapper(parent.context, styleResId), null, 0).also { + it.id = View.generateViewId() - val button = composeButton( - container = container, - text = option.text, - onClickListener = onClickListener - ) + uiTheme.fontRes?.let(parent::getFontCompat)?.also(it::setTypeface) - val params = LinearLayoutCompat.LayoutParams(LinearLayoutCompat.LayoutParams.MATCH_PARENT, LinearLayoutCompat.LayoutParams.WRAP_CONTENT) - container.addView(button, params) + persistentButtonTheme?.button?.also(it::applyButtonTheme) + } + button.layoutParams = LinearLayoutCompat.LayoutParams(LinearLayoutCompat.LayoutParams.MATCH_PARENT, LinearLayoutCompat.LayoutParams.WRAP_CONTENT) + return ButtonViewHolder(button) } - } - - private fun composeButton( - container: ViewGroup, - text: String, - onClickListener: View.OnClickListener - ): MaterialButton { - val styleResId = Utils.getAttrResourceId(container.context, R.attr.gvaOptionButtonStyle) - return MaterialButton(ContextThemeWrapper(container.context, styleResId), null, 0).also { - it.id = View.generateViewId() - it.text = text - it.setOnClickListener(onClickListener) - uiTheme.fontRes?.let(container::getFontCompat)?.also(it::setTypeface) + override fun getItemCount(): Int = options?.size ?: 0 - persistentButtonTheme?.button?.also(it::applyButtonTheme) + override fun onBindViewHolder(holder: ButtonViewHolder, position: Int) { + options?.get(position)?.let { holder.bind(it, buttonsClickListener) } } + } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt index e5b8e4197..0e6aa97b7 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt @@ -1,20 +1,20 @@ package com.glia.widgets.chat.adapter.holder +import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.PagerSnapHelper import com.glia.widgets.UiTheme import com.glia.widgets.chat.adapter.ChatAdapter import com.glia.widgets.chat.adapter.GvaGalleryAdapter import com.glia.widgets.chat.model.GvaGalleryCard import com.glia.widgets.chat.model.GvaGalleryCards import com.glia.widgets.databinding.ChatGvaGalleryLayoutBinding -import com.glia.widgets.databinding.ChatOperatorMessageLayoutBinding internal class GvaGalleryViewHolder( - operatorMessageBinding: ChatOperatorMessageLayoutBinding, - contentBinding: ChatGvaGalleryLayoutBinding, + private val contentBinding: ChatGvaGalleryLayoutBinding, buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, - private val uiTheme: UiTheme -) : OperatorBaseViewHolder(operatorMessageBinding, uiTheme) { + uiTheme: UiTheme +) : OperatorBaseViewHolder(contentBinding.root, contentBinding.chatHeadView, uiTheme) { private val adapter = GvaGalleryAdapter(buttonsClickListener, uiTheme) init { @@ -24,16 +24,27 @@ internal class GvaGalleryViewHolder( LinearLayoutManager.HORIZONTAL, false ) + PagerSnapHelper().attachToRecyclerView(contentBinding.cardRecyclerView) } - fun bind(item: GvaGalleryCards) { + fun bind(item: GvaGalleryCards, measuredHeight: Int?) { updateOperatorStatusView(item) + setupRecyclerViewHeight(measuredHeight) setupItems(item.galleryCards) } private fun setupItems(galleryCards: List) { adapter.setGalleryCards(galleryCards) + contentBinding.cardRecyclerView.scrollToPosition(0) + } + + private fun setupRecyclerViewHeight(measuredHeight: Int?) { + if (measuredHeight != null) { + contentBinding.cardRecyclerView.layoutParams.height = measuredHeight + } else { + contentBinding.cardRecyclerView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + } } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt index 56fdc1259..572ee0a63 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt @@ -26,7 +26,7 @@ internal class GvaPersistentButtonsViewHolder( operatorMessageBinding: ChatOperatorMessageLayoutBinding, private val contentBinding: ChatGvaPersistentButtonsContentBinding, private val uiTheme: UiTheme -) : OperatorBaseViewHolder(operatorMessageBinding, uiTheme) { +) : OperatorBaseViewHolder(operatorMessageBinding.root, operatorMessageBinding.chatHeadView, uiTheme) { private val persistentButtonTheme: GvaPersistentButtonTheme? by lazy { Dependencies.getGliaThemeManager().theme?.chatTheme?.gva?.persistentButtonTheme diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolder.kt index 58755ad07..918cce035 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolder.kt @@ -16,7 +16,7 @@ internal class GvaResponseTextViewHolder( operatorMessageBinding: ChatOperatorMessageLayoutBinding, private val messageContentBinding: ChatReceiveMessageContentBinding, private val uiTheme: UiTheme -) : OperatorBaseViewHolder(operatorMessageBinding, uiTheme) { +) : OperatorBaseViewHolder(operatorMessageBinding.root, operatorMessageBinding.chatHeadView, uiTheme) { init { setupMessageContentView() diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorBaseViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorBaseViewHolder.kt index ba22573bd..d127c3941 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorBaseViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorBaseViewHolder.kt @@ -1,18 +1,20 @@ package com.glia.widgets.chat.adapter.holder +import android.view.View import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.glia.widgets.R import com.glia.widgets.UiTheme import com.glia.widgets.chat.model.OperatorChatItem -import com.glia.widgets.databinding.ChatOperatorMessageLayoutBinding import com.glia.widgets.di.Dependencies +import com.glia.widgets.view.OperatorStatusView import com.glia.widgets.view.unifiedui.theme.chat.MessageBalloonTheme internal open class OperatorBaseViewHolder( - private val operatorMessageBinding: ChatOperatorMessageLayoutBinding, + itemView: View, + private val chatHeadView: OperatorStatusView, private val uiTheme: UiTheme -) : RecyclerView.ViewHolder(operatorMessageBinding.root) { +) : RecyclerView.ViewHolder(itemView) { val operatorTheme: MessageBalloonTheme? by lazy { Dependencies.getGliaThemeManager().theme?.chatTheme?.operatorMessage @@ -23,11 +25,11 @@ internal open class OperatorBaseViewHolder( } fun updateOperatorStatusView(item: OperatorChatItem) { - operatorMessageBinding.chatHeadView.isVisible = item.showChatHead + chatHeadView.isVisible = item.showChatHead if (item.operatorProfileImgUrl != null) { - operatorMessageBinding.chatHeadView.showProfileImage(item.operatorProfileImgUrl) + chatHeadView.showProfileImage(item.operatorProfileImgUrl) } else { - operatorMessageBinding.chatHeadView.showPlaceholder() + chatHeadView.showPlaceholder() } } @@ -55,8 +57,8 @@ internal open class OperatorBaseViewHolder( } private fun setupOperatorStatusView() { - operatorMessageBinding.chatHeadView.setTheme(uiTheme) - operatorMessageBinding.chatHeadView.setShowRippleAnimation(false) - operatorMessageBinding.chatHeadView.applyUserImageTheme(operatorTheme?.userImage) + chatHeadView.setTheme(uiTheme) + chatHeadView.setShowRippleAnimation(false) + chatHeadView.applyUserImageTheme(operatorTheme?.userImage) } } diff --git a/widgetssdk/src/main/res/layout/chat_gva_gallery_item.xml b/widgetssdk/src/main/res/layout/chat_gva_gallery_item.xml index 669f6eaf5..8ca0d9ec1 100644 --- a/widgetssdk/src/main/res/layout/chat_gva_gallery_item.xml +++ b/widgetssdk/src/main/res/layout/chat_gva_gallery_item.xml @@ -3,55 +3,62 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:layout_width="234dp" + android:layout_width="@dimen/glia_chat_gva_gallery_card_width" android:layout_height="match_parent" - android:orientation="vertical" - android:layout_marginEnd="@dimen/glia_large" - android:paddingTop="@dimen/glia_large" - android:paddingHorizontal="@dimen/glia_large" - android:background="@drawable/bg_message"> + android:gravity="bottom" + android:layout_marginEnd="@dimen/glia_large"> - - - + android:paddingHorizontal="@dimen/glia_large" + android:background="@drawable/bg_message"> - + - + + + + + + + \ No newline at end of file diff --git a/widgetssdk/src/main/res/layout/chat_gva_gallery_layout.xml b/widgetssdk/src/main/res/layout/chat_gva_gallery_layout.xml index 63f934c0b..98545d8cb 100644 --- a/widgetssdk/src/main/res/layout/chat_gva_gallery_layout.xml +++ b/widgetssdk/src/main/res/layout/chat_gva_gallery_layout.xml @@ -1,15 +1,42 @@ - + android:layout_marginTop="@dimen/glia_medium" + android:focusable="true"> - + + + + android:layout_marginStart="@dimen/glia_small" + android:layout_marginEnd="@dimen/glia_small" + android:importantForAccessibility="no" + app:imageContentPadding="@dimen/glia_chat_profile_picture_small_content_padding" + app:imageSize="@dimen/glia_chat_profile_picture_small_size" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/chat_operator_guideline" + app:layout_constraintStart_toStartOf="parent" /> - \ No newline at end of file + \ No newline at end of file diff --git a/widgetssdk/src/main/res/values/dimens.xml b/widgetssdk/src/main/res/values/dimens.xml index 70384fbc6..527a7a5d6 100644 --- a/widgetssdk/src/main/res/values/dimens.xml +++ b/widgetssdk/src/main/res/values/dimens.xml @@ -58,6 +58,8 @@ 18sp 6dp + 234dp + 44dp 375dp 64dp From c21f174a0e0e8b877c2f9bccdc37d1e7b97928b6 Mon Sep 17 00:00:00 2001 From: Karl Valliste Date: Wed, 2 Aug 2023 11:32:08 +0300 Subject: [PATCH 20/69] MOB-2498 fixed reversed button dialog (#685) --- widgetssdk/src/main/java/com/glia/widgets/view/Dialogs.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/Dialogs.kt b/widgetssdk/src/main/java/com/glia/widgets/view/Dialogs.kt index 502ac519f..3d3fdf0d4 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/Dialogs.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/Dialogs.kt @@ -169,7 +169,7 @@ object Dialogs { text = negativeButtonText setOnClickListener(negativeButtonClickListener) applyButtonTheme( - backgroundColor = systemNegativeColor, + backgroundColor = if (isButtonsColorsReversed) { brandPrimaryColor } else { systemNegativeColor }, textColor = baseLightColor, textFont = fontFamily ) @@ -179,7 +179,7 @@ object Dialogs { text = positiveButtonText setOnClickListener(positiveButtonClickListener) applyButtonTheme( - backgroundColor = brandPrimaryColor, + backgroundColor = if (isButtonsColorsReversed) { systemNegativeColor } else { brandPrimaryColor }, textColor = baseLightColor, textFont = fontFamily ) From 75cbcd73b8f9477fd05003595ea788ebcaef1c49 Mon Sep 17 00:00:00 2001 From: Karl Valliste Date: Thu, 3 Aug 2023 10:53:09 +0300 Subject: [PATCH 21/69] Added clear all dialogs to engagement ended logic (#687) MOB-2503 --- .../src/main/java/com/glia/widgets/call/CallController.java | 1 + .../main/java/com/glia/widgets/chat/controller/ChatController.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java b/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java index 286d59f46..1cc079915 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java +++ b/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java @@ -231,6 +231,7 @@ public void onHoldChanged(boolean isOnHold) { public void engagementEnded() { Logger.d(TAG, "engagementEndedByOperator"); stop(); + dialogController.dismissDialogs(); } @Override diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index ba79ad2df..9c1fa04a9 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -675,6 +675,7 @@ internal class ChatController( EngagementStateEvent.Type.ENGAGEMENT_ENDED -> { Logger.d(TAG, "Engagement Ended") + dialogController.dismissDialogs() } } } From 705c903fce72c02605e2a2ef952f2bbefed8f5a9 Mon Sep 17 00:00:00 2001 From: Andrii Horishnii Date: Wed, 2 Aug 2023 23:51:47 +0300 Subject: [PATCH 22/69] The region was added to the testing app settings Region value was added as the variable for the local properties. Also, the value can be changed in the application settings. --- app/build.gradle | 10 ++++++---- .../exampleapp/GliaWidgetsConfigManager.kt | 19 ++++++++++++++++--- .../java/com/glia/exampleapp/MainFragment.kt | 3 +-- app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/sdk_basic_prefs.xml | 6 ++++++ 6 files changed, 31 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f8f45ee8b..b267ae331 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,6 +40,7 @@ android { p.each { name, value -> ext[name] = value } } + initEnvProperty('GLIA_REGION', "beta") initEnvProperty('GLIA_API_KEY_SECRET') initEnvProperty('GLIA_API_KEY_ID') initEnvProperty('GLIA_SITE_ID') @@ -47,11 +48,12 @@ android { initEnvProperty('GLIA_JWT', "") initEnvProperty('FIREBASE_PROJECT_ID') initEnvProperty('FIREBASE_API_KEY') - initEnvProperty('FIREBASE_APP_ID') - initEnvProperty('FIREBASE_APP_ID_DEBUG') + initEnvProperty('FIREBASE_APP_ID', "") + initEnvProperty('FIREBASE_APP_ID_DEBUG', "") buildTypes { all { + resValue("string", "environment", GLIA_REGION) resValue("string", "site_id", GLIA_SITE_ID) resValue("string", "glia_api_key_id", GLIA_API_KEY_ID) resValue("string", "glia_api_key_secret", GLIA_API_KEY_SECRET) @@ -63,12 +65,12 @@ android { debug { signingConfig signingConfigs.debug applicationIdSuffix '.debug' - resValue("string", "firebase_app_id", FIREBASE_APP_ID_DEBUG ?: "") + resValue("string", "firebase_app_id", FIREBASE_APP_ID_DEBUG) } release { initWith debug applicationIdSuffix '' - resValue("string", "firebase_app_id", FIREBASE_APP_ID ?: "") + resValue("string", "firebase_app_id", FIREBASE_APP_ID) } } lint { diff --git a/app/src/main/java/com/glia/exampleapp/GliaWidgetsConfigManager.kt b/app/src/main/java/com/glia/exampleapp/GliaWidgetsConfigManager.kt index f86b2c0f1..09568d34d 100644 --- a/app/src/main/java/com/glia/exampleapp/GliaWidgetsConfigManager.kt +++ b/app/src/main/java/com/glia/exampleapp/GliaWidgetsConfigManager.kt @@ -24,13 +24,13 @@ object GliaWidgetsConfigManager { private const val QUEUE_ID_KEY = "queue_id" private const val VISITOR_CONTEXT_ASSET_ID_KEY = "visitor_context_asset_id" private const val REGION_KEY = "environment" - private const val REGION_BETA = "beta" private const val REGION_ACCEPTANCE = "acceptance" private const val BASE_DOMAIN = "base_domain" private const val DEFAULT_BASE_DOMAIN = "at.samo.io" @JvmStatic fun obtainConfigFromDeepLink(data: Uri, applicationContext: Context): GliaWidgetsConfig { + saveRegionIfPresent(data, applicationContext) saveQueueIdToPrefs(data, applicationContext) saveVisitorContextAssetIdIfPresent(data, applicationContext) saveSiteIdToPrefs(data, applicationContext) @@ -53,6 +53,15 @@ object GliaWidgetsConfigManager { ) } + private fun saveRegionIfPresent(data: Uri, applicationContext: Context) { + val visitorContextAssetId = data.getQueryParameter(REGION_KEY) ?: return + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) + sharedPreferences.edit().putString( + applicationContext.getString(R.string.pref_environment), + visitorContextAssetId + ).apply() + } + private fun saveVisitorContextAssetIdIfPresent(data: Uri, applicationContext: Context) { val visitorContextAssetId = data.getQueryParameter(VISITOR_CONTEXT_ASSET_ID_KEY) ?: return val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) @@ -95,10 +104,14 @@ object GliaWidgetsConfigManager { context: Context, uiJsonRemoteConfig: String? = null, runtimeConfig: UiTheme? = null, - region: String = REGION_BETA, + region: String? = null, baseDomain: String = DEFAULT_BASE_DOMAIN, preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) ): GliaWidgetsConfig { + val siteRegion = region ?: preferences.getString( + context.getString(R.string.pref_environment), + context.getString(R.string.environment) + ) val apiKeyId = preferences.getString( context.getString(R.string.pref_api_key_id), context.getString(R.string.glia_api_key_id) @@ -128,7 +141,7 @@ object GliaWidgetsConfigManager { return GliaWidgetsConfig.Builder() .setSiteApiKey(SiteApiKey(apiKeyId!!, apiKeySecret!!)) .setSiteId(siteId) - .setRegion(region) + .setRegion(siteRegion) .setBaseDomain(baseDomain) .setCompanyName(companyName) .setUseOverlay(useOverlay) diff --git a/app/src/main/java/com/glia/exampleapp/MainFragment.kt b/app/src/main/java/com/glia/exampleapp/MainFragment.kt index 2c710e54e..d7e149843 100644 --- a/app/src/main/java/com/glia/exampleapp/MainFragment.kt +++ b/app/src/main/java/com/glia/exampleapp/MainFragment.kt @@ -380,8 +380,7 @@ class MainFragment : Fragment() { createDefaultConfig( context = requireActivity().applicationContext, /*uiJsonRemoteConfig = UnifiedUiConfigurationLoader.fetchLocalGlobalColors(requireContext()),*/ - /*runtimeConfig = createSampleRuntimeConfig(),*/ - /*region = "us"*/ + /*runtimeConfig = createSampleRuntimeConfig()*/ ) ) prepareAuthentication() diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 2354e3106..abe163ea4 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -29,6 +29,7 @@ auth_token queue_id context_asset_id + environment white_label_toggle use_overlay remote_theme_toggle diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d2db43e50..7c325c2be 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,6 +32,7 @@ Site Api Key id Site Api Key Secret Site id (needs restart) + Region (needs restart) Background color Negative color Visitor message bg color diff --git a/app/src/main/res/xml/sdk_basic_prefs.xml b/app/src/main/res/xml/sdk_basic_prefs.xml index 85a2eeed4..daec1786f 100644 --- a/app/src/main/res/xml/sdk_basic_prefs.xml +++ b/app/src/main/res/xml/sdk_basic_prefs.xml @@ -21,6 +21,12 @@ app:title="@string/settings_site_id" app:useSimpleSummaryProvider="true" /> + + Date: Mon, 31 Jul 2023 15:03:04 +0300 Subject: [PATCH 23/69] GVA: Apply Unified Customization to Gallery MOB-2406 --- .../main/res/raw/sample_unified_config.json | 126 ++++++++++++++++++ .../holder/GvaGalleryItemViewHolder.kt | 42 ++++-- .../view/unifiedui/UnifiedUiExtensions.kt | 21 +++ .../config/gva/GvaGalleryCardRemoteConfig.kt | 32 +++++ .../unifiedui/config/gva/GvaRemoteConfig.kt | 8 +- .../unifiedui/theme/defaulttheme/Button.kt | 7 + .../view/unifiedui/theme/defaulttheme/Gva.kt | 37 ++++- .../theme/gva/GvaGalleryCardTheme.kt | 13 ++ .../view/unifiedui/theme/gva/GvaTheme.kt | 3 +- 9 files changed, 277 insertions(+), 12 deletions(-) create mode 100644 widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaGalleryCardRemoteConfig.kt create mode 100644 widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaGalleryCardTheme.kt diff --git a/app/src/main/res/raw/sample_unified_config.json b/app/src/main/res/raw/sample_unified_config.json index 8c5f052d2..96d0bde39 100644 --- a/app/src/main/res/raw/sample_unified_config.json +++ b/app/src/main/res/raw/sample_unified_config.json @@ -3100,6 +3100,132 @@ ] } } + }, + "galleryCard": { + "title": { + "alignment": "center", + "background": { + "type": "fill", + "value": [ + "#F3F3F3" + ] + }, + "font": { + "size": 18, + "style": "bold" + }, + "foreground": { + "type": "fill", + "value": [ + "#04728c" + ] + } + }, + "subtitle": { + "alignment": "center", + "background": { + "type": "fill", + "value": [ + "#F3F3F3" + ] + }, + "font": { + "size": 16, + "style": "regular" + }, + "foreground": { + "type": "fill", + "value": [ + "#000000" + ] + } + }, + "image": { + "border": { + "type": "fill", + "value": [ + "#FFFFFF" + ] + }, + "borderWidth": 2, + "color": { + "type": "fill", + "value": [ + "#F3F3F3" + ] + }, + "cornerRadius": 18 + }, + "button": { + "background": { + "border": { + "type": "fill", + "value": [ + "#FFFFFF" + ] + }, + "borderWidth": 2, + "color": { + "type": "fill", + "value": [ + "#3e95a6" + ] + }, + "cornerRadius": 8 + }, + "shadow": { + "color": { + "type": "fill", + "value": [ + "#FFFFFF" + ] + }, + "offset": 0, + "opacity": 1, + "radius": 0 + }, + "text": { + "alignment": "center", + "background": { + "type": "fill", + "value": [ + "#f0d12b" + ] + }, + "font": { + "size": 16, + "style": "regular" + }, + "foreground": { + "type": "fill", + "value": [ + "#FFFFFF" + ] + } + }, + "tintColor": { + "type": "fill", + "value": [ + "#FFFFFF" + ] + } + }, + "background": { + "border": { + "type": "fill", + "value": [ + "#FFFFFF" + ] + }, + "borderWidth": 2, + "color": { + "type": "fill", + "value": [ + "#1AFFD000" + ] + }, + "cornerRadius": 24 + } } } }, diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt index 989c2a581..5ec476f51 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt @@ -16,19 +16,22 @@ import com.glia.widgets.databinding.ChatGvaGalleryItemBinding import com.glia.widgets.di.Dependencies import com.glia.widgets.helper.Utils import com.glia.widgets.helper.fromHtml +import com.glia.widgets.helper.getColorCompat import com.glia.widgets.helper.getColorStateListCompat import com.glia.widgets.helper.getFontCompat import com.glia.widgets.helper.load import com.glia.widgets.view.unifiedui.applyButtonTheme import com.glia.widgets.view.unifiedui.applyLayerTheme +import com.glia.widgets.view.unifiedui.applyTextTheme +import com.glia.widgets.view.unifiedui.theme.base.ButtonTheme import com.glia.widgets.view.unifiedui.theme.chat.MessageBalloonTheme -import com.glia.widgets.view.unifiedui.theme.gva.GvaPersistentButtonTheme +import com.glia.widgets.view.unifiedui.theme.gva.GvaGalleryCardTheme import com.google.android.material.button.MaterialButton import kotlin.properties.Delegates internal class GvaGalleryItemViewHolder( private val binding: ChatGvaGalleryItemBinding, - private val buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, + buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, private val uiTheme: UiTheme ) : ViewHolder(binding.root) { @@ -38,12 +41,12 @@ internal class GvaGalleryItemViewHolder( Dependencies.getGliaThemeManager().theme?.chatTheme?.operatorMessage } - private val persistentButtonTheme: GvaPersistentButtonTheme? by lazy { - Dependencies.getGliaThemeManager().theme?.chatTheme?.gva?.persistentButtonTheme + private val galleryCardTheme: GvaGalleryCardTheme? by lazy { + Dependencies.getGliaThemeManager().theme?.chatTheme?.gva?.galleryCardTheme } init { - adapter = ButtonsAdapter(buttonsClickListener, uiTheme, persistentButtonTheme) + adapter = ButtonsAdapter(buttonsClickListener, uiTheme, galleryCardTheme?.button) binding.buttonsRecyclerView.adapter = adapter binding.item.apply { uiTheme.operatorMessageBackgroundColor?.let(::getColorStateListCompat)?.also { @@ -53,8 +56,31 @@ internal class GvaGalleryItemViewHolder( importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO // Unified Ui - applyLayerTheme(operatorTheme?.background) + applyLayerTheme(galleryCardTheme?.background ?: operatorTheme?.background) } + binding.title.apply { + uiTheme.operatorMessageTextColor?.let(::getColorCompat)?.also(::setTextColor) + uiTheme.operatorMessageTextColor?.let(::getColorCompat)?.also(::setLinkTextColor) + + uiTheme.fontRes?.let(::getFontCompat)?.also(::setTypeface) + + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + + // Unified Ui + applyTextTheme(galleryCardTheme?.title ?: operatorTheme?.text) + } + binding.subtitle.apply { + uiTheme.operatorMessageTextColor?.let(::getColorCompat)?.also(::setTextColor) + uiTheme.operatorMessageTextColor?.let(::getColorCompat)?.also(::setLinkTextColor) + + uiTheme.fontRes?.let(::getFontCompat)?.also(::setTypeface) + + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + + // Unified Ui + applyTextTheme(galleryCardTheme?.subtitle ?: operatorTheme?.text) + } + galleryCardTheme?.image?.also(binding.image::applyLayerTheme) } fun bind(card: GvaGalleryCard) { @@ -83,7 +109,7 @@ internal class GvaGalleryItemViewHolder( private class ButtonsAdapter( private val buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, private val uiTheme: UiTheme, - private val persistentButtonTheme: GvaPersistentButtonTheme? + private val buttonTheme: ButtonTheme? ) : RecyclerView.Adapter() { private var options: List? = null @@ -110,7 +136,7 @@ internal class GvaGalleryItemViewHolder( uiTheme.fontRes?.let(parent::getFontCompat)?.also(it::setTypeface) - persistentButtonTheme?.button?.also(it::applyButtonTheme) + buttonTheme?.also(it::applyButtonTheme) } button.layoutParams = LinearLayoutCompat.LayoutParams(LinearLayoutCompat.LayoutParams.MATCH_PARENT, LinearLayoutCompat.LayoutParams.WRAP_CONTENT) return ButtonViewHolder(button) diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/UnifiedUiExtensions.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/UnifiedUiExtensions.kt index 36bded1e6..223d738b5 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/UnifiedUiExtensions.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/UnifiedUiExtensions.kt @@ -25,7 +25,9 @@ import com.glia.widgets.view.unifiedui.theme.survey.OptionButtonTheme import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.progressindicator.CircularProgressIndicator +import com.google.android.material.shape.CornerFamily internal fun View.applyColorTheme(color: ColorTheme?) { background = createBackgroundFromTheme(color ?: return) @@ -74,6 +76,25 @@ internal fun View.applyLayerTheme(layer: LayerTheme?) { background = drawable } +internal fun ShapeableImageView.applyLayerTheme(layer: LayerTheme?) { + layer?.fill?.also { + val drawable = (background as? GradientDrawable) ?: GradientDrawable() + if (it.isGradient) { + drawable.colors = it.valuesArray + } else { + drawable.setColor(it.primaryColor) + } + background = drawable + } + + layer?.stroke?.also { strokeColor = ColorStateList.valueOf(it) } + layer?.borderWidth?.also(::setStrokeWidth) + + layer?.cornerRadius?.also { + shapeAppearanceModel = shapeAppearanceModel.toBuilder().setAllCorners(CornerFamily.ROUNDED, it).build() + } +} + internal fun MaterialCardView.applyCardLayerTheme(layer: LayerTheme?) { layer?.fill?.primaryColor?.also { /* diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaGalleryCardRemoteConfig.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaGalleryCardRemoteConfig.kt new file mode 100644 index 000000000..b4297a113 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaGalleryCardRemoteConfig.kt @@ -0,0 +1,32 @@ +package com.glia.widgets.view.unifiedui.config.gva + +import com.glia.widgets.view.unifiedui.config.base.ButtonRemoteConfig +import com.glia.widgets.view.unifiedui.config.base.LayerRemoteConfig +import com.glia.widgets.view.unifiedui.config.base.TextRemoteConfig +import com.glia.widgets.view.unifiedui.theme.gva.GvaGalleryCardTheme +import com.google.gson.annotations.SerializedName + +internal data class GvaGalleryCardRemoteConfig( + @SerializedName("title") + val titleRemoteConfig: TextRemoteConfig?, + + @SerializedName("subtitle") + val subtitleRemoteConfig: TextRemoteConfig?, + + @SerializedName("image") + val imageRemoteConfig: LayerRemoteConfig?, + + @SerializedName("button") + val buttonRemoteConfig: ButtonRemoteConfig?, + + @SerializedName("background") + val backgroundRemoteConfig: LayerRemoteConfig? +) { + fun toGvaGalleryCardTheme(): GvaGalleryCardTheme = GvaGalleryCardTheme( + title = titleRemoteConfig?.toTextTheme(), + subtitle = subtitleRemoteConfig?.toTextTheme(), + image = imageRemoteConfig?.toLayerTheme(), + button = buttonRemoteConfig?.toButtonTheme(), + background = backgroundRemoteConfig?.toLayerTheme() + ) +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaRemoteConfig.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaRemoteConfig.kt index 4b5fb1fc0..7db26dd9b 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaRemoteConfig.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/config/gva/GvaRemoteConfig.kt @@ -9,10 +9,14 @@ internal data class GvaRemoteConfig( val quickReplyRemoteConfig: ButtonRemoteConfig?, @SerializedName("persistentButton") - val persistentButtonRemoteConfig: GvaPersistentButtonRemoteConfig? + val persistentButtonRemoteConfig: GvaPersistentButtonRemoteConfig?, + + @SerializedName("galleryCard") + val galleryCardRemoteConfig: GvaGalleryCardRemoteConfig? ) { fun toGvaTheme(): GvaTheme = GvaTheme( quickReplyTheme = quickReplyRemoteConfig?.toButtonTheme(), - persistentButtonTheme = persistentButtonRemoteConfig?.toGvaPersistentButtonTheme() + persistentButtonTheme = persistentButtonRemoteConfig?.toGvaPersistentButtonTheme(), + galleryCardTheme = galleryCardRemoteConfig?.toGvaGalleryCardTheme() ) } diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Button.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Button.kt index 734f13105..708332c8d 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Button.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Button.kt @@ -45,6 +45,13 @@ internal fun OutlinedButtonTheme( ) } +/** + * Default theme for GVA Button + */ +internal fun GvaDefaultButtonTheme(pallet: ColorPallet) = pallet.run { + DefaultButtonTheme(text = baseDarkColorTheme, background = backgroundColorTheme) +} + private fun DefaultButtonTheme( text: ColorTheme? = null, background: ColorTheme? = null, diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Gva.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Gva.kt index 63312e10d..30093c2ed 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Gva.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/defaulttheme/Gva.kt @@ -2,12 +2,47 @@ package com.glia.widgets.view.unifiedui.theme.defaulttheme +import com.glia.widgets.view.unifiedui.composeIfAtLeastOneNotNull import com.glia.widgets.view.unifiedui.theme.ColorPallet +import com.glia.widgets.view.unifiedui.theme.base.LayerTheme +import com.glia.widgets.view.unifiedui.theme.gva.GvaGalleryCardTheme +import com.glia.widgets.view.unifiedui.theme.gva.GvaPersistentButtonTheme import com.glia.widgets.view.unifiedui.theme.gva.GvaTheme /** * Default Theme for Gva */ internal fun GvaTheme(pallet: ColorPallet): GvaTheme = GvaTheme( - quickReplyTheme = pallet.primaryColorTheme?.let { OutlinedButtonTheme(it, it) } + quickReplyTheme = pallet.primaryColorTheme?.let { OutlinedButtonTheme(it, it) }, + persistentButtonTheme = GvaPersistentButtonTheme(pallet), + galleryCardTheme = GvaGalleryCardTheme(pallet) ) + +private fun GvaPersistentButtonTheme( + pallet: ColorPallet +): GvaPersistentButtonTheme? = pallet.run { + composeIfAtLeastOneNotNull(baseLightColorTheme, baseDarkColorTheme, backgroundColorTheme) { + GvaPersistentButtonTheme( + title = BaseDarkColorTextTheme(this), + background = LayerTheme( + fill = baseLightColorTheme + ), + button = GvaDefaultButtonTheme(this) + ) + } +} + +private fun GvaGalleryCardTheme( + pallet: ColorPallet +): GvaGalleryCardTheme? = pallet.run { + composeIfAtLeastOneNotNull(baseLightColorTheme, baseDarkColorTheme, backgroundColorTheme) { + GvaGalleryCardTheme( + title = BaseDarkColorTextTheme(this), + subtitle = BaseDarkColorTextTheme(this), + background = LayerTheme( + fill = baseLightColorTheme + ), + button = GvaDefaultButtonTheme(this) + ) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaGalleryCardTheme.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaGalleryCardTheme.kt new file mode 100644 index 000000000..ab00918be --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaGalleryCardTheme.kt @@ -0,0 +1,13 @@ +package com.glia.widgets.view.unifiedui.theme.gva + +import com.glia.widgets.view.unifiedui.theme.base.ButtonTheme +import com.glia.widgets.view.unifiedui.theme.base.LayerTheme +import com.glia.widgets.view.unifiedui.theme.base.TextTheme + +internal data class GvaGalleryCardTheme( + val title: TextTheme? = null, + val subtitle: TextTheme? = null, + val image: LayerTheme? = null, + val button: ButtonTheme? = null, + val background: LayerTheme? = null +) diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaTheme.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaTheme.kt index 8580452b8..4fb076216 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaTheme.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/theme/gva/GvaTheme.kt @@ -4,5 +4,6 @@ import com.glia.widgets.view.unifiedui.theme.base.ButtonTheme internal data class GvaTheme( val quickReplyTheme: ButtonTheme? = null, - val persistentButtonTheme: GvaPersistentButtonTheme? = null + val persistentButtonTheme: GvaPersistentButtonTheme? = null, + val galleryCardTheme: GvaGalleryCardTheme? = null ) From c80ca21964b9d5fc28ffb5e453f5e123d5fc4b51 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Mon, 7 Aug 2023 11:10:39 +0300 Subject: [PATCH 24/69] Remove LeakCanary for default builds --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index b267ae331..d84386de6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,7 +109,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' +// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$testLibraryVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" From 85c450878ee7905323083b61a06a15a922d4b955 Mon Sep 17 00:00:00 2001 From: Andrii Horishnii Date: Fri, 4 Aug 2023 15:02:30 +0300 Subject: [PATCH 25/69] Set WebViewCardAdapter as a default CustomCardAdapter MOB-2513 --- .../src/main/java/com/glia/widgets/GliaWidgets.java | 8 ++++++-- .../com/glia/widgets/chat/adapter/WebViewCardAdapter.java | 8 -------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java b/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java index 5f4779a0a..72c979cff 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java +++ b/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java @@ -117,7 +117,7 @@ public class GliaWidgets { public static final String CHAT_TYPE = "chat_type"; @Nullable - private static CustomCardAdapter customCardAdapter; + private static CustomCardAdapter customCardAdapter = new WebViewCardAdapter(); /** * Should be called when the application is starting in {@link Application}.onCreate() @@ -274,8 +274,12 @@ public static void getVisitorInfo(Consumer visitorCallback, Con /** * Allows configuring custom response cards based on metadata. + *

+ * Glia SDK uses {@link WebViewCardAdapter} by default. + * This method allows setting the custom implementation of {@link CustomCardAdapter}. * - * @param customCardAdapter an instance of {@link CustomCardAdapter}. + * @param customCardAdapter an instance of {@link CustomCardAdapter} + * or {@code null} for the default, not Custom Card, Glia message implementation. * @see CustomCardAdapter * @see WebViewCardAdapter */ diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/WebViewCardAdapter.java b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/WebViewCardAdapter.java index 56ed3bb9c..dc73fdd40 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/WebViewCardAdapter.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/WebViewCardAdapter.java @@ -7,20 +7,12 @@ import androidx.annotation.Nullable; import com.glia.androidsdk.chat.ChatMessage; -import com.glia.widgets.GliaWidgets; import com.glia.widgets.UiTheme; import com.glia.widgets.chat.adapter.holder.CustomCardViewHolder; import com.glia.widgets.chat.adapter.holder.WebViewViewHolder; /** * The implementation of {@link CustomCardAdapter} allows displaying all card messages in WebViews. - *

- * It provides a simple way to set up {@link GliaWidgets#setCustomCardAdapter(CustomCardAdapter)}. - *

- * Usage example: - *

{@code
- * GliaWidgets.setCustomCardAdapter(new WebViewCardAdapter());
- * }
  * @see CustomCardAdapter
  * @see WebViewViewHolder
  */

From cf3dbf86497e14651f8bdbbe433153c145633fd7 Mon Sep 17 00:00:00 2001
From: Aleksandr Chatsky 
Date: Wed, 12 Jul 2023 16:32:42 +0300
Subject: [PATCH 26/69] Make CI PR script to create new branch from development

The previous change made it so that it would merge the PR into the development branch however the branch itself was created from the master and not development. As a result, there were some strange additional commits that should not be there.
---
 bitrise.yml | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/bitrise.yml b/bitrise.yml
index 86892fd95..627135479 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -37,22 +37,24 @@ workflows:
             set -x
 
             NEW_VERSION=`./gradlew -q printCurrentVersionName`
-            BRANCH_NAME="project-version-increment/${NEW_VERSION}"
+            NEW_BRANCH_NAME="project-version-increment/${NEW_VERSION}"
+            BASE_BRANCH_NAME="development"
             MESSAGE="Increment project version to ${NEW_VERSION}"
 
-            git checkout -b $BRANCH_NAME
+            git fetch origin $BASE_BRANCH_NAME
+            git checkout -b $NEW_BRANCH_NAME origin/$BASE_BRANCH_NAME
             # git add -u stages modifications and deletions, without new files
             # added it to prevent CHANGELOG.md from being included in the PR
             git add -u
             git commit -m "$MESSAGE"
-            git push origin "$BRANCH_NAME":"$BRANCH_NAME"
+            git push origin "$NEW_BRANCH_NAME":"$NEW_BRANCH_NAME"
 
             PULL_REQUEST_NUMBER=$(curl \
               -X POST \
               -H "Accept: application/vnd.github+json" \
               -H "Authorization: Bearer $GITHUB_API_TOKEN" \
               https://api.github.com/repos/salemove/android-sdk-widgets/pulls \
-              -d "{\"title\":\"${MESSAGE}\",\"head\":\"${BRANCH_NAME}\",\"base\":\"development\"}" | jq --raw-output '.number')
+              -d "{\"title\":\"${MESSAGE}\",\"head\":\"${NEW_BRANCH_NAME}\",\"base\":\"development\"}" | jq --raw-output '.number')
 
             curl \
               -X POST \

From e379dfcdb7922592e6cb9e5c63c09c3bc15b21d5 Mon Sep 17 00:00:00 2001
From: Andrii Horishnii 
Date: Tue, 8 Aug 2023 19:45:40 +0300
Subject: [PATCH 27/69] Remove the notification about audio/video engagement
 starts if an application is killed.

MOB-2481
---
 widgetssdk/src/main/AndroidManifest.xml          |  2 ++
 .../notification/NotificationRemovalService.kt   | 16 ++++++++++++++++
 .../notification/device/NotificationManager.kt   | 12 ++++++++++++
 3 files changed, 30 insertions(+)
 create mode 100644 widgetssdk/src/main/java/com/glia/widgets/core/notification/NotificationRemovalService.kt

diff --git a/widgetssdk/src/main/AndroidManifest.xml b/widgetssdk/src/main/AndroidManifest.xml
index 52468d5d3..58ab6e780 100644
--- a/widgetssdk/src/main/AndroidManifest.xml
+++ b/widgetssdk/src/main/AndroidManifest.xml
@@ -83,6 +83,8 @@
             android:name=".core.screensharing.MediaProjectionService"
             android:foregroundServiceType="mediaProjection" />
 
+        
+
         
diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/notification/NotificationRemovalService.kt b/widgetssdk/src/main/java/com/glia/widgets/core/notification/NotificationRemovalService.kt
new file mode 100644
index 000000000..68e9ce9bb
--- /dev/null
+++ b/widgetssdk/src/main/java/com/glia/widgets/core/notification/NotificationRemovalService.kt
@@ -0,0 +1,16 @@
+package com.glia.widgets.core.notification
+
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+
+class NotificationRemovalService : Service() {
+    override fun onBind(intent: Intent?): IBinder? = null
+
+    override fun onTaskRemoved(rootIntent: Intent?) {
+        val notificationManager = getSystemService(NotificationManager::class.java)
+        notificationManager.cancel(NotificationFactory.CALL_NOTIFICATION_ID)
+        super.onTaskRemoved(rootIntent)
+    }
+}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/notification/device/NotificationManager.kt b/widgetssdk/src/main/java/com/glia/widgets/core/notification/device/NotificationManager.kt
index 346d8293e..246d84a1b 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/core/notification/device/NotificationManager.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/core/notification/device/NotificationManager.kt
@@ -8,6 +8,7 @@ import android.os.Build
 import androidx.annotation.RequiresApi
 import com.glia.widgets.R
 import com.glia.widgets.core.notification.NotificationFactory
+import com.glia.widgets.core.notification.NotificationRemovalService
 import com.glia.widgets.core.notification.areNotificationsEnabledForChannel
 import com.glia.widgets.core.screensharing.MediaProjectionService
 
@@ -60,6 +61,7 @@ internal class NotificationManager(private val applicationContext: Application)
                 NotificationFactory.CALL_NOTIFICATION_ID,
                 NotificationFactory.createAudioCallNotification(applicationContext)
             )
+            startNotificationRemovalService()
         }
     }
 
@@ -69,11 +71,21 @@ internal class NotificationManager(private val applicationContext: Application)
                 NotificationFactory.CALL_NOTIFICATION_ID,
                 NotificationFactory.createVideoCallNotification(applicationContext, isTwoWayVideo, hasAudio)
             )
+            startNotificationRemovalService()
         }
     }
 
+    private fun startNotificationRemovalService() {
+        applicationContext.startService(
+            Intent(applicationContext, NotificationRemovalService::class.java)
+        )
+    }
+
     override fun removeCallNotification() {
         notificationManager.cancel(NotificationFactory.CALL_NOTIFICATION_ID)
+        applicationContext.stopService(
+            Intent(applicationContext, NotificationRemovalService::class.java)
+        )
     }
 
     /**

From a1a03f9c1e174c7cbb426eead0db011aaea14fe6 Mon Sep 17 00:00:00 2001
From: Andrii Horishnii 
Date: Tue, 8 Aug 2023 21:34:09 +0300
Subject: [PATCH 28/69] GVA: Use RecyclerView for the Persistent button options

The ButtonAdapter from the GvaGalleryItemViewHolder was extracted to a separate file. Now it is used for both GvaGalleryItemViewHolder and GvaPersistentButtonsViewHolder.

MOB-2520
---
 .../glia/widgets/chat/adapter/ChatAdapter.kt  |  3 +-
 .../widgets/chat/adapter/GvaButtonsAdapter.kt | 58 ++++++++++++++++++
 .../holder/GvaGalleryItemViewHolder.kt        | 59 +-----------------
 .../holder/GvaPersistentButtonsViewHolder.kt  | 61 +++----------------
 .../chat_gva_persistent_buttons_content.xml   | 12 ++--
 5 files changed, 79 insertions(+), 114 deletions(-)
 create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaButtonsAdapter.kt

diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt
index 1c0017bb4..0fb7e1780 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt
@@ -178,6 +178,7 @@ internal class ChatAdapter(
                         operatorMessageBinding.contentLayout,
                         true
                     ),
+                    onGvaButtonsClickListener,
                     uiTheme
                 )
             }
@@ -249,7 +250,7 @@ internal class ChatAdapter(
 
             is SystemChatItem -> (holder as SystemMessageViewHolder).bind(chatItem.message)
             is GvaResponseText -> (holder as GvaResponseTextViewHolder).bind(chatItem)
-            is GvaPersistentButtons -> (holder as GvaPersistentButtonsViewHolder).bind(chatItem, onGvaButtonsClickListener)
+            is GvaPersistentButtons -> (holder as GvaPersistentButtonsViewHolder).bind(chatItem)
             is GvaGalleryCards -> (holder as GvaGalleryViewHolder).bind(chatItem, chatItemHeightManager.getMeasuredHeight(chatItem))
             is CustomCardChatItem -> {
                 (holder as CustomCardViewHolder).bind(chatItem.message) { text: String, value: String ->
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaButtonsAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaButtonsAdapter.kt
new file mode 100644
index 000000000..6c0155683
--- /dev/null
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaButtonsAdapter.kt
@@ -0,0 +1,58 @@
+package com.glia.widgets.chat.adapter
+
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.view.ContextThemeWrapper
+import androidx.appcompat.widget.LinearLayoutCompat
+import androidx.recyclerview.widget.RecyclerView
+import com.glia.widgets.R
+import com.glia.widgets.UiTheme
+import com.glia.widgets.chat.model.GvaButton
+import com.glia.widgets.helper.Utils
+import com.glia.widgets.helper.getFontCompat
+import com.glia.widgets.view.unifiedui.applyButtonTheme
+import com.glia.widgets.view.unifiedui.theme.base.ButtonTheme
+import com.google.android.material.button.MaterialButton
+
+internal class GvaButtonsAdapter(
+    private val buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener,
+    private val uiTheme: UiTheme,
+    private val buttonTheme: ButtonTheme?
+) : RecyclerView.Adapter() {
+    private var options: List? = null
+
+    fun setOptions(options: List) {
+        this.options = options
+        notifyDataSetChanged()
+    }
+
+    class ButtonViewHolder(
+        private val buttonView: MaterialButton
+    ) : RecyclerView.ViewHolder(buttonView) {
+        fun bind(button: GvaButton, buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener) {
+            buttonView.text = button.text
+            buttonView.setOnClickListener {
+                buttonsClickListener.onGvaButtonClicked(button)
+            }
+        }
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ButtonViewHolder {
+        val styleResId = Utils.getAttrResourceId(parent.context, R.attr.gvaOptionButtonStyle)
+        val button = MaterialButton(ContextThemeWrapper(parent.context, styleResId), null, 0).also {
+            it.id = View.generateViewId()
+
+            uiTheme.fontRes?.let(parent::getFontCompat)?.also(it::setTypeface)
+
+            buttonTheme?.also(it::applyButtonTheme)
+        }
+        button.layoutParams = LinearLayoutCompat.LayoutParams(LinearLayoutCompat.LayoutParams.MATCH_PARENT, LinearLayoutCompat.LayoutParams.WRAP_CONTENT)
+        return ButtonViewHolder(button)
+    }
+
+    override fun getItemCount(): Int = options?.size ?: 0
+
+    override fun onBindViewHolder(holder: ButtonViewHolder, position: Int) {
+        options?.get(position)?.let { holder.bind(it, buttonsClickListener) }
+    }
+}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt
index 5ec476f51..04ac5fdff 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt
@@ -1,32 +1,23 @@
 package com.glia.widgets.chat.adapter.holder
 
 import android.view.View
-import android.view.ViewGroup
-import androidx.appcompat.view.ContextThemeWrapper
-import androidx.appcompat.widget.LinearLayoutCompat
 import androidx.core.view.isVisible
-import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
-import com.glia.widgets.R
 import com.glia.widgets.UiTheme
 import com.glia.widgets.chat.adapter.ChatAdapter
-import com.glia.widgets.chat.model.GvaButton
+import com.glia.widgets.chat.adapter.GvaButtonsAdapter
 import com.glia.widgets.chat.model.GvaGalleryCard
 import com.glia.widgets.databinding.ChatGvaGalleryItemBinding
 import com.glia.widgets.di.Dependencies
-import com.glia.widgets.helper.Utils
 import com.glia.widgets.helper.fromHtml
 import com.glia.widgets.helper.getColorCompat
 import com.glia.widgets.helper.getColorStateListCompat
 import com.glia.widgets.helper.getFontCompat
 import com.glia.widgets.helper.load
-import com.glia.widgets.view.unifiedui.applyButtonTheme
 import com.glia.widgets.view.unifiedui.applyLayerTheme
 import com.glia.widgets.view.unifiedui.applyTextTheme
-import com.glia.widgets.view.unifiedui.theme.base.ButtonTheme
 import com.glia.widgets.view.unifiedui.theme.chat.MessageBalloonTheme
 import com.glia.widgets.view.unifiedui.theme.gva.GvaGalleryCardTheme
-import com.google.android.material.button.MaterialButton
 import kotlin.properties.Delegates
 
 internal class GvaGalleryItemViewHolder(
@@ -35,7 +26,7 @@ internal class GvaGalleryItemViewHolder(
     private val uiTheme: UiTheme
 ) : ViewHolder(binding.root) {
 
-    private var adapter: ButtonsAdapter by Delegates.notNull()
+    private var adapter: GvaButtonsAdapter by Delegates.notNull()
 
     private val operatorTheme: MessageBalloonTheme? by lazy {
         Dependencies.getGliaThemeManager().theme?.chatTheme?.operatorMessage
@@ -46,7 +37,7 @@ internal class GvaGalleryItemViewHolder(
     }
 
     init {
-        adapter = ButtonsAdapter(buttonsClickListener, uiTheme, galleryCardTheme?.button)
+        adapter = GvaButtonsAdapter(buttonsClickListener, uiTheme, galleryCardTheme?.button)
         binding.buttonsRecyclerView.adapter = adapter
         binding.item.apply {
             uiTheme.operatorMessageBackgroundColor?.let(::getColorStateListCompat)?.also {
@@ -105,48 +96,4 @@ internal class GvaGalleryItemViewHolder(
             binding.image.isVisible = false
         }
     }
-
-    private class ButtonsAdapter(
-        private val buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener,
-        private val uiTheme: UiTheme,
-        private val buttonTheme: ButtonTheme?
-    ) : RecyclerView.Adapter() {
-
-        private var options: List? = null
-
-        fun setOptions(options: List) {
-            this.options = options
-            notifyDataSetChanged()
-        }
-
-        class ButtonViewHolder(
-            private val buttonView: MaterialButton
-        ) : ViewHolder(buttonView) {
-            fun bind(button: GvaButton, buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener) {
-                buttonView.text = button.text
-                buttonView.setOnClickListener {
-                    buttonsClickListener.onGvaButtonClicked(button)
-                }
-            }
-        }
-        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ButtonViewHolder {
-            val styleResId = Utils.getAttrResourceId(parent.context, R.attr.gvaOptionButtonStyle)
-            val button = MaterialButton(ContextThemeWrapper(parent.context, styleResId), null, 0).also {
-                it.id = View.generateViewId()
-
-                uiTheme.fontRes?.let(parent::getFontCompat)?.also(it::setTypeface)
-
-                buttonTheme?.also(it::applyButtonTheme)
-            }
-            button.layoutParams = LinearLayoutCompat.LayoutParams(LinearLayoutCompat.LayoutParams.MATCH_PARENT, LinearLayoutCompat.LayoutParams.WRAP_CONTENT)
-            return ButtonViewHolder(button)
-        }
-
-        override fun getItemCount(): Int = options?.size ?: 0
-
-        override fun onBindViewHolder(holder: ButtonViewHolder, position: Int) {
-            options?.get(position)?.let { holder.bind(it, buttonsClickListener) }
-        }
-
-    }
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt
index 572ee0a63..6e62fc518 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt
@@ -1,38 +1,38 @@
 package com.glia.widgets.chat.adapter.holder
 
 import android.view.View
-import android.view.ViewGroup
-import androidx.appcompat.view.ContextThemeWrapper
-import androidx.appcompat.widget.LinearLayoutCompat
-import com.glia.widgets.R
 import com.glia.widgets.UiTheme
 import com.glia.widgets.chat.adapter.ChatAdapter
+import com.glia.widgets.chat.adapter.GvaButtonsAdapter
 import com.glia.widgets.chat.model.GvaPersistentButtons
 import com.glia.widgets.databinding.ChatGvaPersistentButtonsContentBinding
 import com.glia.widgets.databinding.ChatOperatorMessageLayoutBinding
 import com.glia.widgets.di.Dependencies
-import com.glia.widgets.helper.Utils
 import com.glia.widgets.helper.fromHtml
 import com.glia.widgets.helper.getColorCompat
 import com.glia.widgets.helper.getColorStateListCompat
 import com.glia.widgets.helper.getFontCompat
-import com.glia.widgets.view.unifiedui.applyButtonTheme
 import com.glia.widgets.view.unifiedui.applyLayerTheme
 import com.glia.widgets.view.unifiedui.applyTextTheme
 import com.glia.widgets.view.unifiedui.theme.gva.GvaPersistentButtonTheme
-import com.google.android.material.button.MaterialButton
+import kotlin.properties.Delegates
 
 internal class GvaPersistentButtonsViewHolder(
     operatorMessageBinding: ChatOperatorMessageLayoutBinding,
     private val contentBinding: ChatGvaPersistentButtonsContentBinding,
+    buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener,
     private val uiTheme: UiTheme
 ) : OperatorBaseViewHolder(operatorMessageBinding.root, operatorMessageBinding.chatHeadView, uiTheme) {
 
+    private var adapter: GvaButtonsAdapter by Delegates.notNull()
+
     private val persistentButtonTheme: GvaPersistentButtonTheme? by lazy {
         Dependencies.getGliaThemeManager().theme?.chatTheme?.gva?.persistentButtonTheme
     }
 
     init {
+        adapter = GvaButtonsAdapter(buttonsClickListener, uiTheme, persistentButtonTheme?.button)
+        contentBinding.buttonsRecyclerView.adapter = adapter
         contentBinding.root.apply {
             uiTheme.operatorMessageBackgroundColor?.let(::getColorStateListCompat)?.also {
                 backgroundTintList = it
@@ -56,55 +56,12 @@ internal class GvaPersistentButtonsViewHolder(
         }
     }
 
-    fun bind(item: GvaPersistentButtons, onGvaButtonsClickListener: ChatAdapter.OnGvaButtonsClickListener) {
+    fun bind(item: GvaPersistentButtons) {
         updateOperatorStatusView(item)
         updateItemContentDescription(item.operatorName, item.content)
 
         contentBinding.message.text = item.content.fromHtml()
-        setupButtons(item, contentBinding.container, onGvaButtonsClickListener)
-    }
-
-    private fun setupButtons(
-        item: GvaPersistentButtons,
-        container: ViewGroup,
-        onGvaButtonsClickListener: ChatAdapter.OnGvaButtonsClickListener
-    ) {
-        container.removeAllViews()
-        val horizontalMargin = container.resources.getDimensionPixelOffset(R.dimen.glia_large)
-        val verticalMargin = container.resources.getDimensionPixelOffset(R.dimen.glia_medium)
-        for (option in item.options) {
-            val onClickListener = onGvaButtonsClickListener.run {
-                View.OnClickListener { onGvaButtonClicked(option) }
-            }
-
-            val button = composeButton(
-                container = container,
-                text = option.text,
-                onClickListener = onClickListener
-            )
 
-            val params = LinearLayoutCompat.LayoutParams(LinearLayoutCompat.LayoutParams.MATCH_PARENT, LinearLayoutCompat.LayoutParams.WRAP_CONTENT)
-            val topMargin = if (item.options.first() == option) verticalMargin else 0
-            val bottomMargin = if (item.options.last() == option) verticalMargin else 0
-            params.setMargins(horizontalMargin, topMargin, horizontalMargin, bottomMargin)
-            container.addView(button, params)
-        }
-    }
-
-    private fun composeButton(
-        container: ViewGroup,
-        text: String,
-        onClickListener: View.OnClickListener
-    ): MaterialButton {
-        val styleResId = Utils.getAttrResourceId(container.context, R.attr.gvaOptionButtonStyle)
-        return MaterialButton(ContextThemeWrapper(container.context, styleResId), null, 0).also {
-            it.id = View.generateViewId()
-            it.text = text
-            it.setOnClickListener(onClickListener)
-
-            uiTheme.fontRes?.let(container::getFontCompat)?.also(it::setTypeface)
-
-            persistentButtonTheme?.button?.also(it::applyButtonTheme)
-        }
+        adapter.setOptions(item.options)
     }
 }
diff --git a/widgetssdk/src/main/res/layout/chat_gva_persistent_buttons_content.xml b/widgetssdk/src/main/res/layout/chat_gva_persistent_buttons_content.xml
index 87a8ac60d..75a740547 100644
--- a/widgetssdk/src/main/res/layout/chat_gva_persistent_buttons_content.xml
+++ b/widgetssdk/src/main/res/layout/chat_gva_persistent_buttons_content.xml
@@ -1,8 +1,8 @@
 
-
@@ -20,10 +20,12 @@
         android:textIsSelectable="true"
         android:textColorLink="?attr/gliaBaseDarkColor" />
 
-    
+        android:paddingVertical="@dimen/glia_medium"
+        android:paddingHorizontal="@dimen/glia_large"
+        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
 
 
\ No newline at end of file

From 07764eb61eb05a2b9151e7acd638cd8006e45988 Mon Sep 17 00:00:00 2001
From: Andrii Horishnii 
Date: Thu, 3 Aug 2023 22:57:12 +0300
Subject: [PATCH 29/69] GVA: Make Gallery ADA compliant

MOB-2410
---
 .../chat/adapter/ChatItemHeightManager.kt     |  2 +-
 .../widgets/chat/adapter/GvaButtonsAdapter.kt |  1 +
 .../widgets/chat/adapter/GvaGalleryAdapter.kt |  7 +--
 .../holder/GvaGalleryItemViewHolder.kt        | 49 ++++++++++++++++++-
 .../main/res/layout/chat_gva_gallery_item.xml |  6 ++-
 widgetssdk/src/main/res/values/strings.xml    |  1 +
 6 files changed, 58 insertions(+), 8 deletions(-)

diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt
index 1e0cae348..5f247b377 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt
@@ -46,7 +46,7 @@ internal class ChatItemHeightManager(
     }
 
     private fun measureHeight(gvaGalleryCard: GvaGalleryCard): Int {
-        gvaGalleryItemViewHolder.bind(gvaGalleryCard)
+        gvaGalleryItemViewHolder.bindForMeasure(gvaGalleryCard)
         gvaGalleryItemViewHolder.itemView.measure(
             View.MeasureSpec.makeMeasureSpec(gvaGalleryCardWidth, View.MeasureSpec.AT_MOST),
             View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaButtonsAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaButtonsAdapter.kt
index 6c0155683..3173228d2 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaButtonsAdapter.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaButtonsAdapter.kt
@@ -30,6 +30,7 @@ internal class GvaButtonsAdapter(
         private val buttonView: MaterialButton
     ) : RecyclerView.ViewHolder(buttonView) {
         fun bind(button: GvaButton, buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener) {
+            buttonView.contentDescription = button.text
             buttonView.text = button.text
             buttonView.setOnClickListener {
                 buttonsClickListener.onGvaButtonClicked(button)
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt
index 765ee4b76..75c8764a1 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt
@@ -28,9 +28,10 @@ internal class GvaGalleryAdapter(
     )
 
     override fun onBindViewHolder(holder: GvaGalleryItemViewHolder, position: Int) {
-        galleryCards?.get(position)?.let {
-            holder.bind(it)
-            holder.bindImage(it)
+        galleryCards?.apply {
+            getOrNull(position)?.let {
+                holder.bind(it, position, size)
+            }
         }
     }
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt
index 04ac5fdff..ed1ac85c9 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt
@@ -1,8 +1,14 @@
 package com.glia.widgets.chat.adapter.holder
 
+import android.os.Bundle
 import android.view.View
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityNodeInfo
+import androidx.core.view.AccessibilityDelegateCompat
+import androidx.core.view.ViewCompat
 import androidx.core.view.isVisible
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import com.glia.widgets.R
 import com.glia.widgets.UiTheme
 import com.glia.widgets.chat.adapter.ChatAdapter
 import com.glia.widgets.chat.adapter.GvaButtonsAdapter
@@ -37,6 +43,18 @@ internal class GvaGalleryItemViewHolder(
     }
 
     init {
+        ViewCompat.setAccessibilityDelegate(
+            binding.root,
+            object : AccessibilityDelegateCompat() {
+                override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
+                    if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
+                        // Sends an accessibility event of accessibility focus type.
+                        host.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED)
+                    }
+                    return super.performAccessibilityAction(host, action, args)
+                }
+            }
+        )
         adapter = GvaButtonsAdapter(buttonsClickListener, uiTheme, galleryCardTheme?.button)
         binding.buttonsRecyclerView.adapter = adapter
         binding.item.apply {
@@ -74,7 +92,19 @@ internal class GvaGalleryItemViewHolder(
         galleryCardTheme?.image?.also(binding.image::applyLayerTheme)
     }
 
-    fun bind(card: GvaGalleryCard) {
+    fun bindForMeasure(card: GvaGalleryCard) {
+        bindTexts(card)
+        bindButtons(card)
+    }
+
+    fun bind(card: GvaGalleryCard, position: Int, size: Int) {
+        bindTexts(card)
+        bindButtons(card)
+        bindImage(card)
+        updateContendDescription(card, position, size)
+    }
+
+    private fun bindTexts(card: GvaGalleryCard) {
         binding.title.text = card.title.fromHtml()
 
         card.subtitle?.let {
@@ -83,12 +113,14 @@ internal class GvaGalleryItemViewHolder(
         } ?: run {
             binding.subtitle.isVisible = false
         }
+    }
 
+    private fun bindButtons(card: GvaGalleryCard) {
         adapter.setOptions(card.options)
         binding.buttonsRecyclerView.isVisible = card.options.isEmpty().not()
     }
 
-    fun bindImage(card: GvaGalleryCard) {
+    private fun bindImage(card: GvaGalleryCard) {
         card.imageUrl?.let {
             binding.image.load(it)
             binding.image.isVisible = true
@@ -96,4 +128,17 @@ internal class GvaGalleryItemViewHolder(
             binding.image.isVisible = false
         }
     }
+
+    private fun updateContendDescription(card: GvaGalleryCard, position: Int, size: Int) {
+        val cardContentDescription = listOf(card.title, card.subtitle)
+            .filter { it?.isNotEmpty() ?: false }
+            .joinToString(separator = ". ")
+
+        itemView.contentDescription = itemView.resources.getString(
+            R.string.gva_gallery_card_message_content_description,
+            cardContentDescription,
+            position + 1,
+            size
+        )
+    }
 }
diff --git a/widgetssdk/src/main/res/layout/chat_gva_gallery_item.xml b/widgetssdk/src/main/res/layout/chat_gva_gallery_item.xml
index 8ca0d9ec1..5ed6a14c4 100644
--- a/widgetssdk/src/main/res/layout/chat_gva_gallery_item.xml
+++ b/widgetssdk/src/main/res/layout/chat_gva_gallery_item.xml
@@ -38,7 +38,8 @@
             android:textAppearance="?attr/textAppearanceHeadline3"
             android:textColor="?attr/gliaBaseDarkColor"
             android:textIsSelectable="true"
-            android:textColorLink="?attr/gliaBaseDarkColor"/>
+            android:textColorLink="?attr/gliaBaseDarkColor"
+            android:importantForAccessibility="no" />
 
         
+            android:textColorLink="?attr/gliaBaseDarkColor"
+            android:importantForAccessibility="no" />
 
         
     This action is not currently supported on mobile.
+    %1$s \n Card: %2$d of %3$d.
 
 

From 65c7087b5d4958bbf98531eb7f84ab14ba1e70e9 Mon Sep 17 00:00:00 2001
From: Andriy Shevtsov 
Date: Wed, 9 Aug 2023 21:49:21 +0300
Subject: [PATCH 30/69] Enable chat input after engagement transfer (operator
 connected)

MOB-2493
---
 .../src/main/java/com/glia/widgets/chat/model/ChatState.kt     | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt
index e860dd0ce..9db387e3f 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt
@@ -88,7 +88,8 @@ internal data class ChatState(
         operatorProfileImgUrl: String?
     ): ChatState = copy(
         formattedOperatorName = formattedOperatorName,
-        operatorProfileImgUrl = operatorProfileImgUrl
+        operatorProfileImgUrl = operatorProfileImgUrl,
+        chatInputMode = ChatInputMode.ENABLED,
     )
 
     fun historyLoaded(chatItems: List): ChatState = copy(

From 66409dd5716f1a5372816d38589a39293c0e677f Mon Sep 17 00:00:00 2001
From: Andriy Shevtsov 
Date: Thu, 10 Aug 2023 10:36:27 +0300
Subject: [PATCH 31/69] Open dialogs on top of CallVisualizerSupportActivity
 instead of integrator's Activity

This prevents the `java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.MaterialComponents (or a descendant)` crash even if integrator's activity style is not Material

MOB-2515

(cherry picked from commit 8c6eedb2f9cf1d3ca72d28cb901cbbfbc15e1d45)
---
 .../ActivityWatcherForCallVisualizer.kt       | 13 ++++-
 ...ctivityWatcherForCallVisualizerContract.kt |  3 +
 ...ivityWatcherForCallVisualizerController.kt | 24 +++++++-
 .../CallVisualizerSupportActivity.kt          |  1 +
 ...yWatcherForCallVisualizerControllerTest.kt | 55 +++++++++++++++++++
 5 files changed, 92 insertions(+), 4 deletions(-)

diff --git a/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizer.kt b/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizer.kt
index a4b4d3ecf..359d3ffba 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizer.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizer.kt
@@ -73,7 +73,6 @@ internal class ActivityWatcherForCallVisualizer(
             registerForCameraPermissionResult(activity)
             registerForOverlayPermissionResult(activity)
         }
-        super.onActivityCreated(activity, savedInstanceState)
     }
 
     override fun onActivityResumed(activity: Activity) {
@@ -171,6 +170,7 @@ internal class ActivityWatcherForCallVisualizer(
                 // Visitor rejected system permission required for screen sharing
                 controller.onMediaProjectionRejected()
             }
+            controller.setIsWaitingMediaProjectionResult(false)
             destroySupportActivityIfExists(componentActivity)
         }
         activity::class.simpleName?.let {
@@ -208,6 +208,7 @@ internal class ActivityWatcherForCallVisualizer(
         Logger.d(TAG, "Dismiss alert dialog")
         alertDialog?.dismiss()
         alertDialog = null
+        destroySupportActivityIfExists()
     }
 
     override fun removeDialogFromStack() {
@@ -374,6 +375,11 @@ internal class ActivityWatcherForCallVisualizer(
         return themeFromGlobalSetting.getFullHybridTheme(themeFromIntent)
     }
 
+    override fun isSupportActivityOpen(): Boolean {
+        val activity = resumedActivity.get()
+        return (activity == null || activity is CallVisualizerSupportActivity)
+    }
+
     override fun openSupportActivity(permissionType: PermissionType) {
         resumedActivity.get()?.run {
             val intent = Intent(this, CallVisualizerSupportActivity::class.java)
@@ -383,12 +389,15 @@ internal class ActivityWatcherForCallVisualizer(
     }
 
     override fun destroySupportActivityIfExists() {
-        resumedActivity.get()?.let { destroySupportActivityIfExists(it) }
+        resumedActivity.get()?.let {
+            if (!it.isFinishing && !controller.isWaitingMediaProjectionResult()) destroySupportActivityIfExists(it)
+        }
     }
 
     private fun destroySupportActivityIfExists(activity: Activity) {
         if (activity is CallVisualizerSupportActivity) {
             activity.finish()
+            activity.overridePendingTransition(0, 0)
         }
     }
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerContract.kt b/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerContract.kt
index bab69d229..e8a5891f6 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerContract.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerContract.kt
@@ -24,6 +24,8 @@ class ActivityWatcherForCallVisualizerContract {
         fun onRequestedCameraPermissionResult(isGranted: Boolean)
         fun onMediaProjectionPermissionResult(isGranted: Boolean, activity: Activity)
         fun onMediaProjectionRejected()
+        fun isWaitingMediaProjectionResult(): Boolean
+        fun setIsWaitingMediaProjectionResult(isWaiting: Boolean)
     }
 
     interface Watcher {
@@ -48,5 +50,6 @@ class ActivityWatcherForCallVisualizerContract {
         fun checkInitialCameraPermission()
         fun callOverlayDialog()
         fun dismissOverlayDialog()
+        fun isSupportActivityOpen(): Boolean
     }
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerController.kt b/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerController.kt
index 0caf0f95f..e2fd3dd9f 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerController.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerController.kt
@@ -37,6 +37,7 @@ internal class ActivityWatcherForCallVisualizerController(
 
     @Mode
     private var currentDialogMode: Int = MODE_NONE
+    private var shouldWaitMediaProjectionResult: Boolean = false
 
     private lateinit var watcher: ActivityWatcherForCallVisualizerContract.Watcher
 
@@ -102,6 +103,14 @@ internal class ActivityWatcherForCallVisualizerController(
         if (state.mode != MODE_NONE) {
             currentDialogMode = state.mode
         }
+        if (state.mode != MODE_NONE && !watcher.isSupportActivityOpen()) {
+            // This function is executed twice
+            // First call opens CallVisualizerSupportActivity and exits the function.
+            // After that this function is called again when CallVisualizerSupportActivity is started.
+            // Second call shows dialog on top of CallVisualizerSupportActivity
+            watcher.openSupportActivity(None)
+            return
+        }
         when (state.mode) {
             MODE_NONE -> watcher.dismissAlertDialog()
             MODE_MEDIA_UPGRADE -> watcher.showUpgradeDialog(state as MediaUpgrade)
@@ -118,11 +127,14 @@ internal class ActivityWatcherForCallVisualizerController(
 
     override fun onPositiveDialogButtonClicked(activity: Activity?) {
         Logger.d(TAG, "onPositiveButtonDialogButtonClicked() - $currentDialogMode")
-        watcher.removeDialogFromStack()
         when (currentDialogMode) {
             MODE_NONE -> Logger.e(TAG, "$currentDialogMode should not have a dialog to click")
             MODE_ENABLE_SCREEN_SHARING_NOTIFICATIONS_AND_START_SHARING -> watcher.openNotificationChannelScreen()
-            MODE_START_SCREEN_SHARING -> activity?.run { startScreenSharing(this) }
+            MODE_START_SCREEN_SHARING ->
+                activity?.run {
+                    setIsWaitingMediaProjectionResult(true)
+                    startScreenSharing(this)
+                }
             MODE_MEDIA_UPGRADE -> watcher.openCallActivity()
             MODE_ENABLE_NOTIFICATION_CHANNEL -> watcher.openNotificationChannelScreen()
             MODE_OVERLAY_PERMISSION -> {
@@ -131,7 +143,9 @@ internal class ActivityWatcherForCallVisualizerController(
             }
             else -> Logger.d(TAG, "Not relevant")
         }
+        watcher.removeDialogFromStack()
         currentDialogMode = MODE_NONE
+        watcher.destroySupportActivityIfExists()
     }
 
     override fun onNegativeDialogButtonClicked() {
@@ -274,4 +288,10 @@ internal class ActivityWatcherForCallVisualizerController(
             mediaUpgradeOffer?.accept { error -> onMediaUpgradeAccept(error) }
         }
     }
+
+    override fun isWaitingMediaProjectionResult() = shouldWaitMediaProjectionResult
+
+    override fun setIsWaitingMediaProjectionResult(isWaiting: Boolean) {
+        shouldWaitMediaProjectionResult = isWaiting
+    }
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/CallVisualizerSupportActivity.kt b/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/CallVisualizerSupportActivity.kt
index 36eebbe89..0b3ef1975 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/CallVisualizerSupportActivity.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/CallVisualizerSupportActivity.kt
@@ -15,3 +15,4 @@ class CallVisualizerSupportActivity : AppCompatActivity() {
 sealed class PermissionType : Parcelable
 object ScreenSharing : PermissionType()
 object Camera : PermissionType()
+object None : PermissionType()
diff --git a/widgetssdk/src/test/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerControllerTest.kt
index 6785b37c9..49d4a72f7 100644
--- a/widgetssdk/src/test/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerControllerTest.kt
+++ b/widgetssdk/src/test/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerControllerTest.kt
@@ -138,6 +138,7 @@ internal class ActivityWatcherForCallVisualizerControllerTest {
         verify(watcher).removeDialogFromStack()
         verify(watcher).dismissOverlayDialog()
         verify(watcher).openOverlayPermissionView()
+        verify(watcher).destroySupportActivityIfExists()
     }
 
     @Test
@@ -146,6 +147,7 @@ internal class ActivityWatcherForCallVisualizerControllerTest {
         resetMocks()
         controller.onPositiveDialogButtonClicked()
         verify(watcher).removeDialogFromStack()
+        verify(watcher).destroySupportActivityIfExists()
     }
 
     @Test
@@ -158,6 +160,7 @@ internal class ActivityWatcherForCallVisualizerControllerTest {
 
     @Test
     fun `onPositiveDialogButtonClicked notification channel dialog is shown when MODE_ENABLE_SCREEN_SHARING_NOTIFICATIONS_AND_START_SHARING`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(true)
         controller.onDialogControllerCallback(
             DialogState(
                 MODE_ENABLE_SCREEN_SHARING_NOTIFICATIONS_AND_START_SHARING
@@ -167,10 +170,13 @@ internal class ActivityWatcherForCallVisualizerControllerTest {
         controller.onPositiveDialogButtonClicked()
         verify(watcher).removeDialogFromStack()
         verify(watcher).openNotificationChannelScreen()
+        verify(watcher).isSupportActivityOpen()
+        verify(watcher).destroySupportActivityIfExists()
     }
 
     @Test
     fun `onNegativeDialogButtonClicked decline is sent and dialog is dismissed when MODE_ENABLE_SCREEN_SHARING_NOTIFICATIONS_AND_START_SHARING`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(true)
         controller.onDialogControllerCallback(
             DialogState(
                 MODE_ENABLE_SCREEN_SHARING_NOTIFICATIONS_AND_START_SHARING
@@ -178,92 +184,141 @@ internal class ActivityWatcherForCallVisualizerControllerTest {
         )
         verify(watcher).showAllowScreenSharingNotificationsAndStartSharingDialog()
         controller.onNegativeDialogButtonClicked()
+        verify(watcher).isSupportActivityOpen()
         verify(watcher).removeDialogFromStack()
         verify(screenSharingController).onScreenSharingDeclined()
     }
 
     @Test
     fun `onPositiveDialogButtonClicked call activity is called when MODE_MEDIA_UPGRADE`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(true)
         prepareMediaUpgradeApplicationState()
         controller.onPositiveDialogButtonClicked()
         verify(watcher).removeDialogFromStack()
         verify(watcher).openCallActivity()
+        verify(watcher).isSupportActivityOpen()
+        verify(watcher).destroySupportActivityIfExists()
     }
 
     @Test
     fun `onNegativeDialogButtonClicked dialog is dismissed when MODE_MEDIA_UPGRADE`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(true)
         prepareMediaUpgradeApplicationState()
         controller.onNegativeDialogButtonClicked()
         verify(watcher).removeDialogFromStack()
+        verify(watcher).isSupportActivityOpen()
     }
 
     @Test
     fun `onPositiveDialogButtonClicked overlay permissions are called when MODE_OVERLAY_PERMISSIONS`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(true)
         controller.onDialogControllerCallback(DialogState(MODE_OVERLAY_PERMISSION))
         verify(watcher).showOverlayPermissionsDialog()
         controller.onPositiveDialogButtonClicked()
         verify(watcher).dismissOverlayDialog()
         verify(watcher).removeDialogFromStack()
         verify(watcher).openOverlayPermissionView()
+        verify(watcher).isSupportActivityOpen()
+        verify(watcher).destroySupportActivityIfExists()
     }
 
     @Test
     fun `onNegativeDialogButtonClicked dialog is dismissed when MODE_OVERLAY_PERMISSIONS`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(true)
         controller.onDialogControllerCallback(DialogState(MODE_OVERLAY_PERMISSION))
         verify(watcher).showOverlayPermissionsDialog()
         controller.onNegativeDialogButtonClicked()
         verify(watcher).removeDialogFromStack()
         verify(watcher).dismissOverlayDialog()
+        verify(watcher).isSupportActivityOpen()
     }
 
     @Test
     fun `onPositiveDialogButtonClicked notification channel is shown when MODE_ENABLE_NOTIFICAIONTS_CHANNEL`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(true)
         controller.onDialogControllerCallback(DialogState(MODE_ENABLE_NOTIFICATION_CHANNEL))
         verify(watcher).showAllowNotificationsDialog()
         controller.onPositiveDialogButtonClicked()
         verify(watcher).removeDialogFromStack()
         verify(watcher).openNotificationChannelScreen()
+        verify(watcher).isSupportActivityOpen()
+        verify(watcher).destroySupportActivityIfExists()
     }
 
     @Test
     fun `onNegativeDialogButtonClicked dialog is dismissed when MODE_ENABLE_NOTIFICATIONS_CHANNEL`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(true)
         controller.onDialogControllerCallback(DialogState(MODE_ENABLE_NOTIFICATION_CHANNEL))
         verify(watcher).showAllowNotificationsDialog()
         controller.onNegativeDialogButtonClicked()
         verify(watcher).removeDialogFromStack()
+        verify(watcher).isSupportActivityOpen()
+    }
+
+    @Test
+    fun `onPositiveDialogButtonClicked openSupportActivity called when isSupportActivityOpen false and MODE_VISITOR_CODE`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(false)
+        controller.onDialogControllerCallback(DialogState(MODE_VISITOR_CODE))
+        verify(watcher, never()).showVisitorCodeDialog()
+        controller.onPositiveDialogButtonClicked()
+        verify(watcher).removeDialogFromStack()
+        verify(watcher).isSupportActivityOpen()
+        verify(watcher).openSupportActivity(any())
+        verify(watcher).destroySupportActivityIfExists()
     }
 
     @Test
     fun `onPositiveDialogButtonClicked dialog is dismissed when MODE_VISITOR_CODE`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(true)
         controller.onDialogControllerCallback(DialogState(MODE_VISITOR_CODE))
         verify(watcher).showVisitorCodeDialog()
         controller.onPositiveDialogButtonClicked()
         verify(watcher).removeDialogFromStack()
+        verify(watcher).isSupportActivityOpen()
+        verify(watcher).destroySupportActivityIfExists()
     }
 
     @Test
     fun `onNegativeDialogButtonClicked dialog is dismissed when MODE_VISITOR_CODE`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(true)
         controller.onDialogControllerCallback(DialogState(MODE_VISITOR_CODE))
         verify(watcher).showVisitorCodeDialog()
         controller.onNegativeDialogButtonClicked()
         verify(watcher).removeDialogFromStack()
+        verify(watcher).isSupportActivityOpen()
     }
 
+    @Test
+    fun `onPositiveDialogButtonClicked openSupportActivity called when isSupportActivityOpen false and MODE_SCREEN_SHARING`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(false)
+        controller.onDialogControllerCallback(DialogState(MODE_START_SCREEN_SHARING))
+        verify(watcher, never()).showScreenSharingDialog()
+        controller.onPositiveDialogButtonClicked()
+        verify(watcher).removeDialogFromStack()
+        verify(watcher).isSupportActivityOpen()
+        verify(watcher).destroySupportActivityIfExists()
+        verify(watcher).openSupportActivity(any())
+    }
     @Test
     fun `onPositiveDialogButtonClicked dialog is dismissed when MODE_SCREEN_SHARING`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(true)
         controller.onDialogControllerCallback(DialogState(MODE_START_SCREEN_SHARING))
         verify(watcher).showScreenSharingDialog()
         controller.onPositiveDialogButtonClicked()
         verify(watcher).removeDialogFromStack()
+        verify(watcher).isSupportActivityOpen()
+        verify(watcher).destroySupportActivityIfExists()
     }
 
     @Test
     fun `onNegativeDialogButtonClicked dialog is dismissed when MODE_SCREEN_SHARING`() {
+        whenever(watcher.isSupportActivityOpen()).thenReturn(true)
         controller.onDialogControllerCallback(DialogState(MODE_START_SCREEN_SHARING))
         verify(watcher).showScreenSharingDialog()
         controller.onNegativeDialogButtonClicked()
         verify(watcher).removeDialogFromStack()
         verify(screenSharingController).onScreenSharingDeclined()
+        verify(watcher).isSupportActivityOpen()
     }
 
     @Test

From 0ce4ec2e30dddb6297eeaaab4239daad9fa53344 Mon Sep 17 00:00:00 2001
From: Andriy Shevtsov 
Date: Thu, 10 Aug 2023 11:43:35 +0300
Subject: [PATCH 32/69] Increment Core SDK version to 1.0.3 (hotfix)

(cherry picked from commit d3d8149bffc429d16936e050e0a78163e3373953)
---
 version.properties | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/version.properties b/version.properties
index 5d3c92139..c8bdf60a1 100644
--- a/version.properties
+++ b/version.properties
@@ -1,4 +1,4 @@
-#Wed Jul 12 13:00:56 UTC 2023
-dependency.coreSdk.version=1.0.2
-widgets.versionCode=65
+#Thu Aug 10 15:36:20 UTC 2023
+dependency.coreSdk.version=1.0.3
+widgets.versionCode=66
 widgets.versionName=2.0.3

From 6ae0cf3adbecdd27ad66e31aaed7fa770b749259 Mon Sep 17 00:00:00 2001
From: Karl Valliste 
Date: Thu, 10 Aug 2023 19:52:53 +0300
Subject: [PATCH 33/69] =?UTF-8?q?Fixed=20overlay=20dialog=20being=20dismis?=
 =?UTF-8?q?sed=20by=20ENGAGEMENT=5FENDED=20at=20start=20of=20=E2=80=A6=20(?=
 =?UTF-8?q?#703)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Uncommented bug fix with solution until engagement state investigation is done
---
 .../main/java/com/glia/widgets/call/CallController.java   | 7 ++++++-
 .../com/glia/widgets/chat/controller/ChatController.kt    | 8 +++++++-
 2 files changed, 13 insertions(+), 2 deletions(-)

diff --git a/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java b/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java
index 1cc079915..292454ceb 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java
+++ b/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java
@@ -231,7 +231,12 @@ public void onHoldChanged(boolean isOnHold) {
     public void engagementEnded() {
         Logger.d(TAG, "engagementEndedByOperator");
         stop();
-        dialogController.dismissDialogs();
+        // TODO re-enable during MOB-2523
+        /*
+        if (!isOngoingEngagementUseCase.invoke()) {
+            dialogController.dismissDialogs()
+        }
+        */
     }
 
     @Override
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt
index 9c1fa04a9..8d34af31e 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt
@@ -4,6 +4,7 @@ import android.net.Uri
 import android.text.format.DateUtils
 import android.view.View
 import androidx.annotation.VisibleForTesting
+import com.glia.androidsdk.Glia
 import com.glia.androidsdk.GliaException
 import com.glia.androidsdk.Operator
 import com.glia.androidsdk.chat.AttachmentFile
@@ -675,7 +676,12 @@ internal class ChatController(
 
             EngagementStateEvent.Type.ENGAGEMENT_ENDED -> {
                 Logger.d(TAG, "Engagement Ended")
-                dialogController.dismissDialogs()
+                // TODO re-enable during MOB-2523
+                /*
+                if (!isOngoingEngagementUseCase.invoke()) {
+                    dialogController.dismissDialogs()
+                }
+                */
             }
         }
     }

From 208824697d3d8fad2637576813ac2bacec92c8ad Mon Sep 17 00:00:00 2001
From: Andrii Horishnii 
Date: Thu, 10 Aug 2023 19:03:11 +0300
Subject: [PATCH 34/69] Make Quick Reply Button ADA compliant

Move the focus back to the chat list after the quick button perform.

MOB-2400
---
 .../src/main/java/com/glia/widgets/chat/ChatView.kt   | 11 ++++++++++-
 .../src/main/java/com/glia/widgets/chat/GvaChip.kt    |  2 +-
 2 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt
index e9799a93a..34498f311 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt
@@ -17,6 +17,7 @@ import android.text.Editable
 import android.text.TextWatcher
 import android.util.AttributeSet
 import android.view.View
+import android.view.accessibility.AccessibilityEvent
 import android.widget.Toast
 import androidx.annotation.StringRes
 import androidx.appcompat.app.AlertDialog
@@ -829,7 +830,15 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty
         }
         binding.appBarView.setOnXClickedListener { controller?.onXButtonClicked() }
         binding.newMessagesIndicatorCard.setOnClickListener { controller?.newMessagesIndicatorClicked() }
-        binding.gvaQuickRepliesLayout.onItemClickedListener = GvaChipGroup.OnItemClickedListener { controller?.onGvaButtonClicked(it) }
+        binding.gvaQuickRepliesLayout.onItemClickedListener = GvaChipGroup.OnItemClickedListener {
+            controller?.onGvaButtonClicked(it)
+
+            // move the focus back to the chat list
+            binding.chatRecyclerView.adapter?.itemCount?.let { size ->
+                val viewHolder = binding.chatRecyclerView.findViewHolderForAdapterPosition(size - 1)
+                viewHolder?.itemView?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
+            }
+        }
     }
 
     private fun setupAddAttachmentButton() {
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/GvaChip.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/GvaChip.kt
index abde348e3..914cc46cd 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/GvaChip.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/GvaChip.kt
@@ -107,8 +107,8 @@ class GvaChipGroup @JvmOverloads constructor(
             applyUiTheme(uiTheme)
             text = gvaButton.text
             setOnClickListener {
-                this@GvaChipGroup.isVisible = false
                 onItemClickedListener?.onItemClicked(gvaButton)
+                this@GvaChipGroup.isVisible = false
             }
 
             addView(this)

From 877c4b997a2a228500afda8b537b285484de7709 Mon Sep 17 00:00:00 2001
From: Karl Valliste 
Date: Thu, 10 Aug 2023 15:01:53 +0300
Subject: [PATCH 35/69] Increment project version to 2.0.4

---
 version.properties | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/version.properties b/version.properties
index c8bdf60a1..c11d1f4e1 100644
--- a/version.properties
+++ b/version.properties
@@ -1,4 +1,4 @@
-#Thu Aug 10 15:36:20 UTC 2023
+#Fri Aug 11 06:31:54 UTC 2023
 dependency.coreSdk.version=1.0.3
-widgets.versionCode=66
-widgets.versionName=2.0.3
+widgets.versionCode=67
+widgets.versionName=2.0.4

From 133c1d3b6739589151eaee1e89f8d362569ff1e6 Mon Sep 17 00:00:00 2001
From: Davit Dolmazyan 
Date: Tue, 1 Aug 2023 18:59:13 +0300
Subject: [PATCH 36/69] Optimize chat flow

MOB-2479
---
 build.gradle                                  |   2 +-
 .../java/com/glia/widgets/chat/ChatManager.kt | 349 +++++++
 .../java/com/glia/widgets/chat/ChatView.kt    |  23 +-
 .../glia/widgets/chat/adapter/ChatAdapter.kt  |  16 +-
 .../widgets/chat/controller/ChatController.kt | 918 ++----------------
 .../domain/AppendHistoryChatItemUseCases.kt   | 177 ++++
 .../chat/domain/AppendNewChatItemUseCase.kt   | 192 ++++
 .../chat/domain/GliaLoadHistoryUseCase.kt     |  12 +-
 .../chat/domain/GliaOnMessageUseCase.kt       |  12 +-
 .../chat/domain/GliaSendMessageUseCase.kt     |  33 -
 .../domain/HandleCustomCardClickUseCase.kt    |  48 +
 .../domain/IsEnableChatEditTextUseCase.kt     |  13 -
 .../chat/domain/MapChatItemUseCases.kt        |  77 ++
 .../chat/domain/SendUnsentMessagesUseCase.kt  |  14 +
 .../gva/MapGvaGvaGalleryCardsUseCase.kt       |  12 +-
 .../gva/MapGvaGvaQuickRepliesUseCase.kt       |  25 -
 .../gva/MapGvaPersistentButtonsUseCase.kt     |  12 +-
 .../domain/gva/MapGvaQuickRepliesUseCase.kt   |  21 +
 .../domain/gva/MapGvaResponseTextUseCase.kt   |  12 +-
 .../widgets/chat/domain/gva/MapGvaUseCase.kt  |  15 +-
 .../com/glia/widgets/chat/model/ChatItems.kt  |  41 +-
 .../com/glia/widgets/chat/model/ChatState.kt  |  35 +-
 .../glia/widgets/chat/model/GvaChatItems.kt   |  24 +-
 .../GliaEngagementStateRepository.java        |  42 +-
 .../engagement/GliaOperatorRepository.java    |  38 -
 .../core/engagement/GliaOperatorRepository.kt |  40 +
 .../core/engagement/data/LocalOperator.kt     |   3 +
 .../engagement/domain/GetOperatorUseCase.java |  10 +-
 .../domain/GliaOnEngagementUseCase.java       |  23 +-
 .../engagement/domain/MapOperatorUseCase.kt   |  16 +-
 .../domain/model/ChatMessageInternal.kt       |  12 +-
 .../fileupload/FileAttachmentRepository.kt    |   6 +-
 .../SecureFileAttachmentRepository.kt         |   3 +-
 .../domain/IsMessagingAvailableUseCase.kt     |   4 +-
 .../glia/widgets/di/ControllerFactory.java    |  19 +-
 .../com/glia/widgets/di/Dependencies.java     |  55 +-
 .../com/glia/widgets/di/ManagerFactory.kt     |  19 +
 .../com/glia/widgets/di/UseCaseFactory.java   | 160 ++-
 .../data/GliaFileRepositoryImpl.kt            |   3 +-
 .../glia/widgets/helper/CommonExtensions.kt   |  34 +-
 .../viewholder/SingleQuestionViewHolder.kt    |   3 +-
 .../ApplicationChatHeadLayoutController.kt    |   7 +-
 .../glia/widgets/chat/ChatManagerStateTest.kt |  95 ++
 .../com/glia/widgets/chat/ChatManagerTest.kt  | 621 ++++++++++++
 .../widgets/chat/MockChatMessageInternal.kt   |   9 +-
 .../chat/controller/ChatControllerTest.kt     | 213 +---
 .../domain/AppendGvaMessageItemUseCaseTest.kt |  42 +
 .../AppendHistoryChatMessageUseCaseTest.kt    | 144 +++
 .../AppendHistoryCustomCardItemUseCaseTest.kt |  68 ++
 ...ppendHistoryOperatorChatItemUseCaseTest.kt |  73 ++
 ...istoryResponseCardOrTextItemUseCaseTest.kt | 174 ++++
 ...AppendHistoryVisitorChatItemUseCaseTest.kt | 106 ++
 .../domain/AppendNewChatMessageUseCaseTest.kt |  85 ++
 .../AppendNewOperatorMessageUseCaseTest.kt    | 130 +++
 ...endNewResponseCardOrTextItemUseCaseTest.kt | 111 +++
 .../AppendNewVisitorMessageUseCaseTest.kt     | 163 ++++
 .../AppendSystemMessageItemUseCaseTest.kt     |  55 ++
 .../MapOperatorAttachmentUseCaseTest.kt       |  59 ++
 .../domain/MapOperatorPlainTextUseCaseTest.kt |  60 ++
 .../chat/domain/MapResponseCardUseCaseTest.kt |  75 ++
 .../domain/MapVisitorAttachmentUseCaseTest.kt |  50 +
 .../gva/MapGvaGvaGalleryCardsUseCaseTest.kt   |  28 +-
 .../gva/MapGvaGvaQuickRepliesUseCaseTest.kt   |  87 --
 .../gva/MapGvaPersistentButtonsUseCaseTest.kt |  27 +-
 .../gva/MapGvaQuickRepliesUseCaseTest.kt      |  61 ++
 .../gva/MapGvaResponseTextUseCaseTest.kt      |  31 +-
 .../chat/domain/gva/MapGvaUseCaseTest.kt      |  25 +-
 .../GliaOperatorRepositoryTest.java           |  72 --
 .../engagement/GliaOperatorRepositoryTest.kt  | 124 +++
 .../domain/GetOperatorUseCaseTest.java        |  13 +-
 .../domain/MapOperatorUseCaseTest.kt          |   9 +-
 .../glia/widgets/core/model/TestOperator.java |  27 -
 72 files changed, 3743 insertions(+), 1674 deletions(-)
 create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt
 create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendHistoryChatItemUseCases.kt
 create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt
 create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/HandleCustomCardClickUseCase.kt
 delete mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsEnableChatEditTextUseCase.kt
 create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/MapChatItemUseCases.kt
 create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt
 delete mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCase.kt
 create mode 100644 widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaQuickRepliesUseCase.kt
 delete mode 100644 widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.java
 create mode 100644 widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.kt
 create mode 100644 widgetssdk/src/main/java/com/glia/widgets/core/engagement/data/LocalOperator.kt
 create mode 100644 widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerStateTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendGvaMessageItemUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryChatMessageUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryCustomCardItemUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryOperatorChatItemUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryResponseCardOrTextItemUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryVisitorChatItemUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewChatMessageUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewOperatorMessageUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewResponseCardOrTextItemUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewVisitorMessageUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendSystemMessageItemUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapOperatorAttachmentUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapOperatorPlainTextUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapResponseCardUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapVisitorAttachmentUseCaseTest.kt
 delete mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCaseTest.kt
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaQuickRepliesUseCaseTest.kt
 delete mode 100644 widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaOperatorRepositoryTest.java
 create mode 100644 widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaOperatorRepositoryTest.kt
 delete mode 100644 widgetssdk/src/test/java/com/glia/widgets/core/model/TestOperator.java

diff --git a/build.gradle b/build.gradle
index 892d08092..eaf7febfe 100644
--- a/build.gradle
+++ b/build.gradle
@@ -76,7 +76,7 @@ ext {
   mockitoAndroidTestVersion = '4.3.1'
   archCoreVersion = '2.2.0'
   testRulesVersion = '1.5.0'
-  robolectricVersion = '4.9'
+  robolectricVersion = '4.10.3'
 
   //kotlin
   coreKtxVersion = '1.8.0'
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt
new file mode 100644
index 000000000..f40cc74c5
--- /dev/null
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt
@@ -0,0 +1,349 @@
+package com.glia.widgets.chat
+
+import android.text.format.DateUtils
+import androidx.annotation.VisibleForTesting
+import com.glia.androidsdk.chat.SingleChoiceAttachment
+import com.glia.androidsdk.chat.VisitorMessage
+import com.glia.widgets.chat.domain.AddNewMessagesDividerUseCase
+import com.glia.widgets.chat.domain.AppendHistoryChatMessageUseCase
+import com.glia.widgets.chat.domain.AppendNewChatMessageUseCase
+import com.glia.widgets.chat.domain.GliaLoadHistoryUseCase
+import com.glia.widgets.chat.domain.GliaOnMessageUseCase
+import com.glia.widgets.chat.domain.HandleCustomCardClickUseCase
+import com.glia.widgets.chat.domain.SendUnsentMessagesUseCase
+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.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.VisitorMessageItem
+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
+import io.reactivex.BackpressureStrategy
+import io.reactivex.Flowable
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.disposables.Disposable
+import io.reactivex.processors.BehaviorProcessor
+import io.reactivex.processors.PublishProcessor
+import io.reactivex.schedulers.Schedulers
+
+internal class ChatManager constructor(
+    private val onMessageUseCase: GliaOnMessageUseCase,
+    private val loadHistoryUseCase: GliaLoadHistoryUseCase,
+    private val addNewMessagesDividerUseCase: AddNewMessagesDividerUseCase,
+    private val markMessagesReadWithDelayUseCase: MarkMessagesReadWithDelayUseCase,
+    private val appendHistoryChatMessageUseCase: AppendHistoryChatMessageUseCase,
+    private val appendNewChatMessageUseCase: AppendNewChatMessageUseCase,
+    private val sendUnsentMessagesUseCase: SendUnsentMessagesUseCase,
+    private val handleCustomCardClickUseCase: HandleCustomCardClickUseCase,
+    private val compositeDisposable: CompositeDisposable = CompositeDisposable(),
+    private val state: BehaviorProcessor = BehaviorProcessor.create(),
+    private val quickReplies: BehaviorProcessor> = BehaviorProcessor.create(),
+    private val action: PublishProcessor = PublishProcessor.create()
+) {
+    fun initialize(
+        onHistoryLoaded: (hasHistory: Boolean) -> Unit,
+        onQuickReplyReceived: (List) -> Unit,
+        onOperatorMessageReceived: (count: Int) -> Unit
+    ): Flowable> {
+
+        subscribe(onHistoryLoaded, onOperatorMessageReceived, onQuickReplyReceived)
+
+        return state.map(State::immutableChatItems).onBackpressureLatest().share()
+    }
+
+    @VisibleForTesting
+    fun subscribe(
+        onHistoryLoaded: (hasHistory: Boolean) -> Unit,
+        onOperatorMessageReceived: (count: Int) -> Unit,
+        onQuickReplyReceived: (List) -> Unit
+    ) {
+        subscribeToState(onHistoryLoaded, onOperatorMessageReceived).also(compositeDisposable::add)
+        subscribeToQuickReplies(onQuickReplyReceived).also(compositeDisposable::add)
+    }
+
+    fun reset() {
+        state.onNext(State())
+        quickReplies.onNext(emptyList())
+        compositeDisposable.clear()
+    }
+
+    fun onChatAction(action: Action) {
+        this.action.onNext(action)
+    }
+
+    @VisibleForTesting
+    fun subscribeToState(onHistoryLoaded: (hasHistory: Boolean) -> Unit, onOperatorMessageReceived: (count: Int) -> Unit): Disposable = state.run {
+        loadHistory(onHistoryLoaded)
+            .concatWith(subscribeToMessages(onOperatorMessageReceived))
+            .doOnNext(::updateQuickReplies)
+            .doOnError { it.printStackTrace() }
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribeOn(Schedulers.computation())
+            .subscribe(::onNext, ::onError)
+    }
+
+    @VisibleForTesting
+    fun loadHistory(onHistoryLoaded: (hasHistory: Boolean) -> Unit): Flowable = loadHistoryUseCase()
+        .map { mapChatHistory(it) }
+        .doOnSuccess { onHistoryLoaded(it.chatItems.isNotEmpty()) }
+        .toFlowable()
+
+    @VisibleForTesting
+    fun subscribeToMessages(onOperatorMessageReceived: (count: Int) -> Unit): Flowable = Flowable.merge(onMessage(), onAction())
+        .doOnNext { onOperatorMessageReceived(it.addedMessagesCount) }
+
+    @VisibleForTesting
+    fun updateQuickReplies(state: State) {
+        val quickReplyItems = (state.chatItems.lastOrNull() as? GvaQuickReplies)?.run { options } ?: emptyList()
+
+        quickReplies.onNext(quickReplyItems)
+    }
+
+    @VisibleForTesting
+    fun subscribeToQuickReplies(onQuickReplyReceived: (List) -> Unit): Disposable = quickReplies
+        .observeOn(AndroidSchedulers.mainThread())
+        .subscribe { onQuickReplyReceived(it) }
+
+    @VisibleForTesting
+    fun onMessage(): Flowable = onMessageUseCase().toFlowable(BackpressureStrategy.BUFFER).withLatestFrom(state, ::mapNewMessage)
+
+    @VisibleForTesting
+    fun onAction(): Flowable = action.withLatestFrom(state, ::mapAction)
+
+    @VisibleForTesting
+    fun checkUnsentMessages(state: State) {
+        sendUnsentMessagesUseCase(state.unsentItems.firstOrNull()?.message ?: return) {}
+    }
+
+    @VisibleForTesting
+    fun mapChatHistory(historyResponse: ChatHistoryResponse): State {
+        if (historyResponse.items.isEmpty()) return State()
+
+        val chatItems: MutableList = mutableListOf()
+        val chatItemIds: MutableSet = mutableSetOf()
+
+        val rawItems = historyResponse.items
+
+
+        for (index in rawItems.indices.reversed()) {
+
+            val rawMessage = rawItems[index]
+            chatItemIds.add(rawMessage.chatMessage.id)
+
+            appendHistoryChatMessageUseCase(chatItems, rawMessage, index == rawItems.lastIndex)
+        }
+
+        chatItems.reverse()
+
+        if (addNewMessagesDividerUseCase(chatItems, historyResponse.newMessagesCount)) {
+            markMessagesReadWithDelay()
+        }
+
+        val lastMessageWithVisibleOperatorImage = chatItems.lastOrNull() as? OperatorChatItem
+
+        return State(
+            chatItems = chatItems,
+            chatItemIds = chatItemIds,
+            lastMessageWithVisibleOperatorImage = lastMessageWithVisibleOperatorImage
+        )
+    }
+
+    @VisibleForTesting
+    fun mapNewMessage(chatMessage: ChatMessageInternal, messagesState: State): State {
+        if (messagesState.isNew(chatMessage)) {
+            appendNewChatMessageUseCase(messagesState, chatMessage)
+            if (chatMessage.chatMessage is VisitorMessage) {
+                checkUnsentMessages(messagesState)
+            }
+        }
+
+        return messagesState
+    }
+
+    @VisibleForTesting
+    fun mapAction(action: Action, state: State): State {
+        return when (action) {
+            is Action.QueuingStarted -> mapInQueue(action.companyName, state)
+            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 -> state
+        }
+    }
+
+
+    @VisibleForTesting
+    fun mapCustomCardClicked(action: Action.CustomCardClicked, state: State): State = action.run {
+        handleCustomCardClickUseCase(customCard, attachment, state)
+    }
+
+    @VisibleForTesting
+    fun mapMediaUpgradeTimerUpdated(formattedValue: String, state: State): State = state.apply {
+        val oldItem = state.mediaUpgradeTimerItem ?: return@apply
+
+        if (oldItem.time == formattedValue) return@apply
+
+        val newItem = oldItem.updateTime(formattedValue)
+
+        mediaUpgradeTimerItem = newItem
+
+        val index = chatItems.indexOf(oldItem)
+
+        if (index == -1) {
+            chatItems += newItem
+        } else {
+            chatItems[index] = newItem
+        }
+
+    }
+
+    @VisibleForTesting
+    fun mapMediaUpgradeCanceled(state: State): State = state.apply {
+        val oldItem = mediaUpgradeTimerItem
+        mediaUpgradeTimerItem = null
+        chatItems -= oldItem ?: return@apply
+    }
+
+    @VisibleForTesting
+    fun mapUpgradeMediaToVideo(state: State): State = state.apply {
+        val oldItem = this.mediaUpgradeTimerItem
+        val newItem = MediaUpgradeStartedTimerItem.Video(oldItem?.time ?: DateUtils.formatElapsedTime(0))
+        mediaUpgradeTimerItem = newItem
+        chatItems += newItem
+        chatItems -= oldItem ?: return@apply
+    }
+
+    @VisibleForTesting
+    fun mapMediaUpgrade(video: Boolean, state: State): State = state.apply {
+        val mediaUpgradeTimerItem = if (video) MediaUpgradeStartedTimerItem.Video() else MediaUpgradeStartedTimerItem.Audio()
+        this.mediaUpgradeTimerItem = mediaUpgradeTimerItem
+        chatItems += mediaUpgradeTimerItem
+    }
+
+    @VisibleForTesting
+    fun mapOperatorJoined(action: Action.OperatorJoined, state: State): State = state.apply {
+        chatItems += action.run {
+            OperatorStatusItem.Joined(companyName, operatorFormattedName, operatorImageUrl)
+        }
+    }
+
+    @VisibleForTesting
+    fun mapResponseCardClicked(responseCard: OperatorMessageItem.ResponseCard, state: State): State = state.apply {
+        val index = chatItems.indexOf(responseCard)
+        chatItems[index] = responseCard.asPlainText()
+    }
+
+    @VisibleForTesting
+    fun addUnsentMessage(message: VisitorMessageItem.Unsent, state: State): State {
+        state.unsentItems += message
+        return state.apply {
+            val index = if (chatItems.lastOrNull() is OperatorStatusItem.InQueue) chatItems.lastIndex else chatItems.lastIndex + 1
+            chatItems.add(index, message)
+        }
+    }
+
+    @VisibleForTesting
+    fun mapOperatorConnected(action: Action.OperatorConnected, state: State): State {
+        val operatorStatusItem = action.run { OperatorStatusItem.Connected(companyName, operatorFormattedName, operatorImageUrl) }
+        val oldOperatorStatusItem: OperatorStatusItem? = state.operatorStatusItem
+        state.operatorStatusItem = operatorStatusItem
+
+        checkUnsentMessages(state)
+
+        if (oldOperatorStatusItem != null) {
+            val index = state.chatItems.indexOf(oldOperatorStatusItem)
+
+            if (index != -1) {
+                state.chatItems[index] = operatorStatusItem
+                return state
+            }
+        }
+
+        state.chatItems += operatorStatusItem
+
+        return state
+    }
+
+    @VisibleForTesting
+    fun mapTransferring(state: State): State = state.apply {
+        operatorStatusItem?.also { chatItems -= it }
+        operatorStatusItem = OperatorStatusItem.Transferring.also {
+            chatItems += it
+        }
+    }
+
+    @VisibleForTesting
+    fun mapInQueue(companyName: String, state: State): State = state.apply {
+        OperatorStatusItem.InQueue(companyName).also {
+            operatorStatusItem = it
+            chatItems += it
+        }
+    }
+
+    @VisibleForTesting
+    fun markMessagesReadWithDelay() {
+        val disposable = markMessagesReadWithDelayUseCase()
+            .toSingleDefault(Unit)
+            .toFlowable()
+            .withLatestFrom(state) { _, messagesState: State -> removeNewMessagesDivider(messagesState) }
+            .subscribe(state::onNext) { it.printStackTrace() }
+        compositeDisposable.add(disposable)
+    }
+
+    @VisibleForTesting
+    fun removeNewMessagesDivider(messagesState: State) = messagesState.apply {
+        chatItems.remove(NewMessagesDividerItem)
+    }
+
+    internal data class State(
+        val chatItems: MutableList = mutableListOf(),
+        val chatItemIds: MutableSet = mutableSetOf(),
+        val unsentItems: MutableList = mutableListOf(),
+        var lastMessageWithVisibleOperatorImage: OperatorChatItem? = null,
+        var operatorStatusItem: OperatorStatusItem? = null,
+        var mediaUpgradeTimerItem: MediaUpgradeStartedTimerItem? = null,
+        var addedMessagesCount: Int = 0
+    ) {
+        val immutableChatItems: List get() = chatItems.toList()
+
+        fun isNew(chatMessageInternal: ChatMessageInternal): Boolean = chatItemIds.add(chatMessageInternal.chatMessage.id)
+
+        fun isOperatorChanged(operatorChatItem: OperatorChatItem): Boolean = lastMessageWithVisibleOperatorImage.let {
+            lastMessageWithVisibleOperatorImage = operatorChatItem
+            it?.operatorId != operatorChatItem.operatorId
+        }
+
+        fun resetOperator() {
+            lastMessageWithVisibleOperatorImage = null
+        }
+    }
+
+    internal sealed interface Action {
+        data class QueuingStarted(val companyName: String) : Action
+        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: VisitorMessageItem.Unsent) : Action
+        data class ResponseCardClicked(val responseCard: OperatorMessageItem.ResponseCard) : Action
+        data class OnMediaUpgradeStarted(val isVideo: Boolean) : Action
+        data class OnMediaUpgradeTimerUpdated(val formattedValue: String) : Action
+        object OnMediaUpgradeToVideo : Action
+        object OnMediaUpgradeCanceled : Action
+        data class CustomCardClicked(val customCard: CustomCardChatItem, val attachment: SingleChoiceAttachment) : Action
+        object ChatRestored : Action
+    }
+}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt
index 34498f311..80bd51efe 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt
@@ -47,12 +47,12 @@ import com.glia.widgets.chat.adapter.ChatAdapter.OnFileItemClickListener
 import com.glia.widgets.chat.adapter.ChatAdapter.OnImageItemClickListener
 import com.glia.widgets.chat.adapter.ChatItemHeightManager
 import com.glia.widgets.chat.adapter.UploadAttachmentAdapter
-import com.glia.widgets.chat.adapter.holder.WebViewViewHolder
 import com.glia.widgets.chat.controller.ChatController
 import com.glia.widgets.chat.model.AttachmentItem
 import com.glia.widgets.chat.model.ChatInputMode
 import com.glia.widgets.chat.model.ChatItem
 import com.glia.widgets.chat.model.ChatState
+import com.glia.widgets.chat.model.CustomCardChatItem
 import com.glia.widgets.core.configuration.GliaSdkConfiguration
 import com.glia.widgets.core.dialog.Dialog
 import com.glia.widgets.core.dialog.DialogController
@@ -173,17 +173,18 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty
             }
         }
     private val onCustomCardResponse =
-        OnCustomCardResponse { messageId: String, text: String, value: String ->
-            controller?.sendCustomCardResponse(messageId, text, value)
+        OnCustomCardResponse { customCard: CustomCardChatItem, text: String, value: String ->
+            controller?.sendCustomCardResponse(customCard, text, value)
         }
     private val dataObserver: AdapterDataObserver = object : AdapterDataObserver() {
         override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
             super.onItemRangeInserted(positionStart, itemCount)
+
             val totalItemCount = adapter.itemCount
             val lastIndex = totalItemCount - 1
-            if (isInBottom) {
-                val holder = binding.chatRecyclerView.findViewHolderForAdapterPosition(lastIndex)
-                if (holder is WebViewViewHolder) {
+            if (isInBottom && lastIndex != -1) {
+                val itemViewType = adapter.getItemViewType(lastIndex)
+                if (itemViewType == ChatAdapter.CUSTOM_CARD_TYPE) {
                     // WebView needs time for calculating the height.
                     // So to scroll to the bottom, we need to do it with delay.
                     postDelayed(
@@ -631,12 +632,12 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty
                 },
                 negativeButtonClickListener = {
                     dismissAlertDialog()
-                    controller?.notificationsDialogDismissed()
+                    controller?.notificationDialogDismissed()
                     screenSharingController?.onScreenSharingDeclined()
                 },
                 cancelListener = {
                     it.dismiss()
-                    controller?.notificationsDialogDismissed()
+                    controller?.notificationDialogDismissed()
                     screenSharingController?.onScreenSharingDeclined()
                 }
             )
@@ -654,16 +655,16 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty
                 negativeButtonText = resources.getString(R.string.glia_dialog_allow_notifications_no),
                 positiveButtonClickListener = {
                     dismissAlertDialog()
-                    controller?.notificationsDialogDismissed()
+                    controller?.notificationDialogDismissed()
                     this.context.openNotificationChannelScreen()
                 },
                 negativeButtonClickListener = {
                     dismissAlertDialog()
-                    controller?.notificationsDialogDismissed()
+                    controller?.notificationDialogDismissed()
                 },
                 cancelListener = {
                     it.dismiss()
-                    controller?.notificationsDialogDismissed()
+                    controller?.notificationDialogDismissed()
                 }
             )
         }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt
index 0fb7e1780..0894b97c0 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt
@@ -28,6 +28,7 @@ import com.glia.widgets.chat.model.CustomCardChatItem
 import com.glia.widgets.chat.model.GvaButton
 import com.glia.widgets.chat.model.GvaGalleryCards
 import com.glia.widgets.chat.model.GvaPersistentButtons
+import com.glia.widgets.chat.model.GvaQuickReplies
 import com.glia.widgets.chat.model.GvaResponseText
 import com.glia.widgets.chat.model.MediaUpgradeStartedTimerItem
 import com.glia.widgets.chat.model.OperatorAttachmentItem
@@ -156,7 +157,7 @@ internal class ChatAdapter(
                 uiTheme
             )
 
-            GVA_RESPONSE_TEXT_TYPE -> {
+            GVA_RESPONSE_TEXT_TYPE, GVA_QUICK_REPLIES_TYPE -> {
                 val operatorMessageBinding = ChatOperatorMessageLayoutBinding.inflate(inflater, parent, false)
                 GvaResponseTextViewHolder(
                     operatorMessageBinding,
@@ -250,11 +251,12 @@ internal class ChatAdapter(
 
             is SystemChatItem -> (holder as SystemMessageViewHolder).bind(chatItem.message)
             is GvaResponseText -> (holder as GvaResponseTextViewHolder).bind(chatItem)
+            is GvaQuickReplies -> (holder as GvaResponseTextViewHolder).bind(chatItem.asResponseText())
             is GvaPersistentButtons -> (holder as GvaPersistentButtonsViewHolder).bind(chatItem)
             is GvaGalleryCards -> (holder as GvaGalleryViewHolder).bind(chatItem, chatItemHeightManager.getMeasuredHeight(chatItem))
             is CustomCardChatItem -> {
                 (holder as CustomCardViewHolder).bind(chatItem.message) { text: String, value: String ->
-                    onCustomCardResponse.onCustomCardResponse(chatItem.id, text, value)
+                    onCustomCardResponse.onCustomCardResponse(chatItem, text, value)
                 }
             }
         }
@@ -291,7 +293,7 @@ internal class ChatAdapter(
     }
 
     fun interface OnCustomCardResponse {
-        fun onCustomCardResponse(messageId: String, text: String, value: String)
+        fun onCustomCardResponse(customCard: CustomCardChatItem, text: String, value: String)
     }
 
     fun interface OnGvaButtonsClickListener {
@@ -312,11 +314,12 @@ internal class ChatAdapter(
 
         //GVA Types
         const val GVA_RESPONSE_TEXT_TYPE = 10
-        const val GVA_PERSISTENT_BUTTONS_TYPE = 11
-        const val GVA_GALLERY_CARDS_TYPE = 12
+        const val GVA_QUICK_REPLIES_TYPE = 11
+        const val GVA_PERSISTENT_BUTTONS_TYPE = 12
+        const val GVA_GALLERY_CARDS_TYPE = 13
 
         //Custom Card
-        const val CUSTOM_CARD_TYPE = 13 // Should be the last type with the highest value
+        const val CUSTOM_CARD_TYPE = 14 // Should be the last type with the highest value
     }
 
     @IntDef(
@@ -332,6 +335,7 @@ internal class ChatAdapter(
         SYSTEM_MESSAGE_TYPE,
         CUSTOM_CARD_TYPE,
         GVA_RESPONSE_TEXT_TYPE,
+        GVA_QUICK_REPLIES_TYPE,
         GVA_PERSISTENT_BUTTONS_TYPE,
         GVA_GALLERY_CARDS_TYPE
     )
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt
index 8d34af31e..5247db844 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt
@@ -1,17 +1,10 @@
 package com.glia.widgets.chat.controller
 
 import android.net.Uri
-import android.text.format.DateUtils
 import android.view.View
-import androidx.annotation.VisibleForTesting
-import com.glia.androidsdk.Glia
 import com.glia.androidsdk.GliaException
 import com.glia.androidsdk.Operator
 import com.glia.androidsdk.chat.AttachmentFile
-import com.glia.androidsdk.chat.Chat
-import com.glia.androidsdk.chat.ChatMessage
-import com.glia.androidsdk.chat.FilesAttachment
-import com.glia.androidsdk.chat.MessageAttachment
 import com.glia.androidsdk.chat.SingleChoiceAttachment
 import com.glia.androidsdk.chat.SingleChoiceOption
 import com.glia.androidsdk.chat.VisitorMessage
@@ -24,42 +17,27 @@ import com.glia.androidsdk.omnicore.OmnicoreEngagement
 import com.glia.androidsdk.site.SiteInfo
 import com.glia.widgets.Constants
 import com.glia.widgets.GliaWidgets
+import com.glia.widgets.chat.ChatManager
 import com.glia.widgets.chat.ChatType
 import com.glia.widgets.chat.ChatView
 import com.glia.widgets.chat.ChatViewCallback
-import com.glia.widgets.chat.domain.AddNewMessagesDividerUseCase
-import com.glia.widgets.chat.domain.CustomCardAdapterTypeUseCase
-import com.glia.widgets.chat.domain.CustomCardShouldShowUseCase
-import com.glia.widgets.chat.domain.CustomCardTypeUseCase
-import com.glia.widgets.chat.domain.GliaLoadHistoryUseCase
-import com.glia.widgets.chat.domain.GliaOnMessageUseCase
 import com.glia.widgets.chat.domain.GliaOnOperatorTypingUseCase
 import com.glia.widgets.chat.domain.GliaSendMessagePreviewUseCase
 import com.glia.widgets.chat.domain.GliaSendMessageUseCase
-import com.glia.widgets.chat.domain.IsEnableChatEditTextUseCase
+import com.glia.widgets.chat.domain.IsAuthenticatedUseCase
 import com.glia.widgets.chat.domain.IsFromCallScreenUseCase
 import com.glia.widgets.chat.domain.IsSecureConversationsChatAvailableUseCase
 import com.glia.widgets.chat.domain.IsShowSendButtonUseCase
 import com.glia.widgets.chat.domain.SiteInfoUseCase
 import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase
 import com.glia.widgets.chat.domain.gva.DetermineGvaButtonTypeUseCase
-import com.glia.widgets.chat.domain.gva.IsGvaUseCase
-import com.glia.widgets.chat.domain.gva.MapGvaUseCase
-import com.glia.widgets.chat.model.ChatInputMode
 import com.glia.widgets.chat.model.ChatItem
 import com.glia.widgets.chat.model.ChatState
 import com.glia.widgets.chat.model.CustomCardChatItem
 import com.glia.widgets.chat.model.Gva
 import com.glia.widgets.chat.model.GvaButton
-import com.glia.widgets.chat.model.GvaQuickReplies
-import com.glia.widgets.chat.model.MediaUpgradeStartedTimerItem
-import com.glia.widgets.chat.model.NewMessagesDividerItem
-import com.glia.widgets.chat.model.OperatorAttachmentItem
-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.SystemChatItem
-import com.glia.widgets.chat.model.VisitorAttachmentItem
 import com.glia.widgets.chat.model.VisitorMessageItem
 import com.glia.widgets.core.callvisualizer.domain.IsCallVisualizerUseCase
 import com.glia.widgets.core.chathead.domain.HasPendingSurveyUseCase
@@ -73,8 +51,6 @@ import com.glia.widgets.core.engagement.domain.GliaOnEngagementUseCase
 import com.glia.widgets.core.engagement.domain.IsOngoingEngagementUseCase
 import com.glia.widgets.core.engagement.domain.IsQueueingEngagementUseCase
 import com.glia.widgets.core.engagement.domain.SetEngagementConfigUseCase
-import com.glia.widgets.core.engagement.domain.model.ChatHistoryResponse
-import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
 import com.glia.widgets.core.engagement.domain.model.EngagementStateEvent
 import com.glia.widgets.core.engagement.domain.model.EngagementStateEventVisitor
 import com.glia.widgets.core.engagement.domain.model.EngagementStateEventVisitor.OperatorVisitor
@@ -99,7 +75,6 @@ import com.glia.widgets.core.queue.domain.GliaQueueForChatEngagementUseCase
 import com.glia.widgets.core.queue.domain.QueueTicketStateChangeToUnstaffedUseCase
 import com.glia.widgets.core.queue.domain.exception.QueueingOngoingException
 import com.glia.widgets.core.secureconversations.domain.IsSecureEngagementUseCase
-import com.glia.widgets.core.secureconversations.domain.MarkMessagesReadWithDelayUseCase
 import com.glia.widgets.core.survey.OnSurveyListener
 import com.glia.widgets.core.survey.domain.GliaSurveyUseCase
 import com.glia.widgets.di.Dependencies
@@ -111,7 +86,6 @@ import com.glia.widgets.helper.TimeCounter
 import com.glia.widgets.helper.TimeCounter.FormattedTimerStatusListener
 import com.glia.widgets.helper.formattedName
 import com.glia.widgets.helper.imageUrl
-import com.glia.widgets.helper.isImage
 import com.glia.widgets.view.MessagesNotSeenHandler
 import com.glia.widgets.view.MinimizeHandler
 import io.reactivex.android.schedulers.AndroidSchedulers
@@ -119,7 +93,6 @@ import io.reactivex.disposables.CompositeDisposable
 import io.reactivex.disposables.Disposable
 import io.reactivex.schedulers.Schedulers
 import java.util.Observer
-import java.util.UUID
 
 internal class ChatController(
     chatViewCallback: ChatViewCallback,
@@ -129,11 +102,9 @@ internal class ChatController(
     private val dialogController: DialogController,
     private val messagesNotSeenHandler: MessagesNotSeenHandler,
     private val callNotificationUseCase: CallNotificationUseCase,
-    private val loadHistoryUseCase: GliaLoadHistoryUseCase,
     private val queueForChatEngagementUseCase: GliaQueueForChatEngagementUseCase,
     private val getEngagementUseCase: GliaOnEngagementUseCase,
     private val engagementEndUseCase: GliaOnEngagementEndUseCase,
-    private val onMessageUseCase: GliaOnMessageUseCase,
     private val onOperatorTypingUseCase: GliaOnOperatorTypingUseCase,
     private val sendMessagePreviewUseCase: GliaSendMessagePreviewUseCase,
     private val sendMessageUseCase: GliaSendMessageUseCase,
@@ -149,15 +120,11 @@ internal class ChatController(
     private val isShowSendButtonUseCase: IsShowSendButtonUseCase,
     private val isShowOverlayPermissionRequestDialogUseCase: IsShowOverlayPermissionRequestDialogUseCase,
     private val downloadFileUseCase: DownloadFileUseCase,
-    private val isEnableChatEditTextUseCase: IsEnableChatEditTextUseCase,
     private val siteInfoUseCase: SiteInfoUseCase,
     private val surveyUseCase: GliaSurveyUseCase,
     private val getGliaEngagementStateFlowableUseCase: GetEngagementStateFlowableUseCase,
     private val isFromCallScreenUseCase: IsFromCallScreenUseCase,
     private val updateFromCallScreenUseCase: UpdateFromCallScreenUseCase,
-    private val customCardAdapterTypeUseCase: CustomCardAdapterTypeUseCase,
-    private val customCardTypeUseCase: CustomCardTypeUseCase,
-    private val customCardShouldShowUseCase: CustomCardShouldShowUseCase,
     private val ticketStateChangeToUnstaffedUseCase: QueueTicketStateChangeToUnstaffedUseCase,
     private val isQueueingEngagementUseCase: IsQueueingEngagementUseCase,
     private val addMediaUpgradeCallbackUseCase: AddMediaUpgradeOfferCallbackUseCase,
@@ -166,16 +133,14 @@ internal class ChatController(
     private val isOngoingEngagementUseCase: IsOngoingEngagementUseCase,
     private val engagementConfigUseCase: SetEngagementConfigUseCase,
     private val isSecureEngagementAvailableUseCase: IsSecureConversationsChatAvailableUseCase,
-    private val markMessagesReadWithDelayUseCase: MarkMessagesReadWithDelayUseCase,
     private val hasPendingSurveyUseCase: HasPendingSurveyUseCase,
     private val setPendingSurveyUsedUseCase: SetPendingSurveyUsedUseCase,
     private val isCallVisualizerUseCase: IsCallVisualizerUseCase,
-    private val addNewMessagesDividerUseCase: AddNewMessagesDividerUseCase,
     private val isFileReadyForPreviewUseCase: IsFileReadyForPreviewUseCase,
     private val acceptMediaUpgradeOfferUseCase: AcceptMediaUpgradeOfferUseCase,
-    private val isGvaUseCase: IsGvaUseCase,
-    private val mapGvaUseCase: MapGvaUseCase,
-    private val determineGvaButtonTypeUseCase: DetermineGvaButtonTypeUseCase
+    private val determineGvaButtonTypeUseCase: DetermineGvaButtonTypeUseCase,
+    private val isAuthenticatedUseCase: IsAuthenticatedUseCase,
+    private val chatManager: ChatManager
 ) : GliaOnEngagementUseCase.Listener, GliaOnEngagementEndUseCase.Listener, OnSurveyListener {
     private var backClickedListener: ChatView.OnBackClickedListener? = null
     private var viewCallback: ChatViewCallback? = null
@@ -190,20 +155,13 @@ internal class ChatController(
     private val sendMessageCallback: GliaSendMessageUseCase.Listener =
         object : GliaSendMessageUseCase.Listener {
             override fun messageSent(message: VisitorMessage?) {
-                onMessageSent(message)
-            }
-
-            override fun onCardMessageUpdated(message: ChatMessage) {
-                updateCustomCard(message)
+                Logger.d(TAG, "messageSent: $message, id: ${message?.id}")
             }
 
             override fun onMessageValidated() {
                 viewCallback?.clearMessageInput()
-
                 emitViewState {
-                    chatState
-                        .setLastTypedText("")
-                        .setShowSendButton(isShowSendButtonUseCase(""))
+                    chatState.setLastTypedText("").setShowSendButton(isShowSendButtonUseCase(""))
                 }
             }
 
@@ -228,8 +186,7 @@ internal class ChatController(
         viewCallback?.apply {
             emitUploadAttachments(getFileAttachmentsUseCase.execute())
             emitViewState {
-                chatState
-                    .setShowSendButton(isShowSendButtonUseCase(chatState.lastTypedText))
+                chatState.setShowSendButton(isShowSendButtonUseCase(chatState.lastTypedText))
                     .setIsAttachmentButtonEnabled(supportedFileCountCheckUseCase.execute())
             }
         }
@@ -258,6 +215,7 @@ internal class ChatController(
                 if (isSecureEngagement) {
                     emitViewState { chatState.setSecureMessagingState() }
                 }
+                chatManager.onChatAction(ChatManager.Action.ChatRestored)
                 return
             }
 
@@ -267,7 +225,7 @@ internal class ChatController(
             }
             prepareChatComponents()
             emitViewState { initChatState }
-            loadChatHistory()
+            initChatManager()
         }
     }
 
@@ -318,18 +276,6 @@ internal class ChatController(
         }
     }
 
-    @Synchronized
-    private fun emitChatItems(callback: () -> ChatState?) {
-        val state = callback() ?: return
-
-        if (setState(state) && viewCallback != null) {
-            Logger.d(TAG, """Emit chat items: ${state.chatItems} (State): $state""".trimIndent())
-
-            viewCallback?.emitItems(state.chatItems)
-            viewCallback?.emitUploadAttachments(getFileAttachmentsUseCase.execute())
-        }
-    }
-
     fun onDestroy(retain: Boolean) {
         Logger.d(TAG, "onDestroy, retain:$retain")
         dialogController.dismissMessageCenterUnavailableDialog()
@@ -351,6 +297,7 @@ internal class ChatController(
             onOperatorTypingUseCase.unregisterListener()
             removeFileAttachmentObserverUseCase.execute(fileAttachmentObserver)
             shouldHandleEndedEngagement = false
+            chatManager.reset()
         }
     }
 
@@ -396,100 +343,6 @@ internal class ChatController(
         sendMessagePreview("")
     }
 
-    private fun subscribeToMessages() {
-        disposable.add(
-            onMessageUseCase()
-                .doOnNext { onPreEngagementMessage(it) }
-                .subscribe()
-        )
-    }
-
-    private fun onPreEngagementMessage(messageInternal: ChatMessageInternal) {
-        emitChatItems {
-            val message = messageInternal.chatMessage
-            if (message.senderType == Chat.Participant.VISITOR && message.attachment != null &&
-                !isNewMessage(chatState.chatItems, message)
-            ) {
-                val items: MutableList = chatState.chatItems.toMutableList()
-                val currentMessage = items.first { it.id == message.id }
-                val currentMessageIndex = items.indexOf(currentMessage)
-                items.removeAll { it.id == message.id }
-                addVisitorAttachmentItemsToChatItems(items, message, currentMessageIndex + 1)
-                return@emitChatItems chatState.changeItems(items)
-            } else {
-                onMessage(messageInternal)
-            }
-            return@emitChatItems null
-        }
-    }
-
-    private fun onMessage(messageInternal: ChatMessageInternal) {
-
-        emitChatItems {
-            val message = messageInternal.chatMessage
-            if (!isNewMessage(chatState.chatItems, message)) {
-                return@emitChatItems null
-            }
-            addQuickReplyButtons(emptyList())
-
-            val isUnsentMessage =
-                chatState.unsentMessages.isNotEmpty() && chatState.unsentMessages[0].message == message.content
-            Logger.d(
-                TAG,
-                "onMessage: ${message.content}, id: ${message.id}, isUnsentMessage: $isUnsentMessage"
-            )
-            if (isUnsentMessage) {
-                // emitting state because there is no need to change recyclerview items here
-                emitViewState {
-                    val unsentMessages: MutableList = chatState.unsentMessages.toMutableList()
-                    val currentMessage = unsentMessages.removeFirst()
-                    val currentChatItems: MutableList = chatState.chatItems.toMutableList()
-                    val currentMessageIndex = currentChatItems.indexOf(currentMessage)
-                    currentChatItems.remove(currentMessage)
-                    currentChatItems.add(
-                        currentMessageIndex,
-                        message.run { VisitorMessageItem.Delivered(id, timestamp, content) }
-                    )
-
-                    return@emitViewState chatState.changeItems(currentChatItems)
-                        .changeUnsentMessages(unsentMessages)
-                }
-
-                chatState.unsentMessages.firstOrNull()?.also {
-                    sendMessageUseCase.execute(it.message, sendMessageCallback)
-                }
-
-                return@emitChatItems null
-            }
-
-            val items: MutableList = chatState.chatItems.toMutableList()
-            appendMessageItem(items, messageInternal)
-
-            return@emitChatItems chatState.changeItems(items)
-        }
-    }
-
-    private fun onMessageSent(message: VisitorMessage?) {
-        if (message != null) {
-            Logger.d(TAG, "messageSent: $message, id: ${message.id}")
-            emitChatItems {
-                val currentChatItems: MutableList = chatState.chatItems.toMutableList()
-                if (isQueueingOrOngoingEngagement) {
-                    changeDeliveredIndex(currentChatItems, message)
-                } else if (isSecureEngagementUseCase() && isNewMessage(currentChatItems, message)) {
-                    appendSentMessage(currentChatItems, message)
-                }
-
-                // chat input mode has to be set to enable after a message is sent
-                if (isEnableChatEditTextUseCase(currentChatItems)) {
-                    emitViewState { chatState.chatInputModeChanged(ChatInputMode.ENABLED) }
-                }
-
-                return@emitChatItems chatState.changeItems(currentChatItems)
-            }
-        }
-    }
-
     private fun onMessageSendError(exception: GliaException) {
         Logger.d(TAG, "messageSent exception")
         error(exception)
@@ -504,18 +357,13 @@ internal class ChatController(
 
     private fun appendUnsentMessage(message: String) {
         Logger.d(TAG, "appendUnsentMessage: $message")
-        emitChatItems {
-            val unsentMessages = chatState.unsentMessages.toMutableList()
-            val unsentItem = VisitorMessageItem.Unsent("", System.currentTimeMillis(), message)
-            unsentMessages.add(unsentItem)
-            val currentChatItems: MutableList = chatState.chatItems.toMutableList()
-            currentChatItems.add(unsentItem)
-            emitViewState { chatState.changeUnsentMessages(unsentMessages) }
-
-            updateQueueing(currentChatItems)
-
-            return@emitChatItems chatState.changeItems(currentChatItems)
-        }
+        chatManager.onChatAction(
+            ChatManager.Action.UnsentMessageReceived(
+                VisitorMessageItem.Unsent(
+                    message = message
+                )
+            )
+        )
     }
 
     private fun onOperatorTyping(isOperatorTyping: Boolean) {
@@ -544,7 +392,7 @@ internal class ChatController(
             viewCallback?.backToCall()
         } else {
             backClickedListener?.onBackClicked()
-            Dependencies.getControllerFactory().destroyChatController()
+            onDestroy(isQueueingOrOngoingEngagement || isAuthenticatedUseCase())
             Dependencies.getControllerFactory().destroyCallController()
         }
         updateFromCallScreenUseCase.updateFromCallScreen(false)
@@ -598,13 +446,10 @@ internal class ChatController(
         Logger.d(TAG, "setViewCallback")
         viewCallback = chatViewCallback
         viewCallback?.emitState(chatState)
-        viewCallback?.emitItems(chatState.chatItems)
         viewCallback?.emitUploadAttachments(getFileAttachmentsUseCase.execute())
 
         // always start in bottom
-        emitViewState {
-            chatState.isInBottomChanged(true).changeVisibility(true)
-        }
+        emitViewState { chatState.isInBottomChanged(true).changeVisibility(true) }
         viewCallback?.scrollToBottomImmediate()
 
         chatState.pendingNavigationType?.also { viewCallback?.navigateToCall(it) }
@@ -699,14 +544,8 @@ internal class ChatController(
     }
 
     private fun onTransferring() {
-        emitChatItems {
-            val items: MutableList = chatState.chatItems.toMutableList()
-            chatState.operatorStatusItem?.also(items::remove)
-            items.add(OperatorStatusItem.Transferring)
-            emitViewState { chatState.transferring() }
-
-            return@emitChatItems chatState.changeItems(items)
-        }
+        emitViewState { chatState.transferring() }
+        chatManager.onChatAction(ChatManager.Action.Transferring)
     }
 
     fun overlayPermissionsDialogDismissed() {
@@ -735,6 +574,10 @@ internal class ChatController(
         return true
     }
 
+    private fun error(error: Throwable?) {
+        error?.also { error(it.toString()) }
+    }
+
     private fun error(error: String) {
         Logger.e(TAG, error)
         dialogController.showUnexpectedErrorDialog()
@@ -750,10 +593,7 @@ internal class ChatController(
                         // audio call
                         Logger.d(TAG, "audioUpgradeRequested")
                         if (chatState.isOperatorOnline) {
-                            dialogController.showUpgradeAudioDialog(
-                                offer,
-                                chatState.formattedOperatorName
-                            )
+                            dialogController.showUpgradeAudioDialog(offer, chatState.formattedOperatorName)
                         }
                     }
 
@@ -761,20 +601,14 @@ internal class ChatController(
                         // video call
                         Logger.d(TAG, "2 way videoUpgradeRequested")
                         if (chatState.isOperatorOnline) {
-                            dialogController.showUpgradeVideoDialog2Way(
-                                offer,
-                                chatState.formattedOperatorName
-                            )
+                            dialogController.showUpgradeVideoDialog2Way(offer, chatState.formattedOperatorName)
                         }
                     }
 
                     offer.video == MediaDirection.ONE_WAY -> {
                         Logger.d(TAG, "1 way videoUpgradeRequested")
                         if (chatState.isOperatorOnline) {
-                            dialogController.showUpgradeVideoDialog1Way(
-                                offer,
-                                chatState.formattedOperatorName
-                            )
+                            dialogController.showUpgradeVideoDialog1Way(offer, chatState.formattedOperatorName)
                         }
                     }
                 }
@@ -810,15 +644,8 @@ internal class ChatController(
 
     private fun viewInitQueueing() {
         Logger.d(TAG, "viewInitQueueing")
-        emitChatItems {
-            val items: MutableList = chatState.chatItems.toMutableList()
-            chatState.operatorStatusItem?.also(items::remove)
-            val operatorStatusItem = OperatorStatusItem.InQueue(chatState.companyName)
-            items.add(operatorStatusItem)
-            emitViewState { chatState.queueingStarted(operatorStatusItem) }
-
-            return@emitChatItems chatState.changeItems(items)
-        }
+        chatManager.onChatAction(ChatManager.Action.QueuingStarted(chatState.companyName.orEmpty()))
+        emitViewState { chatState.queueingStarted() }
     }
 
     private fun updateQueueing(items: MutableList) {
@@ -840,59 +667,29 @@ internal class ChatController(
     }
 
     private fun operatorConnected(formattedOperatorName: String, profileImgUrl: String?) {
-        emitChatItems {
-            val items: MutableList = chatState.chatItems.toMutableList()
-            if (chatState.operatorStatusItem != null) {
-                // remove previous operator status item
-                val operatorStatusItemIndex = items.indexOf(chatState.operatorStatusItem!!)
-                Logger.d(
-                    TAG,
-                    "operatorStatusItemIndex: " + operatorStatusItemIndex + ", size: " + items.size
-                )
-                items.remove(chatState.operatorStatusItem!!)
-                items.add(
-                    operatorStatusItemIndex,
-                    OperatorStatusItem.Connected(
-                        chatState.companyName,
-                        formattedOperatorName,
-                        profileImgUrl
-                    )
-                )
-            } else {
-                items.add(
-                    OperatorStatusItem.Connected(
-                        chatState.companyName,
-                        formattedOperatorName,
-                        profileImgUrl
-                    )
-                )
-            }
-            emitViewState {
-                chatState
-                    .operatorConnected(formattedOperatorName, profileImgUrl)
-                    .setLiveChatState()
-            }
-
-            return@emitChatItems chatState.changeItems(items)
-        }
+        chatManager.onChatAction(
+            ChatManager.Action.OperatorConnected(
+                chatState.companyName.orEmpty(),
+                formattedOperatorName,
+                profileImgUrl
+            )
+        )
+        emitViewState { chatState.operatorConnected(formattedOperatorName, profileImgUrl).setLiveChatState() }
     }
 
     private fun operatorChanged(formattedOperatorName: String, profileImgUrl: String?) {
-        emitChatItems {
-            val items: MutableList = chatState.chatItems.toMutableList()
-            val operatorStatusItem = OperatorStatusItem.Joined(
-                chatState.companyName,
+        chatManager.onChatAction(
+            ChatManager.Action.OperatorJoined(
+                chatState.companyName.orEmpty(),
                 formattedOperatorName,
                 profileImgUrl
             )
-            items.add(operatorStatusItem)
-
-            return@emitChatItems chatState.changeItems(items)
-        }
+        )
         emitViewState { chatState.operatorConnected(formattedOperatorName, profileImgUrl) }
     }
 
     private fun stop() {
+        chatManager.reset()
         Logger.d(TAG, "Stop, engagement ended")
         disposable.add(
             cancelQueueTicketUseCase.execute()
@@ -905,411 +702,43 @@ internal class ChatController(
         emitViewState { chatState.stop() }
     }
 
-    private fun appendHistoryChatItem(
-        currentChatItems: MutableList,
-        chatMessageInternal: ChatMessageInternal
-    ) {
-        val message = chatMessageInternal.chatMessage
-        when (message.senderType) {
-            Chat.Participant.VISITOR -> {
-                appendHistoryMessage(currentChatItems, message)
-                addVisitorAttachmentItemsToChatItems(currentChatItems, message)
-            }
-
-            Chat.Participant.OPERATOR -> {
-                appendOperatorMessage(currentChatItems, chatMessageInternal)
-            }
-
-            Chat.Participant.SYSTEM -> {
-                appendSystemMessage(currentChatItems, chatMessageInternal)
-            }
-
-            Chat.Participant.UNKNOWN -> Logger.d(TAG, "Unknown type `chat item` received: $message")
-        }
-    }
-
-    private fun appendHistoryMessage(
-        currentChatItems: MutableList,
-        message: ChatMessage
-    ) {
-        if (message.content.isNotEmpty()) {
-            currentChatItems.add(message.run { VisitorMessageItem.History(id, timestamp, content) })
-        }
-    }
-
-    private fun appendMessageItem(
-        currentChatItems: MutableList,
-        messageInternal: ChatMessageInternal
-    ) {
-        val message = messageInternal.chatMessage
-        when (message.senderType) {
-            Chat.Participant.VISITOR -> {
-                appendSentMessage(currentChatItems, message)
-                addVisitorAttachmentItemsToChatItems(currentChatItems, message)
-                changeDeliveredIndex(currentChatItems, messageInternal.chatMessage as VisitorMessage)
-            }
-
-            Chat.Participant.OPERATOR -> {
-                onOperatorMessageReceived(currentChatItems, messageInternal)
-            }
-
-            Chat.Participant.SYSTEM -> {
-                onSystemMessageReceived(currentChatItems, messageInternal)
-            }
-
-            Chat.Participant.UNKNOWN -> Logger.d(TAG, "Unknown type `chat item` received: $message")
-        }
-    }
-
-    private fun onOperatorMessageReceived(
-        currentChatItems: MutableList,
-        messageInternal: ChatMessageInternal
-    ) {
-        appendOperatorMessage(currentChatItems, messageInternal)
-        appendMessagesNotSeen()
-    }
-
-    private fun onSystemMessageReceived(
-        currentChatItems: MutableList,
-        messageInternal: ChatMessageInternal
-    ) {
-        appendSystemMessage(currentChatItems, messageInternal)
-        appendMessagesNotSeen()
-    }
-
-    private fun addVisitorAttachmentItemsToChatItems(
-        currentChatItems: MutableList,
-        chatMessage: ChatMessage,
-        index: Int? = null
-    ) {
-        val attachment = chatMessage.attachment
-        if (attachment is FilesAttachment) {
-            val visitorAttachmentItems = attachment.files.map {
-                if (it.isImage) {
-                    VisitorAttachmentItem.Image(
-                        id = chatMessage.id,
-                        timestamp = chatMessage.timestamp,
-                        attachmentFile = it
-                    )
-                } else {
-                    VisitorAttachmentItem.File(
-                        id = chatMessage.id,
-                        timestamp = chatMessage.timestamp,
-                        attachmentFile = it
-                    )
-                }
-            }
-
-            if (index != null) {
-                currentChatItems.addAll(index, visitorAttachmentItems)
-            } else {
-                currentChatItems.addAll(visitorAttachmentItems)
-            }
-        }
-    }
-
-    private fun appendSentMessage(items: MutableList, message: ChatMessage) {
-        if (message.content.isNotEmpty()) {
-            message.apply {
-                items.add(VisitorMessageItem.Delivered(id, timestamp, content))
-            }
-        }
-    }
-
-    private fun appendMessagesNotSeen() {
-        emitViewState {
-            val notSeenCount = chatState.messagesNotSeen
-            chatState.messagesNotSeenChanged(if (chatState.isChatInBottom) 0 else notSeenCount + 1)
-        }
-    }
-
     private fun initGliaEngagementObserving() {
         getEngagementUseCase.execute(this)
         engagementEndUseCase.execute(this)
     }
 
-    private fun changeDeliveredIndex(
-        currentChatItems: MutableList,
-        message: VisitorMessage
-    ) {
-        // "Delivered" status only applies to visitor messages
-        if (message.senderType != Chat.Participant.VISITOR) return
-
-        val messageId = message.id
-        var foundDelivered = false
-        for (i in currentChatItems.indices.reversed()) {
-            val currentChatItem = currentChatItems[i]
-            if (currentChatItem is VisitorMessageItem) {
-                val itemId = currentChatItem.id
-                when {
-                    currentChatItem is VisitorMessageItem.History -> {
-                        // we reached the history items no point in going searching further
-                        break
-                    }
-
-                    !foundDelivered && itemId == messageId -> {
-                        foundDelivered = true
-                        currentChatItems[i] = VisitorMessageItem.Delivered(
-                            currentChatItem.id,
-                            currentChatItem.timestamp,
-                            currentChatItem.message
-                        )
-                    }
-
-                    currentChatItem.showDelivered -> {
-                        currentChatItems[i] = VisitorMessageItem.New(
-                            currentChatItem.id,
-                            currentChatItem.timestamp,
-                            currentChatItem.message
-                        )
-                    }
-                }
-            } else if (currentChatItem is VisitorAttachmentItem) {
-                if (!foundDelivered && currentChatItem.id == messageId) {
-                    foundDelivered = true
-                    setDelivered(currentChatItems, i, currentChatItem, true)
-                } else if (currentChatItem.showDelivered) {
-                    setDelivered(currentChatItems, i, currentChatItem, false)
-                }
-            }
-        }
-    }
-
-    private fun setDelivered(
-        currentChatItems: MutableList,
-        i: Int,
-        item: VisitorAttachmentItem,
-        delivered: Boolean
-    ) {
-        currentChatItems[i] = item.withDeliveredStatus(delivered)
-    }
-
-    private fun appendSystemMessage(
-        currentChatItems: MutableList,
-        chatMessageInternal: ChatMessageInternal
-    ) {
-        chatMessageInternal.chatMessage.apply {
-            currentChatItems += SystemChatItem(content, id, timestamp)
-        }
-    }
-
-    private fun appendOperatorMessage(
-        currentChatItems: MutableList,
-        chatMessageInternal: ChatMessageInternal
-    ) {
-        setLastOperatorItemChatHeadVisibility(
-            currentChatItems,
-            isOperatorChanged(currentChatItems, chatMessageInternal)
-        )
-        appendOperatorOrCustomCardItem(currentChatItems, chatMessageInternal)
-        appendOperatorAttachmentItems(currentChatItems, chatMessageInternal)
-        setLastOperatorItemChatHeadVisibility(currentChatItems, true)
-    }
-
-    private fun isOperatorChanged(currentChatItems: List, chatMessageInternal: ChatMessageInternal): Boolean =
-        (currentChatItems.lastOrNull() as? OperatorChatItem)?.operatorId != chatMessageInternal.operatorId
-
-    private fun setLastOperatorItemChatHeadVisibility(
-        currentChatItems: MutableList,
-        showChatHead: Boolean
-    ) {
-        val lastItem: OperatorChatItem = currentChatItems.lastOrNull() as? OperatorChatItem ?: return
-
-        currentChatItems.apply {
-            this[lastIndex] = lastItem.withShowChatHead(showChatHead)
-        }
-    }
-
-    private fun appendOperatorAttachmentItems(
-        currentChatItems: MutableList,
-        messageInternal: ChatMessageInternal
-    ) {
-        val message = messageInternal.chatMessage
-        val attachment = message.attachment
-
-        if (attachment is FilesAttachment) {
-            val files = attachment.files
-            for (file in files) {
-
-                val item: OperatorAttachmentItem = if (file.isImage) {
-                    OperatorAttachmentItem.Image(
-                        attachmentFile = file,
-                        id = message.id,
-                        timestamp = message.timestamp,
-                        operatorProfileImgUrl = messageInternal.operatorImageUrl ?: chatState.operatorProfileImgUrl,
-                        operatorId = messageInternal.operatorId ?: UUID.randomUUID().toString()
-
-                    )
-                } else {
-                    OperatorAttachmentItem.File(
-                        attachmentFile = file,
-                        id = message.id,
-                        timestamp = message.timestamp,
-                        operatorProfileImgUrl = messageInternal.operatorImageUrl ?: chatState.operatorProfileImgUrl,
-                        operatorId = messageInternal.operatorId ?: UUID.randomUUID().toString()
-
-                    )
-                }
-
-                currentChatItems.add(item)
-
-            }
-        }
-    }
-
-    private fun appendOperatorOrCustomCardItem(
-        currentChatItems: MutableList,
-        messageInternal: ChatMessageInternal
-    ) {
-        val message = messageInternal.chatMessage
-        when {
-            isGvaUseCase(message) -> appendGvaMessageItem(currentChatItems, messageInternal)
-            message.content.isBlank() -> return
-            customCardAdapterTypeUseCase(message) != null -> appendCustomCardItem(currentChatItems, message, customCardAdapterTypeUseCase(message)!!)
-            else -> appendOperatorMessageItem(currentChatItems, messageInternal)
-        }
-    }
-
-    private fun appendGvaMessageItem(currentChatItems: MutableList, messageInternal: ChatMessageInternal) {
-
-        when (val gvaChatItem = mapGvaUseCase(messageInternal, chatState)) {
-
-            is GvaQuickReplies -> {
-                currentChatItems.add(gvaChatItem.gvaResponseText)
-                addQuickReplyButtons(gvaChatItem.options)
-            }
-
-            else -> {
-                currentChatItems.add(gvaChatItem as OperatorChatItem)
-            }
-        }
-    }
-
     private fun addQuickReplyButtons(options: List) {
         emitViewState { chatState.copy(gvaQuickReplies = options) }
     }
 
-    private fun appendCustomCardItem(
-        currentChatItems: MutableList,
-        message: ChatMessage,
-        viewType: Int
-    ) {
-        val customCardType = customCardTypeUseCase.execute(viewType) ?: return
-        if (customCardShouldShowUseCase.execute(message, customCardType, true)) {
-            currentChatItems.add(message.run { CustomCardChatItem(message, viewType) })
-        }
-
-        (message.attachment as? SingleChoiceAttachment)?.selectedOptionText?.takeIf {
-            it.isNotBlank()
-        }?.let {
-            VisitorMessageItem.New(message.id, message.timestamp, it)
-        }?.also {
-            currentChatItems.add(it)
-        }
-    }
-
-    private fun appendOperatorMessageItem(
-        currentChatItems: MutableList,
-        messageInternal: ChatMessageInternal
-    ) {
-        val message = messageInternal.chatMessage
-        val messageAttachment = message.attachment
-        val singleChoiceAttachmentOptions = getSingleChoiceAttachmentOptions(messageAttachment)
-        val operatorName = messageInternal.operatorName ?: chatState.formattedOperatorName
-        val operatorImage = messageInternal.operatorImageUrl ?: chatState.operatorProfileImgUrl
-        val operatorId = messageInternal.operatorId ?: UUID.randomUUID().toString()
-
-        val item = if (singleChoiceAttachmentOptions.isNullOrEmpty() || !messageInternal.isLatest) {
-            OperatorMessageItem.PlainText(
-                message.id,
-                message.timestamp,
-                false,
-                operatorImage,
-                operatorId,
-                operatorName,
-                message.content
-            )
-        } else {
-            OperatorMessageItem.ResponseCard(
-                message.id,
-                message.timestamp,
-                false,
-                operatorImage,
-                operatorId,
-                operatorName,
-                message.content,
-                singleChoiceAttachmentOptions,
-                getSingleChoiceAttachmentImgUrl(messageAttachment)
-            )
-        }
-
-        currentChatItems.add(item)
-    }
-
-    private fun getSingleChoiceAttachmentImgUrl(attachment: MessageAttachment?): String? =
-        (attachment as? SingleChoiceAttachment)?.imageUrl?.orElse(null)
-
-    private fun getSingleChoiceAttachmentOptions(attachment: MessageAttachment?): List? {
-        return (attachment as? SingleChoiceAttachment)?.options?.toList()
-    }
-
     private fun startTimer() {
         Logger.d(TAG, "startTimer")
         callTimer.startNew(Constants.CALL_TIMER_DELAY, Constants.CALL_TIMER_INTERVAL_VALUE)
     }
 
-    private fun upgradeMediaItem() {
+    private fun upgradeMediaItemToVideo() {
         Logger.d(TAG, "upgradeMediaItem")
-        emitChatItems {
-            val newItems: MutableList = chatState.chatItems.toMutableList()
-            val mediaUpgradeStartedTimerItem = MediaUpgradeStartedTimerItem.Video(
-                chatState.mediaUpgradeStartedTimerItem?.time.orEmpty()
-            )
-            chatState.mediaUpgradeStartedTimerItem?.also { newItems.remove(it) }
-            newItems.add(mediaUpgradeStartedTimerItem)
-
-            return@emitChatItems chatState.changeTimerItem(newItems, mediaUpgradeStartedTimerItem)
-        }
+        emitViewState { chatState.upgradeMedia(true) }
+        chatManager.onChatAction(ChatManager.Action.OnMediaUpgradeToVideo)
     }
 
     private fun createNewTimerCallback() {
         timerStatusListener?.also { callTimer.removeFormattedValueListener(it) }
         timerStatusListener = object : FormattedTimerStatusListener {
             override fun onNewFormattedTimerValue(formatedValue: String) {
-                emitChatItems {
-                    if (chatState.isMediaUpgradeStarted) {
-                        val index = chatState.mediaUpgradeStartedTimerItem?.let {
-                            chatState.chatItems.indexOf(it)
-                        } ?: -1
-
-                        if (index != -1) {
-                            val mediaUpgradeStartedTimerItem =
-                                chatState.mediaUpgradeStartedTimerItem?.updateTime(formatedValue) ?: return@emitChatItems null
-
-                            val newItems: MutableList = chatState.chatItems.toMutableList()
-                            newItems.removeAt(index)
-                            newItems.add(index, mediaUpgradeStartedTimerItem)
-
-                            return@emitChatItems chatState.changeTimerItem(
-                                newItems,
-                                mediaUpgradeStartedTimerItem
-                            )
-                        }
-                    }
-                    return@emitChatItems null
+                if (chatState.isMediaUpgradeStarted) {
+                    chatManager.onChatAction(
+                        ChatManager.Action.OnMediaUpgradeTimerUpdated(
+                            formatedValue
+                        )
+                    )
                 }
             }
 
             override fun onFormattedTimerCancelled() {
-                chatState.apply {
-                    if (isMediaUpgradeStarted && chatItems.contains(mediaUpgradeStartedTimerItem ?: return)) {
-                        emitChatItems {
-                            val newItems: MutableList = chatItems.toMutableList()
-                            newItems.remove(mediaUpgradeStartedTimerItem)
-
-                            return@emitChatItems changeTimerItem(newItems, null)
-                        }
-                    }
+                if (chatState.isMediaUpgradeStarted) {
+                    emitViewState { chatState.upgradeMedia(null) }
+                    chatManager.onChatAction(ChatManager.Action.OnMediaUpgradeCanceled)
                 }
             }
         }
@@ -1321,76 +750,20 @@ internal class ChatController(
     ) {
         Logger.d(TAG, "singleChoiceOptionClicked, id: ${item.id}")
         sendMessageUseCase.execute(selectedOption.asSingleChoiceResponse(), sendMessageCallback)
-        emitChatItems {
-            val modifiedItems: MutableList = chatState.chatItems.toMutableList()
-            val indexInList = modifiedItems.indexOf(item)
-            if (indexInList >= 0) {
-                modifiedItems[indexInList] = item.toPlainText()
-            } else {
-                Logger.e(TAG, "singleChoiceOptionClicked, ResponseCardItem is not on the list!")
-            }
-
-            return@emitChatItems chatState.changeItems(modifiedItems)
-        }
+        chatManager.onChatAction(ChatManager.Action.ResponseCardClicked(item))
     }
 
-    fun sendCustomCardResponse(messageId: String, text: String, value: String) {
-        emitChatItems {
-            chatState.chatItems
-                .firstOrNull { messageId == it.id }
-                ?.let { it as? CustomCardChatItem }
-                ?.also {
-                    sendMessageUseCase.execute(it.message, text, value, sendMessageCallback)
-
-                    val customCardType = customCardTypeUseCase.execute(it.viewType) ?: return@also
-                    val currentMessage = it.message
-                    val showCustomCard = customCardShouldShowUseCase.execute(
-                        currentMessage,
-                        customCardType,
-                        false
-                    )
-                    if (!showCustomCard) {
-                        val chatItems: MutableList = chatState.chatItems.toMutableList()
+    fun sendCustomCardResponse(customCard: CustomCardChatItem, text: String, value: String) {
+        val attachment = SingleChoiceAttachment.from(value, text)
+        sendMessageUseCase.execute(attachment, sendMessageCallback)
 
-                        // If the card should be hidden after the response, we remove it from the item list.
-                        chatItems.remove(it)
-                        return@emitChatItems chatState.changeItems(chatItems)
-                    }
-                    return@emitChatItems null
-                } ?: run {
-                sendMessageUseCase.execute(null, text, value, sendMessageCallback)
-            }
-            return@emitChatItems null
-        }
+        chatManager.onChatAction(ChatManager.Action.CustomCardClicked(customCard, attachment))
     }
 
     private fun sendGvaResponse(singleChoiceAttachment: SingleChoiceAttachment) {
         sendMessageUseCase.execute(singleChoiceAttachment, sendMessageCallback)
     }
 
-    private fun updateCustomCard(message: ChatMessage) {
-        chatState.chatItems
-            .firstOrNull { message.id == it.id }
-            ?.let { it as? CustomCardChatItem }
-            ?.also {
-                emitChatItems {
-                    val chatItems: MutableList = chatState.chatItems.toMutableList()
-                    updateCustomCardSelectedOption(it, message, chatItems)
-
-                    return@emitChatItems chatState.changeItems(chatItems)
-                }
-            }
-    }
-
-    private fun updateCustomCardSelectedOption(
-        currentCustomCardItem: CustomCardChatItem,
-        updatedMessage: ChatMessage,
-        chatItems: MutableList
-    ) {
-        val indexInList = chatItems.indexOf(currentCustomCardItem)
-        chatItems[indexInList] = CustomCardChatItem(updatedMessage, currentCustomCardItem.viewType)
-    }
-
     fun onRecyclerviewPositionChanged(isBottom: Boolean) {
         if (isBottom) {
             Logger.d(TAG, "onRecyclerviewPositionChanged, isBottom = true")
@@ -1406,75 +779,6 @@ internal class ChatController(
         viewCallback?.smoothScrollToBottom()
     }
 
-    private fun loadChatHistory() {
-        val historyDisposable = loadHistoryUseCase()
-            .subscribe({ historyLoaded(it) }, { error(it) })
-        disposable.add(historyDisposable)
-    }
-
-    @Synchronized
-    private fun historyLoaded(historyResponse: ChatHistoryResponse) {
-        Logger.d(TAG, "historyLoaded")
-        val (messages, newMessagesCount) = historyResponse
-        val currentItems: MutableList = chatState.chatItems.toMutableList()
-        val newItems = removeDuplicates(currentItems, messages)
-
-        when {
-            !newItems.isNullOrEmpty() -> submitHistoryItems(
-                newItems,
-                currentItems,
-                newMessagesCount
-            )
-
-            !chatState.engagementRequested && !isSecureEngagement -> queueForEngagement()
-            else -> Logger.d(TAG, "Opened empty Secure Conversations chat")
-        }
-
-        initGliaEngagementObserving()
-        subscribeToMessages()
-    }
-
-    private fun submitHistoryItems(
-        newItems: List,
-        currentItems: MutableList,
-        newMessagesCount: Int
-    ) {
-        newItems.forEach { appendHistoryChatItem(currentItems, it) }
-
-        if (isSecureEngagementUseCase() && !isQueueingOrOngoingEngagement) {
-            emitChatTranscriptItems(currentItems, newMessagesCount)
-        } else {
-            emitChatItems { chatState.historyLoaded(currentItems) }
-        }
-    }
-
-    @VisibleForTesting
-    fun emitChatTranscriptItems(
-        items: MutableList,
-        newMessagesCount: Int
-    ) {
-        if (addNewMessagesDividerUseCase(items, newMessagesCount)) {
-            emitChatItems { chatState.changeItems(items) }
-            markMessagesReadWithDelay()
-        } else {
-            emitChatItems { chatState.changeItems(items) }
-        }
-    }
-
-    private fun markMessagesReadWithDelay() {
-        disposable.add(
-            markMessagesReadWithDelayUseCase().subscribe({
-                removeNewMessagesDivider()
-            }, {
-                Logger.e(TAG, "Marking messages read failed", it)
-            })
-        )
-    }
-
-    private fun removeNewMessagesDivider() {
-        emitChatItems { chatState.run { changeItems(chatItems - NewMessagesDividerItem) } }
-    }
-
     init {
         Logger.d(TAG, "constructor")
 
@@ -1485,40 +789,44 @@ internal class ChatController(
         chatState = ChatState()
     }
 
-    @VisibleForTesting
-    fun removeDuplicates(
-        oldHistory: List?,
-        newHistory: List?
-    ): List? {
-        return if (newHistory.isNullOrEmpty() || oldHistory.isNullOrEmpty()) {
-            newHistory
-        } else {
-            newHistory.filter { isNewMessage(oldHistory, it.chatMessage) }
-        }
-    }
-
-    @VisibleForTesting
-    fun isNewMessage(oldHistory: List?, newMessage: ChatMessage): Boolean =
-        oldHistory?.none { it.id == newMessage.id } ?: true
-
-    private fun error(error: Throwable?) {
-        error?.also { error(it.toString()) }
-    }
-
     override fun newEngagementLoaded(engagement: OmnicoreEngagement) {
         Logger.d(TAG, "newEngagementLoaded")
-        subscribeToMessages()
         onOperatorTypingUseCase.execute { onOperatorTyping(it) }
         addOperatorMediaStateListenerUseCase.execute(operatorMediaStateListener)
         mediaUpgradeOfferRepository.startListening()
-        if (chatState.unsentMessages.isNotEmpty()) {
-            sendMessageUseCase.execute(chatState.unsentMessages[0].message, sendMessageCallback)
-            Logger.d(TAG, "unsentMessage sent!")
-        }
         emitViewState { chatState.engagementStarted() }
-        // Loading chat history again on engagement start in case it was an-authenticated visitor that restored an ongoing engagement
-        // Currently there is no direct way to know if the Visitor is authenticated.
-        loadChatHistory()
+    }
+
+    private fun initChatManager() {
+        chatManager.initialize(::onHistoryLoaded, ::addQuickReplyButtons, ::updateUnSeenMessagesCount)
+            .subscribe(::emitItems, ::error)
+            .also(disposable::add)
+    }
+
+    private fun updateUnSeenMessagesCount(count: Int) {
+        emitViewState {
+            val notSeenCount = chatState.messagesNotSeen
+            chatState.messagesNotSeenChanged(if (chatState.isChatInBottom) 0 else notSeenCount + count)
+        }
+    }
+
+    private fun onHistoryLoaded(hasHistory: Boolean) {
+        Logger.d(TAG, "historyLoaded")
+
+        if (!hasHistory) {
+            if (!chatState.engagementRequested && !isSecureEngagement) {
+                queueForEngagement()
+            } else {
+                Logger.d(TAG, "Opened empty Secure Conversations chat")
+            }
+        }
+
+        emitViewState { chatState.historyLoaded() }
+        initGliaEngagementObserving()
+    }
+
+    private fun emitItems(items: List) {
+        viewCallback?.emitItems(items)
     }
 
     override fun engagementEnded() {
@@ -1553,7 +861,7 @@ internal class ChatController(
     private fun onNewOperatorMediaState(operatorMediaState: OperatorMediaState?) {
         Logger.d(TAG, "newOperatorMediaState: $operatorMediaState")
         if (chatState.isAudioCallStarted && operatorMediaState?.video != null) {
-            upgradeMediaItem()
+            upgradeMediaItemToVideo()
         } else if (!chatState.isMediaUpgradeStarted) {
             addMediaUpgradeItemToChatItems(operatorMediaState)
             if (!callTimer.isRunning) {
@@ -1565,28 +873,17 @@ internal class ChatController(
     }
 
     private fun addMediaUpgradeItemToChatItems(operatorMediaState: OperatorMediaState?) {
-        val time = DateUtils.formatElapsedTime(0)
-
-        val item = when {
-            operatorMediaState?.video == null && operatorMediaState?.audio != null -> {
-                Logger.d(TAG, "starting audio timer")
-                MediaUpgradeStartedTimerItem.Audio(time)
-            }
-
-            operatorMediaState?.video != null -> {
-                Logger.d(TAG, "starting video timer")
-                MediaUpgradeStartedTimerItem.Video(time)
-            }
-
+        val isVideo = when {
+            operatorMediaState?.video == null && operatorMediaState?.audio != null -> false
+            operatorMediaState?.video != null -> true
             else -> null
         } ?: return
 
-        emitChatItems {
-            return@emitChatItems chatState.run { changeTimerItem(chatItems + item, item) }
-        }
+        emitViewState { chatState.upgradeMedia(isVideo) }
+        chatManager.onChatAction(ChatManager.Action.OnMediaUpgradeStarted(isVideo))
     }
 
-    fun notificationsDialogDismissed() {
+    fun notificationDialogDismissed() {
         dialogController.dismissCurrentDialog()
     }
 
@@ -1652,9 +949,7 @@ internal class ChatController(
             downloadFileUseCase(attachmentFile)
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
-                .subscribe({ fileDownloadSuccess(attachmentFile) }) {
-                    fileDownloadError(attachmentFile, it)
-                }
+                .subscribe({ fileDownloadSuccess(attachmentFile) }) { fileDownloadError(attachmentFile, it) }
         )
     }
 
@@ -1667,24 +962,19 @@ internal class ChatController(
     }
 
     private fun updateAllowFileSendState() {
-        siteInfoUseCase.execute { siteInfo: SiteInfo?, _ ->
-            onSiteInfoReceived(siteInfo)
-        }
+        siteInfoUseCase.execute { siteInfo: SiteInfo?, _ -> onSiteInfoReceived(siteInfo) }
     }
 
     private fun onSiteInfoReceived(siteInfo: SiteInfo?) {
         emitViewState {
-            chatState.allowSendAttachmentStateChanged(
-                siteInfo == null || siteInfo.allowedFileSenders.isVisitorAllowed
-            )
+            chatState.allowSendAttachmentStateChanged(siteInfo == null || siteInfo.allowedFileSenders.isVisitorAllowed)
         }
     }
 
     private fun observeQueueTicketState() {
         Logger.d(TAG, "observeQueueTicketState")
         disposable.add(
-            ticketStateChangeToUnstaffedUseCase
-                .execute()
+            ticketStateChangeToUnstaffedUseCase.execute()
                 .subscribe({ dialogController.showNoMoreOperatorsAvailableDialog() }) {
                     Logger.e(TAG, "Error happened while observing queue state : $it")
                 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendHistoryChatItemUseCases.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendHistoryChatItemUseCases.kt
new file mode 100644
index 000000000..a9b9ab8c2
--- /dev/null
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendHistoryChatItemUseCases.kt
@@ -0,0 +1,177 @@
+package com.glia.widgets.chat.domain
+
+import androidx.annotation.VisibleForTesting
+import com.glia.androidsdk.chat.FilesAttachment
+import com.glia.androidsdk.chat.OperatorMessage
+import com.glia.androidsdk.chat.SingleChoiceAttachment
+import com.glia.androidsdk.chat.SystemMessage
+import com.glia.androidsdk.chat.VisitorMessage
+import com.glia.widgets.chat.domain.gva.IsGvaUseCase
+import com.glia.widgets.chat.domain.gva.MapGvaUseCase
+import com.glia.widgets.chat.model.ChatItem
+import com.glia.widgets.chat.model.CustomCardChatItem
+import com.glia.widgets.chat.model.OperatorStatusItem
+import com.glia.widgets.chat.model.SystemChatItem
+import com.glia.widgets.chat.model.VisitorMessageItem
+import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
+import com.glia.widgets.helper.Logger
+import com.glia.widgets.helper.TAG
+import com.glia.widgets.helper.asSingleChoice
+
+
+internal class AppendHistoryChatMessageUseCase(
+    private val appendHistoryVisitorChatItemUseCase: AppendHistoryVisitorChatItemUseCase,
+    private val appendHistoryOperatorChatItemUseCase: AppendHistoryOperatorChatItemUseCase,
+    private val appendSystemMessageItemUseCase: AppendSystemMessageItemUseCase
+) {
+    @VisibleForTesting
+    var operatorId: String? = null
+
+    @VisibleForTesting
+    fun resetOperatorId() {
+        operatorId = null
+    }
+
+    @VisibleForTesting
+    fun shouldShowChatHead(chatMessageInternal: ChatMessageInternal): Boolean {
+        if (operatorId != chatMessageInternal.operatorId) {
+            operatorId = chatMessageInternal.operatorId
+            return true
+        }
+
+        return false
+    }
+
+    operator fun invoke(chatItems: MutableList, chatMessageInternal: ChatMessageInternal, isLatest: Boolean) {
+        when (val message = chatMessageInternal.chatMessage) {
+            is VisitorMessage -> {
+                resetOperatorId()
+                appendHistoryVisitorChatItemUseCase(chatItems, message)
+            }
+
+            is OperatorMessage -> appendHistoryOperatorChatItemUseCase(
+                chatItems,
+                chatMessageInternal,
+                isLatest,
+                shouldShowChatHead(chatMessageInternal)
+            )
+
+            is SystemMessage -> {
+                resetOperatorId()
+                appendSystemMessageItemUseCase(chatItems, message)
+            }
+
+            else -> Logger.d(TAG, "Unexpected type of message received -> $message")
+        }
+    }
+}
+
+internal class AppendHistoryOperatorChatItemUseCase(
+    private val isGvaUseCase: IsGvaUseCase,
+    private val customCardAdapterTypeUseCase: CustomCardAdapterTypeUseCase,
+    private val appendGvaMessageItemUseCase: AppendGvaMessageItemUseCase,
+    private val appendHistoryCustomCardItemUseCase: AppendHistoryCustomCardItemUseCase,
+    private val appendHistoryResponseCardOrTextItemUseCase: AppendHistoryResponseCardOrTextItemUseCase
+) {
+    operator fun invoke(chatItems: MutableList, chatMessageInternal: ChatMessageInternal, isLatest: Boolean, showChatHead: Boolean) {
+        val message: OperatorMessage = chatMessageInternal.chatMessage as OperatorMessage
+        when {
+            isGvaUseCase(message) -> appendGvaMessageItemUseCase(chatItems, chatMessageInternal, showChatHead)
+            customCardAdapterTypeUseCase(message) != null -> appendHistoryCustomCardItemUseCase(
+                chatItems,
+                message,
+                customCardAdapterTypeUseCase(message)!!
+            )
+
+            else -> appendHistoryResponseCardOrTextItemUseCase(chatItems, chatMessageInternal, isLatest, showChatHead)
+        }
+    }
+}
+
+internal class AppendHistoryVisitorChatItemUseCase(
+    private val mapVisitorAttachmentUseCase: MapVisitorAttachmentUseCase
+) {
+    operator fun invoke(chatItems: MutableList, message: VisitorMessage) {
+        message.apply {
+            (attachment as? FilesAttachment)?.files?.reversed()?.forEach {
+                chatItems += mapVisitorAttachmentUseCase(it, message)
+            }
+
+            if (content.isNotBlank()) {
+                chatItems += VisitorMessageItem.History(id, timestamp, content)
+            }
+        }
+
+    }
+}
+
+internal class AppendSystemMessageItemUseCase {
+    operator fun invoke(chatItems: MutableList, message: SystemMessage) {
+        val index = if (chatItems.lastOrNull() is OperatorStatusItem.InQueue) chatItems.lastIndex else chatItems.lastIndex + 1
+        chatItems.add(index, message.run { SystemChatItem(content, id, timestamp) })
+    }
+}
+
+internal class AppendGvaMessageItemUseCase(private val mapGvaUseCase: MapGvaUseCase) {
+    operator fun invoke(chatItems: MutableList, message: ChatMessageInternal, showChatHead: Boolean = true) {
+        chatItems += mapGvaUseCase(message, showChatHead)
+    }
+}
+
+internal class AppendHistoryCustomCardItemUseCase(
+    private val customCardTypeUseCase: CustomCardTypeUseCase,
+    private val customCardShouldShowUseCase: CustomCardShouldShowUseCase,
+) {
+    operator fun invoke(chatItems: MutableList, message: OperatorMessage, viewType: Int) {
+        val customCardType = customCardTypeUseCase.execute(viewType) ?: return
+        if (customCardShouldShowUseCase.execute(message, customCardType, true)) {
+            chatItems.add(message.run { CustomCardChatItem(message, viewType) })
+        }
+
+        message.attachment?.asSingleChoice()?.selectedOptionText?.takeIf {
+            it.isNotBlank()
+        }?.let {
+            VisitorMessageItem.History(message.id, message.timestamp, it)
+        }?.also {
+            chatItems.add(it)
+        }
+    }
+}
+
+internal class AppendHistoryResponseCardOrTextItemUseCase(
+    private val mapOperatorAttachmentUseCase: MapOperatorAttachmentUseCase,
+    private val mapOperatorPlainTextUseCase: MapOperatorPlainTextUseCase,
+    private val mapResponseCardUseCase: MapResponseCardUseCase
+) {
+    operator fun invoke(chatItems: MutableList, message: ChatMessageInternal, isLatest: Boolean, showChatHead: Boolean) {
+        val chatMessage = message.chatMessage
+        chatMessage.attachment?.asSingleChoice()?.takeIf {
+            it.options.isNotEmpty() && isLatest
+        }?.let { addResponseCard(chatItems, it, message, showChatHead) } ?: addPlainTextAndAttachments(chatItems, message, showChatHead)
+    }
+
+    @VisibleForTesting
+    fun addPlainTextAndAttachments(chatItems: MutableList, message: ChatMessageInternal, showChatHead: Boolean) {
+        val filesAttachment = message.chatMessage.attachment as? FilesAttachment
+
+        filesAttachment?.files?.apply {
+            for (index in indices.reversed()) {
+                chatItems += mapOperatorAttachmentUseCase(get(index), message, showChatHead && index == lastIndex)
+            }
+        }
+
+        if (message.chatMessage.content.isNotBlank()) {
+            chatItems += mapOperatorPlainTextUseCase(message, showChatHead && filesAttachment == null)
+        }
+    }
+
+    @VisibleForTesting
+    fun addResponseCard(
+        chatItems: MutableList,
+        attachment: SingleChoiceAttachment,
+        message: ChatMessageInternal,
+        showChatHead: Boolean
+    ) {
+        chatItems += mapResponseCardUseCase(attachment, message, showChatHead)
+    }
+}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt
new file mode 100644
index 000000000..fcae94743
--- /dev/null
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt
@@ -0,0 +1,192 @@
+package com.glia.widgets.chat.domain
+
+import androidx.annotation.VisibleForTesting
+import com.glia.androidsdk.chat.FilesAttachment
+import com.glia.androidsdk.chat.OperatorMessage
+import com.glia.androidsdk.chat.SingleChoiceAttachment
+import com.glia.androidsdk.chat.SystemMessage
+import com.glia.androidsdk.chat.VisitorMessage
+import com.glia.widgets.chat.ChatManager
+import com.glia.widgets.chat.domain.gva.IsGvaUseCase
+import com.glia.widgets.chat.model.ChatItem
+import com.glia.widgets.chat.model.OperatorChatItem
+import com.glia.widgets.chat.model.VisitorChatItem
+import com.glia.widgets.chat.model.VisitorMessageItem
+import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
+import com.glia.widgets.helper.Logger
+import com.glia.widgets.helper.TAG
+import com.glia.widgets.helper.asSingleChoice
+
+
+internal class AppendNewChatMessageUseCase(
+    private val appendNewOperatorMessageUseCase: AppendNewOperatorMessageUseCase,
+    private val appendNewVisitorMessageUseCase: AppendNewVisitorMessageUseCase,
+    private val appendSystemMessageItemUseCase: AppendSystemMessageItemUseCase
+) {
+    operator fun invoke(state: ChatManager.State, chatMessageInternal: ChatMessageInternal) {
+        when (val message = chatMessageInternal.chatMessage) {
+            is VisitorMessage -> {
+                appendNewVisitorMessageUseCase(state, chatMessageInternal)
+                state.resetOperator()
+            }
+
+            is OperatorMessage -> appendNewOperatorMessageUseCase(state, chatMessageInternal)
+
+            is SystemMessage -> {
+                appendSystemMessageItemUseCase(state.chatItems, message)
+                state.resetOperator()
+            }
+
+            else -> Logger.d(TAG, "Unexpected type of message received -> $message")
+        }
+    }
+}
+
+internal class AppendNewOperatorMessageUseCase(
+    private val isGvaUseCase: IsGvaUseCase,
+    private val customCardAdapterTypeUseCase: CustomCardAdapterTypeUseCase,
+    private val appendGvaMessageItemUseCase: AppendGvaMessageItemUseCase,
+    private val appendHistoryCustomCardItemUseCase: AppendHistoryCustomCardItemUseCase,
+    private val appendNewResponseCardOrTextItemUseCase: AppendNewResponseCardOrTextItemUseCase
+) {
+    operator fun invoke(state: ChatManager.State, chatMessageInternal: ChatMessageInternal) {
+        val itemsCount = state.chatItems.count()
+        val message: OperatorMessage = chatMessageInternal.chatMessage as OperatorMessage
+        when {
+            isGvaUseCase(message) -> appendGvaMessageItemUseCase(
+                state.chatItems,
+                chatMessageInternal
+            )
+
+            customCardAdapterTypeUseCase(message) != null -> appendHistoryCustomCardItemUseCase(
+                state.chatItems,
+                message,
+                customCardAdapterTypeUseCase(message)!!
+            )
+
+            else -> appendNewResponseCardOrTextItemUseCase(state.chatItems, chatMessageInternal)
+        }
+
+        state.apply { addedMessagesCount = chatItems.count() - itemsCount }
+
+
+        val lastMessageWithVisibleOperatorImage = state.lastMessageWithVisibleOperatorImage
+
+
+        val lastItem = state.chatItems.lastOrNull()
+
+        if (lastItem !is OperatorChatItem) {
+            state.resetOperator()
+            return
+        }
+
+        if (state.isOperatorChanged(lastItem) || lastMessageWithVisibleOperatorImage == null) {
+            return
+        }
+
+        val index = state.chatItems.indexOf(lastMessageWithVisibleOperatorImage)
+        if (index == -1) return
+        state.chatItems[index] = lastMessageWithVisibleOperatorImage.withShowChatHead(false)
+    }
+}
+
+internal class AppendNewResponseCardOrTextItemUseCase(
+    private val mapOperatorAttachmentUseCase: MapOperatorAttachmentUseCase,
+    private val mapOperatorPlainTextUseCase: MapOperatorPlainTextUseCase,
+    private val mapResponseCardUseCase: MapResponseCardUseCase
+) {
+    operator fun invoke(chatItems: MutableList, message: ChatMessageInternal) {
+        val chatMessage = message.chatMessage
+        chatMessage.attachment?.asSingleChoice()?.takeIf {
+            it.options.isNotEmpty()
+        }?.let { addResponseCard(chatItems, it, message) } ?: addPlainTextAndAttachments(chatItems, message)
+    }
+
+    @VisibleForTesting
+    fun addPlainTextAndAttachments(chatItems: MutableList, message: ChatMessageInternal) {
+        val filesAttachment = message.chatMessage.attachment as? FilesAttachment
+
+        if (message.chatMessage.content.isNotBlank()) {
+            chatItems += mapOperatorPlainTextUseCase(message, filesAttachment?.files.isNullOrEmpty())
+        }
+
+        filesAttachment?.files?.apply {
+            for (index in indices) {
+                chatItems += mapOperatorAttachmentUseCase(get(index), message, index == lastIndex)
+            }
+        }
+    }
+
+    @VisibleForTesting
+    fun addResponseCard(
+        chatItems: MutableList,
+        attachment: SingleChoiceAttachment,
+        message: ChatMessageInternal
+    ) {
+        chatItems += mapResponseCardUseCase(attachment, message, true)
+    }
+}
+
+internal class AppendNewVisitorMessageUseCase(
+    private val mapVisitorAttachmentUseCase: MapVisitorAttachmentUseCase
+) {
+
+    @VisibleForTesting
+    var lastDeliveredItem: VisitorChatItem? = null
+
+    @VisibleForTesting
+    fun addUnsentItem(state: ChatManager.State, message: VisitorMessage): Boolean {
+        if (state.unsentItems.isEmpty()) return false
+
+        val unsentMessage =
+            state.unsentItems.firstOrNull { it.message == message.content } ?: return false
+        state.unsentItems.remove(unsentMessage)
+
+        val index = state.chatItems.indexOf(unsentMessage)
+        if (index != -1) {
+            if (lastDeliveredItem != null) {
+                val lastDeliveredIndex = state.chatItems.indexOf(lastDeliveredItem!!)
+                state.chatItems[lastDeliveredIndex] = lastDeliveredItem!!.withDeliveredStatus(false)
+            }
+
+            state.chatItems[index] = unsentMessage.run {
+                lastDeliveredItem = VisitorMessageItem.Delivered(message.id, timestamp, this.message)
+                lastDeliveredItem!!
+            }
+
+            return true
+        }
+
+        return false
+    }
+
+    operator fun invoke(state: ChatManager.State, chatMessageInternal: ChatMessageInternal) {
+        val message = chatMessageInternal.chatMessage as VisitorMessage
+
+        if (!addUnsentItem(state, message)) {
+            message.apply {
+                val files = (attachment as? FilesAttachment)?.files
+                val hasFiles = !files.isNullOrEmpty()
+
+                if (content.isNotBlank()) {
+                    if (hasFiles) {
+                        state.chatItems += VisitorMessageItem.New(id, timestamp, content)
+                    } else {
+                        state.chatItems += VisitorMessageItem.Delivered(id, timestamp, content)
+                    }
+                }
+
+                files?.forEachIndexed { index, attachmentFile ->
+                    state.chatItems += mapVisitorAttachmentUseCase(attachmentFile, message, index == files.lastIndex)
+                }
+
+                if (lastDeliveredItem != null) {
+                    val index = state.chatItems.indexOf(lastDeliveredItem!!)
+                    state.chatItems[index] = lastDeliveredItem!!.withDeliveredStatus(false)
+                }
+
+                lastDeliveredItem = state.chatItems.last() as? VisitorChatItem
+            }
+        }
+    }
+}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaLoadHistoryUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaLoadHistoryUseCase.kt
index 0b3bff44f..20c9c755f 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaLoadHistoryUseCase.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaLoadHistoryUseCase.kt
@@ -33,18 +33,8 @@ internal class GliaLoadHistoryUseCase(
 
     private fun loadHistoryAndMapOperator(): Single> = loadHistory()
         .flatMapPublisher { Flowable.fromArray(*it) }
-        .concatMapSingle { mapOperatorUseCase(chatMessage = it, isHistory = true) }
+        .concatMapSingle { mapOperatorUseCase(chatMessage = it) }
         .toSortedList(Comparator.comparingLong { it.chatMessage.timestamp })
-        .map { markLastItem(it) }
-
-    private fun markLastItem(mutableList: MutableList): MutableList {
-        if (mutableList.isNotEmpty()) {
-            val lastItem = mutableList.removeLast()
-            mutableList.add(lastItem.copy(isLatest = true))
-        }
-
-        return mutableList
-    }
 
     private fun loadHistory() = Single.create { emitter ->
         loadHistory { messages, error ->
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.kt
index 23990e08d..e85ee7c28 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaOnMessageUseCase.kt
@@ -13,18 +13,14 @@ internal class GliaOnMessageUseCase(
 ) {
 
     private val observable = Observable.create { observer ->
-        val messageListener = Consumer { chatMessage ->
-            observer.onNext(chatMessage)
-        }
+        val messageListener = Consumer { observer.onNext(it) }
 
         messageRepository.listenForAllMessages(messageListener)
 
-        observer.setCancellable {
-            messageRepository.unregisterAllMessageListener(messageListener)
-        }
+        observer.setCancellable { messageRepository.unregisterAllMessageListener(messageListener) }
     }
-        .flatMapSingle { chatMessage: ChatMessage -> mapOperatorUseCase(chatMessage, isHistory = false, isLast = true) }
-        .doOnError { obj: Throwable -> obj.printStackTrace() }
+        .flatMapSingle { mapOperatorUseCase(it) }
+        .doOnError { it.printStackTrace() }
         .share()
 
     operator fun invoke(): Observable = observable
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt
index 9634b91c4..0262466a1 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt
@@ -1,9 +1,7 @@
 package com.glia.widgets.chat.domain
 
 import com.glia.androidsdk.GliaException
-import com.glia.androidsdk.chat.ChatMessage
 import com.glia.androidsdk.chat.FilesAttachment
-import com.glia.androidsdk.chat.OperatorMessage
 import com.glia.androidsdk.chat.SingleChoiceAttachment
 import com.glia.androidsdk.chat.VisitorMessage
 import com.glia.widgets.chat.data.GliaChatRepository
@@ -24,7 +22,6 @@ class GliaSendMessageUseCase(
 ) {
     interface Listener {
         fun messageSent(message: VisitorMessage?)
-        fun onCardMessageUpdated(message: ChatMessage)
         fun onMessageValidated()
         fun errorOperatorNotOnline(message: String)
         fun error(ex: GliaException)
@@ -93,36 +90,6 @@ class GliaSendMessageUseCase(
         chatRepository.sendMessageSingleChoice(singleChoiceAttachment, listener)
     }
 
-    fun execute(chatMessage: ChatMessage?, text: String, value: String, listener: Listener?) {
-        val attachment = SingleChoiceAttachment.from(value, text)
-        chatRepository.sendResponse(attachment) { result: VisitorMessage?, ex: GliaException? ->
-            listener?.let {
-                if (ex != null) {
-                    listener.error(ex)
-                }
-                if (result != null) {
-                    listener.messageSent(result)
-
-                    chatMessage?.let {
-                        it as? OperatorMessage
-                    }?.let {
-                        listener.onCardMessageUpdated(
-                            ChatMessage(
-                                it.id,
-                                it.content,
-                                it.timestamp,
-                                ChatMessage.Sender(it.senderType, it.operatorHref, it.operatorId),
-                                it.deliveredAt,
-                                attachment,
-                                it.metadata
-                            )
-                        )
-                    }
-                }
-            }
-        }
-    }
-
     private val isOperatorOnline: Boolean
         get() = engagementStateRepository.isOperatorPresent
 
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/HandleCustomCardClickUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/HandleCustomCardClickUseCase.kt
new file mode 100644
index 000000000..6bd9bd7bc
--- /dev/null
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/HandleCustomCardClickUseCase.kt
@@ -0,0 +1,48 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.ChatMessage
+import com.glia.androidsdk.chat.OperatorMessage
+import com.glia.androidsdk.chat.SingleChoiceAttachment
+import com.glia.widgets.chat.ChatManager
+import com.glia.widgets.chat.model.CustomCardChatItem
+
+internal class HandleCustomCardClickUseCase(
+    private val customCardTypeUseCase: CustomCardTypeUseCase,
+    private val customCardShouldShowUseCase: CustomCardShouldShowUseCase
+) {
+    operator fun invoke(
+        customCard: CustomCardChatItem,
+        attachment: SingleChoiceAttachment,
+        state: ChatManager.State
+    ): ChatManager.State {
+        val customCardType = customCardTypeUseCase.execute(customCard.viewType) ?: return state
+        val currentMessage = customCard.message
+        val showCustomCard = customCardShouldShowUseCase.execute(
+            currentMessage,
+            customCardType,
+            false
+        )
+        if (!showCustomCard) {
+            state.chatItems.remove(customCard)
+        } else {
+            val index = state.chatItems.indexOf(customCard)
+            val message = (currentMessage as? OperatorMessage)
+            if (index != -1 && message != null) {
+                val newMessage = message.let {
+                    ChatMessage(
+                        it.id,
+                        it.content,
+                        it.timestamp,
+                        ChatMessage.Sender(it.senderType, it.operatorHref, it.operatorId),
+                        it.deliveredAt,
+                        attachment,
+                        it.metadata
+                    )
+                }
+
+                state.chatItems[index] = CustomCardChatItem(newMessage, customCard.viewType)
+            }
+        }
+        return state
+    }
+}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsEnableChatEditTextUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsEnableChatEditTextUseCase.kt
deleted file mode 100644
index c0c67ea76..000000000
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsEnableChatEditTextUseCase.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.glia.widgets.chat.domain
-
-import com.glia.widgets.chat.adapter.ChatAdapter
-import com.glia.widgets.chat.model.ChatItem
-import com.glia.widgets.chat.model.OperatorMessageItem
-
-internal class IsEnableChatEditTextUseCase {
-
-    // should enable only if there is no unselected choice-card last
-    operator fun invoke(items: List?): Boolean = items?.lastOrNull()?.let {
-        it.viewType != ChatAdapter.OPERATOR_MESSAGE_VIEW_TYPE || it !is OperatorMessageItem.ResponseCard
-    } ?: true
-}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/MapChatItemUseCases.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/MapChatItemUseCases.kt
new file mode 100644
index 000000000..6d2082835
--- /dev/null
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/MapChatItemUseCases.kt
@@ -0,0 +1,77 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.AttachmentFile
+import com.glia.androidsdk.chat.SingleChoiceAttachment
+import com.glia.androidsdk.chat.VisitorMessage
+import com.glia.widgets.chat.model.OperatorAttachmentItem
+import com.glia.widgets.chat.model.OperatorMessageItem
+import com.glia.widgets.chat.model.VisitorAttachmentItem
+import com.glia.widgets.chat.model.VisitorChatItem
+import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
+import com.glia.widgets.helper.isImage
+import kotlin.jvm.optionals.getOrNull
+
+internal class MapOperatorAttachmentUseCase {
+    operator fun invoke(attachment: AttachmentFile, chatMessageInternal: ChatMessageInternal, showChatHead: Boolean) = chatMessageInternal.run {
+        if (attachment.isImage) {
+            OperatorAttachmentItem.Image(
+                attachmentFile = attachment,
+                id = chatMessage.id,
+                timestamp = chatMessage.timestamp,
+                showChatHead = showChatHead,
+                operatorProfileImgUrl = operatorImageUrl,
+                operatorId = operatorId
+            )
+        } else {
+            OperatorAttachmentItem.File(
+                attachmentFile = attachment,
+                id = chatMessage.id,
+                timestamp = chatMessage.timestamp,
+                showChatHead = showChatHead,
+                operatorProfileImgUrl = operatorImageUrl,
+                operatorId = operatorId
+            )
+        }
+    }
+}
+
+internal class MapVisitorAttachmentUseCase {
+    operator fun invoke(attachmentFile: AttachmentFile, message: VisitorMessage, showDelivered: Boolean = false): VisitorChatItem = message.run {
+        if (attachmentFile.isImage) {
+            VisitorAttachmentItem.Image(id, timestamp, attachmentFile, showDelivered = showDelivered)
+        } else {
+            VisitorAttachmentItem.File(id, timestamp, attachmentFile, showDelivered = showDelivered)
+        }
+    }
+}
+
+internal class MapOperatorPlainTextUseCase {
+    operator fun invoke(chatMessageInternal: ChatMessageInternal, showChatHead: Boolean): OperatorMessageItem = chatMessageInternal.run {
+        OperatorMessageItem.PlainText(
+            chatMessage.id,
+            chatMessage.timestamp,
+            showChatHead,
+            operatorImageUrl,
+            operatorId,
+            operatorName,
+            chatMessage.content
+        )
+    }
+}
+
+internal class MapResponseCardUseCase {
+    operator fun invoke(attachment: SingleChoiceAttachment, message: ChatMessageInternal, showChatHead: Boolean): OperatorMessageItem.ResponseCard =
+        message.run {
+            OperatorMessageItem.ResponseCard(
+                chatMessage.id,
+                chatMessage.timestamp,
+                showChatHead,
+                operatorImageUrl,
+                operatorId,
+                operatorName,
+                chatMessage.content,
+                attachment.options.asList(),
+                attachment.imageUrl.getOrNull()
+            )
+        }
+}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt
new file mode 100644
index 000000000..08fbb189a
--- /dev/null
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt
@@ -0,0 +1,14 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.widgets.chat.data.GliaChatRepository
+
+
+internal class SendUnsentMessagesUseCase(private val chatRepository: GliaChatRepository) {
+    operator fun invoke(message: String, onSuccess: () -> Unit) {
+        chatRepository.sendMessage(message) { response, exception ->
+            if (exception == null && response != null) {
+                onSuccess()
+            }
+        }
+    }
+}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCase.kt
index 14746b8d7..d7393d2a1 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCase.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCase.kt
@@ -1,25 +1,23 @@
 package com.glia.widgets.chat.domain.gva
 
-import com.glia.widgets.chat.model.ChatState
 import com.glia.widgets.chat.model.GvaGalleryCards
 import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
-import java.util.UUID
 
 internal class MapGvaGvaGalleryCardsUseCase(
     private val parseGvaGalleryCardsUseCase: ParseGvaGalleryCardsUseCase
 ) {
-    operator fun invoke(chatMessage: ChatMessageInternal, chatState: ChatState): GvaGalleryCards {
+    operator fun invoke(chatMessage: ChatMessageInternal, showChatHead: Boolean): GvaGalleryCards {
         val message = chatMessage.chatMessage
         val metadata = message.metadata
 
         return GvaGalleryCards(
             id = message.id,
             galleryCards = parseGvaGalleryCardsUseCase(metadata),
-            showChatHead = false,
-            operatorId = chatMessage.operatorId ?: UUID.randomUUID().toString(),
+            showChatHead = showChatHead,
+            operatorId = chatMessage.operatorId,
             timestamp = message.timestamp,
-            operatorProfileImgUrl = chatMessage.operatorImageUrl ?: chatState.operatorProfileImgUrl,
-            operatorName = chatMessage.operatorName ?: chatState.formattedOperatorName
+            operatorProfileImgUrl = chatMessage.operatorImageUrl,
+            operatorName = chatMessage.operatorName
         )
     }
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCase.kt
deleted file mode 100644
index 6d3ecb476..000000000
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCase.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.glia.widgets.chat.domain.gva
-
-import com.glia.widgets.chat.model.ChatState
-import com.glia.widgets.chat.model.GvaChatItem
-import com.glia.widgets.chat.model.GvaQuickReplies
-import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
-
-internal class MapGvaGvaQuickRepliesUseCase(
-    private val parseGvaButtonsUseCase: ParseGvaButtonsUseCase,
-    private val mapGvaResponseTextUseCase: MapGvaResponseTextUseCase
-) {
-    operator fun invoke(chatMessage: ChatMessageInternal, chatState: ChatState): GvaChatItem {
-        val message = chatMessage.chatMessage
-        val metadata = message.metadata
-
-        if (chatMessage.isHistory && !chatMessage.isLatest) {
-            return mapGvaResponseTextUseCase(chatMessage, chatState)
-        }
-
-        return GvaQuickReplies(
-            gvaResponseText = mapGvaResponseTextUseCase(chatMessage, chatState),
-            options = parseGvaButtonsUseCase(metadata),
-        )
-    }
-}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCase.kt
index 6d142df62..7420574e3 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCase.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCase.kt
@@ -1,15 +1,13 @@
 package com.glia.widgets.chat.domain.gva
 
-import com.glia.widgets.chat.model.ChatState
 import com.glia.widgets.chat.model.Gva
 import com.glia.widgets.chat.model.GvaPersistentButtons
 import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
-import java.util.UUID
 
 internal class MapGvaPersistentButtonsUseCase(
     private val parseGvaButtonsUseCase: ParseGvaButtonsUseCase
 ) {
-    operator fun invoke(chatMessage: ChatMessageInternal, chatState: ChatState): GvaPersistentButtons {
+    operator fun invoke(chatMessage: ChatMessageInternal, showChatHead: Boolean): GvaPersistentButtons {
         val message = chatMessage.chatMessage
         val metadata = message.metadata
 
@@ -17,11 +15,11 @@ internal class MapGvaPersistentButtonsUseCase(
             id = message.id,
             content = metadata?.optString(Gva.Keys.CONTENT).orEmpty(),
             options = parseGvaButtonsUseCase(metadata),
-            showChatHead = false,
-            operatorId = chatMessage.operatorId ?: UUID.randomUUID().toString(),
+            showChatHead = showChatHead,
+            operatorId = chatMessage.operatorId,
             timestamp = message.timestamp,
-            operatorProfileImgUrl = chatMessage.operatorImageUrl ?: chatState.operatorProfileImgUrl,
-            operatorName = chatMessage.operatorName ?: chatState.formattedOperatorName
+            operatorProfileImgUrl = chatMessage.operatorImageUrl,
+            operatorName = chatMessage.operatorName
         )
     }
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaQuickRepliesUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaQuickRepliesUseCase.kt
new file mode 100644
index 000000000..3fb4deb5e
--- /dev/null
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaQuickRepliesUseCase.kt
@@ -0,0 +1,21 @@
+package com.glia.widgets.chat.domain.gva
+
+import com.glia.widgets.chat.model.Gva
+import com.glia.widgets.chat.model.GvaQuickReplies
+import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
+
+internal class MapGvaQuickRepliesUseCase(private val parseGvaButtonsUseCase: ParseGvaButtonsUseCase) {
+    operator fun invoke(message: ChatMessageInternal, showChatHead: Boolean): GvaQuickReplies =
+        message.run {
+            GvaQuickReplies(
+                id = chatMessage.id,
+                content = chatMessage.metadata?.optString(Gva.Keys.CONTENT).orEmpty(),
+                showChatHead = showChatHead,
+                operatorId = operatorId,
+                timestamp = chatMessage.timestamp,
+                operatorProfileImgUrl = operatorImageUrl,
+                operatorName = operatorName,
+                options = parseGvaButtonsUseCase(chatMessage.metadata)
+            )
+        }
+}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCase.kt
index 127cfcbf9..026e56662 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCase.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCase.kt
@@ -1,23 +1,21 @@
 package com.glia.widgets.chat.domain.gva
 
-import com.glia.widgets.chat.model.ChatState
 import com.glia.widgets.chat.model.Gva
 import com.glia.widgets.chat.model.GvaResponseText
 import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
-import java.util.UUID
 
 internal class MapGvaResponseTextUseCase {
-    operator fun invoke(chatMessage: ChatMessageInternal, chatState: ChatState): GvaResponseText {
+    operator fun invoke(chatMessage: ChatMessageInternal, showChatHead: Boolean): GvaResponseText {
         val message = chatMessage.chatMessage
 
         return GvaResponseText(
             id = message.id,
             content = message.metadata?.optString(Gva.Keys.CONTENT).orEmpty(),
-            showChatHead = false,
-            operatorId = chatMessage.operatorId ?: UUID.randomUUID().toString(),
+            showChatHead = showChatHead,
+            operatorId = chatMessage.operatorId,
             timestamp = message.timestamp,
-            operatorProfileImgUrl = chatMessage.operatorImageUrl ?: chatState.operatorProfileImgUrl,
-            operatorName = chatMessage.operatorName ?: chatState.formattedOperatorName
+            operatorProfileImgUrl = chatMessage.operatorImageUrl,
+            operatorName = chatMessage.operatorName
         )
     }
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaUseCase.kt
index b96560542..c3686039c 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaUseCase.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/gva/MapGvaUseCase.kt
@@ -1,23 +1,22 @@
 package com.glia.widgets.chat.domain.gva
 
-import com.glia.widgets.chat.model.ChatState
 import com.glia.widgets.chat.model.Gva
-import com.glia.widgets.chat.model.GvaChatItem
+import com.glia.widgets.chat.model.OperatorChatItem
 import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
 
 internal class MapGvaUseCase(
     private val getGvaTypeUseCase: GetGvaTypeUseCase,
     private val mapGvaResponseTextUseCase: MapGvaResponseTextUseCase,
     private val mapGvaPersistentButtonsUseCase: MapGvaPersistentButtonsUseCase,
-    private val mapGvaGvaQuickRepliesUseCase: MapGvaGvaQuickRepliesUseCase,
+    private val mapGvaQuickRepliesUseCase: MapGvaQuickRepliesUseCase,
     private val mapGvaGvaGalleryCardsUseCase: MapGvaGvaGalleryCardsUseCase
 ) {
-    operator fun invoke(chatMessageInternal: ChatMessageInternal, chatState: ChatState): GvaChatItem =
+    operator fun invoke(chatMessageInternal: ChatMessageInternal, showChatHead: Boolean = true): OperatorChatItem =
         when (getGvaTypeUseCase(chatMessageInternal.chatMessage.metadata!!)) {
-            Gva.Type.PLAIN_TEXT -> mapGvaResponseTextUseCase(chatMessageInternal, chatState)
-            Gva.Type.PERSISTENT_BUTTONS -> mapGvaPersistentButtonsUseCase(chatMessageInternal, chatState)
-            Gva.Type.QUICK_REPLIES -> mapGvaGvaQuickRepliesUseCase(chatMessageInternal, chatState)
-            Gva.Type.GALLERY_CARDS -> mapGvaGvaGalleryCardsUseCase(chatMessageInternal, chatState)
+            Gva.Type.PLAIN_TEXT -> mapGvaResponseTextUseCase(chatMessageInternal, showChatHead)
+            Gva.Type.PERSISTENT_BUTTONS -> mapGvaPersistentButtonsUseCase(chatMessageInternal, showChatHead)
+            Gva.Type.QUICK_REPLIES -> mapGvaQuickRepliesUseCase(chatMessageInternal, showChatHead)
+            Gva.Type.GALLERY_CARDS -> mapGvaGvaGalleryCardsUseCase(chatMessageInternal, showChatHead)
             else -> throw IllegalArgumentException("metadata should contain on of the [${Gva.Type.values().joinToString { it.value }}] types")
         }
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt
index 0e9c3da8e..998261fc1 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt
@@ -1,6 +1,7 @@
 package com.glia.widgets.chat.model
 
 import android.content.Context
+import android.text.format.DateUtils
 import com.glia.androidsdk.chat.AttachmentFile
 import com.glia.androidsdk.chat.ChatMessage
 import com.glia.androidsdk.chat.SingleChoiceOption
@@ -100,6 +101,16 @@ internal sealed class OperatorMessageItem : OperatorChatItem(ChatAdapter.OPERATO
         override val content: String?
     ) : OperatorMessageItem() {
         override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead)
+
+        fun asPlainText(): PlainText = PlainText(
+            id = id,
+            timestamp = timestamp,
+            showChatHead = showChatHead,
+            operatorProfileImgUrl = operatorProfileImgUrl,
+            operatorId = operatorId,
+            operatorName = operatorName,
+            content = content
+        )
     }
 
     data class ResponseCard(
@@ -120,7 +131,7 @@ internal sealed class OperatorMessageItem : OperatorChatItem(ChatAdapter.OPERATO
 
         override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead)
 
-        fun toPlainText() = PlainText(
+        fun asPlainText() = PlainText(
             id = id,
             timestamp = timestamp,
             showChatHead = showChatHead,
@@ -141,11 +152,11 @@ internal sealed class MediaUpgradeStartedTimerItem : ChatItem(ChatAdapter.MEDIA_
 
     abstract fun updateTime(time: String): MediaUpgradeStartedTimerItem
 
-    data class Audio(override val time: String) : MediaUpgradeStartedTimerItem() {
+    data class Audio(override val time: String = DateUtils.formatElapsedTime(0)) : MediaUpgradeStartedTimerItem() {
         override fun updateTime(time: String) = copy(time = time)
     }
 
-    data class Video(override val time: String) : MediaUpgradeStartedTimerItem() {
+    data class Video(override val time: String = DateUtils.formatElapsedTime(0)) : MediaUpgradeStartedTimerItem() {
         override fun updateTime(time: String) = copy(time = time)
     }
 }
@@ -183,9 +194,12 @@ internal sealed class OperatorStatusItem : ChatItem(ChatAdapter.OPERATOR_STATUS_
 
 // Visitor
 
-internal sealed class VisitorAttachmentItem(@ChatAdapter.Type viewType: Int) : ChatItem(viewType), AttachmentItem {
+internal abstract class VisitorChatItem(@ChatAdapter.Type viewType: Int) : ChatItem(viewType) {
     abstract val showDelivered: Boolean
-    abstract fun withDeliveredStatus(delivered: Boolean): VisitorAttachmentItem
+    abstract fun withDeliveredStatus(delivered: Boolean): VisitorChatItem
+}
+
+internal sealed class VisitorAttachmentItem(@ChatAdapter.Type viewType: Int) : VisitorChatItem(viewType), AttachmentItem {
 
     data class Image(
         override val id: String,
@@ -195,7 +209,7 @@ internal sealed class VisitorAttachmentItem(@ChatAdapter.Type viewType: Int) : C
         override val isDownloading: Boolean = false,
         override val showDelivered: Boolean = false
     ) : VisitorAttachmentItem(ChatAdapter.VISITOR_IMAGE_VIEW_TYPE) {
-        override fun withDeliveredStatus(delivered: Boolean): VisitorAttachmentItem = copy(showDelivered = delivered)
+        override fun withDeliveredStatus(delivered: Boolean): VisitorChatItem = copy(showDelivered = delivered)
 
         override fun updateWith(isFileExists: Boolean, isDownloading: Boolean): ChatItem =
             copy(isFileExists = isFileExists, isDownloading = isDownloading)
@@ -209,19 +223,24 @@ internal sealed class VisitorAttachmentItem(@ChatAdapter.Type viewType: Int) : C
         override val isDownloading: Boolean = false,
         override val showDelivered: Boolean = false
     ) : VisitorAttachmentItem(ChatAdapter.VISITOR_FILE_VIEW_TYPE) {
-        override fun withDeliveredStatus(delivered: Boolean): VisitorAttachmentItem = copy(showDelivered = delivered)
+        override fun withDeliveredStatus(delivered: Boolean): VisitorChatItem = copy(showDelivered = delivered)
 
         override fun updateWith(isFileExists: Boolean, isDownloading: Boolean): ChatItem =
             copy(isFileExists = isFileExists, isDownloading = isDownloading)
     }
 }
 
-internal sealed class VisitorMessageItem : ChatItem(ChatAdapter.VISITOR_MESSAGE_TYPE) {
-    val showDelivered: Boolean
+internal sealed class VisitorMessageItem : VisitorChatItem(ChatAdapter.VISITOR_MESSAGE_TYPE) {
+    override val showDelivered: Boolean
         get() = this is Delivered
 
     abstract val message: String
 
+    override fun withDeliveredStatus(delivered: Boolean): VisitorChatItem {
+        check(!delivered) { "The method should be called only with false value, to hide delivered status" }
+        return New(id, timestamp, message)
+    }
+
     data class New(
         override val id: String,
         override val timestamp: Long,
@@ -235,8 +254,8 @@ internal sealed class VisitorMessageItem : ChatItem(ChatAdapter.VISITOR_MESSAGE_
     ) : VisitorMessageItem()
 
     data class Unsent(
-        override val id: String,
-        override val timestamp: Long,
+        override val id: String = "",
+        override val timestamp: Long = System.currentTimeMillis(),
         override val message: String
     ) : VisitorMessageItem()
 
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt
index 9db387e3f..8e7835839 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatState.kt
@@ -10,13 +10,11 @@ internal data class ChatState(
     val companyName: String? = null,
     val queueId: String? = null,
     val visitorContextAssetId: String? = null,
-    val mediaUpgradeStartedTimerItem: MediaUpgradeStartedTimerItem? = null,
-    val chatItems: List = emptyList(),
+    val isMediaUpgradeVide: Boolean? = null,
     val chatInputMode: ChatInputMode = ChatInputMode.ENABLED_NO_ENGAGEMENT,
     val lastTypedText: String = "",
     val engagementRequested: Boolean = false,
     val pendingNavigationType: String? = null,
-    val unsentMessages: List = emptyList(),
     val operatorStatusItem: OperatorStatusItem? = null,
     val showSendButton: Boolean = false,
     val isAttachmentButtonEnabled: Boolean = false,
@@ -29,10 +27,10 @@ internal data class ChatState(
 
     val isOperatorOnline: Boolean get() = formattedOperatorName != null
 
-    val isMediaUpgradeStarted: Boolean get() = mediaUpgradeStartedTimerItem != null
+    val isMediaUpgradeStarted: Boolean get() = isMediaUpgradeVide != null
 
     val isAudioCallStarted: Boolean
-        get() = isMediaUpgradeStarted && mediaUpgradeStartedTimerItem is MediaUpgradeStartedTimerItem.Audio
+        get() = isMediaUpgradeVide != true
 
     val showMessagesUnseenIndicator: Boolean get() = !isChatInBottom && messagesNotSeen > 0
 
@@ -49,12 +47,11 @@ internal data class ChatState(
         isAttachmentAllowed = true
     )
 
-    fun queueingStarted(operatorStatusItem: OperatorStatusItem?): ChatState = copy(
+    fun queueingStarted(): ChatState = copy(
         formattedOperatorName = null,
         operatorProfileImgUrl = null,
         chatInputMode = ChatInputMode.ENABLED,
         engagementRequested = true,
-        operatorStatusItem = operatorStatusItem
     )
 
     fun setSecureMessagingState(): ChatState = copy(
@@ -90,41 +87,26 @@ internal data class ChatState(
         formattedOperatorName = formattedOperatorName,
         operatorProfileImgUrl = operatorProfileImgUrl,
         chatInputMode = ChatInputMode.ENABLED,
+        isAttachmentButtonNeeded = true
     )
 
-    fun historyLoaded(chatItems: List): ChatState = copy(
+    fun historyLoaded(): ChatState = copy(
         chatInputMode = ChatInputMode.ENABLED_NO_ENGAGEMENT,
         isAttachmentButtonNeeded = false,
-        chatItems = chatItems
     )
 
-    fun changeItems(newItems: List): ChatState = copy(chatItems = newItems)
-
-    fun changeTimerItem(
-        newItems: List,
-        mediaUpgradeStartedTimerItem: MediaUpgradeStartedTimerItem?
-    ): ChatState = copy(
-        chatItems = newItems,
-        mediaUpgradeStartedTimerItem = mediaUpgradeStartedTimerItem
-    )
+    fun upgradeMedia(isVideo: Boolean?): ChatState = copy(isMediaUpgradeVide = isVideo)
 
     fun changeVisibility(isVisible: Boolean): ChatState = copy(isVisible = isVisible)
 
     fun setLastTypedText(text: String): ChatState = copy(lastTypedText = text)
 
-    fun chatInputModeChanged(chatInputMode: ChatInputMode): ChatState = copy(
-        chatInputMode = chatInputMode,
-        isAttachmentButtonNeeded = chatInputMode == ChatInputMode.ENABLED
-    )
-
     fun isInBottomChanged(isChatInBottom: Boolean): ChatState = copy(isChatInBottom = isChatInBottom)
 
     fun messagesNotSeenChanged(messagesNotSeen: Int): ChatState = copy(messagesNotSeen = messagesNotSeen)
 
     fun setPendingNavigationType(pendingNavigationType: String?): ChatState = copy(pendingNavigationType = pendingNavigationType)
 
-    fun changeUnsentMessages(unsentMessages: List): ChatState = copy(unsentMessages = unsentMessages)
-
     fun setShowSendButton(isShow: Boolean): ChatState = copy(showSendButton = isShow)
 
     fun setIsOperatorTyping(isOperatorTyping: Boolean): ChatState = copy(isOperatorTyping = isOperatorTyping)
@@ -136,6 +118,7 @@ internal data class ChatState(
         operatorProfileImgUrl = null,
         isVisible = false,
         integratorChatStarted = false,
-        isAttachmentButtonNeeded = false
+        isAttachmentButtonNeeded = false,
+        isMediaUpgradeVide = null
     )
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaChatItems.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaChatItems.kt
index ea24ae9fd..b00360a80 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaChatItems.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/GvaChatItems.kt
@@ -2,8 +2,6 @@ package com.glia.widgets.chat.model
 
 import com.glia.widgets.chat.adapter.ChatAdapter
 
-sealed interface GvaChatItem
-
 internal abstract class GvaOperatorChatItem(@ChatAdapter.Type viewType: Int) : OperatorChatItem(viewType) {
     abstract val operatorName: String?
 }
@@ -16,7 +14,7 @@ internal data class GvaResponseText(
     override val timestamp: Long = -1,
     override val operatorProfileImgUrl: String? = null,
     override val operatorName: String? = null
-) : GvaChatItem, GvaOperatorChatItem(ChatAdapter.GVA_RESPONSE_TEXT_TYPE) {
+) : GvaOperatorChatItem(ChatAdapter.GVA_RESPONSE_TEXT_TYPE) {
     override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead)
 }
 
@@ -29,7 +27,7 @@ internal data class GvaPersistentButtons(
     override val timestamp: Long = -1,
     override val operatorProfileImgUrl: String? = null,
     override val operatorName: String? = null
-) : GvaChatItem, GvaOperatorChatItem(ChatAdapter.GVA_PERSISTENT_BUTTONS_TYPE) {
+) : GvaOperatorChatItem(ChatAdapter.GVA_PERSISTENT_BUTTONS_TYPE) {
     override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead)
 }
 
@@ -41,11 +39,23 @@ internal data class GvaGalleryCards(
     override val timestamp: Long = -1,
     override val operatorProfileImgUrl: String? = null,
     override val operatorName: String? = null
-) : GvaChatItem, GvaOperatorChatItem(ChatAdapter.GVA_GALLERY_CARDS_TYPE) {
+) : GvaOperatorChatItem(ChatAdapter.GVA_GALLERY_CARDS_TYPE) {
     override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead)
 }
 
 internal data class GvaQuickReplies(
-    val gvaResponseText: GvaResponseText,
+    override val id: String = "",
+    val content: String = "",
+    override val showChatHead: Boolean = false,
+    override val operatorId: String? = "",
+    override val timestamp: Long = -1,
+    override val operatorProfileImgUrl: String? = null,
+    override val operatorName: String? = null,
     val options: List = listOf(),
-) : GvaChatItem
+) : GvaOperatorChatItem(ChatAdapter.GVA_QUICK_REPLIES_TYPE) {
+    override fun withShowChatHead(showChatHead: Boolean): OperatorChatItem = copy(showChatHead = showChatHead)
+
+    fun asResponseText(): GvaResponseText = GvaResponseText(
+        id, content, showChatHead, operatorId, timestamp, operatorProfileImgUrl, operatorName
+    )
+}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaEngagementStateRepository.java b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaEngagementStateRepository.java
index f6df6976f..68534450f 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaEngagementStateRepository.java
+++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaEngagementStateRepository.java
@@ -17,23 +17,21 @@
 public class GliaEngagementStateRepository {
     private final EngagementStateEventVisitor visitor = new EngagementStateEventVisitor.OperatorVisitor();
     private final BehaviorProcessor> operatorProcessor =
-            BehaviorProcessor.createDefault(Optional.empty());
+        BehaviorProcessor.createDefault(Optional.empty());
     private final Flowable operatorFlowable =
-            operatorProcessor
-                    .filter(Optional::isPresent)
-                    .map(Optional::get)
-                    .onBackpressureLatest();
+        operatorProcessor
+            .filter(Optional::isPresent)
+            .map(Optional::get)
+            .onBackpressureLatest();
 
     private final BehaviorProcessor> engagementStateProcessor = BehaviorProcessor.createDefault(Optional.empty());
 
     private final BehaviorProcessor engagementStateEventProcessor = BehaviorProcessor.createDefault(
-            new EngagementStateEvent.EngagementEndedEvent()
+        new EngagementStateEvent.EngagementEndedEvent()
     );
     private final Flowable engagementStateEventFlowable = engagementStateEventProcessor.onBackpressureLatest();
-
-    private CompositeDisposable disposable = new CompositeDisposable();
-
     private final GliaOperatorRepository operatorRepository;
+    private CompositeDisposable disposable = new CompositeDisposable();
 
     public GliaEngagementStateRepository(GliaOperatorRepository operatorRepository) {
         this.operatorRepository = operatorRepository;
@@ -42,14 +40,16 @@ public GliaEngagementStateRepository(GliaOperatorRepository operatorRepository)
     public void onEngagementStarted(Engagement engagement) {
         disposable = new CompositeDisposable();
         disposable.add(
-                engagementStateProcessor
-                        .onBackpressureLatest()
-                        .map(state -> mapToEngagementStateChangeEvent(state.orElse(null), getOperator()))
-                        .doOnNext(this::notifyEngagementStateEventUpdate)
-                        .doOnNext(this::updateOperatorOnEngagementStateChanged)
-                        .subscribe()
+            engagementStateProcessor
+                .onBackpressureLatest()
+                .map(state -> mapToEngagementStateChangeEvent(state.orElse(null), getOperator()))
+                .doOnNext(this::notifyEngagementStateEventUpdate)
+                .doOnNext(this::updateOperatorOnEngagementStateChanged)
+                .subscribe()
         );
-        engagement.on(Engagement.Events.STATE_UPDATE, this::notifyEngagementStateUpdate);
+        engagement.on(Engagement.Events.STATE_UPDATE, engagementState -> {
+            notifyEngagementStateUpdate(engagementState);
+        });
         engagement.on(Engagement.Events.END, () -> onEngagementEnded(engagement));
     }
 
@@ -75,7 +75,7 @@ public boolean isOperatorPresent() {
 
     private void notifyOperatorUpdate(Operator operator) {
         if (operator != null) {
-            operatorRepository.addOrUpdateOperator(operator);
+            operatorRepository.emit(operator);
         }
         operatorProcessor.onNext(Optional.ofNullable(operator));
     }
@@ -91,13 +91,13 @@ private void notifyEngagementStateEventUpdate(EngagementStateEvent engagementSta
     private @Nullable
     Operator getOperator() {
         return operatorProcessor
-                .getValue()
-                .orElse(null);
+            .getValue()
+            .orElse(null);
     }
 
     private EngagementStateEvent mapToEngagementStateChangeEvent(
-            EngagementState engagementState,
-            @Nullable Operator operator
+        EngagementState engagementState,
+        @Nullable Operator operator
     ) {
         if (engagementState == null) {
             return new EngagementStateEvent.EngagementEndedEvent();
diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.java b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.java
deleted file mode 100644
index c58b81f8f..000000000
--- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.glia.widgets.core.engagement;
-
-import androidx.annotation.NonNull;
-import androidx.collection.SimpleArrayMap;
-import androidx.core.util.Consumer;
-
-import com.glia.androidsdk.Operator;
-import com.glia.widgets.di.GliaCore;
-
-public class GliaOperatorRepository {
-    private final GliaCore gliaCore;
-
-    private final SimpleArrayMap cachedOperators = new SimpleArrayMap<>();
-
-    public GliaOperatorRepository(GliaCore gliaCore) {
-        this.gliaCore = gliaCore;
-    }
-
-    public void getOperatorById(@NonNull String operatorId, @NonNull Consumer callback) {
-        Operator cachedOperator = cachedOperators.get(operatorId);
-        if (cachedOperator != null) {
-            callback.accept(cachedOperator);
-            return;
-        }
-
-        gliaCore.getOperator(operatorId, (operator, error) -> {
-            if (operator != null) {
-                addOrUpdateOperator(operator);
-            }
-            callback.accept(operator);
-        });
-    }
-
-    public void addOrUpdateOperator(@NonNull Operator operator) {
-        cachedOperators.put(operator.getId(), operator);
-    }
-
-}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.kt
new file mode 100644
index 000000000..f7c5dafe2
--- /dev/null
+++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.kt
@@ -0,0 +1,40 @@
+package com.glia.widgets.core.engagement
+
+import androidx.annotation.VisibleForTesting
+import androidx.collection.SimpleArrayMap
+import androidx.core.util.Consumer
+import com.glia.androidsdk.Operator
+import com.glia.widgets.core.engagement.data.LocalOperator
+import com.glia.widgets.di.GliaCore
+import com.glia.widgets.helper.toLocal
+
+internal class GliaOperatorRepository(private val gliaCore: GliaCore) {
+    private val cachedOperators = SimpleArrayMap()
+    fun getOperatorById(operatorId: String, callback: Consumer) {
+        val cachedOperator = cachedOperators[operatorId]
+        if (cachedOperator != null) {
+            callback.accept(cachedOperator)
+            return
+        }
+        gliaCore.getOperator(operatorId) { operator: Operator?, _ ->
+            operator?.let { updateIfExists(it) }?.also { putOperator(it) }.also { callback.accept(it) }
+        }
+    }
+
+    fun emit(operator: Operator) = putOperator(updateIfExists(operator))
+
+    @VisibleForTesting
+    fun updateIfExists(operator: Operator): LocalOperator {
+        val newOperator = operator.toLocal()
+
+        val oldOperator = cachedOperators[operator.id] ?: return newOperator
+
+        return if (oldOperator.imageUrl != null) oldOperator else oldOperator.copy(imageUrl = newOperator.imageUrl)
+    }
+
+    @VisibleForTesting
+    fun putOperator(operator: LocalOperator) {
+        operator.apply { cachedOperators.put(id, this) }
+    }
+
+}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/data/LocalOperator.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/data/LocalOperator.kt
new file mode 100644
index 000000000..d5e515762
--- /dev/null
+++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/data/LocalOperator.kt
@@ -0,0 +1,3 @@
+package com.glia.widgets.core.engagement.data
+
+internal data class LocalOperator(val id: String, val name: String, val imageUrl: String?)
diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GetOperatorUseCase.java b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GetOperatorUseCase.java
index 9b3076069..dc63d30a5 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GetOperatorUseCase.java
+++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GetOperatorUseCase.java
@@ -2,8 +2,8 @@
 
 import androidx.annotation.NonNull;
 
-import com.glia.androidsdk.Operator;
 import com.glia.widgets.core.engagement.GliaOperatorRepository;
+import com.glia.widgets.core.engagement.data.LocalOperator;
 
 import java.util.Optional;
 
@@ -16,11 +16,11 @@ public GetOperatorUseCase(GliaOperatorRepository gliaOperatorRepository) {
         this.gliaOperatorRepository = gliaOperatorRepository;
     }
 
-    public Single> execute(@NonNull String operatorId) {
+    public Single> execute(@NonNull String operatorId) {
         return Single.create(emitter ->
-                gliaOperatorRepository.getOperatorById(operatorId, operator ->
-                        emitter.onSuccess(Optional.ofNullable(operator))
-                )
+            gliaOperatorRepository.getOperatorById(operatorId, operator ->
+                emitter.onSuccess(Optional.ofNullable(operator))
+            )
         );
     }
 
diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GliaOnEngagementUseCase.java b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GliaOnEngagementUseCase.java
index 82bc59b57..8be154da2 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GliaOnEngagementUseCase.java
+++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/GliaOnEngagementUseCase.java
@@ -3,6 +3,7 @@
 import com.glia.androidsdk.omnicore.OmnicoreEngagement;
 import com.glia.widgets.core.engagement.GliaEngagementRepository;
 import com.glia.widgets.core.engagement.GliaEngagementStateRepository;
+import com.glia.widgets.core.engagement.GliaOperatorRepository;
 import com.glia.widgets.core.operator.GliaOperatorMediaRepository;
 import com.glia.widgets.core.queue.GliaQueueRepository;
 import com.glia.widgets.core.visitor.GliaVisitorMediaRepository;
@@ -11,29 +12,28 @@
 
 public class GliaOnEngagementUseCase implements Consumer {
 
-    public interface Listener {
-        void newEngagementLoaded(OmnicoreEngagement engagement);
-    }
-
     private final GliaEngagementRepository gliaRepository;
     private final GliaOperatorMediaRepository operatorMediaRepository;
     private final GliaQueueRepository gliaQueueRepository;
     private final GliaVisitorMediaRepository gliaVisitorMediaRepository;
     private final GliaEngagementStateRepository gliaEngagementStateRepository;
+    private final GliaOperatorRepository operatorRepository;
     private Listener listener;
 
     public GliaOnEngagementUseCase(
-            GliaEngagementRepository gliaRepository,
-            GliaOperatorMediaRepository operatorMediaRepository,
-            GliaQueueRepository gliaQueueRepository,
-            GliaVisitorMediaRepository gliaVisitorMediaRepository,
-            GliaEngagementStateRepository gliaEngagementStateRepository
+        GliaEngagementRepository gliaRepository,
+        GliaOperatorMediaRepository operatorMediaRepository,
+        GliaQueueRepository gliaQueueRepository,
+        GliaVisitorMediaRepository gliaVisitorMediaRepository,
+        GliaEngagementStateRepository gliaEngagementStateRepository,
+        GliaOperatorRepository operatorRepository
     ) {
         this.gliaRepository = gliaRepository;
         this.operatorMediaRepository = operatorMediaRepository;
         this.gliaQueueRepository = gliaQueueRepository;
         this.gliaVisitorMediaRepository = gliaVisitorMediaRepository;
         this.gliaEngagementStateRepository = gliaEngagementStateRepository;
+        this.operatorRepository = operatorRepository;
     }
 
     public void execute(Listener listener) {
@@ -47,6 +47,7 @@ public void execute(Listener listener) {
 
     @Override
     public void accept(OmnicoreEngagement engagement) {
+        operatorRepository.emit(engagement.getState().getOperator());
         operatorMediaRepository.onEngagementStarted(engagement);
         gliaVisitorMediaRepository.onEngagementStarted(engagement);
         gliaEngagementStateRepository.onEngagementStarted(engagement);
@@ -62,4 +63,8 @@ public void unregisterListener(Listener listener) {
             this.listener = null;
         }
     }
+
+    public interface Listener {
+        void newEngagementLoaded(OmnicoreEngagement engagement);
+    }
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt
index 6fc33d1a3..db6de9bfe 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt
@@ -8,19 +8,15 @@ import io.reactivex.Single
 import kotlin.jvm.optionals.getOrNull
 
 internal class MapOperatorUseCase(private val getOperatorUseCase: GetOperatorUseCase) {
-    @JvmOverloads
-    operator fun invoke(chatMessage: ChatMessage, isHistory: Boolean = false, isLast: Boolean = false): Single =
+    operator fun invoke(chatMessage: ChatMessage): Single =
         when (chatMessage.senderType) {
-            Chat.Participant.OPERATOR -> processOperatorMessage(chatMessage as OperatorMessage, isHistory, isLast)
-            else -> processVisitorMessage(chatMessage, isHistory, isLast)
+            Chat.Participant.OPERATOR -> processOperatorMessage(chatMessage as OperatorMessage)
+            else -> processVisitorMessage(chatMessage)
         }
 
-    private fun processOperatorMessage(chatMessage: OperatorMessage, isHistory: Boolean, isLast: Boolean): Single =
-        getOperatorUseCase.execute(chatMessage.operatorId!!)
-            .map { ChatMessageInternal(chatMessage, isHistory, isLast, it.getOrNull()) }
+    private fun processOperatorMessage(chatMessage: OperatorMessage): Single =
+        getOperatorUseCase.execute(chatMessage.operatorId!!).map { ChatMessageInternal(chatMessage, it.getOrNull()) }
 
-    private fun processVisitorMessage(chatMessage: ChatMessage, isHistory: Boolean, isLast: Boolean): Single = Single.just(
-        ChatMessageInternal(chatMessage, isHistory, isLast)
-    )
+    private fun processVisitorMessage(chatMessage: ChatMessage): Single = Single.just(ChatMessageInternal(chatMessage))
 
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.kt
index 7fa84c01d..55aa0e97b 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/ChatMessageInternal.kt
@@ -1,18 +1,12 @@
 package com.glia.widgets.core.engagement.domain.model
 
-import com.glia.androidsdk.Operator
 import com.glia.androidsdk.chat.Chat
 import com.glia.androidsdk.chat.ChatMessage
-import kotlin.jvm.optionals.getOrNull
+import com.glia.widgets.core.engagement.data.LocalOperator
 
-internal data class ChatMessageInternal(
-    val chatMessage: ChatMessage,
-    val isHistory: Boolean = false,
-    val isLatest: Boolean = false,
-    val operator: Operator? = null
-) {
+internal data class ChatMessageInternal(val chatMessage: ChatMessage, val operator: LocalOperator? = null) {
     val operatorId: String? get() = operator?.id
     val operatorName: String? get() = operator?.name
-    val operatorImageUrl: String? get() = operator?.picture?.url?.getOrNull()
+    val operatorImageUrl: String? get() = operator?.imageUrl
     val isNotVisitor: Boolean get() = chatMessage.senderType != Chat.Participant.VISITOR
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/FileAttachmentRepository.kt b/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/FileAttachmentRepository.kt
index 38640f4bb..802bed81a 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/FileAttachmentRepository.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/FileAttachmentRepository.kt
@@ -11,7 +11,9 @@ import com.glia.widgets.core.engagement.exception.EngagementMissingException
 import com.glia.widgets.core.fileupload.domain.AddFileToAttachmentAndUploadUseCase
 import com.glia.widgets.core.fileupload.model.FileAttachment
 import com.glia.widgets.di.GliaCore
-import java.util.*
+import java.util.Observable
+import java.util.Observer
+import kotlin.jvm.optionals.getOrNull
 
 class FileAttachmentRepository(
     private val gliaCore: GliaCore,
@@ -45,7 +47,7 @@ class FileAttachmentRepository(
     }
 
     fun uploadFile(file: FileAttachment, listener: AddFileToAttachmentAndUploadUseCase.Listener) {
-        val engagement = gliaCore.currentEngagement.orElse(null)
+        val engagement = gliaCore.currentEngagement.getOrNull()
         if (engagement != null) {
             engagement.uploadFile(file.uri, handleFileUpload(file, listener))
         } else if (engagementConfigRepository.chatType == ChatType.SECURE_MESSAGING) {
diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/SecureFileAttachmentRepository.kt b/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/SecureFileAttachmentRepository.kt
index 52ed196a1..571c8735c 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/SecureFileAttachmentRepository.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/core/fileupload/SecureFileAttachmentRepository.kt
@@ -10,6 +10,7 @@ import com.glia.widgets.core.fileupload.model.FileAttachment
 import com.glia.widgets.di.GliaCore
 import io.reactivex.Observable
 import io.reactivex.subjects.BehaviorSubject
+import kotlin.jvm.optionals.getOrNull
 
 class SecureFileAttachmentRepository(private val gliaCore: GliaCore) {
     private val secureConversations: SecureConversations by lazy {
@@ -43,7 +44,7 @@ class SecureFileAttachmentRepository(private val gliaCore: GliaCore) {
     }
 
     fun uploadFile(file: FileAttachment, listener: AddFileToAttachmentAndUploadUseCase.Listener) {
-        val engagement = gliaCore.currentEngagement.orElse(null)
+        val engagement = gliaCore.currentEngagement.getOrNull()
         if (engagement != null) {
             engagement.uploadFile(file.uri, handleFileUpload(file, listener))
         } else {
diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/IsMessagingAvailableUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/IsMessagingAvailableUseCase.kt
index d6324446e..c26c8cdc0 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/IsMessagingAvailableUseCase.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/IsMessagingAvailableUseCase.kt
@@ -5,7 +5,7 @@ import com.glia.androidsdk.queuing.Queue
 import com.glia.androidsdk.queuing.QueueState
 import com.glia.widgets.core.queue.GliaQueueRepository
 import com.glia.widgets.helper.rx.Schedulers
-import com.glia.widgets.helper.supportsMessaging
+import com.glia.widgets.helper.supportMessaging
 import io.reactivex.Observable
 import io.reactivex.subjects.BehaviorSubject
 
@@ -36,5 +36,5 @@ internal class IsMessagingAvailableUseCase(
         .filter { queueIds.contains(it.id) }
         .filterNot { it.state.status == QueueState.Status.CLOSED }
         .filterNot { it.state.status == QueueState.Status.UNKNOWN }
-        .any { it.supportsMessaging() }
+        .any { it.supportMessaging() }
 }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java
index 5cd331a0d..a1de376df 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java
+++ b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java
@@ -47,6 +47,7 @@ public class ControllerFactory {
     private final GliaSdkConfigurationManager sdkConfigurationManager;
     private final FilePreviewController filePreviewController;
     private final ChatHeadPosition chatHeadPosition;
+    private final ManagerFactory managerFactory;
     private ChatController retainedChatController;
     private CallController retainedCallController;
     private ScreenSharingController retainedScreenSharingController;
@@ -58,7 +59,8 @@ public class ControllerFactory {
     public ControllerFactory(
         RepositoryFactory repositoryFactory,
         UseCaseFactory useCaseFactory,
-        GliaSdkConfigurationManager sdkConfigurationManager
+        GliaSdkConfigurationManager sdkConfigurationManager,
+        ManagerFactory managerFactory
     ) {
         this.repositoryFactory = repositoryFactory;
         messagesNotSeenHandler = new MessagesNotSeenHandler(
@@ -79,6 +81,7 @@ public ControllerFactory(
         );
         this.chatHeadPosition = ChatHeadPosition.getInstance();
         this.sdkConfigurationManager = sdkConfigurationManager;
+        this.managerFactory = managerFactory;
     }
 
     public ChatController getChatController(ChatViewCallback chatViewCallback) {
@@ -92,11 +95,9 @@ public ChatController getChatController(ChatViewCallback chatViewCallback) {
                 dialogController,
                 messagesNotSeenHandler,
                 useCaseFactory.createCallNotificationUseCase(),
-                useCaseFactory.createGliaLoadHistoryUseCase(),
                 useCaseFactory.createQueueForChatEngagementUseCase(),
                 useCaseFactory.createOnEngagementUseCase(),
                 useCaseFactory.createOnEngagementEndUseCase(),
-                useCaseFactory.createGliaOnMessageUseCase(),
                 useCaseFactory.createGliaOnOperatorTypingUseCase(),
                 useCaseFactory.createGliaSendMessagePreviewUseCase(),
                 useCaseFactory.createGliaSendMessageUseCase(),
@@ -112,15 +113,11 @@ public ChatController getChatController(ChatViewCallback chatViewCallback) {
                 useCaseFactory.createIsShowSendButtonUseCase(),
                 useCaseFactory.createIsShowOverlayPermissionRequestDialogUseCase(),
                 useCaseFactory.createDownloadFileUseCase(),
-                useCaseFactory.createIsEnableChatEditTextUseCase(),
                 useCaseFactory.createSiteInfoUseCase(),
                 useCaseFactory.getGliaSurveyUseCase(),
                 useCaseFactory.createGetGliaEngagementStateFlowableUseCase(),
                 useCaseFactory.createIsFromCallScreenUseCase(),
                 useCaseFactory.createUpdateFromCallScreenUseCase(),
-                useCaseFactory.createCustomCardAdapterTypeUseCase(),
-                useCaseFactory.createCustomCardTypeUseCase(),
-                useCaseFactory.createCustomCardShouldShowUseCase(),
                 useCaseFactory.createQueueTicketStateChangeToUnstaffedUseCase(),
                 useCaseFactory.createIsQueueingEngagementUseCase(),
                 useCaseFactory.createAddMediaUpgradeOfferCallbackUseCase(),
@@ -129,16 +126,14 @@ public ChatController getChatController(ChatViewCallback chatViewCallback) {
                 useCaseFactory.createIsOngoingEngagementUseCase(),
                 useCaseFactory.createSetEngagementConfigUseCase(),
                 useCaseFactory.createIsSecureConversationsChatAvailableUseCase(),
-                useCaseFactory.createMarkMessagesReadUseCase(),
                 useCaseFactory.createHasPendingSurveyUseCase(),
                 useCaseFactory.createSetPendingSurveyUsed(),
                 useCaseFactory.createIsCallVisualizerUseCase(),
-                useCaseFactory.createAddNewMessagesDividerUseCase(),
                 useCaseFactory.createIsFileReadyForPreviewUseCase(),
                 useCaseFactory.createAcceptMediaUpgradeOfferUseCase(),
-                useCaseFactory.createIsGvaUseCase(),
-                useCaseFactory.createMapGvaUseCase(),
-                useCaseFactory.createDetermineGvaButtonTypeUseCase()
+                useCaseFactory.createDetermineGvaButtonTypeUseCase(),
+                useCaseFactory.createIsAuthenticatedUseCase(),
+                managerFactory.getChatManager()
             );
         } else {
             Logger.d(TAG, "retained chat controller");
diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/Dependencies.java b/widgetssdk/src/main/java/com/glia/widgets/di/Dependencies.java
index dffbdcdd8..ea95f13d2 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/di/Dependencies.java
+++ b/widgetssdk/src/main/java/com/glia/widgets/di/Dependencies.java
@@ -1,7 +1,6 @@
 package com.glia.widgets.di;
 
 import android.app.Application;
-import android.content.Context;
 import android.os.Build;
 
 import androidx.annotation.NonNull;
@@ -19,40 +18,35 @@
 import com.glia.widgets.core.dialog.PermissionDialogManager;
 import com.glia.widgets.core.notification.device.INotificationManager;
 import com.glia.widgets.core.notification.device.NotificationManager;
-import com.glia.widgets.permissions.ActivityWatcherForPermissionsRequest;
 import com.glia.widgets.core.permissions.PermissionManager;
 import com.glia.widgets.filepreview.data.source.local.DownloadsFolderDataSource;
 import com.glia.widgets.helper.ApplicationLifecycleManager;
 import com.glia.widgets.helper.Logger;
 import com.glia.widgets.helper.ResourceProvider;
 import com.glia.widgets.helper.rx.GliaWidgetsSchedulers;
+import com.glia.widgets.permissions.ActivityWatcherForPermissionsRequest;
 import com.glia.widgets.view.head.ActivityWatcherForChatHead;
 import com.glia.widgets.view.head.controller.ServiceChatHeadController;
 import com.glia.widgets.view.unifiedui.theme.UnifiedThemeManager;
 
-import kotlin.jvm.functions.Function2;
-
 public class Dependencies {
 
     private final static String TAG = "Dependencies";
-
+    private static final UnifiedThemeManager UNIFIED_THEME_MANAGER = new UnifiedThemeManager();
     private static ControllerFactory controllerFactory;
     private static INotificationManager notificationManager;
     private static CallVisualizerManager callVisualizerManager;
     private static GliaSdkConfigurationManager sdkConfigurationManager =
             new GliaSdkConfigurationManager();
     private static UseCaseFactory useCaseFactory;
+    private static ManagerFactory managerFactory;
     private static GliaCore gliaCore = new GliaCoreImpl();
     private static ResourceProvider resourceProvider;
-    private static final UnifiedThemeManager UNIFIED_THEME_MANAGER = new UnifiedThemeManager();
 
     public static void onAppCreate(Application application) {
         notificationManager = new NotificationManager(application);
         DownloadsFolderDataSource downloadsFolderDataSource = new DownloadsFolderDataSource(application);
-        RepositoryFactory repositoryFactory = new RepositoryFactory(
-                gliaCore,
-                downloadsFolderDataSource
-        );
+        RepositoryFactory repositoryFactory = new RepositoryFactory(gliaCore, downloadsFolderDataSource);
 
         PermissionManager permissionManager = new PermissionManager(
                 application,
@@ -72,20 +66,12 @@ public static void onAppCreate(Application application) {
                 new GliaWidgetsSchedulers(),
                 gliaCore
         );
-        initAudioControlManager(
-                audioControlManager,
-                useCaseFactory.createOnAudioStartedUseCase()
-        );
+        initAudioControlManager(audioControlManager, useCaseFactory.createOnAudioStartedUseCase());
 
-        controllerFactory = new ControllerFactory(
-                repositoryFactory,
-                useCaseFactory,
-                sdkConfigurationManager
-        );
-        initApplicationLifecycleObserver(
-                new ApplicationLifecycleManager(),
-                controllerFactory.getChatHeadController()
-        );
+        managerFactory = new ManagerFactory(useCaseFactory);
+
+        controllerFactory = new ControllerFactory(repositoryFactory, useCaseFactory, sdkConfigurationManager, managerFactory);
+        initApplicationLifecycleObserver(new ApplicationLifecycleManager(), controllerFactory.getChatHeadController());
 
         ActivityWatcherForCallVisualizer activityWatcherForCallVisualizer =
                 new ActivityWatcherForCallVisualizer(
@@ -96,8 +82,7 @@ public static void onAppCreate(Application application) {
 
         ActivityWatcherForChatHead activityWatcherForChatHead =
                 new ActivityWatcherForChatHead(
-                        getControllerFactory().getActivityWatcherForChatHeadController()
-                );
+                        getControllerFactory().getActivityWatcherForChatHeadController());
         application.registerActivityLifecycleCallbacks(activityWatcherForChatHead);
 
         ActivityWatcherForPermissionsRequest activityWatcherForPermissionsRequest =
@@ -123,6 +108,11 @@ public static GliaSdkConfigurationManager getSdkConfigurationManager() {
         return sdkConfigurationManager;
     }
 
+    @VisibleForTesting
+    public static void setSdkConfigurationManager(@NonNull GliaSdkConfigurationManager sdkConfigurationManager) {
+        Dependencies.sdkConfigurationManager = sdkConfigurationManager;
+    }
+
     @NonNull
     public static UnifiedThemeManager getGliaThemeManager() {
         return UNIFIED_THEME_MANAGER;
@@ -141,6 +131,11 @@ public static ControllerFactory getControllerFactory() {
         return controllerFactory;
     }
 
+    @VisibleForTesting
+    public static void setControllerFactory(ControllerFactory controllerFactory) {
+        Dependencies.controllerFactory = controllerFactory;
+    }
+
     public static GliaCore glia() {
         return gliaCore;
     }
@@ -158,16 +153,6 @@ public static ResourceProvider getResourceProvider() {
         return resourceProvider;
     }
 
-    @VisibleForTesting
-    public static void setControllerFactory(ControllerFactory controllerFactory) {
-        Dependencies.controllerFactory = controllerFactory;
-    }
-
-    @VisibleForTesting
-    public static void setSdkConfigurationManager(@NonNull GliaSdkConfigurationManager sdkConfigurationManager) {
-        Dependencies.sdkConfigurationManager = sdkConfigurationManager;
-    }
-
     @VisibleForTesting
     public static void setResourceProvider(ResourceProvider resourceProvider) {
         Dependencies.resourceProvider = resourceProvider;
diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt b/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt
new file mode 100644
index 000000000..525db9a78
--- /dev/null
+++ b/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt
@@ -0,0 +1,19 @@
+package com.glia.widgets.di
+
+import com.glia.widgets.chat.ChatManager
+
+internal class ManagerFactory(private val useCaseFactory: UseCaseFactory) {
+    val chatManager: ChatManager
+        get() = useCaseFactory.run {
+            ChatManager(
+                onMessageUseCase = createGliaOnMessageUseCase(),
+                loadHistoryUseCase = createGliaLoadHistoryUseCase(),
+                addNewMessagesDividerUseCase = createAddNewMessagesDividerUseCase(),
+                markMessagesReadWithDelayUseCase = createMarkMessagesReadUseCase(),
+                appendHistoryChatMessageUseCase = createAppendHistoryChatMessageUseCase(),
+                appendNewChatMessageUseCase = createAppendNewChatMessageUseCase(),
+                sendUnsentMessagesUseCase = createSendUnsentMessagesUseCase(),
+                handleCustomCardClickUseCase = createHandleCustomCardClickUseCase()
+            )
+        }
+}
diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java
index 51a8c25b7..f2ecce4f6 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java
+++ b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java
@@ -9,6 +9,17 @@
 import com.glia.widgets.call.domain.ToggleVisitorVideoUseCase;
 import com.glia.widgets.callvisualizer.domain.IsCallOrChatScreenActiveUseCase;
 import com.glia.widgets.chat.domain.AddNewMessagesDividerUseCase;
+import com.glia.widgets.chat.domain.AppendGvaMessageItemUseCase;
+import com.glia.widgets.chat.domain.AppendHistoryChatMessageUseCase;
+import com.glia.widgets.chat.domain.AppendHistoryCustomCardItemUseCase;
+import com.glia.widgets.chat.domain.AppendHistoryOperatorChatItemUseCase;
+import com.glia.widgets.chat.domain.AppendHistoryResponseCardOrTextItemUseCase;
+import com.glia.widgets.chat.domain.AppendHistoryVisitorChatItemUseCase;
+import com.glia.widgets.chat.domain.AppendNewChatMessageUseCase;
+import com.glia.widgets.chat.domain.AppendNewOperatorMessageUseCase;
+import com.glia.widgets.chat.domain.AppendNewResponseCardOrTextItemUseCase;
+import com.glia.widgets.chat.domain.AppendNewVisitorMessageUseCase;
+import com.glia.widgets.chat.domain.AppendSystemMessageItemUseCase;
 import com.glia.widgets.chat.domain.CustomCardAdapterTypeUseCase;
 import com.glia.widgets.chat.domain.CustomCardShouldShowUseCase;
 import com.glia.widgets.chat.domain.CustomCardTypeUseCase;
@@ -19,11 +30,16 @@
 import com.glia.widgets.chat.domain.GliaOnOperatorTypingUseCase;
 import com.glia.widgets.chat.domain.GliaSendMessagePreviewUseCase;
 import com.glia.widgets.chat.domain.GliaSendMessageUseCase;
+import com.glia.widgets.chat.domain.HandleCustomCardClickUseCase;
 import com.glia.widgets.chat.domain.IsAuthenticatedUseCase;
-import com.glia.widgets.chat.domain.IsEnableChatEditTextUseCase;
 import com.glia.widgets.chat.domain.IsFromCallScreenUseCase;
 import com.glia.widgets.chat.domain.IsSecureConversationsChatAvailableUseCase;
 import com.glia.widgets.chat.domain.IsShowSendButtonUseCase;
+import com.glia.widgets.chat.domain.MapOperatorAttachmentUseCase;
+import com.glia.widgets.chat.domain.MapOperatorPlainTextUseCase;
+import com.glia.widgets.chat.domain.MapResponseCardUseCase;
+import com.glia.widgets.chat.domain.MapVisitorAttachmentUseCase;
+import com.glia.widgets.chat.domain.SendUnsentMessagesUseCase;
 import com.glia.widgets.chat.domain.SiteInfoUseCase;
 import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase;
 import com.glia.widgets.chat.domain.gva.DetermineGvaButtonTypeUseCase;
@@ -31,8 +47,8 @@
 import com.glia.widgets.chat.domain.gva.GetGvaTypeUseCase;
 import com.glia.widgets.chat.domain.gva.IsGvaUseCase;
 import com.glia.widgets.chat.domain.gva.MapGvaGvaGalleryCardsUseCase;
-import com.glia.widgets.chat.domain.gva.MapGvaGvaQuickRepliesUseCase;
 import com.glia.widgets.chat.domain.gva.MapGvaPersistentButtonsUseCase;
+import com.glia.widgets.chat.domain.gva.MapGvaQuickRepliesUseCase;
 import com.glia.widgets.chat.domain.gva.MapGvaResponseTextUseCase;
 import com.glia.widgets.chat.domain.gva.MapGvaUseCase;
 import com.glia.widgets.chat.domain.gva.ParseGvaButtonsUseCase;
@@ -166,6 +182,30 @@ public UseCaseFactory(RepositoryFactory repositoryFactory,
         this.gliaCore = gliaCore;
     }
 
+    @NonNull
+    private AppendHistoryResponseCardOrTextItemUseCase createAppendHistoryResponseCardOrTextItemUseCase() {
+        return new AppendHistoryResponseCardOrTextItemUseCase(
+            createMapOperatorAttachmentUseCase(),
+            createMapOperatorPlainTextUseCase(),
+            createMapResponseCardUseCase()
+        );
+    }
+
+    @NonNull
+    public MapResponseCardUseCase createMapResponseCardUseCase() {
+        return new MapResponseCardUseCase();
+    }
+
+    @NonNull
+    public MapOperatorAttachmentUseCase createMapOperatorAttachmentUseCase() {
+        return new MapOperatorAttachmentUseCase();
+    }
+
+    @NonNull
+    public MapOperatorPlainTextUseCase createMapOperatorPlainTextUseCase() {
+        return new MapOperatorPlainTextUseCase();
+    }
+
     @NonNull
     public ToggleChatHeadServiceUseCase getToggleChatHeadServiceUseCase() {
         if (toggleChatHeadServiceUseCase == null) {
@@ -299,7 +339,8 @@ public GliaOnEngagementUseCase createOnEngagementUseCase() {
             repositoryFactory.getGliaOperatorMediaRepository(),
             repositoryFactory.getGliaQueueRepository(),
             repositoryFactory.getGliaVisitorMediaRepository(),
-            repositoryFactory.getGliaEngagementStateRepository()
+            repositoryFactory.getGliaEngagementStateRepository(),
+            repositoryFactory.getOperatorRepository()
         );
     }
 
@@ -336,8 +377,8 @@ public SetPendingSurveyUsedUseCase createSetPendingSurveyUsed() {
     @NonNull
     public GliaOnMessageUseCase createGliaOnMessageUseCase() {
         return new GliaOnMessageUseCase(
-                repositoryFactory.getGliaMessageRepository(),
-                getMapOperatorUseCase()
+            repositoryFactory.getGliaMessageRepository(),
+            getMapOperatorUseCase()
         );
     }
 
@@ -490,11 +531,6 @@ public DownloadFileUseCase createDownloadFileUseCase() {
         return new DownloadFileUseCase(repositoryFactory.getGliaFileRepository());
     }
 
-    @NonNull
-    public IsEnableChatEditTextUseCase createIsEnableChatEditTextUseCase() {
-        return new IsEnableChatEditTextUseCase();
-    }
-
     @NonNull
     public OnNextMessageUseCase createOnNextMessageUseCase() {
         return new OnNextMessageUseCase(repositoryFactory.getSendMessageRepository());
@@ -863,8 +899,8 @@ public MapGvaPersistentButtonsUseCase createMapGvaPersistentButtonsUseCase() {
     }
 
     @NonNull
-    public MapGvaGvaQuickRepliesUseCase createMapGvaGvaQuickRepliesUseCase() {
-        return new MapGvaGvaQuickRepliesUseCase(createParseGvaButtonsUseCase(), createMapGvaResponseTextUseCase());
+    public MapGvaQuickRepliesUseCase createMapGvaGvaQuickRepliesUseCase() {
+        return new MapGvaQuickRepliesUseCase(createParseGvaButtonsUseCase());
     }
 
     @NonNull
@@ -893,6 +929,106 @@ public DetermineGvaButtonTypeUseCase createDetermineGvaButtonTypeUseCase() {
         return new DetermineGvaButtonTypeUseCase(createDetermineGvaUrlTypeUseCase());
     }
 
+    @NonNull
+    public HandleCustomCardClickUseCase createHandleCustomCardClickUseCase() {
+        return new HandleCustomCardClickUseCase(
+            createCustomCardTypeUseCase(),
+            createCustomCardShouldShowUseCase()
+        );
+    }
+
+    @NonNull
+    public AppendHistoryChatMessageUseCase createAppendHistoryChatMessageUseCase() {
+        return new AppendHistoryChatMessageUseCase(
+            createAppendHistoryVisitorChatItemUseCase(),
+            createAppendHistoryOperatorChatItemUseCase(),
+            createAppendSystemMessageItemUseCase()
+        );
+    }
+
+    @NonNull
+    public AppendSystemMessageItemUseCase createAppendSystemMessageItemUseCase() {
+        return new AppendSystemMessageItemUseCase();
+    }
+
+    @NonNull
+    public AppendHistoryVisitorChatItemUseCase createAppendHistoryVisitorChatItemUseCase() {
+        return new AppendHistoryVisitorChatItemUseCase(createMapVisitorAttachmentUseCase());
+    }
+
+    @NonNull
+    public MapVisitorAttachmentUseCase createMapVisitorAttachmentUseCase() {
+        return new MapVisitorAttachmentUseCase();
+    }
+
+    @NonNull
+    public AppendHistoryOperatorChatItemUseCase createAppendHistoryOperatorChatItemUseCase() {
+        return new AppendHistoryOperatorChatItemUseCase(
+            createIsGvaUseCase(),
+            createCustomCardAdapterTypeUseCase(),
+            createAppendHistoryGvaMessageItemUseCase(),
+            createAppendHistoryCustomCardItemUseCase(),
+            createAppendHistoryResponseCardOrTextItemUseCase()
+        );
+    }
+
+    @NonNull
+    public AppendHistoryCustomCardItemUseCase createAppendHistoryCustomCardItemUseCase() {
+        return new AppendHistoryCustomCardItemUseCase(
+            createCustomCardTypeUseCase(),
+            createCustomCardShouldShowUseCase()
+        );
+    }
+
+    @NonNull
+    public AppendGvaMessageItemUseCase createAppendHistoryGvaMessageItemUseCase() {
+        return new AppendGvaMessageItemUseCase(createMapGvaUseCase());
+    }
+
+    @NonNull
+    public AppendNewVisitorMessageUseCase createAppendNewVisitorMessageUseCase() {
+        return new AppendNewVisitorMessageUseCase(createMapVisitorAttachmentUseCase());
+    }
+
+    @NonNull
+    public AppendNewOperatorMessageUseCase createAppendNewOperatorMessageUseCase() {
+        return new AppendNewOperatorMessageUseCase(
+            createIsGvaUseCase(),
+            createCustomCardAdapterTypeUseCase(),
+            createAppendGvaMessageItemUseCase(),
+            createAppendHistoryCustomCardItemUseCase(),
+            createAppendNewResponseCardOrTextItemUseCase()
+        );
+    }
+
+    @NonNull
+    private AppendNewResponseCardOrTextItemUseCase createAppendNewResponseCardOrTextItemUseCase() {
+        return new AppendNewResponseCardOrTextItemUseCase(
+            createMapOperatorAttachmentUseCase(),
+            createMapOperatorPlainTextUseCase(),
+            createMapResponseCardUseCase()
+        );
+    }
+
+    @NonNull
+    public AppendGvaMessageItemUseCase createAppendGvaMessageItemUseCase() {
+        return new AppendGvaMessageItemUseCase(createMapGvaUseCase());
+    }
+
+    @NonNull
+    public AppendNewChatMessageUseCase createAppendNewChatMessageUseCase() {
+        return new AppendNewChatMessageUseCase(
+            createAppendNewOperatorMessageUseCase(),
+            createAppendNewVisitorMessageUseCase(),
+            createAppendSystemMessageItemUseCase()
+        );
+    }
+
+    @NonNull
+    public SendUnsentMessagesUseCase createSendUnsentMessagesUseCase() {
+        return new SendUnsentMessagesUseCase(repositoryFactory.getGliaMessageRepository());
+    }
+
     public void resetState() {
         createResetSurveyUseCase().invoke();
     }
diff --git a/widgetssdk/src/main/java/com/glia/widgets/filepreview/data/GliaFileRepositoryImpl.kt b/widgetssdk/src/main/java/com/glia/widgets/filepreview/data/GliaFileRepositoryImpl.kt
index cc2fff4ed..94f786cb0 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/filepreview/data/GliaFileRepositoryImpl.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/filepreview/data/GliaFileRepositoryImpl.kt
@@ -14,6 +14,7 @@ import com.glia.widgets.helper.fileName
 import io.reactivex.Completable
 import io.reactivex.Maybe
 import java.io.InputStream
+import kotlin.jvm.optionals.getOrNull
 
 internal class GliaFileRepositoryImpl(
     private val bitmapCache: InAppBitmapCache,
@@ -67,7 +68,7 @@ internal class GliaFileRepositoryImpl(
         }
 
     private fun fetchFile(attachmentFile: AttachmentFile, callback: RequestCallback) {
-        val engagement = gliaCore.currentEngagement.orElse(null)
+        val engagement = gliaCore.currentEngagement.getOrNull()
         if (engagement == null && engagementConfigRepository.chatType == ChatType.SECURE_MESSAGING) {
             secureConversations.fetchFile(attachmentFile.id, callback)
         } else {
diff --git a/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt b/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt
index d08f6ac14..94051c7ac 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt
@@ -10,41 +10,41 @@ import androidx.core.graphics.drawable.DrawableCompat
 import com.glia.androidsdk.Engagement
 import com.glia.androidsdk.Operator
 import com.glia.androidsdk.chat.AttachmentFile
+import com.glia.androidsdk.chat.MessageAttachment
+import com.glia.androidsdk.chat.SingleChoiceAttachment
 import com.glia.androidsdk.queuing.Queue
 import com.glia.widgets.UiTheme
+import com.glia.widgets.core.engagement.data.LocalOperator
 import com.glia.widgets.view.unifiedui.deepMerge
+import kotlin.jvm.optionals.getOrNull
 
 internal fun Drawable.setTintCompat(@ColorInt color: Int) = DrawableCompat.setTint(this, color)
 
 @ColorInt
-internal fun ColorStateList?.colorForState(state: IntArray): Int? =
-    this?.getColorForState(state, defaultColor)
+internal fun ColorStateList?.colorForState(state: IntArray): Int? = this?.getColorForState(state, defaultColor)
 
 // Common
-internal fun String.separateStringWithSymbol(symbol: String): String =
-    asSequence().joinToString(symbol)
+internal fun String.separateStringWithSymbol(symbol: String): String = asSequence().joinToString(symbol)
 
-internal fun Queue.supportsMessaging() = state.medias.contains(Engagement.MediaType.MESSAGING)
+internal fun Queue.supportMessaging() = state.medias.contains(Engagement.MediaType.MESSAGING)
 
-internal val Operator.imageUrl: String?
-    get() = picture?.url?.orElse(null)
+internal fun formatElapsedTime(elapsedMilliseconds: Long) = DateUtils.formatElapsedTime(elapsedMilliseconds / DateUtils.SECOND_IN_MILLIS)
 
-internal fun formatElapsedTime(elapsedMilliseconds: Long) =
-    DateUtils.formatElapsedTime(elapsedMilliseconds / DateUtils.SECOND_IN_MILLIS)
+internal val Operator.formattedName: String get() = name.substringBefore(' ')
 
-internal val Operator.formattedName: String
-    get() = name.substringBefore(' ')
+internal val Operator.imageUrl: String? get() = picture?.url?.getOrNull()
 
-internal fun UiTheme?.isAlertDialogButtonUseVerticalAlignment(): Boolean =
-    this?.gliaAlertDialogButtonUseVerticalAlignment ?: false
+internal fun Operator.toLocal(): LocalOperator = LocalOperator(id, name, imageUrl)
 
-internal fun UiTheme?.getFullHybridTheme(newTheme: UiTheme?): UiTheme =
-    deepMerge(newTheme) ?: UiTheme.UiThemeBuilder().build()
+internal fun UiTheme?.isAlertDialogButtonUseVerticalAlignment(): Boolean = this?.gliaAlertDialogButtonUseVerticalAlignment ?: false
+
+internal fun UiTheme?.getFullHybridTheme(newTheme: UiTheme?): UiTheme = deepMerge(newTheme) ?: UiTheme.UiThemeBuilder().build()
 
 /**
  * Returns styled text from the provided HTML string.
  */
 internal fun String.fromHtml(flags: Int = Html.FROM_HTML_MODE_COMPACT): Spanned = Html.fromHtml(this, flags)
 
-internal val AttachmentFile.isImage: Boolean
-    get() = contentType.startsWith("image")
+internal val AttachmentFile.isImage: Boolean get() = contentType.startsWith("image")
+
+internal fun MessageAttachment.asSingleChoice(): SingleChoiceAttachment? = this as? SingleChoiceAttachment
diff --git a/widgetssdk/src/main/java/com/glia/widgets/survey/viewholder/SingleQuestionViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/survey/viewholder/SingleQuestionViewHolder.kt
index a22180880..b38b67510 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/survey/viewholder/SingleQuestionViewHolder.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/survey/viewholder/SingleQuestionViewHolder.kt
@@ -21,6 +21,7 @@ import com.glia.widgets.view.configuration.survey.SurveyStyle
 import com.glia.widgets.view.unifiedui.applyTextTheme
 import com.glia.widgets.view.unifiedui.theme.survey.SurveySingleQuestionTheme
 import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
 
 class SingleQuestionViewHolder(
     private val binding: SurveySingleQuestionItemBinding,
@@ -50,7 +51,7 @@ class SingleQuestionViewHolder(
     private fun singleChoice(item: QuestionItem) {
         val selectedId = Optional.ofNullable(item.answer)
             .map { answer: Survey.Answer -> answer.getResponse() as String }
-            .orElse(null)
+            .getOrNull()
         val options = item.question.options ?: return
         radioGroup.removeAllViews()
         for (i in options.indices) {
diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/head/controller/ApplicationChatHeadLayoutController.kt b/widgetssdk/src/main/java/com/glia/widgets/view/head/controller/ApplicationChatHeadLayoutController.kt
index da4ece5c6..e881d35e1 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/view/head/controller/ApplicationChatHeadLayoutController.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/view/head/controller/ApplicationChatHeadLayoutController.kt
@@ -187,14 +187,11 @@ internal class ApplicationChatHeadLayoutController(
         state = State.ENGAGEMENT
         engagementDisposables.add(
             getOperatorFlowableUseCase.execute()
-                .subscribe(
-                    { operator: Operator -> operatorDataLoaded(operator) }
-                ) { throwable: Throwable ->
-                    Logger.e(TAG, "getOperatorFlowableUseCase error: " + throwable.message)
-                }
+                .subscribe({ operatorDataLoaded(it) }) { Logger.e(TAG, "getOperatorFlowableUseCase error: " + it.message) }
         )
         updateChatHeadView()
     }
+
     override fun newEngagementLoaded(engagement: OmnibrowseEngagement) {
         onNewEngagementLoaded()
     }
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerStateTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerStateTest.kt
new file mode 100644
index 000000000..1ef23a705
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerStateTest.kt
@@ -0,0 +1,95 @@
+package com.glia.widgets.chat
+
+import com.glia.androidsdk.chat.ChatMessage
+import com.glia.widgets.chat.model.ChatItem
+import com.glia.widgets.chat.model.OperatorChatItem
+import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
+import junit.framework.TestCase.assertTrue
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class ChatManagerStateTest {
+    private lateinit var state: ChatManager.State
+
+    @Before
+    fun setUp() {
+        state = ChatManager.State()
+    }
+
+    @After
+    fun tearDown() {
+    }
+
+    @Test(expected = UnsupportedOperationException::class)
+    fun `immutableChatItems returns immutableList`() {
+        state.chatItems.add(mock())
+        val immutableList = state.immutableChatItems as MutableList
+        immutableList.add(mock())
+    }
+
+    @Test
+    fun `isNew adds chatItemId to the ids when it is new`() {
+        state.chatItemIds.add("1")
+        state.chatItemIds.add("2")
+        state.chatItemIds.add("3")
+
+        val chatMessage: ChatMessage = mock()
+        whenever(chatMessage.id) doReturn "4"
+
+        val chatMessageInternal: ChatMessageInternal = mock()
+        whenever(chatMessageInternal.chatMessage) doReturn chatMessage
+
+        assertTrue(state.isNew(chatMessageInternal))
+        assertTrue(state.chatItemIds.count() == 4)
+        assertTrue(state.chatItemIds.contains("4"))
+    }
+
+    @Test
+    fun `isNew does not add chatItemId to the ids when it exists`() {
+        state.chatItemIds.add("1")
+        state.chatItemIds.add("2")
+        state.chatItemIds.add("3")
+
+        val chatMessage: ChatMessage = mock()
+        whenever(chatMessage.id) doReturn "3"
+
+        val chatMessageInternal: ChatMessageInternal = mock()
+        whenever(chatMessageInternal.chatMessage) doReturn chatMessage
+
+        assertFalse(state.isNew(chatMessageInternal))
+        assertTrue(state.chatItemIds.count() == 3)
+    }
+
+    @Test
+    fun `resetOperator resets lastMessageWithVisibleOperatorImage`() {
+        state.lastMessageWithVisibleOperatorImage = mock()
+        state.resetOperator()
+        assertNull(state.lastMessageWithVisibleOperatorImage)
+    }
+
+    @Test
+    fun `isOperatorChanged returns true when operator id differs from the existing one`() {
+        val operatorChatItem: OperatorChatItem = mock()
+        whenever(operatorChatItem.operatorId) doReturn "operator_id"
+        assertTrue(state.isOperatorChanged(operatorChatItem))
+        assertEquals(state.lastMessageWithVisibleOperatorImage, operatorChatItem)
+    }
+
+    @Test
+    fun `isOperatorChanged returns false when operator id is the same as existing one`() {
+        val operatorChatItem: OperatorChatItem = mock()
+        whenever(operatorChatItem.operatorId) doReturn "operator_id"
+
+        state.lastMessageWithVisibleOperatorImage = operatorChatItem
+
+        assertFalse(state.isOperatorChanged(operatorChatItem))
+        assertEquals(state.lastMessageWithVisibleOperatorImage, operatorChatItem)
+    }
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt
new file mode 100644
index 000000000..9f2c23beb
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt
@@ -0,0 +1,621 @@
+package com.glia.widgets.chat
+
+import com.glia.androidsdk.chat.ChatMessage
+import com.glia.androidsdk.chat.OperatorMessage
+import com.glia.androidsdk.chat.SystemMessage
+import com.glia.androidsdk.chat.VisitorMessage
+import com.glia.widgets.chat.domain.AddNewMessagesDividerUseCase
+import com.glia.widgets.chat.domain.AppendHistoryChatMessageUseCase
+import com.glia.widgets.chat.domain.AppendNewChatMessageUseCase
+import com.glia.widgets.chat.domain.GliaLoadHistoryUseCase
+import com.glia.widgets.chat.domain.GliaOnMessageUseCase
+import com.glia.widgets.chat.domain.HandleCustomCardClickUseCase
+import com.glia.widgets.chat.domain.SendUnsentMessagesUseCase
+import com.glia.widgets.chat.model.ChatItem
+import com.glia.widgets.chat.model.GvaButton
+import com.glia.widgets.chat.model.GvaQuickReplies
+import com.glia.widgets.chat.model.MediaUpgradeStartedTimerItem
+import com.glia.widgets.chat.model.NewMessagesDividerItem
+import com.glia.widgets.chat.model.OperatorMessageItem
+import com.glia.widgets.chat.model.OperatorStatusItem
+import com.glia.widgets.chat.model.VisitorMessageItem
+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
+import io.reactivex.Completable
+import io.reactivex.Observable
+import io.reactivex.Single
+import io.reactivex.android.plugins.RxAndroidPlugins
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.processors.BehaviorProcessor
+import io.reactivex.processors.PublishProcessor
+import io.reactivex.schedulers.Schedulers
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.robolectric.RobolectricTestRunner
+import java.util.UUID
+
+
+@RunWith(RobolectricTestRunner::class)
+class ChatManagerTest {
+    private lateinit var onMessageUseCase: GliaOnMessageUseCase
+    private lateinit var loadHistoryUseCase: GliaLoadHistoryUseCase
+    private lateinit var addNewMessagesDividerUseCase: AddNewMessagesDividerUseCase
+    private lateinit var markMessagesReadWithDelayUseCase: MarkMessagesReadWithDelayUseCase
+    private lateinit var appendHistoryChatMessageUseCase: AppendHistoryChatMessageUseCase
+    private lateinit var appendNewChatMessageUseCase: AppendNewChatMessageUseCase
+    private lateinit var sendUnsentMessagesUseCase: SendUnsentMessagesUseCase
+    private lateinit var handleCustomCardClickUseCase: HandleCustomCardClickUseCase
+    private lateinit var subjectUnderTest: ChatManager
+    private lateinit var state: ChatManager.State
+    private lateinit var compositeDisposable: CompositeDisposable
+    private lateinit var stateProcessor: BehaviorProcessor
+    private lateinit var quickReplies: BehaviorProcessor>
+    private lateinit var action: PublishProcessor
+
+    @Before
+    fun setUp() {
+        RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
+        onMessageUseCase = mock()
+        loadHistoryUseCase = mock()
+        addNewMessagesDividerUseCase = mock()
+        markMessagesReadWithDelayUseCase = mock()
+        appendHistoryChatMessageUseCase = mock()
+        appendNewChatMessageUseCase = mock()
+        sendUnsentMessagesUseCase = mock()
+        handleCustomCardClickUseCase = mock()
+        compositeDisposable = mock()
+        stateProcessor = spy(BehaviorProcessor.create())
+        quickReplies = BehaviorProcessor.create()
+        action = PublishProcessor.create()
+
+        subjectUnderTest = spy(
+            ChatManager(
+                onMessageUseCase,
+                loadHistoryUseCase,
+                addNewMessagesDividerUseCase,
+                markMessagesReadWithDelayUseCase,
+                appendHistoryChatMessageUseCase,
+                appendNewChatMessageUseCase,
+                sendUnsentMessagesUseCase,
+                handleCustomCardClickUseCase,
+                compositeDisposable,
+                stateProcessor,
+                quickReplies,
+                action
+            )
+        )
+
+        state = spy(ChatManager.State())
+    }
+    @Test
+    fun `removeNewMessagesDivider removes NewMessagesDivider when it contains in chat items`() {
+        state.chatItems.add(NewMessagesDividerItem)
+        subjectUnderTest.removeNewMessagesDivider(state)
+        assertTrue(state.chatItems.isEmpty())
+    }
+
+    @Test
+    fun `markMessagesReadWithDelay removes NewMessagesDivider when it contains in chat items`() {
+        state.chatItems.apply {
+            add(mock())
+            add(NewMessagesDividerItem)
+            add(mock())
+        }
+        stateProcessor.onNext(state)
+        whenever(markMessagesReadWithDelayUseCase()) doReturn Completable.complete()
+        assertTrue(stateProcessor.value!!.chatItems.contains(NewMessagesDividerItem))
+
+        subjectUnderTest.markMessagesReadWithDelay()
+
+        verify(subjectUnderTest).removeNewMessagesDivider(any())
+        assertFalse(stateProcessor.value!!.chatItems.contains(NewMessagesDividerItem))
+        verify(compositeDisposable).add(any())
+        verify(stateProcessor, times(2)).onNext(any())
+    }
+
+    @Test
+    fun `mapInQueue adds OperatorStatusItem_InQueue to chatItems and updates operatorStatusItem`() {
+        val companyName = "company Name"
+
+        val newState = subjectUnderTest.mapInQueue(companyName, state)
+        val lastItem = newState.chatItems.last() as OperatorStatusItem.InQueue
+
+        assertEquals(companyName, lastItem.companyName)
+        assertEquals(companyName, newState.operatorStatusItem!!.companyName)
+        assertEquals(lastItem, newState.operatorStatusItem)
+    }
+
+    @Test
+    fun `mapTransferring removes old OperatorStatusItem when it exists and adds new one`() {
+        subjectUnderTest.mapTransferring(subjectUnderTest.mapInQueue("name", state)).apply {
+            assertTrue(chatItems.count() == 1)
+            assertTrue(chatItems.contains(OperatorStatusItem.Transferring))
+        }
+    }
+
+    @Test
+    fun `mapOperatorConnected adds OperatorStatusItem_Connected when the old is null`() {
+        val action = ChatManager.Action.OperatorConnected("c_name", "o_name", "o_image")
+        subjectUnderTest.mapOperatorConnected(action, state)
+
+        verify(subjectUnderTest).checkUnsentMessages(any())
+        val newItem = state.chatItems.last() as OperatorStatusItem.Connected
+        assertEquals(newItem.operatorName, action.operatorFormattedName)
+        assertEquals(newItem.companyName, action.companyName)
+        assertEquals(newItem.profileImgUrl, action.operatorImageUrl)
+    }
+
+    @Test
+    fun `mapOperatorConnected replaces old OperatorStatusItem when the old is exists`() {
+        val action = ChatManager.Action.OperatorConnected("c_name", "o_name", "o_image")
+
+        state.apply {
+            operatorStatusItem = OperatorStatusItem.Transferring
+            chatItems.add(mock())
+            chatItems.add(mock())
+            chatItems.add(OperatorStatusItem.Transferring)
+        }
+
+        val newState = subjectUnderTest.mapOperatorConnected(action, state)
+
+        verify(subjectUnderTest).checkUnsentMessages(any())
+        val newItem = newState.chatItems.last() as OperatorStatusItem.Connected
+        assertEquals(newItem.operatorName, action.operatorFormattedName)
+        assertEquals(newItem.companyName, action.companyName)
+        assertEquals(newItem.profileImgUrl, action.operatorImageUrl)
+        assertFalse(newState.chatItems.contains(OperatorStatusItem.Transferring))
+    }
+
+    @Test
+    fun `addUnsentMessage adds Unsent message before OperatorStatusItem when chatItems contain OperatorStatusItem_InQueue`() {
+        val message: VisitorMessageItem.Unsent = VisitorMessageItem.Unsent(id = "id", message = "message")
+
+        val inQueue = OperatorStatusItem.InQueue("company_name")
+
+
+        assertTrue(state.unsentItems.isEmpty())
+        assertTrue(state.chatItems.isEmpty())
+
+        val newState = subjectUnderTest.addUnsentMessage(message, state)
+
+        assertTrue(newState.unsentItems.count() == 1)
+        assertTrue(newState.chatItems.count() == 1)
+        assertEquals(newState.unsentItems.last(), newState.chatItems.last())
+
+        newState.chatItems.add(inQueue)
+
+        subjectUnderTest.addUnsentMessage(message, state).apply {
+            assertTrue(unsentItems.count() == 2)
+            assertTrue(chatItems.count() == 3)
+            assertEquals(unsentItems.last(), chatItems[1])
+            assertEquals(chatItems.last(), inQueue)
+        }
+    }
+
+    @Test
+    fun `mapResponseCardClicked converts ResponseCard to PlainText when it clicked`() {
+        val responseCard: OperatorMessageItem.ResponseCard = mock {
+            on { asPlainText() } doReturn mock()
+        }
+        state.chatItems.add(responseCard)
+
+        subjectUnderTest.mapResponseCardClicked(responseCard, state).apply {
+            assertTrue(chatItems.count() == 1)
+            assertTrue(chatItems.last() is OperatorMessageItem.PlainText)
+        }
+    }
+
+    @Test
+    fun `mapOperatorJoined adds OperatorStatusItem_Joined to chatItems when called`() {
+        val action: ChatManager.Action.OperatorJoined = ChatManager.Action.OperatorJoined("", "", "")
+
+        subjectUnderTest.mapOperatorJoined(action, state).apply {
+            assertTrue(chatItems.count() == 1)
+            assertTrue(chatItems.last() is OperatorStatusItem.Joined)
+        }
+    }
+
+    @Test
+    fun `mapMediaUpgrade adds MediaUpgradeStartedTimerItem_Video to chatItems when video is true`() {
+        subjectUnderTest.mapMediaUpgrade(true, state).apply {
+            assertTrue(chatItems.count() == 1)
+            assertTrue(chatItems.last() is MediaUpgradeStartedTimerItem.Video)
+            assertNotNull(mediaUpgradeTimerItem)
+        }
+    }
+
+    @Test
+    fun `mapMediaUpgrade adds MediaUpgradeStartedTimerItem_Audio to chatItems when video is false`() {
+        subjectUnderTest.mapMediaUpgrade(false, state).apply {
+            assertTrue(chatItems.count() == 1)
+            assertTrue(chatItems.last() is MediaUpgradeStartedTimerItem.Audio)
+            assertNotNull(mediaUpgradeTimerItem)
+        }
+    }
+
+    @Test
+    fun `mapUpgradeMediaToVideo removes old MediaUpgradeItem when it exists`() {
+        val time = "10"
+        val oldItem: MediaUpgradeStartedTimerItem.Audio = MediaUpgradeStartedTimerItem.Audio(time)
+        state.mediaUpgradeTimerItem = oldItem
+        state.chatItems.add(oldItem)
+
+        subjectUnderTest.mapUpgradeMediaToVideo(state).apply {
+            assertTrue(chatItems.count() == 1)
+            val newItem = chatItems.last() as MediaUpgradeStartedTimerItem.Video
+            assertEquals(time, newItem.time)
+            assertEquals(newItem, mediaUpgradeTimerItem)
+        }
+    }
+
+    @Test
+    fun `mapMediaUpgradeCanceled removes old MediaUpgradeItem when it exists`() {
+        val time = "10"
+        val oldItem: MediaUpgradeStartedTimerItem.Audio = MediaUpgradeStartedTimerItem.Audio(time)
+        state.mediaUpgradeTimerItem = oldItem
+        state.chatItems.add(oldItem)
+
+        subjectUnderTest.mapMediaUpgradeCanceled(state).apply {
+            assertTrue(chatItems.isEmpty())
+            assertNull(mediaUpgradeTimerItem)
+        }
+    }
+
+    @Test
+    fun `mapMediaUpgradeTimerUpdated updates old MediaUpgradeItem when it called with new time`() {
+        val oldItem: MediaUpgradeStartedTimerItem.Audio = MediaUpgradeStartedTimerItem.Audio("10")
+        state.mediaUpgradeTimerItem = oldItem
+        state.chatItems.add(oldItem)
+
+        subjectUnderTest.mapMediaUpgradeTimerUpdated("11", state).apply {
+            assertTrue(chatItems.isNotEmpty())
+            assertEquals(mediaUpgradeTimerItem!!.time, "11")
+            assertEquals((chatItems.last() as MediaUpgradeStartedTimerItem.Audio).time, "11")
+        }
+    }
+
+    @Test
+    fun `mapCustomCardClicked updates Custom Card`() {
+        val action: ChatManager.Action.CustomCardClicked = mock {
+            on { customCard } doReturn mock()
+            on { attachment } doReturn mock()
+        }
+
+        subjectUnderTest.mapCustomCardClicked(action, state)
+        verify(handleCustomCardClickUseCase).invoke(any(), any(), any())
+    }
+
+    @Test
+    fun `mapAction calls mapInQueue when Action_QueuingStarted passed`() {
+        val action: ChatManager.Action.QueuingStarted = mock {
+            on { companyName } doReturn ""
+        }
+
+        subjectUnderTest.mapAction(action, state)
+        verify(subjectUnderTest).mapInQueue(any(), any())
+    }
+
+    @Test
+    fun `mapAction calls mapOperatorConnected when Action_OperatorConnected passed`() {
+        val action: ChatManager.Action.OperatorConnected = mock {
+            on { companyName } doReturn ""
+            on { operatorFormattedName } doReturn ""
+        }
+
+        subjectUnderTest.mapAction(action, state)
+        verify(subjectUnderTest).mapOperatorConnected(any(), any())
+    }
+
+    @Test
+    fun `mapAction calls mapTransferring when Action_Transferring passed`() {
+        val action: ChatManager.Action.Transferring = ChatManager.Action.Transferring
+
+        subjectUnderTest.mapAction(action, state)
+        verify(subjectUnderTest).mapTransferring(any())
+    }
+
+    @Test
+    fun `mapAction calls mapOperatorJoined when Action_OperatorJoined passed`() {
+        val action: ChatManager.Action.OperatorJoined = mock {
+            on { companyName } doReturn ""
+            on { operatorFormattedName } doReturn ""
+        }
+
+        subjectUnderTest.mapAction(action, state)
+        verify(subjectUnderTest).mapOperatorJoined(any(), any())
+    }
+
+    @Test
+    fun `mapAction calls addUnsentMessage when Action_UnsentMessageReceived passed`() {
+        val action: ChatManager.Action.UnsentMessageReceived = mock {
+            on { message } doReturn mock()
+        }
+
+        subjectUnderTest.mapAction(action, state)
+        verify(subjectUnderTest).addUnsentMessage(any(), any())
+    }
+
+    @Test
+    fun `mapAction calls mapResponseCardClicked when Action_ResponseCardClicked passed`() {
+        val action: ChatManager.Action.ResponseCardClicked = mock {
+            on { responseCard } doReturn mock()
+        }
+        state.chatItems.add(action.responseCard)
+
+        subjectUnderTest.mapAction(action, state)
+        verify(subjectUnderTest).mapResponseCardClicked(any(), any())
+    }
+
+    @Test
+    fun `mapAction calls mapMediaUpgrade when Action_OnMediaUpgradeStarted passed`() {
+        val action: ChatManager.Action.OnMediaUpgradeStarted = mock {
+            on { isVideo } doReturn true
+        }
+
+        subjectUnderTest.mapAction(action, state)
+        verify(subjectUnderTest).mapMediaUpgrade(any(), any())
+    }
+
+    @Test
+    fun `mapAction calls mapUpgradeMediaToVideo when Action_OnMediaUpgradeToVideo passed`() {
+        val action: ChatManager.Action.OnMediaUpgradeToVideo = ChatManager.Action.OnMediaUpgradeToVideo
+
+        subjectUnderTest.mapAction(action, state)
+        verify(subjectUnderTest).mapUpgradeMediaToVideo(any())
+    }
+
+    @Test
+    fun `mapAction calls mapMediaUpgradeCanceled when Action_OnMediaUpgradeCanceled passed`() {
+        val action: ChatManager.Action.OnMediaUpgradeCanceled = ChatManager.Action.OnMediaUpgradeCanceled
+
+        subjectUnderTest.mapAction(action, state)
+        verify(subjectUnderTest).mapMediaUpgradeCanceled(any())
+    }
+
+    @Test
+    fun `mapAction calls mapMediaUpgradeTimerUpgraded when Action_OnMediaUpgradeTimerUpdated passed`() {
+        val action: ChatManager.Action.OnMediaUpgradeTimerUpdated = mock {
+            on { formattedValue } doReturn "10"
+        }
+
+        subjectUnderTest.mapAction(action, state)
+        verify(subjectUnderTest).mapMediaUpgradeTimerUpdated(any(), any())
+    }
+
+    @Test
+    fun `mapAction calls mapCustomCardClicked when Action_CustomCardClicked passed`() {
+        val action: ChatManager.Action.CustomCardClicked = mock {
+            on { customCard } doReturn mock()
+            on { attachment } doReturn mock()
+        }
+
+        subjectUnderTest.mapAction(action, state)
+        verify(subjectUnderTest).mapCustomCardClicked(any(), any())
+    }
+
+    @Test
+    fun `mapAction returns same state when Action_ChatRestored passed`() {
+        val action: ChatManager.Action.ChatRestored = ChatManager.Action.ChatRestored
+
+        assertEquals(state, subjectUnderTest.mapAction(action, state))
+    }
+
+    @Test
+    fun `mapNewMessage does nothing when message is not new`() {
+        val chatMessageInternal = mockChatMessage()
+        doReturn(false).whenever(state).isNew(chatMessageInternal)
+
+        subjectUnderTest.mapNewMessage(chatMessageInternal, state)
+
+        verify(appendNewChatMessageUseCase, never()).invoke(any(), any())
+        verify(subjectUnderTest, never()).checkUnsentMessages(any())
+    }
+
+    @Test
+    fun `mapNewMessage calls appendNewChatMessageUseCase when message is new`() {
+        val chatMessageInternal = mockChatMessage()
+        doReturn(true).whenever(state).isNew(chatMessageInternal)
+
+        subjectUnderTest.mapNewMessage(chatMessageInternal, state)
+
+        verify(appendNewChatMessageUseCase).invoke(any(), any())
+        verify(subjectUnderTest, never()).checkUnsentMessages(any())
+    }
+
+    @Test
+    fun `mapNewMessage calls checkUnsentMessage when message is new VisitorMessage`() {
+        val chatMessageInternal = mockChatMessage()
+        doReturn(true).whenever(state).isNew(chatMessageInternal)
+
+        subjectUnderTest.mapNewMessage(chatMessageInternal, state)
+
+        verify(appendNewChatMessageUseCase).invoke(any(), any())
+        verify(subjectUnderTest).checkUnsentMessages(any())
+    }
+
+    @Test
+    fun `mapChatHistory calls appendHistoryChatMessageUseCase when response contains messages`() {
+        val chatHistoryResponse: ChatHistoryResponse = mock()
+        whenever(chatHistoryResponse.newMessagesCount) doReturn 1
+        val visitorMessage = mockChatMessage()
+        val operatorMessage = mockChatMessage()
+
+        whenever(chatHistoryResponse.items) doReturn listOf(visitorMessage, operatorMessage)
+
+        whenever(addNewMessagesDividerUseCase(any(), any())) doReturn true
+        whenever(markMessagesReadWithDelayUseCase()) doReturn Completable.complete()
+
+        val newState = subjectUnderTest.mapChatHistory(chatHistoryResponse)
+
+        verify(appendHistoryChatMessageUseCase).invoke(any(), any(), eq(true))
+        verify(appendHistoryChatMessageUseCase).invoke(any(), any(), eq(false))
+
+        assertTrue(newState.chatItemIds.count() == 2)
+        verify(subjectUnderTest).markMessagesReadWithDelay()
+    }
+
+    @Test
+    fun `checkUnsentMessages calls sendUnsentMessagesUseCase when unsent messages list is not empty`() {
+        val mockUnsentMessage: VisitorMessageItem.Unsent = mock {
+            on { message } doReturn "message"
+        }
+        state.unsentItems.add(mockUnsentMessage)
+
+        subjectUnderTest.checkUnsentMessages(state)
+
+        verify(sendUnsentMessagesUseCase).invoke(any(), any())
+    }
+
+    @Test
+    fun `onAction triggers mapAction when new action received`() {
+        stateProcessor.onNext(state)
+        val onActionFlowable = subjectUnderTest.onAction().test()
+        onActionFlowable.assertNoValues()
+
+        action.onNext(ChatManager.Action.ChatRestored)
+        onActionFlowable.assertValue(state)
+        verify(subjectUnderTest).mapAction(any(), any())
+    }
+
+    @Test
+    fun `mapNewMessage triggers mapNewMessage when new message received`() {
+        stateProcessor.onNext(state)
+        val messageProcessor: BehaviorProcessor = BehaviorProcessor.create()
+        whenever(onMessageUseCase()) doReturn messageProcessor.share().toObservable()
+
+        val onMessageFlowable = subjectUnderTest.onMessage().test()
+        onMessageFlowable.assertNoValues()
+
+        messageProcessor.onNext(mockChatMessage())
+        verify(subjectUnderTest).mapNewMessage(any(), any())
+    }
+
+    @Test
+    fun `updateQuickReplies triggers quickReplies onNext when the last chat item is GvaQuickReplies`() {
+        val quickRepliesTest = quickReplies.test()
+        val mockOptions: List = listOf(mock())
+        val quickReplies: GvaQuickReplies = mock {
+            on { options } doReturn mockOptions
+        }
+        state.chatItems.add(quickReplies)
+
+        subjectUnderTest.updateQuickReplies(state)
+        quickRepliesTest.assertValue(mockOptions)
+    }
+
+    @Test
+    fun `subscribeToQuickReplies subscribes to quickReplies`() {
+        val testSubscriber = quickReplies.test()
+        val callback: (List) -> Unit = mock()
+        subjectUnderTest.subscribeToQuickReplies(callback)
+        testSubscriber.assertSubscribed()
+
+        quickReplies.onNext(emptyList())
+        verify(callback).invoke(any())
+    }
+
+    @Test
+    fun `loadHistory triggers mapChatHistory when history response receives`() {
+        whenever(loadHistoryUseCase()) doReturn Single.just(mock())
+        val testFlowable = subjectUnderTest.loadHistory { }.test()
+        verify(subjectUnderTest).mapChatHistory(any())
+        assertTrue(testFlowable.valueCount() == 1)
+    }
+
+    @Test
+    fun `subscribeToState subscribes to history and messages`() {
+        val chatMessageInternal = mockChatMessage()
+        doReturn(true).whenever(state).isNew(chatMessageInternal)
+        doReturn(10).whenever(state).addedMessagesCount
+        stateProcessor.onNext(state)
+
+        whenever(loadHistoryUseCase()) doReturn Single.just(mock())
+        whenever(onMessageUseCase()) doReturn Observable.just(chatMessageInternal)
+
+        subjectUnderTest.subscribeToState(mock(), mock())
+        verify(subjectUnderTest).loadHistory(any())
+        verify(subjectUnderTest).subscribeToMessages(any())
+    }
+
+    @Test
+    fun `onChatAction emits action to observable`() {
+        val test = action.test()
+        subjectUnderTest.onChatAction(ChatManager.Action.Transferring)
+        test.assertValue(ChatManager.Action.Transferring)
+    }
+
+    @Test
+    fun `reset clears state`() {
+        stateProcessor.onNext(ChatManager.State(addedMessagesCount = 10))
+        quickReplies.onNext(listOf(mock()))
+
+        subjectUnderTest.reset()
+
+        verify(compositeDisposable).clear()
+        assertEquals(stateProcessor.value, ChatManager.State())
+        assertEquals(quickReplies.value, emptyList())
+    }
+
+    @Test
+    fun `subscribe subscribes to state and quick replies`() {
+        whenever(loadHistoryUseCase()) doReturn Single.just(mock())
+        whenever(onMessageUseCase()) doReturn Observable.just(mock())
+
+        subjectUnderTest.apply {
+            subscribe({ }, { }, { })
+            verify(this).subscribeToState(any(), any())
+            verify(this).subscribeToQuickReplies(any())
+        }
+    }
+
+    @Test
+    fun `initialize subscribes to state and quick replies`() {
+        val chatMessageInternal = mockChatMessage()
+        doReturn(true).whenever(state).isNew(chatMessageInternal)
+
+        whenever(loadHistoryUseCase()) doReturn Single.just(mock())
+        whenever(onMessageUseCase()) doReturn Observable.just(chatMessageInternal)
+
+        val chatItems = mutableListOf(mock())
+        subjectUnderTest.apply {
+            val onHistoryLoaded = mock<(hasHistory: Boolean) -> Unit>()
+            val onQuickReplyReceived = mock<(List) -> Unit>()
+            val onOperatorMessageReceived = mock<(count: Int) -> Unit>()
+            val itemsFlowable = initialize(onHistoryLoaded, onQuickReplyReceived, onOperatorMessageReceived)
+            verify(this).subscribe(onHistoryLoaded, onOperatorMessageReceived, onQuickReplyReceived)
+
+            val test = itemsFlowable.test()
+
+            stateProcessor.onNext(ChatManager.State(chatItems = chatItems))
+
+            assertEquals(test.values().last().last(), chatItems.last())
+        }
+    }
+
+    private inline fun  mockChatMessage(): ChatMessageInternal {
+        val chatMessageInternal: ChatMessageInternal = mock()
+        val chatMessage = mock()
+        whenever(chatMessage.id) doReturn UUID.randomUUID().toString()
+        whenever(chatMessage.content) doReturn UUID.randomUUID().toString()
+        whenever(chatMessage.timestamp) doReturn 100
+        whenever(chatMessageInternal.chatMessage) doReturn chatMessage
+        return chatMessageInternal
+    }
+
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/MockChatMessageInternal.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/MockChatMessageInternal.kt
index 8732cc151..7c9fe4574 100644
--- a/widgetssdk/src/test/java/com/glia/widgets/chat/MockChatMessageInternal.kt
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/MockChatMessageInternal.kt
@@ -1,6 +1,7 @@
 package com.glia.widgets.chat
 
 import com.glia.androidsdk.chat.ChatMessage
+import com.glia.widgets.chat.model.Gva
 import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
 import org.json.JSONObject
 import org.mockito.Mockito
@@ -14,6 +15,7 @@ internal class MockChatMessageInternal {
     val messageTimeStamp = 123L
     val operatorImageUrl = "operator_url"
     val operatorName = "operator_name"
+    val content = "content"
 
     private val chatMessage: ChatMessage = mock()
     val chatMessageInternal: ChatMessageInternal = mock()
@@ -26,6 +28,7 @@ internal class MockChatMessageInternal {
         whenever(chatMessage.id) doReturn messageId
         whenever(chatMessage.timestamp) doReturn messageTimeStamp
         whenever(chatMessage.metadata) doReturn metadata
+        whenever(chatMessage.content) doReturn content
     }
 
     fun mockOperatorProperties() {
@@ -34,11 +37,7 @@ internal class MockChatMessageInternal {
         whenever(chatMessageInternal.operatorName) doReturn operatorName
     }
 
-    fun mockOperatorPropertiesWithNull() {
-        whenever(chatMessageInternal.operatorId) doReturn null
-        whenever(chatMessageInternal.operatorImageUrl) doReturn null
-        whenever(chatMessageInternal.operatorName) doReturn null
-    }
+    fun metadataWithContent(content: String = this.content): JSONObject = JSONObject().put(Gva.Keys.CONTENT, content)
 
     fun reset() {
         Mockito.reset(chatMessage, chatMessageInternal)
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt
index c06da85af..39cd8839a 100644
--- a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt
@@ -1,28 +1,19 @@
 package com.glia.widgets.chat.controller
 
 import android.net.Uri
-import com.glia.androidsdk.chat.ChatMessage
 import com.glia.androidsdk.chat.SingleChoiceAttachment
+import com.glia.widgets.chat.ChatManager
 import com.glia.widgets.chat.ChatViewCallback
-import com.glia.widgets.chat.domain.AddNewMessagesDividerUseCase
-import com.glia.widgets.chat.domain.CustomCardAdapterTypeUseCase
-import com.glia.widgets.chat.domain.CustomCardShouldShowUseCase
-import com.glia.widgets.chat.domain.CustomCardTypeUseCase
-import com.glia.widgets.chat.domain.GliaLoadHistoryUseCase
-import com.glia.widgets.chat.domain.GliaOnMessageUseCase
 import com.glia.widgets.chat.domain.GliaOnOperatorTypingUseCase
 import com.glia.widgets.chat.domain.GliaSendMessagePreviewUseCase
 import com.glia.widgets.chat.domain.GliaSendMessageUseCase
-import com.glia.widgets.chat.domain.IsEnableChatEditTextUseCase
+import com.glia.widgets.chat.domain.IsAuthenticatedUseCase
 import com.glia.widgets.chat.domain.IsFromCallScreenUseCase
 import com.glia.widgets.chat.domain.IsSecureConversationsChatAvailableUseCase
 import com.glia.widgets.chat.domain.IsShowSendButtonUseCase
 import com.glia.widgets.chat.domain.SiteInfoUseCase
 import com.glia.widgets.chat.domain.UpdateFromCallScreenUseCase
 import com.glia.widgets.chat.domain.gva.DetermineGvaButtonTypeUseCase
-import com.glia.widgets.chat.domain.gva.IsGvaUseCase
-import com.glia.widgets.chat.domain.gva.MapGvaUseCase
-import com.glia.widgets.chat.model.ChatItem
 import com.glia.widgets.chat.model.Gva
 import com.glia.widgets.chat.model.GvaButton
 import com.glia.widgets.core.callvisualizer.domain.IsCallVisualizerUseCase
@@ -37,7 +28,6 @@ import com.glia.widgets.core.engagement.domain.GliaOnEngagementUseCase
 import com.glia.widgets.core.engagement.domain.IsOngoingEngagementUseCase
 import com.glia.widgets.core.engagement.domain.IsQueueingEngagementUseCase
 import com.glia.widgets.core.engagement.domain.SetEngagementConfigUseCase
-import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
 import com.glia.widgets.core.fileupload.domain.AddFileAttachmentsObserverUseCase
 import com.glia.widgets.core.fileupload.domain.AddFileToAttachmentAndUploadUseCase
 import com.glia.widgets.core.fileupload.domain.GetFileAttachmentsUseCase
@@ -54,24 +44,18 @@ import com.glia.widgets.core.queue.domain.GliaCancelQueueTicketUseCase
 import com.glia.widgets.core.queue.domain.GliaQueueForChatEngagementUseCase
 import com.glia.widgets.core.queue.domain.QueueTicketStateChangeToUnstaffedUseCase
 import com.glia.widgets.core.secureconversations.domain.IsSecureEngagementUseCase
-import com.glia.widgets.core.secureconversations.domain.MarkMessagesReadWithDelayUseCase
 import com.glia.widgets.core.survey.domain.GliaSurveyUseCase
 import com.glia.widgets.filepreview.domain.usecase.DownloadFileUseCase
 import com.glia.widgets.filepreview.domain.usecase.IsFileReadyForPreviewUseCase
 import com.glia.widgets.helper.TimeCounter
 import com.glia.widgets.view.MessagesNotSeenHandler
 import com.glia.widgets.view.MinimizeHandler
-import io.reactivex.Completable
 import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertFalse
-import junit.framework.TestCase.assertNull
-import junit.framework.TestCase.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.mockito.kotlin.any
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
-import org.mockito.kotlin.never
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
@@ -83,11 +67,9 @@ class ChatControllerTest {
     private lateinit var dialogController: DialogController
     private lateinit var messagesNotSeenHandler: MessagesNotSeenHandler
     private lateinit var callNotificationUseCase: CallNotificationUseCase
-    private lateinit var loadHistoryUseCase: GliaLoadHistoryUseCase
     private lateinit var queueForChatEngagementUseCase: GliaQueueForChatEngagementUseCase
     private lateinit var getEngagementUseCase: GliaOnEngagementUseCase
     private lateinit var engagementEndUseCase: GliaOnEngagementEndUseCase
-    private lateinit var onMessageUseCase: GliaOnMessageUseCase
     private lateinit var onOperatorTypingUseCase: GliaOnOperatorTypingUseCase
     private lateinit var sendMessagePreviewUseCase: GliaSendMessagePreviewUseCase
     private lateinit var sendMessageUseCase: GliaSendMessageUseCase
@@ -103,15 +85,11 @@ class ChatControllerTest {
     private lateinit var isShowSendButtonUseCase: IsShowSendButtonUseCase
     private lateinit var isShowOverlayPermissionRequestDialogUseCase: IsShowOverlayPermissionRequestDialogUseCase
     private lateinit var downloadFileUseCase: DownloadFileUseCase
-    private lateinit var isEnableChatEditTextUseCase: IsEnableChatEditTextUseCase
     private lateinit var siteInfoUseCase: SiteInfoUseCase
     private lateinit var surveyUseCase: GliaSurveyUseCase
     private lateinit var getGliaEngagementStateFlowableUseCase: GetEngagementStateFlowableUseCase
     private lateinit var isFromCallScreenUseCase: IsFromCallScreenUseCase
     private lateinit var updateFromCallScreenUseCase: UpdateFromCallScreenUseCase
-    private lateinit var customCardAdapterTypeUseCase: CustomCardAdapterTypeUseCase
-    private lateinit var customCardTypeUseCase: CustomCardTypeUseCase
-    private lateinit var customCardShouldShowUseCase: CustomCardShouldShowUseCase
     private lateinit var ticketStateChangeToUnstaffedUseCase: QueueTicketStateChangeToUnstaffedUseCase
     private lateinit var addMediaUpgradeOfferCallbackUseCase: AddMediaUpgradeOfferCallbackUseCase
     private lateinit var removeMediaUpgradeOfferCallbackUseCase: RemoveMediaUpgradeOfferCallbackUseCase
@@ -119,19 +97,17 @@ class ChatControllerTest {
     private lateinit var isSecureEngagementUseCase: IsSecureEngagementUseCase
     private lateinit var engagementConfigUseCase: SetEngagementConfigUseCase
     private lateinit var isSecureConversationsChatAvailableUseCase: IsSecureConversationsChatAvailableUseCase
-    private lateinit var markMessagesReadWithDelayUseCase: MarkMessagesReadWithDelayUseCase
     private lateinit var isQueueingEngagementUseCase: IsQueueingEngagementUseCase
     private lateinit var hasPendingSurveyUseCase: HasPendingSurveyUseCase
     private lateinit var setPendingSurveyUsedUseCase: SetPendingSurveyUsedUseCase
     private lateinit var isCallVisualizerUseCase: IsCallVisualizerUseCase
-    private lateinit var addNewMessagesDividerUseCase: AddNewMessagesDividerUseCase
     private lateinit var isFileReadyForPreviewUseCase: IsFileReadyForPreviewUseCase
     private lateinit var acceptMediaUpgradeOfferUseCase: AcceptMediaUpgradeOfferUseCase
-    private lateinit var isGvaUseCase: IsGvaUseCase
-    private lateinit var mapGvaUseCase: MapGvaUseCase
     private lateinit var determineGvaButtonTypeUseCase: DetermineGvaButtonTypeUseCase
 
     private lateinit var chatController: ChatController
+    private lateinit var isAuthenticatedUseCase: IsAuthenticatedUseCase
+    private lateinit var chatManager: ChatManager
 
     @Before
     fun setUp() {
@@ -142,11 +118,9 @@ class ChatControllerTest {
         dialogController = mock()
         messagesNotSeenHandler = mock()
         callNotificationUseCase = mock()
-        loadHistoryUseCase = mock()
         queueForChatEngagementUseCase = mock()
         getEngagementUseCase = mock()
         engagementEndUseCase = mock()
-        onMessageUseCase = mock()
         onOperatorTypingUseCase = mock()
         sendMessagePreviewUseCase = mock()
         sendMessageUseCase = mock()
@@ -162,15 +136,11 @@ class ChatControllerTest {
         isShowSendButtonUseCase = mock()
         isShowOverlayPermissionRequestDialogUseCase = mock()
         downloadFileUseCase = mock()
-        isEnableChatEditTextUseCase = mock()
         siteInfoUseCase = mock()
         surveyUseCase = mock()
         getGliaEngagementStateFlowableUseCase = mock()
         isFromCallScreenUseCase = mock()
         updateFromCallScreenUseCase = mock()
-        customCardAdapterTypeUseCase = mock()
-        customCardTypeUseCase = mock()
-        customCardShouldShowUseCase = mock()
         ticketStateChangeToUnstaffedUseCase = mock()
         addMediaUpgradeOfferCallbackUseCase = mock()
         removeMediaUpgradeOfferCallbackUseCase = mock()
@@ -178,17 +148,15 @@ class ChatControllerTest {
         isSecureEngagementUseCase = mock()
         engagementConfigUseCase = mock()
         isSecureConversationsChatAvailableUseCase = mock()
-        markMessagesReadWithDelayUseCase = mock()
         isQueueingEngagementUseCase = mock()
         hasPendingSurveyUseCase = mock()
         setPendingSurveyUsedUseCase = mock()
         isCallVisualizerUseCase = mock()
-        addNewMessagesDividerUseCase = mock()
         isFileReadyForPreviewUseCase = mock()
         acceptMediaUpgradeOfferUseCase = mock()
-        isGvaUseCase = mock()
-        mapGvaUseCase = mock()
         determineGvaButtonTypeUseCase = mock()
+        isAuthenticatedUseCase = mock()
+        chatManager = mock()
 
         chatController = ChatController(
             chatViewCallback = chatViewCallback,
@@ -198,11 +166,9 @@ class ChatControllerTest {
             dialogController = dialogController,
             messagesNotSeenHandler = messagesNotSeenHandler,
             callNotificationUseCase = callNotificationUseCase,
-            loadHistoryUseCase = loadHistoryUseCase,
             queueForChatEngagementUseCase = queueForChatEngagementUseCase,
             getEngagementUseCase = getEngagementUseCase,
             engagementEndUseCase = engagementEndUseCase,
-            onMessageUseCase = onMessageUseCase,
             onOperatorTypingUseCase = onOperatorTypingUseCase,
             sendMessagePreviewUseCase = sendMessagePreviewUseCase,
             sendMessageUseCase = sendMessageUseCase,
@@ -218,15 +184,11 @@ class ChatControllerTest {
             isShowSendButtonUseCase = isShowSendButtonUseCase,
             isShowOverlayPermissionRequestDialogUseCase = isShowOverlayPermissionRequestDialogUseCase,
             downloadFileUseCase = downloadFileUseCase,
-            isEnableChatEditTextUseCase = isEnableChatEditTextUseCase,
             siteInfoUseCase = siteInfoUseCase,
             surveyUseCase = surveyUseCase,
             getGliaEngagementStateFlowableUseCase = getGliaEngagementStateFlowableUseCase,
             isFromCallScreenUseCase = isFromCallScreenUseCase,
             updateFromCallScreenUseCase = updateFromCallScreenUseCase,
-            customCardAdapterTypeUseCase = customCardAdapterTypeUseCase,
-            customCardTypeUseCase = customCardTypeUseCase,
-            customCardShouldShowUseCase = customCardShouldShowUseCase,
             ticketStateChangeToUnstaffedUseCase = ticketStateChangeToUnstaffedUseCase,
             isOngoingEngagementUseCase = isOngoingEngagementUseCase,
             isSecureEngagementUseCase = isSecureEngagementUseCase,
@@ -234,175 +196,18 @@ class ChatControllerTest {
             addMediaUpgradeCallbackUseCase = addMediaUpgradeOfferCallbackUseCase,
             removeMediaUpgradeCallbackUseCase = removeMediaUpgradeOfferCallbackUseCase,
             isSecureEngagementAvailableUseCase = isSecureConversationsChatAvailableUseCase,
-            markMessagesReadWithDelayUseCase = markMessagesReadWithDelayUseCase,
             isQueueingEngagementUseCase = isQueueingEngagementUseCase,
             hasPendingSurveyUseCase = hasPendingSurveyUseCase,
             setPendingSurveyUsedUseCase = setPendingSurveyUsedUseCase,
             isCallVisualizerUseCase = isCallVisualizerUseCase,
-            addNewMessagesDividerUseCase = addNewMessagesDividerUseCase,
             isFileReadyForPreviewUseCase = isFileReadyForPreviewUseCase,
             acceptMediaUpgradeOfferUseCase = acceptMediaUpgradeOfferUseCase,
-            isGvaUseCase = isGvaUseCase,
-            mapGvaUseCase = mapGvaUseCase,
-            determineGvaButtonTypeUseCase = determineGvaButtonTypeUseCase
+            determineGvaButtonTypeUseCase = determineGvaButtonTypeUseCase,
+            isAuthenticatedUseCase = isAuthenticatedUseCase,
+            chatManager = chatManager
         )
     }
 
-    @Test
-    fun removeDuplicates_returnsNull_whenNewMessagesIsNull() {
-        val oldHistory = listOf(
-            mock().also { whenever(it.id).thenReturn("Id1") },
-            mock().also { whenever(it.id).thenReturn("Id2") },
-            mock().also { whenever(it.id).thenReturn("Id3") }
-        )
-
-        assertNull(chatController.removeDuplicates(oldHistory, null))
-    }
-
-    @Test
-    fun removeDuplicates_returnsNewMessages_whenOldMessagesIsNull() {
-        val newMessages = listOf(
-            mock().also { whenever(it.id).thenReturn("Id1") },
-            mock().also { whenever(it.id).thenReturn("Id2") },
-            mock().also { whenever(it.id).thenReturn("Id3") }
-        ).map { chatItem ->
-            mock().also { whenever(it.chatMessage).thenReturn(chatItem) }
-        }
-
-        val result = chatController.removeDuplicates(null, newMessages)
-        assertEquals(newMessages, result)
-    }
-
-    @Test
-    fun removeDuplicates_returnsAllMessages_whenAllNewMessagesAreNotContainInHistory() {
-        val oldHistory = listOf(
-            mock().also { whenever(it.id).thenReturn("Id1") },
-            mock().also { whenever(it.id).thenReturn("Id2") },
-            mock().also { whenever(it.id).thenReturn("Id3") }
-        )
-        val newMessages = listOf(
-            mock().also { whenever(it.id).thenReturn("Id4") },
-            mock().also { whenever(it.id).thenReturn("Id5") },
-            mock().also { whenever(it.id).thenReturn("Id6") }
-        ).map { chatItem ->
-            mock().also { whenever(it.chatMessage).thenReturn(chatItem) }
-        }
-
-        val result =
-            chatController.removeDuplicates(oldHistory, newMessages)!!.map { it.chatMessage.id }
-        assertEquals(listOf("Id4", "Id5", "Id6"), result)
-    }
-
-    @Test
-    fun removeDuplicates_returnsOnlyNewMessages_whenSomeNewMessagesAreContainInHistory() {
-        val oldHistory = listOf(
-            mock().also { whenever(it.id).thenReturn("Id1") },
-            mock().also { whenever(it.id).thenReturn("Id2") },
-            mock().also { whenever(it.id).thenReturn("Id3") }
-        )
-        val newMessages = listOf(
-            mock().also { whenever(it.id).thenReturn("Id3") },
-            mock().also { whenever(it.id).thenReturn("Id4") },
-            mock().also { whenever(it.id).thenReturn("Id5") }
-        ).map { chatItem ->
-            mock().also { whenever(it.chatMessage).thenReturn(chatItem) }
-        }
-
-        val result =
-            chatController.removeDuplicates(oldHistory, newMessages)!!.map { it.chatMessage.id }
-        assertEquals(listOf("Id4", "Id5"), result)
-    }
-
-    @Test
-    fun removeDuplicates_removesAllMessages_whenAllNewMessagesAreContainInHistory() {
-        val oldHistory = listOf(
-            mock().also { whenever(it.id).thenReturn("Id1") },
-            mock().also { whenever(it.id).thenReturn("Id2") },
-            mock().also { whenever(it.id).thenReturn("Id3") }
-        )
-        val newMessages = listOf(
-            mock().also { whenever(it.id).thenReturn("Id1") },
-            mock().also { whenever(it.id).thenReturn("Id2") },
-            mock().also { whenever(it.id).thenReturn("Id3") }
-        ).map { chatItem ->
-            mock().also { whenever(it.chatMessage).thenReturn(chatItem) }
-        }
-
-        assertTrue(chatController.removeDuplicates(oldHistory, newMessages)!!.isEmpty())
-    }
-
-    @Test
-    fun isNewMessage_returnsTrue_whenOldMessagesEmpty() {
-        val oldHistory = listOf()
-        val newMessage = mock()
-
-        assertTrue(chatController.isNewMessage(oldHistory, newMessage))
-    }
-
-    @Test
-    fun isNewMessage_returnsTrue_whenOldMessagesIsNull() {
-        val newMessage = mock()
-
-        assertTrue(chatController.isNewMessage(null, newMessage))
-    }
-
-    @Test
-    fun isNewMessage_returnsTrue_whenOldMessagesDoesNotContainNewOne() {
-        val oldHistory = listOf(
-            mock().also { whenever(it.id).thenReturn("oldId1") },
-            mock().also { whenever(it.id).thenReturn("oldId2") },
-            mock().also { whenever(it.id).thenReturn("oldId3") }
-        )
-        val newMessage = mock()
-        whenever(newMessage.id).thenReturn("newId")
-
-        assertTrue(chatController.isNewMessage(oldHistory, newMessage))
-    }
-
-    @Test
-    fun isNewMessage_returnsFalse_whenOldMessagesContainsNewOne() {
-        val oldHistory = listOf(
-            mock().also { whenever(it.id).thenReturn("oldId1") },
-            mock().also { whenever(it.id).thenReturn("oldId2") },
-            mock().also { whenever(it.id).thenReturn("oldId3") }
-        )
-        val newMessage = mock()
-        whenever(newMessage.id).thenReturn("oldId2")
-
-        assertFalse(chatController.isNewMessage(oldHistory, newMessage))
-    }
-
-    @Test
-    fun isNewMessage_filterNullId_whenOldMessageDoesNotHaveId() {
-        val oldHistory = listOf(
-            mock().also { whenever(it.id).thenReturn("oldId1") },
-            mock().also { whenever(it.id).thenReturn(null) },
-            mock().also { whenever(it.id).thenReturn(null) }
-        )
-        val newMessage = mock()
-        whenever(newMessage.id).thenReturn("oldId1")
-
-        assertFalse(chatController.isNewMessage(oldHistory, newMessage))
-    }
-
-    @Test
-    fun `emitChatTranscriptItems triggers remove new messages divider when divider is added`() {
-        whenever(addNewMessagesDividerUseCase(any(), any())) doReturn true
-        whenever(markMessagesReadWithDelayUseCase()) doReturn Completable.complete()
-
-        chatController.emitChatTranscriptItems(mutableListOf(), 10)
-        verify(markMessagesReadWithDelayUseCase).invoke()
-    }
-
-    @Test
-    fun `emitChatTranscriptItems not triggers remove new messages divider when divider is added`() {
-        whenever(addNewMessagesDividerUseCase(any(), any())) doReturn false
-        whenever(markMessagesReadWithDelayUseCase()) doReturn Completable.complete()
-
-        chatController.emitChatTranscriptItems(mutableListOf(), 10)
-        verify(markMessagesReadWithDelayUseCase, never()).invoke()
-    }
-
     @Test
     fun `onGvaButtonClicked triggers viewCallback showBroadcastNotSupportedToast when gva type is BroadcastEvent`() {
         val gvaButton: GvaButton = mock()
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendGvaMessageItemUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendGvaMessageItemUseCaseTest.kt
new file mode 100644
index 000000000..dd80f9e2c
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendGvaMessageItemUseCaseTest.kt
@@ -0,0 +1,42 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.widgets.chat.domain.gva.MapGvaUseCase
+import com.glia.widgets.chat.model.ChatItem
+import com.glia.widgets.chat.model.GvaOperatorChatItem
+import org.junit.After
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class AppendGvaMessageItemUseCaseTest {
+    private val items: MutableList = mutableListOf()
+
+    private lateinit var mapGvaUseCase: MapGvaUseCase
+    private lateinit var useCase: AppendGvaMessageItemUseCase
+
+    @Before
+    fun setUp() {
+        mapGvaUseCase = mock()
+        useCase = AppendGvaMessageItemUseCase(mapGvaUseCase)
+    }
+
+    @After
+    fun tearDown() {
+        items.clear()
+    }
+
+    @Test
+    fun `invoke adds GvaOperatorChatItem to the list`() {
+        whenever(mapGvaUseCase.invoke(any(), any())) doReturn mock()
+
+        useCase(items, mock())
+
+        assertTrue(items.count() == 1)
+        assertTrue(items.first() is GvaOperatorChatItem)
+    }
+
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryChatMessageUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryChatMessageUseCaseTest.kt
new file mode 100644
index 000000000..e0f4114d6
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryChatMessageUseCaseTest.kt
@@ -0,0 +1,144 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.OperatorMessage
+import com.glia.androidsdk.chat.SystemMessage
+import com.glia.androidsdk.chat.VisitorMessage
+import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNull
+import junit.framework.TestCase.assertTrue
+import org.junit.Assert.assertFalse
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class AppendHistoryChatMessageUseCaseTest {
+    private lateinit var appendHistoryVisitorChatItemUseCase: AppendHistoryVisitorChatItemUseCase
+    private lateinit var appendHistoryOperatorChatItemUseCase: AppendHistoryOperatorChatItemUseCase
+    private lateinit var appendSystemMessageItemUseCase: AppendSystemMessageItemUseCase
+
+    private lateinit var useCase: AppendHistoryChatMessageUseCase
+    private lateinit var chatMessageInternal: ChatMessageInternal
+
+    @Before
+    fun setUp() {
+        appendHistoryOperatorChatItemUseCase = mock()
+        appendHistoryVisitorChatItemUseCase = mock()
+        appendSystemMessageItemUseCase = mock()
+
+        chatMessageInternal = mock()
+
+        useCase = spy(
+            AppendHistoryChatMessageUseCase(
+                appendHistoryVisitorChatItemUseCase,
+                appendHistoryOperatorChatItemUseCase,
+                appendSystemMessageItemUseCase
+            )
+        )
+    }
+
+    @Test
+    fun `resetOperatorId resets operator Id`() {
+        useCase.apply {
+            operatorId = ""
+            resetOperatorId()
+            assertNull(operatorId)
+        }
+    }
+
+    @Test
+    fun `shouldShowChatHead returns true when new operator is different`() {
+        val operatorId = "operator_id"
+        whenever(chatMessageInternal.operatorId) doReturn operatorId
+
+        useCase.apply {
+            assertTrue(shouldShowChatHead(chatMessageInternal))
+            assertEquals(this.operatorId, operatorId)
+        }
+    }
+
+    @Test
+    fun `shouldShowChatHead returns false when new operator is the same`() {
+        val operatorId = "operator_id"
+        whenever(chatMessageInternal.operatorId) doReturn operatorId
+
+        useCase.apply {
+            this.operatorId = operatorId
+            assertFalse(shouldShowChatHead(chatMessageInternal))
+            assertEquals(this.operatorId, operatorId)
+        }
+    }
+
+    /*    operator fun invoke(chatItems: MutableList, chatMessageInternal: ChatMessageInternal, isLatest: Boolean) {
+            when (val message = chatMessageInternal.chatMessage) {
+                is VisitorMessage -> {
+                    resetOperatorId()
+                    appendHistoryVisitorChatItemUseCase(chatItems, message)
+                }
+
+                is OperatorMessage -> appendHistoryOperatorChatItemUseCase(
+                    chatItems,
+                    chatMessageInternal,
+                    isLatest,
+                    shouldShowChatHead(chatMessageInternal)
+                )
+
+                is SystemMessage -> {
+                    resetOperatorId()
+                    appendSystemMessageItemUseCase(chatItems, message)
+                }
+
+                else -> Logger.d(TAG, "Unexpected type of message received -> $message")
+            }
+        }*/
+
+    @Test
+    fun `invoke resets operatorId and appends visitor item when chatMessage is VisitorMessage`() {
+        whenever(chatMessageInternal.chatMessage) doReturn mock()
+
+        useCase(mock(), chatMessageInternal, true)
+
+        verify(useCase).resetOperatorId()
+        verify(appendHistoryVisitorChatItemUseCase).invoke(any(), any())
+    }
+
+    @Test
+    fun `invoke resets operatorId and appends system item when chatMessage is SystemMessage`() {
+        whenever(chatMessageInternal.chatMessage) doReturn mock()
+
+        useCase(mock(), chatMessageInternal, true)
+
+        verify(useCase).resetOperatorId()
+        verify(appendSystemMessageItemUseCase).invoke(any(), any())
+    }
+
+    @Test
+    fun `invoke appends OperatorItem when chatMessage is OperatorMessage`() {
+        whenever(chatMessageInternal.chatMessage) doReturn mock()
+
+        useCase(mock(), chatMessageInternal, true)
+
+        verify(useCase).shouldShowChatHead(chatMessageInternal)
+        verify(appendHistoryOperatorChatItemUseCase).invoke(any(), any(), any(), any())
+    }
+
+    @Test
+    fun `invoke does nothing when chatMessage is not one of known types`() {
+        whenever(chatMessageInternal.chatMessage) doReturn mock()
+
+        useCase(mock(), chatMessageInternal, true)
+
+        verify(useCase, never()).shouldShowChatHead(any())
+        verify(useCase, never()).resetOperatorId()
+
+        verify(appendHistoryVisitorChatItemUseCase, never()).invoke(any(), any())
+        verify(appendSystemMessageItemUseCase, never()).invoke(any(), any())
+        verify(appendHistoryOperatorChatItemUseCase, never()).invoke(any(), any(), any(), any())
+    }
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryCustomCardItemUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryCustomCardItemUseCaseTest.kt
new file mode 100644
index 000000000..410872d04
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryCustomCardItemUseCaseTest.kt
@@ -0,0 +1,68 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.OperatorMessage
+import com.glia.androidsdk.chat.SingleChoiceAttachment
+import com.glia.widgets.chat.model.ChatItem
+import com.glia.widgets.chat.model.CustomCardChatItem
+import com.glia.widgets.chat.model.VisitorMessageItem
+import org.junit.After
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class AppendHistoryCustomCardItemUseCaseTest {
+    private val items: MutableList = mutableListOf()
+
+    private lateinit var customCardTypeUseCase: CustomCardTypeUseCase
+    private lateinit var customCardShouldShowUseCase: CustomCardShouldShowUseCase
+
+    private lateinit var useCase: AppendHistoryCustomCardItemUseCase
+
+    @Before
+    fun setUp() {
+        customCardTypeUseCase = mock()
+        customCardShouldShowUseCase = mock()
+
+        whenever(customCardTypeUseCase.execute(any())) doReturn 100
+
+        useCase = AppendHistoryCustomCardItemUseCase(customCardTypeUseCase, customCardShouldShowUseCase)
+    }
+
+    @After
+    fun tearDown() {
+        items.clear()
+    }
+
+    @Test
+    fun `invoke adds CustomCardChatItem when customCardShouldShowUseCase returns true`() {
+        whenever(customCardShouldShowUseCase.execute(any(), any(), any())) doReturn true
+
+        useCase(items, mock(), 120)
+
+        assertTrue(items.isNotEmpty())
+        assertTrue(items.first() is CustomCardChatItem)
+    }
+
+    @Test
+    fun `invoke adds VisitorMessage History item  when chatMessage has selectedOptionText`() {
+        val singleChoiceAttachment: SingleChoiceAttachment = mock()
+        whenever(singleChoiceAttachment.selectedOptionText) doReturn "selected option text"
+        val chatMessage: OperatorMessage = mock()
+
+        whenever(chatMessage.attachment) doReturn singleChoiceAttachment
+        whenever(chatMessage.id) doReturn "id"
+        whenever(chatMessage.timestamp) doReturn -1
+
+        whenever(customCardShouldShowUseCase.execute(any(), any(), any())) doReturn true
+
+        useCase(items, chatMessage, 120)
+
+        assertTrue(items.isNotEmpty())
+        assertTrue(items.first() is CustomCardChatItem)
+        assertTrue(items[1] is VisitorMessageItem.History)
+    }
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryOperatorChatItemUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryOperatorChatItemUseCaseTest.kt
new file mode 100644
index 000000000..f9e3c4908
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryOperatorChatItemUseCaseTest.kt
@@ -0,0 +1,73 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.OperatorMessage
+import com.glia.widgets.chat.domain.gva.IsGvaUseCase
+import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class AppendHistoryOperatorChatItemUseCaseTest {
+    private lateinit var isGvaUseCase: IsGvaUseCase
+    private lateinit var customCardAdapterTypeUseCase: CustomCardAdapterTypeUseCase
+    private lateinit var appendGvaMessageItemUseCase: AppendGvaMessageItemUseCase
+    private lateinit var appendHistoryCustomCardItemUseCase: AppendHistoryCustomCardItemUseCase
+    private lateinit var appendHistoryResponseCardOrTextItemUseCase: AppendHistoryResponseCardOrTextItemUseCase
+    private lateinit var useCase: AppendHistoryOperatorChatItemUseCase
+
+    private lateinit var chatMessageInternal: ChatMessageInternal
+
+    @Before
+    fun setUp() {
+        isGvaUseCase = mock()
+        customCardAdapterTypeUseCase = mock()
+        appendGvaMessageItemUseCase = mock()
+        appendHistoryCustomCardItemUseCase = mock()
+        appendHistoryResponseCardOrTextItemUseCase = mock()
+        chatMessageInternal = mock()
+
+        useCase = AppendHistoryOperatorChatItemUseCase(
+            isGvaUseCase,
+            customCardAdapterTypeUseCase,
+            appendGvaMessageItemUseCase,
+            appendHistoryCustomCardItemUseCase,
+            appendHistoryResponseCardOrTextItemUseCase
+        )
+    }
+
+    @Test
+    fun `invoke appends GVA message item when chatMessage is GVA`() {
+        whenever(chatMessageInternal.chatMessage) doReturn mock()
+        whenever(isGvaUseCase.invoke(any())) doReturn true
+
+        useCase(mock(), chatMessageInternal, isLatest = true, showChatHead = true)
+
+        verify(appendGvaMessageItemUseCase).invoke(any(), any(), any())
+    }
+
+    @Test
+    fun `invoke appends custom card message item when chatMessage is custom card`() {
+        whenever(chatMessageInternal.chatMessage) doReturn mock()
+        whenever(isGvaUseCase.invoke(any())) doReturn false
+        whenever(customCardAdapterTypeUseCase.invoke(any())) doReturn 1
+
+        useCase(mock(), chatMessageInternal, isLatest = true, showChatHead = true)
+
+        verify(appendHistoryCustomCardItemUseCase).invoke(any(), any(), any())
+    }
+
+    @Test
+    fun `invoke appends response card or text message item when chatMessage is not GVA or CustomCard`() {
+        whenever(chatMessageInternal.chatMessage) doReturn mock()
+        whenever(isGvaUseCase.invoke(any())) doReturn false
+        whenever(customCardAdapterTypeUseCase.invoke(any())) doReturn null
+
+        useCase(mock(), chatMessageInternal, isLatest = true, showChatHead = true)
+
+        verify(appendHistoryResponseCardOrTextItemUseCase).invoke(any(), any(), any(), any())
+    }
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryResponseCardOrTextItemUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryResponseCardOrTextItemUseCaseTest.kt
new file mode 100644
index 000000000..96fd476bc
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryResponseCardOrTextItemUseCaseTest.kt
@@ -0,0 +1,174 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.AttachmentFile
+import com.glia.androidsdk.chat.FilesAttachment
+import com.glia.androidsdk.chat.SingleChoiceAttachment
+import com.glia.widgets.chat.MockChatMessageInternal
+import com.glia.widgets.chat.model.ChatItem
+import com.glia.widgets.chat.model.OperatorAttachmentItem
+import com.glia.widgets.chat.model.OperatorMessageItem
+import org.junit.After
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class AppendHistoryResponseCardOrTextItemUseCaseTest {
+    private var mockChatMessageInternal: MockChatMessageInternal = MockChatMessageInternal()
+    private lateinit var useCase: AppendHistoryResponseCardOrTextItemUseCase
+
+    private lateinit var mapOperatorAttachmentUseCase: MapOperatorAttachmentUseCase
+    private lateinit var mapOperatorPlainTextUseCase: MapOperatorPlainTextUseCase
+    private lateinit var mapResponseCardUseCase: MapResponseCardUseCase
+    private val items: MutableList = mutableListOf()
+
+    @Before
+    fun setUp() {
+        mapOperatorAttachmentUseCase = mock()
+        mapOperatorPlainTextUseCase = mock()
+        mapResponseCardUseCase = mock()
+
+        useCase = spy(AppendHistoryResponseCardOrTextItemUseCase(mapOperatorAttachmentUseCase, mapOperatorPlainTextUseCase, mapResponseCardUseCase))
+
+        mockChatMessageInternal.mockChatMessage()
+        mockChatMessageInternal.mockOperatorProperties()
+    }
+
+    @After
+    fun tearDown() {
+        mockChatMessageInternal.reset()
+        items.clear()
+    }
+
+    @Test
+    fun `addResponseCard adds OperatorMessageItem_ResponseCard to provided list`() {
+        whenever(mapResponseCardUseCase.invoke(any(), any(), any())) doReturn mock()
+        useCase.addResponseCard(items, mock(), mockChatMessageInternal.chatMessageInternal, true)
+
+        assertTrue(items.count() == 1)
+        assertTrue(items.first() is OperatorMessageItem.ResponseCard)
+    }
+
+    @Test
+    fun `addPlainTextAndAttachments adds OperatorMessageItem_PlainText when chatMessage content is not empty`() {
+        whenever(mockChatMessageInternal.chatMessageInternal.chatMessage.attachment) doReturn mock()
+        whenever(mapOperatorPlainTextUseCase.invoke(any(), any())) doReturn mock()
+
+        useCase.addPlainTextAndAttachments(items, mockChatMessageInternal.chatMessageInternal, true)
+
+        assertTrue(items.count() == 1)
+        assertTrue(items.first() is OperatorMessageItem.PlainText)
+    }
+
+    @Test
+    fun `addPlainTextAndAttachments does not add OperatorMessageItem_PlainText when chatMessage content is null or empty`() {
+        whenever(mockChatMessageInternal.chatMessageInternal.chatMessage.attachment) doReturn mock()
+        whenever(mockChatMessageInternal.chatMessageInternal.chatMessage.content) doReturn ""
+        whenever(mapOperatorPlainTextUseCase.invoke(any(), any())) doReturn mock()
+
+        useCase.addPlainTextAndAttachments(items, mockChatMessageInternal.chatMessageInternal, true)
+
+        assertTrue(items.isEmpty())
+    }
+
+    @Test
+    fun `addPlainTextAndAttachments adds Operator Attachment before plain text when both present`() {
+        val filesAttachment: FilesAttachment = mock()
+        val file: AttachmentFile = mock()
+        whenever(filesAttachment.files) doReturn arrayOf(file)
+
+        whenever(mapOperatorAttachmentUseCase.invoke(any(), any(), any())) doReturn mock()
+
+        whenever(mockChatMessageInternal.chatMessageInternal.chatMessage.attachment) doReturn filesAttachment
+
+        whenever(mapOperatorPlainTextUseCase.invoke(any(), any())) doReturn mock()
+
+        useCase.addPlainTextAndAttachments(items, mockChatMessageInternal.chatMessageInternal, true)
+
+        assertTrue(items.count() == 2)
+        assertTrue(items.first() is OperatorAttachmentItem.File)
+        assertTrue(items[1] is OperatorMessageItem.PlainText)
+    }
+
+    @Test
+    fun `addPlainTextAndAttachments adds Operator Attachment in reversed order if there are more than one`() {
+        val filesAttachment: FilesAttachment = mock()
+        val file1: AttachmentFile = mock()
+        val file2: AttachmentFile = mock()
+        whenever(filesAttachment.files) doReturn arrayOf(file1, file2)
+
+        val operatorAttachment1 = mock()
+        val operatorAttachment2 = mock()
+
+        whenever(mapOperatorAttachmentUseCase.invoke(eq(file1), any(), any())) doReturn operatorAttachment1
+        whenever(mapOperatorAttachmentUseCase.invoke(eq(file2), any(), any())) doReturn operatorAttachment2
+
+        whenever(mockChatMessageInternal.chatMessageInternal.chatMessage.attachment) doReturn filesAttachment
+
+        whenever(mapOperatorPlainTextUseCase.invoke(any(), any())) doReturn mock()
+
+        useCase.addPlainTextAndAttachments(items, mockChatMessageInternal.chatMessageInternal, true)
+
+        assertTrue(items.count() == 3)
+        assertTrue(items.first() is OperatorAttachmentItem.File)
+        assertTrue(items[1] is OperatorAttachmentItem.Image)
+        assertTrue(items[2] is OperatorMessageItem.PlainText)
+    }
+
+    @Test
+    fun `addPlainTextAndAttachments adds Response Card when chatMessage is the latest in history and has SingleChoiceAttachment`() {
+        val singleChoiceAttachment: SingleChoiceAttachment = mock()
+        whenever(singleChoiceAttachment.options) doReturn arrayOf(mock())
+
+        whenever(mockChatMessageInternal.chatMessageInternal.chatMessage.attachment) doReturn singleChoiceAttachment
+
+        useCase.invoke(items, mockChatMessageInternal.chatMessageInternal, isLatest = true, showChatHead = true)
+
+        verify(useCase).addResponseCard(any(), any(), any(), any())
+        verify(useCase, never()).addPlainTextAndAttachments(any(), any(), any())
+    }
+
+    @Test
+    fun `addPlainTextAndAttachments adds Plain Text when chatMessage is not the latest in history and has SingleChoiceAttachment`() {
+        val singleChoiceAttachment: SingleChoiceAttachment = mock()
+        whenever(singleChoiceAttachment.options) doReturn arrayOf(mock())
+
+        whenever(mockChatMessageInternal.chatMessageInternal.chatMessage.attachment) doReturn singleChoiceAttachment
+
+        useCase.invoke(items, mockChatMessageInternal.chatMessageInternal, isLatest = false, showChatHead = true)
+
+        verify(useCase, never()).addResponseCard(any(), any(), any(), any())
+        verify(useCase).addPlainTextAndAttachments(any(), any(), any())
+    }
+
+    @Test
+    fun `addPlainTextAndAttachments adds Plain Text when chatMessage is the latest in history and has empty SingleChoiceAttachment`() {
+        val singleChoiceAttachment: SingleChoiceAttachment = mock()
+        whenever(singleChoiceAttachment.options) doReturn arrayOf()
+
+        whenever(mockChatMessageInternal.chatMessageInternal.chatMessage.attachment) doReturn singleChoiceAttachment
+
+        useCase.invoke(items, mockChatMessageInternal.chatMessageInternal, isLatest = false, showChatHead = true)
+
+        verify(useCase, never()).addResponseCard(any(), any(), any(), any())
+        verify(useCase).addPlainTextAndAttachments(any(), any(), any())
+    }
+
+    @Test
+    fun `addPlainTextAndAttachments adds Plain Text when chatMessage does not has SingleChoiceAttachment`() {
+        whenever(mockChatMessageInternal.chatMessageInternal.chatMessage.attachment) doReturn mock()
+
+        useCase.invoke(items, mockChatMessageInternal.chatMessageInternal, isLatest = false, showChatHead = true)
+
+        verify(useCase, never()).addResponseCard(any(), any(), any(), any())
+        verify(useCase).addPlainTextAndAttachments(any(), any(), any())
+    }
+
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryVisitorChatItemUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryVisitorChatItemUseCaseTest.kt
new file mode 100644
index 000000000..9b75b78c3
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryVisitorChatItemUseCaseTest.kt
@@ -0,0 +1,106 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.AttachmentFile
+import com.glia.androidsdk.chat.FilesAttachment
+import com.glia.androidsdk.chat.VisitorMessage
+import com.glia.widgets.chat.model.ChatItem
+import com.glia.widgets.chat.model.VisitorAttachmentItem
+import com.glia.widgets.chat.model.VisitorMessageItem
+import org.junit.After
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class AppendHistoryVisitorChatItemUseCaseTest {
+    private val items: MutableList = mutableListOf()
+    private lateinit var mapVisitorAttachmentUseCase: MapVisitorAttachmentUseCase
+    private lateinit var visitorMessage: VisitorMessage
+    private lateinit var useCase: AppendHistoryVisitorChatItemUseCase
+
+    @Before
+    fun setUp() {
+        mapVisitorAttachmentUseCase = mock()
+        visitorMessage = mock()
+        useCase = AppendHistoryVisitorChatItemUseCase(mapVisitorAttachmentUseCase)
+    }
+
+    @After
+    fun tearDown() {
+        items.clear()
+    }
+
+    @Test
+    fun `invoke adds VisitorMessageItem_History item to the list when visitor message content is not empty`() {
+        whenever(mapVisitorAttachmentUseCase.invoke(any(), any(), any())) doReturn mock()
+
+        whenever(visitorMessage.id) doReturn "id"
+        whenever(visitorMessage.timestamp) doReturn -1
+        whenever(visitorMessage.content) doReturn "content"
+
+        useCase(items, visitorMessage)
+
+        assertTrue(items.count() == 1)
+        assertTrue(items.last() is VisitorMessageItem.History)
+    }
+
+    @Test
+    fun `invoke does not add VisitorMessageItem_History item to the list when visitor message content is null or empty`() {
+        whenever(mapVisitorAttachmentUseCase.invoke(any(), any(), any())) doReturn mock()
+        whenever(visitorMessage.content) doReturn ""
+        useCase(items, visitorMessage)
+
+        assertTrue(items.isEmpty())
+    }
+
+
+    @Test
+    fun `invoke adds VisitorAttachmentItem before VisitorMessageItem_History when both present`() {
+        whenever(mapVisitorAttachmentUseCase.invoke(any(), any(), any())) doReturn mock()
+
+        val filesAttachment: FilesAttachment = mock()
+        val file: AttachmentFile = mock()
+        whenever(filesAttachment.files) doReturn arrayOf(file)
+
+        whenever(visitorMessage.id) doReturn "id"
+        whenever(visitorMessage.timestamp) doReturn -1
+        whenever(visitorMessage.content) doReturn "content"
+        whenever(visitorMessage.attachment) doReturn filesAttachment
+
+        useCase(items, visitorMessage)
+
+        assertTrue(items.count() == 2)
+        assertTrue(items.first() is VisitorAttachmentItem.File)
+        assertTrue(items[1] is VisitorMessageItem.History)
+    }
+
+    @Test
+    fun `invoke adds VisitorAttachmentItems in reversed order if there are more than one`() {
+        val filesAttachment: FilesAttachment = mock()
+        val file1: AttachmentFile = mock()
+        val file2: AttachmentFile = mock()
+        whenever(filesAttachment.files) doReturn arrayOf(file1, file2)
+
+        val visitorAttachment1 = mock()
+        val visitorAttachment2 = mock()
+
+        whenever(mapVisitorAttachmentUseCase.invoke(eq(file1), any(), any())) doReturn visitorAttachment1
+        whenever(mapVisitorAttachmentUseCase.invoke(eq(file2), any(), any())) doReturn visitorAttachment2
+
+        whenever(visitorMessage.id) doReturn "id"
+        whenever(visitorMessage.timestamp) doReturn -1
+        whenever(visitorMessage.content) doReturn "content"
+        whenever(visitorMessage.attachment) doReturn filesAttachment
+
+        useCase(items, visitorMessage)
+
+        assertTrue(items.count() == 3)
+        assertTrue(items.first() is VisitorAttachmentItem.File)
+        assertTrue(items[1] is VisitorAttachmentItem.Image)
+        assertTrue(items[2] is VisitorMessageItem.History)
+    }
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewChatMessageUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewChatMessageUseCaseTest.kt
new file mode 100644
index 000000000..a8b98846f
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewChatMessageUseCaseTest.kt
@@ -0,0 +1,85 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.ChatMessage
+import com.glia.androidsdk.chat.OperatorMessage
+import com.glia.androidsdk.chat.SystemMessage
+import com.glia.androidsdk.chat.VisitorMessage
+import com.glia.widgets.chat.ChatManager
+import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class AppendNewChatMessageUseCaseTest {
+    private lateinit var appendNewOperatorMessageUseCase: AppendNewOperatorMessageUseCase
+    private lateinit var appendNewVisitorMessageUseCase: AppendNewVisitorMessageUseCase
+    private lateinit var appendSystemMessageItemUseCase: AppendSystemMessageItemUseCase
+    private lateinit var state: ChatManager.State
+    private lateinit var chatMessageInternal: ChatMessageInternal
+    private lateinit var chatMessage: ChatMessage
+    private lateinit var useCase: AppendNewChatMessageUseCase
+
+    @Before
+    fun setUp() {
+        appendNewOperatorMessageUseCase = mock()
+        appendNewVisitorMessageUseCase = mock()
+        appendSystemMessageItemUseCase = mock()
+        state = mock()
+        chatMessageInternal = mock()
+        useCase = AppendNewChatMessageUseCase(appendNewOperatorMessageUseCase, appendNewVisitorMessageUseCase, appendSystemMessageItemUseCase)
+    }
+
+    @Test
+    fun `invoke appends visitor message and reset operator when message is Visitor message`() {
+        mockCHatMessage()
+        useCase(state, chatMessageInternal)
+
+        verify(appendNewVisitorMessageUseCase).invoke(any(), any())
+        verify(appendNewOperatorMessageUseCase, never()).invoke(any(), any())
+        verify(appendSystemMessageItemUseCase, never()).invoke(any(), any())
+        verify(state).resetOperator()
+    }
+
+    @Test
+    fun `invoke appends system message and reset operator when message is System message`() {
+        mockCHatMessage()
+        useCase(state, chatMessageInternal)
+
+        verify(appendNewVisitorMessageUseCase, never()).invoke(any(), any())
+        verify(appendNewOperatorMessageUseCase, never()).invoke(any(), any())
+        verify(appendSystemMessageItemUseCase).invoke(any(), any())
+        verify(state).resetOperator()
+    }
+
+    @Test
+    fun `invoke appends operator message and do not reset operator when message is Operator message`() {
+        mockCHatMessage()
+        useCase(state, chatMessageInternal)
+
+        verify(appendNewVisitorMessageUseCase, never()).invoke(any(), any())
+        verify(appendNewOperatorMessageUseCase).invoke(any(), any())
+        verify(appendSystemMessageItemUseCase, never()).invoke(any(), any())
+        verify(state, never()).resetOperator()
+    }
+
+    @Test
+    fun `invoke does nothing when message is unknown message`() {
+        mockCHatMessage()
+        useCase(state, chatMessageInternal)
+
+        verify(appendNewVisitorMessageUseCase, never()).invoke(any(), any())
+        verify(appendNewOperatorMessageUseCase, never()).invoke(any(), any())
+        verify(appendSystemMessageItemUseCase, never()).invoke(any(), any())
+        verify(state, never()).resetOperator()
+    }
+
+    private inline fun  mockCHatMessage() {
+        chatMessage = mock()
+        whenever(chatMessageInternal.chatMessage) doReturn chatMessage
+    }
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewOperatorMessageUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewOperatorMessageUseCaseTest.kt
new file mode 100644
index 000000000..1129d222c
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewOperatorMessageUseCaseTest.kt
@@ -0,0 +1,130 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.OperatorMessage
+import com.glia.widgets.chat.ChatManager
+import com.glia.widgets.chat.domain.gva.IsGvaUseCase
+import com.glia.widgets.chat.model.OperatorChatItem
+import com.glia.widgets.chat.model.OperatorMessageItem
+import com.glia.widgets.chat.model.VisitorMessageItem
+import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class AppendNewOperatorMessageUseCaseTest {
+    private lateinit var isGvaUseCase: IsGvaUseCase
+    private lateinit var customCardAdapterTypeUseCase: CustomCardAdapterTypeUseCase
+    private lateinit var appendGvaMessageItemUseCase: AppendGvaMessageItemUseCase
+    private lateinit var appendHistoryCustomCardItemUseCase: AppendHistoryCustomCardItemUseCase
+    private lateinit var appendNewResponseCardOrTextItemUseCase: AppendNewResponseCardOrTextItemUseCase
+    private lateinit var operatorMessage: OperatorMessage
+    private lateinit var chatMessageInternal: ChatMessageInternal
+    private lateinit var state: ChatManager.State
+
+    private lateinit var useCase: AppendNewOperatorMessageUseCase
+
+    @Before
+    fun setUp() {
+        isGvaUseCase = mock()
+        customCardAdapterTypeUseCase = mock()
+        appendGvaMessageItemUseCase = mock()
+        appendHistoryCustomCardItemUseCase = mock()
+        appendNewResponseCardOrTextItemUseCase = mock()
+
+        operatorMessage = mock()
+        chatMessageInternal = mock()
+        whenever(chatMessageInternal.chatMessage) doReturn operatorMessage
+
+        state = spy(ChatManager.State())
+
+        useCase = AppendNewOperatorMessageUseCase(
+            isGvaUseCase,
+            customCardAdapterTypeUseCase,
+            appendGvaMessageItemUseCase,
+            appendHistoryCustomCardItemUseCase,
+            appendNewResponseCardOrTextItemUseCase
+        )
+    }
+
+    @Test
+    fun `invoke adds GVA message when chatMessage type is GVA`() {
+        whenever(isGvaUseCase(any())) doReturn true
+        useCase(state, chatMessageInternal)
+        verify(appendGvaMessageItemUseCase).invoke(any(), any(), any())
+        verify(appendHistoryCustomCardItemUseCase, never()).invoke(any(), any(), any())
+        verify(appendNewResponseCardOrTextItemUseCase, never()).invoke(any(), any())
+    }
+
+    @Test
+    fun `invoke adds Custom Card message when chatMessage type is Custom Card`() {
+        whenever(customCardAdapterTypeUseCase(any())) doReturn 100
+        useCase(state, chatMessageInternal)
+        verify(appendGvaMessageItemUseCase, never()).invoke(any(), any(), any())
+        verify(appendHistoryCustomCardItemUseCase).invoke(any(), any(), any())
+        verify(appendNewResponseCardOrTextItemUseCase, never()).invoke(any(), any())
+    }
+
+    @Test
+    fun `invoke adds ResponseCard or Text message when chatMessage type is not Custom Card or GVA`() {
+        whenever(customCardAdapterTypeUseCase(any())) doReturn null
+        whenever(isGvaUseCase(any())) doReturn false
+        useCase(state, chatMessageInternal)
+        verify(appendGvaMessageItemUseCase, never()).invoke(any(), any(), any())
+        verify(appendHistoryCustomCardItemUseCase, never()).invoke(any(), any(), any())
+        verify(appendNewResponseCardOrTextItemUseCase).invoke(any(), any())
+    }
+
+    @Test
+    fun `invoke updates addedMessagesCount when new message added`() {
+        whenever(customCardAdapterTypeUseCase(any())) doReturn null
+        whenever(isGvaUseCase(any())) doReturn false
+        doAnswer {
+            state.chatItems.add(mock())
+            state.chatItems.add(mock())
+        }.whenever(appendNewResponseCardOrTextItemUseCase).invoke(any(), any())
+
+        useCase(state, chatMessageInternal)
+        assertEquals(state.addedMessagesCount, 2)
+    }
+
+    @Test
+    fun `invoke resets operator when the last item is not OperatorChatItem`() {
+        whenever(customCardAdapterTypeUseCase(any())) doReturn null
+        whenever(isGvaUseCase(any())) doReturn false
+        doAnswer {
+            state.chatItems.add(mock())
+            state.chatItems.add(mock())
+        }.whenever(appendNewResponseCardOrTextItemUseCase).invoke(any(), any())
+
+        useCase(state, chatMessageInternal)
+        verify(state).resetOperator()
+        verify(state, never()).isOperatorChanged(any())
+    }
+
+    @Test
+    fun `invoke changes hides operator image for previous item when operator not changed`() {
+        whenever(customCardAdapterTypeUseCase(any())) doReturn null
+        whenever(isGvaUseCase(any())) doReturn false
+
+        val operatorChatItem: OperatorChatItem = OperatorMessageItem.PlainText("id", 1, true, "img", "operator_id", "name", "content")
+        state.lastMessageWithVisibleOperatorImage = operatorChatItem
+
+        doAnswer {
+            state.chatItems.add(operatorChatItem)
+        }.whenever(appendNewResponseCardOrTextItemUseCase).invoke(any(), any())
+
+        whenever(state.isOperatorChanged(operatorChatItem)) doReturn false
+
+        useCase(state, chatMessageInternal)
+        assertFalse((state.chatItems.last() as OperatorChatItem).showChatHead)
+    }
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewResponseCardOrTextItemUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewResponseCardOrTextItemUseCaseTest.kt
new file mode 100644
index 000000000..6d65a4406
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewResponseCardOrTextItemUseCaseTest.kt
@@ -0,0 +1,111 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.ChatMessage
+import com.glia.androidsdk.chat.FilesAttachment
+import com.glia.androidsdk.chat.SingleChoiceAttachment
+import com.glia.widgets.chat.model.ChatItem
+import com.glia.widgets.chat.model.OperatorAttachmentItem
+import com.glia.widgets.chat.model.OperatorMessageItem
+import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
+import org.junit.After
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class AppendNewResponseCardOrTextItemUseCaseTest {
+    private val chatItems: MutableList = mutableListOf()
+    private lateinit var mapOperatorAttachmentUseCase: MapOperatorAttachmentUseCase
+    private lateinit var mapOperatorPlainTextUseCase: MapOperatorPlainTextUseCase
+    private lateinit var mapResponseCardUseCase: MapResponseCardUseCase
+    private lateinit var useCase: AppendNewResponseCardOrTextItemUseCase
+    private lateinit var chatMessageInternal: ChatMessageInternal
+    private lateinit var chatMessage: ChatMessage
+
+    @Before
+    fun setUp() {
+        chatMessageInternal = mock()
+        chatMessage = mock()
+        whenever(chatMessageInternal.chatMessage) doReturn chatMessage
+
+        mapOperatorAttachmentUseCase = mock()
+        mapOperatorPlainTextUseCase = mock()
+        mapResponseCardUseCase = mock()
+        useCase = spy(AppendNewResponseCardOrTextItemUseCase(mapOperatorAttachmentUseCase, mapOperatorPlainTextUseCase, mapResponseCardUseCase))
+    }
+
+    @After
+    fun tearDown() {
+        chatItems.clear()
+    }
+
+    @Test
+    fun `addResponseCard add ResponseCard to the chat items list`() {
+        whenever(mapResponseCardUseCase(any(), any(), any())) doReturn mock()
+        useCase.addResponseCard(chatItems, mock(), mock())
+        assertTrue(chatItems.isNotEmpty())
+        assertTrue(chatItems.last() is OperatorMessageItem.ResponseCard)
+    }
+
+    @Test
+    fun `addPlainTextAndAttachments does not add anything to chat when message content and attachment are empty`() {
+        whenever(chatMessage.content) doReturn ""
+        useCase.addPlainTextAndAttachments(chatItems, chatMessageInternal)
+        assertTrue(chatItems.isEmpty())
+    }
+
+    @Test
+    fun `addPlainTextAndAttachments adds attachments after text when both are available`() {
+        val attachment: FilesAttachment = mock()
+        whenever(attachment.files) doReturn arrayOf(mock())
+
+        whenever(chatMessage.content) doReturn "content"
+        whenever(chatMessage.attachment) doReturn attachment
+        whenever(mapOperatorPlainTextUseCase(any(), any())) doReturn mock()
+
+        val operatorAttachmentItemTrue = mock().apply { whenever(showChatHead) doReturn true }
+        val operatorAttachmentItemFalse = mock().apply { whenever(showChatHead) doReturn false }
+        whenever(mapOperatorAttachmentUseCase(any(), any(), eq(true))) doReturn operatorAttachmentItemTrue
+        whenever(mapOperatorAttachmentUseCase(any(), any(), eq(false))) doReturn operatorAttachmentItemFalse
+
+        useCase.addPlainTextAndAttachments(chatItems, chatMessageInternal)
+
+        assertTrue(chatItems.count() == 2)
+        assertTrue(chatItems.first() is OperatorMessageItem.PlainText)
+        assertTrue((chatItems.last() as OperatorAttachmentItem).showChatHead)
+    }
+
+    @Test
+    fun `invoke adds ResponseCard when message has SingleChoiceAttachment with at least one option`() {
+        val attachment: SingleChoiceAttachment = mock()
+        whenever(attachment.options) doReturn arrayOf(mock())
+
+        whenever(chatMessage.attachment) doReturn attachment
+
+        useCase(chatItems, chatMessageInternal)
+
+        verify(useCase).addResponseCard(any(), any(), any())
+        verify(useCase, never()).addPlainTextAndAttachments(any(), any())
+    }
+
+    @Test
+    fun `invoke adds PlainText when message does not have SingleChoiceAttachment`() {
+        val attachment: FilesAttachment = mock()
+        whenever(attachment.files) doReturn arrayOf(mock())
+
+        whenever(chatMessage.content) doReturn "content"
+        whenever(chatMessage.attachment) doReturn attachment
+
+        useCase(chatItems, chatMessageInternal)
+
+        verify(useCase, never()).addResponseCard(any(), any(), any())
+        verify(useCase).addPlainTextAndAttachments(any(), any())
+    }
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewVisitorMessageUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewVisitorMessageUseCaseTest.kt
new file mode 100644
index 000000000..9663eac14
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewVisitorMessageUseCaseTest.kt
@@ -0,0 +1,163 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.AttachmentFile
+import com.glia.androidsdk.chat.FilesAttachment
+import com.glia.androidsdk.chat.VisitorMessage
+import com.glia.widgets.chat.ChatManager
+import com.glia.widgets.chat.model.VisitorAttachmentItem
+import com.glia.widgets.chat.model.VisitorChatItem
+import com.glia.widgets.chat.model.VisitorMessageItem
+import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+class AppendNewVisitorMessageUseCaseTest {
+    private lateinit var mapVisitorAttachmentUseCase: MapVisitorAttachmentUseCase
+    private lateinit var useCase: AppendNewVisitorMessageUseCase
+    private lateinit var state: ChatManager.State
+
+    private lateinit var chatMessageInternal: ChatMessageInternal
+    private lateinit var visitorMessage: VisitorMessage
+
+    @Before
+    fun setUp() {
+        mapVisitorAttachmentUseCase = mock()
+        useCase = spy(AppendNewVisitorMessageUseCase(mapVisitorAttachmentUseCase))
+        state = ChatManager.State()
+
+        chatMessageInternal = mock()
+        visitorMessage = mock()
+
+        whenever(chatMessageInternal.chatMessage) doReturn visitorMessage
+
+    }
+
+    @Test
+    fun `addUnsentItem returns false when unsentItems is empty`() {
+        assertFalse(useCase.addUnsentItem(state, visitorMessage))
+    }
+
+    @Test
+    fun `addUnsentItem returns false when unsentItems does not contain received message`() {
+        state.unsentItems.add(mock())
+        assertFalse(useCase.addUnsentItem(state, visitorMessage))
+    }
+
+    @Test
+    fun `addUnsentItem returns false when unsentItems contain received message but does not exist in chatItems`() {
+        val content = "content"
+        whenever(visitorMessage.content) doReturn content
+
+        state.unsentItems.add(VisitorMessageItem.Unsent(message = content))
+        assertFalse(useCase.addUnsentItem(state, visitorMessage))
+    }
+
+    @Test
+    fun `addUnsentItem returns true when unsentItems and chatItems contain received message`() {
+        val lastDelivered: VisitorChatItem = VisitorMessageItem.Delivered("id", 1, "message")
+
+        useCase.lastDeliveredItem = lastDelivered
+        state.chatItems.add(lastDelivered)
+
+        val content = "content"
+        whenever(visitorMessage.content) doReturn content
+        whenever(visitorMessage.id) doReturn "id"
+        whenever(visitorMessage.timestamp) doReturn 1
+
+        val unsentMessage = VisitorMessageItem.Unsent(message = content)
+        state.unsentItems.add(unsentMessage)
+        state.chatItems.add(unsentMessage)
+
+        assertTrue(useCase.addUnsentItem(state, visitorMessage))
+        assertTrue(state.unsentItems.isEmpty())
+        assertTrue(state.chatItems.last() is VisitorMessageItem.Delivered)
+        assertTrue(state.chatItems.first() is VisitorMessageItem.New)
+    }
+
+    @Test
+    fun `invoke does nothing when addUnsentItem returns true`() {
+        doReturn(true).whenever(useCase).addUnsentItem(any(), any())
+
+        useCase(state, chatMessageInternal)
+
+        assertTrue(state.chatItems.isEmpty())
+        assertNull(useCase.lastDeliveredItem)
+    }
+
+    @Test
+    fun `invoke appends VisitorMessageItem_Delivered when message has no attachment`() {
+        doReturn(false).whenever(useCase).addUnsentItem(any(), any())
+
+        whenever(visitorMessage.content) doReturn "content"
+        whenever(visitorMessage.timestamp) doReturn 1
+        whenever(visitorMessage.id) doReturn "1"
+
+        useCase(state, chatMessageInternal)
+
+        assertTrue(state.chatItems.count() == 1)
+        assertTrue(state.chatItems.first() is VisitorMessageItem.Delivered)
+    }
+
+    @Test
+    fun `invoke appends VisitorMessageItem_New when message has files`() {
+        doReturn(false).whenever(useCase).addUnsentItem(any(), any())
+
+        val filesAttachment: FilesAttachment = mock()
+        val file: AttachmentFile = mock()
+        whenever(filesAttachment.files) doReturn arrayOf(file)
+
+        whenever(visitorMessage.attachment) doReturn filesAttachment
+        whenever(visitorMessage.content) doReturn "content"
+        whenever(visitorMessage.timestamp) doReturn 1
+        whenever(visitorMessage.id) doReturn "1"
+
+        val attachmentWithShowDeliveredTrue = mock().apply { whenever(this.showDelivered) doReturn true }
+        val attachmentWithShowDeliveredFalse = mock().apply { whenever(this.showDelivered) doReturn false }
+
+        whenever(mapVisitorAttachmentUseCase(any(), any(), eq(true))) doReturn attachmentWithShowDeliveredTrue
+        whenever(mapVisitorAttachmentUseCase(any(), any(), eq(false))) doReturn attachmentWithShowDeliveredFalse
+
+        useCase(state, chatMessageInternal)
+
+        assertTrue(state.chatItems.count() == 2)
+        assertTrue(state.chatItems.first() is VisitorMessageItem.New)
+        assertTrue(state.chatItems.last() is VisitorAttachmentItem.File)
+        assertTrue((state.chatItems.last() as VisitorChatItem).showDelivered)
+        assertTrue(useCase.lastDeliveredItem is VisitorAttachmentItem.File)
+        assertEquals(useCase.lastDeliveredItem, state.chatItems.last())
+    }
+
+    @Test
+    fun `invoke changes lastDeliveredItem when it exists`() {
+        doReturn(false).whenever(useCase).addUnsentItem(any(), any())
+
+        whenever(visitorMessage.content) doReturn "content"
+        whenever(visitorMessage.timestamp) doReturn 1
+        whenever(visitorMessage.id) doReturn "1"
+
+        useCase(state, chatMessageInternal)
+
+        assertTrue(state.chatItems.count() == 1)
+        assertTrue((state.chatItems.last() as VisitorMessageItem).showDelivered)
+        assertEquals(state.chatItems.last(), useCase.lastDeliveredItem)
+
+        whenever(visitorMessage.id) doReturn "2"
+
+        useCase(state, chatMessageInternal)
+
+        assertTrue(state.chatItems.count() == 2)
+        assertFalse((state.chatItems.first() as VisitorMessageItem).showDelivered)
+        assertTrue((state.chatItems.last() as VisitorMessageItem).showDelivered)
+        assertEquals(state.chatItems.last(), useCase.lastDeliveredItem)
+    }
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendSystemMessageItemUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendSystemMessageItemUseCaseTest.kt
new file mode 100644
index 000000000..1cdfb475f
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendSystemMessageItemUseCaseTest.kt
@@ -0,0 +1,55 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.SystemMessage
+import com.glia.widgets.chat.model.ChatItem
+import com.glia.widgets.chat.model.OperatorStatusItem
+import com.glia.widgets.chat.model.SystemChatItem
+import org.junit.After
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class AppendSystemMessageItemUseCaseTest {
+    private val items: MutableList = mutableListOf()
+
+    private lateinit var useCase: AppendSystemMessageItemUseCase
+    private lateinit var systemMessage: SystemMessage
+
+    @Before
+    fun setUp() {
+        useCase = AppendSystemMessageItemUseCase()
+
+        systemMessage = mock().apply {
+            whenever(id) doReturn "id"
+            whenever(timestamp) doReturn -1
+            whenever(content) doReturn "content"
+        }
+
+    }
+
+    @After
+    fun tearDown() {
+        items.clear()
+    }
+
+    @Test
+    fun `invoke adds system message at the end of list when OperatorStatusItem_InQueue is not present in list`() {
+        useCase(items, systemMessage)
+
+        assertTrue(items.count() == 1)
+        assertTrue(items.last() is SystemChatItem)
+    }
+
+    @Test
+    fun `invoke adds system message before OperatorStatusItem_InQueue when it is the latest item in list`() {
+        items += mock()
+        useCase(items, systemMessage)
+
+        assertTrue(items.count() == 2)
+        assertTrue(items.last() is OperatorStatusItem.InQueue)
+        assertTrue(items.first() is SystemChatItem)
+    }
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapOperatorAttachmentUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapOperatorAttachmentUseCaseTest.kt
new file mode 100644
index 000000000..e477fc8be
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapOperatorAttachmentUseCaseTest.kt
@@ -0,0 +1,59 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.AttachmentFile
+import com.glia.widgets.chat.MockChatMessageInternal
+import com.glia.widgets.chat.model.OperatorAttachmentItem
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class MapOperatorAttachmentUseCaseTest {
+    private val mockChatMessageInternal: MockChatMessageInternal = MockChatMessageInternal()
+    private lateinit var mockAttachment: AttachmentFile
+    private val useCase: MapOperatorAttachmentUseCase = MapOperatorAttachmentUseCase()
+
+    @Before
+    fun setUp() {
+        mockAttachment = mock()
+        mockChatMessageInternal.mockChatMessage()
+        mockChatMessageInternal.mockOperatorProperties()
+    }
+
+    @After
+    fun tearDown() {
+        mockChatMessageInternal.reset()
+    }
+
+    @Test
+    fun `invoke returns OperatorAttachmentItem_Image when attachment is Image`() {
+        whenever(mockAttachment.contentType) doReturn "image_asdfg"
+        mockChatMessageInternal.apply {
+            val mappedMessage = useCase(mockAttachment, chatMessageInternal, true)
+            assertTrue(mappedMessage is OperatorAttachmentItem.Image)
+            assertEquals(mappedMessage.attachmentFile, mockAttachment)
+            assertEquals(mappedMessage.timestamp, messageTimeStamp)
+            assertEquals(mappedMessage.showChatHead, true)
+            assertEquals(mappedMessage.operatorProfileImgUrl, operatorImageUrl)
+            assertEquals(mappedMessage.operatorId, operatorId)
+        }
+    }
+
+    @Test
+    fun `invoke returns OperatorAttachmentItem_File when attachment is not Image`() {
+        whenever(mockAttachment.contentType) doReturn "imagse_asdfg"
+        mockChatMessageInternal.apply {
+            val mappedMessage = useCase(mockAttachment, chatMessageInternal, false)
+            assertTrue(mappedMessage is OperatorAttachmentItem.File)
+            assertEquals(mappedMessage.attachmentFile, mockAttachment)
+            assertEquals(mappedMessage.timestamp, messageTimeStamp)
+            assertEquals(mappedMessage.showChatHead, false)
+            assertEquals(mappedMessage.operatorProfileImgUrl, operatorImageUrl)
+            assertEquals(mappedMessage.operatorId, operatorId)
+        }
+    }
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapOperatorPlainTextUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapOperatorPlainTextUseCaseTest.kt
new file mode 100644
index 000000000..53212396d
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapOperatorPlainTextUseCaseTest.kt
@@ -0,0 +1,60 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.widgets.chat.MockChatMessageInternal
+import com.glia.widgets.chat.model.OperatorMessageItem
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+class MapOperatorPlainTextUseCaseTest {
+    private val mockChatMessageInternal: MockChatMessageInternal = MockChatMessageInternal()
+    private val useCase: MapOperatorPlainTextUseCase = MapOperatorPlainTextUseCase()
+
+    @Before
+    fun setUp() {
+        mockChatMessageInternal.mockChatMessage()
+        mockChatMessageInternal.mockOperatorProperties()
+    }
+
+    @After
+    fun tearDown() {
+        mockChatMessageInternal.reset()
+    }
+
+    @Test
+    fun `invoke returns OperatorMessageItem_PlainText with showChatHead true when true is passed`() {
+        mockChatMessageInternal.apply {
+            val message = useCase(chatMessageInternal, true)
+
+            assertTrue(message is OperatorMessageItem.PlainText)
+
+            assertEquals(message.showChatHead, true)
+            assertEquals(message.id, messageId)
+            assertEquals(message.timestamp, messageTimeStamp)
+            assertEquals(message.operatorProfileImgUrl, operatorImageUrl)
+            assertEquals(message.operatorId, operatorId)
+            assertEquals(message.operatorName, operatorName)
+            assertEquals(message.content, content)
+        }
+    }
+
+    @Test
+    fun `invoke returns OperatorMessageItem_PlainText with showChatHead false when false is passed`() {
+        mockChatMessageInternal.apply {
+            val message = useCase(chatMessageInternal, false)
+
+            assertTrue(message is OperatorMessageItem.PlainText)
+
+            assertEquals(message.showChatHead, false)
+            assertEquals(message.id, messageId)
+            assertEquals(message.timestamp, messageTimeStamp)
+            assertEquals(message.operatorProfileImgUrl, operatorImageUrl)
+            assertEquals(message.operatorId, operatorId)
+            assertEquals(message.operatorName, operatorName)
+            assertEquals(message.content, content)
+        }
+    }
+
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapResponseCardUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapResponseCardUseCaseTest.kt
new file mode 100644
index 000000000..61008c2ca
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapResponseCardUseCaseTest.kt
@@ -0,0 +1,75 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.SingleChoiceAttachment
+import com.glia.widgets.chat.MockChatMessageInternal
+import com.glia.widgets.chat.model.OperatorMessageItem
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import java.util.Optional
+
+class MapResponseCardUseCaseTest {
+    private val mockChatMessageInternal: MockChatMessageInternal = MockChatMessageInternal()
+    private val useCase: MapResponseCardUseCase = MapResponseCardUseCase()
+    private val imageUrl = "sd"
+    private lateinit var attachment: SingleChoiceAttachment
+
+    @Before
+    fun setUp() {
+        attachment = mock()
+        whenever(attachment.options) doReturn arrayOf(mock())
+        whenever(attachment.imageUrl) doReturn Optional.ofNullable(imageUrl)
+
+        mockChatMessageInternal.mockChatMessage()
+        mockChatMessageInternal.mockOperatorProperties()
+    }
+
+    @After
+    fun tearDown() {
+        mockChatMessageInternal.reset()
+    }
+
+    @Test
+    fun `invoke returns OperatorMessageItem_ResponseCard with showChatHead true when true is passed`() {
+        mockChatMessageInternal.apply {
+            val message = useCase(attachment, chatMessageInternal, true)
+
+            assertTrue(message is OperatorMessageItem.ResponseCard)
+
+            assertEquals(message.showChatHead, true)
+            assertEquals(message.id, messageId)
+            assertEquals(message.timestamp, messageTimeStamp)
+            assertEquals(message.operatorProfileImgUrl, operatorImageUrl)
+            assertEquals(message.operatorId, operatorId)
+            assertEquals(message.operatorName, operatorName)
+            assertEquals(message.content, content)
+            assertEquals(message.singleChoiceOptions, attachment.options.asList())
+            assertEquals(message.choiceCardImageUrl, imageUrl)
+        }
+    }
+
+    @Test
+    fun `invoke returns OperatorMessageItem_ResponseCard with showChatHead false when false is passed`() {
+        mockChatMessageInternal.apply {
+            val message = useCase(attachment, chatMessageInternal, false)
+
+            assertTrue(message is OperatorMessageItem.ResponseCard)
+
+            assertEquals(message.showChatHead, false)
+            assertEquals(message.id, messageId)
+            assertEquals(message.timestamp, messageTimeStamp)
+            assertEquals(message.operatorProfileImgUrl, operatorImageUrl)
+            assertEquals(message.operatorId, operatorId)
+            assertEquals(message.operatorName, operatorName)
+            assertEquals(message.content, content)
+            assertEquals(message.singleChoiceOptions, attachment.options.asList())
+            assertEquals(message.choiceCardImageUrl, imageUrl)
+        }
+    }
+
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapVisitorAttachmentUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapVisitorAttachmentUseCaseTest.kt
new file mode 100644
index 000000000..af88b44ed
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/MapVisitorAttachmentUseCaseTest.kt
@@ -0,0 +1,50 @@
+package com.glia.widgets.chat.domain
+
+import com.glia.androidsdk.chat.AttachmentFile
+import com.glia.androidsdk.chat.VisitorMessage
+import com.glia.widgets.chat.model.VisitorAttachmentItem
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class MapVisitorAttachmentUseCaseTest {
+    private lateinit var mockAttachment: AttachmentFile
+    private lateinit var visitorMessage: VisitorMessage
+    private val useCase: MapVisitorAttachmentUseCase = MapVisitorAttachmentUseCase()
+
+    @Before
+    fun setUp() {
+        mockAttachment = mock()
+
+        visitorMessage = mock()
+        whenever(visitorMessage.id) doReturn "id"
+        whenever(visitorMessage.timestamp) doReturn -1
+    }
+
+    @Test
+    fun `invoke returns VisitorAttachmentItem_Image when attachment is Image`() {
+        whenever(mockAttachment.contentType) doReturn "image_sdj"
+        val newAttachment = useCase(mockAttachment, visitorMessage, true)
+
+        assertTrue(newAttachment is VisitorAttachmentItem.Image)
+        assertEquals(newAttachment.id, visitorMessage.id)
+        assertEquals(newAttachment.timestamp, visitorMessage.timestamp)
+        assertEquals(newAttachment.showDelivered, true)
+    }
+
+    @Test
+    fun `invoke returns VisitorAttachmentItem_File when attachment is not Image`() {
+        whenever(mockAttachment.contentType) doReturn "imasge_sdj"
+        val newAttachment = useCase(mockAttachment, visitorMessage, false)
+
+        assertTrue(newAttachment is VisitorAttachmentItem.File)
+        assertEquals(newAttachment.id, visitorMessage.id)
+        assertEquals(newAttachment.timestamp, visitorMessage.timestamp)
+        assertEquals(newAttachment.showDelivered, false)
+    }
+
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCaseTest.kt
index dce76be0c..c270f41cb 100644
--- a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCaseTest.kt
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaGalleryCardsUseCaseTest.kt
@@ -1,8 +1,6 @@
 package com.glia.widgets.chat.domain.gva
 
-import com.glia.androidsdk.Operator
 import com.glia.widgets.chat.MockChatMessageInternal
-import com.glia.widgets.chat.model.ChatState
 import com.glia.widgets.chat.model.GvaGalleryCard
 import org.junit.After
 import org.junit.Assert.assertEquals
@@ -16,18 +14,12 @@ import org.mockito.kotlin.whenever
 class MapGvaGvaGalleryCardsUseCaseTest {
     private lateinit var useCase: MapGvaGvaGalleryCardsUseCase
     private lateinit var parseGvaGalleryCardsUseCase: ParseGvaGalleryCardsUseCase
-    private lateinit var chatState: ChatState
-    private lateinit var operator: Operator
     private val mockChatMessageInternal: MockChatMessageInternal = MockChatMessageInternal()
 
     @Before
     fun setUp() {
         parseGvaGalleryCardsUseCase = mock()
         whenever(parseGvaGalleryCardsUseCase(any())) doReturn emptyList()
-
-        chatState = mock()
-        operator = mock()
-
         useCase = MapGvaGvaGalleryCardsUseCase(parseGvaGalleryCardsUseCase)
     }
 
@@ -42,32 +34,16 @@ class MapGvaGvaGalleryCardsUseCaseTest {
             mockChatMessage()
             mockOperatorProperties()
 
-            val galleryCard = useCase(chatMessageInternal, chatState)
+            val galleryCard = useCase(chatMessageInternal, showChatHead = true)
 
             assertEquals(galleryCard.id, messageId)
             assertEquals(galleryCard.galleryCards, emptyList())
-            assertEquals(galleryCard.showChatHead, false)
             assertEquals(galleryCard.operatorId, operatorId)
             assertEquals(galleryCard.id, messageId)
             assertEquals(galleryCard.timestamp, messageTimeStamp)
             assertEquals(galleryCard.operatorProfileImgUrl, operatorImageUrl)
             assertEquals(galleryCard.operatorName, operatorName)
-        }
-    }
-
-    @Test
-    fun `invoke takes operator data from chatState when it is null in ChatMessage`() {
-        mockChatMessageInternal.apply {
-            mockChatMessage()
-            mockOperatorPropertiesWithNull()
-
-            whenever(chatState.operatorProfileImgUrl) doReturn operatorImageUrl
-            whenever(chatState.formattedOperatorName) doReturn operatorName
-
-            val galleryCard = useCase(chatMessageInternal, chatState)
-
-            assertEquals(galleryCard.operatorProfileImgUrl, operatorImageUrl)
-            assertEquals(galleryCard.operatorName, operatorName)
+            assertEquals(galleryCard.showChatHead, true)
         }
     }
 }
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCaseTest.kt
deleted file mode 100644
index b5c3f1a05..000000000
--- a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaGvaQuickRepliesUseCaseTest.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package com.glia.widgets.chat.domain.gva
-
-import com.glia.widgets.chat.MockChatMessageInternal
-import com.glia.widgets.chat.model.ChatState
-import com.glia.widgets.chat.model.GvaChatItem
-import com.glia.widgets.chat.model.GvaQuickReplies
-import com.glia.widgets.chat.model.GvaResponseText
-import org.junit.After
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.mockito.kotlin.any
-import org.mockito.kotlin.anyOrNull
-import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-class MapGvaGvaQuickRepliesUseCaseTest {
-    private val mockChatMessageInternal: MockChatMessageInternal = MockChatMessageInternal()
-    private lateinit var chatState: ChatState
-    private lateinit var useCase: MapGvaGvaQuickRepliesUseCase
-    private lateinit var parseGvaButtonsUseCase: ParseGvaButtonsUseCase
-    private lateinit var mapGvaResponseTextUseCase: MapGvaResponseTextUseCase
-    private lateinit var gvaResponseText: GvaResponseText
-
-    @Before
-    fun setUp() {
-        chatState = mock()
-
-        parseGvaButtonsUseCase = mock()
-        whenever(parseGvaButtonsUseCase(anyOrNull())) doReturn emptyList()
-
-        gvaResponseText = mock()
-
-        mapGvaResponseTextUseCase = mock()
-        whenever(mapGvaResponseTextUseCase(any(), any())) doReturn gvaResponseText
-
-        useCase = MapGvaGvaQuickRepliesUseCase(parseGvaButtonsUseCase, mapGvaResponseTextUseCase)
-    }
-
-    @After
-    fun tearDown() {
-        mockChatMessageInternal.reset()
-    }
-
-    @Test
-    fun `invoke returns GvaResponseText when chat message is not last item in chat transcript`() {
-        mockChatMessageInternal.apply {
-            mockChatMessage()
-            mockOperatorProperties()
-
-            whenever(chatMessageInternal.isHistory) doReturn true
-            whenever(chatMessageInternal.isLatest) doReturn false
-
-            val gvaChatItem: GvaChatItem = useCase(chatMessageInternal, chatState)
-            assertTrue(gvaChatItem is GvaResponseText)
-        }
-    }
-
-    @Test
-    fun `invoke returns GvaQuickReplies when chat message is the last item in chat transcript`() {
-        mockChatMessageInternal.apply {
-            mockChatMessage()
-            mockOperatorProperties()
-
-            whenever(chatMessageInternal.isHistory) doReturn true
-            whenever(chatMessageInternal.isLatest) doReturn true
-
-            val gvaChatItem: GvaChatItem = useCase(chatMessageInternal, chatState)
-            assertTrue(gvaChatItem is GvaQuickReplies)
-        }
-    }
-
-    @Test
-    fun `invoke returns GvaQuickReplies when chat message is not from chat transcript`() {
-        mockChatMessageInternal.apply {
-            mockChatMessage()
-            mockOperatorProperties()
-
-            whenever(chatMessageInternal.isHistory) doReturn false
-            whenever(chatMessageInternal.isLatest) doReturn false
-
-            val gvaChatItem: GvaChatItem = useCase(chatMessageInternal, chatState)
-            assertTrue(gvaChatItem is GvaQuickReplies)
-        }
-    }
-}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCaseTest.kt
index 37411a657..99ce1b236 100644
--- a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCaseTest.kt
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaPersistentButtonsUseCaseTest.kt
@@ -2,11 +2,9 @@ package com.glia.widgets.chat.domain.gva
 
 import com.glia.widgets.chat.MockChatMessageInternal
 import com.glia.widgets.chat.model.ChatState
-import com.glia.widgets.chat.model.Gva
 import com.glia.widgets.chat.model.GvaButton
-import org.json.JSONObject
+import junit.framework.TestCase.assertEquals
 import org.junit.After
-import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
 import org.mockito.kotlin.anyOrNull
@@ -37,17 +35,16 @@ class MapGvaPersistentButtonsUseCaseTest {
 
     @Test
     fun `invoke parses GvaPersistentButtons when ChatMessage passed with appropriate metadata`() {
-        val content = "content"
         mockChatMessageInternal.apply {
-            mockChatMessage(JSONObject().put(Gva.Keys.CONTENT, content))
+            mockChatMessage(metadataWithContent())
             mockOperatorProperties()
 
-            val galleryCard = useCase(chatMessageInternal, chatState)
+            val galleryCard = useCase(chatMessageInternal, true)
 
             assertEquals(galleryCard.id, messageId)
             assertEquals(galleryCard.content, content)
             assertEquals(galleryCard.options, emptyList())
-            assertEquals(galleryCard.showChatHead, false)
+            assertEquals(galleryCard.showChatHead, true)
             assertEquals(galleryCard.operatorId, operatorId)
             assertEquals(galleryCard.id, messageId)
             assertEquals(galleryCard.timestamp, messageTimeStamp)
@@ -55,20 +52,4 @@ class MapGvaPersistentButtonsUseCaseTest {
             assertEquals(galleryCard.operatorName, operatorName)
         }
     }
-
-    @Test
-    fun `invoke takes operator data from chatState when it is null in ChatMessage`() {
-        mockChatMessageInternal.apply {
-            mockChatMessage()
-            mockOperatorPropertiesWithNull()
-
-            whenever(chatState.operatorProfileImgUrl) doReturn operatorImageUrl
-            whenever(chatState.formattedOperatorName) doReturn operatorName
-
-            val galleryCard = useCase(chatMessageInternal, chatState)
-
-            assertEquals(galleryCard.operatorProfileImgUrl, operatorImageUrl)
-            assertEquals(galleryCard.operatorName, operatorName)
-        }
-    }
 }
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaQuickRepliesUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaQuickRepliesUseCaseTest.kt
new file mode 100644
index 000000000..03414a221
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaQuickRepliesUseCaseTest.kt
@@ -0,0 +1,61 @@
+package com.glia.widgets.chat.domain.gva
+
+import com.glia.widgets.chat.MockChatMessageInternal
+import com.glia.widgets.chat.model.GvaButton
+import com.glia.widgets.chat.model.GvaResponseText
+import org.junit.After
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class MapGvaQuickRepliesUseCaseTest {
+    private val mockChatMessageInternal: MockChatMessageInternal = MockChatMessageInternal()
+    private lateinit var useCase: MapGvaQuickRepliesUseCase
+    private lateinit var parseGvaButtonsUseCase: ParseGvaButtonsUseCase
+    private lateinit var mapGvaResponseTextUseCase: MapGvaResponseTextUseCase
+    private lateinit var gvaResponseText: GvaResponseText
+
+    @Before
+    fun setUp() {
+
+        parseGvaButtonsUseCase = mock()
+        whenever(parseGvaButtonsUseCase(anyOrNull())) doReturn emptyList()
+
+        gvaResponseText = mock()
+
+        mapGvaResponseTextUseCase = mock()
+        whenever(mapGvaResponseTextUseCase(any(), any())) doReturn gvaResponseText
+
+        useCase = MapGvaQuickRepliesUseCase(parseGvaButtonsUseCase)
+    }
+
+    @After
+    fun tearDown() {
+        mockChatMessageInternal.reset()
+    }
+
+    @Test
+    fun `invoke parses GvaQuickReplies when ChatMessage passed with appropriate metadata`() {
+        mockChatMessageInternal.apply {
+            mockChatMessage(metadataWithContent())
+            mockOperatorProperties()
+
+            val quickReplies = useCase(chatMessageInternal, showChatHead = true)
+
+            Assert.assertEquals(quickReplies.id, messageId)
+            Assert.assertEquals(quickReplies.content, content)
+            Assert.assertEquals(quickReplies.options, emptyList())
+            Assert.assertEquals(quickReplies.operatorId, operatorId)
+            Assert.assertEquals(quickReplies.id, messageId)
+            Assert.assertEquals(quickReplies.timestamp, messageTimeStamp)
+            Assert.assertEquals(quickReplies.operatorProfileImgUrl, operatorImageUrl)
+            Assert.assertEquals(quickReplies.operatorName, operatorName)
+            Assert.assertEquals(quickReplies.showChatHead, true)
+        }
+    }
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCaseTest.kt
index b27eb525a..5ae4c00b0 100644
--- a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCaseTest.kt
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaResponseTextUseCaseTest.kt
@@ -1,25 +1,17 @@
 package com.glia.widgets.chat.domain.gva
 
 import com.glia.widgets.chat.MockChatMessageInternal
-import com.glia.widgets.chat.model.ChatState
-import com.glia.widgets.chat.model.Gva
-import org.json.JSONObject
+import junit.framework.TestCase.assertEquals
 import org.junit.After
-import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
-import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
 
 class MapGvaResponseTextUseCaseTest {
     private val mockChatMessageInternal: MockChatMessageInternal = MockChatMessageInternal()
-    private lateinit var chatState: ChatState
     private lateinit var useCase: MapGvaResponseTextUseCase
 
     @Before
     fun setUp() {
-        chatState = mock()
         useCase = MapGvaResponseTextUseCase()
     }
 
@@ -30,12 +22,11 @@ class MapGvaResponseTextUseCaseTest {
 
     @Test
     fun `invoke parses GvaResponseText when ChatMessage passed with appropriate metadata`() {
-        val content = "content"
         mockChatMessageInternal.apply {
-            mockChatMessage(JSONObject().put(Gva.Keys.CONTENT, content))
+            mockChatMessage(metadataWithContent())
             mockOperatorProperties()
 
-            val galleryCard = useCase(chatMessageInternal, chatState)
+            val galleryCard = useCase(chatMessageInternal, false)
 
             assertEquals(galleryCard.id, messageId)
             assertEquals(galleryCard.content, content)
@@ -47,20 +38,4 @@ class MapGvaResponseTextUseCaseTest {
             assertEquals(galleryCard.operatorName, operatorName)
         }
     }
-
-    @Test
-    fun `invoke takes operator data from chatState when it is null in ChatMessage`() {
-        mockChatMessageInternal.apply {
-            mockChatMessage()
-            mockOperatorPropertiesWithNull()
-
-            whenever(chatState.operatorProfileImgUrl) doReturn operatorImageUrl
-            whenever(chatState.formattedOperatorName) doReturn operatorName
-
-            val galleryCard = useCase(chatMessageInternal, chatState)
-
-            assertEquals(galleryCard.operatorProfileImgUrl, operatorImageUrl)
-            assertEquals(galleryCard.operatorName, operatorName)
-        }
-    }
 }
diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaUseCaseTest.kt
index 68140790f..f79db4e11 100644
--- a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaUseCaseTest.kt
+++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/gva/MapGvaUseCaseTest.kt
@@ -1,14 +1,13 @@
 package com.glia.widgets.chat.domain.gva
 
 import com.glia.widgets.chat.MockChatMessageInternal
-import com.glia.widgets.chat.model.ChatState
 import com.glia.widgets.chat.model.Gva
 import com.glia.widgets.chat.model.GvaGalleryCards
 import com.glia.widgets.chat.model.GvaPersistentButtons
 import com.glia.widgets.chat.model.GvaQuickReplies
 import com.glia.widgets.chat.model.GvaResponseText
+import junit.framework.TestCase.assertTrue
 import org.junit.After
-import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.mockito.kotlin.any
@@ -21,11 +20,9 @@ class MapGvaUseCaseTest {
     private lateinit var getGvaTypeUseCase: GetGvaTypeUseCase
     private lateinit var mapGvaResponseTextUseCase: MapGvaResponseTextUseCase
     private lateinit var mapGvaPersistentButtonsUseCase: MapGvaPersistentButtonsUseCase
-    private lateinit var mapGvaGvaQuickRepliesUseCase: MapGvaGvaQuickRepliesUseCase
+    private lateinit var mapGvaQuickRepliesUseCase: MapGvaQuickRepliesUseCase
     private lateinit var mapGvaGvaGalleryCardsUseCase: MapGvaGvaGalleryCardsUseCase
 
-    private lateinit var chatState: ChatState
-
     private lateinit var useCase: MapGvaUseCase
 
     @Before
@@ -33,16 +30,14 @@ class MapGvaUseCaseTest {
         getGvaTypeUseCase = mock()
         mapGvaResponseTextUseCase = mock()
         mapGvaPersistentButtonsUseCase = mock()
-        mapGvaGvaQuickRepliesUseCase = mock()
+        mapGvaQuickRepliesUseCase = mock()
         mapGvaGvaGalleryCardsUseCase = mock()
 
-        chatState = mock()
-
         useCase = MapGvaUseCase(
             getGvaTypeUseCase,
             mapGvaResponseTextUseCase,
             mapGvaPersistentButtonsUseCase,
-            mapGvaGvaQuickRepliesUseCase,
+            mapGvaQuickRepliesUseCase,
             mapGvaGvaGalleryCardsUseCase
         )
 
@@ -61,7 +56,7 @@ class MapGvaUseCaseTest {
         whenever(mapGvaResponseTextUseCase(any(), any())) doReturn mock()
 
         mockChatMessageInternal.apply {
-            val gva = useCase(chatMessageInternal, chatState)
+            val gva = useCase(chatMessageInternal)
             assertTrue(gva is GvaResponseText)
         }
     }
@@ -72,7 +67,7 @@ class MapGvaUseCaseTest {
         whenever(mapGvaPersistentButtonsUseCase(any(), any())) doReturn mock()
 
         mockChatMessageInternal.apply {
-            val gva = useCase(chatMessageInternal, chatState)
+            val gva = useCase(chatMessageInternal)
             assertTrue(gva is GvaPersistentButtons)
         }
     }
@@ -80,10 +75,10 @@ class MapGvaUseCaseTest {
     @Test
     fun `invoke returns GvaQuickReplies when GVA type is QUICK_REPLIES`() {
         whenever(getGvaTypeUseCase(any())) doReturn Gva.Type.QUICK_REPLIES
-        whenever(mapGvaGvaQuickRepliesUseCase(any(), any())) doReturn mock()
+        whenever(mapGvaQuickRepliesUseCase(any(), any())) doReturn mock()
 
         mockChatMessageInternal.apply {
-            val gva = useCase(chatMessageInternal, chatState)
+            val gva = useCase(chatMessageInternal)
             assertTrue(gva is GvaQuickReplies)
         }
     }
@@ -94,7 +89,7 @@ class MapGvaUseCaseTest {
         whenever(mapGvaGvaGalleryCardsUseCase(any(), any())) doReturn mock()
 
         mockChatMessageInternal.apply {
-            val gva = useCase(chatMessageInternal, chatState)
+            val gva = useCase(chatMessageInternal)
             assertTrue(gva is GvaGalleryCards)
         }
     }
@@ -103,6 +98,6 @@ class MapGvaUseCaseTest {
     fun `invoke throws exception when ChatMessage is not GVA message`() {
         whenever(getGvaTypeUseCase(any())) doReturn null
 
-        mockChatMessageInternal.apply { useCase(chatMessageInternal, chatState) }
+        mockChatMessageInternal.apply { useCase(chatMessageInternal) }
     }
 }
diff --git a/widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaOperatorRepositoryTest.java b/widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaOperatorRepositoryTest.java
deleted file mode 100644
index db31e19e2..000000000
--- a/widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaOperatorRepositoryTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.glia.widgets.core.engagement;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-import androidx.core.util.Consumer;
-
-import com.glia.androidsdk.GliaException;
-import com.glia.androidsdk.Operator;
-import com.glia.androidsdk.RequestCallback;
-import com.glia.widgets.core.model.TestOperator;
-import com.glia.widgets.di.GliaCore;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mockito;
-
-public class GliaOperatorRepositoryTest {
-    GliaOperatorRepository repository;
-    GliaCore core;
-    Operator operator;
-
-    @Before
-    public void setUp() {
-        core = mock(GliaCore.class);
-        repository = new GliaOperatorRepository(core);
-        operator = TestOperator.DEFAULT;
-    }
-
-    @Test
-    public void getOperatorById_returnsCachedOperator_whenCachedOperatorExists() {
-        repository.addOrUpdateOperator(operator);
-
-        Consumer callback = mock(Consumer.class);
-
-        repository.getOperatorById(operator.getId(), callback);
-
-        verify(callback).accept(operator);
-    }
-
-    @Test
-    public void getOperatorById_returnsOperatorFromApiCall_whenCachedOperatorNotExists() {
-        stubGetOperatorResponse(operator, null);
-
-        Consumer callback = mock(Consumer.class);
-
-        repository.getOperatorById(operator.getId(), callback);
-
-        verify(callback).accept(operator);
-    }
-
-    @Test
-    public void getOperatorById_returnsNull_whenCachedOperatorNotExistsAndApiReturnsError() {
-        stubGetOperatorResponse(null, new GliaException("", GliaException.Cause.INVALID_INPUT));
-
-        Consumer callback = mock(Consumer.class);
-
-        repository.getOperatorById(operator.getId(), callback);
-
-        verify(callback).accept(null);
-    }
-
-    void stubGetOperatorResponse(Operator operator, GliaException exception) {
-        Mockito.doAnswer(invocation -> {
-            RequestCallback callback = invocation.getArgument(1);
-            callback.onResult(operator, exception);
-            return callback;
-        }).when(core).getOperator(anyString(), any());
-    }
-}
\ No newline at end of file
diff --git a/widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaOperatorRepositoryTest.kt b/widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaOperatorRepositoryTest.kt
new file mode 100644
index 000000000..6efc33754
--- /dev/null
+++ b/widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaOperatorRepositoryTest.kt
@@ -0,0 +1,124 @@
+package com.glia.widgets.core.engagement
+
+import androidx.core.util.Consumer
+import com.glia.androidsdk.GliaException
+import com.glia.androidsdk.Operator
+import com.glia.androidsdk.Operator.Picture
+import com.glia.androidsdk.RequestCallback
+import com.glia.widgets.core.engagement.data.LocalOperator
+import com.glia.widgets.di.GliaCore
+import junit.framework.TestCase.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import java.util.Optional
+
+class GliaOperatorRepositoryTest {
+    private lateinit var repository: GliaOperatorRepository
+    private lateinit var core: GliaCore
+    private lateinit var localOperator: LocalOperator
+    private lateinit var operator: Operator
+
+    @Before
+    fun setUp() {
+        core = Mockito.mock(GliaCore::class.java)
+        repository = spy(GliaOperatorRepository(core))
+        localOperator = LocalOperator("id", "name", "imageUrl")
+        operator = mock()
+    }
+
+    private fun mockOperator(imageUrl: String? = localOperator.imageUrl) {
+        val picture: Picture = mock()
+        whenever(picture.url) doReturn Optional.ofNullable(imageUrl)
+        whenever(operator.id) doReturn localOperator.id
+        whenever(operator.name) doReturn localOperator.name
+        whenever(operator.picture) doReturn picture
+    }
+
+    @Test
+    fun `updateIfExists returns the inserted operator if it doesn't exist in cache`() {
+        mockOperator()
+        val newLocalOperator = repository.updateIfExists(operator)
+        assertEquals(localOperator, newLocalOperator)
+    }
+
+    @Test
+    fun `updateIfExists returns the updated operator if it exists in cache without image url`() {
+        val imageUrl = "new_image_url"
+        mockOperator(imageUrl)
+        repository.putOperator(localOperator.copy(imageUrl = null))
+        val newLocalOperator = repository.updateIfExists(operator)
+
+        assertNotEquals(localOperator, newLocalOperator)
+        assertEquals(newLocalOperator.imageUrl, imageUrl)
+    }
+
+
+    @Test
+    fun `updateIfExists returns the old operator if it exists in cache with image url`() {
+        val imageUrl = "new_image_url"
+        mockOperator(imageUrl)
+        repository.putOperator(localOperator)
+        val newLocalOperator = repository.updateIfExists(operator)
+
+        assertEquals(localOperator, newLocalOperator)
+        assertEquals(newLocalOperator.imageUrl, localOperator.imageUrl)
+    }
+
+    @Test
+    fun `emit updates operator and then puts it into cache`() {
+        mockOperator()
+        repository.emit(operator)
+        verify(repository).updateIfExists(operator)
+        verify(repository).putOperator(any())
+    }
+
+    @Test
+    fun `operatorById returns cached operator when exists`() {
+        mockOperator()
+        repository.emit(operator)
+        val callback: Consumer = mock()
+        repository.getOperatorById(operator.id, callback)
+
+        verify(callback).accept(localOperator)
+    }
+
+    @Test
+    fun `operatorById returns operator from API call when cached operator not exists`() {
+        mockOperator()
+        stubGetOperatorResponse(operator, null)
+        val callback: Consumer = mock()
+
+        repository.getOperatorById(operator.id, callback)
+
+        verify(callback).accept(localOperator)
+    }
+
+    @Test
+    fun `operatorById returns null when cached operator not exists and API returns error`() {
+        mockOperator()
+        stubGetOperatorResponse(null, GliaException("", GliaException.Cause.INVALID_INPUT))
+        val callback: Consumer = mock()
+
+        repository.getOperatorById(operator.id, callback);
+        verify(callback).accept(null)
+    }
+
+    private fun stubGetOperatorResponse(operator: Operator?, exception: GliaException?) {
+        doAnswer { invocation: InvocationOnMock ->
+            val callback = invocation.getArgument>(1)
+            callback.onResult(operator, exception)
+            callback
+        }.whenever(core).getOperator(anyString(), any())
+    }
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/core/engagement/domain/GetOperatorUseCaseTest.java b/widgetssdk/src/test/java/com/glia/widgets/core/engagement/domain/GetOperatorUseCaseTest.java
index 7678563a9..b3af0872b 100644
--- a/widgetssdk/src/test/java/com/glia/widgets/core/engagement/domain/GetOperatorUseCaseTest.java
+++ b/widgetssdk/src/test/java/com/glia/widgets/core/engagement/domain/GetOperatorUseCaseTest.java
@@ -7,9 +7,8 @@
 
 import androidx.core.util.Consumer;
 
-import com.glia.androidsdk.Operator;
 import com.glia.widgets.core.engagement.GliaOperatorRepository;
-import com.glia.widgets.core.model.TestOperator;
+import com.glia.widgets.core.engagement.data.LocalOperator;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -20,13 +19,13 @@
 public class GetOperatorUseCaseTest {
     private GliaOperatorRepository gliaOperatorRepository;
     private GetOperatorUseCase getOperatorUseCase;
-    private Operator operator;
+    private LocalOperator operator;
 
     @Before
     public void setUp() throws Exception {
         gliaOperatorRepository = mock(GliaOperatorRepository.class);
         getOperatorUseCase = new GetOperatorUseCase(gliaOperatorRepository);
-        operator = TestOperator.DEFAULT;
+        operator = new LocalOperator("id", "name", "imageUrl");
     }
 
     @Test
@@ -47,11 +46,11 @@ public void execute_returnsEmptyOptional_whenOperatorNotExists() {
                 .assertResult(Optional.empty());
     }
 
-    private void stubGetOperatorResponse(Operator operator) {
+    private void stubGetOperatorResponse(LocalOperator operator) {
         doAnswer(invocation -> {
-            Consumer callback = invocation.getArgument(1);
+            Consumer callback = invocation.getArgument(1);
             callback.accept(operator);
             return callback;
         }).when(gliaOperatorRepository).getOperatorById(anyString(), any());
     }
-}
\ No newline at end of file
+}
diff --git a/widgetssdk/src/test/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCaseTest.kt
index 8b6ea4c40..23dcfd026 100644
--- a/widgetssdk/src/test/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCaseTest.kt
+++ b/widgetssdk/src/test/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCaseTest.kt
@@ -1,10 +1,9 @@
 package com.glia.widgets.core.engagement.domain
 
-import com.glia.androidsdk.Operator
 import com.glia.androidsdk.chat.Chat
 import com.glia.androidsdk.chat.ChatMessage
 import com.glia.androidsdk.chat.OperatorMessage
-import com.glia.widgets.core.model.TestOperator
+import com.glia.widgets.core.engagement.data.LocalOperator
 import io.reactivex.Single
 import junit.framework.TestCase.assertEquals
 import org.junit.Before
@@ -17,14 +16,14 @@ import kotlin.properties.Delegates
 
 class MapOperatorUseCaseTest {
     private var mapOperatorUseCase: MapOperatorUseCase by Delegates.notNull()
-    var getOperatorUseCase: GetOperatorUseCase by Delegates.notNull()
-    var operator: Operator by Delegates.notNull()
+    private var getOperatorUseCase: GetOperatorUseCase by Delegates.notNull()
+    private var operator: LocalOperator by Delegates.notNull()
 
     @Before
     fun setUp() {
         getOperatorUseCase = mock()
         mapOperatorUseCase = MapOperatorUseCase(getOperatorUseCase)
-        operator = TestOperator.DEFAULT
+        operator = LocalOperator("id", "name", "imageUrl")
     }
 
     @Test
diff --git a/widgetssdk/src/test/java/com/glia/widgets/core/model/TestOperator.java b/widgetssdk/src/test/java/com/glia/widgets/core/model/TestOperator.java
deleted file mode 100644
index 7e976cd33..000000000
--- a/widgetssdk/src/test/java/com/glia/widgets/core/model/TestOperator.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.glia.widgets.core.model;
-
-import com.glia.androidsdk.Engagement;
-import com.glia.androidsdk.Operator;
-
-public class TestOperator implements Operator {
-    public static final Operator DEFAULT = new TestOperator();
-    @Override
-    public String getId() {
-        return "Operator ID";
-    }
-
-    @Override
-    public String getName() {
-        return "Test Name";
-    }
-
-    @Override
-    public Picture getPicture() {
-        return new Picture("https://picture");
-    }
-
-    @Override
-    public Engagement.MediaType[] getAvailableMedia() {
-        return new Engagement.MediaType[0];
-    }
-}

From d5bfdac9962f31555786ee6b54cf6fc94c7d5bfa Mon Sep 17 00:00:00 2001
From: Andriy Shevtsov 
Date: Fri, 11 Aug 2023 16:31:33 +0300
Subject: [PATCH 37/69] Make changes according to Team Mobile Code Management
 and Release process

---
 .github/workflows/increment-project-version.yml |  2 +-
 .github/workflows/post-release.yml              |  2 +-
 bitrise.yml                                     | 12 +++++++-----
 3 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/increment-project-version.yml b/.github/workflows/increment-project-version.yml
index f18302cb2..db6d7ae82 100644
--- a/.github/workflows/increment-project-version.yml
+++ b/.github/workflows/increment-project-version.yml
@@ -18,4 +18,4 @@ jobs:
       - uses: actions/checkout@v3
       - name: Runs the increment project version workflow in Bitrise
         run: |
-          curl https://app.bitrise.io/app/${{ secrets.BITRISE_APP_ID }}/build/start.json --data '{"hook_info":{"type":"bitrise","build_trigger_token":"${{ secrets.BITRISE_BUILD_TRIGGER_TOKEN }}"},"build_params":{"branch":"master","workflow_id":"authenticated_increment_project_version","environments":[{"mapped_to":"GITHUB_VERSION_INCREMENT_TYPE","value":"${{ github.event.inputs.type }}","is_expand":true}]},"triggered_by":"curl"}'
+          curl https://app.bitrise.io/app/${{ secrets.BITRISE_APP_ID }}/build/start.json --data '{"hook_info":{"type":"bitrise","build_trigger_token":"${{ secrets.BITRISE_BUILD_TRIGGER_TOKEN }}"},"build_params":{"branch":"development","workflow_id":"authenticated_increment_project_version","environments":[{"mapped_to":"GITHUB_VERSION_INCREMENT_TYPE","value":"${{ github.event.inputs.type }}","is_expand":true}]},"triggered_by":"curl"}'
diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml
index c48777935..4cb5c9a90 100644
--- a/.github/workflows/post-release.yml
+++ b/.github/workflows/post-release.yml
@@ -13,4 +13,4 @@ jobs:
       - uses: actions/checkout@v3
       - name: Runs the post-release workflow in Bitrise
         run: |
-          curl https://app.bitrise.io/app/${{ secrets.BITRISE_APP_ID }}/build/start.json --data '{"hook_info":{"type":"bitrise","build_trigger_token":"${{ secrets.BITRISE_BUILD_TRIGGER_TOKEN }}"},"build_params":{"branch":"master","workflow_id":"post_release","environments":[{"mapped_to":"NEW_VERSION","value":"${{ github.event.inputs.version }}","is_expand":true}]},"triggered_by":"curl"}'
+          curl https://app.bitrise.io/app/${{ secrets.BITRISE_APP_ID }}/build/start.json --data '{"hook_info":{"type":"bitrise","build_trigger_token":"${{ secrets.BITRISE_BUILD_TRIGGER_TOKEN }}"},"build_params":{"branch":"development","workflow_id":"post_release","environments":[{"mapped_to":"NEW_VERSION","value":"${{ github.event.inputs.version }}","is_expand":true}]},"triggered_by":"curl"}'
diff --git a/bitrise.yml b/bitrise.yml
index 627135479..9194015a8 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -162,7 +162,7 @@ workflows:
         - text: Android Build Succeeded!
         - webhook_url_on_error: $SLACK_ANDROID_WEBHOOK
         - channel_on_error: '#tm-mobile'
-        - text_on_error: '@mobile-caretaker Android Build Failed! (master-build)'
+        - text_on_error: '@mobile-caretaker Android Build Failed! (development-build)'
         - emoji_on_error: "\U0001F4A5"
         - color_on_error: '#d9482b'
         - from_username_on_error: Bitrise
@@ -328,20 +328,22 @@ workflows:
             # debug log
             set -x
 
-            BRANCH_NAME="core-sdk-version-increment/${NEW_VERSION}"
+            NEW_BRANCH_NAME="core-sdk-version-increment/${NEW_VERSION}"
+            BASE_BRANCH_NAME="development"
             MESSAGE="Increment Core SDK version to ${NEW_VERSION}"
 
-            git checkout -b $BRANCH_NAME
+            git fetch origin $BASE_BRANCH_NAME
+            git checkout -b $NEW_BRANCH_NAME origin/$BASE_BRANCH_NAME
             git add -A
             git commit -m "$MESSAGE"
-            git push origin "$BRANCH_NAME":"$BRANCH_NAME"
+            git push origin "$NEW_BRANCH_NAME":"$NEW_BRANCH_NAME"
 
             PULL_REQUEST_NUMBER=$(curl \
               -X POST \
               -H "Accept: application/vnd.github+json" \
               -H "Authorization: Bearer $GITHUB_API_TOKEN" \
               https://api.github.com/repos/salemove/android-sdk-widgets/pulls \
-              -d "{\"title\":\"${MESSAGE}\",\"head\":\"${BRANCH_NAME}\",\"base\":\"development\"}" | jq --raw-output '.number')
+              -d "{\"title\":\"${MESSAGE}\",\"head\":\"${NEW_BRANCH_NAME}\",\"base\":\"development\"}" | jq --raw-output '.number')
 
             curl \
               -X POST \

From 1dd50ef36b57541f88eda47e7def5beb5fb059d5 Mon Sep 17 00:00:00 2001
From: Davit Dolmazyan 
Date: Mon, 14 Aug 2023 12:54:03 +0300
Subject: [PATCH 38/69] Fix Attachment button is hidden for sc with optimized
 chat flow

MOB 2530
---
 .../widgets/chat/controller/ChatController.kt     | 15 ++++++++-------
 .../chat/domain/IsShowSendButtonUseCase.kt        |  2 +-
 2 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt
index 5247db844..b3178fa60 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt
@@ -219,12 +219,7 @@ internal class ChatController(
                 return
             }
 
-            var initChatState = chatState.initChat(companyName, queueId, visitorContextAssetId)
-            if (isSecureEngagement) {
-                initChatState = initChatState.setSecureMessagingState()
-            }
-            prepareChatComponents()
-            emitViewState { initChatState }
+            emitViewState { chatState.initChat(companyName, queueId, visitorContextAssetId) }
             initChatManager()
         }
     }
@@ -821,7 +816,13 @@ internal class ChatController(
             }
         }
 
-        emitViewState { chatState.historyLoaded() }
+        if (isSecureEngagement) {
+            emitViewState { chatState.setSecureMessagingState() }
+        } else {
+            emitViewState { chatState.historyLoaded() }
+        }
+
+        prepareChatComponents()
         initGliaEngagementObserving()
     }
 
diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsShowSendButtonUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsShowSendButtonUseCase.kt
index 4004c7283..159c813d8 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsShowSendButtonUseCase.kt
+++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/IsShowSendButtonUseCase.kt
@@ -17,7 +17,7 @@ class IsShowSendButtonUseCase(
     }
 
     private fun hasText(message: String?): Boolean {
-        return message != null && message.isNotEmpty()
+        return !message.isNullOrEmpty()
     }
 
     private fun hadReadyToSendUnsentAttachments(): Boolean {

From 6f58ba25bdc571721b7de93b755d70f10bd5a983 Mon Sep 17 00:00:00 2001
From: Davit Dolmazyan 
Date: Mon, 14 Aug 2023 17:59:04 +0300
Subject: [PATCH 39/69] Replace absent operator image url with default from the
 `SiteInfo`

MOB 2532
---
 .../java/com/glia/widgets/GliaWidgets.java    | 66 ++++++++-----------
 .../widgets/chat/controller/ChatController.kt |  3 +
 .../core/engagement/GliaOperatorRepository.kt | 22 ++++---
 .../engagement/domain/MapOperatorUseCase.kt   |  6 +-
 .../UpdateOperatorDefaultImageUrlUseCase.kt   | 14 ++++
 .../glia/widgets/di/ControllerFactory.java    |  1 +
 .../com/glia/widgets/di/UseCaseFactory.java   |  6 ++
 .../glia/widgets/helper/CommonExtensions.kt   |  7 +-
 .../chat/controller/ChatControllerTest.kt     |  4 ++
 .../engagement/GliaOperatorRepositoryTest.kt  | 46 ++++---------
 10 files changed, 88 insertions(+), 87 deletions(-)
 create mode 100644 widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/UpdateOperatorDefaultImageUrlUseCase.kt

diff --git a/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java b/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java
index 72c979cff..0f2401155 100644
--- a/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java
+++ b/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java
@@ -30,42 +30,35 @@
  * This class is a starting point for integration with Glia Widgets SDK
  */
 public class GliaWidgets {
-    private final static String TAG = "GliaWidgets";
-
     public static final String REMOTE_CONFIGURATION = "remote_configuration";
-
     /**
      * Use with {@link android.os.Bundle} to pass in a {@link UiTheme} as a navigation argument when
      * navigating to {@link com.glia.widgets.chat.ChatActivity}
      */
     public static final String UI_THEME = "ui_theme";
-
     /**
      * Use with {@link android.os.Bundle} to pass in the name of your company as a navigation
      * argument when navigating to {@link com.glia.widgets.chat.ChatActivity}
      */
     public static final String COMPANY_NAME = "company_name";
-
     /**
      * Use with {@link android.os.Bundle} to pass in the id of the queue you wish to enroll in
      * as a navigation argument when navigating to {@link com.glia.widgets.chat.ChatActivity}
      */
     public static final String QUEUE_ID = "queue_id";
-
     /**
      * Use with {@link android.os.Bundle} to pass in a context url as a navigation
      * argument when navigating to {@link com.glia.widgets.chat.ChatActivity}
+     *
      * @deprecated Use {@link com.glia.widgets.GliaWidgets#CONTEXT_ASSET_ID}
      */
     @Deprecated
     public static final String CONTEXT_URL = "context_url";
-
     /**
      * Use with {@link android.os.Bundle} to pass in a context asset ID as a navigation
      * argument when navigating to {@link com.glia.widgets.chat.ChatActivity}
      */
     public static final String CONTEXT_ASSET_ID = "context_asset_id";
-
     /**
      * Use with {@link android.os.Bundle} to pass in a boolean which represents if you would like to
      * use the chat head bubble as an overlay as a navigation argument when
@@ -78,7 +71,6 @@ public class GliaWidgets {
      * When this value is not passed then by default this value is true.
      */
     public static final String USE_OVERLAY = "use_overlay";
-
     /**
      * Use with {@link android.os.Bundle} to pass an input parameter to the call activity to
      * tell it which type of engagement you would like to start. Can be one of:
@@ -86,36 +78,30 @@ public class GliaWidgets {
      * If no parameter is passed then will default to {@link MEDIA_TYPE_AUDIO}
      */
     public static final String MEDIA_TYPE = "media_type";
-
     /**
      * Pass this parameter as an input parameter with {@link MEDIA_TYPE} as its key to
      * {@link com.glia.widgets.call.CallActivity} to start an audio call media engagement.
      */
     public static final String MEDIA_TYPE_AUDIO = "media_type_audio";
-
     /**
      * Pass this parameter as an input parameter with {@link MEDIA_TYPE} as its key to
      * {@link com.glia.widgets.call.CallActivity} to start a video call media engagement.
      */
     public static final String MEDIA_TYPE_VIDEO = "media_type_video";
-
     /**
      * Pass this parameter to call activity to tell it that upgrade to audio/video call is ongoing
      * If no parameter is passed then will default to false
      */
     public static final String IS_UPGRADE_TO_CALL = "upgrade_to_call";
-
     public static final String SURVEY = "survey";
-
     /**
      * Use with {@link android.os.Bundle} to pass in
      * {@link com.glia.androidsdk.screensharing.ScreenSharing.Mode} as a navigation
      * argument when navigating to {@link com.glia.widgets.chat.ChatActivity}
      */
     public static final String SCREEN_SHARING_MODE = "screens_haring_mode";
-
     public static final String CHAT_TYPE = "chat_type";
-
+    private final static String TAG = "GliaWidgets";
     @Nullable
     private static CustomCardAdapter customCardAdapter = new WebViewCardAdapter();
 
@@ -148,16 +134,16 @@ private static GliaConfig createGliaConfig(GliaWidgetsConfig gliaWidgetsConfig)
         GliaConfig.Builder builder = new GliaConfig.Builder();
         setAuthorization(gliaWidgetsConfig, builder);
         return builder
-                .setSiteId(gliaWidgetsConfig.getSiteId())
-                .setRegion(gliaWidgetsConfig.getRegion())
-                .setBaseDomain(gliaWidgetsConfig.getBaseDomain())
-                .setContext(gliaWidgetsConfig.getContext())
-                .build();
+            .setSiteId(gliaWidgetsConfig.getSiteId())
+            .setRegion(gliaWidgetsConfig.getRegion())
+            .setBaseDomain(gliaWidgetsConfig.getBaseDomain())
+            .setContext(gliaWidgetsConfig.getContext())
+            .build();
     }
 
     private static void setAuthorization(
-            GliaWidgetsConfig widgetsConfig,
-            GliaConfig.Builder builder
+        GliaWidgetsConfig widgetsConfig,
+        GliaConfig.Builder builder
     ) {
         if (widgetsConfig.getSiteApiKey() != null) {
             builder.setSiteApiKey(widgetsConfig.getSiteApiKey());
@@ -237,14 +223,14 @@ public static void endEngagement() {
     @Deprecated
     public static void updateVisitorInfo(VisitorInfoUpdate visitorInfoUpdate, Consumer exceptionConsumer) {
         Dependencies.glia().updateVisitorInfo(new VisitorInfoUpdateRequest.Builder()
-                .setName(visitorInfoUpdate.getName())
-                .setEmail(visitorInfoUpdate.getEmail())
-                .setPhone(visitorInfoUpdate.getPhone())
-                .setNote(visitorInfoUpdate.getNote())
-                .setCustomAttributes(visitorInfoUpdate.getCustomAttributes())
-                .setCustomAttrsUpdateMethod(visitorInfoUpdate.getCustomAttrsUpdateMethod())
-                .setNoteUpdateMethod(visitorInfoUpdate.getNoteUpdateMethod())
-                .build(), e -> {
+            .setName(visitorInfoUpdate.getName())
+            .setEmail(visitorInfoUpdate.getEmail())
+            .setPhone(visitorInfoUpdate.getPhone())
+            .setNote(visitorInfoUpdate.getNote())
+            .setCustomAttributes(visitorInfoUpdate.getCustomAttributes())
+            .setCustomAttrsUpdateMethod(visitorInfoUpdate.getCustomAttrsUpdateMethod())
+            .setNoteUpdateMethod(visitorInfoUpdate.getNoteUpdateMethod())
+            .build(), e -> {
             if (e != null) {
                 exceptionConsumer.accept(new GliaWidgetException(e.debugMessage, e.cause));
             } else {
@@ -272,6 +258,14 @@ public static void getVisitorInfo(Consumer visitorCallback, Con
         });
     }
 
+    /**
+     * @return current instance of {@link CustomCardAdapter}
+     */
+    @Nullable
+    public static CustomCardAdapter getCustomCardAdapter() {
+        return customCardAdapter;
+    }
+
     /**
      * Allows configuring custom response cards based on metadata.
      * 

@@ -287,14 +281,6 @@ public static void setCustomCardAdapter(@Nullable CustomCardAdapter customCardAd GliaWidgets.customCardAdapter = customCardAdapter; } - /** - * @return current instance of {@link CustomCardAdapter} - */ - @Nullable - public static CustomCardAdapter getCustomCardAdapter() { - return customCardAdapter; - } - /** * Creates `Authentication` instance for a given JWT token. * @@ -364,7 +350,7 @@ private static void setupRxErrorHandler() { private static void throwUncaughtException(Throwable e) { Thread.UncaughtExceptionHandler handler = - Thread.currentThread().getUncaughtExceptionHandler(); + Thread.currentThread().getUncaughtExceptionHandler(); if (handler != null) { handler.uncaughtException(Thread.currentThread(), e); } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index b3178fa60..b8036b355 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -51,6 +51,7 @@ import com.glia.widgets.core.engagement.domain.GliaOnEngagementUseCase import com.glia.widgets.core.engagement.domain.IsOngoingEngagementUseCase import com.glia.widgets.core.engagement.domain.IsQueueingEngagementUseCase import com.glia.widgets.core.engagement.domain.SetEngagementConfigUseCase +import com.glia.widgets.core.engagement.domain.UpdateOperatorDefaultImageUrlUseCase import com.glia.widgets.core.engagement.domain.model.EngagementStateEvent import com.glia.widgets.core.engagement.domain.model.EngagementStateEventVisitor import com.glia.widgets.core.engagement.domain.model.EngagementStateEventVisitor.OperatorVisitor @@ -140,6 +141,7 @@ internal class ChatController( private val acceptMediaUpgradeOfferUseCase: AcceptMediaUpgradeOfferUseCase, private val determineGvaButtonTypeUseCase: DetermineGvaButtonTypeUseCase, private val isAuthenticatedUseCase: IsAuthenticatedUseCase, + private val updateOperatorDefaultImageUrlUseCase: UpdateOperatorDefaultImageUrlUseCase, private val chatManager: ChatManager ) : GliaOnEngagementUseCase.Listener, GliaOnEngagementEndUseCase.Listener, OnSurveyListener { private var backClickedListener: ChatView.OnBackClickedListener? = null @@ -207,6 +209,7 @@ internal class ChatController( ) { val queueIds = if (queueId != null) arrayOf(queueId) else emptyArray() engagementConfigUseCase(chatType, queueIds) + updateOperatorDefaultImageUrlUseCase() if (!hasPendingSurveyUseCase.invoke()) { ensureSecureMessagingAvailable() diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.kt index f7c5dafe2..c4d0abb32 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaOperatorRepository.kt @@ -6,10 +6,14 @@ import androidx.core.util.Consumer import com.glia.androidsdk.Operator import com.glia.widgets.core.engagement.data.LocalOperator import com.glia.widgets.di.GliaCore -import com.glia.widgets.helper.toLocal +import com.glia.widgets.helper.imageUrl internal class GliaOperatorRepository(private val gliaCore: GliaCore) { private val cachedOperators = SimpleArrayMap() + + @VisibleForTesting + var operatorDefaultImageUrl: String? = null + fun getOperatorById(operatorId: String, callback: Consumer) { val cachedOperator = cachedOperators[operatorId] if (cachedOperator != null) { @@ -17,24 +21,22 @@ internal class GliaOperatorRepository(private val gliaCore: GliaCore) { return } gliaCore.getOperator(operatorId) { operator: Operator?, _ -> - operator?.let { updateIfExists(it) }?.also { putOperator(it) }.also { callback.accept(it) } + operator?.let { mapOperator(it) }?.also { putOperator(it) }.also { callback.accept(it) } } } - fun emit(operator: Operator) = putOperator(updateIfExists(operator)) + fun emit(operator: Operator) = putOperator(mapOperator(operator)) @VisibleForTesting - fun updateIfExists(operator: Operator): LocalOperator { - val newOperator = operator.toLocal() - - val oldOperator = cachedOperators[operator.id] ?: return newOperator - - return if (oldOperator.imageUrl != null) oldOperator else oldOperator.copy(imageUrl = newOperator.imageUrl) - } + fun mapOperator(operator: Operator): LocalOperator = operator.run { LocalOperator(id, name, imageUrl ?: operatorDefaultImageUrl) } @VisibleForTesting fun putOperator(operator: LocalOperator) { operator.apply { cachedOperators.put(id, this) } } + fun updateOperatorDefaultImageUrl(imageUrl: String?) { + operatorDefaultImageUrl = imageUrl + } + } diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt index db6de9bfe..555784202 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/MapOperatorUseCase.kt @@ -4,6 +4,7 @@ import com.glia.androidsdk.chat.Chat import com.glia.androidsdk.chat.ChatMessage import com.glia.androidsdk.chat.OperatorMessage import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal +import com.glia.widgets.helper.toChatMessageInternal import io.reactivex.Single import kotlin.jvm.optionals.getOrNull @@ -14,8 +15,9 @@ internal class MapOperatorUseCase(private val getOperatorUseCase: GetOperatorUse else -> processVisitorMessage(chatMessage) } - private fun processOperatorMessage(chatMessage: OperatorMessage): Single = - getOperatorUseCase.execute(chatMessage.operatorId!!).map { ChatMessageInternal(chatMessage, it.getOrNull()) } + private fun processOperatorMessage(chatMessage: OperatorMessage): Single = chatMessage + .takeIf { it.operatorImageUrl != null }?.toChatMessageInternal()?.let { Single.just(it) } + ?: getOperatorUseCase.execute(chatMessage.operatorId!!).map { ChatMessageInternal(chatMessage, it.getOrNull()) } private fun processVisitorMessage(chatMessage: ChatMessage): Single = Single.just(ChatMessageInternal(chatMessage)) diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/UpdateOperatorDefaultImageUrlUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/UpdateOperatorDefaultImageUrlUseCase.kt new file mode 100644 index 000000000..9e57381e6 --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/UpdateOperatorDefaultImageUrlUseCase.kt @@ -0,0 +1,14 @@ +package com.glia.widgets.core.engagement.domain + +import com.glia.widgets.chat.domain.SiteInfoUseCase +import com.glia.widgets.core.engagement.GliaOperatorRepository +import kotlin.jvm.optionals.getOrNull + +internal class UpdateOperatorDefaultImageUrlUseCase( + private val operatorRepository: GliaOperatorRepository, + private val siteInfoUseCase: SiteInfoUseCase +) { + operator fun invoke() = siteInfoUseCase.execute { response, _ -> + response.defaultOperatorPicture?.url?.getOrNull()?.also(operatorRepository::updateOperatorDefaultImageUrl) + } +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java index a1de376df..16d3690aa 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java @@ -133,6 +133,7 @@ public ChatController getChatController(ChatViewCallback chatViewCallback) { useCaseFactory.createAcceptMediaUpgradeOfferUseCase(), useCaseFactory.createDetermineGvaButtonTypeUseCase(), useCaseFactory.createIsAuthenticatedUseCase(), + useCaseFactory.createUpdateOperatorDefaultImageUrlUseCase(), managerFactory.getChatManager() ); } else { diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java index f2ecce4f6..0cb124b18 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java @@ -87,6 +87,7 @@ import com.glia.widgets.core.engagement.domain.ResetSurveyUseCase; import com.glia.widgets.core.engagement.domain.SetEngagementConfigUseCase; import com.glia.widgets.core.engagement.domain.ShouldShowMediaEngagementViewUseCase; +import com.glia.widgets.core.engagement.domain.UpdateOperatorDefaultImageUrlUseCase; import com.glia.widgets.core.fileupload.domain.AddFileAttachmentsObserverUseCase; import com.glia.widgets.core.fileupload.domain.AddFileToAttachmentAndUploadUseCase; import com.glia.widgets.core.fileupload.domain.GetFileAttachmentsUseCase; @@ -1029,6 +1030,11 @@ public SendUnsentMessagesUseCase createSendUnsentMessagesUseCase() { return new SendUnsentMessagesUseCase(repositoryFactory.getGliaMessageRepository()); } + @NonNull + public UpdateOperatorDefaultImageUrlUseCase createUpdateOperatorDefaultImageUrlUseCase() { + return new UpdateOperatorDefaultImageUrlUseCase(repositoryFactory.getOperatorRepository(), createSiteInfoUseCase()); + } + public void resetState() { createResetSurveyUseCase().invoke(); } diff --git a/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt b/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt index 94051c7ac..2cbd4da4e 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt @@ -11,10 +11,12 @@ import com.glia.androidsdk.Engagement import com.glia.androidsdk.Operator import com.glia.androidsdk.chat.AttachmentFile import com.glia.androidsdk.chat.MessageAttachment +import com.glia.androidsdk.chat.OperatorMessage import com.glia.androidsdk.chat.SingleChoiceAttachment import com.glia.androidsdk.queuing.Queue import com.glia.widgets.UiTheme import com.glia.widgets.core.engagement.data.LocalOperator +import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal import com.glia.widgets.view.unifiedui.deepMerge import kotlin.jvm.optionals.getOrNull @@ -34,8 +36,6 @@ internal val Operator.formattedName: String get() = name.substringBefore(' ') internal val Operator.imageUrl: String? get() = picture?.url?.getOrNull() -internal fun Operator.toLocal(): LocalOperator = LocalOperator(id, name, imageUrl) - internal fun UiTheme?.isAlertDialogButtonUseVerticalAlignment(): Boolean = this?.gliaAlertDialogButtonUseVerticalAlignment ?: false internal fun UiTheme?.getFullHybridTheme(newTheme: UiTheme?): UiTheme = deepMerge(newTheme) ?: UiTheme.UiThemeBuilder().build() @@ -48,3 +48,6 @@ internal fun String.fromHtml(flags: Int = Html.FROM_HTML_MODE_COMPACT): Spanned internal val AttachmentFile.isImage: Boolean get() = contentType.startsWith("image") internal fun MessageAttachment.asSingleChoice(): SingleChoiceAttachment? = this as? SingleChoiceAttachment + +internal fun OperatorMessage.toChatMessageInternal(): ChatMessageInternal = + ChatMessageInternal(this, LocalOperator(operatorId.orEmpty(), operatorName.orEmpty(), operatorImageUrl)) diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt index 39cd8839a..8ee034033 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt @@ -28,6 +28,7 @@ import com.glia.widgets.core.engagement.domain.GliaOnEngagementUseCase import com.glia.widgets.core.engagement.domain.IsOngoingEngagementUseCase import com.glia.widgets.core.engagement.domain.IsQueueingEngagementUseCase import com.glia.widgets.core.engagement.domain.SetEngagementConfigUseCase +import com.glia.widgets.core.engagement.domain.UpdateOperatorDefaultImageUrlUseCase import com.glia.widgets.core.fileupload.domain.AddFileAttachmentsObserverUseCase import com.glia.widgets.core.fileupload.domain.AddFileToAttachmentAndUploadUseCase import com.glia.widgets.core.fileupload.domain.GetFileAttachmentsUseCase @@ -104,6 +105,7 @@ class ChatControllerTest { private lateinit var isFileReadyForPreviewUseCase: IsFileReadyForPreviewUseCase private lateinit var acceptMediaUpgradeOfferUseCase: AcceptMediaUpgradeOfferUseCase private lateinit var determineGvaButtonTypeUseCase: DetermineGvaButtonTypeUseCase + private lateinit var updateOperatorDefaultImageUrlUseCase: UpdateOperatorDefaultImageUrlUseCase private lateinit var chatController: ChatController private lateinit var isAuthenticatedUseCase: IsAuthenticatedUseCase @@ -156,6 +158,7 @@ class ChatControllerTest { acceptMediaUpgradeOfferUseCase = mock() determineGvaButtonTypeUseCase = mock() isAuthenticatedUseCase = mock() + updateOperatorDefaultImageUrlUseCase = mock() chatManager = mock() chatController = ChatController( @@ -204,6 +207,7 @@ class ChatControllerTest { acceptMediaUpgradeOfferUseCase = acceptMediaUpgradeOfferUseCase, determineGvaButtonTypeUseCase = determineGvaButtonTypeUseCase, isAuthenticatedUseCase = isAuthenticatedUseCase, + updateOperatorDefaultImageUrlUseCase = updateOperatorDefaultImageUrlUseCase, chatManager = chatManager ) } diff --git a/widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaOperatorRepositoryTest.kt b/widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaOperatorRepositoryTest.kt index 6efc33754..9bc801376 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaOperatorRepositoryTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaOperatorRepositoryTest.kt @@ -7,7 +7,7 @@ import com.glia.androidsdk.Operator.Picture import com.glia.androidsdk.RequestCallback import com.glia.widgets.core.engagement.data.LocalOperator import com.glia.widgets.di.GliaCore -import junit.framework.TestCase.assertEquals +import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Before import org.junit.Test @@ -46,40 +46,22 @@ class GliaOperatorRepositoryTest { } @Test - fun `updateIfExists returns the inserted operator if it doesn't exist in cache`() { - mockOperator() - val newLocalOperator = repository.updateIfExists(operator) - assertEquals(localOperator, newLocalOperator) - } - - @Test - fun `updateIfExists returns the updated operator if it exists in cache without image url`() { - val imageUrl = "new_image_url" - mockOperator(imageUrl) - repository.putOperator(localOperator.copy(imageUrl = null)) - val newLocalOperator = repository.updateIfExists(operator) - - assertNotEquals(localOperator, newLocalOperator) - assertEquals(newLocalOperator.imageUrl, imageUrl) - } - - - @Test - fun `updateIfExists returns the old operator if it exists in cache with image url`() { - val imageUrl = "new_image_url" - mockOperator(imageUrl) - repository.putOperator(localOperator) - val newLocalOperator = repository.updateIfExists(operator) - - assertEquals(localOperator, newLocalOperator) - assertEquals(newLocalOperator.imageUrl, localOperator.imageUrl) + fun `mapOperator return operator with operatorDefaultImageUrl when operator image is null`() { + mockOperator(null) + val operatorDefaultImageURL = "default_image" + repository.updateOperatorDefaultImageUrl(operatorDefaultImageURL) + val newOperator = repository.mapOperator(operator) + assertEquals(newOperator.imageUrl, operatorDefaultImageURL) + assertNotEquals(localOperator, newOperator) + assertEquals(localOperator.id, newOperator.id) + assertEquals(localOperator.name, newOperator.name) } @Test fun `emit updates operator and then puts it into cache`() { mockOperator() repository.emit(operator) - verify(repository).updateIfExists(operator) + verify(repository).mapOperator(operator) verify(repository).putOperator(any()) } @@ -110,15 +92,13 @@ class GliaOperatorRepositoryTest { stubGetOperatorResponse(null, GliaException("", GliaException.Cause.INVALID_INPUT)) val callback: Consumer = mock() - repository.getOperatorById(operator.id, callback); + repository.getOperatorById(operator.id, callback) verify(callback).accept(null) } private fun stubGetOperatorResponse(operator: Operator?, exception: GliaException?) { doAnswer { invocation: InvocationOnMock -> - val callback = invocation.getArgument>(1) - callback.onResult(operator, exception) - callback + invocation.getArgument>(1).apply { onResult(operator, exception) } }.whenever(core).getOperator(anyString(), any()) } } From 8ee8b32edb146a3eb27d9f696513fdc1f195873c Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Tue, 15 Aug 2023 15:13:36 +0300 Subject: [PATCH 40/69] Improve chat transcript quick reply button behaviour MOB 2535 --- .../java/com/glia/widgets/chat/ChatManager.kt | 11 ++++++--- .../widgets/chat/controller/ChatController.kt | 1 + .../com/glia/widgets/di/ManagerFactory.kt | 3 ++- .../com/glia/widgets/chat/ChatManagerTest.kt | 24 ++++++++++++++++++- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt index f40cc74c5..b104dbc5a 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt @@ -21,6 +21,7 @@ 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.VisitorMessageItem +import com.glia.widgets.core.engagement.domain.IsOngoingEngagementUseCase 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 @@ -42,6 +43,7 @@ internal class ChatManager constructor( private val appendNewChatMessageUseCase: AppendNewChatMessageUseCase, private val sendUnsentMessagesUseCase: SendUnsentMessagesUseCase, private val handleCustomCardClickUseCase: HandleCustomCardClickUseCase, + private val isOngoingEngagementUseCase: IsOngoingEngagementUseCase, private val compositeDisposable: CompositeDisposable = CompositeDisposable(), private val state: BehaviorProcessor = BehaviorProcessor.create(), private val quickReplies: BehaviorProcessor> = BehaviorProcessor.create(), @@ -101,13 +103,16 @@ internal class ChatManager constructor( @VisibleForTesting fun updateQuickReplies(state: State) { - val quickReplyItems = (state.chatItems.lastOrNull() as? GvaQuickReplies)?.run { options } ?: emptyList() - - quickReplies.onNext(quickReplyItems) + state.takeIf { isOngoingEngagementUseCase() } + ?.run { chatItems.lastOrNull() as? GvaQuickReplies } + ?.run { options } + .orEmpty() + .also(quickReplies::onNext) } @VisibleForTesting fun subscribeToQuickReplies(onQuickReplyReceived: (List) -> Unit): Disposable = quickReplies + .distinctUntilChanged() .observeOn(AndroidSchedulers.mainThread()) .subscribe { onQuickReplyReceived(it) } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index b8036b355..a2493c0bc 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -759,6 +759,7 @@ internal class ChatController( } private fun sendGvaResponse(singleChoiceAttachment: SingleChoiceAttachment) { + addQuickReplyButtons(emptyList()) sendMessageUseCase.execute(singleChoiceAttachment, sendMessageCallback) } diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt b/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt index 525db9a78..575ce05b8 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt @@ -13,7 +13,8 @@ internal class ManagerFactory(private val useCaseFactory: UseCaseFactory) { appendHistoryChatMessageUseCase = createAppendHistoryChatMessageUseCase(), appendNewChatMessageUseCase = createAppendNewChatMessageUseCase(), sendUnsentMessagesUseCase = createSendUnsentMessagesUseCase(), - handleCustomCardClickUseCase = createHandleCustomCardClickUseCase() + handleCustomCardClickUseCase = createHandleCustomCardClickUseCase(), + isOngoingEngagementUseCase = createIsOngoingEngagementUseCase() ) } } diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt index 9f2c23beb..8f2232512 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt @@ -19,6 +19,7 @@ import com.glia.widgets.chat.model.NewMessagesDividerItem import com.glia.widgets.chat.model.OperatorMessageItem import com.glia.widgets.chat.model.OperatorStatusItem import com.glia.widgets.chat.model.VisitorMessageItem +import com.glia.widgets.core.engagement.domain.IsOngoingEngagementUseCase 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 @@ -61,6 +62,7 @@ class ChatManagerTest { private lateinit var appendNewChatMessageUseCase: AppendNewChatMessageUseCase private lateinit var sendUnsentMessagesUseCase: SendUnsentMessagesUseCase private lateinit var handleCustomCardClickUseCase: HandleCustomCardClickUseCase + private lateinit var isOngoingEngagementUseCase: IsOngoingEngagementUseCase private lateinit var subjectUnderTest: ChatManager private lateinit var state: ChatManager.State private lateinit var compositeDisposable: CompositeDisposable @@ -79,7 +81,8 @@ class ChatManagerTest { appendNewChatMessageUseCase = mock() sendUnsentMessagesUseCase = mock() handleCustomCardClickUseCase = mock() - compositeDisposable = mock() + isOngoingEngagementUseCase = mock() + compositeDisposable = spy() stateProcessor = spy(BehaviorProcessor.create()) quickReplies = BehaviorProcessor.create() action = PublishProcessor.create() @@ -94,6 +97,7 @@ class ChatManagerTest { appendNewChatMessageUseCase, sendUnsentMessagesUseCase, handleCustomCardClickUseCase, + isOngoingEngagementUseCase, compositeDisposable, stateProcessor, quickReplies, @@ -103,6 +107,7 @@ class ChatManagerTest { state = spy(ChatManager.State()) } + @Test fun `removeNewMessagesDivider removes NewMessagesDivider when it contains in chat items`() { state.chatItems.add(NewMessagesDividerItem) @@ -515,10 +520,27 @@ class ChatManagerTest { } state.chatItems.add(quickReplies) + whenever(isOngoingEngagementUseCase()) doReturn true + subjectUnderTest.updateQuickReplies(state) quickRepliesTest.assertValue(mockOptions) } + @Test + fun `updateQuickReplies triggers quickReplies onNext with empty list when the is no ongoing engagement`() { + val quickRepliesTest = quickReplies.test() + val mockOptions: List = listOf(mock()) + val quickReplies: GvaQuickReplies = mock { + on { options } doReturn mockOptions + } + state.chatItems.add(quickReplies) + + whenever(isOngoingEngagementUseCase()) doReturn false + + subjectUnderTest.updateQuickReplies(state) + quickRepliesTest.assertValue(emptyList()) + } + @Test fun `subscribeToQuickReplies subscribes to quickReplies`() { val testSubscriber = quickReplies.test() From c80bf21c992c3bdb86daddcb1302c5322ef30408 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Tue, 15 Aug 2023 14:19:09 +0300 Subject: [PATCH 41/69] Scroll chat to the end after any type of visitor message sent MOB 2517 --- .../java/com/glia/widgets/chat/controller/ChatController.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index a2493c0bc..7d9b4b163 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -158,6 +158,7 @@ internal class ChatController( object : GliaSendMessageUseCase.Listener { override fun messageSent(message: VisitorMessage?) { Logger.d(TAG, "messageSent: $message, id: ${message?.id}") + scrollChatToBottom() } override fun onMessageValidated() { @@ -999,4 +1000,9 @@ internal class ChatController( is Gva.ButtonType.Url -> viewCallback?.requestOpenUri(buttonType.uri) } } + + private fun scrollChatToBottom() { + emitViewState { chatState.copy(isChatInBottom = true) } + viewCallback?.smoothScrollToBottom() + } } From e15fb6dd48c0d9cc2f4640c3299629a059f2211a Mon Sep 17 00:00:00 2001 From: Karl Valliste Date: Wed, 16 Aug 2023 11:49:56 +0300 Subject: [PATCH 42/69] Bugfix/mob 2523 investigate engagement state (#712) * Implemented new engagement type for avoiding persisting dialogs without dismissing actual dialogs MOB-2523 --- .../com/glia/widgets/call/CallController.java | 5 +- .../widgets/chat/controller/ChatController.kt | 6 +-- .../GliaEngagementStateRepository.java | 18 +++++-- .../domain/model/EngagementStateEvent.java | 15 ++++++ .../GliaEngagementStateRepositoryTest.kt | 54 +++++++++++++++++++ 5 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaEngagementStateRepositoryTest.kt diff --git a/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java b/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java index 292454ceb..f58292193 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java +++ b/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java @@ -231,12 +231,9 @@ public void onHoldChanged(boolean isOnHold) { public void engagementEnded() { Logger.d(TAG, "engagementEndedByOperator"); stop(); - // TODO re-enable during MOB-2523 - /* if (!isOngoingEngagementUseCase.invoke()) { - dialogController.dismissDialogs() + dialogController.dismissDialogs(); } - */ } @Override diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index 7d9b4b163..2543f64d8 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -520,12 +520,12 @@ internal class ChatController( EngagementStateEvent.Type.ENGAGEMENT_ENDED -> { Logger.d(TAG, "Engagement Ended") - // TODO re-enable during MOB-2523 - /* if (!isOngoingEngagementUseCase.invoke()) { dialogController.dismissDialogs() } - */ + } + EngagementStateEvent.Type.NO_ENGAGEMENT -> { + Logger.d(TAG, "NoEngagement") } } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaEngagementStateRepository.java b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaEngagementStateRepository.java index 68534450f..00bc0b691 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaEngagementStateRepository.java +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/GliaEngagementStateRepository.java @@ -1,6 +1,7 @@ package com.glia.widgets.core.engagement; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.glia.androidsdk.Engagement; import com.glia.androidsdk.Operator; @@ -27,11 +28,12 @@ public class GliaEngagementStateRepository { private final BehaviorProcessor> engagementStateProcessor = BehaviorProcessor.createDefault(Optional.empty()); private final BehaviorProcessor engagementStateEventProcessor = BehaviorProcessor.createDefault( - new EngagementStateEvent.EngagementEndedEvent() + new EngagementStateEvent.NoEngagementEvent() ); private final Flowable engagementStateEventFlowable = engagementStateEventProcessor.onBackpressureLatest(); private final GliaOperatorRepository operatorRepository; private CompositeDisposable disposable = new CompositeDisposable(); + private boolean isOngoingEngagement = false; public GliaEngagementStateRepository(GliaOperatorRepository operatorRepository) { this.operatorRepository = operatorRepository; @@ -95,16 +97,21 @@ Operator getOperator() { .orElse(null); } - private EngagementStateEvent mapToEngagementStateChangeEvent( - EngagementState engagementState, - @Nullable Operator operator + @VisibleForTesting + protected EngagementStateEvent mapToEngagementStateChangeEvent( + EngagementState engagementState, + @Nullable Operator operator ) { - if (engagementState == null) { + if (engagementState == null && isOngoingEngagement) { + isOngoingEngagement = false; return new EngagementStateEvent.EngagementEndedEvent(); + } else if (engagementState == null) { + return new EngagementStateEvent.NoEngagementEvent(); } else if (engagementState.getVisitorStatus() == EngagementState.VisitorStatus.TRANSFERRING) { return new EngagementStateEvent.EngagementTransferringEvent(); } else { if (operator == null) { + isOngoingEngagement = true; return new EngagementStateEvent.EngagementOperatorConnectedEvent(engagementState.getOperator()); } else { if (!engagementState.getOperator().getId().equals(operator.getId())) { @@ -124,6 +131,7 @@ private void updateOperatorOnEngagementStateChanged(EngagementStateEvent engagem notifyOperatorUpdate(visitor.visit(engagementStateEvent)); break; case ENGAGEMENT_TRANSFERRING: + case NO_ENGAGEMENT: case ENGAGEMENT_ENDED: notifyOperatorUpdate(null); break; diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/EngagementStateEvent.java b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/EngagementStateEvent.java index 768973b0b..26a272398 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/EngagementStateEvent.java +++ b/widgetssdk/src/main/java/com/glia/widgets/core/engagement/domain/model/EngagementStateEvent.java @@ -9,6 +9,7 @@ enum Type { ENGAGEMENT_ONGOING, ENGAGEMENT_OPERATOR_CONNECTED, ENGAGEMENT_OPERATOR_CHANGED, + NO_ENGAGEMENT } Type getType(); @@ -94,6 +95,7 @@ public T accept(EngagementStateEventVisitor visitor) { } class EngagementEndedEvent implements EngagementStateEvent { + @Override public Type getType() { return Type.ENGAGEMENT_ENDED; @@ -104,6 +106,19 @@ public T accept(EngagementStateEventVisitor visitor) { return visitor.visit(this); } } + + class NoEngagementEvent implements EngagementStateEvent { + + @Override + public Type getType() { + return Type.NO_ENGAGEMENT; + } + + @Override + public T accept(EngagementStateEventVisitor visitor) { + return visitor.visit(this); + } + } } diff --git a/widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaEngagementStateRepositoryTest.kt b/widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaEngagementStateRepositoryTest.kt new file mode 100644 index 000000000..ef632a06c --- /dev/null +++ b/widgetssdk/src/test/java/com/glia/widgets/core/engagement/GliaEngagementStateRepositoryTest.kt @@ -0,0 +1,54 @@ +package com.glia.widgets.core.engagement + +import com.glia.androidsdk.Operator +import com.glia.androidsdk.engagement.EngagementState +import com.glia.widgets.core.engagement.domain.model.EngagementStateEvent +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class GliaEngagementStateRepositoryTest { + + private val engagement: EngagementState = mock() + private val operator1: Operator = mock() + private val operator2: Operator = mock() + private val operatorRepo: GliaOperatorRepository = mock() + private val repo = GliaEngagementStateRepository(operatorRepo) + private val ID1 = "1" + private val ID2 = "2" + + + @Test + fun `mapToEngagementStateChangeEvent returns NoEngagement when engagementState null and is not ongoing engagement`() { + assertTrue(repo.mapToEngagementStateChangeEvent(null, null) is EngagementStateEvent.NoEngagementEvent) + } + + @Test + fun `mapToEngagementStateChangeEvent returns NoEngagement when engagementState null and is ongoing engagement`() { + assertTrue(repo.mapToEngagementStateChangeEvent(engagement, null) is EngagementStateEvent.EngagementOperatorConnectedEvent) + assertTrue(repo.mapToEngagementStateChangeEvent(null, null) is EngagementStateEvent.EngagementEndedEvent) + } + + @Test + fun `mapToEngagementStateChangeEvent returns EngagementOngoingEvent when operator ids equal`() { + whenever(engagement.operator).thenReturn(operator1) + whenever(operator1.id).thenReturn(ID1) + whenever(operator2.id).thenReturn(ID1) + assertTrue(repo.mapToEngagementStateChangeEvent(engagement, operator2) is EngagementStateEvent.EngagementOngoingEvent) + } + + @Test + fun `mapToEngagementStateChangeEvent returns EngagementOperatorChangedEvent when operator ids are different`() { + whenever(engagement.operator).thenReturn(operator1) + whenever(operator1.id).thenReturn(ID1) + whenever(operator2.id).thenReturn(ID2) + assertTrue(repo.mapToEngagementStateChangeEvent(engagement, operator2) is EngagementStateEvent.EngagementOperatorChangedEvent) + } + + @Test + fun `mapToEngagementStateChangeEvent returns EngagementTransferringEvent when operator ids equal`() { + whenever(engagement.visitorStatus).thenReturn(EngagementState.VisitorStatus.TRANSFERRING) + assertTrue(repo.mapToEngagementStateChangeEvent(engagement, null) is EngagementStateEvent.EngagementTransferringEvent) + } +} From 5747ce18a672b71f996cceb117f166e087143ab5 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Thu, 17 Aug 2023 10:29:24 +0300 Subject: [PATCH 43/69] Prevent Chat RecyclerView scroll call when the adapter is empty. MOB 2543 --- widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt index 80bd51efe..2a045b441 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt @@ -439,10 +439,12 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty } override fun smoothScrollToBottom() { + if (adapter.itemCount < 1) return post { binding.chatRecyclerView.smoothScrollToPosition(adapter.itemCount - 1) } } override fun scrollToBottomImmediate() { + if (adapter.itemCount < 1) return post { binding.chatRecyclerView.scrollToPosition(adapter.itemCount - 1) } } From 0cc19e1bca8e797e2037957671247520d4e356e3 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Fri, 18 Aug 2023 11:19:01 +0300 Subject: [PATCH 44/69] =?UTF-8?q?Fix=20=F0=9F=A4=96Line=20breaks=20are=20n?= =?UTF-8?q?ot=20rendered=20in=20GVA=20content?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MOB 2546 --- .../src/main/java/com/glia/widgets/di/UseCaseFactory.java | 2 +- .../src/main/java/com/glia/widgets/helper/CommonExtensions.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java index 0cb124b18..431f58719 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java @@ -863,7 +863,7 @@ public HandleCallPermissionsUseCase createHandleCallPermissionsUseCase() { @NonNull private Gson getGvaGson() { if (gvaGson == null) { - gvaGson = new GsonBuilder().create(); + gvaGson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); } return gvaGson; diff --git a/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt b/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt index 2cbd4da4e..4271b77ca 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt @@ -41,9 +41,9 @@ internal fun UiTheme?.isAlertDialogButtonUseVerticalAlignment(): Boolean = this? internal fun UiTheme?.getFullHybridTheme(newTheme: UiTheme?): UiTheme = deepMerge(newTheme) ?: UiTheme.UiThemeBuilder().build() /** - * Returns styled text from the provided HTML string. + * Returns styled text from the provided HTML string. Replaces \n to
tag. */ -internal fun String.fromHtml(flags: Int = Html.FROM_HTML_MODE_COMPACT): Spanned = Html.fromHtml(this, flags) +internal fun String.fromHtml(flags: Int = Html.FROM_HTML_MODE_COMPACT): Spanned = Html.fromHtml(replace("\n", "
"), flags) internal val AttachmentFile.isImage: Boolean get() = contentType.startsWith("image") From e0d86d9e2d728ab2d76e4511df075fc2e0a47282 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Fri, 18 Aug 2023 15:01:10 +0300 Subject: [PATCH 45/69] Replace PagerSnapHelper to LinearSnapHelper for Gallery list --- .../glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt index 0e6aa97b7..53d6a4813 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt @@ -2,7 +2,7 @@ package com.glia.widgets.chat.adapter.holder import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.PagerSnapHelper +import androidx.recyclerview.widget.LinearSnapHelper import com.glia.widgets.UiTheme import com.glia.widgets.chat.adapter.ChatAdapter import com.glia.widgets.chat.adapter.GvaGalleryAdapter @@ -24,7 +24,7 @@ internal class GvaGalleryViewHolder( LinearLayoutManager.HORIZONTAL, false ) - PagerSnapHelper().attachToRecyclerView(contentBinding.cardRecyclerView) + LinearSnapHelper().attachToRecyclerView(contentBinding.cardRecyclerView) } fun bind(item: GvaGalleryCards, measuredHeight: Int?) { From 40ab1c84bf5c58541e5971cbdb369a58b67473d3 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Fri, 18 Aug 2023 17:35:13 +0300 Subject: [PATCH 46/69] Devs want deep link support in Widgets SDK - `glia://widgets/settings` - should route to the settings page. MOB 2551 --- app/src/main/AndroidManifest.xml | 100 +++++++++--------- .../java/com/glia/exampleapp/Activity.java | 8 ++ .../java/com/glia/exampleapp/MainFragment.kt | 5 +- app/src/main/res/navigation/nav_graph.xml | 12 +-- 4 files changed, 67 insertions(+), 58 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eb8b58ab2..b2579795e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,54 +1,54 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/glia/exampleapp/Activity.java b/app/src/main/java/com/glia/exampleapp/Activity.java index c80d80ee3..30c2c14bb 100644 --- a/app/src/main/java/com/glia/exampleapp/Activity.java +++ b/app/src/main/java/com/glia/exampleapp/Activity.java @@ -1,9 +1,11 @@ package com.glia.exampleapp; +import android.content.Intent; import android.net.Uri; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; +import androidx.navigation.Navigation; import com.glia.androidsdk.Glia; import com.glia.widgets.GliaWidgets; @@ -24,4 +26,10 @@ private void initGliaWidgetsWithDeepLink() { GliaWidgets.init(GliaWidgetsConfigManager.obtainConfigFromDeepLink(uri, getApplicationContext())); } } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + Navigation.findNavController(this, R.id.nav_host_fragment).handleDeepLink(intent); + } } diff --git a/app/src/main/java/com/glia/exampleapp/MainFragment.kt b/app/src/main/java/com/glia/exampleapp/MainFragment.kt index d7e149843..bf7ab3c91 100644 --- a/app/src/main/java/com/glia/exampleapp/MainFragment.kt +++ b/app/src/main/java/com/glia/exampleapp/MainFragment.kt @@ -379,8 +379,9 @@ class MainFragment : Fragment() { GliaWidgets.init( createDefaultConfig( context = requireActivity().applicationContext, - /*uiJsonRemoteConfig = UnifiedUiConfigurationLoader.fetchLocalGlobalColors(requireContext()),*/ - /*runtimeConfig = createSampleRuntimeConfig()*/ +// uiJsonRemoteConfig = UnifiedUiConfigurationLoader.fetchLocalGlobalColors(requireContext()), +// runtimeConfig = createSampleRuntimeConfig(), +// region = "us" ) ) prepareAuthentication() diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 01ab29327..45cb5dc02 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -19,22 +19,22 @@ + + + - + android:name="com.glia.exampleapp.SdkBasicSettingsFragment" /> - + android:name="com.glia.exampleapp.RuntimeThemeSettingsFragment" /> - + android:name="com.glia.exampleapp.RemoteThemeSettingsFragment" /> Date: Mon, 21 Aug 2023 12:45:31 +0300 Subject: [PATCH 47/69] Improve line break replacement logic --- .../src/main/java/com/glia/widgets/helper/CommonExtensions.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt b/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt index 4271b77ca..40cbf3cbe 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/helper/CommonExtensions.kt @@ -41,9 +41,9 @@ internal fun UiTheme?.isAlertDialogButtonUseVerticalAlignment(): Boolean = this? internal fun UiTheme?.getFullHybridTheme(newTheme: UiTheme?): UiTheme = deepMerge(newTheme) ?: UiTheme.UiThemeBuilder().build() /** - * Returns styled text from the provided HTML string. Replaces \n to
tag. + * Returns styled text from the provided HTML string. Replaces \n to
regardless of the operating system where the string was created. */ -internal fun String.fromHtml(flags: Int = Html.FROM_HTML_MODE_COMPACT): Spanned = Html.fromHtml(replace("\n", "
"), flags) +internal fun String.fromHtml(flags: Int = Html.FROM_HTML_MODE_COMPACT): Spanned = Html.fromHtml(replace("(\r\n|\n)".toRegex(), "
"), flags) internal val AttachmentFile.isImage: Boolean get() = contentType.startsWith("image") From 8735fd660f0859ceab62b4d6879a558f2115d6d4 Mon Sep 17 00:00:00 2001 From: Andriy Shevtsov Date: Fri, 18 Aug 2023 20:04:31 +0300 Subject: [PATCH 48/69] Improve solution for opening dialogs on top of CallVisualizerSupportActivity instead of integrator's Activity MOB-2515 (cherry picked from commit 450254dde04f80ac1c3783bcf52521633e002a47) --- .../ActivityWatcherForCallVisualizer.kt | 14 +++++++++++--- .../ActivityWatcherForCallVisualizerController.kt | 7 ++----- .../glia/widgets/core/dialog/DialogController.java | 2 +- .../src/main/java/com/glia/widgets/view/Dialogs.kt | 14 +++++++++++++- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizer.kt b/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizer.kt index 359d3ffba..f7aca9e51 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizer.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizer.kt @@ -96,13 +96,19 @@ internal class ActivityWatcherForCallVisualizer( override fun requestCameraPermission() { if (resumedActivity.get() is ComponentActivity) { - cameraPermissionLauncher?.run { this.launch(Manifest.permission.CAMERA) } + cameraPermissionLauncher?.run { + controller.setIsWaitingMediaProjectionResult(true) + this.launch(Manifest.permission.CAMERA) + } } } override fun requestOverlayPermission() { if (resumedActivity.get() is ComponentActivity) { - overlayPermissionLauncher?.run { this.launch(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) } + overlayPermissionLauncher?.run { + controller.setIsWaitingMediaProjectionResult(true) + this.launch(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) + } } } @@ -120,6 +126,7 @@ internal class ActivityWatcherForCallVisualizer( (activity as? ComponentActivity?)?.let { componentActivity -> cameraPermissionLauncher = componentActivity.registerForActivityResult(RequestPermission()) { isGranted: Boolean -> + controller.setIsWaitingMediaProjectionResult(false) controller.onRequestedCameraPermissionResult(isGranted) } } @@ -133,6 +140,7 @@ internal class ActivityWatcherForCallVisualizer( (activity as? ComponentActivity?)?.let { componentActivity -> overlayPermissionLauncher = componentActivity.registerForActivityResult(RequestPermission()) { isGranted: Boolean -> + controller.setIsWaitingMediaProjectionResult(false) controller.onMediaProjectionPermissionResult(isGranted, componentActivity) } } @@ -341,8 +349,8 @@ internal class ActivityWatcherForCallVisualizer( val contextWithStyle = activity.wrapWithMaterialThemeOverlay() alertDialog = Dialogs.showUpgradeDialog(contextWithStyle, theme, mediaUpgrade, { - dialogController.dismissCurrentDialog() controller.onMediaUpgradeReceived(mediaUpgrade.mediaUpgradeOffer) + dialogController.dismissCurrentDialog() }) { controller.onNegativeDialogButtonClicked() } diff --git a/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerController.kt b/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerController.kt index e2fd3dd9f..ff04588ce 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerController.kt @@ -130,11 +130,7 @@ internal class ActivityWatcherForCallVisualizerController( when (currentDialogMode) { MODE_NONE -> Logger.e(TAG, "$currentDialogMode should not have a dialog to click") MODE_ENABLE_SCREEN_SHARING_NOTIFICATIONS_AND_START_SHARING -> watcher.openNotificationChannelScreen() - MODE_START_SCREEN_SHARING -> - activity?.run { - setIsWaitingMediaProjectionResult(true) - startScreenSharing(this) - } + MODE_START_SCREEN_SHARING -> activity?.run { startScreenSharing(this) } MODE_MEDIA_UPGRADE -> watcher.openCallActivity() MODE_ENABLE_NOTIFICATION_CHANNEL -> watcher.openNotificationChannelScreen() MODE_OVERLAY_PERMISSION -> { @@ -196,6 +192,7 @@ internal class ActivityWatcherForCallVisualizerController( startMediaProjectionLaunchers[activity::class.simpleName].let { startMediaProjection -> activity.getSystemService(MediaProjectionManager::class.java) ?.let { mediaProjectionManager -> + setIsWaitingMediaProjectionResult(true) startMediaProjection?.launch(mediaProjectionManager.createScreenCaptureIntent()) Logger.d(TAG, "Acquire a media projection token: launching permission request") } diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/dialog/DialogController.java b/widgetssdk/src/main/java/com/glia/widgets/core/dialog/DialogController.java index 5a20494ea..9b0acfd0f 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/dialog/DialogController.java +++ b/widgetssdk/src/main/java/com/glia/widgets/core/dialog/DialogController.java @@ -116,7 +116,7 @@ public void showOverlayPermissionsDialog() { } public void dismissOverlayPermissionsDialog() { - Logger.d(TAG, "Dismiss Visitor Code Dialog"); + Logger.d(TAG, "Dismiss Overlay Permissions Dialog"); dialogManager.remove(new DialogState(Dialog.MODE_OVERLAY_PERMISSION)); } diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/Dialogs.kt b/widgetssdk/src/main/java/com/glia/widgets/view/Dialogs.kt index 3d3fdf0d4..8f80010a1 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/Dialogs.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/Dialogs.kt @@ -22,11 +22,13 @@ import androidx.core.view.isVisible import com.glia.widgets.GliaWidgets import com.glia.widgets.R import com.glia.widgets.UiTheme +import com.glia.widgets.callvisualizer.CallVisualizerSupportActivity import com.glia.widgets.core.dialog.model.DialogState.MediaUpgrade import com.glia.widgets.di.Dependencies import com.glia.widgets.helper.applyButtonTheme import com.glia.widgets.helper.applyImageColorTheme import com.glia.widgets.helper.applyTextTheme +import com.glia.widgets.helper.asActivity import com.glia.widgets.helper.isAlertDialogButtonUseVerticalAlignment import com.glia.widgets.view.button.BaseConfigurableButton import com.glia.widgets.view.button.GliaPositiveButton @@ -91,13 +93,23 @@ object Dialogs { horizontalInset: Int = 0, cancelable: Boolean = true ): AlertDialog { - return MaterialAlertDialogBuilder(context) + val dialog = MaterialAlertDialogBuilder(context) .setView(view) .setBackgroundInsetStart(horizontalInset) .setBackgroundInsetEnd(horizontalInset) .setCancelable(cancelable) + .setOnCancelListener { + Dependencies.getControllerFactory().dialogController.dismissVisitorCodeDialog() + context.asActivity()?.let { + if (it is CallVisualizerSupportActivity) { + it.overridePendingTransition(0, 0) + it.finish() + } + } + } .show() .also { setDialogBackground(it, theme.gliaChatBackgroundColor) } + return dialog } private fun showDialog( From 4b0833637857c7e06b121dbf8c53a7a1b85f778a Mon Sep 17 00:00:00 2001 From: Andrii Horishnii Date: Tue, 15 Aug 2023 18:08:09 +0300 Subject: [PATCH 49/69] Add Paparazzi for Screenshot Testing MOB-2524 --- build.gradle | 3 +- widgetssdk/build.gradle | 15 ++- .../src/main/res/values/themes_material.xml | 88 ++++++++++++++++++ .../GvaGalleryItemViewHolderSnapshotTest.kt | 39 ++++++++ .../src/test/java/android/text/TextUtils.java | 11 --- .../src/test/java/android/util/Log.java | 28 ------ .../com/glia/widgets/GliaWidgetsTest.java | 6 ++ ...yWatcherForCallVisualizerControllerTest.kt | 3 + .../data/GliaScreenSharingRepositoryTest.java | 3 + ...readMessagesCountWithTimeoutUseCaseTest.kt | 3 + ...yItemViewHolderSnapshotTest_testHolder.png | Bin 0 -> 12123 bytes 11 files changed, 158 insertions(+), 41 deletions(-) create mode 100644 widgetssdk/src/main/res/values/themes_material.xml create mode 100644 widgetssdk/src/snapshotTest/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt delete mode 100644 widgetssdk/src/test/java/android/text/TextUtils.java delete mode 100644 widgetssdk/src/test/java/android/util/Log.java create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_testHolder.png diff --git a/build.gradle b/build.gradle index eaf7febfe..d58751595 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ plugins { id("io.github.gradle-nexus.publish-plugin") version "1.1.0" id("org.jetbrains.kotlin.android") version "$kotlin_version" apply false id("org.jlleitschuh.gradle.ktlint") version "11.5.0" + id("app.cash.paparazzi") version '1.3.1' apply false } allprojects { @@ -74,7 +75,7 @@ ext { mockitoVersion = '5.1.1' mockitoKotlinVersion = '4.1.0' mockitoAndroidTestVersion = '4.3.1' - archCoreVersion = '2.2.0' + archCoreVersion = '2.1.0' testRulesVersion = '1.5.0' robolectricVersion = '4.10.3' diff --git a/widgetssdk/build.gradle b/widgetssdk/build.gradle index 0b3ac0c20..88d8c251b 100644 --- a/widgetssdk/build.gradle +++ b/widgetssdk/build.gradle @@ -1,11 +1,13 @@ apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'kotlin-parcelize' +apply plugin: 'app.cash.paparazzi' android { compileSdkVersion 31 buildToolsVersion "30.0.3" defaultPublishConfig "debug" + namespace 'com.glia.widgets' defaultConfig { minSdkVersion 24 targetSdkVersion 31 @@ -47,7 +49,18 @@ android { viewBinding true buildConfig true } - namespace 'com.glia.widgets' + + testOptions { + unitTests { + includeAndroidResources true + } + } + + sourceSets { + test { + java.srcDirs += ['src/snapshotTest/java'] + } + } } dependencies { diff --git a/widgetssdk/src/main/res/values/themes_material.xml b/widgetssdk/src/main/res/values/themes_material.xml new file mode 100644 index 000000000..2c3db72d3 --- /dev/null +++ b/widgetssdk/src/main/res/values/themes_material.xml @@ -0,0 +1,88 @@ + + + + + + diff --git a/widgetssdk/src/snapshotTest/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt b/widgetssdk/src/snapshotTest/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt new file mode 100644 index 000000000..64ff39dc3 --- /dev/null +++ b/widgetssdk/src/snapshotTest/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt @@ -0,0 +1,39 @@ +package com.glia.widgets.chat.adapter.holder + +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import com.android.ide.common.rendering.api.SessionParams +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.model.GvaButton +import com.glia.widgets.chat.model.GvaGalleryCard +import com.glia.widgets.databinding.ChatGvaGalleryItemBinding +import org.junit.Rule +import org.junit.Test + +class GvaGalleryItemViewHolderSnapshotTest { + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = DeviceConfig.PIXEL_4A, + renderingMode = SessionParams.RenderingMode.SHRINK, + showSystemUi = false, + theme = "ThemeOverlay_Glia_Chat_Material" + ) + + @Test + fun testHolder() { + val binding = ChatGvaGalleryItemBinding.inflate(paparazzi.layoutInflater) + val viewHolder = GvaGalleryItemViewHolder(binding, {}, UiTheme()) + + val gvaGalleryCard = GvaGalleryCard( + title = "Title", + subtitle = "Subtitle", + options = listOf( + GvaButton("Button 1"), + GvaButton("Button 2") + ) + ) + viewHolder.bind(gvaGalleryCard, 1, 4) + + paparazzi.snapshot(binding.root) + } +} diff --git a/widgetssdk/src/test/java/android/text/TextUtils.java b/widgetssdk/src/test/java/android/text/TextUtils.java deleted file mode 100644 index 4e473430d..000000000 --- a/widgetssdk/src/test/java/android/text/TextUtils.java +++ /dev/null @@ -1,11 +0,0 @@ -package android.text; - -/** - * Mock implementation of isEmpty() in android.text.TextUtils. - * It's needed for unit tests. - */ -public class TextUtils { - public static boolean isEmpty(CharSequence str) { - return str == null || str.length() == 0; - } -} diff --git a/widgetssdk/src/test/java/android/util/Log.java b/widgetssdk/src/test/java/android/util/Log.java deleted file mode 100644 index 46adcb6a9..000000000 --- a/widgetssdk/src/test/java/android/util/Log.java +++ /dev/null @@ -1,28 +0,0 @@ -package android.util; - -public class Log { - public static int d(String tag, String msg) { - System.out.println("DEBUG: " + tag + ": " + msg); - return 0; - } - - public static int i(String tag, String msg) { - System.out.println("INFO: " + tag + ": " + msg); - return 0; - } - - public static int w(String tag, String msg) { - System.out.println("WARNING: " + tag + ": " + msg); - return 0; - } - - public static int e(String tag, String msg) { - System.out.println("ERROR: " + tag + ": " + msg); - return 0; - } - - public static int e(String tag, String msg, Throwable exception) { - System.out.println("ERROR: " + tag + ": " + msg); - return 0; - } -} diff --git a/widgetssdk/src/test/java/com/glia/widgets/GliaWidgetsTest.java b/widgetssdk/src/test/java/com/glia/widgets/GliaWidgetsTest.java index 50d0b51c0..363967466 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/GliaWidgetsTest.java +++ b/widgetssdk/src/test/java/com/glia/widgets/GliaWidgetsTest.java @@ -6,6 +6,7 @@ import static org.mockito.Mockito.when; import android.app.Application; +import android.app.NotificationManager; import android.content.Context; import androidx.arch.core.executor.testing.InstantTaskExecutorRule; @@ -22,8 +23,11 @@ import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TestRule; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +@RunWith(RobolectricTestRunner.class) public class GliaWidgetsTest { @ClassRule public static final TestRule rule = new InstantTaskExecutorRule(); @@ -43,6 +47,8 @@ public void setUp() { @Test public void onAppCreate_setApplicationToGliaCore_whenCalled() { Application application = mock(Application.class); + NotificationManager notificationManager = mock(NotificationManager.class); + when(application.getSystemService(NotificationManager.class)).thenReturn(notificationManager); GliaWidgets.onAppCreate(application); diff --git a/widgetssdk/src/test/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerControllerTest.kt index 49d4a72f7..e5ab3f639 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerControllerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerControllerTest.kt @@ -26,6 +26,7 @@ import junit.framework.TestCase.assertTrue import org.junit.After import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.Mockito.mock import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -38,8 +39,10 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner import java.util.function.Consumer +@RunWith(RobolectricTestRunner::class) internal class ActivityWatcherForCallVisualizerControllerTest { private val callVisualizerRepository = mock(CallVisualizerRepository::class.java) diff --git a/widgetssdk/src/test/java/com/glia/widgets/core/screensharing/data/GliaScreenSharingRepositoryTest.java b/widgetssdk/src/test/java/com/glia/widgets/core/screensharing/data/GliaScreenSharingRepositoryTest.java index a9dc108a6..9d9cce8bc 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/core/screensharing/data/GliaScreenSharingRepositoryTest.java +++ b/widgetssdk/src/test/java/com/glia/widgets/core/screensharing/data/GliaScreenSharingRepositoryTest.java @@ -20,9 +20,12 @@ import com.glia.widgets.di.GliaCore; import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; import java.util.function.Consumer; +@RunWith(RobolectricTestRunner.class) public class GliaScreenSharingRepositoryTest { @Test diff --git a/widgetssdk/src/test/java/com/glia/widgets/core/secureconversations/domain/GetUnreadMessagesCountWithTimeoutUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/core/secureconversations/domain/GetUnreadMessagesCountWithTimeoutUseCaseTest.kt index 16f6da515..27e4513f5 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/core/secureconversations/domain/GetUnreadMessagesCountWithTimeoutUseCaseTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/core/secureconversations/domain/GetUnreadMessagesCountWithTimeoutUseCaseTest.kt @@ -6,14 +6,17 @@ import com.glia.widgets.core.secureconversations.SecureConversationsRepository import io.reactivex.plugins.RxJavaPlugins import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.internal.stubbing.answers.AnswersWithDelay import org.mockito.kotlin.any import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.mockito.stubbing.Answer +import org.robolectric.RobolectricTestRunner import kotlin.properties.Delegates +@RunWith(RobolectricTestRunner::class) class GetUnreadMessagesCountWithTimeoutUseCaseTest { private var repository: SecureConversationsRepository by Delegates.notNull() private var useCase: GetUnreadMessagesCountWithTimeoutUseCase by Delegates.notNull() diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_testHolder.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_testHolder.png new file mode 100644 index 0000000000000000000000000000000000000000..46bf5a590f7a02890d5b0ebba94bbe82fd2f2652 GIT binary patch literal 12123 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 U&#eY7&4X;tLC;qGcJ Date: Sat, 19 Aug 2023 11:25:48 +0300 Subject: [PATCH 50/69] Configure project for snapshot testing MOB-2553 --- build.gradle | 2 +- widgetssdk/build.gradle | 17 +++++++++++++---- ...ityWatcherForCallVisualizerControllerTest.kt | 1 + .../chat/controller/ChatControllerTest.kt | 3 +++ .../AppendHistoryChatMessageUseCaseTest.kt | 3 +++ .../domain/AppendNewChatMessageUseCaseTest.kt | 3 +++ .../ScreenSharingControllerTest.java | 3 +++ .../MarkMessagesReadWithDelayUseCaseTest.kt | 3 +++ .../filepreview/ui/FilePreviewControllerTest.kt | 3 +++ .../view/head/ActivityWatcherForChatHeadTest.kt | 3 +++ .../GvaGalleryItemViewHolderSnapshotTest.kt | 0 .../res/values/themes_material.xml | 0 12 files changed, 36 insertions(+), 5 deletions(-) rename widgetssdk/src/{snapshotTest => testSnapshot}/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt (100%) rename widgetssdk/src/{main => testSnapshot}/res/values/themes_material.xml (100%) diff --git a/build.gradle b/build.gradle index d58751595..5327511fd 100644 --- a/build.gradle +++ b/build.gradle @@ -77,7 +77,7 @@ ext { mockitoAndroidTestVersion = '4.3.1' archCoreVersion = '2.1.0' testRulesVersion = '1.5.0' - robolectricVersion = '4.10.3' + robolectricVersion = '4.9.2' //kotlin coreKtxVersion = '1.8.0' diff --git a/widgetssdk/build.gradle b/widgetssdk/build.gradle index 88d8c251b..e9645757d 100644 --- a/widgetssdk/build.gradle +++ b/widgetssdk/build.gradle @@ -26,6 +26,9 @@ android { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } + snapshot { + // Build type used for snapshot testing. + } } compileOptions { sourceCompatibility JavaVersion.VERSION_11 @@ -55,11 +58,17 @@ android { includeAndroidResources true } } - sourceSets { - test { - java.srcDirs += ['src/snapshotTest/java'] - } + // Exclude the default test directory ("src/test/java") for all build types. + // It is necessary to exclude unit tests from snapshot tests. + test.java.srcDirs = [] + + // Add the default test directory for debug and release build types. + testDebug.java.srcDirs += ['src/test/java'] + testRelease.java.srcDirs += ['src/test/java'] + + // Add resource directory for snapshot testing. + snapshot.res.srcDirs += ['src/testSnapshot/res'] } } diff --git a/widgetssdk/src/test/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerControllerTest.kt index e5ab3f639..cc60eb627 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerControllerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/callvisualizer/ActivityWatcherForCallVisualizerControllerTest.kt @@ -389,6 +389,7 @@ internal class ActivityWatcherForCallVisualizerControllerTest { controller.onMediaUpgradeReceived(mediaUpgradeOffer) resetMocks() controller.onInitialCameraPermissionResult(isGranted = true, isComponentActivity = true) + verify(mediaUpgradeOffer).accept(notNull()) } @Test diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt index 8ee034033..d15bc518d 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/controller/ChatControllerTest.kt @@ -54,12 +54,15 @@ import com.glia.widgets.view.MinimizeHandler import junit.framework.TestCase.assertEquals import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class ChatControllerTest { private lateinit var chatViewCallback: ChatViewCallback private lateinit var mediaUpgradeOfferRepository: MediaUpgradeOfferRepository diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryChatMessageUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryChatMessageUseCaseTest.kt index e0f4114d6..41efbba0a 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryChatMessageUseCaseTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryChatMessageUseCaseTest.kt @@ -10,6 +10,7 @@ import junit.framework.TestCase.assertTrue import org.junit.Assert.assertFalse import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock @@ -17,7 +18,9 @@ import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class AppendHistoryChatMessageUseCaseTest { private lateinit var appendHistoryVisitorChatItemUseCase: AppendHistoryVisitorChatItemUseCase private lateinit var appendHistoryOperatorChatItemUseCase: AppendHistoryOperatorChatItemUseCase diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewChatMessageUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewChatMessageUseCaseTest.kt index a8b98846f..5a979eaa5 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewChatMessageUseCaseTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewChatMessageUseCaseTest.kt @@ -8,13 +8,16 @@ import com.glia.widgets.chat.ChatManager import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class AppendNewChatMessageUseCaseTest { private lateinit var appendNewOperatorMessageUseCase: AppendNewOperatorMessageUseCase private lateinit var appendNewVisitorMessageUseCase: AppendNewVisitorMessageUseCase diff --git a/widgetssdk/src/test/java/com/glia/widgets/core/screensharing/ScreenSharingControllerTest.java b/widgetssdk/src/test/java/com/glia/widgets/core/screensharing/ScreenSharingControllerTest.java index f5ff36c37..9543a007e 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/core/screensharing/ScreenSharingControllerTest.java +++ b/widgetssdk/src/test/java/com/glia/widgets/core/screensharing/ScreenSharingControllerTest.java @@ -18,7 +18,10 @@ import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +@RunWith(RobolectricTestRunner.class) public class ScreenSharingControllerTest { private GliaScreenSharingRepository gliaScreenSharingRepository; diff --git a/widgetssdk/src/test/java/com/glia/widgets/core/secureconversations/domain/MarkMessagesReadWithDelayUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/core/secureconversations/domain/MarkMessagesReadWithDelayUseCaseTest.kt index ecc43b141..d38e5e88b 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/core/secureconversations/domain/MarkMessagesReadWithDelayUseCaseTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/core/secureconversations/domain/MarkMessagesReadWithDelayUseCaseTest.kt @@ -7,15 +7,18 @@ import io.reactivex.schedulers.TestScheduler import org.junit.After import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner import java.util.concurrent.TimeUnit import kotlin.properties.Delegates +@RunWith(RobolectricTestRunner::class) class MarkMessagesReadWithDelayUseCaseTest { private var repository: SecureConversationsRepository by Delegates.notNull() private var useCase: MarkMessagesReadWithDelayUseCase by Delegates.notNull() diff --git a/widgetssdk/src/test/java/com/glia/widgets/filepreview/ui/FilePreviewControllerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/filepreview/ui/FilePreviewControllerTest.kt index 1fa6eb085..c16fdd342 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/filepreview/ui/FilePreviewControllerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/filepreview/ui/FilePreviewControllerTest.kt @@ -14,6 +14,7 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor @@ -22,7 +23,9 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class FilePreviewControllerTest { private lateinit var view: FilePreviewContract.View private lateinit var getImageFileFromDownloadsUseCase: GetImageFileFromDownloadsUseCase diff --git a/widgetssdk/src/test/java/com/glia/widgets/view/head/ActivityWatcherForChatHeadTest.kt b/widgetssdk/src/test/java/com/glia/widgets/view/head/ActivityWatcherForChatHeadTest.kt index 674103994..bc46247ed 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/view/head/ActivityWatcherForChatHeadTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/view/head/ActivityWatcherForChatHeadTest.kt @@ -14,6 +14,7 @@ import com.glia.widgets.view.head.controller.ServiceChatHeadController import org.junit.After import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -22,7 +23,9 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) internal class ActivityWatcherForChatHeadTest { private val watcher = Mockito.mock(ActivityWatcherForChatHeadContract.Watcher::class.java) diff --git a/widgetssdk/src/snapshotTest/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt similarity index 100% rename from widgetssdk/src/snapshotTest/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt rename to widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt diff --git a/widgetssdk/src/main/res/values/themes_material.xml b/widgetssdk/src/testSnapshot/res/values/themes_material.xml similarity index 100% rename from widgetssdk/src/main/res/values/themes_material.xml rename to widgetssdk/src/testSnapshot/res/values/themes_material.xml From ffdef064c94158bc10730c7c991bd16b3700a374 Mon Sep 17 00:00:00 2001 From: Andrii Horishnii Date: Tue, 22 Aug 2023 12:26:57 +0300 Subject: [PATCH 51/69] Config Git LFS MOB-2554 --- .gitattributes | 1 + ...yItemViewHolderSnapshotTest_testHolder.png | Bin 12123 -> 130 bytes 2 files changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..afe75b717 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +widgetssdk/src/test/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_testHolder.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_testHolder.png index 46bf5a590f7a02890d5b0ebba94bbe82fd2f2652..806b37bec07015f2612f4d68fccb843be340f578 100644 GIT binary patch literal 130 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 U&#eY7&4X;tLC;qGcJ Date: Tue, 22 Aug 2023 21:58:49 +0300 Subject: [PATCH 52/69] Configure CI for snapshot testing MOB-2555 --- bitrise.yml | 55 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/bitrise.yml b/bitrise.yml index 9194015a8..c5a2d1813 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -225,15 +225,15 @@ workflows: - script@1: title: Generate changelog inputs: - - content: |- - #!/usr/bin/env bash - # fail if any commands fails - set -e - # debug log - set -x - - # run the local bash script - bash ./scripts/generate-changelog.sh + - content: |- + #!/usr/bin/env bash + # fail if any commands fails + set -e + # debug log + set -x + + # run the local bash script + bash ./scripts/generate-changelog.sh - github-release@0: inputs: - username: $GITHUB_USERNAME @@ -274,7 +274,35 @@ workflows: inputs: - project_location: $PROJECT_LOCATION - module: $WIDGET_SDK_MODULE - - variant: $SDK_VARIANT + - variant: debug + - gradle-unit-test@1: + title: Snapshot Test for SDK + inputs: + - unit_test_flags: "" + - unit_test_task: $WIDGET_SDK_MODULE:verifyPaparazziSnapshot + is_always_run: true + - script@1: + title: Export reports for Snapshot Test for SDK + inputs: + - content: |- + #!/usr/bin/env bash + # fail if any commands fails + set -e + # debug log + set -x + + REPORTS_DIR=$BITRISE_SOURCE_DIR/widgetssdk/build/reports/tests/testSnapshotUnitTest + if [ -d "$REPORTS_DIR" ]; then + cd $REPORTS_DIR + zip -r $BITRISE_DEPLOY_DIR/widgetssdk-snapshotTests.zip * + fi + + SNAPSHOT_DIR=$BITRISE_SOURCE_DIR/widgetssdk/build/paparazzi + if [ -d "$SNAPSHOT_DIR" ]; then + cd $SNAPSHOT_DIR + zip -r $BITRISE_DEPLOY_DIR/widgetssdk-snapshots.zip * + fi + is_always_run: true - android-lint@0: title: Run Lint for APP inputs: @@ -297,6 +325,7 @@ workflows: inputs: - test_type: instrumentation - use_verbose_log: true + - deploy-to-bitrise-io@2: {} - cache-push@2: {} upgrade_dependencies: steps: @@ -357,6 +386,12 @@ app: - opts: is_expand: false PROJECT_LOCATION: . + - opts: + is_expand: false + GRADLE_BUILD_FILE_PATH: build.gradle + - opts: + is_expand: false + GRADLEW_PATH: "./gradlew" - opts: is_expand: false EXAMPLE_APP_MODULE: app From f7e00444dd28d14ea4721bc6c00cb55fc2713fc5 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Mon, 28 Aug 2023 11:21:39 +0300 Subject: [PATCH 53/69] Fix The chat history is empty after restoring the chat. - During chat flow optimization, I forgot to add the chat history reload step when a new engagement is established. --- .../java/com/glia/widgets/chat/ChatManager.kt | 34 +++++++++++++------ .../widgets/chat/controller/ChatController.kt | 1 + .../com/glia/widgets/di/ManagerFactory.kt | 3 +- .../com/glia/widgets/chat/ChatManagerTest.kt | 22 +++++++++++- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt index b104dbc5a..d559dd3b0 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt @@ -10,6 +10,7 @@ import com.glia.widgets.chat.domain.AppendNewChatMessageUseCase import com.glia.widgets.chat.domain.GliaLoadHistoryUseCase import com.glia.widgets.chat.domain.GliaOnMessageUseCase import com.glia.widgets.chat.domain.HandleCustomCardClickUseCase +import com.glia.widgets.chat.domain.IsAuthenticatedUseCase import com.glia.widgets.chat.domain.SendUnsentMessagesUseCase import com.glia.widgets.chat.model.ChatItem import com.glia.widgets.chat.model.CustomCardChatItem @@ -25,6 +26,8 @@ import com.glia.widgets.core.engagement.domain.IsOngoingEngagementUseCase 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 +import com.glia.widgets.helper.Logger +import com.glia.widgets.helper.TAG import io.reactivex.BackpressureStrategy import io.reactivex.Flowable import io.reactivex.android.schedulers.AndroidSchedulers @@ -44,6 +47,7 @@ internal class ChatManager constructor( private val sendUnsentMessagesUseCase: SendUnsentMessagesUseCase, private val handleCustomCardClickUseCase: HandleCustomCardClickUseCase, private val isOngoingEngagementUseCase: IsOngoingEngagementUseCase, + private val isAuthenticatedUseCase: IsAuthenticatedUseCase, private val compositeDisposable: CompositeDisposable = CompositeDisposable(), private val state: BehaviorProcessor = BehaviorProcessor.create(), private val quickReplies: BehaviorProcessor> = BehaviorProcessor.create(), @@ -128,11 +132,12 @@ internal class ChatManager constructor( } @VisibleForTesting - fun mapChatHistory(historyResponse: ChatHistoryResponse): State { - if (historyResponse.items.isEmpty()) return State() + fun mapChatHistory(historyResponse: ChatHistoryResponse, currentState: State? = null): State { + val state: State = currentState ?: State() + + if (historyResponse.items.isEmpty()) return state val chatItems: MutableList = mutableListOf() - val chatItemIds: MutableSet = mutableSetOf() val rawItems = historyResponse.items @@ -140,9 +145,10 @@ internal class ChatManager constructor( for (index in rawItems.indices.reversed()) { val rawMessage = rawItems[index] - chatItemIds.add(rawMessage.chatMessage.id) - appendHistoryChatMessageUseCase(chatItems, rawMessage, index == rawItems.lastIndex) + if (state.isNew(rawMessage)) { + appendHistoryChatMessageUseCase(chatItems, rawMessage, index == rawItems.lastIndex) + } } chatItems.reverse() @@ -151,13 +157,10 @@ internal class ChatManager constructor( markMessagesReadWithDelay() } - val lastMessageWithVisibleOperatorImage = chatItems.lastOrNull() as? OperatorChatItem + state.lastMessageWithVisibleOperatorImage = chatItems.lastOrNull() as? OperatorChatItem + state.chatItems.addAll(chatItems) - return State( - chatItems = chatItems, - chatItemIds = chatItemIds, - lastMessageWithVisibleOperatorImage = lastMessageWithVisibleOperatorImage - ) + return state } @VisibleForTesting @@ -314,6 +317,15 @@ internal class ChatManager constructor( chatItems.remove(NewMessagesDividerItem) } + fun reloadHistoryIfNeeded() { + if (isAuthenticatedUseCase()) return + + compositeDisposable.add( + loadHistoryUseCase().map { mapChatHistory(it, state.value) } + .subscribe({ state.onNext(it) }) { Logger.e(TAG, "Chat reload failed", it) } + ) + } + internal data class State( val chatItems: MutableList = mutableListOf(), val chatItemIds: MutableSet = mutableSetOf(), diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index 2543f64d8..fd7fc7df2 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -795,6 +795,7 @@ internal class ChatController( addOperatorMediaStateListenerUseCase.execute(operatorMediaStateListener) mediaUpgradeOfferRepository.startListening() emitViewState { chatState.engagementStarted() } + chatManager.reloadHistoryIfNeeded() } private fun initChatManager() { diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt b/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt index 575ce05b8..4a9ebc6ee 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt @@ -14,7 +14,8 @@ internal class ManagerFactory(private val useCaseFactory: UseCaseFactory) { appendNewChatMessageUseCase = createAppendNewChatMessageUseCase(), sendUnsentMessagesUseCase = createSendUnsentMessagesUseCase(), handleCustomCardClickUseCase = createHandleCustomCardClickUseCase(), - isOngoingEngagementUseCase = createIsOngoingEngagementUseCase() + isOngoingEngagementUseCase = createIsOngoingEngagementUseCase(), + isAuthenticatedUseCase = createIsAuthenticatedUseCase() ) } } diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt index 8f2232512..5a5254b6f 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt @@ -10,6 +10,7 @@ import com.glia.widgets.chat.domain.AppendNewChatMessageUseCase import com.glia.widgets.chat.domain.GliaLoadHistoryUseCase import com.glia.widgets.chat.domain.GliaOnMessageUseCase import com.glia.widgets.chat.domain.HandleCustomCardClickUseCase +import com.glia.widgets.chat.domain.IsAuthenticatedUseCase import com.glia.widgets.chat.domain.SendUnsentMessagesUseCase import com.glia.widgets.chat.model.ChatItem import com.glia.widgets.chat.model.GvaButton @@ -42,6 +43,7 @@ import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.spy @@ -63,6 +65,7 @@ class ChatManagerTest { private lateinit var sendUnsentMessagesUseCase: SendUnsentMessagesUseCase private lateinit var handleCustomCardClickUseCase: HandleCustomCardClickUseCase private lateinit var isOngoingEngagementUseCase: IsOngoingEngagementUseCase + private lateinit var isAuthenticatedUseCase: IsAuthenticatedUseCase private lateinit var subjectUnderTest: ChatManager private lateinit var state: ChatManager.State private lateinit var compositeDisposable: CompositeDisposable @@ -82,6 +85,7 @@ class ChatManagerTest { sendUnsentMessagesUseCase = mock() handleCustomCardClickUseCase = mock() isOngoingEngagementUseCase = mock() + isAuthenticatedUseCase = mock() compositeDisposable = spy() stateProcessor = spy(BehaviorProcessor.create()) quickReplies = BehaviorProcessor.create() @@ -98,6 +102,7 @@ class ChatManagerTest { sendUnsentMessagesUseCase, handleCustomCardClickUseCase, isOngoingEngagementUseCase, + isAuthenticatedUseCase, compositeDisposable, stateProcessor, quickReplies, @@ -556,7 +561,7 @@ class ChatManagerTest { fun `loadHistory triggers mapChatHistory when history response receives`() { whenever(loadHistoryUseCase()) doReturn Single.just(mock()) val testFlowable = subjectUnderTest.loadHistory { }.test() - verify(subjectUnderTest).mapChatHistory(any()) + verify(subjectUnderTest).mapChatHistory(any(), isNull()) assertTrue(testFlowable.valueCount() == 1) } @@ -630,6 +635,21 @@ class ChatManagerTest { } } + @Test + fun `reloadHistoryIfNeeded does nothing when authenticated`() { + whenever(isAuthenticatedUseCase()) doReturn true + subjectUnderTest.reloadHistoryIfNeeded() + verify(loadHistoryUseCase, never()).invoke() + } + + @Test + fun `reloadHistoryIfNeeded reloads history when not authenticated`() { + whenever(isAuthenticatedUseCase()) doReturn false + whenever(loadHistoryUseCase()) doReturn Single.just(mock()) + subjectUnderTest.reloadHistoryIfNeeded() + verify(loadHistoryUseCase).invoke() + } + private inline fun mockChatMessage(): ChatMessageInternal { val chatMessageInternal: ChatMessageInternal = mock() val chatMessage = mock() From 31aa0d8c23e95021ad519d6354c3fbaafa8c9470 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Fri, 25 Aug 2023 10:50:46 +0300 Subject: [PATCH 54/69] Remove engagement end Listener from MessagesNotSeenHandler on GliaWidgets.endEngagement --- .../java/com/glia/widgets/GliaWidgets.java | 3 ++- .../glia/widgets/di/ControllerFactory.java | 1 + .../widgets/view/MessagesNotSeenHandler.java | 24 ++++++++++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java b/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java index 0f2401155..b2ba91581 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java +++ b/widgetssdk/src/main/java/com/glia/widgets/GliaWidgets.java @@ -205,12 +205,13 @@ public static void clearVisitorSession() { */ public static void endEngagement() { Logger.d(TAG, "endEngagement"); + Dependencies.getControllerFactory().destroyControllers(); + Dependencies.glia().getCurrentEngagement().ifPresent(engagement -> engagement.end(e -> { if (e != null) { Logger.e(TAG, "Ending engagement error: " + e); } })); - Dependencies.getControllerFactory().destroyControllers(); } /** diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java index 16d3690aa..fc483182a 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java @@ -210,6 +210,7 @@ public void destroyControllers() { destroyChatController(); serviceChatHeadController.onDestroy(); applicationChatHeadController.onDestroy(); + messagesNotSeenHandler.onDestroy(); } public void destroyCallController() { diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/MessagesNotSeenHandler.java b/widgetssdk/src/main/java/com/glia/widgets/view/MessagesNotSeenHandler.java index 5f8957e2d..4076539da 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/MessagesNotSeenHandler.java +++ b/widgetssdk/src/main/java/com/glia/widgets/view/MessagesNotSeenHandler.java @@ -10,12 +10,12 @@ public class MessagesNotSeenHandler implements GliaOnEngagementEndUseCase.Listener { + private final static String TAG = "MessagesNotSeenHandler"; private final GliaOnMessageUseCase gliaOnMessageUseCase; private final GliaOnEngagementEndUseCase gliaOnEngagementEndUseCase; - private final static String TAG = "MessagesNotSeenHandler"; + private final List listeners = new ArrayList<>(); private int count = 0; private boolean isCounting = false; - private final List listeners = new ArrayList<>(); public MessagesNotSeenHandler( GliaOnMessageUseCase gliaOnMessageUseCase, @@ -28,7 +28,6 @@ public MessagesNotSeenHandler( public void init() { Logger.d(TAG, "init"); gliaOnMessageUseCase.invoke().doOnNext(this::onMessage).subscribe(); - gliaOnEngagementEndUseCase.execute(this); } public void chatOnBackClicked() { @@ -59,13 +58,27 @@ public void chatUpgradeOfferAccepted() { public void addListener(MessagesNotSeenHandlerListener listener) { Logger.d(TAG, "addListener"); + registerEngagementEndUseCaseListener(); this.listeners.add(listener); listener.onNewCount(count); } + private void registerEngagementEndUseCaseListener() { + if (listeners.isEmpty()) { + gliaOnEngagementEndUseCase.execute(this); + } + } + public void removeListener(MessagesNotSeenHandlerListener listener) { Logger.d(TAG, "removeListener"); listeners.remove(listener); + unregisterEngagementEndUseCaseListener(); + } + + private void unregisterEngagementEndUseCaseListener() { + if (listeners.isEmpty()) { + gliaOnEngagementEndUseCase.unregisterListener(this); + } } private void emitCount(int newCount) { @@ -87,6 +100,11 @@ public void onMessage(ChatMessageInternal messageInternal) { } } + public void onDestroy() { + gliaOnEngagementEndUseCase.unregisterListener(this); + engagementEnded(); + } + public interface MessagesNotSeenHandlerListener { void onNewCount(int count); } From ef7829b259208e01f49a470fe6dd9005d6c4dfea Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Thu, 24 Aug 2023 12:17:34 +0300 Subject: [PATCH 55/69] Enable Operator Image for Joined Status --- .../holder/OperatorStatusViewHolder.kt | 32 ++++--------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorStatusViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorStatusViewHolder.kt index a51d5ca8c..0414f0bef 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorStatusViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorStatusViewHolder.kt @@ -78,8 +78,7 @@ internal class OperatorStatusViewHolder( is OperatorStatusItem.Joined -> applyConnectedState(item.operatorName, item.profileImgUrl) is OperatorStatusItem.Transferring -> applyTransferringState() } - - statusPictureView.isVisible = isShowStatusPictureView(item) + statusPictureView.isVisible = true statusPictureView.setShowRippleAnimation(isShowStatusViewRippleAnimation(item)) } @@ -87,18 +86,16 @@ internal class OperatorStatusViewHolder( statusPictureView.showPlaceholder() applyChatStartingViewsVisibility() applyChatStartedViewsVisibility(false) - itemView.contentDescription = - itemView.resources.getString( - R.string.glia_chat_in_queue_message_content_description, - companyName ?: "" - ) + itemView.contentDescription = itemView.resources.getString( + R.string.glia_chat_in_queue_message_content_description, + companyName ?: "" + ) engagementStatesTheme?.queue.also(::applyEngagementState) } private fun applyConnectedState(operatorName: String, profileImgUrl: String?) { - profileImgUrl?.let { statusPictureView.showProfileImage(it) } - ?: statusPictureView.showPlaceholder() + profileImgUrl?.let { statusPictureView.showProfileImage(it) } ?: statusPictureView.showPlaceholder() applyChatStartingViewsVisibility(false) applyChatStartedViewsVisibility() @@ -114,21 +111,6 @@ internal class OperatorStatusViewHolder( engagementStatesTheme?.connected.also(::applyEngagementState) } - private fun applyJoinedState(operatorName: String) { - chatStartedNameView.text = operatorName - chatStartedCaptionView.text = - itemView.resources.getString(R.string.glia_chat_operator_has_joined, operatorName) - itemView.contentDescription = itemView.resources.getString( - R.string.glia_chat_operator_has_joined_content_description, - operatorName - ) - - applyChatStartingViewsVisibility(false) - applyChatStartedViewsVisibility() - - engagementStatesTheme?.connecting.also(::applyEngagementState) - } - private fun applyTransferringState() { statusPictureView.showPlaceholder() applyChatStartingViewsVisibility() @@ -162,8 +144,6 @@ internal class OperatorStatusViewHolder( } } - private fun isShowStatusPictureView(item: OperatorStatusItem): Boolean = item !is OperatorStatusItem.Joined - private fun isShowStatusViewRippleAnimation(item: OperatorStatusItem): Boolean = when (item) { is OperatorStatusItem.InQueue, is OperatorStatusItem.Transferring -> true else -> false From 4354f611c88e609766d990547f1bfeb904cb65dc Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Tue, 29 Aug 2023 13:21:37 +0300 Subject: [PATCH 56/69] Send Gva Response from Secure Conversations history --- .../glia/widgets/chat/domain/GliaSendMessageUseCase.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt index 0262466a1..65b9101aa 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt @@ -86,8 +86,14 @@ class GliaSendMessageUseCase( } } - fun execute(singleChoiceAttachment: SingleChoiceAttachment?, listener: Listener?) { - chatRepository.sendMessageSingleChoice(singleChoiceAttachment, listener) + fun execute(singleChoiceAttachment: SingleChoiceAttachment, listener: Listener) { + if (isSecureEngagement) { + singleChoiceAttachment.apply { + secureConversationsRepository.send(selectedOptionText, engagementConfigRepository.queueIds, singleChoiceAttachment, listener) + } + } else { + chatRepository.sendMessageSingleChoice(singleChoiceAttachment, listener) + } } private val isOperatorOnline: Boolean From dafd6edd1788829220f90d431011fb629937d38a Mon Sep 17 00:00:00 2001 From: Andriy Shevtsov Date: Mon, 28 Aug 2023 19:25:23 +0300 Subject: [PATCH 57/69] Do not unsubscribe from engagement events for Call Visualizer Some listeners subscribe only once, so we shouldn't unsubscribe MOB-2556 --- .../main/java/com/glia/widgets/call/CallController.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java b/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java index f58292193..a23668977 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java +++ b/widgetssdk/src/main/java/com/glia/widgets/call/CallController.java @@ -331,7 +331,7 @@ public void onDestroy(boolean retain) { viewCallback = null; if (!retain) { - disposable.dispose(); + disposable.clear(); mediaUpgradeOfferRepository.stopAll(); mediaUpgradeOfferRepositoryCallback = null; if (callTimerStatusListener != null) { @@ -826,10 +826,7 @@ private void subscribeToOmnibrowseEvents() { break; } }); - omnibrowseEngagement.on(Engagement.Events.END, engagementState -> { - Glia.omnibrowse.off(Omnibrowse.Events.ENGAGEMENT); - viewCallback.destroyView(); - }); + omnibrowseEngagement.on(Engagement.Events.END, engagementState -> viewCallback.destroyView()); }); } From 3fdd626533076cbae6520e959d67d53b7c0f6dae Mon Sep 17 00:00:00 2001 From: Andrii Horishnii Date: Fri, 25 Aug 2023 19:58:18 +0300 Subject: [PATCH 58/69] Cover Gallery with snapshot tests MOB-2582 --- .../chat/adapter/ChatItemHeightManager.kt | 7 +- .../widgets/chat/adapter/GvaGalleryAdapter.kt | 7 +- .../holder/GvaGalleryItemViewHolder.kt | 9 +- .../adapter/holder/GvaGalleryViewHolder.kt | 9 +- .../adapter/holder/OperatorBaseViewHolder.kt | 6 +- .../parse/RemoteConfigurationParser.kt | 7 +- ...eryItemViewHolderSnapshotTest_allViews.png | 3 + ...rSnapshotTest_allViewsWithGlobalColors.png | 3 + ...HolderSnapshotTest_allViewsWithUiTheme.png | 3 + ...rSnapshotTest_allViewsWithUnifiedTheme.png | 3 + ...est_allViewsWithUnifiedThemeWithoutGva.png | 3 + ...iewHolderSnapshotTest_longButtonsTitle.png | 3 + ...tTest_longButtonsTitleWithGlobalColors.png | 3 + ...apshotTest_longButtonsTitleWithUiTheme.png | 3 + ...tTest_longButtonsTitleWithUnifiedTheme.png | 3 + ...ButtonsTitleWithUnifiedThemeWithoutGva.png | 3 + ...temViewHolderSnapshotTest_longSubtitle.png | 3 + ...pshotTest_longSubtitleWithGlobalColors.png | 3 + ...erSnapshotTest_longSubtitleWithUiTheme.png | 3 + ...pshotTest_longSubtitleWithUnifiedTheme.png | 3 + ...longSubtitleWithUnifiedThemeWithoutGva.png | 3 + ...ryItemViewHolderSnapshotTest_longTitle.png | 3 + ...SnapshotTest_longTitleWithGlobalColors.png | 3 + ...olderSnapshotTest_longTitleWithUiTheme.png | 3 + ...SnapshotTest_longTitleWithUnifiedTheme.png | 3 + ...st_longTitleWithUnifiedThemeWithoutGva.png | 3 + ...emViewHolderSnapshotTest_onlyMandatory.png | 3 + ...shotTest_onlyMandatoryWithGlobalColors.png | 3 + ...rSnapshotTest_onlyMandatoryWithUiTheme.png | 3 + ...shotTest_onlyMandatoryWithUnifiedTheme.png | 3 + ...nlyMandatoryWithUnifiedThemeWithoutGva.png | 3 + ...yItemViewHolderSnapshotTest_testHolder.png | 3 - ...ewHolderSnapshotTest_itemsWithChatHead.png | 3 + ...Test_itemsWithChatHeadWithGlobalColors.png | 3 + ...pshotTest_itemsWithChatHeadWithUiTheme.png | 3 + ...Test_itemsWithChatHeadWithUnifiedTheme.png | 3 + ...WithChatHeadWithUnifiedThemeWithoutGva.png | 3 + ...olderSnapshotTest_itemsWithoutChatHead.png | 3 + ...t_itemsWithoutChatHeadWithGlobalColors.png | 3 + ...otTest_itemsWithoutChatHeadWithUiTheme.png | 3 + ...t_itemsWithoutChatHeadWithUnifiedTheme.png | 3 + ...houtChatHeadWithUnifiedThemeWithoutGva.png | 3 + .../java/com/glia/widgets/SnapshotTest.kt | 53 +++ .../GvaGalleryItemViewHolderSnapshotTest.kt | 354 ++++++++++++++++-- .../GvaGalleryViewHolderSnapshotTest.kt | 177 +++++++++ .../widgets/snapshotutils/SnapshotContent.kt | 14 + .../glia/widgets/snapshotutils/SnapshotGva.kt | 24 ++ .../widgets/snapshotutils/SnapshotStrings.kt | 12 + .../widgets/snapshotutils/SnapshotTheme.kt | 147 ++++++++ .../testSnapshot/res/drawable/test_banner.png | Bin 0 -> 12005 bytes .../res/drawable/test_ic_app_bar_back.xml | 5 + .../res/drawable/test_ic_call_audio_off.xml | 5 + .../res/drawable/test_ic_call_audio_on.xml | 5 + .../res/drawable/test_ic_call_chat.xml | 5 + .../res/drawable/test_ic_call_minimize.xml | 5 + .../res/drawable/test_ic_call_speaker_off.xml | 5 + .../res/drawable/test_ic_call_speaker_on.xml | 5 + .../res/drawable/test_ic_call_video_off.xml | 5 + .../res/drawable/test_ic_call_video_on.xml | 5 + .../drawable/test_ic_chat_audio_upgrade.xml | 5 + .../drawable/test_ic_chat_video_upgrade.xml | 5 + .../res/drawable/test_ic_end_screen_share.xml | 6 + .../res/drawable/test_ic_leave_queue.xml | 5 + .../res/drawable/test_ic_on_hold.xml | 5 + .../res/drawable/test_ic_placeholder.xml | 11 + .../test_ic_screen_sharing_dialog.xml | 5 + .../res/drawable/test_ic_send_message.xml | 5 + .../drawable/test_ic_upgrade_audio_dialog.xml | 5 + .../drawable/test_ic_upgrade_video_dialog.xml | 5 + .../src/testSnapshot/res/font/expletus.xml | 24 ++ .../testSnapshot/res/font/expletus_sans.ttf | Bin 0 -> 56856 bytes .../res/font/expletus_sans_bold.ttf | Bin 0 -> 62616 bytes .../res/raw/global_colors_unified_config.json | 12 + .../res/raw/test_unified_config.json | 149 ++++++++ .../testSnapshot/res/values/test_colors.xml | 37 ++ 75 files changed, 1217 insertions(+), 41 deletions(-) create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViews.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithGlobalColors.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithUiTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithUnifiedTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithUnifiedThemeWithoutGva.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitle.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithGlobalColors.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithUiTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithUnifiedTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithUnifiedThemeWithoutGva.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitle.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithGlobalColors.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithUiTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithUnifiedTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithUnifiedThemeWithoutGva.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitle.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithGlobalColors.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithUiTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithUnifiedTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithUnifiedThemeWithoutGva.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatory.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithGlobalColors.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithUiTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithUnifiedTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithUnifiedThemeWithoutGva.png delete mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_testHolder.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHead.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithGlobalColors.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithUiTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithUnifiedTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithUnifiedThemeWithoutGva.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHead.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithGlobalColors.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithUiTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithUnifiedTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithUnifiedThemeWithoutGva.png create mode 100644 widgetssdk/src/testSnapshot/java/com/glia/widgets/SnapshotTest.kt create mode 100644 widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolderSnapshotTest.kt create mode 100644 widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotContent.kt create mode 100644 widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotGva.kt create mode 100644 widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotStrings.kt create mode 100644 widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotTheme.kt create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_banner.png create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_app_bar_back.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_call_audio_off.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_call_audio_on.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_call_chat.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_call_minimize.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_call_speaker_off.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_call_speaker_on.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_call_video_off.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_call_video_on.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_chat_audio_upgrade.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_chat_video_upgrade.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_end_screen_share.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_leave_queue.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_on_hold.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_placeholder.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_screen_sharing_dialog.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_send_message.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_upgrade_audio_dialog.xml create mode 100644 widgetssdk/src/testSnapshot/res/drawable/test_ic_upgrade_video_dialog.xml create mode 100644 widgetssdk/src/testSnapshot/res/font/expletus.xml create mode 100644 widgetssdk/src/testSnapshot/res/font/expletus_sans.ttf create mode 100644 widgetssdk/src/testSnapshot/res/font/expletus_sans_bold.ttf create mode 100644 widgetssdk/src/testSnapshot/res/raw/global_colors_unified_config.json create mode 100644 widgetssdk/src/testSnapshot/res/raw/test_unified_config.json create mode 100644 widgetssdk/src/testSnapshot/res/values/test_colors.xml diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt index 5f247b377..3ca797b76 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatItemHeightManager.kt @@ -11,16 +11,19 @@ import com.glia.widgets.chat.model.ChatItem import com.glia.widgets.chat.model.GvaGalleryCard import com.glia.widgets.chat.model.GvaGalleryCards import com.glia.widgets.databinding.ChatGvaGalleryItemBinding +import com.glia.widgets.di.Dependencies +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme internal class ChatItemHeightManager( private val uiTheme: UiTheme, private val layoutInflater: LayoutInflater, - private val resources: Resources + private val resources: Resources, + private val unifiedTheme: UnifiedTheme? = Dependencies.getGliaThemeManager().theme ) { private val measuredHeightsMap = ArrayMap() private val gvaGalleryItemViewHolder: GvaGalleryItemViewHolder by lazy { - GvaGalleryItemViewHolder(ChatGvaGalleryItemBinding.inflate(layoutInflater), {}, uiTheme) + GvaGalleryItemViewHolder(ChatGvaGalleryItemBinding.inflate(layoutInflater), {}, uiTheme, unifiedTheme) } private val gvaGalleryCardWidth: Int by lazy { diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt index 75c8764a1..25d273557 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/GvaGalleryAdapter.kt @@ -7,10 +7,12 @@ import com.glia.widgets.chat.adapter.holder.GvaGalleryItemViewHolder import com.glia.widgets.chat.model.GvaGalleryCard import com.glia.widgets.databinding.ChatGvaGalleryItemBinding import com.glia.widgets.helper.layoutInflater +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme internal class GvaGalleryAdapter( private val buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, - private val uiTheme: UiTheme + private val uiTheme: UiTheme, + private val unifiedTheme: UnifiedTheme? ) : RecyclerView.Adapter() { private var galleryCards: List? = null @@ -24,7 +26,8 @@ internal class GvaGalleryAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = GvaGalleryItemViewHolder( ChatGvaGalleryItemBinding.inflate(parent.layoutInflater, parent, false), buttonsClickListener, - uiTheme + uiTheme, + unifiedTheme ) override fun onBindViewHolder(holder: GvaGalleryItemViewHolder, position: Int) { diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt index ed1ac85c9..5c3567833 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolder.kt @@ -14,7 +14,6 @@ import com.glia.widgets.chat.adapter.ChatAdapter import com.glia.widgets.chat.adapter.GvaButtonsAdapter import com.glia.widgets.chat.model.GvaGalleryCard import com.glia.widgets.databinding.ChatGvaGalleryItemBinding -import com.glia.widgets.di.Dependencies import com.glia.widgets.helper.fromHtml import com.glia.widgets.helper.getColorCompat import com.glia.widgets.helper.getColorStateListCompat @@ -22,6 +21,7 @@ import com.glia.widgets.helper.getFontCompat import com.glia.widgets.helper.load import com.glia.widgets.view.unifiedui.applyLayerTheme import com.glia.widgets.view.unifiedui.applyTextTheme +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme import com.glia.widgets.view.unifiedui.theme.chat.MessageBalloonTheme import com.glia.widgets.view.unifiedui.theme.gva.GvaGalleryCardTheme import kotlin.properties.Delegates @@ -29,17 +29,18 @@ import kotlin.properties.Delegates internal class GvaGalleryItemViewHolder( private val binding: ChatGvaGalleryItemBinding, buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, - private val uiTheme: UiTheme + private val uiTheme: UiTheme, + private val unifiedTheme: UnifiedTheme? ) : ViewHolder(binding.root) { private var adapter: GvaButtonsAdapter by Delegates.notNull() private val operatorTheme: MessageBalloonTheme? by lazy { - Dependencies.getGliaThemeManager().theme?.chatTheme?.operatorMessage + unifiedTheme?.chatTheme?.operatorMessage } private val galleryCardTheme: GvaGalleryCardTheme? by lazy { - Dependencies.getGliaThemeManager().theme?.chatTheme?.gva?.galleryCardTheme + unifiedTheme?.chatTheme?.gva?.galleryCardTheme } init { diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt index 53d6a4813..86e26fae7 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolder.kt @@ -9,13 +9,16 @@ import com.glia.widgets.chat.adapter.GvaGalleryAdapter import com.glia.widgets.chat.model.GvaGalleryCard import com.glia.widgets.chat.model.GvaGalleryCards import com.glia.widgets.databinding.ChatGvaGalleryLayoutBinding +import com.glia.widgets.di.Dependencies +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme internal class GvaGalleryViewHolder( private val contentBinding: ChatGvaGalleryLayoutBinding, buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, - uiTheme: UiTheme -) : OperatorBaseViewHolder(contentBinding.root, contentBinding.chatHeadView, uiTheme) { - private val adapter = GvaGalleryAdapter(buttonsClickListener, uiTheme) + uiTheme: UiTheme, + unifiedTheme: UnifiedTheme? = Dependencies.getGliaThemeManager().theme +) : OperatorBaseViewHolder(contentBinding.root, contentBinding.chatHeadView, uiTheme, unifiedTheme) { + private val adapter = GvaGalleryAdapter(buttonsClickListener, uiTheme, unifiedTheme) init { contentBinding.cardRecyclerView.adapter = adapter diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorBaseViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorBaseViewHolder.kt index d127c3941..e2d2c8fe6 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorBaseViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/OperatorBaseViewHolder.kt @@ -8,16 +8,18 @@ import com.glia.widgets.UiTheme import com.glia.widgets.chat.model.OperatorChatItem import com.glia.widgets.di.Dependencies import com.glia.widgets.view.OperatorStatusView +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme import com.glia.widgets.view.unifiedui.theme.chat.MessageBalloonTheme internal open class OperatorBaseViewHolder( itemView: View, private val chatHeadView: OperatorStatusView, - private val uiTheme: UiTheme + private val uiTheme: UiTheme, + unifiedTheme: UnifiedTheme? = Dependencies.getGliaThemeManager().theme ) : RecyclerView.ViewHolder(itemView) { val operatorTheme: MessageBalloonTheme? by lazy { - Dependencies.getGliaThemeManager().theme?.chatTheme?.operatorMessage + unifiedTheme?.chatTheme?.operatorMessage } init { diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/parse/RemoteConfigurationParser.kt b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/parse/RemoteConfigurationParser.kt index 07a5161a0..49f483676 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/parse/RemoteConfigurationParser.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/view/unifiedui/parse/RemoteConfigurationParser.kt @@ -1,6 +1,7 @@ package com.glia.widgets.view.unifiedui.parse import com.glia.widgets.di.Dependencies +import com.glia.widgets.helper.ResourceProvider import com.glia.widgets.view.unifiedui.config.RemoteConfiguration import com.glia.widgets.view.unifiedui.config.alert.AxisRemoteConfig import com.glia.widgets.view.unifiedui.config.base.AlignmentTypeRemoteConfig @@ -13,7 +14,9 @@ import com.glia.widgets.view.unifiedui.config.chat.AttachmentSourceTypeRemoteCon import com.google.gson.Gson import com.google.gson.GsonBuilder -internal class RemoteConfigurationParser { +internal class RemoteConfigurationParser( + resourceProvider: ResourceProvider = Dependencies.getResourceProvider() +) { /** * @return [Gson] instance with applied deserializers to parse remote config. */ @@ -23,7 +26,7 @@ internal class RemoteConfigurationParser { .registerTypeAdapter(ColorLayerRemoteConfig::class.java, ColorLayerDeserializer()) .registerTypeAdapter( SizeDpRemoteConfig::class.java, - DpDeserializer(Dependencies.getResourceProvider()) + DpDeserializer(resourceProvider) ) .registerTypeAdapter(SizeSpRemoteConfig::class.java, SpDeserializer()) .registerTypeAdapter(TextStyleRemoteConfig::class.java, TextStyleDeserializer()) diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViews.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViews.png new file mode 100644 index 000000000..474096206 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViews.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39ba083e9ad5edac8255b3000003cd08d7cf2ed5ad1cfda1bed93df4fdec2f5a +size 67841 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithGlobalColors.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithGlobalColors.png new file mode 100644 index 000000000..44bee095d --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithGlobalColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:907b3bfd4bbdbaa33821dda1586e4309e96fbd088673d8cecf81b83cbfa3b77c +size 71234 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithUiTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithUiTheme.png new file mode 100644 index 000000000..5b3e4da3e --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithUiTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f515b3527f5be88138343cafd12d66a666942d90fbb1c343a2612da53b32465 +size 72647 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithUnifiedTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithUnifiedTheme.png new file mode 100644 index 000000000..d4cd1562d --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithUnifiedTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:072a158149928d8259e4b9b263615fa628b08435751f6cf6089c61056ef4b93f +size 75851 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithUnifiedThemeWithoutGva.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithUnifiedThemeWithoutGva.png new file mode 100644 index 000000000..4ba7ca2ab --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_allViewsWithUnifiedThemeWithoutGva.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df903dc3bbee44891b6991f7de11dc17cbd9a65ef7ab640f188cc4c4e4b0e557 +size 76884 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitle.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitle.png new file mode 100644 index 000000000..7c175aee4 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d228ce9627c0b627cb84f75138884ed1365111fbdc0955d9cb3621225a719c7 +size 54553 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithGlobalColors.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithGlobalColors.png new file mode 100644 index 000000000..d7fcf3250 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithGlobalColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b366ac5b8b235d1b25d5bb1b5f067572b99c23de754280879313a4a8605926e5 +size 52667 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithUiTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithUiTheme.png new file mode 100644 index 000000000..e77477e68 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithUiTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7a6fbf82cccf164cc95d510e8fface386e19479018d1d7a4bc056d0dcd75b8f +size 55587 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithUnifiedTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithUnifiedTheme.png new file mode 100644 index 000000000..10a97787f --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithUnifiedTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b623d76ddf544e0ceb5a3c77309d707b89baf6e3d275420c18b5c38bbcf996f5 +size 49587 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithUnifiedThemeWithoutGva.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithUnifiedThemeWithoutGva.png new file mode 100644 index 000000000..8a05ce28a --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longButtonsTitleWithUnifiedThemeWithoutGva.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45556e1517d676b59fe673cbda7b3244f659b7986a86d69a8379d3f1291af388 +size 65852 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitle.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitle.png new file mode 100644 index 000000000..94ea5d052 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98eb3fe38c003f8eb88844a63a2144cfe2e29c0f209902e017b0a4142e87f532 +size 44704 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithGlobalColors.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithGlobalColors.png new file mode 100644 index 000000000..55c5a815f --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithGlobalColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87ffcaf53da7d371f0d221f319d3e81d59880542ff17e83947a5978b4870171e +size 46895 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithUiTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithUiTheme.png new file mode 100644 index 000000000..84c864340 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithUiTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3947d4d819fec65e0110ec6f557a3777a01eab945ad5913c35123a44f320686d +size 47312 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithUnifiedTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithUnifiedTheme.png new file mode 100644 index 000000000..9080d0dc2 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithUnifiedTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38a544f1f8b05f861958d2010500c99e3b18596a0d17818830d260d42c878d79 +size 36854 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithUnifiedThemeWithoutGva.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithUnifiedThemeWithoutGva.png new file mode 100644 index 000000000..cdf5ebab4 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longSubtitleWithUnifiedThemeWithoutGva.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d9f47b09fd3c190d0de25f26020f335324e54d881f12e3a84673c8c143a45c9 +size 44054 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitle.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitle.png new file mode 100644 index 000000000..ac51f04f2 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:daf7f6bc1a80affd889d830cf5a20011905ff4e9363c32c89bf5b9392838ca57 +size 35495 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithGlobalColors.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithGlobalColors.png new file mode 100644 index 000000000..d1eca842e --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithGlobalColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9b2ab131cbd1b7eae68e0088cbcee0f99aa5d6a1bc3b556250266b6b6edddcf +size 37845 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithUiTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithUiTheme.png new file mode 100644 index 000000000..0425ded8b --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithUiTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a190d4b7cf2657995c4d7ab5768173533d9dc309da3d98387b800823967bcbd4 +size 39441 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithUnifiedTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithUnifiedTheme.png new file mode 100644 index 000000000..24c0cc86f --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithUnifiedTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a57bd418a95a7c9cfa77238f6ceadc93ed67915458fe7efcb0657a39590079db +size 48856 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithUnifiedThemeWithoutGva.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithUnifiedThemeWithoutGva.png new file mode 100644 index 000000000..9d6fafad2 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_longTitleWithUnifiedThemeWithoutGva.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e41ad68753c08617c75c7529deb5d13894892b75638ab94a0c5ef1a7f4976ac4 +size 31501 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatory.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatory.png new file mode 100644 index 000000000..fb4bc5826 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatory.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf4a5e650c405f4baa5ddcbc0ea5f16f0e87bebc12a0b719d730c57ad9c9d179 +size 2434 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithGlobalColors.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithGlobalColors.png new file mode 100644 index 000000000..5deaa5540 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithGlobalColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce98a5e73089251cc7d8bdbadd5815a9af3c1e639028ca14cf6cf4da5ad28c68 +size 3913 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithUiTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithUiTheme.png new file mode 100644 index 000000000..0806594cd --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithUiTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe3c716ea7b97e92bddf58b2c1d7cac00dd028380f74a3b6b5193d8fb3836666 +size 3947 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithUnifiedTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithUnifiedTheme.png new file mode 100644 index 000000000..b4e3af747 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithUnifiedTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cbbf1ddcd4d150f007f739f5f41d14e42c52fc0604892c2de792020b064cb87 +size 4674 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithUnifiedThemeWithoutGva.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithUnifiedThemeWithoutGva.png new file mode 100644 index 000000000..d5e671866 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_onlyMandatoryWithUnifiedThemeWithoutGva.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a291eb05c5ccb6c8f0b84a540861669b7abc52703055691d824d36a46f8a726 +size 6500 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_testHolder.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_testHolder.png deleted file mode 100644 index 806b37bec..000000000 --- a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryItemViewHolderSnapshotTest_testHolder.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b73b00033301ca68994df2abe78dbbc175f705e97bad2199b802cab08ec1d2af -size 12123 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHead.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHead.png new file mode 100644 index 000000000..8b514fde0 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHead.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52f7048efa1b32a589245e052169defe686a4e6c1c7b580539d64f5274ae967e +size 59764 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithGlobalColors.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithGlobalColors.png new file mode 100644 index 000000000..6f6a8d88d --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithGlobalColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7405832d37a7018708e18604c41b6500e302fdd0daba61d783198f072943f488 +size 64938 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithUiTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithUiTheme.png new file mode 100644 index 000000000..fe3a4c286 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithUiTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:618654e11b59ddad67d9819346e7e14a5f8a003842a289b4e8ce2e5cc9184403 +size 69236 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithUnifiedTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithUnifiedTheme.png new file mode 100644 index 000000000..b556f4882 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithUnifiedTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:615cb200ef07f2f41419377b9456c54f3efd86b3f16a45dd893fbddc6ac77bcb +size 92971 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithUnifiedThemeWithoutGva.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithUnifiedThemeWithoutGva.png new file mode 100644 index 000000000..defcc0177 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithChatHeadWithUnifiedThemeWithoutGva.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7610c8f43138072b1caf02d8162e0e50ed3d13bd5a4c6137f4f967ce58e76b89 +size 69463 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHead.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHead.png new file mode 100644 index 000000000..55a41af81 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHead.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b6ef694ec7000b245d48ccbd47f4daf37b88d10d70ffd6c40910abc9c8388d0 +size 58236 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithGlobalColors.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithGlobalColors.png new file mode 100644 index 000000000..b50ac0788 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithGlobalColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f05727709c62f3ea3f9ad72a120e2ac83318e88e947654d2fac53da7ac849dd4 +size 63215 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithUiTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithUiTheme.png new file mode 100644 index 000000000..20261bf58 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithUiTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a069fb7504002f68607b0f4c473698b5b1150492916266bad9cf6f7d89f343fa +size 67095 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithUnifiedTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithUnifiedTheme.png new file mode 100644 index 000000000..966f7daa4 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithUnifiedTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da1473debbcfe68f915045c4668923b61d510f4930f7412f8972523f101e664b +size 90422 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithUnifiedThemeWithoutGva.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithUnifiedThemeWithoutGva.png new file mode 100644 index 000000000..1e88fc9e0 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaGalleryViewHolderSnapshotTest_itemsWithoutChatHeadWithUnifiedThemeWithoutGva.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d9f1906f70d7e81d0e6968190fedf71a80ef14f3c90282c4071abbb1a23fb90 +size 67678 diff --git a/widgetssdk/src/testSnapshot/java/com/glia/widgets/SnapshotTest.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/SnapshotTest.kt new file mode 100644 index 000000000..4312f745b --- /dev/null +++ b/widgetssdk/src/testSnapshot/java/com/glia/widgets/SnapshotTest.kt @@ -0,0 +1,53 @@ +package com.glia.widgets + +import com.glia.widgets.snapshotutils.SnapshotTheme +import android.content.Context +import android.content.res.Resources +import android.view.LayoutInflater +import android.view.View +import androidx.annotation.RawRes +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import com.android.ide.common.rendering.api.SessionParams +import com.glia.widgets.snapshotutils.SnapshotContent +import com.glia.widgets.snapshotutils.SnapshotStrings +import org.junit.After +import org.junit.Before +import org.junit.Rule +import java.io.BufferedReader + +open class SnapshotTest : SnapshotContent, SnapshotStrings, SnapshotTheme { + @Suppress("PropertyName") + @get:Rule + val _paparazzi = Paparazzi( + deviceConfig = DeviceConfig.PIXEL_4A, + renderingMode = SessionParams.RenderingMode.SHRINK, + showSystemUi = false, + theme = "ThemeOverlay_Glia_Chat_Material" + ) + + override val context: Context + get() = _paparazzi.context + + override val resources: Resources + get() = _paparazzi.resources + + override val layoutInflater: LayoutInflater + get() = _paparazzi.layoutInflater + + override fun rawRes(@RawRes resId: Int): String { + return resources.openRawResource(resId).use { + BufferedReader(it.reader()).readText() + } + } + + fun snapshot(view: View, name: String? = null, offsetMillis: Long = 0L) { + _paparazzi.snapshot(view, name, offsetMillis) + } + + @Before + open fun setUp() {} + + @After + open fun tearDown() {} +} diff --git a/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt index 64ff39dc3..1e0ac0cf6 100644 --- a/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt +++ b/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt @@ -1,39 +1,347 @@ package com.glia.widgets.chat.adapter.holder -import app.cash.paparazzi.DeviceConfig -import app.cash.paparazzi.Paparazzi -import com.android.ide.common.rendering.api.SessionParams +import android.view.View +import androidx.annotation.RawRes +import com.glia.widgets.R +import com.glia.widgets.SnapshotTest import com.glia.widgets.UiTheme import com.glia.widgets.chat.model.GvaButton import com.glia.widgets.chat.model.GvaGalleryCard import com.glia.widgets.databinding.ChatGvaGalleryItemBinding -import org.junit.Rule +import com.glia.widgets.snapshotutils.SnapshotGva +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme import org.junit.Test -class GvaGalleryItemViewHolderSnapshotTest { - @get:Rule - val paparazzi = Paparazzi( - deviceConfig = DeviceConfig.PIXEL_4A, - renderingMode = SessionParams.RenderingMode.SHRINK, - showSystemUi = false, - theme = "ThemeOverlay_Glia_Chat_Material" +class GvaGalleryItemViewHolderSnapshotTest : SnapshotTest(), SnapshotGva { + + // MARK: tests with all views + + private fun allViewsCard() = GvaGalleryCard( + title = "Title", + subtitle = "Subtitle", + options = listOf( + GvaButton("Button 1"), + GvaButton("Button 2") + ) + ) + + @Test + fun allViews() { + snapshot( + setupView( + allViewsCard(), + R.drawable.test_banner + ).viewHolder.itemView + ) + } + + @Test + fun allViewsWithUiTheme() { + snapshot( + setupView( + allViewsCard(), + R.drawable.test_banner, + uiTheme = uiTheme() + ).viewHolder.itemView + ) + } + + @Test + fun allViewsWithGlobalColors() { + snapshot( + setupView( + allViewsCard(), + R.drawable.test_banner, + unifiedThemeWithGlobalColors() + ).viewHolder.itemView + ) + } + + @Test + fun allViewsWithUnifiedTheme() { + snapshot( + setupView( + allViewsCard(), + R.drawable.test_banner, + unifiedTheme() + ).viewHolder.itemView + ) + } + + @Test + fun allViewsWithUnifiedThemeWithoutGva() { + snapshot( + setupView( + allViewsCard(), + R.drawable.test_banner, + unifiedThemeWithoutGva() + ).viewHolder.itemView + ) + } + + // MARK: tests with mandatory view + + private fun onlyMandatoryCard() = GvaGalleryCard( + title = "Title" + ) + + @Test + fun onlyMandatory() { + snapshot( + setupView( + onlyMandatoryCard() + ).viewHolder.itemView + ) + } + + @Test + fun onlyMandatoryWithUiTheme() { + snapshot( + setupView( + onlyMandatoryCard(), + uiTheme = uiTheme() + ).viewHolder.itemView + ) + } + + @Test + fun onlyMandatoryWithGlobalColors() { + snapshot( + setupView( + onlyMandatoryCard(), + unifiedTheme = unifiedThemeWithGlobalColors() + ).viewHolder.itemView + ) + } + + @Test + fun onlyMandatoryWithUnifiedTheme() { + snapshot( + setupView( + onlyMandatoryCard(), + unifiedTheme = unifiedTheme() + ).viewHolder.itemView + ) + } + + @Test + fun onlyMandatoryWithUnifiedThemeWithoutGva() { + snapshot( + setupView( + onlyMandatoryCard(), + unifiedTheme = unifiedThemeWithoutGva() + ).viewHolder.itemView + ) + } + + // MARK: tests with long title + + private fun longTitleCard() = GvaGalleryCard( + title = gvaLongTitle() + ) + + @Test + fun longTitle() { + snapshot( + setupView( + longTitleCard() + ).viewHolder.itemView + ) + } + + @Test + fun longTitleWithUiTheme() { + snapshot( + setupView( + longTitleCard(), + uiTheme = uiTheme() + ).viewHolder.itemView + ) + } + + @Test + fun longTitleWithGlobalColors() { + snapshot( + setupView( + longTitleCard(), + unifiedTheme = unifiedThemeWithGlobalColors() + ).viewHolder.itemView + ) + } + + @Test + fun longTitleWithUnifiedTheme() { + snapshot( + setupView( + longTitleCard(), + unifiedTheme = unifiedTheme() + ).viewHolder.itemView + ) + } + + @Test + fun longTitleWithUnifiedThemeWithoutGva() { + snapshot( + setupView( + longTitleCard(), + unifiedTheme = unifiedThemeWithoutGva() + ).viewHolder.itemView + ) + } + + // MARK: tests with long subtitle + + private fun longSubtitleCard() = GvaGalleryCard( + title = "Title", + subtitle = gvaLongSubtitle(), + options = listOf( + GvaButton() + ) ) @Test - fun testHolder() { - val binding = ChatGvaGalleryItemBinding.inflate(paparazzi.layoutInflater) - val viewHolder = GvaGalleryItemViewHolder(binding, {}, UiTheme()) + fun longSubtitle() { + snapshot( + setupView( + longSubtitleCard() + ).viewHolder.itemView + ) + } - val gvaGalleryCard = GvaGalleryCard( - title = "Title", - subtitle = "Subtitle", - options = listOf( - GvaButton("Button 1"), - GvaButton("Button 2") - ) + @Test + fun longSubtitleWithUiTheme() { + snapshot( + setupView( + longSubtitleCard(), + uiTheme = uiTheme() + ).viewHolder.itemView ) - viewHolder.bind(gvaGalleryCard, 1, 4) + } + + @Test + fun longSubtitleWithGlobalColors() { + snapshot( + setupView( + longSubtitleCard(), + unifiedTheme = unifiedThemeWithGlobalColors() + ).viewHolder.itemView + ) + } + + @Test + fun longSubtitleWithUnifiedTheme() { + snapshot( + setupView( + longSubtitleCard(), + unifiedTheme = unifiedTheme() + ).viewHolder.itemView + ) + } + + @Test + fun longSubtitleWithUnifiedThemeWithoutGva() { + snapshot( + setupView( + longSubtitleCard(), + unifiedTheme = unifiedThemeWithoutGva() + ).viewHolder.itemView + ) + } + + // MARK: tests with long subtitle + + private fun longButtonsTitleCard() = GvaGalleryCard( + title = "Title", + options = mediumLengthTexts().map { GvaButton(it) } + ) + + @Test + fun longButtonsTitle() { + val itemView = setupView( + longButtonsTitleCard() + ).viewHolder.itemView + measureHeight(itemView) + + snapshot(itemView) + } + + @Test + fun longButtonsTitleWithUiTheme() { + val itemView = setupView( + longButtonsTitleCard(), + uiTheme = uiTheme() + + ).viewHolder.itemView + measureHeight(itemView) + + snapshot(itemView) + } + + @Test + fun longButtonsTitleWithGlobalColors() { + val itemView = setupView( + longButtonsTitleCard(), + unifiedTheme = unifiedThemeWithGlobalColors() + + ).viewHolder.itemView + measureHeight(itemView) + + snapshot(itemView) + } + + @Test + fun longButtonsTitleWithUnifiedTheme() { + val itemView = setupView( + longButtonsTitleCard(), + unifiedTheme = unifiedTheme() + + ).viewHolder.itemView + measureHeight(itemView) - paparazzi.snapshot(binding.root) + snapshot(itemView) + } + + @Test + fun longButtonsTitleWithUnifiedThemeWithoutGva() { + val itemView = setupView( + longButtonsTitleCard(), + unifiedTheme = unifiedThemeWithoutGva() + + ).viewHolder.itemView + measureHeight(itemView) + + snapshot(itemView) + } + + // MARK: utils for tests + + private data class ViewData(val binding: ChatGvaGalleryItemBinding, val viewHolder: GvaGalleryItemViewHolder) + + private fun setupView( + card: GvaGalleryCard, + @RawRes imageRes: Int? = null, + unifiedTheme: UnifiedTheme? = null, + uiTheme: UiTheme = UiTheme() + ): ViewData { + val binding = ChatGvaGalleryItemBinding.inflate(layoutInflater) + val viewHolder = GvaGalleryItemViewHolder(binding, {}, uiTheme, unifiedTheme) + + viewHolder.bind(card, 1, 4) + + imageRes?.also { + // Set image without Picasso + binding.image.setImageResource(it) + binding.image.visibility = View.VISIBLE + } + + return ViewData(binding, viewHolder) + } + + private fun measureHeight(view: View) { + val width = resources.getDimensionPixelOffset(R.dimen.glia_chat_gva_gallery_card_width) + view.measure( + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.AT_MOST), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + ) } } diff --git a/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolderSnapshotTest.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolderSnapshotTest.kt new file mode 100644 index 000000000..d45fdb4ec --- /dev/null +++ b/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryViewHolderSnapshotTest.kt @@ -0,0 +1,177 @@ +package com.glia.widgets.chat.adapter.holder + +import com.glia.widgets.SnapshotTest +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.adapter.ChatItemHeightManager +import com.glia.widgets.chat.model.GvaButton +import com.glia.widgets.chat.model.GvaGalleryCard +import com.glia.widgets.chat.model.GvaGalleryCards +import com.glia.widgets.databinding.ChatGvaGalleryLayoutBinding +import com.glia.widgets.snapshotutils.SnapshotGva +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme +import org.junit.Test + +class GvaGalleryViewHolderSnapshotTest : SnapshotTest(), SnapshotGva { + + private fun galleryCardList() = listOf( + GvaGalleryCard( + title = "Gallery card title", + subtitle = gvaLongSubtitle(), + options = listOf( + GvaButton("Button 1"), + GvaButton("Button 2") + ) + ), + GvaGalleryCard( + title = "Buttons gallery card", + options = mediumLengthTexts().map { GvaButton(it) } + ) + ) + + // MARK: tests for view without chat head + + @Test + fun itemsWithoutChatHead() { + snapshot( + setupView( + GvaGalleryCards( + galleryCards = galleryCardList() + ) + ).viewHolder.itemView + ) + } + + @Test + fun itemsWithoutChatHeadWithUiTheme() { + snapshot( + setupView( + GvaGalleryCards( + galleryCards = galleryCardList() + ), + uiTheme = uiTheme() + ).viewHolder.itemView + ) + } + + @Test + fun itemsWithoutChatHeadWithGlobalColors() { + snapshot( + setupView( + GvaGalleryCards( + galleryCards = galleryCardList() + ), + unifiedTheme = unifiedThemeWithGlobalColors() + ).viewHolder.itemView + ) + } + + @Test + fun itemsWithoutChatHeadWithUnifiedTheme() { + snapshot( + setupView( + GvaGalleryCards( + galleryCards = galleryCardList() + ), + unifiedTheme = unifiedTheme() + ).viewHolder.itemView + ) + } + + @Test + fun itemsWithoutChatHeadWithUnifiedThemeWithoutGva() { + snapshot( + setupView( + GvaGalleryCards( + galleryCards = galleryCardList() + ), + unifiedTheme = unifiedThemeWithoutGva() + ).viewHolder.itemView + ) + } + + // MARK: tests for view with chat head + + @Test + fun itemsWithChatHead() { + snapshot( + setupView( + GvaGalleryCards( + galleryCards = galleryCardList(), + showChatHead = true + ) + ).viewHolder.itemView + ) + } + + @Test + fun itemsWithChatHeadWithUiTheme() { + snapshot( + setupView( + GvaGalleryCards( + galleryCards = galleryCardList(), + showChatHead = true + ), + uiTheme = uiTheme() + ).viewHolder.itemView + ) + } + + @Test + fun itemsWithChatHeadWithGlobalColors() { + snapshot( + setupView( + GvaGalleryCards( + galleryCards = galleryCardList(), + showChatHead = true + ), + unifiedTheme = unifiedThemeWithGlobalColors() + ).viewHolder.itemView + ) + } + + @Test + fun itemsWithChatHeadWithUnifiedTheme() { + snapshot( + setupView( + GvaGalleryCards( + galleryCards = galleryCardList(), + showChatHead = true + ), + unifiedTheme = unifiedTheme() + ).viewHolder.itemView + ) + } + + @Test + fun itemsWithChatHeadWithUnifiedThemeWithoutGva() { + snapshot( + setupView( + GvaGalleryCards( + galleryCards = galleryCardList(), + showChatHead = true + ), + unifiedTheme = unifiedThemeWithoutGva() + ).viewHolder.itemView + ) + } + + // MARK: utils for tests + + private data class ViewData(val binding: ChatGvaGalleryLayoutBinding, val viewHolder: GvaGalleryViewHolder) + + private fun setupView( + galleryCards: GvaGalleryCards, + unifiedTheme: UnifiedTheme? = null, + uiTheme: UiTheme = UiTheme() + ): ViewData { + val heightManager = ChatItemHeightManager(uiTheme, layoutInflater, resources, unifiedTheme) + heightManager.measureHeight(listOf(galleryCards)) + + val binding = ChatGvaGalleryLayoutBinding.inflate(layoutInflater) + val viewHolder = GvaGalleryViewHolder(binding, {}, uiTheme, unifiedTheme) + + viewHolder.bind(galleryCards, heightManager.getMeasuredHeight(galleryCards)) + + return ViewData(binding, viewHolder) + } +} diff --git a/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotContent.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotContent.kt new file mode 100644 index 000000000..d94fb1b90 --- /dev/null +++ b/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotContent.kt @@ -0,0 +1,14 @@ +package com.glia.widgets.snapshotutils + +import android.content.Context +import android.content.res.Resources +import android.view.LayoutInflater +import androidx.annotation.RawRes + +interface SnapshotContent { + val context: Context + val resources: Resources + val layoutInflater: LayoutInflater + + fun rawRes(@RawRes resId: Int): String +} diff --git a/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotGva.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotGva.kt new file mode 100644 index 000000000..97d8a3a5f --- /dev/null +++ b/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotGva.kt @@ -0,0 +1,24 @@ +package com.glia.widgets.snapshotutils + +import com.glia.widgets.R +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme +import com.google.gson.JsonObject + +internal interface SnapshotGva : SnapshotTheme { + + fun gvaLongTitle() = "\uD83D\uDE80 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam at interdum nisi. Nullam sem urna, vehicula eget metus vel." + + fun gvaLongSubtitle() = "Quisque ut sollicitudin augue \uD83D\uDC40
" + + "Ut lobortis sit amet neque nec gravida:" + + "

  • Sed risus
  • Maecenas
" + + "Nam commodo ligula non justo semper pellentesque. \uD83D\uDE4C" + + fun unifiedThemeWithoutGva(): UnifiedTheme = unifiedTheme(R.raw.test_unified_config) { unifiedTheme -> + unifiedTheme.add( + "chatScreen", + (unifiedTheme.remove("chatScreen") as JsonObject).also { + it.remove("gva") + } + ) + } +} diff --git a/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotStrings.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotStrings.kt new file mode 100644 index 000000000..d8eb42bf9 --- /dev/null +++ b/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotStrings.kt @@ -0,0 +1,12 @@ +package com.glia.widgets.snapshotutils + +interface SnapshotStrings { + + fun mediumLengthTexts() = listOf( + "Maecenas sed faucibus metus", + "Ut posuere dignissim dolor", + "Duis sed vestibulum risus", + "In pulvinar id turpis ut interdum", + "Sed quis ex nisl vestibulum risus" + ) +} diff --git a/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotTheme.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotTheme.kt new file mode 100644 index 000000000..c8d2625d3 --- /dev/null +++ b/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotTheme.kt @@ -0,0 +1,147 @@ +package com.glia.widgets.snapshotutils + +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.FontRes +import androidx.annotation.RawRes +import com.glia.widgets.R +import com.glia.widgets.UiTheme +import com.glia.widgets.helper.ResourceProvider +import com.glia.widgets.view.unifiedui.config.RemoteConfiguration +import com.glia.widgets.view.unifiedui.parse.RemoteConfigurationParser +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme +import com.google.gson.Gson +import com.google.gson.JsonObject + +internal interface SnapshotTheme : SnapshotContent { + + fun unifiedTheme(json: String): UnifiedTheme { + val resourceProvider = ResourceProvider(context) + return RemoteConfigurationParser(resourceProvider).defaultGson + .fromJson(json, RemoteConfiguration::class.java).toUnifiedTheme()!! + } + + fun unifiedTheme(@RawRes resId: Int, modifier: ((JsonObject) -> Unit)? = null): UnifiedTheme { + val jsonRaw = rawRes(resId) + if (modifier != null) { + val jsonObject = Gson().fromJson(rawRes(R.raw.test_unified_config), JsonObject::class.java) + modifier(jsonObject) + return unifiedTheme(jsonObject.toString()) + } + return unifiedTheme(jsonRaw) + } + + fun unifiedThemeWithGlobalColors(): UnifiedTheme = unifiedTheme(R.raw.global_colors_unified_config) + + fun unifiedTheme(): UnifiedTheme = unifiedTheme(R.raw.test_unified_config) + + fun uiTheme( + appBarTitle: String? = "Snapshot Test", + @ColorRes brandPrimaryColor: Int? = R.color.brandPrimaryColor, + @ColorRes baseLightColor: Int? = R.color.baseLightColor, + @ColorRes baseDarkColor: Int? = R.color.baseDarkColor, + @ColorRes baseNormalColor: Int? = R.color.baseNormalColor, + @ColorRes baseShadeColor: Int? = R.color.baseShadeColor, + @ColorRes systemAgentBubbleColor: Int? = R.color.systemAgentBubbleColor, + @ColorRes systemNegativeColor: Int? = R.color.systemNegativeColor, + @ColorRes visitorMessageBackgroundColor: Int? = R.color.visitorMessageBackgroundColor, + @ColorRes visitorMessageTextColor: Int? = R.color.visitorMessageTextColor, + @ColorRes operatorMessageBackgroundColor: Int? = R.color.operatorMessageBackgroundColor, + @ColorRes operatorMessageTextColor: Int? = R.color.operatorMessageTextColor, + @ColorRes newMessageDividerColor: Int? = R.color.newMessageDividerColor, + @ColorRes newMessageDividerTextColor: Int? = R.color.newMessageDividerTextColor, + @ColorRes botActionButtonBackgroundColor: Int? = R.color.botActionButtonBackgroundColor, + @ColorRes botActionButtonTextColor: Int? = R.color.botActionButtonTextColor, + @ColorRes botActionButtonSelectedBackgroundColor: Int? = R.color.botActionButtonSelectedBackgroundColor, + @ColorRes botActionButtonSelectedTextColor: Int? = R.color.botActionButtonSelectedTextColor, + @ColorRes sendMessageButtonTintColor: Int? = R.color.sendMessageButtonTintColor, + @ColorRes gliaChatBackgroundColor: Int? = R.color.gliaChatBackgroundColor, + @ColorRes gliaChatHeaderTitleTintColor: Int? = R.color.gliaChatHeaderTitleTintColor, + @ColorRes gliaChatHeaderHomeButtonTintColor: Int? = R.color.gliaChatHeaderHomeButtonTintColor, + @ColorRes gliaChatHeaderExitQueueButtonTintColor: Int? = R.color.gliaChatHeaderExitQueueButtonTintColor, + @ColorRes visitorCodeTextColor: Int? = R.color.visitorCodeTextColor, + @ColorRes visitorCodeBackgroundColor: Int? = R.color.visitorCodeBackgroundColor, + @ColorRes visitorCodeBorderColor: Int? = R.color.visitorCodeBorderColor, + @ColorRes gvaQuickReplyBackgroundColor: Int? = R.color.gvaQuickReplyBackgroundColor, + @ColorRes gvaQuickReplyStrokeColor: Int? = R.color.gvaQuickReplyStrokeColor, + @ColorRes gvaQuickReplyTextColor: Int? = R.color.gvaQuickReplyTextColor, + @ColorRes endScreenShareTintColor: Int? = R.color.endScreenShareTintColor, + @FontRes fontRes: Int? = R.font.expletus, + @DrawableRes iconAppBarBack: Int? = R.drawable.test_ic_app_bar_back, + @DrawableRes iconLeaveQueue: Int? = R.drawable.test_ic_leave_queue, + @DrawableRes iconSendMessage: Int? = R.drawable.test_ic_send_message, + @DrawableRes iconChatAudioUpgrade: Int? = R.drawable.test_ic_chat_audio_upgrade, + @DrawableRes iconUpgradeAudioDialog: Int? = R.drawable.test_ic_upgrade_audio_dialog, + @DrawableRes iconCallAudioOn: Int? = R.drawable.test_ic_call_audio_on, + @DrawableRes iconChatVideoUpgrade: Int? = R.drawable.test_ic_chat_video_upgrade, + @DrawableRes iconUpgradeVideoDialog: Int? = R.drawable.test_ic_upgrade_video_dialog, + @DrawableRes iconScreenSharingDialog: Int? = R.drawable.test_ic_screen_sharing_dialog, + @DrawableRes iconCallVideoOn: Int? = R.drawable.test_ic_call_video_on, + @DrawableRes iconCallAudioOff: Int? = R.drawable.test_ic_call_audio_off, + @DrawableRes iconCallVideoOff: Int? = R.drawable.test_ic_call_video_off, + @DrawableRes iconCallChat: Int? = R.drawable.test_ic_call_chat, + @DrawableRes iconCallSpeakerOn: Int? = R.drawable.test_ic_call_speaker_on, + @DrawableRes iconCallSpeakerOff: Int? = R.drawable.test_ic_call_speaker_off, + @DrawableRes iconCallMinimize: Int? = R.drawable.test_ic_call_minimize, + @DrawableRes iconPlaceholder: Int? = R.drawable.test_ic_placeholder, + @DrawableRes iconOnHold: Int? = R.drawable.test_ic_on_hold, + @DrawableRes iconEndScreenShare: Int? = R.drawable.test_ic_end_screen_share, + whiteLabel: Boolean? = null, + gliaAlertDialogButtonUseVerticalAlignment: Boolean? = null + ): UiTheme { + return UiTheme( + appBarTitle = "Snapshot Test", + brandPrimaryColor = brandPrimaryColor, + baseLightColor = baseLightColor, + baseDarkColor = baseDarkColor, + baseNormalColor = baseNormalColor, + baseShadeColor = baseShadeColor, + systemAgentBubbleColor = systemAgentBubbleColor, + systemNegativeColor = systemNegativeColor, + visitorMessageBackgroundColor = visitorMessageBackgroundColor, + visitorMessageTextColor = visitorMessageTextColor, + operatorMessageBackgroundColor = operatorMessageBackgroundColor, + operatorMessageTextColor = operatorMessageTextColor, + newMessageDividerColor = newMessageDividerColor, + newMessageDividerTextColor = newMessageDividerTextColor, + botActionButtonBackgroundColor = botActionButtonBackgroundColor, + botActionButtonTextColor = botActionButtonTextColor, + botActionButtonSelectedBackgroundColor = botActionButtonSelectedBackgroundColor, + botActionButtonSelectedTextColor = botActionButtonSelectedTextColor, + sendMessageButtonTintColor = sendMessageButtonTintColor, + gliaChatBackgroundColor = gliaChatBackgroundColor, + gliaChatHeaderTitleTintColor = gliaChatHeaderTitleTintColor, + gliaChatHeaderHomeButtonTintColor = gliaChatHeaderHomeButtonTintColor, + gliaChatHeaderExitQueueButtonTintColor = gliaChatHeaderExitQueueButtonTintColor, + visitorCodeTextColor = visitorCodeTextColor, + visitorCodeBackgroundColor = visitorCodeBackgroundColor, + visitorCodeBorderColor = visitorCodeBorderColor, + gvaQuickReplyBackgroundColor = gvaQuickReplyBackgroundColor, + gvaQuickReplyStrokeColor = gvaQuickReplyStrokeColor, + gvaQuickReplyTextColor = gvaQuickReplyTextColor, + endScreenShareTintColor = endScreenShareTintColor, + fontRes = fontRes, + iconAppBarBack = iconAppBarBack, + iconLeaveQueue = iconLeaveQueue, + iconSendMessage = iconSendMessage, + iconChatAudioUpgrade = iconChatAudioUpgrade, + iconUpgradeAudioDialog = iconUpgradeAudioDialog, + iconCallAudioOn = iconCallAudioOn, + iconChatVideoUpgrade = iconChatVideoUpgrade, + iconUpgradeVideoDialog = iconUpgradeVideoDialog, + iconScreenSharingDialog = iconScreenSharingDialog, + iconCallVideoOn = iconCallVideoOn, + iconCallAudioOff = iconCallAudioOff, + iconCallVideoOff = iconCallVideoOff, + iconCallChat = iconCallChat, + iconCallSpeakerOn = iconCallSpeakerOn, + iconCallSpeakerOff = iconCallSpeakerOff, + iconCallMinimize = iconCallMinimize, + iconPlaceholder = iconPlaceholder, + iconOnHold = iconOnHold, + iconEndScreenShare = iconEndScreenShare, + whiteLabel = whiteLabel, + gliaAlertDialogButtonUseVerticalAlignment = gliaAlertDialogButtonUseVerticalAlignment, + ) + } +} diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_banner.png b/widgetssdk/src/testSnapshot/res/drawable/test_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..4ab931c49e598021ae1a43f81d22cb0fd9852057 GIT binary patch literal 12005 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 G_kRGB&ndkC literal 0 HcmV?d00001 diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_app_bar_back.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_app_bar_back.xml new file mode 100644 index 000000000..060e232e4 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_app_bar_back.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_audio_off.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_audio_off.xml new file mode 100644 index 000000000..39d7f4d44 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_audio_off.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_audio_on.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_audio_on.xml new file mode 100644 index 000000000..97498b4a8 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_audio_on.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_chat.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_chat.xml new file mode 100644 index 000000000..4e5e7c6b6 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_chat.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_minimize.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_minimize.xml new file mode 100644 index 000000000..6395d62c2 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_minimize.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_speaker_off.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_speaker_off.xml new file mode 100644 index 000000000..a7f4911b8 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_speaker_off.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_speaker_on.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_speaker_on.xml new file mode 100644 index 000000000..50a772b37 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_speaker_on.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_video_off.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_video_off.xml new file mode 100644 index 000000000..35340b19e --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_video_off.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_video_on.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_video_on.xml new file mode 100644 index 000000000..e0546d476 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_call_video_on.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_chat_audio_upgrade.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_chat_audio_upgrade.xml new file mode 100644 index 000000000..5496fb8e0 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_chat_audio_upgrade.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_chat_video_upgrade.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_chat_video_upgrade.xml new file mode 100644 index 000000000..baa19f9eb --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_chat_video_upgrade.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_end_screen_share.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_end_screen_share.xml new file mode 100644 index 000000000..7b42ceec0 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_end_screen_share.xml @@ -0,0 +1,6 @@ + + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_leave_queue.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_leave_queue.xml new file mode 100644 index 000000000..5bdc37964 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_leave_queue.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_on_hold.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_on_hold.xml new file mode 100644 index 000000000..46cdf872d --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_on_hold.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_placeholder.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_placeholder.xml new file mode 100644 index 000000000..8966de256 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_placeholder.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_screen_sharing_dialog.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_screen_sharing_dialog.xml new file mode 100644 index 000000000..d4265167a --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_screen_sharing_dialog.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_send_message.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_send_message.xml new file mode 100644 index 000000000..bd4dfa735 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_send_message.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_upgrade_audio_dialog.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_upgrade_audio_dialog.xml new file mode 100644 index 000000000..a7859f18c --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_upgrade_audio_dialog.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/drawable/test_ic_upgrade_video_dialog.xml b/widgetssdk/src/testSnapshot/res/drawable/test_ic_upgrade_video_dialog.xml new file mode 100644 index 000000000..34b26df6f --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/drawable/test_ic_upgrade_video_dialog.xml @@ -0,0 +1,5 @@ + + + diff --git a/widgetssdk/src/testSnapshot/res/font/expletus.xml b/widgetssdk/src/testSnapshot/res/font/expletus.xml new file mode 100644 index 000000000..078e33b91 --- /dev/null +++ b/widgetssdk/src/testSnapshot/res/font/expletus.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/widgetssdk/src/testSnapshot/res/font/expletus_sans.ttf b/widgetssdk/src/testSnapshot/res/font/expletus_sans.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4b29fa3ccad4fc5a3dfc40b19a642238dce7cac7 GIT binary patch literal 56856 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 literal 0 HcmV?d00001 diff --git a/widgetssdk/src/testSnapshot/res/font/expletus_sans_bold.ttf b/widgetssdk/src/testSnapshot/res/font/expletus_sans_bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..209fa072b6ad3dbe230f14d938d876bf56ea1bff GIT binary patch literal 62616 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

61i=z$42v5Sq;lV4@5I~$-2-xh-fM`-60~$%1 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 + + #9C27B0 + #2196F3 + #F40336 + #FFC157 + #4C6F50 + #673A37 + #FF6732 + #E91E63 + #3F51B5 + #FF9800 + #00BCD4 + #CDDC39 + #FFEB3B + #8BC34A + #5F7B3B + #03A9F4 + #FFC107 + #9C2700 + #536DFE + #FFD740 + #8B534A + #50BCD4 + #673AB7 + #D9CEF3 + #4CAF50 + #FF5722 + #F44336 + #CD6C89 + #7FC107 + #3C27B0 + #536D9E + #FFFFFF + #6FD740 + + From 3e01f06a34c82c338febdbad087430868a1aa9fb Mon Sep 17 00:00:00 2001 From: Andrii Horishnii Date: Tue, 29 Aug 2023 19:03:38 +0300 Subject: [PATCH 59/69] Cover Persistent Button with snapshot tests MOB-2595 --- .../holder/GvaPersistentButtonsViewHolder.kt | 8 +- ...onsViewHolderSnapshotTest_withChatHead.png | 3 + ...est_withChatHeadUnifiedThemeWithoutGva.png | 3 + ...pshotTest_withChatHeadWithGlobalColors.png | 3 + ...erSnapshotTest_withChatHeadWithUiTheme.png | 3 + ...pshotTest_withChatHeadWithUnifiedTheme.png | 3 + ...ViewHolderSnapshotTest_withoutChatHead.png | 3 + ..._withoutChatHeadUnifiedThemeWithoutGva.png | 3 + ...otTest_withoutChatHeadWithGlobalColors.png | 3 + ...napshotTest_withoutChatHeadWithUiTheme.png | 3 + ...otTest_withoutChatHeadWithUnifiedTheme.png | 3 + .../GvaGalleryItemViewHolderSnapshotTest.kt | 2 +- ...PersistentButtonsViewHolderSnapshotTest.kt | 162 ++++++++++++++++++ .../res/raw/test_unified_config.json | 71 ++++++++ 14 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHead.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadUnifiedThemeWithoutGva.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadWithGlobalColors.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadWithUiTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadWithUnifiedTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHead.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadUnifiedThemeWithoutGva.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadWithGlobalColors.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadWithUiTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadWithUnifiedTheme.png create mode 100644 widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolderSnapshotTest.kt diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt index 6e62fc518..13c6d4d4b 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolder.kt @@ -14,6 +14,7 @@ import com.glia.widgets.helper.getColorStateListCompat import com.glia.widgets.helper.getFontCompat import com.glia.widgets.view.unifiedui.applyLayerTheme import com.glia.widgets.view.unifiedui.applyTextTheme +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme import com.glia.widgets.view.unifiedui.theme.gva.GvaPersistentButtonTheme import kotlin.properties.Delegates @@ -21,13 +22,14 @@ internal class GvaPersistentButtonsViewHolder( operatorMessageBinding: ChatOperatorMessageLayoutBinding, private val contentBinding: ChatGvaPersistentButtonsContentBinding, buttonsClickListener: ChatAdapter.OnGvaButtonsClickListener, - private val uiTheme: UiTheme -) : OperatorBaseViewHolder(operatorMessageBinding.root, operatorMessageBinding.chatHeadView, uiTheme) { + private val uiTheme: UiTheme, + unifiedTheme: UnifiedTheme? = Dependencies.getGliaThemeManager().theme +) : OperatorBaseViewHolder(operatorMessageBinding.root, operatorMessageBinding.chatHeadView, uiTheme, unifiedTheme) { private var adapter: GvaButtonsAdapter by Delegates.notNull() private val persistentButtonTheme: GvaPersistentButtonTheme? by lazy { - Dependencies.getGliaThemeManager().theme?.chatTheme?.gva?.persistentButtonTheme + unifiedTheme?.chatTheme?.gva?.persistentButtonTheme } init { diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHead.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHead.png new file mode 100644 index 000000000..d405d5890 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHead.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:373d66d7d4f9139518e75d86d4808dbd9cfe3d51226aa9b0acef79f31c466f42 +size 91028 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadUnifiedThemeWithoutGva.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadUnifiedThemeWithoutGva.png new file mode 100644 index 000000000..fd5061241 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadUnifiedThemeWithoutGva.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad048b26a408ca9066275863ca8ab950b6b8ac5af9d729d3b1fafd31fbc56023 +size 105670 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadWithGlobalColors.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadWithGlobalColors.png new file mode 100644 index 000000000..23f20e7c7 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadWithGlobalColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:316ac64b8f4ea91490d5183f7d64b6660f59db0074cdb17336f285c9db77f582 +size 94135 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadWithUiTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadWithUiTheme.png new file mode 100644 index 000000000..2f5b707be --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadWithUiTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f32eafd0cdd3e9607f2c960aa08121a441accb19b10cad66e98893c717ee3777 +size 89454 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadWithUnifiedTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadWithUnifiedTheme.png new file mode 100644 index 000000000..59808d17d --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withChatHeadWithUnifiedTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ff6b16648ca76e07883fe5dec12d7a38dcdbc1580deb61bae84bb6bade403fb +size 120058 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHead.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHead.png new file mode 100644 index 000000000..bd2488c3b --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHead.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c609ed3c918cc7bb4d61370b60d4dd0ff09eec498e9ede91331c335d1774dab +size 88869 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadUnifiedThemeWithoutGva.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadUnifiedThemeWithoutGva.png new file mode 100644 index 000000000..76bc8e162 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadUnifiedThemeWithoutGva.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e2d214c7fcd6a54d65d5c8e6ddf9a54a4b14bfc92b551d44103dfbbe7da07e2 +size 103088 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadWithGlobalColors.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadWithGlobalColors.png new file mode 100644 index 000000000..e0542bcce --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadWithGlobalColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba42f3ab95d2006197f1891462d804b256b5a1dc720f6b84ff5aa6b12596f126 +size 91652 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadWithUiTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadWithUiTheme.png new file mode 100644 index 000000000..aec64bbdb --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadWithUiTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aec0a64b724a977ac0fc67055ab3a34fabdf9fbb05aae9702cf7be1b37cb3b49 +size 86875 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadWithUnifiedTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadWithUnifiedTheme.png new file mode 100644 index 000000000..66d55e61a --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaPersistentButtonsViewHolderSnapshotTest_withoutChatHeadWithUnifiedTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b50d68410c40f38daf6479c255beb522633632d703da0e54578e53a6d5af5fc1 +size 117381 diff --git a/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt index 1e0ac0cf6..d3b6b4608 100644 --- a/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt +++ b/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaGalleryItemViewHolderSnapshotTest.kt @@ -334,7 +334,7 @@ class GvaGalleryItemViewHolderSnapshotTest : SnapshotTest(), SnapshotGva { binding.image.visibility = View.VISIBLE } - return ViewData(binding, viewHolder) + return ViewData(binding, viewHolder) } private fun measureHeight(view: View) { diff --git a/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolderSnapshotTest.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolderSnapshotTest.kt new file mode 100644 index 000000000..81f95ccf5 --- /dev/null +++ b/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolderSnapshotTest.kt @@ -0,0 +1,162 @@ +package com.glia.widgets.chat.adapter.holder + +import com.glia.widgets.SnapshotTest +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.model.GvaButton +import com.glia.widgets.chat.model.GvaPersistentButtons +import com.glia.widgets.databinding.ChatGvaPersistentButtonsContentBinding +import com.glia.widgets.databinding.ChatOperatorMessageLayoutBinding +import com.glia.widgets.snapshotutils.SnapshotGva +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme +import org.junit.Test + +class GvaPersistentButtonsViewHolderSnapshotTest : SnapshotTest(), SnapshotGva { + + private fun gvaPersistentButtons(showChatHead: Boolean = false) = GvaPersistentButtons( + content = gvaLongSubtitle(), + options = mediumLengthTexts().map { GvaButton(it) }, + showChatHead = showChatHead + ) + + // MARK: tests with all views + + @Test + fun withoutChatHead() { + snapshot( + setupView( + gvaPersistentButtons() + ).viewHolder.itemView + ) + } + + @Test + fun withoutChatHeadWithUiTheme() { + snapshot( + setupView( + gvaPersistentButtons(), + uiTheme = uiTheme() + ).viewHolder.itemView + ) + } + + @Test + fun withoutChatHeadWithGlobalColors() { + snapshot( + setupView( + gvaPersistentButtons(), + unifiedTheme = unifiedThemeWithGlobalColors() + ).viewHolder.itemView + ) + } + + @Test + fun withoutChatHeadWithUnifiedTheme() { + snapshot( + setupView( + gvaPersistentButtons(), + unifiedTheme = unifiedTheme() + ).viewHolder.itemView + ) + } + + @Test + fun withoutChatHeadUnifiedThemeWithoutGva() { + snapshot( + setupView( + gvaPersistentButtons(), + unifiedTheme = unifiedThemeWithoutGva() + ).viewHolder.itemView + ) + } + + // MARK: tests with long content + + @Test + fun withChatHead() { + snapshot( + setupView( + gvaPersistentButtons( + showChatHead = true + ), + ).viewHolder.itemView + ) + } + + @Test + fun withChatHeadWithUiTheme() { + snapshot( + setupView( + gvaPersistentButtons( + showChatHead = true + ), + uiTheme = uiTheme() + ).viewHolder.itemView + ) + } + + @Test + fun withChatHeadWithGlobalColors() { + snapshot( + setupView( + gvaPersistentButtons( + showChatHead = true + ), + unifiedTheme = unifiedThemeWithGlobalColors() + ).viewHolder.itemView + ) + } + + @Test + fun withChatHeadWithUnifiedTheme() { + snapshot( + setupView( + gvaPersistentButtons( + showChatHead = true + ), + unifiedTheme = unifiedTheme() + ).viewHolder.itemView + ) + } + + @Test + fun withChatHeadUnifiedThemeWithoutGva() { + snapshot( + setupView( + gvaPersistentButtons( + showChatHead = true + ), + unifiedTheme = unifiedThemeWithoutGva() + ).viewHolder.itemView + ) + } + + private data class ViewData( + val chatOperatorMessageLayoutBinding: ChatOperatorMessageLayoutBinding, + val gvaPersistentButtonsContentBinding: ChatGvaPersistentButtonsContentBinding, + val viewHolder: GvaPersistentButtonsViewHolder + ) + + private fun setupView( + card: GvaPersistentButtons, + unifiedTheme: UnifiedTheme? = null, + uiTheme: UiTheme = UiTheme() + ): ViewData { + val chatOperatorMessageLayoutBinding = ChatOperatorMessageLayoutBinding.inflate(layoutInflater) + val gvaPersistentButtonsContentBinding = ChatGvaPersistentButtonsContentBinding.inflate( + layoutInflater, + chatOperatorMessageLayoutBinding.contentLayout, + true + ) + val viewHolder = GvaPersistentButtonsViewHolder( + chatOperatorMessageLayoutBinding, + gvaPersistentButtonsContentBinding, + {}, + uiTheme, + unifiedTheme + ) + + viewHolder.bind(card) + + return ViewData(chatOperatorMessageLayoutBinding, gvaPersistentButtonsContentBinding, viewHolder) + } +} diff --git a/widgetssdk/src/testSnapshot/res/raw/test_unified_config.json b/widgetssdk/src/testSnapshot/res/raw/test_unified_config.json index 9097534d3..8e66d6959 100644 --- a/widgetssdk/src/testSnapshot/res/raw/test_unified_config.json +++ b/widgetssdk/src/testSnapshot/res/raw/test_unified_config.json @@ -59,6 +59,77 @@ } }, "gva": { + "persistentButton": { + "title": { + "alignment": "center", + "background": { + "type": "gradient", + "value": ["#11e7b1", "#da6eff"] + }, + "font": { + "size": 14, + "style": "bold" + }, + "foreground": { + "type": "fill", + "value": ["#a90029"] + } + }, + "background": { + "color": { + "type": "gradient", + "value": ["#e7c915", "#0505e7"] + }, + "border": { + "type": "fill", + "value": ["#a7c9ff"] + }, + "borderWidth": 5, + "cornerRadius": 17 + }, + "button": { + "background": { + "color": { + "type": "gradient", + "value": ["#1bdce7", "#e7857f"] + }, + "border": { + "type": "fill", + "value": ["#e050ff"] + }, + "borderWidth": 4, + "cornerRadius": 14 + }, + "shadow": { + "color": { + "type": "fill", + "value": ["#9900ff"] + }, + "offset": 1, + "opacity": 2, + "radius": 3 + }, + "text": { + "alignment": "center", + "background": { + "type": "gradient", + "value": ["#cce78f", "#ff3e3f"] + }, + "font": { + "size": 16, + "style": "regular" + }, + "foreground": { + "type": "fill", + "value": ["#47a28f"] + } + }, + "tintColor": { + "type": "fill", + "value": ["#b1b232"] + } + } + }, "galleryCard": { "title": { "foreground": { From 60f34e42ebc1f3e4218503c4eccc9d7ecde2f035 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Tue, 29 Aug 2023 17:16:39 +0300 Subject: [PATCH 60/69] Devs want GVA `postback` buttons response caching for authenticated visitors with chat history MOB 2572 --- .../java/com/glia/widgets/chat/ChatManager.kt | 12 ++++++------ .../widgets/chat/controller/ChatController.kt | 17 ++++++----------- .../chat/domain/AppendNewChatItemUseCase.kt | 9 ++++----- .../chat/domain/GliaSendMessageUseCase.kt | 17 ++++++++++------- .../chat/domain/SendUnsentMessagesUseCase.kt | 19 ++++++++++++++++++- .../com/glia/widgets/chat/model/ChatItems.kt | 8 ++++++++ .../SecureConversationsRepository.kt | 2 +- .../domain/SendMessageButtonStateUseCase.kt | 2 +- .../domain/SendSecureMessageUseCase.kt | 2 +- .../com/glia/widgets/chat/ChatManagerTest.kt | 10 +++++----- .../AppendNewVisitorMessageUseCaseTest.kt | 7 ++++--- 11 files changed, 64 insertions(+), 41 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt index d559dd3b0..46506d84b 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt @@ -21,7 +21,7 @@ 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.VisitorMessageItem +import com.glia.widgets.chat.model.Unsent import com.glia.widgets.core.engagement.domain.IsOngoingEngagementUseCase import com.glia.widgets.core.engagement.domain.model.ChatHistoryResponse import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal @@ -128,7 +128,7 @@ internal class ChatManager constructor( @VisibleForTesting fun checkUnsentMessages(state: State) { - sendUnsentMessagesUseCase(state.unsentItems.firstOrNull()?.message ?: return) {} + sendUnsentMessagesUseCase(state.unsentItems.firstOrNull() ?: return) {} } @VisibleForTesting @@ -256,11 +256,11 @@ internal class ChatManager constructor( } @VisibleForTesting - fun addUnsentMessage(message: VisitorMessageItem.Unsent, state: State): State { + fun addUnsentMessage(message: Unsent, state: State): State { state.unsentItems += message return state.apply { val index = if (chatItems.lastOrNull() is OperatorStatusItem.InQueue) chatItems.lastIndex else chatItems.lastIndex + 1 - chatItems.add(index, message) + chatItems.add(index, message.chatMessage) } } @@ -329,7 +329,7 @@ internal class ChatManager constructor( internal data class State( val chatItems: MutableList = mutableListOf(), val chatItemIds: MutableSet = mutableSetOf(), - val unsentItems: MutableList = mutableListOf(), + val unsentItems: MutableList = mutableListOf(), var lastMessageWithVisibleOperatorImage: OperatorChatItem? = null, var operatorStatusItem: OperatorStatusItem? = null, var mediaUpgradeTimerItem: MediaUpgradeStartedTimerItem? = null, @@ -354,7 +354,7 @@ internal class ChatManager constructor( 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: VisitorMessageItem.Unsent) : 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 diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index fd7fc7df2..44199533d 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -38,7 +38,7 @@ import com.glia.widgets.chat.model.Gva import com.glia.widgets.chat.model.GvaButton import com.glia.widgets.chat.model.OperatorMessageItem import com.glia.widgets.chat.model.OperatorStatusItem -import com.glia.widgets.chat.model.VisitorMessageItem +import com.glia.widgets.chat.model.Unsent import com.glia.widgets.core.callvisualizer.domain.IsCallVisualizerUseCase import com.glia.widgets.core.chathead.domain.HasPendingSurveyUseCase import com.glia.widgets.core.chathead.domain.SetPendingSurveyUsedUseCase @@ -168,7 +168,7 @@ internal class ChatController( } } - override fun errorOperatorNotOnline(message: String) { + override fun errorOperatorNotOnline(message: Unsent) { onSendMessageOperatorOffline(message) } @@ -347,22 +347,17 @@ internal class ChatController( error(exception) } - private fun onSendMessageOperatorOffline(message: String) { + private fun onSendMessageOperatorOffline(message: Unsent) { appendUnsentMessage(message) if (!chatState.engagementRequested) { queueForEngagement() } } - private fun appendUnsentMessage(message: String) { + private fun appendUnsentMessage(message: Unsent) { Logger.d(TAG, "appendUnsentMessage: $message") - chatManager.onChatAction( - ChatManager.Action.UnsentMessageReceived( - VisitorMessageItem.Unsent( - message = message - ) - ) - ) + chatManager.onChatAction(ChatManager.Action.UnsentMessageReceived(message)) + scrollChatToBottom() } private fun onOperatorTyping(isOperatorTyping: Boolean) { diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt index fcae94743..a2eaa10a2 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt @@ -138,19 +138,18 @@ internal class AppendNewVisitorMessageUseCase( fun addUnsentItem(state: ChatManager.State, message: VisitorMessage): Boolean { if (state.unsentItems.isEmpty()) return false - val unsentMessage = - state.unsentItems.firstOrNull { it.message == message.content } ?: return false + val unsentMessage = state.unsentItems.firstOrNull { it.content == message.content } ?: return false state.unsentItems.remove(unsentMessage) - val index = state.chatItems.indexOf(unsentMessage) + val index = state.chatItems.indexOf(unsentMessage.chatMessage) if (index != -1) { if (lastDeliveredItem != null) { val lastDeliveredIndex = state.chatItems.indexOf(lastDeliveredItem!!) state.chatItems[lastDeliveredIndex] = lastDeliveredItem!!.withDeliveredStatus(false) } - state.chatItems[index] = unsentMessage.run { - lastDeliveredItem = VisitorMessageItem.Delivered(message.id, timestamp, this.message) + state.chatItems[index] = message.run { + lastDeliveredItem = VisitorMessageItem.Delivered(id, timestamp, content) lastDeliveredItem!! } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt index 65b9101aa..a5c1f8b5f 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/GliaSendMessageUseCase.kt @@ -5,6 +5,7 @@ import com.glia.androidsdk.chat.FilesAttachment import com.glia.androidsdk.chat.SingleChoiceAttachment import com.glia.androidsdk.chat.VisitorMessage import com.glia.widgets.chat.data.GliaChatRepository +import com.glia.widgets.chat.model.Unsent import com.glia.widgets.core.engagement.GliaEngagementConfigRepository import com.glia.widgets.core.engagement.GliaEngagementStateRepository import com.glia.widgets.core.fileupload.FileAttachmentRepository @@ -12,7 +13,7 @@ import com.glia.widgets.core.fileupload.model.FileAttachment import com.glia.widgets.core.secureconversations.SecureConversationsRepository import com.glia.widgets.core.secureconversations.domain.IsSecureEngagementUseCase -class GliaSendMessageUseCase( +internal class GliaSendMessageUseCase( private val chatRepository: GliaChatRepository, private val fileAttachmentRepository: FileAttachmentRepository, private val engagementStateRepository: GliaEngagementStateRepository, @@ -23,7 +24,7 @@ class GliaSendMessageUseCase( interface Listener { fun messageSent(message: VisitorMessage?) fun onMessageValidated() - fun errorOperatorNotOnline(message: String) + fun errorOperatorNotOnline(message: Unsent) fun error(ex: GliaException) fun errorMessageInvalid() { @@ -79,7 +80,7 @@ class GliaSendMessageUseCase( sendMessage(message, listener) } } else { - listener.errorOperatorNotOnline(message) + listener.errorOperatorNotOnline(Unsent.Message(message)) } } else { listener.errorMessageInvalid() @@ -87,12 +88,14 @@ class GliaSendMessageUseCase( } fun execute(singleChoiceAttachment: SingleChoiceAttachment, listener: Listener) { - if (isSecureEngagement) { - singleChoiceAttachment.apply { + when { + isSecureEngagement -> singleChoiceAttachment.apply { secureConversationsRepository.send(selectedOptionText, engagementConfigRepository.queueIds, singleChoiceAttachment, listener) } - } else { - chatRepository.sendMessageSingleChoice(singleChoiceAttachment, listener) + + isOperatorOnline -> chatRepository.sendMessageSingleChoice(singleChoiceAttachment, listener) + + else -> listener.errorOperatorNotOnline(Unsent.Attachment(singleChoiceAttachment)) } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt index 08fbb189a..0a15a3aa7 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt @@ -1,10 +1,27 @@ package com.glia.widgets.chat.domain +import com.glia.androidsdk.chat.SingleChoiceAttachment import com.glia.widgets.chat.data.GliaChatRepository +import com.glia.widgets.chat.model.Unsent internal class SendUnsentMessagesUseCase(private val chatRepository: GliaChatRepository) { - operator fun invoke(message: String, onSuccess: () -> Unit) { + operator fun invoke(message: Unsent, onSuccess: () -> Unit) { + when (message) { + is Unsent.Attachment -> sendAttachment(message.attachment, onSuccess) + is Unsent.Message -> sendMessage(message.message, onSuccess) + } + } + + private fun sendAttachment(attachment: SingleChoiceAttachment, onSuccess: () -> Unit) { + chatRepository.sendResponse(attachment) { response, exception -> + if (exception == null && response != null) { + onSuccess() + } + } + } + + private fun sendMessage(message: String, onSuccess: () -> Unit) { chatRepository.sendMessage(message) { response, exception -> if (exception == null && response != null) { onSuccess() diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt index 998261fc1..8a1053cba 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt @@ -4,6 +4,7 @@ import android.content.Context import android.text.format.DateUtils import com.glia.androidsdk.chat.AttachmentFile import com.glia.androidsdk.chat.ChatMessage +import com.glia.androidsdk.chat.SingleChoiceAttachment import com.glia.androidsdk.chat.SingleChoiceOption import com.glia.widgets.chat.adapter.ChatAdapter import com.glia.widgets.helper.isDownloaded @@ -265,3 +266,10 @@ internal sealed class VisitorMessageItem : VisitorChatItem(ChatAdapter.VISITOR_M override val message: String ) : VisitorMessageItem() } + +internal sealed class Unsent(val content: String) { + val chatMessage: VisitorMessageItem.Unsent = VisitorMessageItem.Unsent(message = content) + + data class Message(val message: String) : Unsent(message) + data class Attachment(val attachment: SingleChoiceAttachment) : Unsent(attachment.selectedOptionText) +} diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/SecureConversationsRepository.kt b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/SecureConversationsRepository.kt index 3af8947c8..b7b7e3625 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/SecureConversationsRepository.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/SecureConversationsRepository.kt @@ -11,7 +11,7 @@ import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.Subject -class SecureConversationsRepository(private val secureConversations: SecureConversations) { +internal class SecureConversationsRepository(private val secureConversations: SecureConversations) { private val _messageSendingObservable: Subject = BehaviorSubject.createDefault(false) val messageSendingObservable: Observable = _messageSendingObservable diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendMessageButtonStateUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendMessageButtonStateUseCase.kt index 901b2df52..d2486a86b 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendMessageButtonStateUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendMessageButtonStateUseCase.kt @@ -7,7 +7,7 @@ import com.glia.widgets.helper.rx.Schedulers import com.glia.widgets.messagecenter.MessageCenterState import io.reactivex.Observable -class SendMessageButtonStateUseCase( +internal class SendMessageButtonStateUseCase( private val sendMessageRepository: SendMessageRepository, private val fileAttachmentRepository: SecureFileAttachmentRepository, private val secureConversationsRepository: SecureConversationsRepository, diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendSecureMessageUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendSecureMessageUseCase.kt index 1d66dba94..b4e8477d7 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendSecureMessageUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SendSecureMessageUseCase.kt @@ -10,7 +10,7 @@ import com.glia.widgets.core.fileupload.model.FileAttachment import com.glia.widgets.core.secureconversations.SecureConversationsRepository import com.glia.widgets.core.secureconversations.SendMessageRepository -class SendSecureMessageUseCase( +internal class SendSecureMessageUseCase( private val queueId: String, private val sendMessageRepository: SendMessageRepository, private val secureConversationsRepository: SecureConversationsRepository, diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt index 5a5254b6f..b42891b8f 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt @@ -19,7 +19,7 @@ import com.glia.widgets.chat.model.MediaUpgradeStartedTimerItem import com.glia.widgets.chat.model.NewMessagesDividerItem import com.glia.widgets.chat.model.OperatorMessageItem import com.glia.widgets.chat.model.OperatorStatusItem -import com.glia.widgets.chat.model.VisitorMessageItem +import com.glia.widgets.chat.model.Unsent import com.glia.widgets.core.engagement.domain.IsOngoingEngagementUseCase import com.glia.widgets.core.engagement.domain.model.ChatHistoryResponse import com.glia.widgets.core.engagement.domain.model.ChatMessageInternal @@ -194,7 +194,7 @@ class ChatManagerTest { @Test fun `addUnsentMessage adds Unsent message before OperatorStatusItem when chatItems contain OperatorStatusItem_InQueue`() { - val message: VisitorMessageItem.Unsent = VisitorMessageItem.Unsent(id = "id", message = "message") + val message: Unsent.Message = Unsent.Message(message = "message") val inQueue = OperatorStatusItem.InQueue("company_name") @@ -206,14 +206,14 @@ class ChatManagerTest { assertTrue(newState.unsentItems.count() == 1) assertTrue(newState.chatItems.count() == 1) - assertEquals(newState.unsentItems.last(), newState.chatItems.last()) + assertEquals(newState.unsentItems.last().chatMessage, newState.chatItems.last()) newState.chatItems.add(inQueue) subjectUnderTest.addUnsentMessage(message, state).apply { assertTrue(unsentItems.count() == 2) assertTrue(chatItems.count() == 3) - assertEquals(unsentItems.last(), chatItems[1]) + assertEquals(unsentItems.last().chatMessage, chatItems[1]) assertEquals(chatItems.last(), inQueue) } } @@ -482,7 +482,7 @@ class ChatManagerTest { @Test fun `checkUnsentMessages calls sendUnsentMessagesUseCase when unsent messages list is not empty`() { - val mockUnsentMessage: VisitorMessageItem.Unsent = mock { + val mockUnsentMessage: Unsent.Message = mock { on { message } doReturn "message" } state.unsentItems.add(mockUnsentMessage) diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewVisitorMessageUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewVisitorMessageUseCaseTest.kt index 9663eac14..4f9b3c4a4 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewVisitorMessageUseCaseTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewVisitorMessageUseCaseTest.kt @@ -4,6 +4,7 @@ import com.glia.androidsdk.chat.AttachmentFile import com.glia.androidsdk.chat.FilesAttachment import com.glia.androidsdk.chat.VisitorMessage import com.glia.widgets.chat.ChatManager +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.VisitorMessageItem @@ -58,7 +59,7 @@ class AppendNewVisitorMessageUseCaseTest { val content = "content" whenever(visitorMessage.content) doReturn content - state.unsentItems.add(VisitorMessageItem.Unsent(message = content)) + state.unsentItems.add(Unsent.Message(message = content)) assertFalse(useCase.addUnsentItem(state, visitorMessage)) } @@ -74,9 +75,9 @@ class AppendNewVisitorMessageUseCaseTest { whenever(visitorMessage.id) doReturn "id" whenever(visitorMessage.timestamp) doReturn 1 - val unsentMessage = VisitorMessageItem.Unsent(message = content) + val unsentMessage = Unsent.Message(message = content) state.unsentItems.add(unsentMessage) - state.chatItems.add(unsentMessage) + state.chatItems.add(unsentMessage.chatMessage) assertTrue(useCase.addUnsentItem(state, visitorMessage)) assertTrue(state.unsentItems.isEmpty()) From 7188dd4b7f6eaaf610cb4f55261eb58bde4c9e06 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Tue, 29 Aug 2023 18:52:55 +0300 Subject: [PATCH 61/69] Show Quick Replies for all cases when the QR is the latest in history MOB 2575 --- .../java/com/glia/widgets/chat/ChatManager.kt | 5 +---- .../com/glia/widgets/di/ManagerFactory.kt | 1 - .../com/glia/widgets/chat/ChatManagerTest.kt | 21 ------------------- 3 files changed, 1 insertion(+), 26 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt index 46506d84b..2a4109947 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt @@ -22,7 +22,6 @@ 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.core.engagement.domain.IsOngoingEngagementUseCase 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 @@ -46,7 +45,6 @@ internal class ChatManager constructor( private val appendNewChatMessageUseCase: AppendNewChatMessageUseCase, private val sendUnsentMessagesUseCase: SendUnsentMessagesUseCase, private val handleCustomCardClickUseCase: HandleCustomCardClickUseCase, - private val isOngoingEngagementUseCase: IsOngoingEngagementUseCase, private val isAuthenticatedUseCase: IsAuthenticatedUseCase, private val compositeDisposable: CompositeDisposable = CompositeDisposable(), private val state: BehaviorProcessor = BehaviorProcessor.create(), @@ -107,8 +105,7 @@ internal class ChatManager constructor( @VisibleForTesting fun updateQuickReplies(state: State) { - state.takeIf { isOngoingEngagementUseCase() } - ?.run { chatItems.lastOrNull() as? GvaQuickReplies } + state.run { chatItems.lastOrNull() as? GvaQuickReplies } ?.run { options } .orEmpty() .also(quickReplies::onNext) diff --git a/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt b/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt index 4a9ebc6ee..808d8e161 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/di/ManagerFactory.kt @@ -14,7 +14,6 @@ internal class ManagerFactory(private val useCaseFactory: UseCaseFactory) { appendNewChatMessageUseCase = createAppendNewChatMessageUseCase(), sendUnsentMessagesUseCase = createSendUnsentMessagesUseCase(), handleCustomCardClickUseCase = createHandleCustomCardClickUseCase(), - isOngoingEngagementUseCase = createIsOngoingEngagementUseCase(), isAuthenticatedUseCase = createIsAuthenticatedUseCase() ) } diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt index b42891b8f..c04a66b10 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt @@ -20,7 +20,6 @@ import com.glia.widgets.chat.model.NewMessagesDividerItem 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.core.engagement.domain.IsOngoingEngagementUseCase 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 @@ -64,7 +63,6 @@ class ChatManagerTest { private lateinit var appendNewChatMessageUseCase: AppendNewChatMessageUseCase private lateinit var sendUnsentMessagesUseCase: SendUnsentMessagesUseCase private lateinit var handleCustomCardClickUseCase: HandleCustomCardClickUseCase - private lateinit var isOngoingEngagementUseCase: IsOngoingEngagementUseCase private lateinit var isAuthenticatedUseCase: IsAuthenticatedUseCase private lateinit var subjectUnderTest: ChatManager private lateinit var state: ChatManager.State @@ -84,7 +82,6 @@ class ChatManagerTest { appendNewChatMessageUseCase = mock() sendUnsentMessagesUseCase = mock() handleCustomCardClickUseCase = mock() - isOngoingEngagementUseCase = mock() isAuthenticatedUseCase = mock() compositeDisposable = spy() stateProcessor = spy(BehaviorProcessor.create()) @@ -101,7 +98,6 @@ class ChatManagerTest { appendNewChatMessageUseCase, sendUnsentMessagesUseCase, handleCustomCardClickUseCase, - isOngoingEngagementUseCase, isAuthenticatedUseCase, compositeDisposable, stateProcessor, @@ -525,27 +521,10 @@ class ChatManagerTest { } state.chatItems.add(quickReplies) - whenever(isOngoingEngagementUseCase()) doReturn true - subjectUnderTest.updateQuickReplies(state) quickRepliesTest.assertValue(mockOptions) } - @Test - fun `updateQuickReplies triggers quickReplies onNext with empty list when the is no ongoing engagement`() { - val quickRepliesTest = quickReplies.test() - val mockOptions: List = listOf(mock()) - val quickReplies: GvaQuickReplies = mock { - on { options } doReturn mockOptions - } - state.chatItems.add(quickReplies) - - whenever(isOngoingEngagementUseCase()) doReturn false - - subjectUnderTest.updateQuickReplies(state) - quickRepliesTest.assertValue(emptyList()) - } - @Test fun `subscribeToQuickReplies subscribes to quickReplies`() { val testSubscriber = quickReplies.test() From 08548b393d33230a37f19646b55ae220f3c8737b Mon Sep 17 00:00:00 2001 From: BitriseBot Date: Wed, 30 Aug 2023 16:18:18 +0000 Subject: [PATCH 62/69] Increment Core SDK version to 1.0.4 --- version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.properties b/version.properties index c11d1f4e1..0bcc28522 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ -#Fri Aug 11 06:31:54 UTC 2023 -dependency.coreSdk.version=1.0.3 +#Wed Aug 30 16:18:14 UTC 2023 +dependency.coreSdk.version=1.0.4 widgets.versionCode=67 widgets.versionName=2.0.4 From 3dabc62e8f6655826719c1cd67e2e9c01940f4be Mon Sep 17 00:00:00 2001 From: Andrii Horishnii Date: Wed, 30 Aug 2023 17:14:40 +0300 Subject: [PATCH 63/69] Cover Response text with snapshot tests MOB-2601 --- .../holder/GvaResponseTextViewHolder.kt | 7 +- ...extViewHolderSnapshotTest_withChatHead.png | 3 + ...pshotTest_withChatHeadWithGlobalColors.png | 3 + ...erSnapshotTest_withChatHeadWithUiTheme.png | 3 + ...pshotTest_withChatHeadWithUnifiedTheme.png | 3 + ...ViewHolderSnapshotTest_withoutChatHead.png | 3 + ...otTest_withoutChatHeadWithGlobalColors.png | 3 + ...napshotTest_withoutChatHeadWithUiTheme.png | 3 + ...otTest_withoutChatHeadWithUnifiedTheme.png | 3 + ...PersistentButtonsViewHolderSnapshotTest.kt | 2 + .../GvaResponseTextViewHolderSnapshotTest.kt | 139 ++++++++++++++++++ 11 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHead.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHeadWithGlobalColors.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHeadWithUiTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHeadWithUnifiedTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHead.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHeadWithGlobalColors.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHeadWithUiTheme.png create mode 100644 widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHeadWithUnifiedTheme.png create mode 100644 widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolderSnapshotTest.kt diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolder.kt index 918cce035..469b9da79 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolder.kt @@ -5,18 +5,21 @@ import com.glia.widgets.UiTheme import com.glia.widgets.chat.model.GvaResponseText import com.glia.widgets.databinding.ChatOperatorMessageLayoutBinding import com.glia.widgets.databinding.ChatReceiveMessageContentBinding +import com.glia.widgets.di.Dependencies import com.glia.widgets.helper.fromHtml import com.glia.widgets.helper.getColorCompat import com.glia.widgets.helper.getColorStateListCompat import com.glia.widgets.helper.getFontCompat import com.glia.widgets.view.unifiedui.applyLayerTheme import com.glia.widgets.view.unifiedui.applyTextTheme +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme internal class GvaResponseTextViewHolder( operatorMessageBinding: ChatOperatorMessageLayoutBinding, private val messageContentBinding: ChatReceiveMessageContentBinding, - private val uiTheme: UiTheme -) : OperatorBaseViewHolder(operatorMessageBinding.root, operatorMessageBinding.chatHeadView, uiTheme) { + private val uiTheme: UiTheme, + unifiedTheme: UnifiedTheme? = Dependencies.getGliaThemeManager().theme +) : OperatorBaseViewHolder(operatorMessageBinding.root, operatorMessageBinding.chatHeadView, uiTheme, unifiedTheme) { init { setupMessageContentView() diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHead.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHead.png new file mode 100644 index 000000000..239d0b1c0 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHead.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64bb80818a30c995cb26dca3503adc0980c14314b4832f1760e6665aedaf803f +size 77969 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHeadWithGlobalColors.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHeadWithGlobalColors.png new file mode 100644 index 000000000..d834b2cad --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHeadWithGlobalColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d0e5f584796438f33ce56b1517812f46b8a08b34c46dedcf5fa19e8bad1eeb9 +size 69252 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHeadWithUiTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHeadWithUiTheme.png new file mode 100644 index 000000000..fbdf51d00 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHeadWithUiTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6a0e343ed4894b03902daa760e64d570be927bc09108693a7d3e60d05ac12bf +size 76486 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHeadWithUnifiedTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHeadWithUnifiedTheme.png new file mode 100644 index 000000000..f6a670c79 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withChatHeadWithUnifiedTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48a76ed9c06ae5955a719432d0f3e050dc0e12a2c9c30c15b00eb5fecb433855 +size 72695 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHead.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHead.png new file mode 100644 index 000000000..ea6f6f5df --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHead.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:837d81cc9a5d27d08b59dc0b436e8405649dacb4b854a3d6922e18868f94dc33 +size 74942 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHeadWithGlobalColors.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHeadWithGlobalColors.png new file mode 100644 index 000000000..3c12e7646 --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHeadWithGlobalColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:562b3136193a81b2cc0737f3e5e200c575fdf0a4d8398329c65637008cffe767 +size 65418 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHeadWithUiTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHeadWithUiTheme.png new file mode 100644 index 000000000..5cf69d89d --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHeadWithUiTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d36e7677f161d2e030f6ecac01953412653e891802c9f2701e63efa10364dae2 +size 72093 diff --git a/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHeadWithUnifiedTheme.png b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHeadWithUnifiedTheme.png new file mode 100644 index 000000000..43b09b5da --- /dev/null +++ b/widgetssdk/src/test/snapshots/images/com.glia.widgets.chat.adapter.holder_GvaResponseTextViewHolderSnapshotTest_withoutChatHeadWithUnifiedTheme.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08b3e6d568233d4cc8140cd5e2b6c520f023148060df2cda80bbda6eab173b83 +size 69171 diff --git a/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolderSnapshotTest.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolderSnapshotTest.kt index 81f95ccf5..ceb9d2363 100644 --- a/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolderSnapshotTest.kt +++ b/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaPersistentButtonsViewHolderSnapshotTest.kt @@ -130,6 +130,8 @@ class GvaPersistentButtonsViewHolderSnapshotTest : SnapshotTest(), SnapshotGva { ) } + // MARK: utils for tests + private data class ViewData( val chatOperatorMessageLayoutBinding: ChatOperatorMessageLayoutBinding, val gvaPersistentButtonsContentBinding: ChatGvaPersistentButtonsContentBinding, diff --git a/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolderSnapshotTest.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolderSnapshotTest.kt new file mode 100644 index 000000000..a80a59927 --- /dev/null +++ b/widgetssdk/src/testSnapshot/java/com/glia/widgets/chat/adapter/holder/GvaResponseTextViewHolderSnapshotTest.kt @@ -0,0 +1,139 @@ +package com.glia.widgets.chat.adapter.holder + +import com.glia.widgets.SnapshotTest +import com.glia.widgets.UiTheme +import com.glia.widgets.chat.model.GvaResponseText +import com.glia.widgets.databinding.ChatOperatorMessageLayoutBinding +import com.glia.widgets.databinding.ChatReceiveMessageContentBinding +import com.glia.widgets.snapshotutils.SnapshotGva +import com.glia.widgets.view.unifiedui.theme.UnifiedTheme +import org.junit.Test + +class GvaResponseTextViewHolderSnapshotTest : SnapshotTest(), SnapshotGva { + + private fun gvaResponseText(showChatHead: Boolean = false) = GvaResponseText( + content = gvaLongSubtitle(), + showChatHead = showChatHead + ) + + // MARK: tests with all views + + @Test + fun withoutChatHead() { + snapshot( + setupView( + gvaResponseText() + ).viewHolder.itemView + ) + } + + @Test + fun withoutChatHeadWithUiTheme() { + snapshot( + setupView( + gvaResponseText(), + uiTheme = uiTheme() + ).viewHolder.itemView + ) + } + + @Test + fun withoutChatHeadWithGlobalColors() { + snapshot( + setupView( + gvaResponseText(), + unifiedTheme = unifiedThemeWithGlobalColors() + ).viewHolder.itemView + ) + } + + @Test + fun withoutChatHeadWithUnifiedTheme() { + snapshot( + setupView( + gvaResponseText(), + unifiedTheme = unifiedTheme() + ).viewHolder.itemView + ) + } + + // MARK: tests with long content + + @Test + fun withChatHead() { + snapshot( + setupView( + gvaResponseText( + showChatHead = true + ), + ).viewHolder.itemView + ) + } + + @Test + fun withChatHeadWithUiTheme() { + snapshot( + setupView( + gvaResponseText( + showChatHead = true + ), + uiTheme = uiTheme() + ).viewHolder.itemView + ) + } + + @Test + fun withChatHeadWithGlobalColors() { + snapshot( + setupView( + gvaResponseText( + showChatHead = true + ), + unifiedTheme = unifiedThemeWithGlobalColors() + ).viewHolder.itemView + ) + } + + @Test + fun withChatHeadWithUnifiedTheme() { + snapshot( + setupView( + gvaResponseText( + showChatHead = true + ), + unifiedTheme = unifiedTheme() + ).viewHolder.itemView + ) + } + + // MARK: utils for tests + + private data class ViewData( + val chatOperatorMessageLayoutBinding: ChatOperatorMessageLayoutBinding, + val gvaPersistentButtonsContentBinding: ChatReceiveMessageContentBinding, + val viewHolder: GvaResponseTextViewHolder + ) + + private fun setupView( + card: GvaResponseText, + unifiedTheme: UnifiedTheme? = null, + uiTheme: UiTheme = UiTheme() + ): ViewData { + val chatOperatorMessageLayoutBinding = ChatOperatorMessageLayoutBinding.inflate(layoutInflater) + val gvaPersistentButtonsContentBinding = ChatReceiveMessageContentBinding.inflate( + layoutInflater, + chatOperatorMessageLayoutBinding.contentLayout, + true + ) + val viewHolder = GvaResponseTextViewHolder( + chatOperatorMessageLayoutBinding, + gvaPersistentButtonsContentBinding, + uiTheme, + unifiedTheme + ) + + viewHolder.bind(card) + + return ViewData(chatOperatorMessageLayoutBinding, gvaPersistentButtonsContentBinding, viewHolder) + } +} From c327f198dfa8ad0ded10d8907b7d6f37a7f48c26 Mon Sep 17 00:00:00 2001 From: BitriseBot Date: Wed, 30 Aug 2023 19:18:18 +0300 Subject: [PATCH 64/69] Increment project version to 2.0.5 --- version.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.properties b/version.properties index 0bcc28522..6198468be 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ -#Wed Aug 30 16:18:14 UTC 2023 +#Thu Aug 31 09:32:00 UTC 2023 dependency.coreSdk.version=1.0.4 -widgets.versionCode=67 -widgets.versionName=2.0.4 +widgets.versionCode=68 +widgets.versionName=2.0.5 \ No newline at end of file From a59b64a66aade0b49748de03533b2b3dac4596b5 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Thu, 31 Aug 2023 17:44:07 +0300 Subject: [PATCH 65/69] Improve chat flow to handle sent message responses - Add functions to handle send message response - Remove redundant sealed classes for Visitor message - Optimize ChatAdapter for delivered status update MOB 2603 --- .../java/com/glia/widgets/chat/ChatManager.kt | 8 +++- .../glia/widgets/chat/adapter/ChatAdapter.kt | 25 +++++++++++- .../chat/adapter/ChatAdapterDiffCallback.kt | 2 + .../holder/VisitorMessageViewHolder.kt | 4 ++ .../VisitorFileAttachmentViewHolder.java | 4 ++ .../VisitorImageAttachmentViewHolder.java | 18 +++++---- .../widgets/chat/controller/ChatController.kt | 2 + .../domain/AppendHistoryChatItemUseCases.kt | 4 +- .../chat/domain/AppendNewChatItemUseCase.kt | 8 +--- .../chat/domain/SendUnsentMessagesUseCase.kt | 11 ++--- .../com/glia/widgets/chat/model/ChatItems.kt | 40 +++++-------------- .../com/glia/widgets/chat/ChatManagerTest.kt | 17 +++++++- .../AppendHistoryCustomCardItemUseCaseTest.kt | 2 +- ...AppendHistoryVisitorChatItemUseCaseTest.kt | 6 +-- .../AppendNewOperatorMessageUseCaseTest.kt | 2 +- .../AppendNewVisitorMessageUseCaseTest.kt | 10 ++--- 16 files changed, 98 insertions(+), 65 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt index 2a4109947..252b7cda7 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt @@ -125,7 +125,9 @@ internal class ChatManager constructor( @VisibleForTesting fun checkUnsentMessages(state: State) { - sendUnsentMessagesUseCase(state.unsentItems.firstOrNull() ?: return) {} + sendUnsentMessagesUseCase(state.unsentItems.firstOrNull() ?: return) { + onChatAction(Action.MessageSent(it)) + } } @VisibleForTesting @@ -187,9 +189,12 @@ internal class ChatManager constructor( is Action.OnMediaUpgradeTimerUpdated -> mapMediaUpgradeTimerUpdated(action.formattedValue, state) is Action.CustomCardClicked -> mapCustomCardClicked(action, state) Action.ChatRestored -> state + is Action.MessageSent -> mapMessageSent(action.message, state) } } + @VisibleForTesting + fun mapMessageSent(message: VisitorMessage, state: State): State = mapNewMessage(ChatMessageInternal(message), state) @VisibleForTesting fun mapCustomCardClicked(action: Action.CustomCardClicked, state: State): State = action.run { @@ -359,5 +364,6 @@ internal class ChatManager constructor( object OnMediaUpgradeCanceled : Action data class CustomCardClicked(val customCard: CustomCardChatItem, val attachment: SingleChoiceAttachment) : Action object ChatRestored : Action + data class MessageSent(val message: VisitorMessage) : Action } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt index 0894b97c0..d8ea77c94 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapter.kt @@ -215,8 +215,11 @@ internal class ChatAdapter( } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList) { - val isHandled: Boolean = when (val item: ChatItem = differ.currentList[position]) { - is MediaUpgradeStartedTimerItem -> updateMediaUpgradeTimer(payloads, holder as MediaUpgradeStartedViewHolder) + val isHandled: Boolean = when (holder) { + is MediaUpgradeStartedViewHolder -> updateMediaUpgradeTimer(payloads, holder) + is VisitorMessageViewHolder -> updateDeliveredState(payloads, holder) + is VisitorFileAttachmentViewHolder -> updateDeliveredState(payloads, holder) + is VisitorImageAttachmentViewHolder -> updateDeliveredState(payloads, holder) else -> false } @@ -225,6 +228,24 @@ internal class ChatAdapter( } } + private fun updateDeliveredState(payloads: MutableList, holder: VisitorMessageViewHolder): Boolean = + payloads.lastOrNull { it is Boolean }?.let { + holder.updateDelivered(it as Boolean) + true + } ?: false + + private fun updateDeliveredState(payloads: MutableList, holder: VisitorFileAttachmentViewHolder): Boolean = + payloads.lastOrNull { it is Boolean }?.let { + holder.updateDelivered(it as Boolean) + true + } ?: false + + private fun updateDeliveredState(payloads: MutableList, holder: VisitorImageAttachmentViewHolder): Boolean = + payloads.lastOrNull { it is Boolean }?.let { + holder.updateDelivered(it as Boolean) + true + } ?: false + private fun updateMediaUpgradeTimer(payloads: MutableList, viewHolder: MediaUpgradeStartedViewHolder): Boolean = payloads.run { firstOrNull() as? String }?.let { diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapterDiffCallback.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapterDiffCallback.kt index 8aa27a8d0..b9fd760ce 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapterDiffCallback.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/ChatAdapterDiffCallback.kt @@ -3,6 +3,7 @@ package com.glia.widgets.chat.adapter import androidx.recyclerview.widget.DiffUtil import com.glia.widgets.chat.model.ChatItem import com.glia.widgets.chat.model.MediaUpgradeStartedTimerItem +import com.glia.widgets.chat.model.VisitorChatItem internal class ChatAdapterDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: ChatItem, newItem: ChatItem): Boolean = oldItem.id == newItem.id @@ -11,6 +12,7 @@ internal class ChatAdapterDiffCallback : DiffUtil.ItemCallback() { override fun getChangePayload(oldItem: ChatItem, newItem: ChatItem): Any? = when { oldItem is MediaUpgradeStartedTimerItem.Audio && newItem is MediaUpgradeStartedTimerItem.Audio -> newItem.time oldItem is MediaUpgradeStartedTimerItem.Video && newItem is MediaUpgradeStartedTimerItem.Video -> newItem.time + oldItem is VisitorChatItem && newItem is VisitorChatItem -> newItem.showDelivered else -> null } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/VisitorMessageViewHolder.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/VisitorMessageViewHolder.kt index 484a62e82..2b1e81d1b 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/VisitorMessageViewHolder.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/VisitorMessageViewHolder.kt @@ -57,4 +57,8 @@ internal class VisitorMessageViewHolder( ) itemView.contentDescription = contentDescription } + + fun updateDelivered(delivered: Boolean) { + binding.deliveredView.isVisible = delivered + } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/VisitorFileAttachmentViewHolder.java b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/VisitorFileAttachmentViewHolder.java index f9b416cfb..50dd6ab50 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/VisitorFileAttachmentViewHolder.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/fileattachment/VisitorFileAttachmentViewHolder.java @@ -70,4 +70,8 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo } }); } + + public void updateDelivered(boolean delivered) { + deliveredView.setVisibility(delivered ? View.VISIBLE : View.GONE); + } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/VisitorImageAttachmentViewHolder.java b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/VisitorImageAttachmentViewHolder.java index fe73bff6a..298d33c80 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/VisitorImageAttachmentViewHolder.java +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/adapter/holder/imageattachment/VisitorImageAttachmentViewHolder.java @@ -20,11 +20,11 @@ public class VisitorImageAttachmentViewHolder extends ImageAttachmentViewHolder private final TextView deliveredView; public VisitorImageAttachmentViewHolder( - @NonNull View itemView, - UiTheme uiTheme, - GetImageFileFromCacheUseCase getImageFileFromCacheUseCase, - GetImageFileFromDownloadsUseCase getImageFileFromDownloadsUseCase, - GetImageFileFromNetworkUseCase getImageFileFromNetworkUseCase + @NonNull View itemView, + UiTheme uiTheme, + GetImageFileFromCacheUseCase getImageFileFromCacheUseCase, + GetImageFileFromDownloadsUseCase getImageFileFromDownloadsUseCase, + GetImageFileFromNetworkUseCase getImageFileFromNetworkUseCase ) { super(itemView, getImageFileFromCacheUseCase, getImageFileFromDownloadsUseCase, getImageFileFromNetworkUseCase); deliveredView = itemView.findViewById(R.id.delivered_view); @@ -41,10 +41,10 @@ public void bind(AttachmentFile attachmentFile, boolean showDelivered) { private void setAccessibilityLabels(boolean showDelivered) { if (showDelivered) { itemView.setContentDescription(itemView.getResources().getString( - R.string.glia_chat_visitor_image_delivered_content_description)); + R.string.glia_chat_visitor_image_delivered_content_description)); } else { itemView.setContentDescription(itemView.getResources().getString( - R.string.glia_chat_visitor_image_content_description)); + R.string.glia_chat_visitor_image_content_description)); } } @@ -59,4 +59,8 @@ private void setupDeliveredView(Context context, UiTheme uiTheme) { } deliveredView.setTextColor(ContextCompat.getColor(context, uiTheme.getBaseNormalColor())); } + + public void updateDelivered(boolean delivered) { + deliveredView.setVisibility(delivered ? View.VISIBLE : View.GONE); + } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt index 44199533d..4c1d9ab9b 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/controller/ChatController.kt @@ -158,6 +158,7 @@ internal class ChatController( object : GliaSendMessageUseCase.Listener { override fun messageSent(message: VisitorMessage?) { Logger.d(TAG, "messageSent: $message, id: ${message?.id}") + message?.also { chatManager.onChatAction(ChatManager.Action.MessageSent(it)) } scrollChatToBottom() } @@ -519,6 +520,7 @@ internal class ChatController( dialogController.dismissDialogs() } } + EngagementStateEvent.Type.NO_ENGAGEMENT -> { Logger.d(TAG, "NoEngagement") } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendHistoryChatItemUseCases.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendHistoryChatItemUseCases.kt index a9b9ab8c2..c4d5969b2 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendHistoryChatItemUseCases.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendHistoryChatItemUseCases.kt @@ -98,7 +98,7 @@ internal class AppendHistoryVisitorChatItemUseCase( } if (content.isNotBlank()) { - chatItems += VisitorMessageItem.History(id, timestamp, content) + chatItems += VisitorMessageItem(content, id, timestamp) } } @@ -131,7 +131,7 @@ internal class AppendHistoryCustomCardItemUseCase( message.attachment?.asSingleChoice()?.selectedOptionText?.takeIf { it.isNotBlank() }?.let { - VisitorMessageItem.History(message.id, message.timestamp, it) + VisitorMessageItem(it, message.id, message.timestamp) }?.also { chatItems.add(it) } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt index a2eaa10a2..809122cfa 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/AppendNewChatItemUseCase.kt @@ -149,7 +149,7 @@ internal class AppendNewVisitorMessageUseCase( } state.chatItems[index] = message.run { - lastDeliveredItem = VisitorMessageItem.Delivered(id, timestamp, content) + lastDeliveredItem = VisitorMessageItem(content, id, timestamp, true) lastDeliveredItem!! } @@ -168,11 +168,7 @@ internal class AppendNewVisitorMessageUseCase( val hasFiles = !files.isNullOrEmpty() if (content.isNotBlank()) { - if (hasFiles) { - state.chatItems += VisitorMessageItem.New(id, timestamp, content) - } else { - state.chatItems += VisitorMessageItem.Delivered(id, timestamp, content) - } + state.chatItems += VisitorMessageItem(content, id, timestamp, !hasFiles) } files?.forEachIndexed { index, attachmentFile -> diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt index 0a15a3aa7..ad159c6f6 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/domain/SendUnsentMessagesUseCase.kt @@ -1,30 +1,31 @@ package com.glia.widgets.chat.domain import com.glia.androidsdk.chat.SingleChoiceAttachment +import com.glia.androidsdk.chat.VisitorMessage import com.glia.widgets.chat.data.GliaChatRepository import com.glia.widgets.chat.model.Unsent internal class SendUnsentMessagesUseCase(private val chatRepository: GliaChatRepository) { - operator fun invoke(message: Unsent, onSuccess: () -> Unit) { + operator fun invoke(message: Unsent, onSuccess: (VisitorMessage) -> Unit) { when (message) { is Unsent.Attachment -> sendAttachment(message.attachment, onSuccess) is Unsent.Message -> sendMessage(message.message, onSuccess) } } - private fun sendAttachment(attachment: SingleChoiceAttachment, onSuccess: () -> Unit) { + private fun sendAttachment(attachment: SingleChoiceAttachment, onSuccess: (VisitorMessage) -> Unit) { chatRepository.sendResponse(attachment) { response, exception -> if (exception == null && response != null) { - onSuccess() + onSuccess(response) } } } - private fun sendMessage(message: String, onSuccess: () -> Unit) { + private fun sendMessage(message: String, onSuccess: (VisitorMessage) -> Unit) { chatRepository.sendMessage(message) { response, exception -> if (exception == null && response != null) { - onSuccess() + onSuccess(response) } } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt index 8a1053cba..b87b21a87 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/model/ChatItems.kt @@ -8,6 +8,7 @@ import com.glia.androidsdk.chat.SingleChoiceAttachment import com.glia.androidsdk.chat.SingleChoiceOption import com.glia.widgets.chat.adapter.ChatAdapter import com.glia.widgets.helper.isDownloaded +import java.util.UUID internal abstract class ChatItem(@ChatAdapter.Type val viewType: Int) { abstract val id: String @@ -231,44 +232,21 @@ internal sealed class VisitorAttachmentItem(@ChatAdapter.Type viewType: Int) : V } } -internal sealed class VisitorMessageItem : VisitorChatItem(ChatAdapter.VISITOR_MESSAGE_TYPE) { - override val showDelivered: Boolean - get() = this is Delivered - - abstract val message: String +internal data class VisitorMessageItem( + val message: String, + override val id: String = UUID.randomUUID().toString(), + override val timestamp: Long = System.currentTimeMillis(), + override val showDelivered: Boolean = false +) : VisitorChatItem(ChatAdapter.VISITOR_MESSAGE_TYPE) { override fun withDeliveredStatus(delivered: Boolean): VisitorChatItem { check(!delivered) { "The method should be called only with false value, to hide delivered status" } - return New(id, timestamp, message) + return copy(showDelivered = delivered) } - - data class New( - override val id: String, - override val timestamp: Long, - override val message: String - ) : VisitorMessageItem() - - data class History( - override val id: String, - override val timestamp: Long, - override val message: String - ) : VisitorMessageItem() - - data class Unsent( - override val id: String = "", - override val timestamp: Long = System.currentTimeMillis(), - override val message: String - ) : VisitorMessageItem() - - data class Delivered( - override val id: String, - override val timestamp: Long, - override val message: String - ) : VisitorMessageItem() } internal sealed class Unsent(val content: String) { - val chatMessage: VisitorMessageItem.Unsent = VisitorMessageItem.Unsent(message = content) + val chatMessage: VisitorMessageItem = VisitorMessageItem(message = content) data class Message(val message: String) : Unsent(message) data class Attachment(val attachment: SingleChoiceAttachment) : Unsent(attachment.selectedOptionText) diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt index c04a66b10..d0ac5bc96 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt @@ -296,6 +296,16 @@ class ChatManagerTest { } } + @Test + fun `mapMessageSent adds new Message`() { + val action: ChatManager.Action.MessageSent = mock { + on { message } doReturn mock() + } + + subjectUnderTest.mapMessageSent(action.message, state) + verify(subjectUnderTest).mapNewMessage(any(), eq(state)) + } + @Test fun `mapCustomCardClicked updates Custom Card`() { val action: ChatManager.Action.CustomCardClicked = mock { @@ -481,11 +491,16 @@ class ChatManagerTest { val mockUnsentMessage: Unsent.Message = mock { on { message } doReturn "message" } + + whenever(sendUnsentMessagesUseCase(any(), any())).thenAnswer { + (it.getArgument(1) as ((VisitorMessage) -> Unit)).invoke(mock()) + } + state.unsentItems.add(mockUnsentMessage) subjectUnderTest.checkUnsentMessages(state) - verify(sendUnsentMessagesUseCase).invoke(any(), any()) + verify(subjectUnderTest).onChatAction(any()) } @Test diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryCustomCardItemUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryCustomCardItemUseCaseTest.kt index 410872d04..12aa40213 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryCustomCardItemUseCaseTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryCustomCardItemUseCaseTest.kt @@ -63,6 +63,6 @@ class AppendHistoryCustomCardItemUseCaseTest { assertTrue(items.isNotEmpty()) assertTrue(items.first() is CustomCardChatItem) - assertTrue(items[1] is VisitorMessageItem.History) + assertTrue(items[1] is VisitorMessageItem) } } diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryVisitorChatItemUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryVisitorChatItemUseCaseTest.kt index 9b75b78c3..6f2182ba9 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryVisitorChatItemUseCaseTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendHistoryVisitorChatItemUseCaseTest.kt @@ -45,7 +45,7 @@ class AppendHistoryVisitorChatItemUseCaseTest { useCase(items, visitorMessage) assertTrue(items.count() == 1) - assertTrue(items.last() is VisitorMessageItem.History) + assertTrue(items.last() is VisitorMessageItem) } @Test @@ -75,7 +75,7 @@ class AppendHistoryVisitorChatItemUseCaseTest { assertTrue(items.count() == 2) assertTrue(items.first() is VisitorAttachmentItem.File) - assertTrue(items[1] is VisitorMessageItem.History) + assertTrue(items[1] is VisitorMessageItem) } @Test @@ -101,6 +101,6 @@ class AppendHistoryVisitorChatItemUseCaseTest { assertTrue(items.count() == 3) assertTrue(items.first() is VisitorAttachmentItem.File) assertTrue(items[1] is VisitorAttachmentItem.Image) - assertTrue(items[2] is VisitorMessageItem.History) + assertTrue(items[2] is VisitorMessageItem) } } diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewOperatorMessageUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewOperatorMessageUseCaseTest.kt index 1129d222c..68b976f2c 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewOperatorMessageUseCaseTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewOperatorMessageUseCaseTest.kt @@ -102,7 +102,7 @@ class AppendNewOperatorMessageUseCaseTest { whenever(isGvaUseCase(any())) doReturn false doAnswer { state.chatItems.add(mock()) - state.chatItems.add(mock()) + state.chatItems.add(mock()) }.whenever(appendNewResponseCardOrTextItemUseCase).invoke(any(), any()) useCase(state, chatMessageInternal) diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewVisitorMessageUseCaseTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewVisitorMessageUseCaseTest.kt index 4f9b3c4a4..6ffbea72d 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewVisitorMessageUseCaseTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/domain/AppendNewVisitorMessageUseCaseTest.kt @@ -65,7 +65,7 @@ class AppendNewVisitorMessageUseCaseTest { @Test fun `addUnsentItem returns true when unsentItems and chatItems contain received message`() { - val lastDelivered: VisitorChatItem = VisitorMessageItem.Delivered("id", 1, "message") + val lastDelivered: VisitorChatItem = VisitorMessageItem("message", "id", 1, true) useCase.lastDeliveredItem = lastDelivered state.chatItems.add(lastDelivered) @@ -81,8 +81,8 @@ class AppendNewVisitorMessageUseCaseTest { assertTrue(useCase.addUnsentItem(state, visitorMessage)) assertTrue(state.unsentItems.isEmpty()) - assertTrue(state.chatItems.last() is VisitorMessageItem.Delivered) - assertTrue(state.chatItems.first() is VisitorMessageItem.New) + assertTrue(state.chatItems.last() is VisitorMessageItem) + assertTrue(state.chatItems.first() is VisitorMessageItem) } @Test @@ -106,7 +106,7 @@ class AppendNewVisitorMessageUseCaseTest { useCase(state, chatMessageInternal) assertTrue(state.chatItems.count() == 1) - assertTrue(state.chatItems.first() is VisitorMessageItem.Delivered) + assertTrue(state.chatItems.first() is VisitorMessageItem) } @Test @@ -131,7 +131,7 @@ class AppendNewVisitorMessageUseCaseTest { useCase(state, chatMessageInternal) assertTrue(state.chatItems.count() == 2) - assertTrue(state.chatItems.first() is VisitorMessageItem.New) + assertTrue(state.chatItems.first() is VisitorMessageItem) assertTrue(state.chatItems.last() is VisitorAttachmentItem.File) assertTrue((state.chatItems.last() as VisitorChatItem).showDelivered) assertTrue(useCase.lastDeliveredItem is VisitorAttachmentItem.File) From ddb2f553acbd807ac1e8ed9a543da8c62daeb40b Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Fri, 1 Sep 2023 15:42:47 +0300 Subject: [PATCH 66/69] Quick Replies are hidden after restoring the engagement MOB 2606 --- widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt | 3 +-- .../src/test/java/com/glia/widgets/chat/ChatManagerTest.kt | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt index 252b7cda7..2c958e05a 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatManager.kt @@ -59,7 +59,7 @@ internal class ChatManager constructor( subscribe(onHistoryLoaded, onOperatorMessageReceived, onQuickReplyReceived) - return state.map(State::immutableChatItems).onBackpressureLatest().share() + return state.doOnNext(::updateQuickReplies).map(State::immutableChatItems).onBackpressureLatest().share() } @VisibleForTesting @@ -86,7 +86,6 @@ internal class ChatManager constructor( fun subscribeToState(onHistoryLoaded: (hasHistory: Boolean) -> Unit, onOperatorMessageReceived: (count: Int) -> Unit): Disposable = state.run { loadHistory(onHistoryLoaded) .concatWith(subscribeToMessages(onOperatorMessageReceived)) - .doOnNext(::updateQuickReplies) .doOnError { it.printStackTrace() } .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.computation()) diff --git a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt index d0ac5bc96..6a9d1f568 100644 --- a/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt +++ b/widgetssdk/src/test/java/com/glia/widgets/chat/ChatManagerTest.kt @@ -40,6 +40,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any +import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.isNull @@ -625,6 +626,8 @@ class ChatManagerTest { stateProcessor.onNext(ChatManager.State(chatItems = chatItems)) + verify(this, atLeastOnce()).updateQuickReplies(any()) + assertEquals(test.values().last().last(), chatItems.last()) } } From e75c073cacedd7d2472270a7cac9b401f08872a6 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Fri, 1 Sep 2023 16:18:30 +0300 Subject: [PATCH 67/69] Fix an app crash when there is no application to open uri MOB 2608 --- .../src/main/java/com/glia/widgets/chat/ChatView.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt index 2a045b441..064cde9a0 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt @@ -512,6 +512,7 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty if (intent.resolveActivity(context.packageManager) != null) { context.startActivity(intent) } else { + Logger.e(TAG, "No email client, uri - $uri") showToast(context.getString(R.string.glia_dialog_unexpected_error_title)) } } @@ -522,13 +523,19 @@ class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defSty if (intent.resolveActivity(context.packageManager) != null) { context.startActivity(intent) } else { + Logger.e(TAG, "No dialer uri - $uri") showToast(context.getString(R.string.glia_dialog_unexpected_error_title)) } } private fun requestOpenUri(uri: Uri) { Intent(Intent.ACTION_VIEW, uri).addCategory(Intent.CATEGORY_BROWSABLE).also { - context.startActivity(it) + if (it.resolveActivity(context.packageManager) != null) { + context.startActivity(it) + } else { + Logger.e(TAG, "No app to open url - $uri") + showToast(context.getString(R.string.glia_dialog_unexpected_error_title)) + } } } From 0ca7b6b824f4a01b998fe3380a608f8ebd359366 Mon Sep 17 00:00:00 2001 From: BitriseBot Date: Mon, 4 Sep 2023 15:11:38 +0000 Subject: [PATCH 68/69] Increment Core SDK version to 1.1.0 --- version.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.properties b/version.properties index 6198468be..27d3f4e36 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ -#Thu Aug 31 09:32:00 UTC 2023 -dependency.coreSdk.version=1.0.4 +#Mon Sep 04 15:11:36 UTC 2023 +dependency.coreSdk.version=1.1.0 widgets.versionCode=68 -widgets.versionName=2.0.5 \ No newline at end of file +widgets.versionName=2.0.5 From 73484ac009a5be88a620f2ea909f004192d81469 Mon Sep 17 00:00:00 2001 From: Davit Dolmazyan Date: Mon, 4 Sep 2023 23:18:04 +0300 Subject: [PATCH 69/69] Increment Widgets SDK version --- version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.properties b/version.properties index 27d3f4e36..b37995d23 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ #Mon Sep 04 15:11:36 UTC 2023 dependency.coreSdk.version=1.1.0 widgets.versionCode=68 -widgets.versionName=2.0.5 +widgets.versionName=2.1.0