From 51de7e55c44a5537d490581cee63dafed768b17b Mon Sep 17 00:00:00 2001 From: Aleksandr Chatsky Date: Mon, 18 Nov 2024 16:41:37 +0200 Subject: [PATCH] Add 'Need Live Support?' banner to SC chat MOB-3639 --- .../java/com/glia/widgets/chat/ChatView.kt | 96 ++++++++++++++++++- .../widgets/chat/controller/ChatController.kt | 22 ++++- .../com/glia/widgets/chat/model/ChatState.kt | 13 ++- ...eConversationTopBannerVisibilityUseCase.kt | 2 + .../glia/widgets/di/ControllerFactory.java | 3 +- .../com/glia/widgets/di/UseCaseFactory.java | 6 ++ .../glia/widgets/entrywidget/EntryWidget.kt | 8 +- .../entrywidget/EntryWidgetFragment.kt | 12 +-- .../widgets/entrywidget/EntryWidgetView.kt | 80 +++++++++++----- .../entrywidget/adapter/EntryWidgetAdapter.kt | 23 ++++- ...ecoration.kt => EntryWidgetItemDivider.kt} | 2 +- .../animation/SimpleTransitionListener.kt | 39 ++++++++ .../drawable/ic_minimalistic_arrow_down.xml | 9 ++ widgetssdk/src/main/res/layout/chat_view.xml | 82 +++++++++++++++- .../src/main/res/values/new_strings.xml | 1 + widgetssdk/src/main/res/values/strings.xml | 1 + .../chat/controller/ChatControllerTest.kt | 14 ++- .../EntryWidgetEmbeddedViewTest.kt | 11 +-- .../widgets/snapshotutils/SnapshotChatView.kt | 3 + 19 files changed, 368 insertions(+), 59 deletions(-) rename widgetssdk/src/main/java/com/glia/widgets/entrywidget/adapter/{EntryWidgetItemDecoration.kt => EntryWidgetItemDivider.kt} (97%) create mode 100644 widgetssdk/src/main/java/com/glia/widgets/view/animation/SimpleTransitionListener.kt create mode 100644 widgetssdk/src/main/res/drawable/ic_minimalistic_arrow_down.xml 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 ab87e058c0..62adb9c59c 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/chat/ChatView.kt @@ -8,10 +8,12 @@ import android.text.TextWatcher import android.util.AttributeSet import android.view.View import android.view.accessibility.AccessibilityEvent +import android.widget.FrameLayout import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.VisibleForTesting import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet import androidx.core.app.ActivityOptionsCompat import androidx.core.content.withStyledAttributes import androidx.core.view.ViewCompat @@ -20,6 +22,12 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver +import androidx.transition.ChangeBounds +import androidx.transition.ChangeTransform +import androidx.transition.Fade +import androidx.transition.Transition +import androidx.transition.TransitionManager +import androidx.transition.TransitionSet import com.glia.androidsdk.Engagement.MediaType import com.glia.androidsdk.chat.AttachmentFile import com.glia.widgets.Constants @@ -36,13 +44,14 @@ 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.RemoteAttachmentItem import com.glia.widgets.core.dialog.DialogContract import com.glia.widgets.core.dialog.model.DialogState import com.glia.widgets.core.dialog.model.LeaveDialogAction import com.glia.widgets.core.fileupload.model.LocalAttachment import com.glia.widgets.databinding.ChatViewBinding import com.glia.widgets.di.Dependencies +import com.glia.widgets.entrywidget.EntryWidgetContract +import com.glia.widgets.entrywidget.adapter.EntryWidgetAdapter import com.glia.widgets.helper.Logger import com.glia.widgets.helper.SimpleTextWatcher import com.glia.widgets.helper.SimpleWindowInsetsAndAnimationHandler @@ -66,11 +75,13 @@ import com.glia.widgets.launcher.ActivityLauncher import com.glia.widgets.locale.LocaleString import com.glia.widgets.view.Dialogs import com.glia.widgets.view.SingleChoiceCardView.OnOptionClickedListener +import com.glia.widgets.view.animation.SimpleTransitionListener import com.glia.widgets.view.dialog.base.DialogDelegate import com.glia.widgets.view.dialog.base.DialogDelegateImpl import com.glia.widgets.view.head.ChatHeadContract import com.glia.widgets.view.unifiedui.applyButtonTheme import com.glia.widgets.view.unifiedui.applyColorTheme +import com.glia.widgets.view.unifiedui.applyImageColorTheme import com.glia.widgets.view.unifiedui.applyLayerTheme import com.glia.widgets.view.unifiedui.applyTextTheme import com.glia.widgets.view.unifiedui.theme.UnifiedTheme @@ -157,6 +168,8 @@ internal class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: In } private var binding by Delegates.notNull() + private val rootConstraintLayout + get() = binding.root as ConstraintLayout private val attachmentPopup by lazy { AttachmentPopup( binding.chatMessageLayout, Dependencies.gliaThemeManager.theme?.chatTheme?.attachmentsPopup @@ -647,6 +660,16 @@ internal class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: In binding.scErrorLabel.setLocaleText(R.string.secure_messaging_chat_unavailable_message) binding.scBottomBannerLabel.setLocaleText(R.string.secure_messaging_chat_banner_bottom) binding.sendButton.setLocaleContentDescription(R.string.general_send) + binding.scTopBannerTitle.setLocaleText(R.string.secure_messaging_chat_banner_top) + + binding.scTopBannerOptions.apply { + val entryWidgetTheme = Dependencies.gliaThemeManager.theme?.chatTheme?.secureMessaging + val entryWidgetsAdapter = EntryWidgetAdapter( + EntryWidgetContract.ViewType.MESSAGING_LIVE_SUPPORT, + entryWidgetTheme + ) + setAdapter(entryWidgetsAdapter) + } applyTheme(Dependencies.gliaThemeManager.theme) } @@ -675,6 +698,10 @@ internal class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: In viewHolder?.itemView?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) } } + + binding.scTopBannerTitle.setOnClickListener(::onNeedSupportButtonClicked) + binding.scTopBannerIcon.setOnClickListener(::onNeedSupportButtonClicked) + binding.blockingCurtain.setOnClickListener(::onNeedSupportButtonClicked) } private fun setupAddAttachmentButton() { @@ -830,10 +857,21 @@ internal class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: In binding.scBottomBannerDivider.applyColorTheme(secureMessagingTheme.bottomBannerDividerColor) binding.scErrorLabel.applyLayerTheme(secureMessagingTheme.unavailableStatusBackground) binding.scErrorLabel.applyTextTheme(secureMessagingTheme.unavailableStatusText) + binding.scTopBannerBackground.applyLayerTheme(secureMessagingTheme.topBannerBackground) + binding.scTopBannerTitle.applyTextTheme(secureMessagingTheme.topBannerText) + binding.scTopBannerIcon.applyImageColorTheme(secureMessagingTheme.topBannerDropDownIconColor) + binding.scTopBannerDivider.applyColorTheme(secureMessagingTheme.topBannerDividerColor) + binding.scTopBannerOptions.setSecureMessagingTheme(secureMessagingTheme) } private fun updateSecureMessagingState(state: ChatState) { - binding.scErrorLabel.isVisible = state.isSecureMessagingUnavailableLabelVisible + TransitionManager.beginDelayedTransition(rootConstraintLayout) + binding.scErrorLabel.isVisible = state.isSecureConversationsUnavailableLabelVisible + if (state.isSecureConversationsTopBannerVisible.not() && isNeedSupportDropDownShown()) { + onNeedSupportButtonClicked(null) + } + binding.scTopBannerGroup.isVisible = state.isSecureConversationsTopBannerVisible + binding.scTopBannerDivider.isVisible = state.isSecureConversationsTopBannerVisible } @VisibleForTesting @@ -870,4 +908,58 @@ internal class ChatView(context: Context, attrs: AttributeSet?, defStyleAttr: In fun interface OnTitleUpdatedListener { fun onTitleUpdated(title: LocaleString?) } + + private fun onNeedSupportButtonClicked(ignored: View?) { + val animationRules = createSecureConversationAnimationRules() + + TransitionManager.beginDelayedTransition(rootConstraintLayout, animationRules) + updateLayoutToNewNeedSupportBannerState(!isNeedSupportDropDownShown()) + } + + private fun isNeedSupportDropDownShown(): Boolean { + return binding.blockingCurtain.isVisible + } + + private fun createSecureConversationAnimationRules(): TransitionSet { + return TransitionSet() + .addTransition( + ChangeTransform() + .addTarget(binding.scTopBannerIcon) + ) + .addTransition( + Fade() + .addTarget(binding.blockingCurtain) + .addTarget(binding.scTopBannerDivider) + ) + .addTransition( + ChangeBounds() + .addTarget(binding.blockingCurtain) + .addTarget(binding.scTopBannerOptions) + ) + .setDuration(500) + .addListener(object : SimpleTransitionListener() { + override fun onTransitionEnd(transition: Transition) { + binding.blockingCurtain.apply { + if (isInvisible) { visibility = FrameLayout.GONE } + } + } + }) + } + + private fun updateLayoutToNewNeedSupportBannerState(shouldShowDropDown: Boolean) { + ConstraintSet().apply { + clone(rootConstraintLayout) + if (shouldShowDropDown) { + clear(R.id.sc_top_banner_options, ConstraintSet.BOTTOM) + setVisibility(R.id.blocking_curtain, VISIBLE) + setVisibility(R.id.sc_top_banner_divider, View.INVISIBLE) + setRotation(R.id.sc_top_banner_icon, 180f) + } else { + connect(R.id.sc_top_banner_options, ConstraintSet.BOTTOM, R.id.header_barrier, ConstraintSet.BOTTOM) + setVisibility(R.id.blocking_curtain, INVISIBLE) + setVisibility(R.id.sc_top_banner_divider, VISIBLE) + setRotation(R.id.sc_top_banner_icon, 0f) + } + }.applyTo(rootConstraintLayout) + } } 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 27c2f66365..f95973ed82 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 @@ -57,6 +57,7 @@ import com.glia.widgets.core.permissions.domain.WithCameraPermissionUseCase import com.glia.widgets.core.permissions.domain.WithReadWritePermissionsUseCase import com.glia.widgets.core.secureconversations.domain.IsMessagingAvailableUseCase import com.glia.widgets.core.secureconversations.domain.ManageSecureMessagingStatusUseCase +import com.glia.widgets.core.secureconversations.domain.SecureConversationTopBannerVisibilityUseCase import com.glia.widgets.di.Dependencies import com.glia.widgets.engagement.EngagementUpdateState import com.glia.widgets.engagement.ScreenSharingState @@ -135,7 +136,8 @@ internal class ChatController( private val requestNotificationPermissionIfPushNotificationsSetUpUseCase: RequestNotificationPermissionIfPushNotificationsSetUpUseCase, private val releaseResourcesUseCase: ReleaseResourcesUseCase, private val getUrlFromLinkUseCase: GetUrlFromLinkUseCase, - private val isMessagingAvailableUseCase: IsMessagingAvailableUseCase + private val isMessagingAvailableUseCase: IsMessagingAvailableUseCase, + private val shouldShowTopBannerUseCase: SecureConversationTopBannerVisibilityUseCase ) : ChatContract.Controller { private var backClickedListener: ChatView.OnBackClickedListener? = null private var view: ChatContract.View? = null @@ -253,6 +255,24 @@ internal class ChatController( initChatManager() emitViewState { chatState.initChat().setSecureMessagingState() } ensureSecureMessagingAvailable() + observeTopBannerUseCase() + } + + private fun observeTopBannerUseCase() { + disposable.add(shouldShowTopBannerUseCase().subscribe( + { result -> + result.getOrNull()?.let { shouldBeVisible -> + emitViewState { + chatState.setSecureConversationsTopBannerVisibility(shouldBeVisible) + } + } ?: run{ + Logger.w(TAG, "Failed to get secure messaging top banner visibility flag.\n ${result.exceptionOrNull()}") + } + }, + { error -> + Logger.w(TAG, "Secure messaging top banner visibility flag observable failed.\n $error") + } + )) } private fun initLiveChat() { 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 27571b4469..74513a34ea 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 @@ -13,7 +13,8 @@ internal data class ChatState( val operatorStatusItem: OperatorStatusItem? = null, val isSendButtonVisible: Boolean = false, val isSendButtonEnabled: Boolean = false, - val isSecureMessagingUnavailableLabelVisible: Boolean = false, + val isSecureConversationsUnavailableLabelVisible: Boolean = false, + val isSecureConversationsTopBannerVisible: Boolean = false, val isAttachmentButtonEnabled: Boolean = false, val isAttachmentButtonNeeded: Boolean = false, val isOperatorTyping: Boolean = false, @@ -59,7 +60,7 @@ internal data class ChatState( fun setLiveChatState(): ChatState = copy( isSecureMessaging = false, chatInputMode = ChatInputMode.ENABLED_NO_ENGAGEMENT, - isSecureMessagingUnavailableLabelVisible = false, + isSecureConversationsUnavailableLabelVisible = false, ) fun allowSendAttachmentStateChanged(isAttachmentAllowed: Boolean): ChatState = copy(isAttachmentAllowed = isAttachmentAllowed) @@ -108,7 +109,7 @@ internal data class ChatState( fun setShowSendButton(isShow: Boolean): ChatState = copy(isSendButtonVisible = isShow) fun setSecureMessagingUnavailable(): ChatState = copy( - isSecureMessagingUnavailableLabelVisible = true, + isSecureConversationsUnavailableLabelVisible = true, isAttachmentButtonNeeded = true, isAttachmentButtonEnabled = false, isSendButtonVisible = true, @@ -117,7 +118,7 @@ internal data class ChatState( ) fun setSecureMessagingAvailable(): ChatState = copy( - isSecureMessagingUnavailableLabelVisible = false, + isSecureConversationsUnavailableLabelVisible = false, isAttachmentButtonNeeded = true, isAttachmentButtonEnabled = true, isSendButtonVisible = true, @@ -125,6 +126,10 @@ internal data class ChatState( chatInputMode = ChatInputMode.ENABLED ) + fun setSecureConversationsTopBannerVisibility(isVisible: Boolean): ChatState = copy( + isSecureConversationsTopBannerVisible = isVisible + ) + fun setIsOperatorTyping(isOperatorTyping: Boolean): ChatState = copy(isOperatorTyping = isOperatorTyping) fun setIsAttachmentButtonEnabled(isAttachmentButtonEnabled: Boolean): ChatState = copy(isAttachmentButtonEnabled = isAttachmentButtonEnabled) diff --git a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SecureConversationTopBannerVisibilityUseCase.kt b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SecureConversationTopBannerVisibilityUseCase.kt index 584256e827..1afac98b12 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SecureConversationTopBannerVisibilityUseCase.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/core/secureconversations/domain/SecureConversationTopBannerVisibilityUseCase.kt @@ -6,6 +6,7 @@ import com.glia.androidsdk.queuing.QueueState import com.glia.widgets.core.queue.QueueRepository import com.glia.widgets.core.queue.QueuesState import io.reactivex.rxjava3.core.BackpressureStrategy +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable /** @@ -26,6 +27,7 @@ internal class SecureConversationTopBannerVisibilityUseCase( } } .map { Result.success(it) } + .observeOn(AndroidSchedulers.mainThread()) .onErrorReturn { Result.failure(it) } .distinctUntilChanged() 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 46b931b731..90006ac11e 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/ControllerFactory.java @@ -139,7 +139,8 @@ public ChatContract.Controller getChatController() { useCaseFactory.getRequestNotificationPermissionIfPushNotificationsSetUpUseCase(), useCaseFactory.getReleaseResourcesUseCase(getDialogController()), useCaseFactory.createGetUrlFromLinkUseCase(), - useCaseFactory.createIsMessagingAvailableUseCase() + useCaseFactory.createIsMessagingAvailableUseCase(), + useCaseFactory.createSecureConversationTopBannerVisibilityUseCase() ); } 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 579c7afd47..0bb525c41d 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java +++ b/widgetssdk/src/main/java/com/glia/widgets/di/UseCaseFactory.java @@ -113,6 +113,7 @@ import com.glia.widgets.core.secureconversations.domain.OnNextMessageUseCase; import com.glia.widgets.core.secureconversations.domain.RemoveSecureFileAttachmentUseCase; import com.glia.widgets.core.secureconversations.domain.ResetMessageCenterUseCase; +import com.glia.widgets.core.secureconversations.domain.SecureConversationTopBannerVisibilityUseCase; import com.glia.widgets.core.secureconversations.domain.SendMessageButtonStateUseCase; import com.glia.widgets.core.secureconversations.domain.SendSecureMessageUseCase; import com.glia.widgets.core.secureconversations.domain.ShowMessageLimitErrorUseCase; @@ -597,6 +598,11 @@ public IsMessagingAvailableUseCase createIsMessagingAvailableUseCase() { return new IsMessagingAvailableUseCase(repositoryFactory.getQueueRepository()); } + @NonNull + public SecureConversationTopBannerVisibilityUseCase createSecureConversationTopBannerVisibilityUseCase() { + return new SecureConversationTopBannerVisibilityUseCase(repositoryFactory.getQueueRepository(), getHasPendingSecureConversationsWithTimeoutUseCase()); + } + @NonNull public GetUnreadMessagesCountWithTimeoutUseCase createSubscribeToUnreadMessagesCountUseCase() { return new GetUnreadMessagesCountWithTimeoutUseCase( diff --git a/widgetssdk/src/main/java/com/glia/widgets/entrywidget/EntryWidget.kt b/widgetssdk/src/main/java/com/glia/widgets/entrywidget/EntryWidget.kt index d3fcc3781c..d18c5a7304 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/entrywidget/EntryWidget.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/entrywidget/EntryWidget.kt @@ -55,11 +55,15 @@ internal class EntryWidgetImpl( } override fun getView(context: Context): View { + val entryWidgetTheme = themeManager.theme?.entryWidgetTheme val adapter = EntryWidgetAdapter( EntryWidgetContract.ViewType.EMBEDDED_VIEW, - themeManager.theme?.entryWidgetTheme + entryWidgetTheme ) - return EntryWidgetView(context, adapter) + return EntryWidgetView(context).apply { + setAdapter(adapter) + setEntryWidgetTheme(entryWidgetTheme) + } } override fun hide() { diff --git a/widgetssdk/src/main/java/com/glia/widgets/entrywidget/EntryWidgetFragment.kt b/widgetssdk/src/main/java/com/glia/widgets/entrywidget/EntryWidgetFragment.kt index 52507dbe39..ce9026409f 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/entrywidget/EntryWidgetFragment.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/entrywidget/EntryWidgetFragment.kt @@ -60,13 +60,11 @@ internal class EntryWidgetFragment : BottomSheetDialogFragment() { entryWidgetsTheme ) - EntryWidgetView( - context = context, - viewAdapter = entryWidgetAdapter, - entryWidgetTheme = entryWidgetsTheme - ).also { - binding.container.addView(it) - it.onDismissListener = { + EntryWidgetView(context).apply { + setAdapter(entryWidgetAdapter) + setEntryWidgetTheme(entryWidgetsTheme) + binding.container.addView(this) + onDismissListener = { dismiss() } } diff --git a/widgetssdk/src/main/java/com/glia/widgets/entrywidget/EntryWidgetView.kt b/widgetssdk/src/main/java/com/glia/widgets/entrywidget/EntryWidgetView.kt index ca1cb3ed16..b876f43404 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/entrywidget/EntryWidgetView.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/entrywidget/EntryWidgetView.kt @@ -4,12 +4,14 @@ import android.app.Activity import android.content.Context import android.graphics.drawable.Drawable import android.graphics.drawable.InsetDrawable +import android.util.AttributeSet +import android.util.TypedValue import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.glia.widgets.R import com.glia.widgets.di.Dependencies import com.glia.widgets.entrywidget.adapter.EntryWidgetAdapter -import com.glia.widgets.entrywidget.adapter.EntryWidgetItemDecoration +import com.glia.widgets.entrywidget.adapter.EntryWidgetItemDivider import com.glia.widgets.helper.getDrawableCompat import com.glia.widgets.helper.requireActivity import com.glia.widgets.helper.wrapWithMaterialThemeOverlay @@ -18,34 +20,55 @@ import com.glia.widgets.view.unifiedui.theme.base.ColorTheme import com.glia.widgets.view.unifiedui.theme.base.LayerTheme import com.glia.widgets.view.unifiedui.theme.entrywidget.EntryWidgetTheme import com.glia.widgets.view.unifiedui.theme.entrywidget.MediaTypeItemsTheme +import com.glia.widgets.view.unifiedui.theme.securemessaging.SecureMessagingTheme /** * EntryWidgetView provides a way to display the entry points for the user to start a chat, audio call, video call, or secure messaging. */ -internal class EntryWidgetView( - context: Context, - private val viewAdapter: EntryWidgetAdapter, - backgroundTheme: LayerTheme? = null, - mediaTypeItemsTheme: MediaTypeItemsTheme? = null, -) : RecyclerView(context.wrapWithMaterialThemeOverlay(), null, 0), EntryWidgetContract.View { +internal class EntryWidgetView : RecyclerView, EntryWidgetContract.View { + + constructor(context: Context) : super(context.wrapWithMaterialThemeOverlay()) + + constructor( + context: Context, + attrs: AttributeSet + ) : super(context.wrapWithMaterialThemeOverlay(), attrs) constructor( context: Context, - viewAdapter: EntryWidgetAdapter, - entryWidgetTheme: EntryWidgetTheme? - ) : this( - context.wrapWithMaterialThemeOverlay(), - viewAdapter, - entryWidgetTheme?.background, - entryWidgetTheme?.mediaTypeItems, - ) + attrs: AttributeSet, + defStyle: Int + ) : super(context.wrapWithMaterialThemeOverlay(), attrs, defStyle) var onDismissListener: (() -> Unit)? = null - private lateinit var controller: EntryWidgetContract.Controller + private var controller: EntryWidgetContract.Controller + private var _viewAdapter: EntryWidgetAdapter? = null + private val viewAdapter: EntryWidgetAdapter + get() = _viewAdapter ?: throw IllegalStateException("Make sure adapter is set up before attempting to show any items") + private var dividerView: EntryWidgetItemDivider? = null init { - setAdapter(viewAdapter) + controller = Dependencies.controllerFactory.entryWidgetController + itemAnimator = null + setupDefaultViewAppearance() + } + + private fun setupDefaultViewAppearance() { + val value = TypedValue() + context.theme.resolveAttribute(R.attr.gliaBaseLightColor, value, true) + setBackgroundColor(value.data) + } + + override fun setController(controller: EntryWidgetContract.Controller) { + this.controller = controller + } + + fun setAdapter(viewAdapter: EntryWidgetAdapter) { + this._viewAdapter?.release() + this._viewAdapter = viewAdapter + + super.setAdapter(viewAdapter) viewAdapter.onItemClickListener = { controller.onItemClicked(it, context.requireActivity()) } @@ -56,14 +79,20 @@ internal class EntryWidgetView( // Scrolling is only applicable for bottom sheet view. enableScrolling(isBottomSheet) + applyTheme(null, null) + controller.setView(this, viewAdapter.viewType) + } + + fun setEntryWidgetTheme(entryWidgetTheme: EntryWidgetTheme?) { + val backgroundTheme = entryWidgetTheme?.background + val mediaTypeItemsTheme = entryWidgetTheme?.mediaTypeItems applyTheme(backgroundTheme, mediaTypeItemsTheme) - setController(Dependencies.controllerFactory.entryWidgetController) - itemAnimator = null } - override fun setController(controller: EntryWidgetContract.Controller) { - this.controller = controller - controller.setView(this, viewAdapter.viewType) + fun setSecureMessagingTheme(secureMessagingTheme: SecureMessagingTheme?) { + val backgroundTheme = secureMessagingTheme?.topBannerBackground + val mediaTypeItemsTheme = secureMessagingTheme?.mediaTypeItems + applyTheme(backgroundTheme, mediaTypeItemsTheme) } override fun showItems(items: List) { @@ -91,9 +120,10 @@ internal class EntryWidgetView( private fun applyTheme(backgroundTheme: LayerTheme? = null, mediaTypeItemsTheme: MediaTypeItemsTheme? = null) { backgroundTheme?.let { applyLayerTheme(it) } - createDividerDrawable(mediaTypeItemsTheme?.dividerColor)?.let { - addItemDecoration(EntryWidgetItemDecoration(it)) - } + dividerView?.let(::removeItemDecoration) + dividerView = createDividerDrawable(mediaTypeItemsTheme?.dividerColor)?.let(::EntryWidgetItemDivider) + dividerView?.let(::addItemDecoration) + } private fun createDividerDrawable(dividerColor: ColorTheme?): Drawable? { diff --git a/widgetssdk/src/main/java/com/glia/widgets/entrywidget/adapter/EntryWidgetAdapter.kt b/widgetssdk/src/main/java/com/glia/widgets/entrywidget/adapter/EntryWidgetAdapter.kt index 007a013d95..02e74327e2 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/entrywidget/adapter/EntryWidgetAdapter.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/entrywidget/adapter/EntryWidgetAdapter.kt @@ -17,6 +17,7 @@ import com.glia.widgets.view.unifiedui.theme.base.ButtonTheme import com.glia.widgets.view.unifiedui.theme.base.TextTheme import com.glia.widgets.view.unifiedui.theme.entrywidget.EntryWidgetTheme import com.glia.widgets.view.unifiedui.theme.entrywidget.MediaTypeItemsTheme +import com.glia.widgets.view.unifiedui.theme.securemessaging.SecureMessagingTheme import io.reactivex.rxjava3.disposables.CompositeDisposable internal class EntryWidgetAdapter( @@ -38,6 +39,20 @@ internal class EntryWidgetAdapter( entryWidgetTheme?.errorButton ) + constructor( + viewType: EntryWidgetContract.ViewType, + secureMessagingTheme: SecureMessagingTheme? + ) : this( + viewType, + secureMessagingTheme?.mediaTypeItems, + null, + null, + null + ) + + var onItemClickListener: ((EntryWidgetContract.ItemType) -> Unit)? = null + val disposable: CompositeDisposable = CompositeDisposable() + init { when (viewType) { EntryWidgetContract.ViewType.BOTTOM_SHEET, @@ -75,9 +90,6 @@ internal class EntryWidgetAdapter( PROVIDED_BY_ITEM } - var onItemClickListener: ((EntryWidgetContract.ItemType) -> Unit)? = null - val disposable: CompositeDisposable = CompositeDisposable() - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val isInsideSecureConversation = this.viewType == EntryWidgetContract.ViewType.MESSAGING_LIVE_SUPPORT return when (viewType) { @@ -132,4 +144,9 @@ internal class EntryWidgetAdapter( open fun bind(itemType: EntryWidgetContract.ItemType, onClickListener: View.OnClickListener) {} } + fun release() { + onItemClickListener = null + disposable.dispose() + } + } diff --git a/widgetssdk/src/main/java/com/glia/widgets/entrywidget/adapter/EntryWidgetItemDecoration.kt b/widgetssdk/src/main/java/com/glia/widgets/entrywidget/adapter/EntryWidgetItemDivider.kt similarity index 97% rename from widgetssdk/src/main/java/com/glia/widgets/entrywidget/adapter/EntryWidgetItemDecoration.kt rename to widgetssdk/src/main/java/com/glia/widgets/entrywidget/adapter/EntryWidgetItemDivider.kt index 91a222705a..ffe425ce82 100644 --- a/widgetssdk/src/main/java/com/glia/widgets/entrywidget/adapter/EntryWidgetItemDecoration.kt +++ b/widgetssdk/src/main/java/com/glia/widgets/entrywidget/adapter/EntryWidgetItemDivider.kt @@ -6,7 +6,7 @@ import android.graphics.drawable.Drawable import android.view.View import androidx.recyclerview.widget.RecyclerView -internal class EntryWidgetItemDecoration( +internal class EntryWidgetItemDivider( private val divider: Drawable ) : RecyclerView.ItemDecoration() { diff --git a/widgetssdk/src/main/java/com/glia/widgets/view/animation/SimpleTransitionListener.kt b/widgetssdk/src/main/java/com/glia/widgets/view/animation/SimpleTransitionListener.kt new file mode 100644 index 0000000000..ffb8639f3b --- /dev/null +++ b/widgetssdk/src/main/java/com/glia/widgets/view/animation/SimpleTransitionListener.kt @@ -0,0 +1,39 @@ +package com.glia.widgets.view.animation + +import androidx.transition.Transition + +/** + * Only purpose of this class is to provide option to override only needed + * methods instead of defining all of them even when they do not need any implementation. + * + * For example: + * + * val transition = TransitionSet() + * .addTransition(Fade().addTarget(binding.root)) + * .addListener(object : SimpleTransitionListener() { + * override fun onTransitionEnd(transition: Transition) { + * TODO("Not yet implemented") + * } + * }) + */ +internal open class SimpleTransitionListener : Transition.TransitionListener { + override fun onTransitionStart(transition: Transition) { + /* no-op */ + } + + override fun onTransitionEnd(transition: Transition) { + /* no-op */ + } + + override fun onTransitionCancel(transition: Transition) { + /* no-op */ + } + + override fun onTransitionPause(transition: Transition) { + /* no-op */ + } + + override fun onTransitionResume(transition: Transition) { + /* no-op */ + } +} diff --git a/widgetssdk/src/main/res/drawable/ic_minimalistic_arrow_down.xml b/widgetssdk/src/main/res/drawable/ic_minimalistic_arrow_down.xml new file mode 100644 index 0000000000..84bfcb23d2 --- /dev/null +++ b/widgetssdk/src/main/res/drawable/ic_minimalistic_arrow_down.xml @@ -0,0 +1,9 @@ + + + diff --git a/widgetssdk/src/main/res/layout/chat_view.xml b/widgetssdk/src/main/res/layout/chat_view.xml index 3171d2d3e9..35420e34ba 100644 --- a/widgetssdk/src/main/res/layout/chat_view.xml +++ b/widgetssdk/src/main/res/layout/chat_view.xml @@ -12,14 +12,65 @@ app:layout_constraintTop_toTopOf="parent" tools:visibility="visible" /> + + + + + + + + + + + + + + + + diff --git a/widgetssdk/src/main/res/values/new_strings.xml b/widgetssdk/src/main/res/values/new_strings.xml index ebd462d757..e7829ea810 100644 --- a/widgetssdk/src/main/res/values/new_strings.xml +++ b/widgetssdk/src/main/res/values/new_strings.xml @@ -169,6 +169,7 @@ File picker @string/message_center_bottom_banner @string/message_center_error_unavailable_message + @string/need_live_support_label @string/glia_visitor_code_refresh_button diff --git a/widgetssdk/src/main/res/values/strings.xml b/widgetssdk/src/main/res/values/strings.xml index 92c9a890b5..31fae0ba23 100644 --- a/widgetssdk/src/main/res/values/strings.xml +++ b/widgetssdk/src/main/res/values/strings.xml @@ -92,6 +92,7 @@ Your message has been sent. We will get back to you within 1 business day. Secure messaging has an expected response time of 1 business day. Sending messages is currently not available. + Need live support? Screen Sharing Your Screen is Being Shared The file has been downloaded. 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 a9a6cfd754..98e599c8a4 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 @@ -39,6 +39,7 @@ import com.glia.widgets.core.permissions.domain.WithCameraPermissionUseCase import com.glia.widgets.core.permissions.domain.WithReadWritePermissionsUseCase import com.glia.widgets.core.secureconversations.domain.IsMessagingAvailableUseCase import com.glia.widgets.core.secureconversations.domain.ManageSecureMessagingStatusUseCase +import com.glia.widgets.core.secureconversations.domain.SecureConversationTopBannerVisibilityUseCase import com.glia.widgets.engagement.domain.AcceptMediaUpgradeOfferUseCase import com.glia.widgets.engagement.domain.DeclineMediaUpgradeOfferUseCase import com.glia.widgets.engagement.domain.EndEngagementUseCase @@ -132,6 +133,7 @@ class ChatControllerTest { private lateinit var isMessagingAvailableUseCase: IsMessagingAvailableUseCase private lateinit var manageSecureMessagingStatusUseCase: ManageSecureMessagingStatusUseCase + private lateinit var shouldShowTopBannerVisibilityUseCase: SecureConversationTopBannerVisibilityUseCase @Before fun setUp() { @@ -195,6 +197,9 @@ class ChatControllerTest { getUrlFromLinkUseCase = mock() isMessagingAvailableUseCase = mock() manageSecureMessagingStatusUseCase = mock() + shouldShowTopBannerVisibilityUseCase = mock { + on { invoke() } doReturn Flowable.empty() + } chatController = ChatController( callTimer = callTimer, @@ -241,7 +246,8 @@ class ChatControllerTest { releaseResourcesUseCase = releaseResourcesUseCase, getUrlFromLinkUseCase = getUrlFromLinkUseCase, isMessagingAvailableUseCase = isMessagingAvailableUseCase, - manageSecureMessagingStatusUseCase = manageSecureMessagingStatusUseCase + manageSecureMessagingStatusUseCase = manageSecureMessagingStatusUseCase, + shouldShowTopBannerUseCase = shouldShowTopBannerVisibilityUseCase ) chatController.setView(chatView) Mockito.clearInvocations(chatView) @@ -257,7 +263,7 @@ class ChatControllerTest { assertTrue(isAttachmentButtonEnabled) // Live chat state assertFalse(isSecureMessaging) - assertFalse(isSecureMessagingUnavailableLabelVisible) + assertFalse(isSecureConversationsUnavailableLabelVisible) assertEquals(ChatInputMode.ENABLED_NO_ENGAGEMENT, chatInputMode) } } @@ -286,7 +292,7 @@ class ChatControllerTest { assertTrue(isAttachmentButtonEnabled) assertTrue(isSecureMessaging) assertTrue(isAttachmentButtonNeeded) - assertFalse(isSecureMessagingUnavailableLabelVisible) + assertFalse(isSecureConversationsUnavailableLabelVisible) assertEquals(ChatInputMode.ENABLED, chatInputMode) } } @@ -300,7 +306,7 @@ class ChatControllerTest { assertFalse(isAttachmentButtonEnabled) assertTrue(isSecureMessaging) assertTrue(isAttachmentButtonNeeded) - assertTrue(isSecureMessagingUnavailableLabelVisible) + assertTrue(isSecureConversationsUnavailableLabelVisible) assertEquals(ChatInputMode.DISABLED, chatInputMode) } } diff --git a/widgetssdk/src/testSnapshot/java/com/glia/widgets/entrywidget/EntryWidgetEmbeddedViewTest.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/entrywidget/EntryWidgetEmbeddedViewTest.kt index ff3d693066..81a3aacdc6 100644 --- a/widgetssdk/src/testSnapshot/java/com/glia/widgets/entrywidget/EntryWidgetEmbeddedViewTest.kt +++ b/widgetssdk/src/testSnapshot/java/com/glia/widgets/entrywidget/EntryWidgetEmbeddedViewTest.kt @@ -312,14 +312,9 @@ internal open class EntryWidgetEmbeddedViewTest : SnapshotTest( val entryWidgetTheme = unifiedTheme?.entryWidgetTheme - return EntryWidgetView( - context, - viewAdapter = EntryWidgetAdapter( - viewType, - entryWidgetTheme - ), - entryWidgetTheme = entryWidgetTheme - ).apply { + return EntryWidgetView(context).apply { + setAdapter(EntryWidgetAdapter(viewType, entryWidgetTheme)) + setEntryWidgetTheme(entryWidgetTheme) showItems(items) } } diff --git a/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotChatView.kt b/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotChatView.kt index 62608c5679..f37003c0fc 100644 --- a/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotChatView.kt +++ b/widgetssdk/src/testSnapshot/java/com/glia/widgets/snapshotutils/SnapshotChatView.kt @@ -13,6 +13,7 @@ import com.glia.widgets.core.fileupload.model.LocalAttachment import com.glia.widgets.databinding.ChatActivityBinding import com.glia.widgets.di.ControllerFactory import com.glia.widgets.di.Dependencies +import com.glia.widgets.entrywidget.EntryWidgetContract import com.glia.widgets.view.unifiedui.theme.UnifiedTheme import org.mockito.kotlin.KArgumentCaptor import org.mockito.kotlin.argumentCaptor @@ -41,7 +42,9 @@ internal interface SnapshotChatView : SnapshotContent, SnapshotTheme, SnapshotAc val controllerFactoryMock = mock() val chatControllerMock = mock() + val entryWidgetControllerMock = mock() whenever(controllerFactoryMock.chatController).thenReturn(chatControllerMock) + whenever(controllerFactoryMock.entryWidgetController).thenReturn(entryWidgetControllerMock) Dependencies.controllerFactory = controllerFactoryMock return Mock(activityMock, imageFileMock, schedulersMock, controllerFactoryMock, chatControllerMock)