From e01f8464a8c9e3ee1df0a76318f1abcc47dd8e9c Mon Sep 17 00:00:00 2001 From: Rasmus Tauts Date: Fri, 6 Dec 2024 10:39:13 +0200 Subject: [PATCH] Handle ongoing engagement in EntryWidget This PR handles ongoing engagement in EntryWidget for both core engagements and call visualizer. MOB-3806 --- GliaWidgets/Localization.swift | 20 +++ .../Public/Glia/Glia+EntryWidget.swift | 3 +- .../Resources/en.lproj/Localizable.strings | 4 + .../SecureConversations.TranscriptModel.swift | 2 + .../CallVisualizer/CallVisualizer.swift | 1 + .../EntryWidget/EntryWidget.Environment.swift | 4 +- .../EntryWidget.MediaTypeItem.swift | 11 ++ .../Sources/EntryWidget/EntryWidget.swift | 76 +++++++---- .../Sources/EntryWidget/EntryWidgetView.swift | 65 +++++++++- .../EntryWidget/EntryWidgetViewModel.swift | 9 ++ .../MediaTypeItemStyle.Accessibility.swift | 13 +- .../MediaTypeItemStyle.Mock.swift | 14 +- .../MediaTypeItem/MediaTypeItemStyle.swift | 33 ++++- .../Sources/Interactor/Interactor.swift | 14 +- .../Sources/Theme/Theme.EntryWidget.swift | 12 +- .../ViewModel/EngagementViewModel.swift | 3 +- .../EntryWidget/EntryWidgetTests.swift | 120 ++++++++++++++++++ 17 files changed, 364 insertions(+), 40 deletions(-) diff --git a/GliaWidgets/Localization.swift b/GliaWidgets/Localization.swift index 6feab79d6..db4713efb 100644 --- a/GliaWidgets/Localization.swift +++ b/GliaWidgets/Localization.swift @@ -453,6 +453,16 @@ internal enum Localization { } } } + internal enum CallVisualizer { + internal enum Button { + /// Call Visualizer + internal static var label: String { Localization.tr("Localizable", "entry_widget.call_visualizer.button.label", fallback: "Call Visualizer") } + internal enum Accessibility { + /// Starts call visualizer session + internal static var hint: String { Localization.tr("Localizable", "entry_widget.call_visualizer.button.accessibility.hint", fallback: "Starts call visualizer session") } + } + } + } internal enum EmptyState { /// We are here to assist you during our business hours. internal static var description: String { Localization.tr("Localizable", "entry_widget.empty_state.description", fallback: "We are here to assist you during our business hours.") } @@ -489,6 +499,16 @@ internal enum Localization { internal static var label: String { Localization.tr("Localizable", "entry_widget.loading.accessibility.label", fallback: "Loading indicator. Waiting for available options.") } } } + internal enum OngoingEngagement { + internal enum CallVisualizer { + /// Screen sharing in progress + internal static var message: String { Localization.tr("Localizable", "entry_widget.ongoing_engagement.call_visualizer.message", fallback: "Screen sharing in progress") } + } + internal enum Core { + /// Ongoing • Tap to return + internal static var message: String { Localization.tr("Localizable", "entry_widget.ongoing_engagement.core.message", fallback: "Ongoing • Tap to return") } + } + } internal enum SecureMessaging { internal enum Button { /// Start a conversation, we’ll get back to you diff --git a/GliaWidgets/Public/Glia/Glia+EntryWidget.swift b/GliaWidgets/Public/Glia/Glia+EntryWidget.swift index 99ab32470..a406a54d1 100644 --- a/GliaWidgets/Public/Glia/Glia+EntryWidget.swift +++ b/GliaWidgets/Public/Glia/Glia+EntryWidget.swift @@ -27,7 +27,8 @@ extension Glia { hasPendingInteraction: { [weak self] in guard let self else { return false } return pendingInteraction.hasPendingInteraction - } + }, + currentInteractor: { self.interactor } ) ) } diff --git a/GliaWidgets/Resources/en.lproj/Localizable.strings b/GliaWidgets/Resources/en.lproj/Localizable.strings index d19996213..61a97d4df 100644 --- a/GliaWidgets/Resources/en.lproj/Localizable.strings +++ b/GliaWidgets/Resources/en.lproj/Localizable.strings @@ -262,9 +262,13 @@ "entry_widget.secure_messaging.button.label" = "Secure Messaging"; "entry_widget.secure_messaging.button.description" = "Start a conversation, we’ll get back to you"; "entry_widget.secure_messaging.button.accessibility.hint" = "Starts messaging with us"; +"entry_widget.call_visualizer.button.label" = "Call Visualizer"; +"entry_widget.call_visualizer.button.accessibility.hint" = "Starts call visualizer session"; "entry_widget.empty_state.title" = "Support team is currently offline"; "entry_widget.empty_state.description" = "We are here to assist you during our business hours."; "entry_widget.error_state.title" = "Could not load the contacts"; "entry_widget.error_state.description" = "We could not load the contacts at this time. This may be due to a temporary syncing issue or network problem."; "entry_widget.error_state.try_again.button.label" = "Try again"; "entry_widget.loading.accessibility.label" = "Loading indicator. Waiting for available options."; +"entry_widget.ongoing_engagement.core.message" = "Ongoing • Tap to return"; +"entry_widget.ongoing_engagement.call_visualizer.message" = "Screen sharing in progress"; diff --git a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift index 7a0537973..cb6cfe28b 100644 --- a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift +++ b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift @@ -784,6 +784,8 @@ extension SecureConversations.TranscriptModel { kind = .chat case .secureMessaging: kind = .messaging(.welcome) + case .callVisualizer: + return } self?.environment.switchToEngagement(kind) })) diff --git a/GliaWidgets/Sources/CallVisualizer/CallVisualizer.swift b/GliaWidgets/Sources/CallVisualizer/CallVisualizer.swift index 2c566ee92..8b302c7bb 100644 --- a/GliaWidgets/Sources/CallVisualizer/CallVisualizer.swift +++ b/GliaWidgets/Sources/CallVisualizer/CallVisualizer.swift @@ -121,6 +121,7 @@ extension CallVisualizer { func startObservingInteractorEvents() { environment.interactorProviding()?.addObserver(self) { [weak self] event in if case .stateChanged(.ended(.byOperator)) = event { + self?.environment.interactorProviding()?.currentEngagement = nil self?.endSession() self?.environment.log.prefixed(Self.self).info("Call visualizer engagement ended") } diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidget.Environment.swift b/GliaWidgets/Sources/EntryWidget/EntryWidget.Environment.swift index cd0d4ec5b..e35f85a55 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidget.Environment.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidget.Environment.swift @@ -10,6 +10,7 @@ extension EntryWidget { var log: CoreSdkClient.Logger var isAuthenticated: () -> Bool var hasPendingInteraction: () -> Bool + var currentInteractor: () -> Interactor? } } @@ -25,7 +26,8 @@ extension EntryWidget.Environment { theme: .mock(), log: .mock, isAuthenticated: { true }, - hasPendingInteraction: { false } + hasPendingInteraction: { false }, + currentInteractor: { .mock() } ) } } diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidget.MediaTypeItem.swift b/GliaWidgets/Sources/EntryWidget/EntryWidget.MediaTypeItem.swift index acd29f58f..afde4866a 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidget.MediaTypeItem.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidget.MediaTypeItem.swift @@ -64,6 +64,15 @@ extension EntryWidget { hintline: Localization.EntryWidget.SecureMessaging.Button.Accessibility.hint, image: Asset.mcEnvelope.image ) + case .callVisualizer: + self.init( + type: type, + badgeCount: 0, + headline: "Call Visualizer", + subheadline: "", + hintline: "", + image: Asset.screensharing.image + ) } } } @@ -73,6 +82,7 @@ extension EntryWidget { case audio case chat case secureMessaging + case callVisualizer var description: String { switch self { @@ -80,6 +90,7 @@ extension EntryWidget { case .audio: return "audio" case .chat: return "chat" case .secureMessaging: return "secureMessaging" + case .callVisualizer: return "callVisualizer" } } } diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidget.swift b/GliaWidgets/Sources/EntryWidget/EntryWidget.swift index 5e0555c13..c3eb4f8c2 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidget.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidget.swift @@ -17,9 +17,10 @@ public final class EntryWidget: NSObject { private let environment: Environment @Published private var unreadSecureMessageCount: Int? private(set) var unreadSecureMessageSubscriptionId: String? - + private var engagementCancellable: AnyCancellable? @Published var viewState: ViewState = .loading @Published private(set) var availableEngagementTypes: [EntryWidget.EngagementType] = [] + private var ongoingEngagement: Engagement? // MARK: - Initialization @@ -43,6 +44,13 @@ public final class EntryWidget: NSObject { Publishers.CombineLatest(environment.queuesMonitor.$state, $unreadSecureMessageCount) .sink(receiveValue: handleQueuesMonitorUpdates(state:unreadSecureMessagesCount:)) .store(in: &cancellables) + + engagementCancellable = environment.currentInteractor()?.$currentEngagement + .sink { [weak self] engagement in + guard let self = self else { return } + self.ongoingEngagement = engagement + handleQueuesMonitorUpdates(state: environment.queuesMonitor.state, unreadSecureMessagesCount: unreadSecureMessageCount) + } } deinit { @@ -96,6 +104,8 @@ extension EntryWidget { case .loading, .error, .offline: // 4 gives the desired fixed size mediaTypesCount = 4 + case .ongoingEngagement: + mediaTypesCount = 1 } var appliedHeight: CGFloat = 0 appliedHeight += configuration.sizeConstraints.sheetHeaderHeight @@ -108,30 +118,47 @@ extension EntryWidget { // MARK: - Private methods private extension EntryWidget { - func handleQueuesMonitorUpdates(state: QueuesMonitor.State, unreadSecureMessagesCount: Int?) { - switch state { - case .idle: - viewState = .loading - case .updated(let queues): - let availableEngagementTypes = resolveAvailableEngagementTypes(from: queues) - if availableEngagementTypes.isEmpty { - viewState = .offline - } else { - let mediaTypes = availableEngagementTypes.map { type in - if type == .secureMessaging { - return EntryWidget.MediaTypeItem( - type: type, - badgeCount: unreadSecureMessagesCount ?? 0 - ) + func handleQueuesMonitorUpdates( + state: QueuesMonitor.State, + unreadSecureMessagesCount: Int? + ) { + if let ongoingEngagement { + if ongoingEngagement.source == .callVisualizer { + viewState = .ongoingEngagement(.callVisualizer) + } else if ongoingEngagement.source == .coreEngagement { + if ongoingEngagement.mediaStreams.video != nil { + viewState = .ongoingEngagement(.video) + } else if ongoingEngagement.mediaStreams.audio != nil { + viewState = .ongoingEngagement(.audio) + } else { + viewState = .ongoingEngagement(.chat) + } + } + } else { + switch state { + case .idle: + viewState = .loading + case .updated(let queues): + let availableEngagementTypes = resolveAvailableEngagementTypes(from: queues) + if availableEngagementTypes.isEmpty { + viewState = .offline + } else { + let mediaTypes = availableEngagementTypes.map { type in + if type == .secureMessaging { + return EntryWidget.MediaTypeItem( + type: type, + badgeCount: unreadSecureMessagesCount ?? 0 + ) + } + return EntryWidget.MediaTypeItem(type: type) } - return EntryWidget.MediaTypeItem(type: type) + viewState = .mediaTypes(mediaTypes) } - viewState = .mediaTypes(mediaTypes) + self.availableEngagementTypes = availableEngagementTypes + case .failed(let error): + viewState = .error + environment.log.prefixed(Self.self).error("Setting up queues. Failed to get site queues \(error)") } - self.availableEngagementTypes = availableEngagementTypes - case .failed(let error): - viewState = .error - environment.log.prefixed(Self.self).error("Setting up queues. Failed to get site queues \(error)") } } @@ -178,6 +205,8 @@ private extension EntryWidget { try self.environment.engagementLauncher.startVideoCall() case .secureMessaging: try self.environment.engagementLauncher.startSecureMessaging() + case .callVisualizer: + break } } catch { self.viewState = .error @@ -314,11 +343,12 @@ private extension EntryWidget { // MARK: - View State extension EntryWidget { - enum ViewState { + enum ViewState: Equatable { case loading case mediaTypes([MediaTypeItem]) case offline case error + case ongoingEngagement(EngagementType) } } diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidgetView.swift b/GliaWidgets/Sources/EntryWidget/EntryWidgetView.swift index 73ff16f44..7f1966f9f 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidgetView.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidgetView.swift @@ -15,6 +15,8 @@ struct EntryWidgetView: View { mediaTypesView(types) case .offline: offilineView() + case let .ongoingEngagement(engagementType): + ongoingEngagementTypeView(engagementType) } } } @@ -156,7 +158,7 @@ private extension EntryWidgetView { func headerView() -> some View { VStack { Capsule(style: .continuous) - .fill(model.style.dividerColor.swiftUIColor()) + .fill(model.style.dividerColor.swiftUIColor().opacity(0.4)) .width(model.configuration.sizeConstraints.sheetHeaderDraggerWidth) .height(model.configuration.sizeConstraints.sheetHeaderDraggerHeight) } @@ -222,6 +224,7 @@ private extension EntryWidgetView { func icon(_ image: UIImage) -> some View { image.asSwiftUIImage() .resizable() + .renderingMode(.template) .fit() .width(model.configuration.sizeConstraints.singleCellIconSize) .height(model.configuration.sizeConstraints.singleCellIconSize) @@ -287,3 +290,63 @@ private extension EntryWidgetView { } } } + +// Views related to ongoing engagement view state +private extension EntryWidgetView { + @ViewBuilder + func ongoingEngagementMediaTypes( + mediaType: EntryWidget.MediaTypeItem, + engagementType: EntryWidget.EngagementType + ) -> some View { + VStack(spacing: 0) { + HStack(spacing: 16) { + icon(mediaType.image) + VStack(alignment: .leading, spacing: 2) { + headlineText(model.style.mediaTypeItem.title(for: mediaType)) + if !sizeCategory.isAccessibilityCategory { + Text(model.ongoingEngagementLabel(for: engagementType)) + .setFont(model.style.mediaTypeItem.ongoingEngagementMessageFont) + .setColor(model.style.mediaTypeItem.ongoingEngagementMessageColor) + } + } + unreadMessageCountView(for: mediaType) + } + .maxWidth(alignment: .leading) + .height(model.configuration.sizeConstraints.singleCellHeight) + .applyColorTypeBackground(model.style.mediaTypeItem.backgroundColor) + .contentShape(.rect) + .accessibilityElement(children: .combine) + .accessibility(addTraits: .isButton) + .accessibilityHint(model.style.mediaTypeItem.accessibility.hint(for: mediaType)) + .accessibilityIdentifier("entryWidget_\(mediaType.type)_item") + .onTapGesture { + model.selectMediaType(mediaType) + } + .padding(.horizontal) + Divider() + .height(model.configuration.sizeConstraints.dividerHeight) + .setColor(model.style.dividerColor) + .padding(.horizontal, model.configuration.sizeConstraints.dividerHorizontalPadding) + } + } + + @ViewBuilder + func ongoingEngagementTypeView(_ engagementType: EntryWidget.EngagementType) -> some View { + VStack(spacing: 0) { + if model.showHeader { + headerView() + .padding(.horizontal) + } + ongoingEngagementMediaTypes( + mediaType: .init(type: engagementType), + engagementType: engagementType + ) + if model.showPoweredBy { + poweredByView() + .padding(.horizontal) + } + } + .maxSize() + .applyColorTypeBackground(model.style.backgroundColor) + } +} diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift b/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift index 74a17cb56..983a4034a 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift @@ -46,5 +46,14 @@ extension EntryWidgetView { func onTryAgainTapped() { retryMonitoring?() } + + func ongoingEngagementLabel(for engagementType: EntryWidget.EngagementType) -> String { + switch engagementType { + case .video, .audio, .chat, .secureMessaging: + return style.mediaTypeItem.ongoingCoreEngagementMessage + case .callVisualizer: + return style.mediaTypeItem.ongoingCallVisualizerMessage + } + } } } diff --git a/GliaWidgets/Sources/EntryWidget/MediaTypeItem/MediaTypeItemStyle.Accessibility.swift b/GliaWidgets/Sources/EntryWidget/MediaTypeItem/MediaTypeItemStyle.Accessibility.swift index ceb456183..c5c193d4e 100644 --- a/GliaWidgets/Sources/EntryWidget/MediaTypeItem/MediaTypeItemStyle.Accessibility.swift +++ b/GliaWidgets/Sources/EntryWidget/MediaTypeItem/MediaTypeItemStyle.Accessibility.swift @@ -15,22 +15,28 @@ extension EntryWidgetStyle.MediaTypeItemStyle { /// The accessibility hint for secure messaging media type. public var secureMessagingHint: String + /// The accessibility hint for call visualizer. + public var callVisualizerHint: String + /// - Parameters: /// - chatHint: The accessibility hint for chat media type. /// - audioHint: The accessibility hint for audio media type. /// - videoHint: The accessibility hint for video media type. /// - secureMessagingHint: The accessibility hint for secure messaging media type. + /// - callVisualizer: The accessibility hint for call visualizer. /// public init( chatHint: String, audioHint: String, videoHint: String, - secureMessagingHint: String + secureMessagingHint: String, + callVisualizerHint: String ) { self.chatHint = chatHint self.audioHint = audioHint self.videoHint = videoHint self.secureMessagingHint = secureMessagingHint + self.callVisualizerHint = callVisualizerHint } func hint(for item: EntryWidget.MediaTypeItem) -> String { @@ -43,6 +49,8 @@ extension EntryWidgetStyle.MediaTypeItemStyle { return videoHint case .secureMessaging: return secureMessagingHint + case .callVisualizer: + return callVisualizerHint } } } @@ -54,6 +62,7 @@ extension EntryWidgetStyle.MediaTypeItemStyle.Accessibility { chatHint: "", audioHint: "", videoHint: "", - secureMessagingHint: "" + secureMessagingHint: "", + callVisualizerHint: "" ) } diff --git a/GliaWidgets/Sources/EntryWidget/MediaTypeItem/MediaTypeItemStyle.Mock.swift b/GliaWidgets/Sources/EntryWidget/MediaTypeItem/MediaTypeItemStyle.Mock.swift index 8b0a4fa7b..dc5ab357d 100644 --- a/GliaWidgets/Sources/EntryWidget/MediaTypeItem/MediaTypeItemStyle.Mock.swift +++ b/GliaWidgets/Sources/EntryWidget/MediaTypeItem/MediaTypeItemStyle.Mock.swift @@ -7,6 +7,7 @@ extension EntryWidgetStyle.MediaTypeItemStyle { audioTitle: String = "Audio", videoTitle: String = "Video", secureMessagingTitle: String = "Secure messaging", + callVisualizerTitle: String = "Call visualizer", titleFont: UIFont = .systemFont(ofSize: 16), titleColor: UIColor = .black, titleTextStyle: UIFont.TextStyle = .body, @@ -23,13 +24,18 @@ extension EntryWidgetStyle.MediaTypeItemStyle { iconColor: ColorType = .fill(color: .blue), backgroundColor: ColorType = .fill(color: .white), loading: LoadingStyle = .init(loadingTintColor: .fill(color: .gray)), - accessibility: Accessibility = .unsupported + accessibility: Accessibility = .unsupported, + ongoingCoreEngagementMessage: String = "Ongoing • Tap to return", + ongoingCallVisualizerMessage: String = "Screen sharing in progress", + ongoingEngagementMessageFont: UIFont = UIFont.systemFont(ofSize: 14, weight: .medium), + ongoingEngagementMessageColor: UIColor = Color.primary ) -> Self { Self( chatTitle: chatTitle, audioTitle: audioTitle, videoTitle: videoTitle, secureMessagingTitle: secureMessagingTitle, + callVisualizerTitle: callVisualizerTitle, titleFont: titleFont, titleColor: titleColor, titleTextStyle: titleTextStyle, @@ -46,7 +52,11 @@ extension EntryWidgetStyle.MediaTypeItemStyle { iconColor: iconColor, backgroundColor: backgroundColor, loading: loading, - accessibility: accessibility + accessibility: accessibility, + ongoingCoreEngagementMessage: ongoingCoreEngagementMessage, + ongoingCallVisualizerMessage: ongoingCallVisualizerMessage, + ongoingEngagementMessageFont: ongoingEngagementMessageFont, + ongoingEngagementMessageColor: ongoingEngagementMessageColor ) } } diff --git a/GliaWidgets/Sources/EntryWidget/MediaTypeItem/MediaTypeItemStyle.swift b/GliaWidgets/Sources/EntryWidget/MediaTypeItem/MediaTypeItemStyle.swift index 881ebb5f7..3635cebc4 100644 --- a/GliaWidgets/Sources/EntryWidget/MediaTypeItem/MediaTypeItemStyle.swift +++ b/GliaWidgets/Sources/EntryWidget/MediaTypeItem/MediaTypeItemStyle.swift @@ -14,6 +14,9 @@ extension EntryWidgetStyle { /// The title for secure messaging media type. public var secureMessagingTitle: String + /// The title for call visualizer media type. + public var callVisualizerTitle: String + /// Font of the headline text. public var titleFont: UIFont @@ -65,6 +68,18 @@ extension EntryWidgetStyle { /// Accessibility properties for EntryWidget MediaType Item. public var accessibility: Accessibility + /// The ongoing core engagement message string + public var ongoingCoreEngagementMessage: String + + /// The ongoing call visualizer message string + public var ongoingCallVisualizerMessage: String + + /// The ongoing engagement message font + public var ongoingEngagementMessageFont: UIFont + + /// The ongoing engagement message color + public var ongoingEngagementMessageColor: UIColor + /// - Parameters: /// - chatTitle: Title for chat media type. /// - audioTitle: Title for audio media type. @@ -92,6 +107,7 @@ extension EntryWidgetStyle { audioTitle: String, videoTitle: String, secureMessagingTitle: String, + callVisualizerTitle: String, titleFont: UIFont, titleColor: UIColor, titleTextStyle: UIFont.TextStyle, @@ -108,12 +124,17 @@ extension EntryWidgetStyle { iconColor: ColorType, backgroundColor: ColorType, loading: LoadingStyle, - accessibility: Accessibility + accessibility: Accessibility, + ongoingCoreEngagementMessage: String, + ongoingCallVisualizerMessage: String, + ongoingEngagementMessageFont: UIFont, + ongoingEngagementMessageColor: UIColor ) { self.chatTitle = chatTitle self.audioTitle = audioTitle self.videoTitle = videoTitle self.secureMessagingTitle = secureMessagingTitle + self.callVisualizerTitle = callVisualizerTitle self.titleFont = titleFont self.titleColor = titleColor self.titleTextStyle = titleTextStyle @@ -131,6 +152,10 @@ extension EntryWidgetStyle { self.backgroundColor = backgroundColor self.loading = loading self.accessibility = accessibility + self.ongoingCoreEngagementMessage = ongoingCoreEngagementMessage + self.ongoingCallVisualizerMessage = ongoingCallVisualizerMessage + self.ongoingEngagementMessageFont = ongoingEngagementMessageFont + self.ongoingEngagementMessageColor = ongoingEngagementMessageColor } func title(for item: EntryWidget.MediaTypeItem) -> String { @@ -143,6 +168,8 @@ extension EntryWidgetStyle { return videoTitle case .secureMessaging: return secureMessagingTitle + case .callVisualizer: + return callVisualizerTitle } } @@ -156,6 +183,10 @@ extension EntryWidgetStyle { return videoMessage case .secureMessaging: return secureMessagingMessage + case .callVisualizer: + // This will not be used, because EntryWidget will show + // ongoing engagement message there + return "" } } } diff --git a/GliaWidgets/Sources/Interactor/Interactor.swift b/GliaWidgets/Sources/Interactor/Interactor.swift index 5cbf1c7fa..f48400e49 100644 --- a/GliaWidgets/Sources/Interactor/Interactor.swift +++ b/GliaWidgets/Sources/Interactor/Interactor.swift @@ -58,7 +58,7 @@ class Interactor { } let visitorContext: Configuration.VisitorContext? - var currentEngagement: CoreSdkClient.Engagement? + @Published var currentEngagement: CoreSdkClient.Engagement? private var observers = [() -> (AnyObject?, EventHandler)]() @@ -323,20 +323,24 @@ extension Interactor: CoreSdkClient.Interactable { var onAudioStreamAdded: CoreSdkClient.AudioStreamAddedBlock { return { [weak self] stream, error in + guard let self else { return } if let stream = stream { - self?.notify(.audioStreamAdded(stream)) + notify(.audioStreamAdded(stream)) + currentEngagement = environment.coreSdk.getCurrentEngagement() } else if let error = error { - self?.notify(.audioStreamError(error)) + notify(.audioStreamError(error)) } } } var onVideoStreamAdded: CoreSdkClient.VideoStreamAddedBlock { return { [weak self] stream, error in + guard let self else { return } if let stream = stream { - self?.notify(.videoStreamAdded(stream)) + notify(.videoStreamAdded(stream)) + currentEngagement = environment.coreSdk.getCurrentEngagement() } else if let error = error { - self?.notify(.videoStreamError(error)) + notify(.videoStreamError(error)) } } } diff --git a/GliaWidgets/Sources/Theme/Theme.EntryWidget.swift b/GliaWidgets/Sources/Theme/Theme.EntryWidget.swift index e8061ac1a..21e3f5d08 100644 --- a/GliaWidgets/Sources/Theme/Theme.EntryWidget.swift +++ b/GliaWidgets/Sources/Theme/Theme.EntryWidget.swift @@ -11,7 +11,8 @@ extension Theme { chatHint: Localization.EntryWidget.LiveChat.Button.Accessibility.hint, audioHint: Localization.EntryWidget.Audio.Button.Accessibility.hint, videoHint: Localization.EntryWidget.Video.Button.Accessibility.hint, - secureMessagingHint: Localization.EntryWidget.SecureMessaging.Button.Accessibility.hint + secureMessagingHint: Localization.EntryWidget.SecureMessaging.Button.Accessibility.hint, + callVisualizerHint: Localization.EntryWidget.CallVisualizer.Button.Accessibility.hint ) let mediaTypeItem: EntryWidgetStyle.MediaTypeItemStyle = .init( @@ -19,6 +20,7 @@ extension Theme { audioTitle: Localization.EntryWidget.Audio.Button.label, videoTitle: Localization.EntryWidget.Video.Button.label, secureMessagingTitle: Localization.EntryWidget.SecureMessaging.Button.label, + callVisualizerTitle: Localization.EntryWidget.CallVisualizer.Button.label, titleFont: font.bodyText, titleColor: color.baseDark, titleTextStyle: .body, @@ -35,7 +37,11 @@ extension Theme { iconColor: .fill(color: color.primary), backgroundColor: .fill(color: color.baseLight), loading: loading, - accessibility: mediaTypeAccessibility + accessibility: mediaTypeAccessibility, + ongoingCoreEngagementMessage: Localization.EntryWidget.OngoingEngagement.Core.message, + ongoingCallVisualizerMessage: Localization.EntryWidget.OngoingEngagement.CallVisualizer.message, + ongoingEngagementMessageFont: font.mediumSubtitle2, + ongoingEngagementMessageColor: color.primary ) let backgroundColor: ColorType = .fill(color: color.baseLight) @@ -59,7 +65,7 @@ extension Theme { backgroundColor: backgroundColor, cornerRadius: 24, poweredBy: poweredBy, - dividerColor: color.baseNeutral, + dividerColor: color.baseNormal, errorTitle: Localization.EntryWidget.ErrorState.title, errorTitleFont: font.header3, errorTitleStyle: .body, diff --git a/GliaWidgets/Sources/ViewModel/EngagementViewModel.swift b/GliaWidgets/Sources/ViewModel/EngagementViewModel.swift index 79fa8d872..0b48219b5 100644 --- a/GliaWidgets/Sources/ViewModel/EngagementViewModel.swift +++ b/GliaWidgets/Sources/ViewModel/EngagementViewModel.swift @@ -122,7 +122,7 @@ class EngagementViewModel: CommonEngagementModel { ) ) engagementDelegate?(.finished) - + interactor.currentEngagement = nil case .ended(let reason) where reason == .byOperator: interactor.currentEngagement?.getSurvey(completion: { [weak self] result in guard let self = self else { return } @@ -132,6 +132,7 @@ class EngagementViewModel: CommonEngagementModel { } self.engagementAction?(.showAlert(.operatorEndedEngagement(action: { [weak self] in self?.endSession() + self?.interactor.currentEngagement = nil self?.engagementDelegate?( .engaged( operatorImageUrl: nil diff --git a/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetTests.swift b/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetTests.swift index c7fa63e33..78c3a013d 100644 --- a/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetTests.swift +++ b/GliaWidgetsTests/Sources/EntryWidget/EntryWidgetTests.swift @@ -231,4 +231,124 @@ class EntryWidgetTests: XCTestCase { XCTAssertEqual(entryWidget.availableEngagementTypes, [.video, .audio, .chat]) } + + func test_ongoingEngagementViewStateIsShownWhenCoreEngagementIsChat() { + let mockQueueId = "mockQueueId" + let mockQueue = Queue.mock(id: mockQueueId, media: [.messaging, .audio]) + + var queueMonitorEnvironment: QueuesMonitor.Environment = .mock + queueMonitorEnvironment.listQueues = { completion in + completion([mockQueue], nil) + } + queueMonitorEnvironment.subscribeForQueuesUpdates = { _, completion in + completion(.success(mockQueue)) + return UUID.mock.uuidString + } + var environment = EntryWidget.Environment.mock() + environment.isAuthenticated = { false } + environment.queuesMonitor = QueuesMonitor(environment: queueMonitorEnvironment) + let interactor: Interactor = .mock() + interactor.currentEngagement = .mock() + environment.currentInteractor = { interactor } + + let entryWidget = EntryWidget( + queueIds: [mockQueueId], + configuration: .default, + environment: environment + ) + + entryWidget.show(in: .init()) + + XCTAssertEqual(entryWidget.viewState, .ongoingEngagement(.chat)) + } + + func test_ongoingEngagementViewStateIsShownWhenCoreEngagementIsAudio() { + let mockQueueId = "mockQueueId" + let mockQueue = Queue.mock(id: mockQueueId, media: [.messaging, .audio]) + + var queueMonitorEnvironment: QueuesMonitor.Environment = .mock + queueMonitorEnvironment.listQueues = { completion in + completion([mockQueue], nil) + } + queueMonitorEnvironment.subscribeForQueuesUpdates = { _, completion in + completion(.success(mockQueue)) + return UUID.mock.uuidString + } + var environment = EntryWidget.Environment.mock() + environment.isAuthenticated = { false } + environment.queuesMonitor = QueuesMonitor(environment: queueMonitorEnvironment) + let interactor: Interactor = .mock() + interactor.currentEngagement = .mock(media: .init(audio: .twoWay, video: nil)) + environment.currentInteractor = { interactor } + + let entryWidget = EntryWidget( + queueIds: [mockQueueId], + configuration: .default, + environment: environment + ) + + entryWidget.show(in: .init()) + + XCTAssertEqual(entryWidget.viewState, .ongoingEngagement(.audio)) + } + + func test_ongoingEngagementViewStateIsShownWhenCoreEngagementIsVideo() { + let mockQueueId = "mockQueueId" + let mockQueue = Queue.mock(id: mockQueueId, media: [.messaging, .audio]) + + var queueMonitorEnvironment: QueuesMonitor.Environment = .mock + queueMonitorEnvironment.listQueues = { completion in + completion([mockQueue], nil) + } + queueMonitorEnvironment.subscribeForQueuesUpdates = { _, completion in + completion(.success(mockQueue)) + return UUID.mock.uuidString + } + var environment = EntryWidget.Environment.mock() + environment.isAuthenticated = { false } + environment.queuesMonitor = QueuesMonitor(environment: queueMonitorEnvironment) + let interactor: Interactor = .mock() + interactor.currentEngagement = .mock(media: .init(audio: .twoWay, video: .twoWay)) + environment.currentInteractor = { interactor } + + let entryWidget = EntryWidget( + queueIds: [mockQueueId], + configuration: .default, + environment: environment + ) + + entryWidget.show(in: .init()) + + XCTAssertEqual(entryWidget.viewState, .ongoingEngagement(.video)) + } + + func test_ongoingEngagementViewStateIsShownWhenCallVisualizer() { + let mockQueueId = "mockQueueId" + let mockQueue = Queue.mock(id: mockQueueId, media: [.messaging, .audio]) + + var queueMonitorEnvironment: QueuesMonitor.Environment = .mock + queueMonitorEnvironment.listQueues = { completion in + completion([mockQueue], nil) + } + queueMonitorEnvironment.subscribeForQueuesUpdates = { _, completion in + completion(.success(mockQueue)) + return UUID.mock.uuidString + } + var environment = EntryWidget.Environment.mock() + environment.isAuthenticated = { false } + environment.queuesMonitor = QueuesMonitor(environment: queueMonitorEnvironment) + let interactor: Interactor = .mock() + interactor.currentEngagement = .mock(source: .callVisualizer) + environment.currentInteractor = { interactor } + + let entryWidget = EntryWidget( + queueIds: [mockQueueId], + configuration: .default, + environment: environment + ) + + entryWidget.show(in: .init()) + + XCTAssertEqual(entryWidget.viewState, .ongoingEngagement(.callVisualizer)) + } }