diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index ca95b7d46..9d91bbf56 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -587,6 +587,8 @@ C0175A282A67D470001FACDE /* GvaPersistentButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A272A67D470001FACDE /* GvaPersistentButtonStyle.swift */; }; C0175A2A2A67D499001FACDE /* GvaStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A292A67D499001FACDE /* GvaStyle.swift */; }; C0175A2C2A67E2E9001FACDE /* Theme+Gva.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A2B2A67E2E9001FACDE /* Theme+Gva.swift */; }; + C02248A72AD53DDA00CC4930 /* LiveObservationConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02248A62AD53DDA00CC4930 /* LiveObservationConfirmation.swift */; }; + C02248AA2AD53E6100CC4930 /* LiveObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02248A92AD53E6100CC4930 /* LiveObservation.swift */; }; C03A8047292BA76D00DDECA6 /* ChatViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03A8046292BA76D00DDECA6 /* ChatViewControllerTests.swift */; }; C03A8049292BC8DB00DDECA6 /* CallViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03A8048292BC8DB00DDECA6 /* CallViewControllerTests.swift */; }; C05AB01C295F416700AA381F /* VisitorCodeCloseButtonProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05AB01B295F416700AA381F /* VisitorCodeCloseButtonProperties.swift */; }; @@ -599,10 +601,10 @@ C06A7586296ECC57006B69A2 /* VisitorCodeStyle.Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06A7585296ECC57006B69A2 /* VisitorCodeStyle.Accessibility.swift */; }; C06A7588296ECD75006B69A2 /* Theme+VisitorCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06A7587296ECD75006B69A2 /* Theme+VisitorCode.swift */; }; C07F62462ABC322B003EFC97 /* OrientationManager.Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07F62452ABC322B003EFC97 /* OrientationManager.Mock.swift */; }; + C07F62772AC1BA2B003EFC97 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07F62762AC1BA2B003EFC97 /* UIViewController+Extensions.swift */; }; C07F62792AC2D2E8003EFC97 /* BackgroundSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07F62782AC2D2E8003EFC97 /* BackgroundSwiftUI.swift */; }; C07F627D2AC2F31F003EFC97 /* ScreenSharingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07F627C2AC2F31F003EFC97 /* ScreenSharingViewModel.swift */; }; C07F62812AC3057C003EFC97 /* ScreenSharingViewModel.mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07F62802AC3057C003EFC97 /* ScreenSharingViewModel.mock.swift */; }; - C07F62772AC1BA2B003EFC97 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07F62762AC1BA2B003EFC97 /* UIViewController+Extensions.swift */; }; C07F62832AC33BB9003EFC97 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07F62822AC33BB9003EFC97 /* UIView+Extensions.swift */; }; C07FA04029AF542A00E9FB7F /* ScreenSharingViewStyle+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84265E0B298AECBA00D65842 /* ScreenSharingViewStyle+Mock.swift */; }; C07FA04B29AF83B900E9FB7F /* ActionButton.Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07FA04929AF83A400E9FB7F /* ActionButton.Mock.swift */; }; @@ -1329,6 +1331,8 @@ C0175A272A67D470001FACDE /* GvaPersistentButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GvaPersistentButtonStyle.swift; sourceTree = ""; }; C0175A292A67D499001FACDE /* GvaStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GvaStyle.swift; sourceTree = ""; }; C0175A2B2A67E2E9001FACDE /* Theme+Gva.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+Gva.swift"; sourceTree = ""; }; + C02248A62AD53DDA00CC4930 /* LiveObservationConfirmation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveObservationConfirmation.swift; sourceTree = ""; }; + C02248A92AD53E6100CC4930 /* LiveObservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveObservation.swift; sourceTree = ""; }; C03A8046292BA76D00DDECA6 /* ChatViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewControllerTests.swift; sourceTree = ""; }; C03A8048292BC8DB00DDECA6 /* CallViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewControllerTests.swift; sourceTree = ""; }; C05AB016295DA9FC00AA381F /* AlertViewController+VisitorCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertViewController+VisitorCode.swift"; sourceTree = ""; }; @@ -1342,13 +1346,11 @@ C06A7585296ECC57006B69A2 /* VisitorCodeStyle.Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitorCodeStyle.Accessibility.swift; sourceTree = ""; }; C06A7587296ECD75006B69A2 /* Theme+VisitorCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+VisitorCode.swift"; sourceTree = ""; }; C07F62452ABC322B003EFC97 /* OrientationManager.Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationManager.Mock.swift; sourceTree = ""; }; + C07F62762AC1BA2B003EFC97 /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = ""; }; C07F62782AC2D2E8003EFC97 /* BackgroundSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundSwiftUI.swift; sourceTree = ""; }; C07F627C2AC2F31F003EFC97 /* ScreenSharingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSharingViewModel.swift; sourceTree = ""; }; C07F62802AC3057C003EFC97 /* ScreenSharingViewModel.mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSharingViewModel.mock.swift; sourceTree = ""; }; - C07F62762AC1BA2B003EFC97 /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = ""; }; C07F62822AC33BB9003EFC97 /* UIView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; - C07FA04129AF550500E9FB7F /* ScreenSharingView.Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSharingView.Mock.swift; sourceTree = ""; }; - C07FA04429AF55F600E9FB7F /* ScreenSharingViewController.Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSharingViewController.Mock.swift; sourceTree = ""; }; C07FA04929AF83A400E9FB7F /* ActionButton.Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.Mock.swift; sourceTree = ""; }; C07FA04C29B0E41A00E9FB7F /* ScreenShareViewControllerVoiceOverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenShareViewControllerVoiceOverTests.swift; sourceTree = ""; }; C07FA04D29B0E41A00E9FB7F /* VideoCallViewControllerVoiceOverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCallViewControllerVoiceOverTests.swift; sourceTree = ""; }; @@ -2059,6 +2061,7 @@ 1A60AFC12566857200E53F53 /* Sources */ = { isa = PBXGroup; children = ( + C02248A82AD53DDF00CC4930 /* LiveObservation */, 84D5B95E2A14DEFD00807F92 /* QuickLookBased */, C05E3EDC29C99DEE0013BC81 /* ProximityManager */, C4794E6E270673FC00F989F7 /* Animation */, @@ -3513,6 +3516,15 @@ path = GVA; sourceTree = ""; }; + C02248A82AD53DDF00CC4930 /* LiveObservation */ = { + isa = PBXGroup; + children = ( + C02248A92AD53E6100CC4930 /* LiveObservation.swift */, + C02248A62AD53DDA00CC4930 /* LiveObservationConfirmation.swift */, + ); + path = LiveObservation; + sourceTree = ""; + }; C05E3EDC29C99DEE0013BC81 /* ProximityManager */ = { isa = PBXGroup; children = ( @@ -4471,6 +4483,7 @@ 84D2293028C61FF300F64FE7 /* WKNavigationPolicyProvider.Mock.swift in Sources */, 1A0C144425B868FC00B00695 /* CallCoordinator.swift in Sources */, 8491AF022A6FBBBA00CC3E72 /* GvaGalleryListView.swift in Sources */, + C02248A72AD53DDA00CC4930 /* LiveObservationConfirmation.swift in Sources */, 7594098C298D38C2008B173A /* CallVisualizer+Environment.swift in Sources */, 1A1E309B25F8E1F700850E68 /* DataStorage.swift in Sources */, AF6AB34D298A9F2500003645 /* SecureConversations.FileUploadListViewModel.swift in Sources */, @@ -4743,6 +4756,7 @@ AF3D520F2983BC5600AD8E69 /* FileUploader.Environment.Mock.swift in Sources */, C43D7A1125FF92680064B1DA /* ChoiceCardStyle.swift in Sources */, 9A186A4127F605B90055886D /* HeaderButtonStyle.Accessibility.swift in Sources */, + C02248AA2AD53E6100CC4930 /* LiveObservation.swift in Sources */, 9AB196D827C3E27300FD60AB /* ChatViewModel.Environment.Mock.swift in Sources */, 3100EEFF293F7E0900D57F71 /* Theme+SecureConversationsWelcome.swift in Sources */, 9A8130BF27D7AEF700220BBD /* FileUpload.Mock.swift in Sources */, diff --git a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.CommonEngagementModel.swift b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.CommonEngagementModel.swift index 4e1bc9045..eb0ddfb38 100644 --- a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.CommonEngagementModel.swift +++ b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.CommonEngagementModel.swift @@ -3,5 +3,6 @@ import Foundation protocol CommonEngagementModel: AnyObject { var engagementAction: EngagementViewModel.ActionCallback? { get set } var engagementDelegate: EngagementViewModel.DelegateCallback? { get set } + var hasViewAppeared: Bool { get set } func event(_ event: EngagementViewModel.Event) } diff --git a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift index cae493e8e..c7cb084cc 100644 --- a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift +++ b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel.swift @@ -29,7 +29,7 @@ extension SecureConversations { var action: ActionCallback? var delegate: DelegateCallback? - + var hasViewAppeared: Bool var engagementAction: EngagementViewModel.ActionCallback? var engagementDelegate: EngagementViewModel.DelegateCallback? @@ -117,7 +117,7 @@ extension SecureConversations { self.deliveredStatusText = deliveredStatusText self.interactor = interactor self.alertConfiguration = alertConfiguration - + self.hasViewAppeared = false let uploader = FileUploader( maximumUploads: Self.maximumUploads, environment: .init( diff --git a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.swift b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.swift index 5376ac2f8..f398f4832 100644 --- a/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.swift +++ b/GliaWidgets/Sources/Coordinators/EngagementCoordinator/EngagementCoordinator.swift @@ -50,6 +50,7 @@ class EngagementCoordinator: SubFlowCoordinator, FlowCoordinator { // swiftlint:disable function_body_length func start() { + switch engagementKind { case .none: break @@ -59,6 +60,7 @@ class EngagementCoordinator: SubFlowCoordinator, FlowCoordinator { showsCallBubble: false ) engagement = .chat(chatViewController) + interactor.state = .enqueueing(.text) navigationPresenter.setViewControllers( [chatViewController], animated: false @@ -81,15 +83,15 @@ class EngagementCoordinator: SubFlowCoordinator, FlowCoordinator { call.kind.addObserver(self) { [weak self] _, _ in self?.engagementKind = EngagementKind(with: call.kind.value) } - let chatViewController = startChat( - withAction: .none, - showsCallBubble: true - ) - let callViewController = startCall( call, withAction: .engagement(mediaType: mediaType) ) + interactor.state = .enqueueing(mediaType) + let chatViewController = startChat( + withAction: .none, + showsCallBubble: true + ) engagement = .call( callViewController, @@ -97,6 +99,7 @@ class EngagementCoordinator: SubFlowCoordinator, FlowCoordinator { .none, call ) + navigationPresenter.setViewControllers( [callViewController], animated: false diff --git a/GliaWidgets/Sources/Interactor/Interactor.swift b/GliaWidgets/Sources/Interactor/Interactor.swift index 95ab3e77e..672ff30db 100644 --- a/GliaWidgets/Sources/Interactor/Interactor.swift +++ b/GliaWidgets/Sources/Interactor/Interactor.swift @@ -2,7 +2,7 @@ import Foundation enum InteractorState { case none - case enqueueing + case enqueueing(CoreSdkClient.MediaType) case enqueued(CoreSdkClient.QueueTicket) case engaged(CoreSdkClient.Operator?) case ended(EndEngagementReason) @@ -140,8 +140,6 @@ extension Interactor { success: @escaping () -> Void, failure: @escaping (CoreSdkClient.SalemoveError) -> Void ) { - state = .enqueueing - let options = mediaType == .audio || mediaType == .video ? CoreSdkClient.EngagementOptions(mediaDirection: .twoWay) : nil diff --git a/GliaWidgets/Sources/LiveObservation/LiveObservation.swift b/GliaWidgets/Sources/LiveObservation/LiveObservation.swift new file mode 100644 index 000000000..4af8ddaec --- /dev/null +++ b/GliaWidgets/Sources/LiveObservation/LiveObservation.swift @@ -0,0 +1,3 @@ +import Foundation + +struct LiveObservation {} diff --git a/GliaWidgets/Sources/LiveObservation/LiveObservationConfirmation.swift b/GliaWidgets/Sources/LiveObservation/LiveObservationConfirmation.swift new file mode 100644 index 000000000..ea2d41296 --- /dev/null +++ b/GliaWidgets/Sources/LiveObservation/LiveObservationConfirmation.swift @@ -0,0 +1,9 @@ +import Foundation + +extension LiveObservation { + struct Confirmation { + let conf: ConfirmationAlertConfiguration + let accepted: () -> Void + let declined: () -> Void + } +} diff --git a/GliaWidgets/Sources/ViewController/Chat/ChatViewController.Mock.swift b/GliaWidgets/Sources/ViewController/Chat/ChatViewController.Mock.swift index 5b1c85064..a8a7022e5 100644 --- a/GliaWidgets/Sources/ViewController/Chat/ChatViewController.Mock.swift +++ b/GliaWidgets/Sources/ViewController/Chat/ChatViewController.Mock.swift @@ -299,7 +299,7 @@ extension ChatViewController { fileUploadListModel.environment.uploader.uploads.append(fileUploadWithError) fileUploadWithError.state.value = .error(FileUpload.Error.fileTooBig) - + chatViewModel.interactor.state = .enqueueing(.text) chatViewModel.action?(.sendButtonHidden(false)) chatViewModel.action?(.updateUnreadMessageIndicator(itemCount: 5)) chatViewModel.action?(.setChoiceCardInputModeEnabled(false)) diff --git a/GliaWidgets/Sources/ViewController/EngagementViewController.swift b/GliaWidgets/Sources/ViewController/EngagementViewController.swift index 09db258dc..19823631f 100644 --- a/GliaWidgets/Sources/ViewController/EngagementViewController.swift +++ b/GliaWidgets/Sources/ViewController/EngagementViewController.swift @@ -3,6 +3,7 @@ import UIKit class EngagementViewController: UIViewController, AlertPresenter, MediaUpgradePresenter, ScreenShareOfferPresenter { let viewFactory: ViewFactory private var viewModel: CommonEngagementModel + private var pendingLiveObservationConfirmation: LiveObservation.Confirmation? init(viewModel: CommonEngagementModel, viewFactory: ViewFactory) { self.viewModel = viewModel @@ -27,6 +28,11 @@ class EngagementViewController: UIViewController, AlertPresenter, MediaUpgradePr override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) viewModel.event(.viewDidAppear) + viewModel.hasViewAppeared = true + if let alertConfig = pendingLiveObservationConfirmation { + showLiveObservationConfirmationAlert(with: alertConfig) + pendingLiveObservationConfirmation = nil + } } override func viewDidDisappear(_ animated: Bool) { @@ -63,6 +69,8 @@ class EngagementViewController: UIViewController, AlertPresenter, MediaUpgradePr view?.header.showEndButton() case .showEndScreenShareButton: view?.header.showEndScreenSharingButton() + case let .showLiveObservationConfirmation(configuration): + self.showLiveObservationConfirmation(with: configuration) } } } @@ -71,4 +79,26 @@ class EngagementViewController: UIViewController, AlertPresenter, MediaUpgradePr self.viewModel = engagementModel bind(engagementViewModel: engagementModel) } + + private func showLiveObservationConfirmation( + with config: LiveObservation.Confirmation + ) { + switch viewModel.hasViewAppeared { + case true: showLiveObservationConfirmationAlert(with: config) + case false: pendingLiveObservationConfirmation = config + } + } + + private func showLiveObservationConfirmationAlert(with config: LiveObservation.Confirmation) { + let alert = AlertViewController( + kind: .liveObservationConfirmation( + config.conf, + accepted: config.accepted, + declined: config.declined + ), + viewFactory: self.viewFactory + ) + + replacePresentedOfferIfPossible(with: alert) + } } diff --git a/GliaWidgets/Sources/ViewModel/Call/CallViewModel.swift b/GliaWidgets/Sources/ViewModel/Call/CallViewModel.swift index ec2f08e4d..e08826785 100644 --- a/GliaWidgets/Sources/ViewModel/Call/CallViewModel.swift +++ b/GliaWidgets/Sources/ViewModel/Call/CallViewModel.swift @@ -75,9 +75,8 @@ class CallViewModel: EngagementViewModel, ViewModel { update(for: interactor.state) switch startWith { - case .engagement(let mediaType): - enqueue(mediaType: mediaType) - + case .engagement: + break case .call(offer: let offer, answer: let answer): call.upgrade(to: offer) showConnecting() diff --git a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift index 3f1495faf..b40667d96 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift @@ -168,18 +168,7 @@ class ChatViewModel: EngagementViewModel { override func start() { super.start() - - loadHistory { [weak self] history in - guard let self = self else { return } - // We only proceed to considering enqueue flow if `startAction` is about starting of engagement. - guard case .startEngagement = self.startAction else { return } - // We enqueue eagerly in case if this is the first engagement for visitor (by evaluating previous chat history) - // or in case if engagement has been restored. - - if history.isEmpty || self.environment.getCurrentEngagement() != nil { - self.enqueue(mediaType: .text) - } - } + loadHistory() } override func update(for state: InteractorState) { @@ -198,7 +187,6 @@ class ChatViewModel: EngagementViewModel { action?(.queue) action?(.scrollToBottom(animated: true)) - case .engaged(let engagedOperator): let name = engagedOperator?.firstName let pictureUrl = engagedOperator?.picture?.url @@ -247,7 +235,6 @@ class ChatViewModel: EngagementViewModel { } } } - default: break } @@ -378,7 +365,7 @@ extension ChatViewModel { // MARK: History extension ChatViewModel { - private func loadHistory(_ completion: @escaping ([ChatMessage]) -> Void) { + private func loadHistory() { environment.fetchChatHistory { [weak self] result in guard let self else { return } let messages = (try? result.get()) ?? [] @@ -401,7 +388,6 @@ extension ChatViewModel { self.historySection.set(items) self.action?(.refreshSection(self.historySection.index)) self.action?(.scrollToBottom(animated: false)) - completion(messages) } } } diff --git a/GliaWidgets/Sources/ViewModel/EngagementViewModel.swift b/GliaWidgets/Sources/ViewModel/EngagementViewModel.swift index e1e615749..6b3dd86d4 100644 --- a/GliaWidgets/Sources/ViewModel/EngagementViewModel.swift +++ b/GliaWidgets/Sources/ViewModel/EngagementViewModel.swift @@ -6,7 +6,7 @@ class EngagementViewModel: CommonEngagementModel { static let alertSingleActionAccessibilityIdentifier = "alert_close_engagementEnded" var engagementAction: ActionCallback? var engagementDelegate: DelegateCallback? - + var hasViewAppeared: Bool let interactor: Interactor let alertConfiguration: AlertConfiguration let environment: Environment @@ -25,6 +25,7 @@ class EngagementViewModel: CommonEngagementModel { self.alertConfiguration = alertConfiguration self.screenShareHandler = screenShareHandler self.environment = environment + self.hasViewAppeared = false self.interactor.addObserver(self) { [weak self] event in self?.interactorEvent(event) } @@ -129,7 +130,6 @@ class EngagementViewModel: CommonEngagementModel { activeEngagement = environment.getCurrentEngagement() case .ended(let reason) where reason == .byVisitor: - engagementDelegate?( .engaged( operatorImageUrl: nil @@ -161,7 +161,8 @@ class EngagementViewModel: CommonEngagementModel { ) ) }) - + case let .enqueueing(mediaType): + showLiveObservationConfirmation(in: mediaType) default: break } @@ -314,6 +315,29 @@ private extension EngagementViewModel { } } +// MARK: Live Observation + +extension EngagementViewModel { + private func showLiveObservationConfirmation(in mediaType: CoreSdkClient.MediaType) { + let liveObservationAlertConfig = createLiveObservationAlertConfig(with: mediaType) + engagementAction?(.showLiveObservationConfirmation(liveObservationAlertConfig)) + } + + private func createLiveObservationAlertConfig( + with mediaType: CoreSdkClient.MediaType + ) -> LiveObservation.Confirmation { + .init( + conf: self.alertConfiguration.liveObservationConfirmation, + accepted: { [weak self] in + self?.enqueue(mediaType: mediaType) + }, + declined: { [weak self] in + self?.endSession() + } + ) + } +} + extension EngagementViewModel: Hashable { static func == (lhs: EngagementViewModel, rhs: EngagementViewModel) -> Bool { return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) @@ -362,6 +386,7 @@ extension EngagementViewModel { ) case showEndButton case showEndScreenShareButton + case showLiveObservationConfirmation(LiveObservation.Confirmation) } enum DelegateEvent { diff --git a/GliaWidgetsTests/Sources/CallViewModelTests.swift b/GliaWidgetsTests/Sources/CallViewModelTests.swift index b3500afff..4e969551f 100644 --- a/GliaWidgetsTests/Sources/CallViewModelTests.swift +++ b/GliaWidgetsTests/Sources/CallViewModelTests.swift @@ -335,4 +335,85 @@ class CallViewModelTests: XCTestCase { XCTAssertEqual(call.kind.value, .audio) } + + func test_viewModelStartDoesNotInitiateEnqueuingWithStartActionAsEngagement() { + let viewModel: CallViewModel = .mock() + viewModel.start() + + XCTAssertEqual(viewModel.interactor.state, .none) + } + + func test_liveObservationAlertPresentationInitiatedWhenInteractorStateIsEnqueuing() { + enum Call { + case showLiveObservationAlert + } + var calls: [Call] = [] + let interactor: Interactor = .mock() + let viewModel: CallViewModel = .mock(interactor: interactor) + viewModel.engagementAction = { action in + switch action { + case .showLiveObservationConfirmation: + calls.append(.showLiveObservationAlert) + default: + XCTFail() + } + } + interactor.state = .enqueueing(.audio) + XCTAssertEqual(calls, [.showLiveObservationAlert]) + } + + func test_liveObservationAllowTriggersEnqueue() { + var interactorEnv: Interactor.Environment = .mock + interactorEnv.coreSdk.queueForEngagement = { _, completion in + completion(.success(.mock)) + } + + let interactor: Interactor = .mock(environment: interactorEnv) + interactor.isConfigurationPerformed = true + + var alertConfig: LiveObservation.Confirmation? + + let viewModel: CallViewModel = .mock(interactor: interactor) + viewModel.engagementAction = { action in + switch action { + case let .showLiveObservationConfirmation(config): + alertConfig = config + default: + XCTFail() + } + } + interactor.state = .enqueueing(.audio) + alertConfig?.accepted() + XCTAssertEqual(interactor.state, .enqueued(.mock)) + } + + func test_liveObservationDeclineTriggersNone() { + enum Call { + case queueForEngagement + } + var calls: [Call] = [] + var interactorEnv: Interactor.Environment = .mock + interactorEnv.coreSdk.queueForEngagement = { _, _ in + calls.append(.queueForEngagement) + } + + let interactor: Interactor = .mock(environment: interactorEnv) + interactor.isConfigurationPerformed = true + + var alertConfig: LiveObservation.Confirmation? + + let viewModel: CallViewModel = .mock(interactor: interactor) + viewModel.engagementAction = { action in + switch action { + case let .showLiveObservationConfirmation(config): + alertConfig = config + default: + XCTFail() + } + } + interactor.state = .enqueueing(.audio) + alertConfig?.declined() + XCTAssertEqual(interactor.state, .ended(.byVisitor)) + XCTAssertTrue(calls.isEmpty) + } } diff --git a/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests+Gva.swift b/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests+Gva.swift index 805b1dbdc..2dc86eaab 100644 --- a/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests+Gva.swift +++ b/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests+Gva.swift @@ -89,10 +89,9 @@ extension ChatViewModelTests { env.createFileUploadListModel = { _ in .mock() } env.createSendMessagePayload = { _, _ in .mock() } viewModel = .mock(interactor: interactorMock, environment: env) - viewModel.gvaOptionAction(for: option)() - - XCTAssertEqual(interactorMock.state, .enqueueing) + viewModel.interactor.state = .enqueueing(.text) + XCTAssertEqual(interactorMock.state, .enqueueing(.text)) } func test_broadcastEventAction() { diff --git a/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests.swift b/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests.swift index 64593b0a2..d7831cff9 100644 --- a/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests.swift +++ b/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests.swift @@ -72,7 +72,7 @@ class ChatViewModelTests: XCTestCase { func test_secureTranscriptChatTypeCases() throws { let viewModel: ChatViewModel = .mock(chatType: .secureTranscript) - viewModel.update(for: .enqueueing) + viewModel.update(for: .enqueueing(.text)) XCTAssertEqual(viewModel.queueOperatorSection.itemCount, 0) viewModel.handle(pendingMessage: .mock()) XCTAssertEqual(viewModel.queueOperatorSection.itemCount, 1) @@ -80,7 +80,7 @@ class ChatViewModelTests: XCTestCase { func test_authenticatedChatTypeCases() throws { let viewModel: ChatViewModel = .mock(chatType: .authenticated) - viewModel.update(for: .enqueueing) + viewModel.update(for: .enqueueing(.text)) XCTAssertEqual(viewModel.queueOperatorSection.itemCount, 1) viewModel.handle(pendingMessage: .mock()) XCTAssertEqual(viewModel.queueOperatorSection.itemCount, 1) @@ -88,7 +88,7 @@ class ChatViewModelTests: XCTestCase { func test_nonAuthenticatedChatTypeCases() throws { let viewModel: ChatViewModel = .mock(chatType: .nonAuthenticated) - viewModel.update(for: .enqueueing) + viewModel.update(for: .enqueueing(.text)) XCTAssertEqual(viewModel.queueOperatorSection.itemCount, 1) viewModel.handle(pendingMessage: .mock()) XCTAssertEqual(viewModel.queueOperatorSection.itemCount, 1) @@ -135,6 +135,7 @@ class ChatViewModelTests: XCTestCase { viewModelEnv.loadChatMessagesFromHistory = { true } let viewModel = ChatViewModel.mock(interactor: interactor, environment: viewModelEnv) viewModel.start() + viewModel.enqueue(mediaType: .audio) XCTAssertEqual(calls, [.configureWithInteractor, .configureWithConfiguration]) } @@ -150,7 +151,7 @@ class ChatViewModelTests: XCTestCase { let mockOperator: CoreSdkClient.Operator = .mock() XCTAssertEqual(0, viewModel.numberOfItems(in: queueSectionIndex)) - viewModel.update(for: .enqueueing) + viewModel.update(for: .enqueueing(.text)) XCTAssertEqual(1, viewModel.numberOfItems(in: queueSectionIndex)) viewModel.update(for: .engaged(mockOperator)) XCTAssertEqual(0, viewModel.numberOfItems(in: queueSectionIndex)) @@ -257,7 +258,7 @@ class ChatViewModelTests: XCTestCase { let viewModel = ChatViewModel.mock(interactor: interactor, environment: viewModelEnv) // When - viewModel.update(for: .enqueueing) + viewModel.update(for: .enqueueing(.text)) // Then XCTAssertEqual(calls, []) @@ -684,6 +685,7 @@ class ChatViewModelTests: XCTestCase { let viewModel: ChatViewModel = .mock(interactor: interactor, environment: viewModelEnv) viewModel.isViewLoaded = true viewModel.start() + viewModel.interactor.state = .enqueueing(.text) interactor.state = .engaged(.mock()) viewModel.invokeSetTextAndSendMessage(text: "Mock send message.") viewModel.interactorEvent(.receivedMessage(.mock(id: expectedMessageId))) @@ -726,6 +728,80 @@ class ChatViewModelTests: XCTestCase { XCTAssertEqual(viewModel.messagesSection.items.count, 0) XCTAssertEqual(viewModel.receivedMessageIds, []) } + + func test_liveObservationAlertPresentationInitiatedWhenInteractorStateIsEnqueuing() { + enum Call { + case showLiveObservationAlert + } + var calls: [Call] = [] + let interactor: Interactor = .mock() + let viewModel: ChatViewModel = .mock(interactor: interactor) + viewModel.engagementAction = { action in + switch action { + case .showLiveObservationConfirmation: + calls.append(.showLiveObservationAlert) + default: + XCTFail() + } + } + interactor.state = .enqueueing(.audio) + XCTAssertEqual(calls, [.showLiveObservationAlert]) + } + + func test_liveObservationAllowTriggersEnqueue() { + var interactorEnv: Interactor.Environment = .mock + interactorEnv.coreSdk.queueForEngagement = { _, completion in + completion(.success(.mock)) + } + + let interactor: Interactor = .mock(environment: interactorEnv) + interactor.isConfigurationPerformed = true + + var alertConfig: LiveObservation.Confirmation? + + let viewModel: ChatViewModel = .mock(interactor: interactor) + viewModel.engagementAction = { action in + switch action { + case let .showLiveObservationConfirmation(config): + alertConfig = config + default: + XCTFail() + } + } + interactor.state = .enqueueing(.audio) + alertConfig?.accepted() + XCTAssertEqual(interactor.state, .enqueued(.mock)) + } + + func test_liveObservationDeclineTriggersNone() { + enum Call { + case queueForEngagement + } + var calls: [Call] = [] + var interactorEnv: Interactor.Environment = .mock + interactorEnv.coreSdk.queueForEngagement = { _, _ in + calls.append(.queueForEngagement) + } + + let interactor: Interactor = .mock(environment: interactorEnv) + interactor.isConfigurationPerformed = true + + var alertConfig: LiveObservation.Confirmation? + + let viewModel: ChatViewModel = .mock(interactor: interactor) + viewModel.engagementAction = { action in + switch action { + case let .showLiveObservationConfirmation(config): + alertConfig = config + default: + XCTFail() + } + } + interactor.state = .enqueueing(.audio) + alertConfig?.declined() + XCTAssertEqual(interactor.state, .ended(.byVisitor)) + XCTAssertTrue(calls.isEmpty) + } } extension ChatChoiceCardOption { diff --git a/GliaWidgetsTests/Sources/InteractorTests.swift b/GliaWidgetsTests/Sources/InteractorTests.swift index 887f719f3..ffdf66f9e 100644 --- a/GliaWidgetsTests/Sources/InteractorTests.swift +++ b/GliaWidgetsTests/Sources/InteractorTests.swift @@ -173,7 +173,7 @@ class InteractorTests: XCTestCase { return } } - + interactor.state = .enqueueing(.text) interactor.enqueueForEngagement( mediaType: .text, success: {}, @@ -208,6 +208,7 @@ class InteractorTests: XCTestCase { } } + interactor.state = .enqueued(.mock) interactor.enqueueForEngagement( mediaType: .text, success: {}, @@ -277,7 +278,7 @@ class InteractorTests: XCTestCase { interactorEnv.gcd = .mock let interactor = Interactor.mock(environment: interactorEnv) - interactor.state = .enqueueing + interactor.state = .enqueueing(.text) interactor.addObserver(self) { event in switch event { case .stateChanged(let state): @@ -387,7 +388,7 @@ class InteractorTests: XCTestCase { interactorEnv.gcd = .mock let interactor = Interactor.mock(environment: interactorEnv) - interactor.state = .enqueueing + interactor.state = .enqueueing(.text) interactor.addObserver(self) { event in switch event { case .stateChanged(let state):